Clean push: Removed heavy files & added only latest snapshot

This commit is contained in:
2025-07-26 05:16:12 +05:30
commit acf84e8767
250 changed files with 58564 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
"""
Utils package initialization
"""
# This file marks the directory as a Python package

View File

@@ -0,0 +1,304 @@
import cv2
import numpy as np
from PySide6.QtGui import QImage, QPixmap
from PySide6.QtCore import Qt
from typing import Dict, List, Any
# Color mapping for traffic-related classes
COLORS = {
'person': (255, 165, 0), # Orange
'bicycle': (255, 0, 255), # Magenta
'car': (0, 255, 0), # Green
'motorcycle': (255, 255, 0), # Cyan
'bus': (0, 0, 255), # Red
'truck': (0, 128, 255), # Orange-Blue
'traffic light': (0, 165, 255), # Orange
'stop sign': (0, 0, 139), # Dark Red
'parking meter': (128, 0, 128), # Purple
'default': (0, 255, 255) # Yellow as default
}
VIOLATION_COLORS = {
'red_light_violation': (0, 0, 255), # Red
'stop_sign_violation': (0, 100, 255), # Orange-Red
'speed_violation': (0, 255, 255), # Yellow
'lane_violation': (255, 0, 255), # Magenta
'default': (255, 0, 0) # Red as default
}
def draw_detections(frame: np.ndarray, detections: List[Dict],
draw_labels: bool = True, draw_confidence: bool = True) -> np.ndarray:
"""
Draw detection bounding boxes on the frame.
Args:
frame: Input video frame
detections: List of detection dictionaries
draw_labels: Whether to draw class labels
draw_confidence: Whether to draw confidence scores
Returns:
Annotated frame
"""
if frame is None or not isinstance(frame, np.ndarray):
return np.zeros((300, 300, 3), dtype=np.uint8)
annotated_frame = frame.copy()
for det in detections:
if 'bbox' not in det:
continue
try:
bbox = det['bbox']
if not isinstance(bbox, (list, tuple)) or len(bbox) < 4:
continue
x1, y1, x2, y2 = map(int, bbox)
if x1 >= x2 or y1 >= y2:
continue
class_name = det.get('class_name', 'unknown')
confidence = det.get('confidence', 0.0)
track_id = det.get('track_id', None)
# Get color based on class
color = COLORS.get(class_name.lower(), COLORS['default'])
# Draw bounding box
cv2.rectangle(annotated_frame, (x1, y1), (x2, y2), color, 2)
# Prepare label text
label_text = ""
if draw_labels:
label_text += class_name
if track_id is not None:
label_text += f" #{track_id}"
if draw_confidence and confidence > 0:
label_text += f" {confidence:.2f}"
# Draw label background
if label_text:
text_size = cv2.getTextSize(label_text, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 2)[0]
cv2.rectangle(
annotated_frame,
(x1, y1 - text_size[1] - 8),
(x1 + text_size[0] + 8, y1),
color,
-1
)
cv2.putText(
annotated_frame,
label_text,
(x1 + 4, y1 - 4),
cv2.FONT_HERSHEY_SIMPLEX,
0.5,
(255, 255, 255),
2
)
except Exception as e:
print(f"Error drawing detection: {e}")
return annotated_frame
def draw_violations(frame: np.ndarray, violations: List[Dict]) -> np.ndarray:
"""
Draw violation indicators on the frame.
(Currently disabled - just returns the original frame)
Args:
frame: Input video frame
violations: List of violation dictionaries
Returns:
Annotated frame
"""
# Violation detection is disabled - simply return the original frame
if frame is None or not isinstance(frame, np.ndarray):
return np.zeros((300, 300, 3), dtype=np.uint8)
# Just return a copy of the frame without drawing violations
return frame.copy()
def draw_performance_metrics(frame: np.ndarray, metrics: Dict) -> np.ndarray:
"""
Draw performance metrics overlay on the frame.
Args:
frame: Input video frame
metrics: Dictionary of performance metrics
Returns:
Annotated frame
"""
if frame is None or not isinstance(frame, np.ndarray):
return np.zeros((300, 300, 3), dtype=np.uint8)
annotated_frame = frame.copy()
height = annotated_frame.shape[0]
# Create semi-transparent overlay
overlay = annotated_frame.copy()
cv2.rectangle(overlay, (10, height - 140), (250, height - 20), (0, 0, 0), -1)
alpha = 0.7
cv2.addWeighted(overlay, alpha, annotated_frame, 1 - alpha, 0, annotated_frame)
# Draw metrics
text_y = height - 120
for metric, value in metrics.items():
text = f"{metric}: {value}"
cv2.putText(
annotated_frame,
text,
(20, text_y),
cv2.FONT_HERSHEY_SIMPLEX,
0.6,
(0, 255, 255),
2
)
text_y += 25
return annotated_frame
def convert_cv_to_qimage(cv_img):
"""
Convert OpenCV image to QImage for display in Qt widgets.
Args:
cv_img: OpenCV image (numpy array)
Returns:
QImage object
"""
if cv_img is None or not isinstance(cv_img, np.ndarray):
return QImage(1, 1, QImage.Format_RGB888)
try:
# Make a copy to ensure the data stays in scope
img_copy = cv_img.copy()
# Convert BGR to RGB
rgb_image = cv2.cvtColor(img_copy, cv2.COLOR_BGR2RGB)
h, w, ch = rgb_image.shape
bytes_per_line = ch * w
# Create QImage - this approach ensures continuous memory layout
# which is important for QImage to work correctly
qimage = QImage(rgb_image.tobytes(), w, h, bytes_per_line, QImage.Format_RGB888)
# Return a copy to ensure it remains valid
return qimage.copy()
except Exception as e:
print(f"Error converting image: {e}")
return QImage(1, 1, QImage.Format_RGB888)
def convert_cv_to_pixmap(cv_img, target_width=None):
"""
Convert OpenCV image to QPixmap for display in Qt widgets.
Args:
cv_img: OpenCV image (numpy array)
target_width: Optional width to resize to (maintains aspect ratio)
Returns:
QPixmap object
"""
try:
if cv_img is None:
print("WARNING: convert_cv_to_pixmap received None image")
empty_pixmap = QPixmap(640, 480)
empty_pixmap.fill(Qt.black)
return empty_pixmap
# Make a copy to ensure the data stays in scope
img_copy = cv_img.copy()
# Convert BGR to RGB directly
rgb_image = cv2.cvtColor(img_copy, cv2.COLOR_BGR2RGB)
h, w, ch = rgb_image.shape
bytes_per_line = ch * w
# Create QImage using tobytes() to ensure a continuous copy is made
# This avoids memory layout issues with numpy arrays
qimg = QImage(rgb_image.tobytes(), w, h, bytes_per_line, QImage.Format_RGB888)
if qimg.isNull():
print("WARNING: Failed to create QImage")
empty_pixmap = QPixmap(640, 480)
empty_pixmap.fill(Qt.black)
return empty_pixmap
# Resize if needed
if target_width and qimg.width() > target_width:
qimg = qimg.scaledToWidth(target_width, Qt.SmoothTransformation)
# Convert to pixmap
pixmap = QPixmap.fromImage(qimg)
if pixmap.isNull():
print("WARNING: Failed to create QPixmap from QImage")
empty_pixmap = QPixmap(640, 480)
empty_pixmap.fill(Qt.black)
return empty_pixmap
return pixmap
except Exception as e:
print(f"ERROR in convert_cv_to_pixmap: {e}")
# Return a black pixmap as fallback
empty_pixmap = QPixmap(640, 480)
empty_pixmap.fill(Qt.black)
return empty_pixmap
def resize_frame_for_display(frame: np.ndarray, max_width: int = 1280, max_height: int = 720) -> np.ndarray:
"""
Resize frame for display while maintaining aspect ratio.
Args:
frame: Input video frame
max_width: Maximum display width
max_height: Maximum display height
Returns:
Resized frame
"""
if frame is None:
return np.zeros((300, 300, 3), dtype=np.uint8)
height, width = frame.shape[:2]
# No resize needed if image is already smaller than max dimensions
if width <= max_width and height <= max_height:
return frame
# Calculate scale factor to fit within max dimensions
scale_width = max_width / width if width > max_width else 1.0
scale_height = max_height / height if height > max_height else 1.0
# Use the smaller scale to ensure image fits within bounds
scale = min(scale_width, scale_height)
# Resize using calculated scale
new_width = int(width * scale)
new_height = int(height * scale)
return cv2.resize(frame, (new_width, new_height), interpolation=cv2.INTER_AREA)
def pipeline_with_violation_line(frame: np.ndarray, draw_violation_line_func, violation_line_y: int = None) -> QPixmap:
"""
Example pipeline to ensure violation line is drawn and color order is correct.
Args:
frame: Input BGR frame (np.ndarray)
draw_violation_line_func: Function to draw violation line (should accept BGR frame)
violation_line_y: Y position for the violation line (int)
Returns:
QPixmap ready for display
"""
annotated_frame = frame.copy()
if violation_line_y is not None:
annotated_frame = draw_violation_line_func(annotated_frame, violation_line_y, color=(0, 0, 255), label='VIOLATION LINE')
display_frame = resize_frame_for_display(annotated_frame, max_width=1280, max_height=720)
pixmap = convert_cv_to_pixmap(display_frame)
return pixmap

View File

