Clean push: Removed heavy files & added only latest snapshot
This commit is contained in:
5
qt_app_pyside1/utils/__init__.py
Normal file
5
qt_app_pyside1/utils/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""
|
||||
Utils package initialization
|
||||
"""
|
||||
|
||||
# This file marks the directory as a Python package
|
||||
BIN
qt_app_pyside1/utils/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
qt_app_pyside1/utils/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
qt_app_pyside1/utils/__pycache__/helpers.cpython-311.pyc
Normal file
BIN
qt_app_pyside1/utils/__pycache__/helpers.cpython-311.pyc
Normal file
Binary file not shown.
BIN
qt_app_pyside1/utils/__pycache__/mqtt_publisher.cpython-311.pyc
Normal file
BIN
qt_app_pyside1/utils/__pycache__/mqtt_publisher.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
304
qt_app_pyside1/utils/annotation_utils.py
Normal file
304
qt_app_pyside1/utils/annotation_utils.py
Normal 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
|
||||
65
qt_app_pyside1/utils/classical_crosswalk.py
Normal file
65
qt_app_pyside1/utils/classical_crosswalk.py
Normal 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)
|
||||
50
qt_app_pyside1/utils/classical_traffic_light.py
Normal file
50
qt_app_pyside1/utils/classical_traffic_light.py
Normal 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)
|
||||
951
qt_app_pyside1/utils/crosswalk_backup.py
Normal file
951
qt_app_pyside1/utils/crosswalk_backup.py
Normal 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)
|
||||
462
qt_app_pyside1/utils/crosswalk_utils.py
Normal file
462
qt_app_pyside1/utils/crosswalk_utils.py
Normal 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)
|
||||
649
qt_app_pyside1/utils/crosswalk_utils1.py
Normal file
649
qt_app_pyside1/utils/crosswalk_utils1.py
Normal 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)
|
||||
337
qt_app_pyside1/utils/crosswalk_utils2.py
Normal file
337
qt_app_pyside1/utils/crosswalk_utils2.py
Normal 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)
|
||||
623
qt_app_pyside1/utils/crosswalk_utils_advanced.py
Normal file
623
qt_app_pyside1/utils/crosswalk_utils_advanced.py
Normal 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)
|
||||
73
qt_app_pyside1/utils/custom_classical_crosswalk.py
Normal file
73
qt_app_pyside1/utils/custom_classical_crosswalk.py
Normal 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
|
||||
43
qt_app_pyside1/utils/custom_classical_traffic_light.py
Normal file
43
qt_app_pyside1/utils/custom_classical_traffic_light.py
Normal 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)
|
||||
318
qt_app_pyside1/utils/embedder_openvino.py
Normal file
318
qt_app_pyside1/utils/embedder_openvino.py
Normal 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
|
||||
414
qt_app_pyside1/utils/enhanced_annotation_utils.py
Normal file
414
qt_app_pyside1/utils/enhanced_annotation_utils.py
Normal 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
|
||||
279
qt_app_pyside1/utils/helpers.py
Normal file
279
qt_app_pyside1/utils/helpers.py
Normal 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 {}
|
||||
0
qt_app_pyside1/utils/mqtt_publisher.py
Normal file
0
qt_app_pyside1/utils/mqtt_publisher.py
Normal file
533
qt_app_pyside1/utils/traffic_light_utils.py
Normal file
533
qt_app_pyside1/utils/traffic_light_utils.py
Normal 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}
|
||||
Reference in New Issue
Block a user