@@ -0,0 +1,65 @@
import cv2
import numpy as np
import math
from sklearn import linear_model
def lineCalc(vx, vy, x0, y0):
scale = 10
x1 = x0 + scale * vx
y1 = y0 + scale * vy
m = (y1 - y0) / (x1 - x0)
b = y1 - m * x1
return m, b
def lineIntersect(m1, b1, m2, b2):
a_1 = -m1
b_1 = 1
c_1 = b1
a_2 = -m2
b_2 = 1
c_2 = b2
d = a_1 * b_2 - a_2 * b_1
dx = c_1 * b_2 - c_2 * b_1
dy = a_1 * c_2 - a_2 * c_1
intersectionX = dx / d
intersectionY = dy / d
return intersectionX, intersectionY
def detect_crosswalk(frame):
'''Detects crosswalk/zebra lines robustly for various camera angles using adaptive thresholding and Hough Line Transform.'''
import cv2
import numpy as np
img = frame.copy()
H, W = img.shape[:2]
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Adaptive thresholding for lighting invariance
binary = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY, 15, 7)
# Morphology to clean up
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (W // 30, 3))
morphed = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel, iterations=2)
# Hough Line Transform to find lines
lines = cv2.HoughLinesP(morphed, 1, np.pi / 180, threshold=80, minLineLength=W // 10, maxLineGap=20)
crosswalk_lines = []
if lines is not None:
for line in lines:
x1, y1, x2, y2 = line[0]
# Filter for nearly horizontal lines (crosswalk stripes)
angle = np.degrees(np.arctan2(y2 - y1, x2 - x1))
if -20 < angle < 20: # adjust as needed for your camera
crosswalk_lines.append((x1, y1, x2, y2))
cv2.line(img, (x1, y1), (x2, y2), (0, 255, 0), 2)
# If no crosswalk lines found, return
if not crosswalk_lines:
return None, [], img
# Use the lowest (max y) line as the violation line
violation_line_y = max([max(y1, y2) for (x1, y1, x2, y2) in crosswalk_lines])
cv2.line(img, (0, violation_line_y), (W, violation_line_y), (0, 0, 255), 2)
return violation_line_y, crosswalk_lines, img
if __name__ == "__main__":
import sys
img = cv2.imread(sys.argv[1])
vp, medians, vis = detect_crosswalk(img)
cv2.imshow("Crosswalk Detection", vis)
cv2.waitKey(0)

View File

@@ -0,0 +1,50 @@
import cv2
import numpy as np
def findNonZero(rgb_image):
rows, cols, _ = rgb_image.shape
counter = 0
for row in range(rows):
for col in range(cols):
pixel = rgb_image[row, col]
if sum(pixel) != 0:
counter += 1
return counter
def red_green_yellow(rgb_image):
hsv = cv2.cvtColor(rgb_image, cv2.COLOR_RGB2HSV)
sum_saturation = np.sum(hsv[:,:,1])
area = rgb_image.shape[0] * rgb_image.shape[1]
avg_saturation = sum_saturation / area
sat_low = int(avg_saturation * 1.3)
val_low = 140
lower_green = np.array([70,sat_low,val_low])
upper_green = np.array([100,255,255])
green_mask = cv2.inRange(hsv, lower_green, upper_green)
lower_yellow = np.array([10,sat_low,val_low])
upper_yellow = np.array([60,255,255])
yellow_mask = cv2.inRange(hsv, lower_yellow, upper_yellow)
lower_red = np.array([150,sat_low,val_low])
upper_red = np.array([180,255,255])
red_mask = cv2.inRange(hsv, lower_red, upper_red)
sum_green = findNonZero(cv2.bitwise_and(rgb_image, rgb_image, mask=green_mask))
sum_yellow = findNonZero(cv2.bitwise_and(rgb_image, rgb_image, mask=yellow_mask))
sum_red = findNonZero(cv2.bitwise_and(rgb_image, rgb_image, mask=red_mask))
if sum_red >= sum_yellow and sum_red >= sum_green:
return "red"
if sum_yellow >= sum_green:
return "yellow"
return "green"
def detect_traffic_light_color(frame, bbox):
x, y, w, h = bbox
roi = frame[y:y+h, x:x+w]
roi_rgb = cv2.cvtColor(roi, cv2.COLOR_BGR2RGB)
return red_green_yellow(roi_rgb)
if __name__ == "__main__":
import sys
img = cv2.imread(sys.argv[1])
bbox = (int(sys.argv[2]), int(sys.argv[3]), int(sys.argv[4]), int(sys.argv[5]))
color = detect_traffic_light_color(img, bbox)
print("Detected color:", color)

View File

@@ -0,0 +1,951 @@
print("🟡 [CROSSWALK_UTILS] This is d:/Downloads/finale6/Khatam final/khatam/qt_app_pyside/utils/crosswalk_utils.py LOADED")
import cv2
import numpy as np
from typing import Tuple, Optional
def detect_crosswalk_and_violation_line(frame: np.ndarray, traffic_light_position: Optional[Tuple[int, int]] = None):
"""
Detects crosswalk (zebra crossing) or fallback stop line in a traffic scene using classical CV.
Args:
frame: BGR image frame from video feed
traffic_light_position: Optional (x, y) of traffic light in frame
Returns:
crosswalk_bbox: (x, y, w, h) or None if fallback used
violation_line_y: int (y position for violation check)
debug_info: dict (for visualization/debugging)
"""
debug_info = {}
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
h, w = gray.shape
# --- Preprocessing for zebra crossing ---
# Enhance contrast for night/low-light
if np.mean(gray) < 80:
gray = cv2.equalizeHist(gray)
debug_info['hist_eq'] = True
else:
debug_info['hist_eq'] = False
# Adaptive threshold to isolate white stripes
thresh = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C,
cv2.THRESH_BINARY, 19, 7)
# Morphology to connect stripes
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (15, 3))
morph = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel, iterations=2)
# Find contours
contours, _ = cv2.findContours(morph, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
zebra_rects = []
for cnt in contours:
x, y, rw, rh = cv2.boundingRect(cnt)
area = rw * rh
aspect = rw / rh if rh > 0 else 0
# Heuristic: long, thin, bright, horizontal stripes
if area > 500 and 2 < aspect < 15 and rh < h * 0.15:
zebra_rects.append((x, y, rw, rh))
debug_info['zebra_rects'] = zebra_rects
# Group rectangles that are aligned horizontally (zebra crossing)
crosswalk_bbox = None
violation_line_y = None
if len(zebra_rects) >= 3:
# Sort by y, then group by proximity
zebra_rects = sorted(zebra_rects, key=lambda r: r[1])
groups = []
group = [zebra_rects[0]]
for rect in zebra_rects[1:]:
if abs(rect[1] - group[-1][1]) < 40: # 40px vertical tolerance
group.append(rect)
else:
if len(group) >= 3:
groups.append(group)
group = [rect]
if len(group) >= 3:
groups.append(group)
# Pick group closest to traffic light (if provided), else lowest in frame
def group_center_y(g):
return np.mean([r[1] + r[3] // 2 for r in g])
if groups:
if traffic_light_position:
tx, ty = traffic_light_position
best_group = min(groups, key=lambda g: abs(group_center_y(g) - ty))
else:
best_group = max(groups, key=group_center_y)
# Union bbox
xs = [r[0] for r in best_group] + [r[0] + r[2] for r in best_group]
ys = [r[1] for r in best_group] + [r[1] + r[3] for r in best_group]
x1, x2 = min(xs), max(xs)
y1, y2 = min(ys), max(ys)
crosswalk_bbox = (x1, y1, x2 - x1, y2 - y1)
# Violation line: just before crosswalk starts (bottom of bbox - margin)
violation_line_y = y2 - 5
debug_info['crosswalk_group'] = best_group
# --- Fallback: Stop line detection ---
if crosswalk_bbox is None:
edges = cv2.Canny(gray, 80, 200)
lines = cv2.HoughLinesP(edges, 1, np.pi / 180, threshold=80, minLineLength=60, maxLineGap=20)
stop_lines = []
if lines is not None:
for l in lines:
x1, y1, x2, y2 = l[0]
angle = np.degrees(np.arctan2(y2 - y1, x2 - x1))
if abs(angle) < 20 or abs(angle) > 160: # horizontal
if y1 > h // 2 or y2 > h // 2: # lower half
stop_lines.append((x1, y1, x2, y2))
debug_info['stop_lines'] = stop_lines
if stop_lines:
# Pick the lowest (closest to bottom or traffic light)
if traffic_light_position:
tx, ty = traffic_light_position
best_line = min(stop_lines, key=lambda l: abs(((l[1]+l[3])//2) - ty))
else:
best_line = max(stop_lines, key=lambda l: max(l[1], l[3]))
x1, y1, x2, y2 = best_line
crosswalk_bbox = None
violation_line_y = min(y1, y2) - 5
debug_info['stop_line'] = best_line
return crosswalk_bbox, violation_line_y, debug_info
# Example usage:
# bbox, vline, dbg = detect_crosswalk_and_violation_line(frame, (tl_x, tl_y))
print("🟡 [CROSSWALK_UTILS] This is d:/Downloads/finale6/Khatam final/khatam/qt_app_pyside/utils/crosswalk_utils.py LOADED")
import cv2
import numpy as np
from typing import Dict, List, Tuple, Optional, Any
import math
# --- DeepLabV3+ Crosswalk Segmentation Integration ---
import sys
import os
sys.path.append(r'D:\Downloads\finale6\Khatam final\khatam\qt_app_pyside\DeepLabV3Plus-Pytorch')
import torch
import torch.nn as nn
from PIL import Image
from torchvision import transforms as T
def detect_crosswalk(frame: np.ndarray, roi_height_percentage: float = 0.4) -> Optional[List[int]]:
"""
[DEPRECATED] Use detect_and_draw_crosswalk for advanced visualization and analytics.
This function is kept for backward compatibility but will print a warning.
"""
print("[WARN] detect_crosswalk is deprecated. Use detect_and_draw_crosswalk instead.")
try:
height, width = frame.shape[:2]
roi_height = int(height * roi_height_percentage)
roi_y = height - roi_height
# Extract ROI
roi = frame[roi_y:height, 0:width]
# Convert to grayscale
gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
# Apply adaptive thresholding
binary = cv2.adaptiveThreshold(
gray,
255,
cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY,
19,
2
)
# Apply morphological operations to clean up the binary image
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 3))
binary = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)
binary = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel)
# Find contours
contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# Filter contours by shape and aspect ratio
potential_stripes = []
for contour in contours:
x, y, w, h = cv2.boundingRect(contour)
aspect_ratio = w / h if h > 0 else 0
area = cv2.contourArea(contour)
# Stripe criteria: Rectangular, wide, not too tall
if area > 100 and aspect_ratio >= 3 and aspect_ratio <= 20:
potential_stripes.append((x, y + roi_y, w, h))
# Group nearby stripes into crosswalk
if len(potential_stripes) >= 3:
# Sort by y-coordinate (top to bottom)
potential_stripes.sort(key=lambda s: s[1])
# Find groups of stripes with similar y-positions
stripe_groups = []
current_group = [potential_stripes[0]]
for i in range(1, len(potential_stripes)):
# If this stripe is close to the previous one in y-direction
if abs(potential_stripes[i][1] - current_group[-1][1]) < 50:
current_group.append(potential_stripes[i])
else:
# Start a new group
if len(current_group) >= 3:
stripe_groups.append(current_group)
current_group = [potential_stripes[i]]
# Add the last group if it has enough stripes
if len(current_group) >= 3:
stripe_groups.append(current_group)
# Find the largest group
if stripe_groups:
largest_group = max(stripe_groups, key=len)
# Compute bounding box for the crosswalk
min_x = min(stripe[0] for stripe in largest_group)
min_y = min(stripe[1] for stripe in largest_group)
max_x = max(stripe[0] + stripe[2] for stripe in largest_group)
max_y = max(stripe[1] + stripe[3] for stripe in largest_group)
return [min_x, min_y, max_x, max_y]
return None
except Exception as e:
print(f"Error detecting crosswalk: {e}")
return None
def detect_stop_line(frame: np.ndarray) -> Optional[int]:
"""
Detect stop line in a frame using edge detection and Hough Line Transform.
Args:
frame: Input video frame
Returns:
Y-coordinate of the stop line or None if not detected
"""
try:
height, width = frame.shape[:2]
# Define ROI - bottom 30% of the frame
roi_height = int(height * 0.3)
roi_y = height - roi_height
roi = frame[roi_y:height, 0:width].copy()
# Convert to grayscale
gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
# Apply Gaussian blur to reduce noise
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
# Apply Canny edge detection
edges = cv2.Canny(blurred, 50, 150)
# Apply Hough Line Transform
lines = cv2.HoughLinesP(
edges,
rho=1,
theta=np.pi/180,
threshold=80,
minLineLength=width//3, # Lines should be at least 1/3 of image width
maxLineGap=50
)
if lines is None or len(lines) == 0:
return None
# Filter horizontal lines (slope close to 0)
horizontal_lines = []
for line in lines:
x1, y1, x2, y2 = line[0]
if x2 - x1 == 0: # Avoid division by zero
continue
slope = abs((y2 - y1) / (x2 - x1))
# Horizontal line has slope close to 0
if slope < 0.2:
horizontal_lines.append((x1, y1, x2, y2, slope))
if not horizontal_lines:
return None
# Sort by y-coordinate (bottom to top)
horizontal_lines.sort(key=lambda line: max(line[1], line[3]), reverse=True)
# Get the uppermost horizontal line
if horizontal_lines:
x1, y1, x2, y2, _ = horizontal_lines[0]
stop_line_y = roi_y + max(y1, y2)
return stop_line_y
return None
except Exception as e:
print(f"Error detecting stop line: {e}")
return None
def draw_violation_line(frame: np.ndarray, y_coord: int, color: Tuple[int, int, int] = (0, 0, 255),
label: str = "VIOLATION LINE", thickness: int = 2) -> np.ndarray:
"""
Draw a violation line on the frame with customizable label.
Args:
frame: Input video frame
y_coord: Y-coordinate for the line
color: Line color (BGR)
label: Custom label text to display
thickness: Line thickness
Returns:
Frame with the violation line drawn
"""
height, width = frame.shape[:2]
cv2.line(frame, (0, y_coord), (width, y_coord), color, thickness)
# Add label with transparent background for better visibility
text_size, _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.6, 2)
text_x = width // 2 - text_size[0] // 2
text_y = y_coord - 10
# Draw semi-transparent background
overlay = frame.copy()
cv2.rectangle(
overlay,
(text_x - 5, text_y - text_size[1] - 5),
(text_x + text_size[0] + 5, text_y + 5),
(0, 0, 0),
-1
)
cv2.addWeighted(overlay, 0.6, frame, 0.4, 0, frame)
# Add label
cv2.putText(
frame,
label,
(text_x, text_y),
cv2.FONT_HERSHEY_SIMPLEX,
0.6,
color,
2
)
return frame
def check_vehicle_violation(vehicle_bbox: List[int], violation_line_y: int) -> bool:
"""
Check if a vehicle has crossed the violation line.
Args:
vehicle_bbox: Vehicle bounding box [x1, y1, x2, y2]
violation_line_y: Y-coordinate of the violation line
Returns:
True if violation detected, False otherwise
"""
# Get the bottom-center point of the vehicle
x1, y1, x2, y2 = vehicle_bbox
vehicle_bottom = y2
vehicle_center_y = (y1 + y2) / 2
# Calculate how much of the vehicle is below the violation line
height = y2 - y1
if height <= 0: # Avoid division by zero
return False
# A vehicle is considered in violation if either:
# 1. Its bottom edge is below the violation line
# 2. Its center is below the violation line (for large vehicles)
is_violation = (vehicle_bottom > violation_line_y) or (vehicle_center_y > violation_line_y)
if is_violation:
print(f"🚨 Vehicle crossing violation line! Vehicle bottom: {vehicle_bottom}, Line: {violation_line_y}")
return is_violation
def get_deeplab_model(weights_path, device='cpu', model_name='deeplabv3plus_mobilenet', num_classes=21, output_stride=8):
"""
Loads DeepLabV3+ model and weights for crosswalk segmentation.
"""
print(f"[DEBUG] get_deeplab_model called with weights_path={weights_path}, device={device}, model_name={model_name}")
import network # DeepLabV3Plus-Pytorch/network/__init__.py
model = network.modeling.__dict__[model_name](num_classes=num_classes, output_stride=output_stride)
if weights_path is not None and os.path.isfile(weights_path):
print(f"[DEBUG] Loading weights from: {weights_path}")
checkpoint = torch.load(weights_path, map_location=torch.device(device))
model.load_state_dict(checkpoint["model_state"])
else:
print(f"[DEBUG] Weights file not found: {weights_path}")
model = nn.DataParallel(model)
model.to(device)
model.eval()
print(f"[DEBUG] Model loaded and moved to {device}")
return model
def run_inference(model, frame, device='cpu'):
"""
Preprocesses frame and runs DeepLabV3+ model to get mask.
"""
# frame: np.ndarray (H, W, 3) in BGR
img_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
pil_img = Image.fromarray(img_rgb)
transform = T.Compose([
T.ToTensor(),
T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])
input_tensor = transform(pil_img).unsqueeze(0).to(device)
with torch.no_grad():
output = model(input_tensor)
if isinstance(output, dict):
output = output["out"] if "out" in output else list(output.values())[0]
mask = output.argmax(1).squeeze().cpu().numpy().astype(np.uint8)
return mask
def detect_and_draw_crosswalk(frame: np.ndarray, roi_height_percentage: float = 0.4, use_deeplab: bool = True) -> Tuple[np.ndarray, Optional[List[int]], Optional[List]]:
"""
Advanced crosswalk detection with DeepLabV3+ segmentation (if enabled),
otherwise falls back to Hough Transform + line clustering.
Args:
frame: Input video frame
roi_height_percentage: Percentage of the frame height to use as ROI
use_deeplab: If True, use DeepLabV3+ segmentation for crosswalk detection
Returns:
Tuple containing:
- Annotated frame with crosswalk visualization
- Crosswalk bounding box [x, y, w, h] or None if not detected
- List of detected crosswalk contours or lines or None
"""
try:
height, width = frame.shape[:2]
annotated_frame = frame.copy()
print(f"[DEBUG] detect_and_draw_crosswalk called, use_deeplab={use_deeplab}")
# --- DeepLabV3+ Segmentation Path ---
if use_deeplab:
# Load model only once (cache in function attribute)
if not hasattr(detect_and_draw_crosswalk, '_deeplab_model'):
weights_path = os.path.join(os.path.dirname(__file__), '../DeepLabV3Plus-Pytorch/best_crosswalk.pth')
print(f"[DEBUG] Loading DeepLabV3+ model from: {weights_path}")
detect_and_draw_crosswalk._deeplab_model = get_deeplab_model(weights_path, device='cpu')
model = detect_and_draw_crosswalk._deeplab_model
# Run inference
mask = run_inference(model, frame)
print(f"[DEBUG] DeepLabV3+ mask shape: {mask.shape}, unique values: {np.unique(mask)}")
# Assume crosswalk class index is 12 (change if needed)
crosswalk_class = 12
crosswalk_mask = (mask == crosswalk_class).astype(np.uint8) * 255
print(f"[DEBUG] crosswalk_mask unique values: {np.unique(crosswalk_mask)}")
# Find contours in mask
contours, _ = cv2.findContours(crosswalk_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
print(f"[DEBUG] DeepLabV3+ found {len(contours)} contours")
if not contours:
print("[DEBUG] No contours found in DeepLabV3+ mask, falling back to classic method.")
# Fallback to classic method if nothing found
return detect_and_draw_crosswalk(frame, roi_height_percentage, use_deeplab=False)
# Draw all crosswalk contours
x_min, y_min, x_max, y_max = width, height, 0, 0
for cnt in contours:
x, y, w, h = cv2.boundingRect(cnt)
x_min = min(x_min, x)
y_min = min(y_min, y)
x_max = max(x_max, x + w)
y_max = max(y_max, y + h)
cv2.drawContours(annotated_frame, [cnt], -1, (0, 255, 255), 3)
# Clamp bbox to frame and ensure non-negative values
x_min = max(0, min(x_min, width - 1))
y_min = max(0, min(y_min, height - 1))
x_max = max(0, min(x_max, width - 1))
y_max = max(0, min(y_max, height - 1))
w = max(0, x_max - x_min)
h = max(0, y_max - y_min)
crosswalk_bbox = [x_min, y_min, w, h]
# Ignore invalid bboxes
if w <= 0 or h <= 0:
print("[DEBUG] Ignoring invalid crosswalk_bbox (zero or negative size)")
return annotated_frame, None, contours
# TODO: Mask out detected vehicles before running crosswalk detection to reduce false positives.
cv2.rectangle(
annotated_frame,
(crosswalk_bbox[0], crosswalk_bbox[1]),
(crosswalk_bbox[0] + crosswalk_bbox[2], crosswalk_bbox[1] + crosswalk_bbox[3]),
(0, 255, 255), 2
)
cv2.putText(
annotated_frame,
"CROSSWALK",
(crosswalk_bbox[0], crosswalk_bbox[1] - 10),
cv2.FONT_HERSHEY_SIMPLEX,
0.7,
(0, 255, 255),
2
)
print(f"[DEBUG] DeepLabV3+ crosswalk_bbox: {crosswalk_bbox}")
return annotated_frame, crosswalk_bbox, contours
# --- Classic Hough Transform Fallback ---
print("[DEBUG] Using classic Hough Transform fallback method.")
height, width = frame.shape[:2]
roi_height = int(height * roi_height_percentage)
roi_y = height - roi_height
roi = frame[roi_y:height, 0:width]
gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
edges = cv2.Canny(blurred, 50, 150, apertureSize=3)
lines = cv2.HoughLinesP(edges, 1, np.pi/180, threshold=60, minLineLength=40, maxLineGap=30)
print(f"[DEBUG] HoughLinesP found {0 if lines is None else len(lines)} lines")
if lines is None:
return frame, None, None
angle_threshold = 12 # degrees
parallel_lines = []
for line in lines:
x1, y1, x2, y2 = line[0]
angle = math.degrees(math.atan2(y2 - y1, x2 - x1))
if -angle_threshold <= angle <= angle_threshold or 80 <= abs(angle) <= 100:
parallel_lines.append((x1, y1, x2, y2, angle))
print(f"[DEBUG] {len(parallel_lines)} parallel lines after angle filtering")
if len(parallel_lines) < 3:
return frame, None, None
parallel_lines = sorted(parallel_lines, key=lambda l: min(l[1], l[3]))
clusters = []
cluster = [parallel_lines[0]]
min_spacing = 10
max_spacing = 60
for i in range(1, len(parallel_lines)):
prev_y = min(cluster[-1][1], cluster[-1][3])
curr_y = min(parallel_lines[i][1], parallel_lines[i][3])
spacing = abs(curr_y - prev_y)
if min_spacing < spacing < max_spacing:
cluster.append(parallel_lines[i])
else:
if len(cluster) >= 3:
clusters.append(cluster)
cluster = [parallel_lines[i]]
if len(cluster) >= 3:
clusters.append(cluster)
print(f"[DEBUG] {len(clusters)} clusters found")
if not clusters:
return frame, None, None
best_cluster = max(clusters, key=len)
x_min = width
y_min = roi_height
x_max = 0
y_max = 0
for x1, y1, x2, y2, angle in best_cluster:
cv2.line(annotated_frame, (x1, y1 + roi_y), (x2, y2 + roi_y), (0, 255, 255), 3)
x_min = min(x_min, x1, x2)
y_min = min(y_min, y1, y2)
x_max = max(x_max, x1, x2)
y_max = max(y_max, y1, y2)
crosswalk_bbox = [x_min, y_min + roi_y, x_max - x_min, y_max - y_min]
cv2.rectangle(
annotated_frame,
(crosswalk_bbox[0], crosswalk_bbox[1]),
(crosswalk_bbox[0] + crosswalk_bbox[2], crosswalk_bbox[1] + crosswalk_bbox[3]),
(0, 255, 255), 2
)
cv2.putText(
annotated_frame,
"CROSSWALK",
(crosswalk_bbox[0], crosswalk_bbox[1] - 10),
cv2.FONT_HERSHEY_SIMPLEX,
0.7,
(0, 255, 255),
2
)
print(f"[DEBUG] Classic method crosswalk_bbox: {crosswalk_bbox}")
return annotated_frame, crosswalk_bbox, best_cluster
except Exception as e:
print(f"Error in detect_and_draw_crosswalk: {str(e)}")
import traceback
traceback.print_exc()
return frame, None, None
#working
print("🟡 [CROSSWALK_UTILS] This is d:/Downloads/finale6/Khatam final/khatam/qt_app_pyside/utils/crosswalk_utils.py LOADED")
import cv2
import numpy as np
from typing import Tuple, Optional
def detect_crosswalk_and_violation_line(frame: np.ndarray, traffic_light_position: Optional[Tuple[int, int]] = None, perspective_M: Optional[np.ndarray] = None):
"""
Detects crosswalk (zebra crossing) or fallback stop line in a traffic scene using classical CV.
Args:
frame: BGR image frame from video feed
traffic_light_position: Optional (x, y) of traffic light in frame
perspective_M: Optional 3x3 homography matrix for bird's eye view normalization
Returns:
result_frame: frame with overlays (for visualization)
crosswalk_bbox: (x, y, w, h) or None if fallback used
violation_line_y: int (y position for violation check)
debug_info: dict (for visualization/debugging)
"""
debug_info = {}
orig_frame = frame.copy()
h, w = frame.shape[:2]
# 1. Perspective Normalization (Bird's Eye View)
if perspective_M is not None:
frame = cv2.warpPerspective(frame, perspective_M, (w, h))
debug_info['perspective_warped'] = True
else:
debug_info['perspective_warped'] = False
# 1. White Color Filtering (relaxed)
mask_white = cv2.inRange(frame, (160, 160, 160), (255, 255, 255))
debug_info['mask_white_ratio'] = np.sum(mask_white > 0) / (h * w)
# 2. Grayscale for adaptive threshold
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# Enhance contrast for night/low-light
if np.mean(gray) < 80:
gray = cv2.equalizeHist(gray)
debug_info['hist_eq'] = True
else:
debug_info['hist_eq'] = False
# 5. Adaptive threshold (tuned)
thresh = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY, 15, 5)
# Combine with color mask
combined = cv2.bitwise_and(thresh, mask_white)
# 2. Morphology (tuned)
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (25, 3))
morph = cv2.morphologyEx(combined, cv2.MORPH_CLOSE, kernel, iterations=1)
# Find contours
contours, _ = cv2.findContours(morph, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
zebra_rects = []
for cnt in contours:
x, y, w, h = cv2.boundingRect(cnt)
aspect_ratio = w / max(h, 1)
area = w * h
angle = 0 # For simplicity, assume horizontal stripes
# Heuristic: wide, short, and not too small
if aspect_ratio > 3 and 1000 < area < 0.5 * frame.shape[0] * frame.shape[1] and h < 60:
zebra_rects.append((x, y, w, h, angle))
cv2.rectangle(orig_frame, (x, y), (x+w, y+h), (0, 255, 0), 2)
# --- Overlay drawing for debugging: draw all zebra candidates ---
for r in zebra_rects:
x, y, rw, rh, _ = r
cv2.rectangle(orig_frame, (x, y), (x+rw, y+rh), (0, 255, 0), 2)
# Draw all zebra candidate rectangles for debugging (no saving)
for r in zebra_rects:
x, y, rw, rh, _ = r
cv2.rectangle(orig_frame, (x, y), (x+rw, y+rh), (0, 255, 0), 2)
# --- Probabilistic Scoring for Groups ---
def group_score(group):
if len(group) < 3:
return 0
heights = [r[3] for r in group]
x_centers = [r[0] + r[2]//2 for r in group]
angles = [r[4] for r in group]
# Stripe count (normalized)
count_score = min(len(group) / 6, 1.0)
# Height consistency
height_score = 1.0 - min(np.std(heights) / (np.mean(heights) + 1e-6), 1.0)
# X-center alignment
x_score = 1.0 - min(np.std(x_centers) / (w * 0.2), 1.0)
# Angle consistency (prefer near 0 or 90)
mean_angle = np.mean([abs(a) for a in angles])
angle_score = 1.0 - min(np.std(angles) / 10.0, 1.0)
# Whiteness (mean mask_white in group area)
whiteness = 0
for r in group:
x, y, rw, rh, _ = r
whiteness += np.mean(mask_white[y:y+rh, x:x+rw]) / 255
whiteness_score = whiteness / len(group)
# Final score (weighted sum)
score = 0.25*count_score + 0.2*height_score + 0.2*x_score + 0.15*angle_score + 0.2*whiteness_score
return score
# 4. Dynamic grouping tolerance
y_tolerance = int(h * 0.05)
crosswalk_bbox = None
violation_line_y = None
best_score = 0
best_group = None
if len(zebra_rects) >= 3:
zebra_rects = sorted(zebra_rects, key=lambda r: r[1])
groups = []
group = [zebra_rects[0]]
for rect in zebra_rects[1:]:
if abs(rect[1] - group[-1][1]) < y_tolerance:
group.append(rect)
else:
if len(group) >= 3:
groups.append(group)
group = [rect]
if len(group) >= 3:
groups.append(group)
# Score all groups
scored_groups = [(group_score(g), g) for g in groups if group_score(g) > 0.1]
print(f"[CROSSWALK DEBUG] scored_groups: {[s for s, _ in scored_groups]}")
if scored_groups:
scored_groups.sort(reverse=True, key=lambda x: x[0])
best_score, best_group = scored_groups[0]
print("Best group score:", best_score)
# Visualization for debugging
debug_vis = orig_frame.copy()
for r in zebra_rects:
x, y, rw, rh, _ = r
cv2.rectangle(debug_vis, (x, y), (x+rw, y+rh), (255, 0, 255), 2)
for r in best_group:
x, y, rw, rh, _ = r
cv2.rectangle(debug_vis, (x, y), (x+rw, y+rh), (0, 255, 255), 3)
cv2.imwrite(f"debug_crosswalk_group.png", debug_vis)
# Optionally, filter by vanishing point as before
# ...existing vanishing point code...
xs = [r[0] for r in best_group] + [r[0] + r[2] for r in best_group]
ys = [r[1] for r in best_group] + [r[1] + r[3] for r in best_group]
x1, x2 = min(xs), max(xs)
y1, y2 = min(ys), max(ys)
crosswalk_bbox = (x1, y1, x2 - x1, y2 - y1)
violation_line_y = y2 - 5
debug_info['crosswalk_group'] = best_group
debug_info['crosswalk_score'] = best_score
debug_info['crosswalk_angles'] = [r[4] for r in best_group]
# --- Fallback: Stop line detection ---
if crosswalk_bbox is None:
edges = cv2.Canny(gray, 80, 200)
lines = cv2.HoughLinesP(edges, 1, np.pi / 180, threshold=80, minLineLength=60, maxLineGap=20)
stop_lines = []
if lines is not None:
for l in lines:
x1, y1, x2, y2 = l[0]
angle = np.degrees(np.arctan2(y2 - y1, x2 - x1))
if abs(angle) < 20 or abs(angle) > 160: # horizontal
if y1 > h // 2 or y2 > h // 2: # lower half
stop_lines.append((x1, y1, x2, y2))
debug_info['stop_lines'] = stop_lines
print(f"[CROSSWALK DEBUG] stop_lines: {len(stop_lines)} found")
if stop_lines:
if traffic_light_position:
tx, ty = traffic_light_position
best_line = min(stop_lines, key=lambda l: abs(((l[1]+l[3])//2) - ty))
else:
best_line = max(stop_lines, key=lambda l: max(l[1], l[3]))
x1, y1, x2, y2 = best_line
crosswalk_bbox = None
violation_line_y = min(y1, y2) - 5
debug_info['stop_line'] = best_line
print(f"[CROSSWALK DEBUG] using stop_line: {best_line}")
# Draw fallback violation line overlay for debugging (no saving)
if crosswalk_bbox is None and violation_line_y is not None:
print(f"[DEBUG] Drawing violation line at y={violation_line_y} (frame height={orig_frame.shape[0]})")
if 0 <= violation_line_y < orig_frame.shape[0]:
orig_frame = draw_violation_line(orig_frame, violation_line_y, color=(0, 255, 255), thickness=8, style='solid', label='Fallback Stop Line')
else:
print(f"[WARNING] Invalid violation line position: {violation_line_y}")
# --- Manual overlay for visualization pipeline test ---
# Removed fake overlays that could overwrite the real violation line
print(f"[CROSSWALK DEBUG] crosswalk_bbox: {crosswalk_bbox}, violation_line_y: {violation_line_y}")
return orig_frame, crosswalk_bbox, violation_line_y, debug_info
def draw_violation_line(frame: np.ndarray, y: int, color=(0, 255, 255), thickness=8, style='solid', label='Violation Line'):
"""
Draws a thick, optionally dashed, labeled violation line at the given y-coordinate.
Args:
frame: BGR image
y: y-coordinate for the line
color: BGR color tuple
thickness: line thickness
style: 'solid' or 'dashed'
label: Optional label to draw above the line
Returns:
frame with line overlay
"""
import cv2
h, w = frame.shape[:2]
x1, x2 = 0, w
overlay = frame.copy()
if style == 'dashed':
dash_len = 30
gap = 20
for x in range(x1, x2, dash_len + gap):
x_end = min(x + dash_len, x2)
cv2.line(overlay, (x, y), (x_end, y), color, thickness, lineType=cv2.LINE_AA)
else:
cv2.line(overlay, (x1, y), (x2, y), color, thickness, lineType=cv2.LINE_AA)
# Blend for semi-transparency
cv2.addWeighted(overlay, 0.7, frame, 0.3, 0, frame)
# Draw label
if label:
font = cv2.FONT_HERSHEY_SIMPLEX
text_size, _ = cv2.getTextSize(label, font, 0.8, 2)
text_x = max(10, (w - text_size[0]) // 2)
text_y = max(0, y - 12)
cv2.rectangle(frame, (text_x - 5, text_y - text_size[1] - 5), (text_x + text_size[0] + 5, text_y + 5), (0,0,0), -1)
cv2.putText(frame, label, (text_x, text_y), font, 0.8, color, 2, cv2.LINE_AA)
return frame
def get_violation_line_y(frame, traffic_light_bbox=None, crosswalk_bbox=None):
"""
Returns the y-coordinate of the violation line using the following priority:
1. Crosswalk bbox (most accurate)
2. Stop line detection via image processing (CV)
3. Traffic light bbox heuristic
4. Fallback (default)
"""
height, width = frame.shape[:2]
# 1. Crosswalk bbox
if crosswalk_bbox is not None and len(crosswalk_bbox) == 4:
return int(crosswalk_bbox[1]) - 15
# 2. Stop line detection (CV)
roi_height = int(height * 0.4)
roi_y = height - roi_height
roi = frame[roi_y:height, 0:width]
gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
binary = cv2.adaptiveThreshold(
gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY, 15, -2
)
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (15, 1))
processed = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)
contours, _ = cv2.findContours(processed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
stop_line_candidates = []
for cnt in contours:
x, y, w, h = cv2.boundingRect(cnt)
aspect_ratio = w / max(h, 1)
normalized_width = w / width
if (aspect_ratio > 5 and normalized_width > 0.3 and h < 15 and y > roi_height * 0.5):
abs_y = y + roi_y
stop_line_candidates.append((abs_y, w))
if stop_line_candidates:
stop_line_candidates.sort(key=lambda x: x[1], reverse=True)
return stop_line_candidates[0][0]
# 3. Traffic light bbox heuristic
if traffic_light_bbox is not None and len(traffic_light_bbox) == 4:
traffic_light_bottom = traffic_light_bbox[3]
traffic_light_height = traffic_light_bbox[3] - traffic_light_bbox[1]
estimated_distance = min(5 * traffic_light_height, height * 0.3)
return min(int(traffic_light_bottom + estimated_distance), height - 20)
# 4. Fallback
return int(height * 0.75)
# Example usage:
# bbox, vline, dbg = detect_crosswalk_and_violation_line(frame, (tl_x, tl_y), perspective_M)
##working
print("🟡 [CROSSWALK_UTILS] This is d:/Downloads/finale6/Khatam final/khatam/qt_app_pyside/utils/crosswalk_utils.py LOADED")
import cv2
import numpy as np
from sklearn import linear_model
def detect_crosswalk_and_violation_line(frame, traffic_light_position=None, debug=False):
"""
Robust crosswalk and violation line detection for red-light violation system.
Returns:
frame_with_overlays, crosswalk_bbox, violation_line_y, debug_info
"""
frame_out = frame.copy()
h, w = frame.shape[:2]
debug_info = {}
# === Step 1: Robust white color mask (HSV) ===
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
lower_white = np.array([0, 0, 180])
upper_white = np.array([180, 80, 255])
mask = cv2.inRange(hsv, lower_white, upper_white)
# === Step 2: Morphological filtering ===
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (7, 3))
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
# === Step 3: Contour extraction and filtering ===
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
crosswalk_bars = []
for cnt in contours:
x, y, cw, ch = cv2.boundingRect(cnt)
if cw > w * 0.05 and ch < h * 0.15:
crosswalk_bars.append((x, y, cw, ch))
# === Step 4: Draw detected bars for debug ===
for (x, y, cw, ch) in crosswalk_bars:
cv2.rectangle(frame_out, (x, y), (x + cw, y + ch), (0, 255, 255), 2) # yellow
# === Step 5: Violation line placement at bottom of bars ===
ys = np.array([y for (x, y, w, h) in crosswalk_bars])
hs = np.array([h for (x, y, w, h) in crosswalk_bars])
if len(ys) >= 3:
bottom_edges = ys + hs
violation_line_y = int(np.max(bottom_edges)) + 5 # +5 offset
violation_line_y = min(violation_line_y, h - 1)
crosswalk_bbox = (0, int(np.min(ys)), w, int(np.max(bottom_edges)) - int(np.min(ys)))
# Draw semi-transparent crosswalk region
overlay = frame_out.copy()
cv2.rectangle(overlay, (0, int(np.min(ys))), (w, int(np.max(bottom_edges))), (0, 255, 0), -1)
frame_out = cv2.addWeighted(overlay, 0.2, frame_out, 0.8, 0)
cv2.rectangle(frame_out, (0, int(np.min(ys))), (w, int(np.max(bottom_edges))), (0, 255, 0), 2)
cv2.putText(frame_out, "Crosswalk", (10, int(np.min(ys)) - 10),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
else:
violation_line_y = int(h * 0.65)
crosswalk_bbox = None
# === Draw violation line ===
cv2.line(frame_out, (0, violation_line_y), (w, violation_line_y), (0, 0, 255), 3)
cv2.putText(frame_out, "Violation Line", (10, violation_line_y - 10),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
debug_info['crosswalk_bars'] = crosswalk_bars
debug_info['violation_line_y'] = violation_line_y
debug_info['crosswalk_bbox'] = crosswalk_bbox
return frame_out, crosswalk_bbox, violation_line_y, debug_info
def draw_violation_line(frame: np.ndarray, y: int, color=(0, 0, 255), thickness=4, style='solid', label='Violation Line'):
h, w = frame.shape[:2]
x1, x2 = 0, w
overlay = frame.copy()
if style == 'dashed':
dash_len = 30
gap = 20
for x in range(x1, x2, dash_len + gap):
x_end = min(x + dash_len, x2)
cv2.line(overlay, (x, y), (x_end, y), color, thickness, lineType=cv2.LINE_AA)
else:
cv2.line(overlay, (x1, y), (x2, y), color, thickness, lineType=cv2.LINE_AA)
cv2.addWeighted(overlay, 0.7, frame, 0.3, 0, frame)
if label:
font = cv2.FONT_HERSHEY_SIMPLEX
text_size, _ = cv2.getTextSize(label, font, 0.8, 2)
text_x = max(10, (w - text_size[0]) // 2)
text_y = max(0, y - 12)
cv2.rectangle(frame, (text_x - 5, text_y - text_size[1] - 5), (text_x + text_size[0] + 5, text_y + 5), (0,0,0), -1)
cv2.putText(frame, label, (text_x, text_y), font, 0.8, color, 2, cv2.LINE_AA)
return frame
def get_violation_line_y(frame, traffic_light_bbox=None, crosswalk_bbox=None):
"""
Returns the y-coordinate of the violation line using the following priority:
1. Crosswalk bbox (most accurate)
2. Stop line detection via image processing (CV)
3. Traffic light bbox heuristic
4. Fallback (default)
"""
height, width = frame.shape[:2]
# 1. Crosswalk bbox
if crosswalk_bbox is not None and len(crosswalk_bbox) == 4:
return int(crosswalk_bbox[1]) - 15
# 2. Stop line detection (CV)
roi_height = int(height * 0.4)
roi_y = height - roi_height
roi = frame[roi_y:height, 0:width]
gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
binary = cv2.adaptiveThreshold(
gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY, 15, -2
)
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (15, 1))
processed = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)
contours, _ = cv2.findContours(processed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
stop_line_candidates = []
for cnt in contours:
x, y, w, h = cv2.boundingRect(cnt)
aspect_ratio = w / max(h, 1)
normalized_width = w / width
if (aspect_ratio > 5 and normalized_width > 0.3 and h < 15 and y > roi_height * 0.5):
abs_y = y + roi_y
stop_line_candidates.append((abs_y, w))
if stop_line_candidates:
stop_line_candidates.sort(key=lambda x: x[1], reverse=True)
return stop_line_candidates[0][0]
# 3. Traffic light bbox heuristic
if traffic_light_bbox is not None and len(traffic_light_bbox) == 4:
traffic_light_bottom = traffic_light_bbox[3]
traffic_light_height = traffic_light_bbox[3] - traffic_light_bbox[1]
estimated_distance = min(5 * traffic_light_height, height * 0.3)
return min(int(traffic_light_bottom + estimated_distance), height - 20)
# 4. Fallback
return int(height * 0.75)
# Example usage:
# bbox, vline, dbg = detect_crosswalk_and_violation_line(frame, (tl_x, tl_y), perspective_M)

View File

@@ -0,0 +1,462 @@
# print("🟡 [CROSSWALK_UTILS] This is d:/Downloads/finale6/Khatam final/khatam/qt_app_pyside/utils/crosswalk_utils.py LOADED")
# import cv2
# import numpy as np
# def detect_crosswalk_and_violation_line(frame, traffic_light_detected=False, perspective_M=None, debug=False):
# """
# Detects crosswalk (zebra crossing) or fallback stop line in a traffic scene using classical CV.
# Only runs crosswalk detection if a traffic light is present in the frame.
# If no traffic light is present, no violation line is drawn or returned.
# Returns:
# result_frame: frame with overlays (for visualization)
# crosswalk_bbox: (x, y, w, h) or None if fallback used
# violation_line_y: int (y position for violation check) or None if not applicable
# debug_info: dict (for visualization/debugging)
# """
# debug_info = {}
# orig_frame = frame.copy()
# h, w = frame.shape[:2]
# if not traffic_light_detected:
# # No traffic light: do not draw or return any violation line
# debug_info['crosswalk_bbox'] = None
# debug_info['violation_line_y'] = None
# debug_info['note'] = 'No traffic light detected, no violation line.'
# return orig_frame, None, None, debug_info
# # 1. Perspective Normalization (Bird's Eye View)
# if perspective_M is not None:
# frame = cv2.warpPerspective(frame, perspective_M, (w, h))
# debug_info['perspective_warped'] = True
# else:
# debug_info['perspective_warped'] = False
# # 2. White Color Filtering (relaxed)
# mask_white = cv2.inRange(frame, (160, 160, 160), (255, 255, 255))
# debug_info['mask_white_ratio'] = np.sum(mask_white > 0) / (h * w)
# # 3. Grayscale for adaptive threshold
# gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# if np.mean(gray) < 80:
# gray = cv2.equalizeHist(gray)
# debug_info['hist_eq'] = True
# else:
# debug_info['hist_eq'] = False
# # 4. Adaptive threshold (tuned)
# thresh = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
# cv2.THRESH_BINARY, 15, 5)
# combined = cv2.bitwise_and(thresh, mask_white)
# # 5. Morphology (tuned)
# kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (25, 3))
# morph = cv2.morphologyEx(combined, cv2.MORPH_CLOSE, kernel, iterations=1)
# # 6. Find contours for crosswalk bars
# contours, _ = cv2.findContours(morph, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# zebra_rects = []
# for cnt in contours:
# x, y, rw, rh = cv2.boundingRect(cnt)
# aspect_ratio = rw / max(rh, 1)
# area = rw * rh
# if aspect_ratio > 3 and 1000 < area < 0.5 * h * w and rh < 60:
# zebra_rects.append((x, y, rw, rh))
# # 7. Group crosswalk bars by y (vertical alignment)
# y_tolerance = int(h * 0.05)
# crosswalk_bbox = None
# violation_line_y = None
# if len(zebra_rects) >= 3:
# zebra_rects = sorted(zebra_rects, key=lambda r: r[1])
# groups = []
# group = [zebra_rects[0]]
# for rect in zebra_rects[1:]:
# if abs(rect[1] - group[-1][1]) < y_tolerance:
# group.append(rect)
# else:
# if len(group) >= 3:
# groups.append(group)
# group = [rect]
# if len(group) >= 3:
# groups.append(group)
# # Use the largest group
# if groups:
# best_group = max(groups, key=len)
# xs = [r[0] for r in best_group] + [r[0] + r[2] for r in best_group]
# ys = [r[1] for r in best_group] + [r[1] + r[3] for r in best_group]
# x1, x2 = min(xs), max(xs)
# y1, y2 = min(ys), max(ys)
# crosswalk_bbox = (x1, y1, x2 - x1, y2 - y1)
# violation_line_y = min(y2 + 5, h - 1) # Place just before crosswalk
# # Draw crosswalk region
# overlay = orig_frame.copy()
# cv2.rectangle(overlay, (x1, y1), (x2, y2), (0, 255, 0), -1)
# orig_frame = cv2.addWeighted(overlay, 0.2, orig_frame, 0.8, 0)
# cv2.rectangle(orig_frame, (x1, y1), (x2, y2), (0, 255, 0), 2)
# cv2.putText(orig_frame, "Crosswalk", (10, y1 - 10),
# cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
# # --- Fallback: Stop line detection ---
# if crosswalk_bbox is None:
# edges = cv2.Canny(gray, 80, 200)
# lines = cv2.HoughLinesP(edges, 1, np.pi / 180, threshold=80, minLineLength=60, maxLineGap=20)
# stop_lines = []
# if lines is not None:
# for l in lines:
# x1, y1, x2, y2 = l[0]
# angle = np.degrees(np.arctan2(y2 - y1, x2 - x1))
# if abs(angle) < 20 or abs(angle) > 160: # horizontal
# if y1 > h // 2 or y2 > h // 2: # lower half
# stop_lines.append((x1, y1, x2, y2))
# if stop_lines:
# best_line = max(stop_lines, key=lambda l: max(l[1], l[3]))
# x1, y1, x2, y2 = best_line
# violation_line_y = min(y1, y2) - 5
# cv2.line(orig_frame, (0, violation_line_y), (w, violation_line_y), (0, 255, 255), 8)
# cv2.putText(orig_frame, "Fallback Stop Line", (10, violation_line_y - 10),
# cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2)
# else:
# # Final fallback: bottom third
# violation_line_y = int(h * 0.75)
# cv2.line(orig_frame, (0, violation_line_y), (w, violation_line_y), (0, 0, 255), 3)
# cv2.putText(orig_frame, "Default Violation Line", (10, violation_line_y - 10),
# cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
# # Always draw the violation line if found
# if violation_line_y is not None and crosswalk_bbox is not None:
# cv2.line(orig_frame, (0, violation_line_y), (w, violation_line_y), (0, 0, 255), 3)
# cv2.putText(orig_frame, "Violation Line", (10, violation_line_y - 10),
# cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
# debug_info['crosswalk_bbox'] = crosswalk_bbox
# debug_info['violation_line_y'] = violation_line_y
# return orig_frame, crosswalk_bbox, violation_line_y, debug_info
# def draw_violation_line(frame: np.ndarray, y: int, color=(0, 0, 255), thickness=4, style='solid', label='Violation Line'):
# h, w = frame.shape[:2]
# x1, x2 = 0, w
# overlay = frame.copy()
# if style == 'dashed':
# dash_len = 30
# gap = 20
# for x in range(x1, x2, dash_len + gap):
# x_end = min(x + dash_len, x2)
# cv2.line(overlay, (x, y), (x_end, y), color, thickness, lineType=cv2.LINE_AA)
# else:
# cv2.line(overlay, (x1, y), (x2, y), color, thickness, lineType=cv2.LINE_AA)
# cv2.addWeighted(overlay, 0.7, frame, 0.3, 0, frame)
# if label:
# font = cv2.FONT_HERSHEY_SIMPLEX
# text_size, _ = cv2.getTextSize(label, font, 0.8, 2)
# text_x = max(10, (w - text_size[0]) // 2)
# text_y = max(0, y - 12)
# cv2.rectangle(frame, (text_x - 5, text_y - text_size[1] - 5), (text_x + text_size[0] + 5, text_y + 5), (0,0,0), -1)
# cv2.putText(frame, label, (text_x, text_y), font, 0.8, color, 2, cv2.LINE_AA)
# return frame
# def get_violation_line_y(frame, traffic_light_bbox=None, crosswalk_bbox=None):
# """
# Returns the y-coordinate of the violation line using the following priority:
# 1. Crosswalk bbox (most accurate)
# 2. Stop line detection via image processing (CV)
# 3. Traffic light bbox heuristic
# 4. Fallback (default)
# """
# height, width = frame.shape[:2]
# # 1. Crosswalk bbox
# if crosswalk_bbox is not None and len(crosswalk_bbox) == 4:
# return int(crosswalk_bbox[1]) - 15
# # 2. Stop line detection (CV)
# roi_height = int(height * 0.4)
# roi_y = height - roi_height
# roi = frame[roi_y:height, 0:width]
# gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
# binary = cv2.adaptiveThreshold(
# gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
# cv2.THRESH_BINARY, 15, -2
# )
# kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (15, 1))
# processed = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)
# contours, _ = cv2.findContours(processed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# stop_line_candidates = []
# for cnt in contours:
# x, y, w, h = cv2.boundingRect(cnt)
# aspect_ratio = w / max(h, 1)
# normalized_width = w / width
# if (aspect_ratio > 5 and normalized_width > 0.3 and h < 15 and y > roi_height * 0.5):
# abs_y = y + roi_y
# stop_line_candidates.append((abs_y, w))
# if stop_line_candidates:
# stop_line_candidates.sort(key=lambda x: x[1], reverse=True)
# return stop_line_candidates[0][0]
# # 3. Traffic light bbox heuristic
# if traffic_light_bbox is not None and len(traffic_light_bbox) == 4:
# traffic_light_bottom = traffic_light_bbox[3]
# traffic_light_height = traffic_light_bbox[3] - traffic_light_bbox[1]
# estimated_distance = min(5 * traffic_light_height, height * 0.3)
# return min(int(traffic_light_bottom + estimated_distance), height - 20)
# # 4. Fallback
# return int(height * 0.75)
# # Example usage:
# # bbox, vline, dbg = detect_crosswalk_and_violation_line(frame, (tl_x, tl_y), perspective_M)
print("🟡 [CROSSWALK_UTILS]222 This is d:/Downloads/finale6/Khatam final/khatam/qt_app_pyside/utils/crosswalk_utils.py LOADED")
import cv2
import numpy as np
from typing import Tuple, Optional
def detect_crosswalk_and_violation_line(frame: np.ndarray, traffic_light_position: Optional[Tuple[int, int]] = None, perspective_M: Optional[np.ndarray] = None):
"""
Detects crosswalk (zebra crossing) or fallback stop line in a traffic scene using classical CV.
Args:
frame: BGR image frame from video feed
traffic_light_position: Optional (x, y) of traffic light in frame
perspective_M: Optional 3x3 homography matrix for bird's eye view normalization
Returns:
result_frame: frame with overlays (for visualization)
crosswalk_bbox: (x, y, w, h) or None if fallback used
violation_line_y: int (y position for violation check)
debug_info: dict (for visualization/debugging)
"""
debug_info = {}
orig_frame = frame.copy()
h, w = frame.shape[:2]
# 1. Perspective Normalization (Bird's Eye View)
if perspective_M is not None:
frame = cv2.warpPerspective(frame, perspective_M, (w, h))
debug_info['perspective_warped'] = True
else:
debug_info['perspective_warped'] = False
# 1. White Color Filtering (relaxed)
mask_white = cv2.inRange(frame, (160, 160, 160), (255, 255, 255))
debug_info['mask_white_ratio'] = np.sum(mask_white > 0) / (h * w)
# 2. Grayscale for adaptive threshold
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# Enhance contrast for night/low-light
if np.mean(gray) < 80:
gray = cv2.equalizeHist(gray)
debug_info['hist_eq'] = True
else:
debug_info['hist_eq'] = False
# 5. Adaptive threshold (tuned)
thresh = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY, 15, 5)
# Combine with color mask
combined = cv2.bitwise_and(thresh, mask_white)
# 2. Morphology (tuned)
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (25, 3))
morph = cv2.morphologyEx(combined, cv2.MORPH_CLOSE, kernel, iterations=1)
# Find contours
contours, _ = cv2.findContours(morph, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
zebra_rects = []
for cnt in contours:
x, y, w, h = cv2.boundingRect(cnt)
aspect_ratio = w / max(h, 1)
area = w * h
angle = 0 # For simplicity, assume horizontal stripes
# Heuristic: wide, short, and not too small
if aspect_ratio > 3 and 1000 < area < 0.5 * frame.shape[0] * frame.shape[1] and h < 60:
zebra_rects.append((x, y, w, h, angle))
cv2.rectangle(orig_frame, (x, y), (x+w, y+h), (0, 255, 0), 2)
# --- Overlay drawing for debugging: draw all zebra candidates ---
for r in zebra_rects:
x, y, rw, rh, _ = r
cv2.rectangle(orig_frame, (x, y), (x+rw, y+rh), (0, 255, 0), 2)
# Draw all zebra candidate rectangles for debugging (no saving)
for r in zebra_rects:
x, y, rw, rh, _ = r
cv2.rectangle(orig_frame, (x, y), (x+rw, y+rh), (0, 255, 0), 2)
# --- Probabilistic Scoring for Groups ---
def group_score(group):
if len(group) < 3:
return 0
heights = [r[3] for r in group]
x_centers = [r[0] + r[2]//2 for r in group]
angles = [r[4] for r in group]
# Stripe count (normalized)
count_score = min(len(group) / 6, 1.0)
# Height consistency
height_score = 1.0 - min(np.std(heights) / (np.mean(heights) + 1e-6), 1.0)
# X-center alignment
x_score = 1.0 - min(np.std(x_centers) / (w * 0.2), 1.0)
# Angle consistency (prefer near 0 or 90)
mean_angle = np.mean([abs(a) for a in angles])
angle_score = 1.0 - min(np.std(angles) / 10.0, 1.0)
# Whiteness (mean mask_white in group area)
whiteness = 0
for r in group:
x, y, rw, rh, _ = r
whiteness += np.mean(mask_white[y:y+rh, x:x+rw]) / 255
whiteness_score = whiteness / len(group)
# Final score (weighted sum)
score = 0.25*count_score + 0.2*height_score + 0.2*x_score + 0.15*angle_score + 0.2*whiteness_score
return score
# 4. Dynamic grouping tolerance
y_tolerance = int(h * 0.05)
crosswalk_bbox = None
violation_line_y = None
best_score = 0
best_group = None
if len(zebra_rects) >= 3:
zebra_rects = sorted(zebra_rects, key=lambda r: r[1])
groups = []
group = [zebra_rects[0]]
for rect in zebra_rects[1:]:
if abs(rect[1] - group[-1][1]) < y_tolerance:
group.append(rect)
else:
if len(group) >= 3:
groups.append(group)
group = [rect]
if len(group) >= 3:
groups.append(group)
# Score all groups
scored_groups = [(group_score(g), g) for g in groups if group_score(g) > 0.1]
print(f"[CROSSWALK DEBUG] scored_groups: {[s for s, _ in scored_groups]}")
if scored_groups:
scored_groups.sort(reverse=True, key=lambda x: x[0])
best_score, best_group = scored_groups[0]
print("Best group score:", best_score)
# Visualization for debugging
debug_vis = orig_frame.copy()
for r in zebra_rects:
x, y, rw, rh, _ = r
cv2.rectangle(debug_vis, (x, y), (x+rw, y+rh), (255, 0, 255), 2)
for r in best_group:
x, y, rw, rh, _ = r
cv2.rectangle(debug_vis, (x, y), (x+rw, y+rh), (0, 255, 255), 3)
cv2.imwrite(f"debug_crosswalk_group.png", debug_vis)
# Optionally, filter by vanishing point as before
# ...existing vanishing point code...
xs = [r[0] for r in best_group] + [r[0] + r[2] for r in best_group]
ys = [r[1] for r in best_group] + [r[1] + r[3] for r in best_group]
x1, x2 = min(xs), max(xs)
y1, y2 = min(ys), max(ys)
crosswalk_bbox = (x1, y1, x2 - x1, y2 - y1)
violation_line_y = y2 - 5
debug_info['crosswalk_group'] = best_group
debug_info['crosswalk_score'] = best_score
debug_info['crosswalk_angles'] = [r[4] for r in best_group]
# --- Fallback: Stop line detection ---
if crosswalk_bbox is None:
edges = cv2.Canny(gray, 80, 200)
lines = cv2.HoughLinesP(edges, 1, np.pi / 180, threshold=80, minLineLength=60, maxLineGap=20)
stop_lines = []
if lines is not None:
for l in lines:
x1, y1, x2, y2 = l[0]
angle = np.degrees(np.arctan2(y2 - y1, x2 - x1))
if abs(angle) < 20 or abs(angle) > 160: # horizontal
if y1 > h // 2 or y2 > h // 2: # lower half
stop_lines.append((x1, y1, x2, y2))
debug_info['stop_lines'] = stop_lines
print(f"[CROSSWALK DEBUG] stop_lines: {len(stop_lines)} found")
if stop_lines:
if traffic_light_position:
tx, ty = traffic_light_position
best_line = min(stop_lines, key=lambda l: abs(((l[1]+l[3])//2) - ty))
else:
best_line = max(stop_lines, key=lambda l: max(l[1], l[3]))
x1, y1, x2, y2 = best_line
crosswalk_bbox = None
violation_line_y = min(y1, y2) - 5
debug_info['stop_line'] = best_line
print(f"[CROSSWALK DEBUG] using stop_line: {best_line}")
# Draw fallback violation line overlay for debugging (no saving)
if crosswalk_bbox is None and violation_line_y is not None:
print(f"[DEBUG] Drawing violation line at y={violation_line_y} (frame height={orig_frame.shape[0]})")
if 0 <= violation_line_y < orig_frame.shape[0]:
orig_frame = draw_violation_line(orig_frame, violation_line_y, color=(0, 255, 255), thickness=8, style='solid', label='Fallback Stop Line')
else:
print(f"[WARNING] Invalid violation line position: {violation_line_y}")
# --- Manual overlay for visualization pipeline test ---
# Removed fake overlays that could overwrite the real violation line
print(f"[CROSSWALK DEBUG] crosswalk_bbox: {crosswalk_bbox}, violation_line_y: {violation_line_y}")
return orig_frame, crosswalk_bbox, violation_line_y, debug_info
def draw_violation_line(frame: np.ndarray, y: int, color=(0, 255, 255), thickness=8, style='solid', label='Violation Line'):
"""
Draws a thick, optionally dashed, labeled violation line at the given y-coordinate.
Args:
frame: BGR image
y: y-coordinate for the line
color: BGR color tuple
thickness: line thickness
style: 'solid' or 'dashed'
label: Optional label to draw above the line
Returns:
frame with line overlay
"""
import cv2
h, w = frame.shape[:2]
x1, x2 = 0, w
overlay = frame.copy()
if style == 'dashed':
dash_len = 30
gap = 20
for x in range(x1, x2, dash_len + gap):
x_end = min(x + dash_len, x2)
cv2.line(overlay, (x, y), (x_end, y), color, thickness, lineType=cv2.LINE_AA)
else:
cv2.line(overlay, (x1, y), (x2, y), color, thickness, lineType=cv2.LINE_AA)
# Blend for semi-transparency
cv2.addWeighted(overlay, 0.7, frame, 0.3, 0, frame)
# Draw label
if label:
font = cv2.FONT_HERSHEY_SIMPLEX
text_size, _ = cv2.getTextSize(label, font, 0.8, 2)
text_x = max(10, (w - text_size[0]) // 2)
text_y = max(0, y - 12)
cv2.rectangle(frame, (text_x - 5, text_y - text_size[1] - 5), (text_x + text_size[0] + 5, text_y + 5), (0,0,0), -1)
cv2.putText(frame, label, (text_x, text_y), font, 0.8, color, 2, cv2.LINE_AA)
return frame
def get_violation_line_y(frame, traffic_light_bbox=None, crosswalk_bbox=None):
"""
Returns the y-coordinate of the violation line using the following priority:
1. Crosswalk bbox (most accurate)
2. Stop line detection via image processing (CV)
3. Traffic light bbox heuristic
4. Fallback (default)
"""
height, width = frame.shape[:2]
# 1. Crosswalk bbox
if crosswalk_bbox is not None and len(crosswalk_bbox) == 4:
return int(crosswalk_bbox[1]) - 15
# 2. Stop line detection (CV)
roi_height = int(height * 0.4)
roi_y = height - roi_height
roi = frame[roi_y:height, 0:width]
gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
binary = cv2.adaptiveThreshold(
gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY, 15, -2
)
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (15, 1))
processed = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)
contours, _ = cv2.findContours(processed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
stop_line_candidates = []
for cnt in contours:
x, y, w, h = cv2.boundingRect(cnt)
aspect_ratio = w / max(h, 1)
normalized_width = w / width
if (aspect_ratio > 5 and normalized_width > 0.3 and h < 15 and y > roi_height * 0.5):
abs_y = y + roi_y
stop_line_candidates.append((abs_y, w))
if stop_line_candidates:
stop_line_candidates.sort(key=lambda x: x[1], reverse=True)
return stop_line_candidates[0][0]
# 3. Traffic light bbox heuristic
if traffic_light_bbox is not None and len(traffic_light_bbox) == 4:
traffic_light_bottom = traffic_light_bbox[3]
traffic_light_height = traffic_light_bbox[3] - traffic_light_bbox[1]
estimated_distance = min(5 * traffic_light_height, height * 0.3)
return min(int(traffic_light_bottom + estimated_distance), height - 20)
# 4. Fallback
return int(height * 0.75)
# Example usage:
# bbox, vline, dbg = detect_crosswalk_and_violation_line(frame, (tl_x, tl_y), perspective_M)

View File

@@ -0,0 +1,649 @@
print("🟡 [CROSSWALK_UTILS]1111 This is d:/Downloads/finale6/Khatam final/khatam/qt_app_pyside/utils/crosswalk_utils.py LOADED")
import cv2
import numpy as np
from typing import Tuple, Optional
def detect_crosswalk_and_violation_line(frame: np.ndarray, traffic_light_position: Optional[Tuple[int, int]] = None, perspective_M: Optional[np.ndarray] = None):
"""
Detects crosswalk (zebra crossing) or fallback stop line in a traffic scene using classical CV.
Args:
frame: BGR image frame from video feed
traffic_light_position: Optional (x, y) of traffic light in frame
perspective_M: Optional 3x3 homography matrix for bird's eye view normalization
Returns:
result_frame: frame with overlays (for visualization)
crosswalk_bbox: (x, y, w, h) or None if fallback used
violation_line_y: int (y position for violation check)
debug_info: dict (for visualization/debugging)
"""
debug_info = {}
orig_frame = frame.copy()
h, w = frame.shape[:2]
# 1. Perspective Normalization (Bird's Eye View)
if perspective_M is not None:
frame = cv2.warpPerspective(frame, perspective_M, (w, h))
debug_info['perspective_warped'] = True
else:
debug_info['perspective_warped'] = False
# 1. White Color Filtering (relaxed)
mask_white = cv2.inRange(frame, (160, 160, 160), (255, 255, 255))
debug_info['mask_white_ratio'] = np.sum(mask_white > 0) / (h * w)
# 2. Grayscale for adaptive threshold
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# Enhance contrast for night/low-light
if np.mean(gray) < 80:
gray = cv2.equalizeHist(gray)
debug_info['hist_eq'] = True
else:
debug_info['hist_eq'] = False
# 5. Adaptive threshold (tuned)
thresh = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY, 15, 5)
# Combine with color mask
combined = cv2.bitwise_and(thresh, mask_white)
# 2. Morphology (tuned)
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (25, 3))
morph = cv2.morphologyEx(combined, cv2.MORPH_CLOSE, kernel, iterations=1)
# Find contours
contours, _ = cv2.findContours(morph, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
zebra_rects = []
for cnt in contours:
x, y, w, h = cv2.boundingRect(cnt)
aspect_ratio = w / max(h, 1)
area = w * h
angle = 0 # For simplicity, assume horizontal stripes
# Heuristic: wide, short, and not too small
if aspect_ratio > 3 and 1000 < area < 0.5 * frame.shape[0] * frame.shape[1] and h < 60:
zebra_rects.append((x, y, w, h, angle))
cv2.rectangle(orig_frame, (x, y), (x+w, y+h), (0, 255, 0), 2)
# --- Overlay drawing for debugging: draw all zebra candidates ---
for r in zebra_rects:
x, y, rw, rh, _ = r
cv2.rectangle(orig_frame, (x, y), (x+rw, y+rh), (0, 255, 0), 2)
# Draw all zebra candidate rectangles for debugging (no saving)
for r in zebra_rects:
x, y, rw, rh, _ = r
cv2.rectangle(orig_frame, (x, y), (x+rw, y+rh), (0, 255, 0), 2)
# --- Probabilistic Scoring for Groups ---
def group_score(group):
if len(group) < 3:
return 0
heights = [r[3] for r in group]
x_centers = [r[0] + r[2]//2 for r in group]
angles = [r[4] for r in group]
# Stripe count (normalized)
count_score = min(len(group) / 6, 1.0)
# Height consistency
height_score = 1.0 - min(np.std(heights) / (np.mean(heights) + 1e-6), 1.0)
# X-center alignment
x_score = 1.0 - min(np.std(x_centers) / (w * 0.2), 1.0)
# Angle consistency (prefer near 0 or 90)
mean_angle = np.mean([abs(a) for a in angles])
angle_score = 1.0 - min(np.std(angles) / 10.0, 1.0)
# Whiteness (mean mask_white in group area)
whiteness = 0
for r in group:
x, y, rw, rh, _ = r
whiteness += np.mean(mask_white[y:y+rh, x:x+rw]) / 255
whiteness_score = whiteness / len(group)
# Final score (weighted sum)
score = 0.25*count_score + 0.2*height_score + 0.2*x_score + 0.15*angle_score + 0.2*whiteness_score
return score
# 4. Dynamic grouping tolerance
y_tolerance = int(h * 0.05)
crosswalk_bbox = None
violation_line_y = None
best_score = 0
best_group = None
if len(zebra_rects) >= 3:
zebra_rects = sorted(zebra_rects, key=lambda r: r[1])
groups = []
group = [zebra_rects[0]]
for rect in zebra_rects[1:]:
if abs(rect[1] - group[-1][1]) < y_tolerance:
group.append(rect)
else:
if len(group) >= 3:
groups.append(group)
group = [rect]
if len(group) >= 3:
groups.append(group)
# Score all groups
scored_groups = [(group_score(g), g) for g in groups if group_score(g) > 0.1]
print(f"[CROSSWALK DEBUG] scored_groups: {[s for s, _ in scored_groups]}")
if scored_groups:
scored_groups.sort(reverse=True, key=lambda x: x[0])
best_score, best_group = scored_groups[0]
print("Best group score:", best_score)
# Visualization for debugging
debug_vis = orig_frame.copy()
for r in zebra_rects:
x, y, rw, rh, _ = r
cv2.rectangle(debug_vis, (x, y), (x+rw, y+rh), (255, 0, 255), 2)
for r in best_group:
x, y, rw, rh, _ = r
cv2.rectangle(debug_vis, (x, y), (x+rw, y+rh), (0, 255, 255), 3)
cv2.imwrite(f"debug_crosswalk_group.png", debug_vis)
# Optionally, filter by vanishing point as before
# ...existing vanishing point code...
xs = [r[0] for r in best_group] + [r[0] + r[2] for r in best_group]
ys = [r[1] for r in best_group] + [r[1] + r[3] for r in best_group]
x1, x2 = min(xs), max(xs)
y1, y2 = min(ys), max(ys)
crosswalk_bbox = (x1, y1, x2 - x1, y2 - y1)
violation_line_y = y2 - 5
debug_info['crosswalk_group'] = best_group
debug_info['crosswalk_score'] = best_score
debug_info['crosswalk_angles'] = [r[4] for r in best_group]
# --- Fallback: Stop line detection ---
if crosswalk_bbox is None:
edges = cv2.Canny(gray, 80, 200)
lines = cv2.HoughLinesP(edges, 1, np.pi / 180, threshold=80, minLineLength=60, maxLineGap=20)
stop_lines = []
if lines is not None:
for l in lines:
x1, y1, x2, y2 = l[0]
angle = np.degrees(np.arctan2(y2 - y1, x2 - x1))
if abs(angle) < 20 or abs(angle) > 160: # horizontal
if y1 > h // 2 or y2 > h // 2: # lower half
stop_lines.append((x1, y1, x2, y2))
debug_info['stop_lines'] = stop_lines
print(f"[CROSSWALK DEBUG] stop_lines: {len(stop_lines)} found")
if stop_lines:
if traffic_light_position:
tx, ty = traffic_light_position
best_line = min(stop_lines, key=lambda l: abs(((l[1]+l[3])//2) - ty))
else:
best_line = max(stop_lines, key=lambda l: max(l[1], l[3]))
x1, y1, x2, y2 = best_line
crosswalk_bbox = None
violation_line_y = min(y1, y2) - 5
debug_info['stop_line'] = best_line
print(f"[CROSSWALK DEBUG] using stop_line: {best_line}")
# Draw fallback violation line overlay for debugging (no saving)
if crosswalk_bbox is None and violation_line_y is not None:
print(f"[DEBUG] Drawing violation line at y={violation_line_y} (frame height={orig_frame.shape[0]})")
if 0 <= violation_line_y < orig_frame.shape[0]:
orig_frame = draw_violation_line(orig_frame, violation_line_y, color=(0, 255, 255), thickness=8, style='solid', label='Fallback Stop Line')
else:
print(f"[WARNING] Invalid violation line position: {violation_line_y}")
# --- Manual overlay for visualization pipeline test ---
# Removed fake overlays that could overwrite the real violation line
print(f"[CROSSWALK DEBUG] crosswalk_bbox: {crosswalk_bbox}, violation_line_y: {violation_line_y}")
return orig_frame, crosswalk_bbox, violation_line_y, debug_info
def draw_violation_line(frame: np.ndarray, y: int, color=(0, 255, 255), thickness=8, style='solid', label='Violation Line'):
"""
Draws a thick, optionally dashed, labeled violation line at the given y-coordinate.
Args:
frame: BGR image
y: y-coordinate for the line
color: BGR color tuple
thickness: line thickness
style: 'solid' or 'dashed'
label: Optional label to draw above the line
Returns:
frame with line overlay
"""
import cv2
h, w = frame.shape[:2]
x1, x2 = 0, w
overlay = frame.copy()
if style == 'dashed':
dash_len = 30
gap = 20
for x in range(x1, x2, dash_len + gap):
x_end = min(x + dash_len, x2)
cv2.line(overlay, (x, y), (x_end, y), color, thickness, lineType=cv2.LINE_AA)
else:
cv2.line(overlay, (x1, y), (x2, y), color, thickness, lineType=cv2.LINE_AA)
# Blend for semi-transparency
cv2.addWeighted(overlay, 0.7, frame, 0.3, 0, frame)
# Draw label
if label:
font = cv2.FONT_HERSHEY_SIMPLEX
text_size, _ = cv2.getTextSize(label, font, 0.8, 2)
text_x = max(10, (w - text_size[0]) // 2)
text_y = max(0, y - 12)
cv2.rectangle(frame, (text_x - 5, text_y - text_size[1] - 5), (text_x + text_size[0] + 5, text_y + 5), (0,0,0), -1)
cv2.putText(frame, label, (text_x, text_y), font, 0.8, color, 2, cv2.LINE_AA)
return frame
def get_violation_line_y(frame, traffic_light_bbox=None, crosswalk_bbox=None):
"""
Returns the y-coordinate of the violation line using the following priority:
1. Crosswalk bbox (most accurate)
2. Stop line detection via image processing (CV)
3. Traffic light bbox heuristic
4. Fallback (default)
"""
height, width = frame.shape[:2]
# 1. Crosswalk bbox
if crosswalk_bbox is not None and len(crosswalk_bbox) == 4:
return int(crosswalk_bbox[1]) - 15
# 2. Stop line detection (CV)
roi_height = int(height * 0.4)
roi_y = height - roi_height
roi = frame[roi_y:height, 0:width]
gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
binary = cv2.adaptiveThreshold(
gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY, 15, -2
)
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (15, 1))
processed = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)
contours, _ = cv2.findContours(processed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
stop_line_candidates = []
for cnt in contours:
x, y, w, h = cv2.boundingRect(cnt)
aspect_ratio = w / max(h, 1)
normalized_width = w / width
if (aspect_ratio > 5 and normalized_width > 0.3 and h < 15 and y > roi_height * 0.5):
abs_y = y + roi_y
stop_line_candidates.append((abs_y, w))
if stop_line_candidates:
stop_line_candidates.sort(key=lambda x: x[1], reverse=True)
return stop_line_candidates[0][0]
# 3. Traffic light bbox heuristic
if traffic_light_bbox is not None and len(traffic_light_bbox) == 4:
traffic_light_bottom = traffic_light_bbox[3]
traffic_light_height = traffic_light_bbox[3] - traffic_light_bbox[1]
estimated_distance = min(5 * traffic_light_height, height * 0.3)
return min(int(traffic_light_bottom + estimated_distance), height - 20)
# 4. Fallback
return int(height * 0.75)
# Example usage:
# bbox, vline, dbg = detect_crosswalk_and_violation_line(frame, (tl_x, tl_y), perspective_M)
##working
print("🟡 [CROSSWALK_UTILS] This is d:/Downloads/finale6/Khatam final/khatam/qt_app_pyside/utils/crosswalk_utils.py LOADED")
import cv2
import numpy as np
from sklearn import linear_model
def detect_crosswalk_and_violation_line(frame, traffic_light_position=None, debug=False):
"""
Robust crosswalk and violation line detection for red-light violation system.
Returns:
frame_with_overlays, crosswalk_bbox, violation_line_y, debug_info
"""
frame_out = frame.copy()
h, w = frame.shape[:2]
debug_info = {}
# === Step 1: Robust white color mask (HSV) ===
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
lower_white = np.array([0, 0, 180])
upper_white = np.array([180, 80, 255])
mask = cv2.inRange(hsv, lower_white, upper_white)
# === Step 2: Morphological filtering ===
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (7, 3))
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
# === Step 3: Contour extraction and filtering ===
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
crosswalk_bars = []
for cnt in contours:
x, y, cw, ch = cv2.boundingRect(cnt)
if cw > w * 0.05 and ch < h * 0.15:
crosswalk_bars.append((x, y, cw, ch))
# === Step 4: Draw detected bars for debug ===
for (x, y, cw, ch) in crosswalk_bars:
cv2.rectangle(frame_out, (x, y), (x + cw, y + ch), (0, 255, 255), 2) # yellow
# === Step 5: Violation line placement at bottom of bars ===
ys = np.array([y for (x, y, w, h) in crosswalk_bars])
hs = np.array([h for (x, y, w, h) in crosswalk_bars])
if len(ys) >= 3:
bottom_edges = ys + hs
violation_line_y = int(np.max(bottom_edges)) + 5 # +5 offset
violation_line_y = min(violation_line_y, h - 1)
crosswalk_bbox = (0, int(np.min(ys)), w, int(np.max(bottom_edges)) - int(np.min(ys)))
# Draw semi-transparent crosswalk region
overlay = frame_out.copy()
cv2.rectangle(overlay, (0, int(np.min(ys))), (w, int(np.max(bottom_edges))), (0, 255, 0), -1)
frame_out = cv2.addWeighted(overlay, 0.2, frame_out, 0.8, 0)
cv2.rectangle(frame_out, (0, int(np.min(ys))), (w, int(np.max(bottom_edges))), (0, 255, 0), 2)
cv2.putText(frame_out, "Crosswalk", (10, int(np.min(ys)) - 10),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
else:
violation_line_y = int(h * 0.65)
crosswalk_bbox = None
# === Draw violation line ===
cv2.line(frame_out, (0, violation_line_y), (w, violation_line_y), (0, 0, 255), 3)
cv2.putText(frame_out, "Violation Line", (10, violation_line_y - 10),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
debug_info['crosswalk_bars'] = crosswalk_bars
debug_info['violation_line_y'] = violation_line_y
debug_info['crosswalk_bbox'] = crosswalk_bbox
return frame_out, crosswalk_bbox, violation_line_y, debug_info
def draw_violation_line(frame: np.ndarray, y: int, color=(0, 0, 255), thickness=4, style='solid', label='Violation Line'):
h, w = frame.shape[:2]
x1, x2 = 0, w
overlay = frame.copy()
if style == 'dashed':
dash_len = 30
gap = 20
for x in range(x1, x2, dash_len + gap):
x_end = min(x + dash_len, x2)
cv2.line(overlay, (x, y), (x_end, y), color, thickness, lineType=cv2.LINE_AA)
else:
cv2.line(overlay, (x1, y), (x2, y), color, thickness, lineType=cv2.LINE_AA)
cv2.addWeighted(overlay, 0.7, frame, 0.3, 0, frame)
if label:
font = cv2.FONT_HERSHEY_SIMPLEX
text_size, _ = cv2.getTextSize(label, font, 0.8, 2)
text_x = max(10, (w - text_size[0]) // 2)
text_y = max(0, y - 12)
cv2.rectangle(frame, (text_x - 5, text_y - text_size[1] - 5), (text_x + text_size[0] + 5, text_y + 5), (0,0,0), -1)
cv2.putText(frame, label, (text_x, text_y), font, 0.8, color, 2, cv2.LINE_AA)
return frame
def get_violation_line_y(frame, traffic_light_bbox=None, crosswalk_bbox=None):
"""
Returns the y-coordinate of the violation line using the following priority:
1. Crosswalk bbox (most accurate)
2. Stop line detection via image processing (CV)
3. Traffic light bbox heuristic
4. Fallback (default)
"""
height, width = frame.shape[:2]
# 1. Crosswalk bbox
if crosswalk_bbox is not None and len(crosswalk_bbox) == 4:
return int(crosswalk_bbox[1]) - 15
# 2. Stop line detection (CV)
roi_height = int(height * 0.4)
roi_y = height - roi_height
roi = frame[roi_y:height, 0:width]
gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
binary = cv2.adaptiveThreshold(
gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY, 15, -2
)
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (15, 1))
processed = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)
contours, _ = cv2.findContours(processed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
stop_line_candidates = []
for cnt in contours:
x, y, w, h = cv2.boundingRect(cnt)
aspect_ratio = w / max(h, 1)
normalized_width = w / width
if (aspect_ratio > 5 and normalized_width > 0.3 and h < 15 and y > roi_height * 0.5):
abs_y = y + roi_y
stop_line_candidates.append((abs_y, w))
if stop_line_candidates:
stop_line_candidates.sort(key=lambda x: x[1], reverse=True)
return stop_line_candidates[0][0]
# 3. Traffic light bbox heuristic
if traffic_light_bbox is not None and len(traffic_light_bbox) == 4:
traffic_light_bottom = traffic_light_bbox[3]
traffic_light_height = traffic_light_bbox[3] - traffic_light_bbox[1]
estimated_distance = min(5 * traffic_light_height, height * 0.3)
return min(int(traffic_light_bottom + estimated_distance), height - 20)
# 4. Fallback
return int(height * 0.75)
# Example usage:
# bbox, vline, dbg = detect_crosswalk_and_violation_line(frame, (tl_x, tl_y), perspective_M)
print("🟡 [CROSSWALK_UTILS]222 This is d:/Downloads/finale6/Khatam final/khatam/qt_app_pyside/utils/crosswalk_utils.py LOADED")
import cv2
import numpy as np
from typing import Tuple, Optional
def detect_crosswalk_and_violation_line(frame: np.ndarray, traffic_light_position: Optional[Tuple[int, int]] = None, perspective_M: Optional[np.ndarray] = None):
"""
Detects crosswalk (zebra crossing) or fallback stop line in a traffic scene using classical CV.
Args:
frame: BGR image frame from video feed
traffic_light_position: Optional (x, y) of traffic light in frame
perspective_M: Optional 3x3 homography matrix for bird's eye view normalization
Returns:
result_frame: frame with overlays (for visualization)
crosswalk_bbox: (x, y, w, h) or None if fallback used
violation_line_y: int (y position for violation check)
debug_info: dict (for visualization/debugging)
"""
debug_info = {}
orig_frame = frame.copy()
h, w = frame.shape[:2]
# 1. Perspective Normalization (Bird's Eye View)
if perspective_M is not None:
frame = cv2.warpPerspective(frame, perspective_M, (w, h))
debug_info['perspective_warped'] = True
else:
debug_info['perspective_warped'] = False
# 1. White Color Filtering (relaxed)
mask_white = cv2.inRange(frame, (160, 160, 160), (255, 255, 255))
debug_info['mask_white_ratio'] = np.sum(mask_white > 0) / (h * w)
# 2. Grayscale for adaptive threshold
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# Enhance contrast for night/low-light
if np.mean(gray) < 80:
gray = cv2.equalizeHist(gray)
debug_info['hist_eq'] = True
else:
debug_info['hist_eq'] = False
# 5. Adaptive threshold (tuned)
thresh = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY, 15, 5)
# Combine with color mask
combined = cv2.bitwise_and(thresh, mask_white)
# 2. Morphology (tuned)
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (25, 3))
morph = cv2.morphologyEx(combined, cv2.MORPH_CLOSE, kernel, iterations=1)
# Find contours
contours, _ = cv2.findContours(morph, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
zebra_rects = []
for cnt in contours:
x, y, w, h = cv2.boundingRect(cnt)
aspect_ratio = w / max(h, 1)
area = w * h
angle = 0 # For simplicity, assume horizontal stripes
# Heuristic: wide, short, and not too small
if aspect_ratio > 3 and 1000 < area < 0.5 * frame.shape[0] * frame.shape[1] and h < 60:
zebra_rects.append((x, y, w, h, angle))
cv2.rectangle(orig_frame, (x, y), (x+w, y+h), (0, 255, 0), 2)
# --- Overlay drawing for debugging: draw all zebra candidates ---
for r in zebra_rects:
x, y, rw, rh, _ = r
cv2.rectangle(orig_frame, (x, y), (x+rw, y+rh), (0, 255, 0), 2)
# Draw all zebra candidate rectangles for debugging (no saving)
for r in zebra_rects:
x, y, rw, rh, _ = r
cv2.rectangle(orig_frame, (x, y), (x+rw, y+rh), (0, 255, 0), 2)
# --- Probabilistic Scoring for Groups ---
def group_score(group):
if len(group) < 3:
return 0
heights = [r[3] for r in group]
x_centers = [r[0] + r[2]//2 for r in group]
angles = [r[4] for r in group]
# Stripe count (normalized)
count_score = min(len(group) / 6, 1.0)
# Height consistency
height_score = 1.0 - min(np.std(heights) / (np.mean(heights) + 1e-6), 1.0)
# X-center alignment
x_score = 1.0 - min(np.std(x_centers) / (w * 0.2), 1.0)
# Angle consistency (prefer near 0 or 90)
mean_angle = np.mean([abs(a) for a in angles])
angle_score = 1.0 - min(np.std(angles) / 10.0, 1.0)
# Whiteness (mean mask_white in group area)
whiteness = 0
for r in group:
x, y, rw, rh, _ = r
whiteness += np.mean(mask_white[y:y+rh, x:x+rw]) / 255
whiteness_score = whiteness / len(group)
# Final score (weighted sum)
score = 0.25*count_score + 0.2*height_score + 0.2*x_score + 0.15*angle_score + 0.2*whiteness_score
return score
# 4. Dynamic grouping tolerance
y_tolerance = int(h * 0.05)
crosswalk_bbox = None
violation_line_y = None
best_score = 0
best_group = None
if len(zebra_rects) >= 3:
zebra_rects = sorted(zebra_rects, key=lambda r: r[1])
groups = []
group = [zebra_rects[0]]
for rect in zebra_rects[1:]:
if abs(rect[1] - group[-1][1]) < y_tolerance:
group.append(rect)
else:
if len(group) >= 3:
groups.append(group)
group = [rect]
if len(group) >= 3:
groups.append(group)
# Score all groups
scored_groups = [(group_score(g), g) for g in groups if group_score(g) > 0.1]
print(f"[CROSSWALK DEBUG] scored_groups: {[s for s, _ in scored_groups]}")
if scored_groups:
scored_groups.sort(reverse=True, key=lambda x: x[0])
best_score, best_group = scored_groups[0]
print("Best group score:", best_score)
# Visualization for debugging
debug_vis = orig_frame.copy()
for r in zebra_rects:
x, y, rw, rh, _ = r
cv2.rectangle(debug_vis, (x, y), (x+rw, y+rh), (255, 0, 255), 2)
for r in best_group:
x, y, rw, rh, _ = r
cv2.rectangle(debug_vis, (x, y), (x+rw, y+rh), (0, 255, 255), 3)
cv2.imwrite(f"debug_crosswalk_group.png", debug_vis)
# Optionally, filter by vanishing point as before
# ...existing vanishing point code...
xs = [r[0] for r in best_group] + [r[0] + r[2] for r in best_group]
ys = [r[1] for r in best_group] + [r[1] + r[3] for r in best_group]
x1, x2 = min(xs), max(xs)
y1, y2 = min(ys), max(ys)
crosswalk_bbox = (x1, y1, x2 - x1, y2 - y1)
violation_line_y = y2 - 5
debug_info['crosswalk_group'] = best_group
debug_info['crosswalk_score'] = best_score
debug_info['crosswalk_angles'] = [r[4] for r in best_group]
# --- Fallback: Stop line detection ---
if crosswalk_bbox is None:
edges = cv2.Canny(gray, 80, 200)
lines = cv2.HoughLinesP(edges, 1, np.pi / 180, threshold=80, minLineLength=60, maxLineGap=20)
stop_lines = []
if lines is not None:
for l in lines:
x1, y1, x2, y2 = l[0]
angle = np.degrees(np.arctan2(y2 - y1, x2 - x1))
if abs(angle) < 20 or abs(angle) > 160: # horizontal
if y1 > h // 2 or y2 > h // 2: # lower half
stop_lines.append((x1, y1, x2, y2))
debug_info['stop_lines'] = stop_lines
print(f"[CROSSWALK DEBUG] stop_lines: {len(stop_lines)} found")
if stop_lines:
if traffic_light_position:
tx, ty = traffic_light_position
best_line = min(stop_lines, key=lambda l: abs(((l[1]+l[3])//2) - ty))
else:
best_line = max(stop_lines, key=lambda l: max(l[1], l[3]))
x1, y1, x2, y2 = best_line
crosswalk_bbox = None
violation_line_y = min(y1, y2) - 5
debug_info['stop_line'] = best_line
print(f"[CROSSWALK DEBUG] using stop_line: {best_line}")
# Draw fallback violation line overlay for debugging (no saving)
return orig_frame, crosswalk_bbox, violation_line_y, debug_info
def draw_violation_line(frame: np.ndarray, y: int, color=(0, 0, 255), thickness=8, style='solid', label='Violation Line'):
"""
Draws a thick, optionally dashed, labeled violation line at the given y-coordinate.
Args:
frame: BGR image
y: y-coordinate for the line
color: BGR color tuple
thickness: line thickness
style: 'solid' or 'dashed'
label: Optional label to draw above the line
Returns:
frame with line overlay
"""
import cv2
h, w = frame.shape[:2]
x1, x2 = 0, w
overlay = frame.copy()
if style == 'dashed':
dash_len = 30
gap = 20
for x in range(x1, x2, dash_len + gap):
x_end = min(x + dash_len, x2)
cv2.line(overlay, (x, y), (x_end, y), color, thickness, lineType=cv2.LINE_AA)
else:
cv2.line(overlay, (x1, y), (x2, y), color, thickness, lineType=cv2.LINE_AA)
# Blend for semi-transparency
cv2.addWeighted(overlay, 0.7, frame, 0.3, 0, frame)
# Draw label
if label:
font = cv2.FONT_HERSHEY_SIMPLEX
text_size, _ = cv2.getTextSize(label, font, 0.8, 2)
text_x = max(10, (w - text_size[0]) // 2)
text_y = max(0, y - 12)
cv2.rectangle(frame, (text_x - 5, text_y - text_size[1] - 5), (text_x + text_size[0] + 5, text_y + 5), (0,0,0), -1)
cv2.putText(frame, label, (text_x, text_y), font, 0.8, color, 2, cv2.LINE_AA)
return frame
def get_violation_line_y(frame, traffic_light_bbox=None, crosswalk_bbox=None):
"""
Returns the y-coordinate of the violation line using the following priority:
1. Crosswalk bbox (most accurate)
2. Stop line detection via image processing (CV)
3. Traffic light bbox heuristic
4. Fallback (default)
"""
height, width = frame.shape[:2]
# 1. Crosswalk bbox
if crosswalk_bbox is not None and len(crosswalk_bbox) == 4:
return int(crosswalk_bbox[1]) - 15
# 2. Stop line detection (CV)
roi_height = int(height * 0.4)
roi_y = height - roi_height
roi = frame[roi_y:height, 0:width]
gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
binary = cv2.adaptiveThreshold(
gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY, 15, -2
)
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (15, 1))
processed = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)
contours, _ = cv2.findContours(processed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
stop_line_candidates = []
for cnt in contours:
x, y, w, h = cv2.boundingRect(cnt)
aspect_ratio = w / max(h, 1)
normalized_width = w / width
if (aspect_ratio > 5 and normalized_width > 0.3 and h < 15 and y > roi_height * 0.5):
abs_y = y + roi_y
stop_line_candidates.append((abs_y, w))
if stop_line_candidates:
stop_line_candidates.sort(key=lambda x: x[1], reverse=True)
return stop_line_candidates[0][0]
# 3. Traffic light bbox heuristic
if traffic_light_bbox is not None and len(traffic_light_bbox) == 4:
traffic_light_bottom = traffic_light_bbox[3]
traffic_light_height = traffic_light_bbox[3] - traffic_light_bbox[1]
estimated_distance = min(5 * traffic_light_height, height * 0.3)
return min(int(traffic_light_bottom + estimated_distance), height - 20)
# 4. Fallback
return int(height * 0.75)
# Example usage:
# bbox, vline, dbg = detect_crosswalk_and_violation_line(frame, (tl_x, tl_y), perspective_M)

View File

@@ -0,0 +1,337 @@
print("<EFBFBD> [CROSSWALK_UTILS2] This is d:/Downloads/finale6/Khatam final/khatam/qt_app_pyside/utils/crosswalk_utils2.py LOADED")
import cv2
import numpy as np
from typing import Tuple, Optional
def detect_crosswalk_and_violation_line(frame: np.ndarray, traffic_light_position: Optional[Tuple[int, int]] = None, perspective_M: Optional[np.ndarray] = None):
"""
Detects crosswalk (zebra crossing) or fallback stop line in a traffic scene using classical CV.
Args:
frame: BGR image frame from video feed
traffic_light_position: Optional (x, y) of traffic light in frame
perspective_M: Optional 3x3 homography matrix for bird's eye view normalization
Returns:
result_frame: frame with overlays (for visualization)
crosswalk_bbox: (x, y, w, h) or None if fallback used
violation_line_y: int (y position for violation check)
debug_info: dict (for visualization/debugging)
"""
# --- PROCESS CROSSWALK DETECTION REGARDLESS OF TRAFFIC LIGHT ---
print(f"[CROSSWALK DEBUG] Starting crosswalk detection. Traffic light: {traffic_light_position}")
if traffic_light_position is None:
print("[CROSSWALK DEBUG] No traffic light detected, but proceeding with crosswalk detection")
debug_info = {}
orig_frame = frame.copy()
h, w = frame.shape[:2]
# 1. Perspective Normalization (Bird's Eye View)
if perspective_M is not None:
frame = cv2.warpPerspective(frame, perspective_M, (w, h))
debug_info['perspective_warped'] = True
else:
debug_info['perspective_warped'] = False
# 1. Enhanced White Color Filtering (more permissive for zebra stripes)
mask_white = cv2.inRange(frame, (140, 140, 140), (255, 255, 255))
debug_info['mask_white_ratio'] = np.sum(mask_white > 0) / (h * w)
# 2. Grayscale for adaptive threshold
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# Enhance contrast for night/low-light
if np.mean(gray) < 80:
gray = cv2.equalizeHist(gray)
debug_info['hist_eq'] = True
else:
debug_info['hist_eq'] = False
# 3. Adaptive threshold (more permissive)
thresh = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY, 11, 3)
# Combine with color mask
combined = cv2.bitwise_and(thresh, mask_white)
# 4. Better morphology for zebra stripe detection
# Horizontal kernel to connect zebra stripes
kernel_h = cv2.getStructuringElement(cv2.MORPH_RECT, (15, 3))
morph = cv2.morphologyEx(combined, cv2.MORPH_CLOSE, kernel_h, iterations=1)
# Vertical kernel to separate stripes
kernel_v = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 5))
morph = cv2.morphologyEx(morph, cv2.MORPH_OPEN, kernel_v, iterations=1)
# Find contours
contours, _ = cv2.findContours(morph, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
zebra_rects = []
# Focus on lower half of frame where crosswalks typically are
roi_y_start = int(h * 0.4) # Start from 40% down
for cnt in contours:
x, y, w, h_rect = cv2.boundingRect(cnt)
# Skip if in upper part of frame
if y < roi_y_start:
continue
aspect_ratio = w / max(h_rect, 1)
area = w * h_rect
# More permissive criteria for zebra stripe detection
min_area = 300 # Smaller minimum area
max_area = 0.3 * frame.shape[0] * frame.shape[1] # Larger max area
min_aspect = 2.0 # Lower aspect ratio requirement
max_height = 40 # Allow taller stripes
if (aspect_ratio > min_aspect and
min_area < area < max_area and
h_rect < max_height and
w > 50): # Minimum width for zebra stripe
angle = 0 # For simplicity, assume horizontal stripes
zebra_rects.append((x, y, w, h_rect, angle))
print(f"[CROSSWALK DEBUG] Found {len(zebra_rects)} zebra stripe candidates")
# --- Enhanced Grouping and Scoring for Crosswalk Detection ---
def group_score(group):
if len(group) < 2: # Reduced minimum requirement
return 0
heights = [r[3] for r in group]
x_centers = [r[0] + r[2]//2 for r in group]
y_centers = [r[1] + r[3]//2 for r in group]
# Stripe count (normalized) - more permissive
count_score = min(len(group) / 4, 1.0) # Reduced from 6 to 4
# Height consistency
if len(heights) > 1:
height_score = 1.0 - min(np.std(heights) / (np.mean(heights) + 1e-6), 1.0)
else:
height_score = 0.5
# Horizontal alignment (zebra stripes should be roughly aligned)
if len(y_centers) > 1:
y_score = 1.0 - min(np.std(y_centers) / (h * 0.1), 1.0)
else:
y_score = 0.5
# Regular spacing between stripes
if len(group) >= 3:
x_sorted = sorted([r[0] for r in group])
gaps = [x_sorted[i+1] - x_sorted[i] for i in range(len(x_sorted)-1)]
gap_consistency = 1.0 - min(np.std(gaps) / (np.mean(gaps) + 1e-6), 1.0)
else:
gap_consistency = 0.3
# Area coverage (zebra crossing should cover reasonable area)
total_area = sum(r[2] * r[3] for r in group)
area_score = min(total_area / (w * h * 0.05), 1.0) # At least 5% of frame
# Final score (weighted sum)
score = (0.3*count_score + 0.2*height_score + 0.2*y_score +
0.15*gap_consistency + 0.15*area_score)
return score
# 4. More flexible grouping
crosswalk_bbox = None
violation_line_y = None
if len(zebra_rects) >= 2: # Reduced minimum requirement from 3 to 2
# Sort by y-coordinate for grouping
zebra_rects = sorted(zebra_rects, key=lambda r: r[1])
# Group stripes that are horizontally aligned
y_tolerance = int(h * 0.08) # Increased tolerance to 8%
groups = []
if zebra_rects:
group = [zebra_rects[0]]
for rect in zebra_rects[1:]:
# Check if this stripe is roughly at the same y-level as the group
group_y_avg = sum(r[1] for r in group) / len(group)
if abs(rect[1] - group_y_avg) < y_tolerance:
group.append(rect)
else:
if len(group) >= 2: # Reduced from 3 to 2
groups.append(group)
group = [rect]
# Don't forget the last group
if len(group) >= 2:
groups.append(group)
# Score all groups
scored_groups = [(group_score(g), g) for g in groups]
# More permissive threshold
scored_groups = [(s, g) for s, g in scored_groups if s > 0.05] # Reduced from 0.1
print(f"[CROSSWALK DEBUG] Found {len(groups)} potential crosswalk groups")
print(f"[CROSSWALK DEBUG] scored_groups: {[round(s, 3) for s, _ in scored_groups]}")
if scored_groups:
scored_groups.sort(reverse=True, key=lambda x: x[0])
best_score, best_group = scored_groups[0]
print(f"[CROSSWALK DEBUG] Best crosswalk group score: {best_score:.3f}")
print(f"[CROSSWALK DEBUG] Best group has {len(best_group)} stripes")
# Calculate crosswalk bounding box
xs = [r[0] for r in best_group] + [r[0] + r[2] for r in best_group]
ys = [r[1] for r in best_group] + [r[1] + r[3] for r in best_group]
x1, x2 = min(xs), max(xs)
y1, y2 = min(ys), max(ys)
crosswalk_bbox = (x1, y1, x2 - x1, y2 - y1)
# Place violation line just before the crosswalk
violation_line_y = y1 - 15 # 15 pixels before crosswalk starts
debug_info['crosswalk_group'] = best_group
debug_info['crosswalk_score'] = best_score
debug_info['crosswalk_bbox'] = crosswalk_bbox
print(f"[CROSSWALK DEBUG] CROSSWALK DETECTED at bbox: {crosswalk_bbox}")
print(f"[CROSSWALK DEBUG] Violation line at y={violation_line_y}")
else:
print("[CROSSWALK DEBUG] No valid crosswalk groups found")
# --- Fallback: Improved Stop line detection ---
if crosswalk_bbox is None:
# Enhanced edge detection for stop lines
edges = cv2.Canny(gray, 50, 150, apertureSize=3)
# Focus on lower half of frame where stop lines typically are
roi_height = int(h * 0.6) # Lower 60% of frame
roi_y = h - roi_height
roi_edges = edges[roi_y:h, :]
# Detect horizontal lines (stop lines)
lines = cv2.HoughLinesP(roi_edges, 1, np.pi / 180,
threshold=50, minLineLength=100, maxLineGap=30)
stop_lines = []
if lines is not None:
for l in lines:
x1, y1, x2, y2 = l[0]
# Convert back to full frame coordinates
y1 += roi_y
y2 += roi_y
# Check if line is horizontal (stop line characteristic)
angle = np.degrees(np.arctan2(y2 - y1, x2 - x1))
line_length = np.sqrt((x2-x1)**2 + (y2-y1)**2)
if (abs(angle) < 15 or abs(angle) > 165) and line_length > 80:
stop_lines.append((x1, y1, x2, y2))
debug_info['stop_lines'] = stop_lines
print(f"[CROSSWALK DEBUG] stop_lines: {len(stop_lines)} found")
if stop_lines:
# Choose the best stop line based on traffic light position or bottom-most line
if traffic_light_position:
tx, ty = traffic_light_position
# Find line closest to traffic light but below it
valid_lines = [l for l in stop_lines if ((l[1]+l[3])//2) > ty + 50]
if valid_lines:
best_line = min(valid_lines, key=lambda l: abs(((l[1]+l[3])//2) - (ty + 100)))
else:
best_line = min(stop_lines, key=lambda l: abs(((l[1]+l[3])//2) - ty))
else:
# Use the bottom-most horizontal line as stop line
best_line = max(stop_lines, key=lambda l: max(l[1], l[3]))
x1, y1, x2, y2 = best_line
crosswalk_bbox = None
# Place violation line slightly above the detected stop line
violation_line_y = min(y1, y2) - 10
debug_info['stop_line'] = best_line
print(f"[CROSSWALK DEBUG] using stop_line: {best_line}")
print(f"[CROSSWALK DEBUG] violation line placed at y={violation_line_y}")
# Draw violation line on the frame for visualization
result_frame = orig_frame.copy()
if violation_line_y is not None:
print(f"[CROSSWALK DEBUG] Drawing VIOLATION LINE at y={violation_line_y}")
result_frame = draw_violation_line(result_frame, violation_line_y,
color=(0, 0, 255), thickness=8,
style='solid', label='VIOLATION LINE')
return result_frame, crosswalk_bbox, violation_line_y, debug_info
def draw_violation_line(frame: np.ndarray, y: int, color=(0, 0, 255), thickness=8, style='solid', label='Violation Line'):
"""
Draws a thick, optionally dashed, labeled violation line at the given y-coordinate.
Args:
frame: BGR image
y: y-coordinate for the line
color: BGR color tuple
thickness: line thickness
style: 'solid' or 'dashed'
label: Optional label to draw above the line
Returns:
frame with line overlay
"""
import cv2
h, w = frame.shape[:2]
x1, x2 = 0, w
overlay = frame.copy()
if style == 'dashed':
dash_len = 30
gap = 20
for x in range(x1, x2, dash_len + gap):
x_end = min(x + dash_len, x2)
cv2.line(overlay, (x, y), (x_end, y), color, thickness, lineType=cv2.LINE_AA)
else:
cv2.line(overlay, (x1, y), (x2, y), color, thickness, lineType=cv2.LINE_AA)
# Blend for semi-transparency
cv2.addWeighted(overlay, 0.7, frame, 0.3, 0, frame)
# Draw label
if label:
font = cv2.FONT_HERSHEY_SIMPLEX
text_size, _ = cv2.getTextSize(label, font, 0.8, 2)
text_x = max(10, (w - text_size[0]) // 2)
text_y = max(0, y - 12)
cv2.rectangle(frame, (text_x - 5, text_y - text_size[1] - 5), (text_x + text_size[0] + 5, text_y + 5), (0,0,0), -1)
cv2.putText(frame, label, (text_x, text_y), font, 0.8, color, 2, cv2.LINE_AA)
return frame
def get_violation_line_y(frame, traffic_light_bbox=None, crosswalk_bbox=None):
"""
Returns the y-coordinate of the violation line using the following priority:
1. Crosswalk bbox (most accurate)
2. Stop line detection via image processing (CV)
3. Traffic light bbox heuristic
4. Fallback (default)
"""
height, width = frame.shape[:2]
# 1. Crosswalk bbox
if crosswalk_bbox is not None and len(crosswalk_bbox) == 4:
return int(crosswalk_bbox[1]) - 15
# 2. Stop line detection (CV)
roi_height = int(height * 0.4)
roi_y = height - roi_height
roi = frame[roi_y:height, 0:width]
gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
binary = cv2.adaptiveThreshold(
gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY, 15, -2
)
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (15, 1))
processed = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)
contours, _ = cv2.findContours(processed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
stop_line_candidates = []
for cnt in contours:
x, y, w, h = cv2.boundingRect(cnt)
aspect_ratio = w / max(h, 1)
normalized_width = w / width
if (aspect_ratio > 5 and normalized_width > 0.3 and h < 15 and y > roi_height * 0.5):
abs_y = y + roi_y
stop_line_candidates.append((abs_y, w))
if stop_line_candidates:
stop_line_candidates.sort(key=lambda x: x[1], reverse=True)
return stop_line_candidates[0][0]
# 3. Traffic light bbox heuristic
if traffic_light_bbox is not None and len(traffic_light_bbox) == 4:
traffic_light_bottom = traffic_light_bbox[3]
traffic_light_height = traffic_light_bbox[3] - traffic_light_bbox[1]
estimated_distance = min(5 * traffic_light_height, height * 0.3)
return min(int(traffic_light_bottom + estimated_distance), height - 20)
# Example usage:
# bbox, vline, dbg = detect_crosswalk_and_violation_line(frame, (tl_x, tl_y), perspective_M)

View File

@@ -0,0 +1,623 @@
print("🔧 [CROSSWALK_UTILS_ADVANCED] Advanced crosswalk detection with CLAHE, HSV, Sobel, and hierarchical clustering LOADED")
import cv2
import numpy as np
from typing import Tuple, Optional, List, Dict, Any
# Try to import scipy for hierarchical clustering, fallback to simple grouping
try:
from scipy.cluster.hierarchy import fcluster, linkage
from scipy.spatial.distance import pdist
SCIPY_AVAILABLE = True
print("[CROSSWALK_ADVANCED] Scipy available - using hierarchical clustering")
except ImportError:
SCIPY_AVAILABLE = False
print("[CROSSWALK_ADVANCED] Scipy not available - using simple grouping")
def detect_crosswalk_and_violation_line(frame: np.ndarray, traffic_light_position: Optional[Tuple[int, int]] = None, perspective_M: Optional[np.ndarray] = None):
"""
Advanced crosswalk detection using CLAHE, HSV, Sobel, and hierarchical clustering.
Args:
frame: BGR image frame from video feed
traffic_light_position: Optional (x, y) of traffic light in frame
perspective_M: Optional 3x3 homography matrix for bird's eye view normalization
Returns:
result_frame: frame with overlays (for visualization)
crosswalk_bbox: (x, y, w, h) or None if fallback used
violation_line_y: int (y position for violation check)
debug_info: dict (for visualization/debugging)
"""
print(f"[CROSSWALK_ADVANCED] Starting advanced detection. Traffic light: {traffic_light_position}")
debug_info = {}
orig_frame = frame.copy()
h, w = frame.shape[:2]
# 1⃣ PERSPECTIVE NORMALIZATION (Bird's Eye View)
if perspective_M is not None:
frame = cv2.warpPerspective(frame, perspective_M, (w, h))
debug_info['perspective_warped'] = True
print("[CROSSWALK_ADVANCED] Applied perspective warping")
else:
debug_info['perspective_warped'] = False
# 2⃣ ADVANCED PREPROCESSING
# CLAHE-enhanced grayscale for shadow and low-light handling
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
gray = clahe.apply(gray)
debug_info['clahe_applied'] = True
# HSV + V channel for bright white detection robust to hue variations
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
v = hsv[:, :, 2]
mask_white = cv2.inRange(v, 180, 255)
debug_info['hsv_white_ratio'] = np.sum(mask_white > 0) / (h * w)
# Blend mask with adaptive threshold
thresh = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY, 11, 2)
combined = cv2.bitwise_and(thresh, mask_white)
# 3⃣ EDGE DETECTION WITH SOBEL HORIZONTAL EMPHASIS
sobelx = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=3)
sobelx = cv2.convertScaleAbs(sobelx)
# Combine Sobel with white mask for better stripe detection
sobel_combined = cv2.bitwise_and(sobelx, mask_white)
# 4⃣ MORPHOLOGICAL ENHANCEMENT
# Horizontal kernel to connect broken stripes
kernel_h = cv2.getStructuringElement(cv2.MORPH_RECT, (15, 3))
morph = cv2.morphologyEx(combined, cv2.MORPH_CLOSE, kernel_h, iterations=1)
# Vertical kernel to remove vertical noise
kernel_v = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 7))
morph = cv2.morphologyEx(morph, cv2.MORPH_OPEN, kernel_v, iterations=1)
# Additional processing with Sobel results
sobel_morph = cv2.morphologyEx(sobel_combined, cv2.MORPH_CLOSE, kernel_h, iterations=1)
# Combine both approaches
final_mask = cv2.bitwise_or(morph, sobel_morph)
# 5⃣ CONTOUR EXTRACTION WITH ADVANCED FILTERING
contours, _ = cv2.findContours(final_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# Focus on lower ROI where crosswalks typically are
roi_y_start = int(h * 0.4)
zebra_stripes = []
for cnt in contours:
x, y, w_rect, h_rect = cv2.boundingRect(cnt)
# Skip if in upper part of frame
if y < roi_y_start:
continue
# Advanced filtering criteria
aspect_ratio = w_rect / max(h_rect, 1)
area = w_rect * h_rect
normalized_width = w_rect / w
# 1. Aspect Ratio: Wide and short
if aspect_ratio < 2.0:
continue
# 2. Area: Covers meaningful width
min_area = 200
max_area = 0.25 * h * w
if not (min_area < area < max_area):
continue
# 3. Coverage: Should cover significant width
if normalized_width < 0.05: # At least 5% of frame width
continue
# 4. Parallelism: Check if stripe is roughly horizontal
if len(cnt) >= 5:
[vx, vy, cx, cy] = cv2.fitLine(cnt, cv2.DIST_L2, 0, 0.01, 0.01)
angle = np.degrees(np.arctan2(vy, vx))
if not (abs(angle) < 15 or abs(angle) > 165):
continue
zebra_stripes.append({
'contour': cnt,
'bbox': (x, y, w_rect, h_rect),
'center': (x + w_rect//2, y + h_rect//2),
'area': area,
'aspect_ratio': aspect_ratio,
'normalized_width': normalized_width
})
print(f"[CROSSWALK_ADVANCED] Found {len(zebra_stripes)} potential zebra stripes")
# 6⃣ STRIPE GROUPING (Hierarchical Clustering or Simple Grouping)
crosswalk_bbox = None
violation_line_y = None
if len(zebra_stripes) >= 2:
if SCIPY_AVAILABLE:
# Use hierarchical clustering
clusters = perform_hierarchical_clustering(zebra_stripes, h)
else:
# Use simple distance-based grouping
clusters = perform_simple_grouping(zebra_stripes, h)
# 7⃣ ADVANCED SCORING FOR CROSSWALK IDENTIFICATION
scored_clusters = []
for cluster_id, stripes in clusters.items():
if len(stripes) < 2: # Need at least 2 stripes
continue
score = calculate_crosswalk_score(stripes, w, h)
scored_clusters.append((score, stripes, cluster_id))
debug_info['clusters_found'] = len(clusters)
debug_info['scored_clusters'] = len(scored_clusters)
if scored_clusters:
# Select best cluster
scored_clusters.sort(reverse=True, key=lambda x: x[0])
best_score, best_stripes, best_cluster_id = scored_clusters[0]
print(f"[CROSSWALK_ADVANCED] Best cluster score: {best_score:.3f} with {len(best_stripes)} stripes")
if best_score > 0.3: # Threshold for valid crosswalk
# Calculate crosswalk bounding box
all_bboxes = [s['bbox'] for s in best_stripes]
xs = [bbox[0] for bbox in all_bboxes] + [bbox[0] + bbox[2] for bbox in all_bboxes]
ys = [bbox[1] for bbox in all_bboxes] + [bbox[1] + bbox[3] for bbox in all_bboxes]
x1, x2 = min(xs), max(xs)
y1, y2 = min(ys), max(ys)
crosswalk_bbox = (x1, y1, x2 - x1, y2 - y1)
# Place violation line before crosswalk
violation_line_y = y1 - 20
debug_info['crosswalk_detected'] = True
debug_info['crosswalk_score'] = best_score
debug_info['crosswalk_bbox'] = crosswalk_bbox
debug_info['best_stripes'] = best_stripes
print(f"[CROSSWALK_ADVANCED] CROSSWALK DETECTED at bbox: {crosswalk_bbox}")
print(f"[CROSSWALK_ADVANCED] Violation line at y={violation_line_y}")
# 8⃣ FALLBACK: ENHANCED STOP-LINE DETECTION
if crosswalk_bbox is None:
print("[CROSSWALK_ADVANCED] No crosswalk found, using stop-line detection fallback")
violation_line_y = detect_stop_line_fallback(frame, traffic_light_position, h, w, debug_info)
# 9⃣ TRAFFIC LIGHT ALIGNMENT (if provided)
if traffic_light_position and violation_line_y:
violation_line_y = align_violation_line_to_traffic_light(
violation_line_y, traffic_light_position, crosswalk_bbox, h
)
debug_info['traffic_light_aligned'] = True
# 🔟 VISUALIZATION
result_frame = orig_frame.copy()
if violation_line_y is not None:
result_frame = draw_violation_line(result_frame, violation_line_y,
color=(0, 0, 255), thickness=8,
style='solid', label='VIOLATION LINE')
# Draw crosswalk bbox if detected
if crosswalk_bbox:
x, y, w_box, h_box = crosswalk_bbox
cv2.rectangle(result_frame, (x, y), (x + w_box, y + h_box), (0, 255, 0), 3)
cv2.putText(result_frame, 'CROSSWALK', (x, y - 10),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
return result_frame, crosswalk_bbox, violation_line_y, debug_info
def draw_violation_line(frame: np.ndarray, y: int, color=(0, 0, 255), thickness=8, style='solid', label='Violation Line'):
"""
Draws a thick, optionally dashed, labeled violation line at the given y-coordinate.
Args:
frame: BGR image
y: y-coordinate for the line
color: BGR color tuple
thickness: line thickness
style: 'solid' or 'dashed'
label: Optional label to draw above the line
Returns:
frame with line overlay
"""
import cv2
h, w = frame.shape[:2]
x1, x2 = 0, w
overlay = frame.copy()
if style == 'dashed':
dash_len = 30
gap = 20
for x in range(x1, x2, dash_len + gap):
x_end = min(x + dash_len, x2)
cv2.line(overlay, (x, y), (x_end, y), color, thickness, lineType=cv2.LINE_AA)
else:
cv2.line(overlay, (x1, y), (x2, y), color, thickness, lineType=cv2.LINE_AA)
# Blend for semi-transparency
cv2.addWeighted(overlay, 0.7, frame, 0.3, 0, frame)
# Draw label
if label:
font = cv2.FONT_HERSHEY_SIMPLEX
text_size, _ = cv2.getTextSize(label, font, 0.8, 2)
text_x = max(10, (w - text_size[0]) // 2)
text_y = max(0, y - 12)
cv2.rectangle(frame, (text_x - 5, text_y - text_size[1] - 5), (text_x + text_size[0] + 5, text_y + 5), (0,0,0), -1)
cv2.putText(frame, label, (text_x, text_y), font, 0.8, color, 2, cv2.LINE_AA)
return frame
def get_violation_line_y(frame, traffic_light_bbox=None, crosswalk_bbox=None):
"""
Returns the y-coordinate of the violation line using the following priority:
1. Crosswalk bbox (most accurate)
2. Stop line detection via image processing (CV)
3. Traffic light bbox heuristic
4. Fallback (default)
"""
height, width = frame.shape[:2]
# 1. Crosswalk bbox
if crosswalk_bbox is not None and len(crosswalk_bbox) == 4:
return int(crosswalk_bbox[1]) - 15
# 2. Stop line detection (CV)
roi_height = int(height * 0.4)
roi_y = height - roi_height
roi = frame[roi_y:height, 0:width]
gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
binary = cv2.adaptiveThreshold(
gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY, 15, -2
)
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (15, 1))
processed = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)
contours, _ = cv2.findContours(processed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
stop_line_candidates = []
for cnt in contours:
x, y, w, h = cv2.boundingRect(cnt)
aspect_ratio = w / max(h, 1)
normalized_width = w / width
if (aspect_ratio > 5 and normalized_width > 0.3 and h < 15 and y > roi_height * 0.5):
abs_y = y + roi_y
stop_line_candidates.append((abs_y, w))
if stop_line_candidates:
stop_line_candidates.sort(key=lambda x: x[1], reverse=True)
return stop_line_candidates[0][0]
# 3. Traffic light bbox heuristic
if traffic_light_bbox is not None and len(traffic_light_bbox) == 4:
traffic_light_bottom = traffic_light_bbox[3]
traffic_light_height = traffic_light_bbox[3] - traffic_light_bbox[1]
estimated_distance = min(5 * traffic_light_height, height * 0.3)
return min(int(traffic_light_bottom + estimated_distance), height - 20)
def calculate_crosswalk_score(stripes: List[Dict], frame_width: int, frame_height: int) -> float:
"""
Advanced scoring function for crosswalk validation using multiple criteria.
Args:
stripes: List of stripe dictionaries with bbox, area, etc.
frame_width: Width of the frame
frame_height: Height of the frame
Returns:
score: Float between 0-1, higher is better
"""
if len(stripes) < 2:
return 0.0
# Extract metrics
heights = [s['bbox'][3] for s in stripes]
widths = [s['bbox'][2] for s in stripes]
y_centers = [s['center'][1] for s in stripes]
x_centers = [s['center'][0] for s in stripes]
areas = [s['area'] for s in stripes]
# 1. Stripe Count Score (more stripes = more confident)
count_score = min(len(stripes) / 5.0, 1.0) # Optimal around 5 stripes
# 2. Height Consistency Score
if len(heights) > 1:
height_std = np.std(heights)
height_mean = np.mean(heights)
height_score = max(0, 1.0 - (height_std / (height_mean + 1e-6)))
else:
height_score = 0.5
# 3. Horizontal Alignment Score (y-coordinates should be similar)
if len(y_centers) > 1:
y_std = np.std(y_centers)
y_tolerance = frame_height * 0.05 # 5% of frame height
y_score = max(0, 1.0 - (y_std / y_tolerance))
else:
y_score = 0.5
# 4. Regular Spacing Score
if len(stripes) >= 3:
x_sorted = sorted(x_centers)
gaps = [x_sorted[i+1] - x_sorted[i] for i in range(len(x_sorted)-1)]
gap_mean = np.mean(gaps)
gap_std = np.std(gaps)
spacing_score = max(0, 1.0 - (gap_std / (gap_mean + 1e-6)))
else:
spacing_score = 0.3
# 5. Coverage Score (should span reasonable width)
total_width = max(x_centers) - min(x_centers)
coverage_ratio = total_width / frame_width
coverage_score = min(coverage_ratio / 0.3, 1.0) # Target 30% coverage
# 6. Area Consistency Score
if len(areas) > 1:
area_std = np.std(areas)
area_mean = np.mean(areas)
area_score = max(0, 1.0 - (area_std / (area_mean + 1e-6)))
else:
area_score = 0.5
# 7. Aspect Ratio Consistency Score
aspect_ratios = [s['aspect_ratio'] for s in stripes]
if len(aspect_ratios) > 1:
aspect_std = np.std(aspect_ratios)
aspect_mean = np.mean(aspect_ratios)
aspect_score = max(0, 1.0 - (aspect_std / (aspect_mean + 1e-6)))
else:
aspect_score = 0.5
# Weighted final score
weights = {
'count': 0.2,
'height': 0.15,
'alignment': 0.2,
'spacing': 0.15,
'coverage': 0.15,
'area': 0.075,
'aspect': 0.075
}
final_score = (
weights['count'] * count_score +
weights['height'] * height_score +
weights['alignment'] * y_score +
weights['spacing'] * spacing_score +
weights['coverage'] * coverage_score +
weights['area'] * area_score +
weights['aspect'] * aspect_score
)
return final_score
def detect_stop_line_fallback(frame: np.ndarray, traffic_light_position: Optional[Tuple[int, int]],
frame_height: int, frame_width: int, debug_info: Dict) -> Optional[int]:
"""
Enhanced stop-line detection using Canny + HoughLinesP with improved filtering.
Args:
frame: Input frame
traffic_light_position: Optional traffic light position
frame_height: Height of frame
frame_width: Width of frame
debug_info: Debug information dictionary
Returns:
violation_line_y: Y-coordinate of violation line or None
"""
print("[CROSSWALK_ADVANCED] Running stop-line detection fallback")
# Convert to grayscale and apply CLAHE
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
gray = clahe.apply(gray)
# Focus on lower ROI where stop lines typically are
roi_height = int(frame_height * 0.6) # Lower 60% of frame
roi_y = frame_height - roi_height
roi_gray = gray[roi_y:frame_height, :]
# Enhanced edge detection
edges = cv2.Canny(roi_gray, 50, 150, apertureSize=3)
# Morphological operations to connect broken lines
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (15, 1))
edges = cv2.morphologyEx(edges, cv2.MORPH_CLOSE, kernel)
# Detect horizontal lines using HoughLinesP
lines = cv2.HoughLinesP(edges, 1, np.pi / 180,
threshold=40, minLineLength=int(frame_width * 0.2), maxLineGap=20)
stop_line_candidates = []
if lines is not None:
for line in lines:
x1, y1, x2, y2 = line[0]
# Convert back to full frame coordinates
y1 += roi_y
y2 += roi_y
# Calculate line properties
angle = np.degrees(np.arctan2(y2 - y1, x2 - x1))
line_length = np.sqrt((x2-x1)**2 + (y2-y1)**2)
line_center_y = (y1 + y2) // 2
# Filter for horizontal lines
if (abs(angle) < 10 or abs(angle) > 170) and line_length > frame_width * 0.15:
stop_line_candidates.append({
'line': (x1, y1, x2, y2),
'center_y': line_center_y,
'length': line_length,
'angle': angle
})
debug_info['stop_line_candidates'] = len(stop_line_candidates)
if stop_line_candidates:
# Score and select best stop line
best_line = None
if traffic_light_position:
tx, ty = traffic_light_position
# Find line that's appropriately positioned relative to traffic light
valid_candidates = [
candidate for candidate in stop_line_candidates
if candidate['center_y'] > ty + 30 # Below traffic light
]
if valid_candidates:
# Select line closest to expected distance from traffic light
expected_distance = frame_height * 0.3 # 30% of frame height
target_y = ty + expected_distance
best_candidate = min(valid_candidates,
key=lambda c: abs(c['center_y'] - target_y))
best_line = best_candidate['line']
else:
# Fallback to longest line
best_candidate = max(stop_line_candidates, key=lambda c: c['length'])
best_line = best_candidate['line']
else:
# Select the bottom-most line with good length
best_candidate = max(stop_line_candidates,
key=lambda c: c['center_y'] + c['length'] * 0.1)
best_line = best_candidate['line']
if best_line:
x1, y1, x2, y2 = best_line
violation_line_y = min(y1, y2) - 15 # 15 pixels before stop line
debug_info['stop_line_used'] = best_line
print(f"[CROSSWALK_ADVANCED] Stop line detected, violation line at y={violation_line_y}")
return violation_line_y
# Final fallback - use heuristic based on frame and traffic light
if traffic_light_position:
tx, ty = traffic_light_position
fallback_y = int(ty + frame_height * 0.25) # 25% below traffic light
else:
fallback_y = int(frame_height * 0.75) # 75% down the frame
debug_info['fallback_used'] = True
print(f"[CROSSWALK_ADVANCED] Using fallback violation line at y={fallback_y}")
return fallback_y
def align_violation_line_to_traffic_light(violation_line_y: int, traffic_light_position: Tuple[int, int],
crosswalk_bbox: Optional[Tuple], frame_height: int) -> int:
"""
Align violation line dynamically based on traffic light position.
Args:
violation_line_y: Current violation line y-coordinate
traffic_light_position: (x, y) of traffic light
crosswalk_bbox: Crosswalk bounding box if detected
frame_height: Height of frame
Returns:
adjusted_violation_line_y: Adjusted y-coordinate
"""
tx, ty = traffic_light_position
# Calculate expected distance from traffic light to violation line
if crosswalk_bbox:
# If crosswalk detected, maintain current position but validate
expected_distance = frame_height * 0.2 # 20% of frame height
actual_distance = violation_line_y - ty
# If too close or too far, adjust slightly
if actual_distance < expected_distance * 0.5:
violation_line_y = int(ty + expected_distance * 0.7)
elif actual_distance > expected_distance * 2:
violation_line_y = int(ty + expected_distance * 1.3)
else:
# For stop lines, use standard distance
standard_distance = frame_height * 0.25 # 25% of frame height
violation_line_y = int(ty + standard_distance)
# Ensure violation line is within frame bounds
violation_line_y = max(20, min(violation_line_y, frame_height - 20))
print(f"[CROSSWALK_ADVANCED] Traffic light aligned violation line at y={violation_line_y}")
return violation_line_y
def perform_hierarchical_clustering(zebra_stripes: List[Dict], frame_height: int) -> Dict:
"""
Perform hierarchical clustering on zebra stripes using scipy.
Args:
zebra_stripes: List of stripe dictionaries
frame_height: Height of frame for distance threshold
Returns:
clusters: Dictionary of cluster_id -> list of stripes
"""
# Extract y-coordinates for clustering
y_coords = np.array([stripe['center'][1] for stripe in zebra_stripes]).reshape(-1, 1)
if len(y_coords) <= 1:
return {1: zebra_stripes}
# Perform hierarchical clustering
distances = pdist(y_coords, metric='euclidean')
linkage_matrix = linkage(distances, method='ward')
# Get clusters (max distance threshold)
max_distance = frame_height * 0.08 # 8% of frame height
cluster_labels = fcluster(linkage_matrix, max_distance, criterion='distance')
# Group stripes by cluster
clusters = {}
for i, label in enumerate(cluster_labels):
if label not in clusters:
clusters[label] = []
clusters[label].append(zebra_stripes[i])
return clusters
def perform_simple_grouping(zebra_stripes: List[Dict], frame_height: int) -> Dict:
"""
Perform simple distance-based grouping when scipy is not available.
Args:
zebra_stripes: List of stripe dictionaries
frame_height: Height of frame for distance threshold
Returns:
clusters: Dictionary of cluster_id -> list of stripes
"""
if not zebra_stripes:
return {}
# Sort stripes by y-coordinate
sorted_stripes = sorted(zebra_stripes, key=lambda s: s['center'][1])
clusters = {}
cluster_id = 1
y_tolerance = frame_height * 0.08 # 8% of frame height
current_cluster = [sorted_stripes[0]]
for i in range(1, len(sorted_stripes)):
current_stripe = sorted_stripes[i]
prev_stripe = sorted_stripes[i-1]
y_diff = abs(current_stripe['center'][1] - prev_stripe['center'][1])
if y_diff <= y_tolerance:
# Add to current cluster
current_cluster.append(current_stripe)
else:
# Start new cluster
if len(current_cluster) >= 2: # Only keep clusters with 2+ stripes
clusters[cluster_id] = current_cluster
cluster_id += 1
current_cluster = [current_stripe]
# Don't forget the last cluster
if len(current_cluster) >= 2:
clusters[cluster_id] = current_cluster
return clusters
# Example usage:
# bbox, vline, dbg = detect_crosswalk_and_violation_line(frame, (tl_x, tl_y), perspective_M)

View File

@@ -0,0 +1,73 @@
import cv2
import numpy as np
import math
from sklearn import linear_model
def lineCalc(vx, vy, x0, y0):
scale = 10
x1 = x0 + scale * vx
y1 = y0 + scale * vy
m = (y1 - y0) / (x1 - x0)
b = y1 - m * x1
return m, b
def lineIntersect(m1, b1, m2, b2):
a_1 = -m1
b_1 = 1
c_1 = b1
a_2 = -m2
b_2 = 1
c_2 = b2
d = a_1 * b_2 - a_2 * b_1
dx = c_1 * b_2 - c_2 * b_1
dy = a_1 * c_2 - a_2 * c_1
intersectionX = dx / d
intersectionY = dy / d
return intersectionX, intersectionY
def detect_crosswalk(frame):
'''Detects crosswalk/zebra lines and vanishing point in a BGR frame.'''
H, W = frame.shape[:2]
radius = 250
bw_width = 170
lower = np.array([170, 170, 170])
upper = np.array([255, 255, 255])
mask = cv2.inRange(frame, lower, upper)
erodeSize = int(H / 30)
erodeStructure = cv2.getStructuringElement(cv2.MORPH_RECT, (erodeSize, 1))
erode = cv2.erode(mask, erodeStructure, (-1, -1))
contours, _ = cv2.findContours(erode, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
bxbyLeftArray, bxbyRightArray = [], []
for cnt in contours:
bx, by, bw, bh = cv2.boundingRect(cnt)
if bw > bw_width:
cv2.line(frame, (bx, by), (bx + bw, by), (0, 255, 0), 2)
bxbyLeftArray.append([bx, by])
bxbyRightArray.append([bx + bw, by])
cv2.circle(frame, (int(bx), int(by)), 5, (0, 250, 250), 2)
cv2.circle(frame, (int(bx + bw), int(by)), 5, (250, 250, 0), 2)
if len(bxbyLeftArray) < 2 or len(bxbyRightArray) < 2:
return None, None, frame
medianL = np.median(bxbyLeftArray, axis=0)
medianR = np.median(bxbyRightArray, axis=0)
boundedLeft = [i for i in bxbyLeftArray if ((medianL[0] - i[0]) ** 2 + (medianL[1] - i[1]) ** 2) < radius ** 2]
boundedRight = [i for i in bxbyRightArray if ((medianR[0] - i[0]) ** 2 + (medianR[1] - i[1]) ** 2) < radius ** 2]
if len(boundedLeft) < 2 or len(boundedRight) < 2:
return None, None, frame
bxLeft = np.asarray([pt[0] for pt in boundedLeft]).reshape(-1, 1)
byLeft = np.asarray([pt[1] for pt in boundedLeft])
bxRight = np.asarray([pt[0] for pt in boundedRight]).reshape(-1, 1)
byRight = np.asarray([pt[1] for pt in boundedRight])
modelL = linear_model.RANSACRegressor().fit(bxLeft, byLeft)
modelR = linear_model.RANSACRegressor().fit(bxRight, byRight)
vx, vy, x0, y0 = cv2.fitLine(np.array(boundedLeft), cv2.DIST_L2, 0, 0.01, 0.01)
vx_R, vy_R, x0_R, y0_R = cv2.fitLine(np.array(boundedRight), cv2.DIST_L2, 0, 0.01, 0.01)
m_L, b_L = lineCalc(vx, vy, x0, y0)
m_R, b_R = lineCalc(vx_R, vy_R, x0_R, y0_R)
intersectionX, intersectionY = lineIntersect(m_R, b_R, m_L, b_L)
m = radius * 10
if intersectionY < H / 2:
cv2.circle(frame, (int(intersectionX), int(intersectionY)), 10, (0, 0, 255), 15)
cv2.line(frame, (int(x0 - m * vx), int(y0 - m * vy)), (int(x0 + m * vx), int(y0 + m * vy)), (255, 0, 0), 3)
cv2.line(frame, (int(x0_R - m * vx_R), int(y0_R - m * vy_R)), (int(x0_R + m * vx_R), int(y0_R + m * vy_R)), (255, 0, 0), 3)
return (int(intersectionX), int(intersectionY)), [list(medianL) + list(medianR)], frame

View File

@@ -0,0 +1,43 @@
import cv2
import numpy as np
def findNonZero(rgb_image):
rows, cols, _ = rgb_image.shape
counter = 0
for row in range(rows):
for col in range(cols):
pixel = rgb_image[row, col]
if sum(pixel) != 0:
counter += 1
return counter
def red_green_yellow(rgb_image):
hsv = cv2.cvtColor(rgb_image, cv2.COLOR_RGB2HSV)
sum_saturation = np.sum(hsv[:,:,1])
area = rgb_image.shape[0] * rgb_image.shape[1]
avg_saturation = sum_saturation / area
sat_low = int(avg_saturation * 1.3)
val_low = 140
lower_green = np.array([70,sat_low,val_low])
upper_green = np.array([100,255,255])
green_mask = cv2.inRange(hsv, lower_green, upper_green)
lower_yellow = np.array([10,sat_low,val_low])
upper_yellow = np.array([60,255,255])
yellow_mask = cv2.inRange(hsv, lower_yellow, upper_yellow)
lower_red = np.array([150,sat_low,val_low])
upper_red = np.array([180,255,255])
red_mask = cv2.inRange(hsv, lower_red, upper_red)
sum_green = findNonZero(cv2.bitwise_and(rgb_image, rgb_image, mask=green_mask))
sum_yellow = findNonZero(cv2.bitwise_and(rgb_image, rgb_image, mask=yellow_mask))
sum_red = findNonZero(cv2.bitwise_and(rgb_image, rgb_image, mask=red_mask))
if sum_red >= sum_yellow and sum_red >= sum_green:
return "red"
if sum_yellow >= sum_green:
return "yellow"
return "green"
def detect_traffic_light_color(frame, bbox):
x1, y1, x2, y2 = bbox
roi = frame[y1:y2, x1:x2]
roi_rgb = cv2.cvtColor(roi, cv2.COLOR_BGR2RGB)
return red_green_yellow(roi_rgb)

View File

@@ -0,0 +1,318 @@
"""
OpenVINO-based embedder for DeepSORT tracking.
"""
import os
import numpy as np
from pathlib import Path
import cv2
import time
from typing import List, Optional, Union
try:
import openvino as ov
except ImportError:
print("Installing openvino...")
os.system('pip install --quiet "openvino>=2024.0.0"')
import openvino as ov
class OpenVINOEmbedder:
"""
OpenVINO embedder for DeepSORT tracking.
This class provides an optimized version of the feature embedder used in DeepSORT,
using OpenVINO for inference acceleration.
"""
def __init__(
self,
model_path: Optional[str] = None,
device: str = "AUTO",
input_size: tuple = (128, 64),
batch_size: int = 16,
bgr: bool = True,
half: bool = True
):
"""
Initialize the OpenVINO embedder.
Args:
model_path: Path to the model file. If None, will use the default MobileNetV2 model.
device: Device to run inference on ('CPU', 'GPU', 'AUTO', etc.)
input_size: Input size for the model (height, width)
batch_size: Batch size for inference
bgr: Whether input images are BGR (True) or RGB (False)
half: Whether to use half precision (FP16)
"""
self.device = device
self.input_size = input_size # (h, w)
self.batch_size = batch_size
self.bgr = bgr
self.half = half
# Initialize OpenVINO Core
self.core = ov.Core()
# Find and load model
if model_path is None:
# Use MobileNetV2 converted to OpenVINO
model_path = self._find_mobilenet_model()
# If model not found, convert it
if model_path is None:
print("⚠️ MobileNetV2 OpenVINO model not found. Creating it...")
model_path = self._convert_mobilenet()
else:
# When model_path is explicitly provided, verify it exists
if not os.path.exists(model_path):
print(f"⚠️ Specified model path does not exist: {model_path}")
print("Falling back to default model search...")
model_path = self._find_mobilenet_model()
if model_path is None:
print("⚠️ Default model search also failed. Creating new model...")
model_path = self._convert_mobilenet()
else:
print(f"✅ Using explicitly provided model: {model_path}")
print(f"📦 Loading embedder model: {model_path} on {device}")
# Load and compile the model
self.model = self.core.read_model(model_path)
# Set up configuration for device
ov_config = {}
if device != "CPU":
self.model.reshape({0: [self.batch_size, 3, self.input_size[0], self.input_size[1]]})
if "GPU" in device or ("AUTO" in device and "GPU" in self.core.available_devices):
ov_config = {"GPU_DISABLE_WINOGRAD_CONVOLUTION": "YES"}
# Compile model for the specified device
self.compiled_model = self.core.compile_model(model=self.model, device_name=self.device, config=ov_config)
# Get input and output tensors
self.input_layer = self.compiled_model.inputs[0]
self.output_layer = self.compiled_model.outputs[0]
# Create inference requests for async inference
self.infer_requests = [self.compiled_model.create_infer_request() for _ in range(2)]
self.current_request_idx = 0
# Performance stats
self.total_inference_time = 0
self.inference_count = 0
def _find_mobilenet_model(self) -> Optional[str]:
"""
Find MobileNetV2 model converted to OpenVINO format.
Returns:
Path to the model file or None if not found
"""
search_paths = [
# Standard locations
"mobilenetv2_embedder/mobilenetv2.xml",
"../mobilenetv2_embedder/mobilenetv2.xml",
"../../mobilenetv2_embedder/mobilenetv2.xml",
# Look in models directory
"../models/mobilenetv2.xml",
"../../models/mobilenetv2.xml",
# Look relative to DeepSORT location
os.path.join(os.path.dirname(__file__), "models/mobilenetv2.xml"),
# Look in openvino_models
"../openvino_models/mobilenetv2.xml",
"../../openvino_models/mobilenetv2.xml"
]
for path in search_paths:
if os.path.exists(path):
return path
return None
def _convert_mobilenet(self) -> str:
"""
Convert MobileNetV2 model to OpenVINO IR format.
Returns:
Path to the converted model
"""
try:
# Create directory for the model
output_dir = Path("mobilenetv2_embedder")
output_dir.mkdir(exist_ok=True)
# First, we need to download the PyTorch model
import torch
import torch.nn as nn
from torchvision.models import mobilenet_v2, MobileNet_V2_Weights
print("⬇️ Downloading MobileNetV2 model...")
model = mobilenet_v2(weights=MobileNet_V2_Weights.IMAGENET1K_V1)
# Modify for feature extraction (remove classifier)
class FeatureExtractor(nn.Module):
def __init__(self, model):
super(FeatureExtractor, self).__init__()
self.features = nn.Sequential(*list(model.children())[:-1])
def forward(self, x):
return self.features(x).squeeze()
feature_model = FeatureExtractor(model)
feature_model.eval()
# Save to ONNX
onnx_path = output_dir / "mobilenetv2.onnx"
print(f"💾 Converting to ONNX: {onnx_path}")
dummy_input = torch.randn(1, 3, self.input_size[0], self.input_size[1])
torch.onnx.export(
feature_model,
dummy_input,
onnx_path,
input_names=["input"],
output_names=["output"],
dynamic_axes={"input": {0: "batch_size"}, "output": {0: "batch_size"}},
opset_version=11
)
# Convert ONNX to OpenVINO IR
ir_path = output_dir / "mobilenetv2.xml"
print(f"💾 Converting to OpenVINO IR: {ir_path}")
# Use the proper OpenVINO API to convert the model
try:
from openvino.tools.mo import convert_model
print(f"Converting ONNX model using OpenVINO convert_model API...")
print(f"Input model: {onnx_path}")
print(f"Output directory: {output_dir}")
print(f"Input shape: [{self.batch_size},3,{self.input_size[0]},{self.input_size[1]}]")
print(f"Data type: {'FP16' if self.half else 'FP32'}")
# Convert using the proper API
convert_model(
model_path=str(onnx_path),
output_dir=str(output_dir),
input_shape=[self.batch_size, 3, self.input_size[0], self.input_size[1]],
data_type="FP16" if self.half else "FP32"
)
print(f"✅ Model successfully converted using OpenVINO convert_model API")
except Exception as e:
print(f"Error with convert_model: {e}, trying alternative approach...")
# Fallback to subprocess with explicit path if needed
import subprocess
import sys
import os
# Try to find mo.py in the OpenVINO installation
mo_paths = [
os.path.join(os.environ.get("INTEL_OPENVINO_DIR", ""), "tools", "mo", "mo.py"),
os.path.join(os.path.dirname(os.path.dirname(os.__file__)), "openvino", "tools", "mo", "mo.py"),
"C:/Program Files (x86)/Intel/openvino_2021/tools/mo/mo.py",
"C:/Program Files (x86)/Intel/openvino/tools/mo/mo.py"
]
mo_script = None
for path in mo_paths:
if os.path.exists(path):
mo_script = path
break
if not mo_script:
raise FileNotFoundError("Cannot find OpenVINO Model Optimizer (mo.py)")
cmd = [
sys.executable,
mo_script,
"--input_model", str(onnx_path),
"--output_dir", str(output_dir),
"--input_shape", f"[{self.batch_size},3,{self.input_size[0]},{self.input_size[1]}]",
"--data_type", "FP16" if self.half else "FP32"
]
print(f"Running Model Optimizer: {' '.join(cmd)}")
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
print(f"Error running Model Optimizer: {result.stderr}")
raise RuntimeError(f"Model Optimizer failed: {result.stderr}")
print(f"✅ Model converted: {ir_path}")
return str(ir_path)
except Exception as e:
print(f"❌ Error converting model: {e}")
import traceback
traceback.print_exc()
return None
def preprocess(self, crops: List[np.ndarray]) -> np.ndarray:
"""
Preprocess image crops for model input.
Args:
crops: List of image crops
Returns:
Preprocessed batch tensor
"""
processed = []
for crop in crops:
# Resize to expected input size
crop = cv2.resize(crop, (self.input_size[1], self.input_size[0]))
# Convert BGR to RGB if needed
if not self.bgr and crop.shape[2] == 3:
crop = cv2.cvtColor(crop, cv2.COLOR_BGR2RGB)
# Normalize (0-255 to 0-1)
crop = crop.astype(np.float32) / 255.0
# Change to NCHW format
crop = crop.transpose(2, 0, 1)
processed.append(crop)
# Stack into batch
batch = np.stack(processed)
return batch
def __call__(self, crops: List[np.ndarray]) -> np.ndarray:
"""
Get embeddings for the image crops.
Args:
crops: List of image crops
Returns:
Embeddings for each crop
"""
if not crops:
return np.array([])
# Preprocess crops
batch = self.preprocess(crops)
# Run inference
start_time = time.time()
# Use async inference to improve performance
request = self.infer_requests[self.current_request_idx]
self.current_request_idx = (self.current_request_idx + 1) % len(self.infer_requests)
request.start_async({self.input_layer.any_name: batch})
request.wait()
# Get output
embeddings = request.get_output_tensor().data
# Track inference time
inference_time = time.time() - start_time
self.total_inference_time += inference_time
self.inference_count += 1
# Normalize embeddings
embeddings = embeddings / np.linalg.norm(embeddings, axis=1, keepdims=True)
return embeddings

View File

@@ -0,0 +1,414 @@
import cv2
import numpy as np
from typing import Dict, List, Tuple, Any, Optional
from PySide6.QtGui import QImage, QPixmap
from PySide6.QtCore import Qt
# Color mapping for traffic-related classes
COLORS = {
'person': (255, 165, 0), # Orange
'bicycle': (255, 0, 255), # Magenta
'car': (0, 255, 0), # Green
'motorcycle': (255, 255, 0), # Cyan
'bus': (0, 0, 255), # Red
'truck': (0, 128, 255), # Orange-Blue
'traffic light': (0, 165, 255), # Orange
'stop sign': (0, 0, 139), # Dark Red
'parking meter': (128, 0, 128), # Purple
'default': (0, 255, 255) # Yellow as default
}
# Enhanced class colors for consistent visualization
def get_enhanced_class_color(class_name: str, class_id: int = -1) -> Tuple[int, int, int]:
"""
Get color for class with enhanced mapping (traffic classes only)
Args:
class_name: Name of the detected class
class_id: COCO class ID
Returns:
BGR color tuple
"""
# Only traffic class IDs/colors
enhanced_colors = {
0: (255, 165, 0), # person - Orange
1: (255, 0, 255), # bicycle - Magenta
2: (0, 255, 0), # car - Green
3: (255, 255, 0), # motorcycle - Cyan
4: (0, 0, 255), # bus - Red
5: (0, 128, 255), # truck - Orange-Blue
6: (0, 165, 255), # traffic light - Orange
7: (0, 0, 139), # stop sign - Dark Red
8: (128, 0, 128), # parking meter - Purple
}
# Get color from class name if available
if class_name and class_name.lower() in COLORS:
return COLORS[class_name.lower()]
# Get color from class ID if available
if isinstance(class_id, int) and class_id in enhanced_colors:
return enhanced_colors[class_id]
# Default color
return COLORS['default']
def enhanced_draw_detections(frame: np.ndarray, detections: List[Dict],
draw_labels: bool = True, draw_confidence: bool = True) -> np.ndarray:
"""
Enhanced version of draw_detections with better visualization
Args:
frame: Input video frame
detections: List of detection dictionaries
draw_labels: Whether to draw class labels
draw_confidence: Whether to draw confidence scores
Returns:
Annotated frame
"""
if frame is None or not isinstance(frame, np.ndarray) or frame.size == 0:
print("Warning: Invalid frame provided to enhanced_draw_detections")
return np.zeros((300, 300, 3), dtype=np.uint8) # Return blank frame as fallback
annotated_frame = frame.copy()
# Handle case when detections is None or empty
if detections is None or len(detections) == 0:
return annotated_frame
# Get frame dimensions for validation
h, w = frame.shape[:2]
for detection in detections:
if not isinstance(detection, dict):
continue
try:
# Skip detection if it doesn't have bbox or has invalid confidence
if 'bbox' not in detection:
continue
# Skip if confidence is below threshold (don't rely on external filtering)
confidence = detection.get('confidence', 0.0)
if confidence < 0.1: # Apply a minimal threshold to ensure we're not drawing noise
continue
bbox = detection['bbox']
class_name = detection.get('class_name', 'unknown')
class_id = detection.get('class_id', -1)
# Get color for class
color = get_enhanced_class_color(class_name, class_id)
# Ensure bbox has enough coordinates and they are numeric values
if len(bbox) < 4 or not all(isinstance(coord, (int, float)) for coord in bbox[:4]):
continue
# Convert coordinates to integers
try:
x1, y1, x2, y2 = map(int, bbox[:4])
except (ValueError, TypeError):
print(f"Warning: Invalid bbox format: {bbox}")
continue
# Validate coordinates are within frame bounds
x1 = max(0, min(x1, w-1))
y1 = max(0, min(y1, h-1))
x2 = max(0, min(x2, w))
y2 = max(0, min(y2, h))
# Ensure x2 > x1 and y2 > y1 (at least 1 pixel width/height)
if x2 <= x1 or y2 <= y1:
# Instead of skipping, fix the coordinates to ensure at least 1 pixel width/height
x2 = max(x1 + 1, x2)
y2 = max(y1 + 1, y2)
# Draw bounding box with thicker line for better visibility
cv2.rectangle(annotated_frame, (x1, y1), (x2, y2), color, 2)
# Prepare label text
label_parts = []
if draw_labels:
# Display proper class name
display_name = class_name.replace('_', ' ').title()
label_parts.append(display_name)
# Add tracking ID if available
track_id = detection.get('track_id')
if track_id is not None:
label_parts[-1] += f" #{track_id}"
if draw_confidence and confidence > 0:
label_parts.append(f"{confidence:.2f}")
# Draw traffic light color indicator if available
if class_name == 'traffic light' and 'traffic_light_color' in detection:
light_color = detection['traffic_light_color']
# Add traffic light color to label
if light_color != 'unknown':
# Set color indicator based on traffic light state
if light_color == 'red':
color_indicator = (0, 0, 255) # Red
label_parts.append("🔴 RED")
elif light_color == 'yellow':
color_indicator = (0, 255, 255) # Yellow
label_parts.append("🟡 YELLOW")
elif light_color == 'green':
color_indicator = (0, 255, 0) # Green
label_parts.append("🟢 GREEN")
# Draw traffic light visual indicator (circle with detected color)
circle_y = y1 - 15
circle_x = x1 + 10
circle_radius = 10
if light_color == 'red':
cv2.circle(annotated_frame, (circle_x, circle_y), circle_radius, (0, 0, 255), -1)
elif light_color == 'yellow':
cv2.circle(annotated_frame, (circle_x, circle_y), circle_radius, (0, 255, 255), -1)
elif light_color == 'green':
cv2.circle(annotated_frame, (circle_x, circle_y), circle_radius, (0, 255, 0), -1)
# Draw label if we have any text
if label_parts:
label = " ".join(label_parts)
try:
# Get text size for background
(text_width, text_height), baseline = cv2.getTextSize(
label, cv2.FONT_HERSHEY_SIMPLEX, 0.6, 2
)
# Ensure label position is within frame
text_y = max(text_height + 10, y1)
# Draw label background (use colored background)
bg_color = tuple(int(c * 0.7) for c in color) # Darker version of box color
cv2.rectangle(
annotated_frame,
(x1, text_y - text_height - 10),
(x1 + text_width + 10, text_y),
bg_color,
-1
)
# Draw label text (white text on colored background)
cv2.putText(
annotated_frame,
label,
(x1 + 5, text_y - 5),
cv2.FONT_HERSHEY_SIMPLEX,
0.6,
(255, 255, 255), # White text
2
)
except Exception as e:
print(f"Error drawing label: {e}")
except Exception as e:
print(f"Error drawing detection: {e}")
continue
return annotated_frame
def draw_performance_overlay(frame: np.ndarray, metrics: Dict) -> np.ndarray:
"""
Draw enhanced performance metrics overlay on the frame.
Args:
frame: Input video frame
metrics: Dictionary of performance metrics
Returns:
Annotated frame
"""
if frame is None or not isinstance(frame, np.ndarray):
return np.zeros((300, 300, 3), dtype=np.uint8)
annotated_frame = frame.copy()
height, width = annotated_frame.shape[:2]
# Create semi-transparent overlay for metrics panel
overlay = annotated_frame.copy()
# Calculate panel size based on metrics count
text_height = 25
padding = 10
metrics_count = len(metrics)
panel_height = metrics_count * text_height + 2 * padding
panel_width = 220 # Fixed width
# Position panel at bottom left
panel_x = 10
panel_y = height - panel_height - 10
# Draw background panel with transparency
cv2.rectangle(
overlay,
(panel_x, panel_y),
(panel_x + panel_width, panel_y + panel_height),
(0, 0, 0),
-1
)
# Apply transparency
alpha = 0.7
cv2.addWeighted(overlay, alpha, annotated_frame, 1 - alpha, 0, annotated_frame)
# Draw metrics with custom formatting
text_y = panel_y + padding + text_height
for metric, value in metrics.items():
# Format metric name and value
metric_text = f"{metric}: {value}"
# Choose color based on metric type
if "FPS" in metric:
color = (0, 255, 0) # Green for FPS
elif "ms" in str(value):
color = (0, 255, 255) # Yellow for timing metrics
else:
color = (255, 255, 255) # White for other metrics
# Draw text with drop shadow for better readability
cv2.putText(
annotated_frame,
metric_text,
(panel_x + padding + 1, text_y + 1),
cv2.FONT_HERSHEY_SIMPLEX,
0.6,
(0, 0, 0), # Black shadow
2
)
cv2.putText(
annotated_frame,
metric_text,
(panel_x + padding, text_y),
cv2.FONT_HERSHEY_SIMPLEX,
0.6,
color,
2
)
text_y += text_height
return annotated_frame
def resize_frame_for_display(frame: np.ndarray, max_width: int = 1280, max_height: int = 720) -> np.ndarray:
"""
Resize frame for display while maintaining aspect ratio.
Args:
frame: Input video frame
max_width: Maximum display width
max_height: Maximum display height
Returns:
Resized frame
"""
if frame is None:
return np.zeros((300, 300, 3), dtype=np.uint8)
height, width = frame.shape[:2]
# No resize needed if image is already smaller than max dimensions
if width <= max_width and height <= max_height:
return frame
# Calculate scale factor to fit within max dimensions
scale_width = max_width / width if width > max_width else 1.0
scale_height = max_height / height if height > max_height else 1.0
# Use the smaller scale to ensure image fits within bounds
scale = min(scale_width, scale_height)
# Resize using calculated scale
new_width = int(width * scale)
new_height = int(height * scale)
return cv2.resize(frame, (new_width, new_height), interpolation=cv2.INTER_AREA)
def enhanced_cv_to_qimage(cv_img: np.ndarray) -> QImage:
"""
Enhanced converter from OpenCV image to QImage with robust error handling.
Args:
cv_img: OpenCV image (numpy array)
Returns:
QImage object
"""
if cv_img is None or not isinstance(cv_img, np.ndarray):
print("Warning: Invalid image in enhanced_cv_to_qimage")
# Return a small black image as fallback
return QImage(10, 10, QImage.Format_RGB888)
try:
# Get image dimensions and verify its validity
h, w, ch = cv_img.shape
if h <= 0 or w <= 0 or ch != 3:
raise ValueError(f"Invalid image dimensions: {h}x{w}x{ch}")
# OpenCV uses BGR, Qt uses RGB format, so convert
rgb_image = cv2.cvtColor(cv_img, cv2.COLOR_BGR2RGB)
# Calculate bytes per line
bytes_per_line = ch * w
# Use numpy array data directly
# This avoids a copy, but ensures the data is properly aligned
# by creating a contiguous array
contiguous_data = np.ascontiguousarray(rgb_image)
# Create QImage from numpy array
q_image = QImage(contiguous_data.data, w, h, bytes_per_line, QImage.Format_RGB888)
# Create a copy to ensure the data stays valid when returning
return q_image.copy()
except Exception as e:
print(f"Error in enhanced_cv_to_qimage: {e}")
# Return a small black image as fallback
return QImage(10, 10, QImage.Format_RGB888)
def enhanced_cv_to_pixmap(cv_img: np.ndarray, target_width: int = None) -> QPixmap:
"""
Enhanced converter from OpenCV image to QPixmap with robust error handling.
Args:
cv_img: OpenCV image (numpy array)
target_width: Optional width to resize to (maintains aspect ratio)
Returns:
QPixmap object
"""
if cv_img is None or not isinstance(cv_img, np.ndarray):
print("Warning: Invalid image in enhanced_cv_to_pixmap")
# Create an empty pixmap with visual indication of error
empty_pixmap = QPixmap(640, 480)
empty_pixmap.fill(Qt.black)
return empty_pixmap
try:
# First convert to QImage
q_image = enhanced_cv_to_qimage(cv_img)
if q_image.isNull():
raise ValueError("Generated null QImage")
# Resize if needed
if target_width and q_image.width() > target_width:
q_image = q_image.scaledToWidth(target_width, Qt.SmoothTransformation)
# Convert to QPixmap
pixmap = QPixmap.fromImage(q_image)
if pixmap.isNull():
raise ValueError("Generated null QPixmap")
return pixmap
except Exception as e:
print(f"Error in enhanced_cv_to_pixmap: {e}")
# Create an empty pixmap with visual indication of error
empty_pixmap = QPixmap(640, 480)
empty_pixmap.fill(Qt.black)
return empty_pixmap

View File

@@ -0,0 +1,279 @@
import json
import os
import sys
import time
import cv2
import numpy as np
from pathlib import Path
from typing import Dict, List, Tuple, Optional, Any
from datetime import datetime, timedelta
def bbox_iou(box1, box2):
"""
Calculate IoU (Intersection over Union) between two bounding boxes
Args:
box1: First bounding box in format [x1, y1, x2, y2]
box2: Second bounding box in format [x1, y1, x2, y2]
Returns:
IoU score between 0 and 1
"""
# Ensure boxes are in [x1, y1, x2, y2] format and have valid dimensions
if len(box1) < 4 or len(box2) < 4:
return 0.0
# Convert to float and ensure x2 > x1 and y2 > y1
x1_1, y1_1, x2_1, y2_1 = map(float, box1[:4])
x1_2, y1_2, x2_2, y2_2 = map(float, box2[:4])
if x2_1 <= x1_1 or y2_1 <= y1_1 or x2_2 <= x1_2 or y2_2 <= y1_2:
return 0.0
# Calculate area of each box
area1 = (x2_1 - x1_1) * (y2_1 - y1_1)
area2 = (x2_2 - x1_2) * (y2_2 - y1_2)
if area1 <= 0 or area2 <= 0:
return 0.0
# Calculate intersection area
x1_i = max(x1_1, x1_2)
y1_i = max(y1_1, y1_2)
x2_i = min(x2_1, x2_2)
y2_i = min(y2_1, y2_2)
if x2_i <= x1_i or y2_i <= y1_i:
return 0.0 # No intersection
intersection_area = (x2_i - x1_i) * (y2_i - y1_i)
# Calculate IoU
union_area = area1 + area2 - intersection_area
if union_area <= 0:
return 0.0
iou = intersection_area / union_area
return iou
def load_configuration(config_file: str) -> Dict:
"""
Load configuration from JSON file.
Args:
config_file: Path to configuration file
Returns:
Configuration dictionary
"""
default_config = {
"detection": {
"confidence_threshold": 0.5,
"enable_ocr": True,
"enable_tracking": True,
"model_path": None
},
"violations": {
"red_light_grace_period": 2.0,
"stop_sign_duration": 2.0,
"speed_tolerance": 5
},
"display": {
"max_display_width": 800,
"show_confidence": True,
"show_labels": True,
"show_license_plates": True
},
"performance": {
"max_history_frames": 1000,
"cleanup_interval": 3600
}
}
if not os.path.exists(config_file):
return default_config
try:
with open(config_file, 'r') as f:
config = json.load(f)
# Merge with defaults
for section in default_config:
if section in config:
default_config[section].update(config[section])
return default_config
except Exception as e:
print(f"Error loading config: {e}")
return default_config
def save_configuration(config: Dict, config_file: str) -> bool:
"""
Save configuration to JSON file.
Args:
config: Configuration dictionary
config_file: Path to save configuration file
Returns:
True if successful, False otherwise
"""
try:
with open(config_file, 'w') as f:
json.dump(config, f, indent=2)
return True
except Exception as e:
print(f"Error saving config: {e}")
return False
def format_timestamp(timestamp: float) -> str:
"""
Format timestamp as readable string.
Args:
timestamp: Unix timestamp
Returns:
Formatted timestamp string
"""
dt = datetime.fromtimestamp(timestamp)
return dt.strftime("%Y-%m-%d %H:%M:%S")
def format_duration(seconds: float) -> str:
"""
Format duration in seconds as readable string.
Args:
seconds: Duration in seconds
Returns:
Formatted duration string
"""
if seconds < 60:
return f"{seconds:.1f}s"
elif seconds < 3600:
minutes = seconds / 60
return f"{minutes:.1f}m"
else:
hours = seconds / 3600
return f"{hours:.1f}h"
def create_export_csv(detections: List[Dict], filename: str) -> bool:
"""
Export detections to CSV file.
Args:
detections: List of detection dictionaries
filename: Output CSV filename
Returns:
True if successful, False otherwise
"""
try:
import pandas as pd
# Create DataFrame from detections
rows = []
for det in detections:
row = {
'timestamp': det.get('timestamp', 0),
'class': det.get('class_name', 'unknown'),
'confidence': det.get('confidence', 0),
'x1': det.get('bbox', [0, 0, 0, 0])[0],
'y1': det.get('bbox', [0, 0, 0, 0])[1],
'x2': det.get('bbox', [0, 0, 0, 0])[2],
'y2': det.get('bbox', [0, 0, 0, 0])[3]
}
rows.append(row)
df = pd.DataFrame(rows)
# Save to CSV
df.to_csv(filename, index=False)
return True
except Exception as e:
print(f"Error exporting to CSV: {e}")
return False
def create_export_json(data: Dict, filename: str) -> bool:
"""
Export data to JSON file.
Args:
data: Data to export
filename: Output JSON filename
Returns:
True if successful, False otherwise
"""
try:
with open(filename, 'w') as f:
json.dump(data, f, indent=2)
return True
except Exception as e:
print(f"Error exporting to JSON: {e}")
return False
def create_unique_filename(prefix: str, ext: str) -> str:
"""
Create unique filename with timestamp.
Args:
prefix: Filename prefix
ext: File extension
Returns:
Unique filename
"""
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
return f"{prefix}_{timestamp}.{ext}"
def save_snapshot(frame: np.ndarray, filename: str = None) -> str:
"""
Save video frame as image file.
Args:
frame: Video frame
filename: Output filename (optional)
Returns:
Path to saved image
"""
if filename is None:
filename = create_unique_filename("snapshot", "jpg")
try:
cv2.imwrite(filename, frame)
return filename
except Exception as e:
print(f"Error saving snapshot: {e}")
return None
def get_video_properties(source):
"""
Get video file properties.
Args:
source: Video source (file path or device number)
Returns:
Dictionary of video properties
"""
try:
cap = cv2.VideoCapture(source)
if not cap.isOpened():
return {}
props = {
'width': int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)),
'height': int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)),
'fps': cap.get(cv2.CAP_PROP_FPS),
'frame_count': int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
}
cap.release()
return props
except Exception as e:
print(f"Error getting video properties: {e}")
return {}

View File

View File

@@ -0,0 +1,533 @@
"""
Traffic light color detection utilities
"""
import cv2
import numpy as np
import os
import time
from typing import Dict, List, Tuple, Optional
import logging
from collections import Counter, deque
# HSV thresholds as config constants
HSV_THRESHOLDS = {
"red": [
(np.array([0, 40, 40]), np.array([15, 255, 255])), # Lower red range (more permissive)
(np.array([160, 40, 40]), np.array([180, 255, 255])) # Upper red range (more permissive)
],
"yellow": [
(np.array([15, 50, 50]), np.array([40, 255, 255])) # Wider yellow range
],
"green": [
(np.array([35, 25, 25]), np.array([95, 255, 255])) # More permissive green range
]
}
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)
# History buffer for smoothing (can be used in controller)
COLOR_HISTORY = []
HISTORY_SIZE = 5
# Global color history for temporal smoothing
COLOR_HISTORY_DICT = {}
HISTORY_LEN = 7 # Number of frames to smooth over
def get_light_id(bbox):
# Use bbox center as a simple unique key (rounded to nearest 10 pixels)
x1, y1, x2, y2 = bbox
cx = int((x1 + x2) / 2 // 10 * 10)
cy = int((y1 + y2) / 2 // 10 * 10)
return (cx, cy)
def detect_dominant_color(hsv_img):
"""
Detect the dominant color in a traffic light based on simple HSV thresholding.
Useful as a fallback for small traffic lights where circle detection may fail.
"""
h, w = hsv_img.shape[:2]
# Create masks for each color
color_masks = {}
color_areas = {}
# Create a visualization image for debugging
debug_img = cv2.cvtColor(hsv_img, cv2.COLOR_HSV2BGR)
for color, thresholds in HSV_THRESHOLDS.items():
mask = np.zeros((h, w), dtype=np.uint8)
for lower, upper in thresholds:
color_mask = cv2.inRange(hsv_img, lower, upper)
mask = cv2.bitwise_or(mask, color_mask)
# Calculate the percentage of pixels matching each color
color_areas[color] = np.count_nonzero(mask) / (h * w) if h * w > 0 else 0
# Create a colored mask for visualization
color_viz = np.zeros((h, w, 3), dtype=np.uint8)
if color == "red":
color_viz[:, :] = [0, 0, 255] # BGR red
elif color == "yellow":
color_viz[:, :] = [0, 255, 255] # BGR yellow
elif color == "green":
color_viz[:, :] = [0, 255, 0] # BGR green
# Apply the mask to the color
color_viz = cv2.bitwise_and(color_viz, color_viz, mask=mask)
# Blend with debug image for visualization
alpha = 0.5
mask_expanded = cv2.cvtColor(mask, cv2.COLOR_GRAY2BGR) / 255.0
debug_img = debug_img * (1 - alpha * mask_expanded) + color_viz * (alpha * mask_expanded)
# Show debug visualization
cv2.imshow(f"Color Masks", debug_img.astype(np.uint8))
cv2.waitKey(1)
# Debug output
print(f"Color areas: Red={color_areas.get('red', 0):.3f}, Yellow={color_areas.get('yellow', 0):.3f}, Green={color_areas.get('green', 0):.3f}")
# If any color exceeds the threshold, consider it detected
best_color = max(color_areas.items(), key=lambda x: x[1]) if color_areas else ("unknown", 0)
# Only return a color if it has a minimum area percentage
if best_color[1] > 0.02: # at least 2% of pixels match the color (reduced from 3%)
return best_color[0], best_color[1]
return "unknown", 0
def detect_traffic_light_color(frame: np.ndarray, bbox: list) -> dict:
from collections import Counter
x1, y1, x2, y2 = [int(v) for v in bbox]
h, w = frame.shape[:2]
x1 = max(0, min(x1, w-1))
y1 = max(0, min(y1, h-1))
x2 = max(0, min(x2, w-1))
y2 = max(0, min(y2, h-1))
if x2 <= x1 or y2 <= y1:
return {"color": "unknown", "confidence": 0.0}
roi = frame[y1:y2, x1:x2]
if roi.size == 0:
return {"color": "unknown", "confidence": 0.0}
roi = cv2.resize(roi, (32, 64))
roi = cv2.GaussianBlur(roi, (5, 5), 0)
hsv = cv2.cvtColor(roi, cv2.COLOR_BGR2HSV)
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
hsv[..., 2] = clahe.apply(hsv[..., 2])
red_lower1 = np.array([0, 120, 120])
red_upper1 = np.array([10, 255, 255])
red_lower2 = np.array([160, 120, 120])
red_upper2 = np.array([180, 255, 255])
yellow_lower = np.array([18, 110, 110])
yellow_upper = np.array([38, 255, 255])
green_lower = np.array([42, 90, 90])
green_upper = np.array([90, 255, 255])
red_mask1 = cv2.inRange(hsv, red_lower1, red_upper1)
red_mask2 = cv2.inRange(hsv, red_lower2, red_upper2)
red_mask = cv2.bitwise_or(red_mask1, red_mask2)
yellow_mask = cv2.inRange(hsv, yellow_lower, yellow_upper)
green_mask = cv2.inRange(hsv, green_lower, green_upper)
red_count = cv2.countNonZero(red_mask)
yellow_count = cv2.countNonZero(yellow_mask)
green_count = cv2.countNonZero(green_mask)
total_pixels = hsv.shape[0] * hsv.shape[1]
red_ratio = red_count / total_pixels
yellow_ratio = yellow_count / total_pixels
green_ratio = green_count / total_pixels
color_counts = {'red': red_count, 'yellow': yellow_count, 'green': green_count}
color_ratios = {'red': red_ratio, 'yellow': yellow_ratio, 'green': green_ratio}
print(f"[DEBUG] ratios: red={red_ratio:.3f}, yellow={yellow_ratio:.3f}, green={green_ratio:.3f}")
# --- Improved Decision Logic ---
min_area = 0.025 # 2.5% of ROI must be the color
dominance_margin = 1.5 # Must be 50% more pixels than next best
detected_color = "unknown"
confidence = 0.0
if green_ratio > min_area:
if red_ratio < 2 * green_ratio:
detected_color = "green"
confidence = float(green_ratio)
if detected_color == "unknown" and yellow_ratio > min_area:
if red_ratio < 1.5 * yellow_ratio:
detected_color = "yellow"
confidence = float(yellow_ratio)
if detected_color == "unknown" and red_ratio > min_area and red_ratio > green_ratio and red_ratio > yellow_ratio:
detected_color = "red"
confidence = float(red_ratio)
# Fallbacks (vertical thirds, hough, etc.)
if detected_color == "unknown":
# Fallback: vertical thirds (classic traffic light layout)
h_roi, w_roi = roi.shape[:2]
top_roi = roi[0:h_roi//3, :]
middle_roi = roi[h_roi//3:2*h_roi//3, :]
bottom_roi = roi[2*h_roi//3:, :]
try:
top_hsv = cv2.cvtColor(top_roi, cv2.COLOR_BGR2HSV)
middle_hsv = cv2.cvtColor(middle_roi, cv2.COLOR_BGR2HSV)
bottom_hsv = cv2.cvtColor(bottom_roi, cv2.COLOR_BGR2HSV)
top_avg = np.mean(top_hsv, axis=(0,1))
middle_avg = np.mean(middle_hsv, axis=(0,1))
bottom_avg = np.mean(bottom_hsv, axis=(0,1))
if (top_avg[0] <= 15 or top_avg[0] >= 160) and top_avg[1] > 40:
detected_color = "red"
confidence = 0.7
elif 18 <= middle_avg[0] <= 38 and middle_avg[1] > 40:
detected_color = "yellow"
confidence = 0.7
elif 42 <= bottom_avg[0] <= 90 and bottom_avg[1] > 35:
detected_color = "green"
confidence = 0.7
except Exception as e:
print(f"[DEBUG] thirds fallback error: {e}")
# If still unknown, try Hough Circle fallback
if detected_color == "unknown":
gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
gray = cv2.medianBlur(gray, 5)
circles = cv2.HoughCircles(
gray, cv2.HOUGH_GRADIENT, dp=1.2, minDist=5,
param1=50, param2=10, minRadius=3, maxRadius=15)
detected_colors = []
if circles is not None:
for circle in circles[0, :]:
cx, cy, r = map(int, circle)
if 0 <= cy < hsv.shape[0] and 0 <= cx < hsv.shape[1]:
h, s, v = hsv[cy, cx]
if (h <= 10 or h >= 160):
detected_colors.append("red")
elif 18 <= h <= 38:
detected_colors.append("yellow")
elif 42 <= h <= 90:
detected_colors.append("green")
if detected_colors:
counter = Counter(detected_colors)
detected_color, count = counter.most_common(1)[0]
confidence = count / len(detected_colors)
# --- Temporal Consistency Filtering ---
light_id = get_light_id(bbox)
if light_id not in COLOR_HISTORY_DICT:
COLOR_HISTORY_DICT[light_id] = deque(maxlen=HISTORY_LEN)
if detected_color != "unknown":
COLOR_HISTORY_DICT[light_id].append(detected_color)
# Soft voting
if len(COLOR_HISTORY_DICT[light_id]) > 0:
most_common = Counter(COLOR_HISTORY_DICT[light_id]).most_common(1)[0][0]
# Optionally, only output if the most common color is at least 2/3 of the buffer
count = Counter(COLOR_HISTORY_DICT[light_id])[most_common]
if count >= (len(COLOR_HISTORY_DICT[light_id]) // 2 + 1):
return {"color": most_common, "confidence": confidence}
# If not enough history, return current detected color
return {"color": detected_color, "confidence": confidence}
def detect_traffic_light_color_old(frame: np.ndarray, bbox: list) -> dict:
print("[DEBUG] detect_traffic_light_color called")
"""
Hybrid robust traffic light color detection:
1. Preprocess ROI (resize, blur, CLAHE, HSV)
2. Pixel-ratio HSV masking and thresholding (fast, robust)
3. If ambiguous, fallback to Hough Circle detection
Returns: {"color": str, "confidence": float}
"""
import cv2
import numpy as np
from collections import Counter
# --- Preprocessing ---
x1, y1, x2, y2 = [int(v) for v in bbox]
h, w = frame.shape[:2]
x1 = max(0, min(x1, w-1))
y1 = max(0, min(y1, h-1))
x2 = max(0, min(x2, w-1))
y2 = max(0, min(y2, h-1))
if x2 <= x1 or y2 <= y1:
return {"color": "unknown", "confidence": 0.0}
roi = frame[y1:y2, x1:x2]
if roi.size == 0:
return {"color": "unknown", "confidence": 0.0}
roi = cv2.resize(roi, (32, 64))
roi = cv2.GaussianBlur(roi, (5, 5), 0)
hsv = cv2.cvtColor(roi, cv2.COLOR_BGR2HSV)
# CLAHE on V channel
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
hsv[..., 2] = clahe.apply(hsv[..., 2])
# --- HSV Masking ---
# Refined thresholds
red_lower1 = np.array([0, 110, 110])
red_upper1 = np.array([10, 255, 255])
red_lower2 = np.array([160, 110, 110])
red_upper2 = np.array([180, 255, 255])
yellow_lower = np.array([18, 110, 110])
yellow_upper = np.array([38, 255, 255])
green_lower = np.array([42, 80, 80])
green_upper = np.array([90, 255, 255])
red_mask1 = cv2.inRange(hsv, red_lower1, red_upper1)
red_mask2 = cv2.inRange(hsv, red_lower2, red_upper2)
red_mask = cv2.bitwise_or(red_mask1, red_mask2)
yellow_mask = cv2.inRange(hsv, yellow_lower, yellow_upper)
green_mask = cv2.inRange(hsv, green_lower, green_upper)
# --- Pixel Counting ---
red_count = cv2.countNonZero(red_mask)
yellow_count = cv2.countNonZero(yellow_mask)
green_count = cv2.countNonZero(green_mask)
total_pixels = hsv.shape[0] * hsv.shape[1]
red_ratio = red_count / total_pixels
yellow_ratio = yellow_count / total_pixels
green_ratio = green_count / total_pixels
# Stricter threshold for red, slightly relaxed for green/yellow
thresholds = {'red': 0.04, 'yellow': 0.02, 'green': 0.02} # 4% for red, 2% for others
color = "unknown"
confidence = 0.0
# Prefer green/yellow if their ratio is close to red (within 80%)
if green_ratio > thresholds['green'] and green_ratio >= 0.8 * red_ratio:
color = "green"
confidence = green_ratio
elif yellow_ratio > thresholds['yellow'] and yellow_ratio >= 0.8 * red_ratio:
color = "yellow"
confidence = yellow_ratio
elif red_ratio > thresholds['red']:
color = "red"
confidence = red_ratio
# --- If strong color found, return ---
if color != "unknown" and confidence > 0.01:
print(f"[DEBUG] detect_traffic_light_color result: {color}, confidence: {confidence:.2f}")
return {"color": color, "confidence": float(confidence)}
# --- Fallback: Hough Circle Detection ---
gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
gray = cv2.medianBlur(gray, 5)
circles = cv2.HoughCircles(
gray, cv2.HOUGH_GRADIENT, dp=1.2, minDist=5,
param1=50, param2=10, minRadius=3, maxRadius=15)
detected_colors = []
if circles is not None:
for circle in circles[0, :]:
cx, cy, r = map(int, circle)
if 0 <= cy < hsv.shape[0] and 0 <= cx < hsv.shape[1]:
h, s, v = hsv[cy, cx]
if (h <= 10 or h >= 160):
detected_colors.append("red")
elif 18 <= h <= 38:
detected_colors.append("yellow")
elif 42 <= h <= 90:
detected_colors.append("green")
if detected_colors:
counter = Counter(detected_colors)
final_color, count = counter.most_common(1)[0]
confidence = count / len(detected_colors)
print(f"[DEBUG] detect_traffic_light_color (hough): {final_color}, confidence: {confidence:.2f}")
return {"color": final_color, "confidence": float(confidence)}
# --- If still unknown, return unknown ---
print("[DEBUG] detect_traffic_light_color result: unknown")
return {"color": "unknown", "confidence": 0.0}
def draw_traffic_light_status(frame: np.ndarray, bbox: List[int], color_info) -> np.ndarray:
"""
Draw traffic light status on the frame with confidence score.
Args:
frame: Image to draw on
bbox: Bounding box coordinates [x1, y1, x2, y2]
color_info: Either a string ("red", "yellow", "green", "unknown") or
a dict {"color": str, "confidence": float}
Returns:
Frame with color status drawn
"""
try:
# Handle both string and dictionary formats
if isinstance(color_info, dict):
color = color_info.get("color", "unknown")
confidence = color_info.get("confidence", 0.0)
confidence_text = f"{confidence:.2f}"
else:
color = color_info
confidence_text = ""
# Debug message
print(f"📝 Drawing traffic light status: {color} at bbox {bbox}")
# Parse and validate bbox
x1, y1, x2, y2 = [int(c) for c in bbox]
# Define color for drawing
status_colors = {
"red": (0, 0, 255), # BGR: Red
"yellow": (0, 255, 255), # BGR: Yellow
"green": (0, 255, 0), # BGR: Green
"unknown": (255, 255, 255) # BGR: White
}
draw_color = status_colors.get(color, (255, 255, 255))
# Draw rectangle with color-specific border (thicker for visibility)
cv2.rectangle(frame, (x1, y1), (x2, y2), draw_color, 3)
# Add text label with the color and confidence if available
if confidence_text:
label = f"Traffic Light: {color.upper()} ({confidence_text})"
else:
label = f"Traffic Light: {color.upper()}"
text_size, _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.7, 2)
# Draw background rectangle for text
cv2.rectangle(
frame,
(x1, y1 - text_size[1] - 10),
(x1 + text_size[0], y1),
draw_color,
-1
)
# Draw text
cv2.putText(
frame,
label,
(x1, y1 - 5),
cv2.FONT_HERSHEY_SIMPLEX,
0.7,
(0, 0, 0), # Black text
2
)
# Also draw a large indicator at the top of the frame for high visibility
indicator_size = 30
margin = 10
# Draw colored circle indicator at top-right
cv2.circle(
frame,
(frame.shape[1] - margin - indicator_size, margin + indicator_size),
indicator_size,
draw_color,
-1
)
# Remove the extra white rectangle/text from the UI overlay
# In draw_traffic_light_status, the white rectangle and text are likely drawn by this block:
# cv2.circle(
# frame,
# (frame.shape[1] - margin - indicator_size, margin + indicator_size),
# indicator_size,
# draw_color,
# -1
# )
# cv2.putText(
# frame,
# color.upper(),
# (frame.shape[1] - margin - indicator_size*2 - 80, margin + indicator_size + 10),
# cv2.FONT_HERSHEY_SIMPLEX,
# 1.0,
# draw_color,
# 3
# )
# To remove the white overlay, comment out or remove the cv2.putText line for the color text at the top.
# Only keep the circle indicator if you want, or remove both if you want no indicator at the top.
# Let's remove the cv2.putText for color at the top.
return frame
except Exception as e:
print(f"❌ Error drawing traffic light status: {e}")
import traceback
traceback.print_exc()
return frame
def ensure_traffic_light_color(frame, bbox):
print("[DEBUG] ensure_traffic_light_color called")
"""
Emergency function to always return a traffic light color even with poor quality crops.
This function is less strict and will fall back to enforced color detection.
"""
try:
# First try the regular detection
result = detect_traffic_light_color(frame, bbox)
if isinstance(result, dict) and result.get('color', 'unknown') != 'unknown':
print(f"[DEBUG] ensure_traffic_light_color result (from detect): {result}")
return result
# If we got unknown, extract traffic light region again
x1, y1, x2, y2 = [int(c) for c in bbox]
h, w = frame.shape[:2]
x1 = max(0, min(x1, w-1))
y1 = max(0, min(y1, h-1))
x2 = max(0, min(x2, w-1))
y2 = max(0, min(y2, h-1))
if x2 <= x1 or y2 <= y1:
print("❌ Invalid bbox for traffic light")
return {"color": "unknown", "confidence": 0.0}
roi = frame[y1:y2, x1:x2]
if roi.size == 0:
print("❌ Empty ROI for traffic light")
return {"color": "unknown", "confidence": 0.0}
# Try analyzing by vertical thirds (typical traffic light pattern)
h_roi, w_roi = roi.shape[:2]
top_roi = roi[0:h_roi//3, :]
middle_roi = roi[h_roi//3:2*h_roi//3, :]
bottom_roi = roi[2*h_roi//3:, :]
try:
top_hsv = cv2.cvtColor(top_roi, cv2.COLOR_BGR2HSV)
middle_hsv = cv2.cvtColor(middle_roi, cv2.COLOR_BGR2HSV)
bottom_hsv = cv2.cvtColor(bottom_roi, cv2.COLOR_BGR2HSV)
top_avg = np.mean(top_hsv, axis=(0,1))
middle_avg = np.mean(middle_hsv, axis=(0,1))
bottom_avg = np.mean(bottom_hsv, axis=(0,1))
print(f"Traffic light regions - Top HSV: {top_avg}, Middle HSV: {middle_avg}, Bottom HSV: {bottom_avg}")
# Check for red in top
if (top_avg[0] <= 15 or top_avg[0] >= 160) and top_avg[1] > 40:
return {"color": "red", "confidence": 0.7}
# Check for yellow in middle
if 18 <= middle_avg[0] <= 38 and middle_avg[1] > 40:
return {"color": "yellow", "confidence": 0.7}
# Check for green in bottom
if 42 <= bottom_avg[0] <= 90 and bottom_avg[1] > 35:
return {"color": "green", "confidence": 0.7}
except:
pass
# If we still haven't found a color, look at overall color distribution
try:
hsv_roi = cv2.cvtColor(roi, cv2.COLOR_BGR2HSV)
very_permissive_red1 = cv2.inRange(hsv_roi, np.array([0, 30, 30]), np.array([20, 255, 255]))
very_permissive_red2 = cv2.inRange(hsv_roi, np.array([155, 30, 30]), np.array([180, 255, 255]))
very_permissive_red = cv2.bitwise_or(very_permissive_red1, very_permissive_red2)
very_permissive_yellow = cv2.inRange(hsv_roi, np.array([10, 30, 30]), np.array([45, 255, 255]))
very_permissive_green = cv2.inRange(hsv_roi, np.array([30, 20, 20]), np.array([100, 255, 255]))
red_count = cv2.countNonZero(very_permissive_red)
yellow_count = cv2.countNonZero(very_permissive_yellow)
green_count = cv2.countNonZero(very_permissive_green)
total_pixels = hsv_roi.shape[0] * hsv_roi.shape[1]
print(f"Very permissive detection: Red={red_count/total_pixels:.3f}, Yellow={yellow_count/total_pixels:.3f}, Green={green_count/total_pixels:.3f}")
max_count = max(red_count, yellow_count, green_count)
if max_count > 0:
# Prefer green/yellow if close to red
if green_count == max_count and green_count >= 0.9 * red_count:
return {"color": "green", "confidence": 0.5 * green_count/total_pixels}
elif yellow_count == max_count and yellow_count >= 0.9 * red_count:
return {"color": "yellow", "confidence": 0.5 * yellow_count/total_pixels}
elif red_count == max_count:
return {"color": "red", "confidence": 0.5 * red_count/total_pixels}
except Exception as e:
print(f"❌ Error in permissive analysis: {e}")
# Last resort - analyze mean color
mean_color = np.mean(roi, axis=(0,1))
b, g, r = mean_color
if r > g and r > b and r > 60:
return {"color": "red", "confidence": 0.4}
elif g > r and g > b and g > 60:
return {"color": "green", "confidence": 0.4}
elif r > 70 and g > 70 and r/g > 0.7 and r/g < 1.3:
return {"color": "yellow", "confidence": 0.4}
print("[DEBUG] ensure_traffic_light_color fallback to unknown")
return {"color": "unknown", "confidence": 0.0}
except Exception as e:
print(f"❌ Error in ensure_traffic_light_color: {e}")
import traceback
traceback.print_exc()
return {"color": "unknown", "confidence": 0.0}