Spaces:
Sleeping
Sleeping
copilot-swe-agent[bot]
kr4phy
commited on
Commit
·
4ae3a76
1
Parent(s):
63ea6ab
Add progress bar support and new lane detection methods
Browse filesCo-authored-by: kr4phy <168257476+kr4phy@users.noreply.github.com>
- app.py +63 -20
- lane_detection.py +373 -9
app.py
CHANGED
|
@@ -3,10 +3,10 @@ import tempfile
|
|
| 3 |
from lane_detection import process_video as process_video_file
|
| 4 |
|
| 5 |
|
| 6 |
-
def process_video(video_path, method, use_enhanced, use_segmented):
|
| 7 |
"""
|
| 8 |
Process the uploaded video and return side-by-side comparison.
|
| 9 |
-
Wrapper function for Gradio interface.
|
| 10 |
"""
|
| 11 |
if video_path is None:
|
| 12 |
return None
|
|
@@ -16,8 +16,12 @@ def process_video(video_path, method, use_enhanced, use_segmented):
|
|
| 16 |
output_path = temp_output.name
|
| 17 |
temp_output.close()
|
| 18 |
|
| 19 |
-
#
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
if success:
|
| 23 |
return output_path
|
|
@@ -28,7 +32,7 @@ def process_video(video_path, method, use_enhanced, use_segmented):
|
|
| 28 |
# Create Gradio interface
|
| 29 |
with gr.Blocks(title="Lane Detection Demo") as demo:
|
| 30 |
gr.Markdown("# 🚗 OpenCV Lane Detection Demo")
|
| 31 |
-
gr.Markdown("Upload a video to detect lane lines. Choose between
|
| 32 |
|
| 33 |
with gr.Row():
|
| 34 |
with gr.Column():
|
|
@@ -36,10 +40,17 @@ with gr.Blocks(title="Lane Detection Demo") as demo:
|
|
| 36 |
|
| 37 |
with gr.Row():
|
| 38 |
method_selector = gr.Radio(
|
| 39 |
-
choices=[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
value="advanced",
|
| 41 |
label="Detection Method",
|
| 42 |
-
info="
|
| 43 |
)
|
| 44 |
|
| 45 |
enhanced_checkbox = gr.Checkbox(
|
|
@@ -64,7 +75,7 @@ with gr.Blocks(title="Lane Detection Demo") as demo:
|
|
| 64 |
# Update checkbox visibility based on method
|
| 65 |
def update_checkboxes(method):
|
| 66 |
enhanced_visible = (method == "advanced")
|
| 67 |
-
segmented_visible = (method
|
| 68 |
return gr.Checkbox(visible=enhanced_visible), gr.Checkbox(visible=segmented_visible)
|
| 69 |
|
| 70 |
method_selector.change(
|
|
@@ -82,31 +93,63 @@ with gr.Blocks(title="Lane Detection Demo") as demo:
|
|
| 82 |
gr.Markdown("""
|
| 83 |
### Detection Methods:
|
| 84 |
|
| 85 |
-
**🔹 Basic
|
| 86 |
- Fast and lightweight
|
| 87 |
- Good for straight lanes
|
| 88 |
-
-
|
| 89 |
-
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
|
| 91 |
-
**🔹 Advanced
|
| 92 |
- Perspective transform to bird's eye view
|
| 93 |
- Polynomial fitting with sliding windows
|
| 94 |
- Excellent for curved and dashed lanes
|
| 95 |
-
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
|
| 97 |
### How it works:
|
| 98 |
1. Upload a video file containing road scenes
|
| 99 |
-
2. Select detection method
|
| 100 |
-
- For **Basic
|
| 101 |
-
- For
|
| 102 |
-
|
|
|
|
|
|
|
| 103 |
4. The system will process each frame and create a side-by-side comparison
|
| 104 |
5. Download the result video showing original and detected lanes
|
| 105 |
|
| 106 |
### Tips:
|
| 107 |
-
-
|
| 108 |
-
-
|
| 109 |
-
-
|
|
|
|
|
|
|
| 110 |
""")
|
| 111 |
|
| 112 |
|
|
|
|
| 3 |
from lane_detection import process_video as process_video_file
|
| 4 |
|
| 5 |
|
| 6 |
+
def process_video(video_path, method, use_enhanced, use_segmented, progress=gr.Progress()):
|
| 7 |
"""
|
| 8 |
Process the uploaded video and return side-by-side comparison.
|
| 9 |
+
Wrapper function for Gradio interface with progress tracking.
|
| 10 |
"""
|
| 11 |
if video_path is None:
|
| 12 |
return None
|
|
|
|
| 16 |
output_path = temp_output.name
|
| 17 |
temp_output.close()
|
| 18 |
|
| 19 |
+
# Progress callback function
|
| 20 |
+
def update_progress(value, desc):
|
| 21 |
+
progress(value, desc=desc)
|
| 22 |
+
|
| 23 |
+
# Process the video with selected method and progress callback
|
| 24 |
+
success = process_video_file(video_path, output_path, method, use_enhanced, use_segmented, progress_callback=update_progress)
|
| 25 |
|
| 26 |
if success:
|
| 27 |
return output_path
|
|
|
|
| 32 |
# Create Gradio interface
|
| 33 |
with gr.Blocks(title="Lane Detection Demo") as demo:
|
| 34 |
gr.Markdown("# 🚗 OpenCV Lane Detection Demo")
|
| 35 |
+
gr.Markdown("Upload a video to detect lane lines. Choose between multiple advanced methods.")
|
| 36 |
|
| 37 |
with gr.Row():
|
| 38 |
with gr.Column():
|
|
|
|
| 40 |
|
| 41 |
with gr.Row():
|
| 42 |
method_selector = gr.Radio(
|
| 43 |
+
choices=[
|
| 44 |
+
"basic_standard",
|
| 45 |
+
"basic_segmented",
|
| 46 |
+
"advanced",
|
| 47 |
+
"yolop",
|
| 48 |
+
"ufld",
|
| 49 |
+
"scnn"
|
| 50 |
+
],
|
| 51 |
value="advanced",
|
| 52 |
label="Detection Method",
|
| 53 |
+
info="Select lane detection algorithm"
|
| 54 |
)
|
| 55 |
|
| 56 |
enhanced_checkbox = gr.Checkbox(
|
|
|
|
| 75 |
# Update checkbox visibility based on method
|
| 76 |
def update_checkboxes(method):
|
| 77 |
enhanced_visible = (method == "advanced")
|
| 78 |
+
segmented_visible = (method in ["basic_standard", "basic_segmented"])
|
| 79 |
return gr.Checkbox(visible=enhanced_visible), gr.Checkbox(visible=segmented_visible)
|
| 80 |
|
| 81 |
method_selector.change(
|
|
|
|
| 93 |
gr.Markdown("""
|
| 94 |
### Detection Methods:
|
| 95 |
|
| 96 |
+
**🔹 Basic Standard (Hough Transform):**
|
| 97 |
- Fast and lightweight
|
| 98 |
- Good for straight lanes
|
| 99 |
+
- Uses single averaged line per lane
|
| 100 |
+
- Fastest processing speed
|
| 101 |
+
|
| 102 |
+
**🔹 Basic Segmented (Hough Transform):**
|
| 103 |
+
- Fast processing
|
| 104 |
+
- Multiple line segments for better curve representation
|
| 105 |
+
- Good for moderately curved lanes
|
| 106 |
+
- Better than basic for curves
|
| 107 |
|
| 108 |
+
**🔹 Advanced (Perspective Transform + Polynomial):**
|
| 109 |
- Perspective transform to bird's eye view
|
| 110 |
- Polynomial fitting with sliding windows
|
| 111 |
- Excellent for curved and dashed lanes
|
| 112 |
+
- Enhanced mode uses CLAHE and gradient filtering
|
| 113 |
+
- Best accuracy but slower
|
| 114 |
+
|
| 115 |
+
**🔹 YOLOP (Multi-task Learning):**
|
| 116 |
+
- Inspired by YOLOP (You Only Look Once for Panoptic Driving)
|
| 117 |
+
- Multi-color lane detection (white and yellow)
|
| 118 |
+
- Contour-based segmentation
|
| 119 |
+
- Good for various lane colors
|
| 120 |
+
- Fast with good accuracy
|
| 121 |
+
|
| 122 |
+
**🔹 UFLD (Ultra Fast Lane Detection):**
|
| 123 |
+
- Inspired by Ultra Fast Structure-aware Deep Lane Detection
|
| 124 |
+
- Row-wise classification approach
|
| 125 |
+
- Adaptive thresholding with CLAHE
|
| 126 |
+
- Excellent balance of speed and accuracy
|
| 127 |
+
- Real-time capable
|
| 128 |
+
|
| 129 |
+
**🔹 SCNN (Spatial CNN):**
|
| 130 |
+
- Inspired by Spatial CNN for traffic lane detection
|
| 131 |
+
- Spatial message passing in four directions
|
| 132 |
+
- Multi-scale edge detection
|
| 133 |
+
- Best for complex scenarios
|
| 134 |
+
- High accuracy for challenging conditions
|
| 135 |
|
| 136 |
### How it works:
|
| 137 |
1. Upload a video file containing road scenes
|
| 138 |
+
2. Select detection method:
|
| 139 |
+
- For fastest: Use **Basic Standard**
|
| 140 |
+
- For curves: Use **Basic Segmented**, **UFLD**, or **Advanced**
|
| 141 |
+
- For best accuracy: Use **SCNN** or **Advanced + Enhanced**
|
| 142 |
+
- For multi-color lanes: Use **YOLOP**
|
| 143 |
+
3. Click "Process Video" button and monitor the progress bar
|
| 144 |
4. The system will process each frame and create a side-by-side comparison
|
| 145 |
5. Download the result video showing original and detected lanes
|
| 146 |
|
| 147 |
### Tips:
|
| 148 |
+
- **Basic Standard/Segmented**: Fastest, good for straight or gentle curves
|
| 149 |
+
- **YOLOP**: Best for detecting both white and yellow lanes
|
| 150 |
+
- **UFLD**: Excellent balance of speed and accuracy
|
| 151 |
+
- **Advanced + Enhanced**: Best for dashed and curved lanes
|
| 152 |
+
- **SCNN**: Best overall accuracy for complex road conditions
|
| 153 |
""")
|
| 154 |
|
| 155 |
|
lane_detection.py
CHANGED
|
@@ -527,19 +527,376 @@ def draw_poly_lines(img, binary_warped, left_fit, right_fit, Minv):
|
|
| 527 |
return result
|
| 528 |
|
| 529 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 530 |
def process_frame(frame, method="advanced", use_enhanced=True, use_segmented=False):
|
| 531 |
"""
|
| 532 |
Process a single frame for lane detection.
|
| 533 |
-
method: "basic"
|
| 534 |
use_enhanced: Use enhanced thresholding for better accuracy (advanced method only)
|
| 535 |
use_segmented: Use segmented lines for curve representation (basic method only)
|
| 536 |
"""
|
| 537 |
-
if method == "basic":
|
| 538 |
-
return process_frame_basic(frame, use_segmented)
|
|
|
|
|
|
|
| 539 |
elif method == "advanced":
|
| 540 |
return process_frame_advanced(frame, use_enhanced)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 541 |
else:
|
| 542 |
-
raise ValueError(f"Unknown method: {method}. Use 'basic'
|
| 543 |
|
| 544 |
|
| 545 |
def process_frame_advanced(frame, use_enhanced=True):
|
|
@@ -565,12 +922,13 @@ def process_frame_advanced(frame, use_enhanced=True):
|
|
| 565 |
return result
|
| 566 |
|
| 567 |
|
| 568 |
-
def process_video(input_path, output_path, method="advanced", use_enhanced=True, use_segmented=False):
|
| 569 |
"""
|
| 570 |
Process the video and create side-by-side comparison.
|
| 571 |
-
method: "basic"
|
| 572 |
use_enhanced: Use enhanced thresholding for better accuracy (advanced method only)
|
| 573 |
use_segmented: Use segmented lines for curve representation (basic method only)
|
|
|
|
| 574 |
Returns True if successful, False otherwise.
|
| 575 |
"""
|
| 576 |
# Open the video
|
|
@@ -586,8 +944,8 @@ def process_video(input_path, output_path, method="advanced", use_enhanced=True,
|
|
| 586 |
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
| 587 |
|
| 588 |
# Video writer for output (side-by-side, so width is doubled)
|
| 589 |
-
# Use
|
| 590 |
-
fourcc = cv2.VideoWriter_fourcc(*'
|
| 591 |
out = cv2.VideoWriter(output_path, fourcc, fps, (width * 2, height))
|
| 592 |
|
| 593 |
frame_count = 0
|
|
@@ -615,7 +973,10 @@ def process_video(input_path, output_path, method="advanced", use_enhanced=True,
|
|
| 615 |
frame_count += 1
|
| 616 |
|
| 617 |
# Progress indicator
|
| 618 |
-
if frame_count %
|
|
|
|
|
|
|
|
|
|
| 619 |
progress = (frame_count / total_frames) * 100 if total_frames > 0 else 0
|
| 620 |
print(f"Progress: {frame_count}/{total_frames} frames ({progress:.1f}%)")
|
| 621 |
|
|
@@ -623,6 +984,9 @@ def process_video(input_path, output_path, method="advanced", use_enhanced=True,
|
|
| 623 |
cap.release()
|
| 624 |
out.release()
|
| 625 |
|
|
|
|
|
|
|
|
|
|
| 626 |
print(f"✓ Completed! Processed {frame_count} frames using {method} method.")
|
| 627 |
|
| 628 |
return frame_count > 0
|
|
|
|
| 527 |
return result
|
| 528 |
|
| 529 |
|
| 530 |
+
def process_frame_yolop(frame):
|
| 531 |
+
"""
|
| 532 |
+
YOLOP-inspired lane detection method.
|
| 533 |
+
Simulates multi-task learning approach with semantic segmentation.
|
| 534 |
+
Uses enhanced color-based segmentation with adaptive thresholding.
|
| 535 |
+
"""
|
| 536 |
+
height, width = frame.shape[:2]
|
| 537 |
+
|
| 538 |
+
# Convert to HLS for better color segmentation
|
| 539 |
+
hls = cv2.cvtColor(frame, cv2.COLOR_BGR2HLS)
|
| 540 |
+
h_channel = hls[:, :, 0]
|
| 541 |
+
l_channel = hls[:, :, 1]
|
| 542 |
+
s_channel = hls[:, :, 2]
|
| 543 |
+
|
| 544 |
+
# Multi-threshold approach for different lane colors
|
| 545 |
+
# White lanes - high lightness
|
| 546 |
+
white_mask = cv2.inRange(l_channel, 200, 255)
|
| 547 |
+
|
| 548 |
+
# Yellow lanes - specific hue range
|
| 549 |
+
yellow_mask = cv2.inRange(h_channel, 15, 35) & cv2.inRange(s_channel, 80, 255)
|
| 550 |
+
|
| 551 |
+
# Combine masks
|
| 552 |
+
color_mask = cv2.bitwise_or(white_mask, yellow_mask)
|
| 553 |
+
|
| 554 |
+
# Apply morphological operations
|
| 555 |
+
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
|
| 556 |
+
color_mask = cv2.morphologyEx(color_mask, cv2.MORPH_CLOSE, kernel)
|
| 557 |
+
color_mask = cv2.morphologyEx(color_mask, cv2.MORPH_OPEN, kernel)
|
| 558 |
+
|
| 559 |
+
# Apply ROI
|
| 560 |
+
vertices = np.array([[
|
| 561 |
+
(int(width * 0.1), height),
|
| 562 |
+
(int(width * 0.45), int(height * 0.6)),
|
| 563 |
+
(int(width * 0.55), int(height * 0.6)),
|
| 564 |
+
(int(width * 0.9), height)
|
| 565 |
+
]], dtype=np.int32)
|
| 566 |
+
|
| 567 |
+
color_mask = region_of_interest(color_mask, vertices)
|
| 568 |
+
|
| 569 |
+
# Find contours for lane segments
|
| 570 |
+
contours, _ = cv2.findContours(color_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
| 571 |
+
|
| 572 |
+
# Create output image
|
| 573 |
+
result = frame.copy()
|
| 574 |
+
overlay = np.zeros_like(frame)
|
| 575 |
+
|
| 576 |
+
# Separate left and right lane contours
|
| 577 |
+
left_contours = []
|
| 578 |
+
right_contours = []
|
| 579 |
+
|
| 580 |
+
midpoint = width // 2
|
| 581 |
+
for contour in contours:
|
| 582 |
+
if cv2.contourArea(contour) > 100:
|
| 583 |
+
M = cv2.moments(contour)
|
| 584 |
+
if M["m00"] != 0:
|
| 585 |
+
cx = int(M["m10"] / M["m00"])
|
| 586 |
+
if cx < midpoint:
|
| 587 |
+
left_contours.append(contour)
|
| 588 |
+
else:
|
| 589 |
+
right_contours.append(contour)
|
| 590 |
+
|
| 591 |
+
# Draw lane regions
|
| 592 |
+
if len(left_contours) > 0 or len(right_contours) > 0:
|
| 593 |
+
# Fill lane area
|
| 594 |
+
if len(left_contours) > 0 and len(right_contours) > 0:
|
| 595 |
+
# Get bounding points
|
| 596 |
+
left_points = np.vstack(left_contours).squeeze()
|
| 597 |
+
right_points = np.vstack(right_contours).squeeze()
|
| 598 |
+
|
| 599 |
+
if len(left_points.shape) == 2 and len(right_points.shape) == 2:
|
| 600 |
+
# Sort by y coordinate
|
| 601 |
+
left_points = left_points[left_points[:, 1].argsort()]
|
| 602 |
+
right_points = right_points[right_points[:, 1].argsort()]
|
| 603 |
+
|
| 604 |
+
# Create polygon
|
| 605 |
+
poly_points = np.vstack([left_points, right_points[::-1]])
|
| 606 |
+
cv2.fillPoly(overlay, [poly_points], (0, 255, 0))
|
| 607 |
+
|
| 608 |
+
# Draw lane lines
|
| 609 |
+
for contour in left_contours:
|
| 610 |
+
cv2.drawContours(overlay, [contour], -1, (0, 0, 255), 5)
|
| 611 |
+
for contour in right_contours:
|
| 612 |
+
cv2.drawContours(overlay, [contour], -1, (0, 0, 255), 5)
|
| 613 |
+
|
| 614 |
+
# Blend with original
|
| 615 |
+
result = cv2.addWeighted(result, 0.8, overlay, 0.5, 0)
|
| 616 |
+
|
| 617 |
+
return result
|
| 618 |
+
|
| 619 |
+
|
| 620 |
+
def process_frame_ufld(frame):
|
| 621 |
+
"""
|
| 622 |
+
UFLD-inspired (Ultra Fast Lane Detection) method.
|
| 623 |
+
Uses row-wise classification approach with efficient feature extraction.
|
| 624 |
+
Focuses on speed and accuracy for real-time applications.
|
| 625 |
+
"""
|
| 626 |
+
height, width = frame.shape[:2]
|
| 627 |
+
|
| 628 |
+
# Convert to grayscale
|
| 629 |
+
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
| 630 |
+
|
| 631 |
+
# Apply CLAHE for enhanced contrast
|
| 632 |
+
clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8, 8))
|
| 633 |
+
enhanced = clahe.apply(gray)
|
| 634 |
+
|
| 635 |
+
# Apply bilateral filter to preserve edges while reducing noise
|
| 636 |
+
filtered = cv2.bilateralFilter(enhanced, 9, 75, 75)
|
| 637 |
+
|
| 638 |
+
# Adaptive thresholding
|
| 639 |
+
binary = cv2.adaptiveThreshold(
|
| 640 |
+
filtered, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
|
| 641 |
+
cv2.THRESH_BINARY, 11, 2
|
| 642 |
+
)
|
| 643 |
+
|
| 644 |
+
# Apply ROI
|
| 645 |
+
vertices = np.array([[
|
| 646 |
+
(int(width * 0.1), height),
|
| 647 |
+
(int(width * 0.45), int(height * 0.6)),
|
| 648 |
+
(int(width * 0.55), int(height * 0.6)),
|
| 649 |
+
(int(width * 0.9), height)
|
| 650 |
+
]], dtype=np.int32)
|
| 651 |
+
|
| 652 |
+
binary = region_of_interest(binary, vertices)
|
| 653 |
+
|
| 654 |
+
# Row-wise lane point detection
|
| 655 |
+
row_samples = 18 # Number of rows to sample
|
| 656 |
+
row_step = height // row_samples
|
| 657 |
+
|
| 658 |
+
left_lane_points = []
|
| 659 |
+
right_lane_points = []
|
| 660 |
+
|
| 661 |
+
midpoint = width // 2
|
| 662 |
+
|
| 663 |
+
for i in range(row_samples):
|
| 664 |
+
y = height - i * row_step - row_step // 2
|
| 665 |
+
if y < int(height * 0.6):
|
| 666 |
+
continue
|
| 667 |
+
|
| 668 |
+
row = binary[y, :]
|
| 669 |
+
|
| 670 |
+
# Find peaks in left and right halves
|
| 671 |
+
left_half = row[:midpoint]
|
| 672 |
+
right_half = row[midpoint:]
|
| 673 |
+
|
| 674 |
+
# Find lane positions
|
| 675 |
+
left_peaks = np.where(left_half > 200)[0]
|
| 676 |
+
right_peaks = np.where(right_half > 200)[0]
|
| 677 |
+
|
| 678 |
+
if len(left_peaks) > 0:
|
| 679 |
+
# Use the rightmost peak in left half
|
| 680 |
+
x = left_peaks[-1]
|
| 681 |
+
left_lane_points.append([x, y])
|
| 682 |
+
|
| 683 |
+
if len(right_peaks) > 0:
|
| 684 |
+
# Use the leftmost peak in right half
|
| 685 |
+
x = midpoint + right_peaks[0]
|
| 686 |
+
right_lane_points.append([x, y])
|
| 687 |
+
|
| 688 |
+
# Create result image
|
| 689 |
+
result = frame.copy()
|
| 690 |
+
overlay = np.zeros_like(frame)
|
| 691 |
+
|
| 692 |
+
# Fit curves to lane points
|
| 693 |
+
if len(left_lane_points) >= 3:
|
| 694 |
+
left_lane_points = np.array(left_lane_points)
|
| 695 |
+
left_fit = np.polyfit(left_lane_points[:, 1], left_lane_points[:, 0], 2)
|
| 696 |
+
|
| 697 |
+
# Generate smooth curve
|
| 698 |
+
ploty = np.linspace(int(height * 0.6), height, 100)
|
| 699 |
+
left_fitx = left_fit[0] * ploty**2 + left_fit[1] * ploty + left_fit[2]
|
| 700 |
+
left_fitx = np.clip(left_fitx, 0, width - 1)
|
| 701 |
+
|
| 702 |
+
left_curve = np.array([np.transpose(np.vstack([left_fitx, ploty]))], dtype=np.int32)
|
| 703 |
+
cv2.polylines(overlay, left_curve, False, (0, 0, 255), 8)
|
| 704 |
+
|
| 705 |
+
if len(right_lane_points) >= 3:
|
| 706 |
+
right_lane_points = np.array(right_lane_points)
|
| 707 |
+
right_fit = np.polyfit(right_lane_points[:, 1], right_lane_points[:, 0], 2)
|
| 708 |
+
|
| 709 |
+
# Generate smooth curve
|
| 710 |
+
ploty = np.linspace(int(height * 0.6), height, 100)
|
| 711 |
+
right_fitx = right_fit[0] * ploty**2 + right_fit[1] * ploty + right_fit[2]
|
| 712 |
+
right_fitx = np.clip(right_fitx, 0, width - 1)
|
| 713 |
+
|
| 714 |
+
right_curve = np.array([np.transpose(np.vstack([right_fitx, ploty]))], dtype=np.int32)
|
| 715 |
+
cv2.polylines(overlay, right_curve, False, (0, 0, 255), 8)
|
| 716 |
+
|
| 717 |
+
# Fill lane area
|
| 718 |
+
if len(left_lane_points) >= 3 and len(right_lane_points) >= 3:
|
| 719 |
+
ploty = np.linspace(int(height * 0.6), height, 100)
|
| 720 |
+
left_fitx = left_fit[0] * ploty**2 + left_fit[1] * ploty + left_fit[2]
|
| 721 |
+
right_fitx = right_fit[0] * ploty**2 + right_fit[1] * ploty + right_fit[2]
|
| 722 |
+
|
| 723 |
+
left_fitx = np.clip(left_fitx, 0, width - 1)
|
| 724 |
+
right_fitx = np.clip(right_fitx, 0, width - 1)
|
| 725 |
+
|
| 726 |
+
pts_left = np.array([np.transpose(np.vstack([left_fitx, ploty]))])
|
| 727 |
+
pts_right = np.array([np.flipud(np.transpose(np.vstack([right_fitx, ploty])))])
|
| 728 |
+
pts = np.hstack((pts_left, pts_right))
|
| 729 |
+
|
| 730 |
+
cv2.fillPoly(overlay, np.int32([pts]), (0, 255, 0))
|
| 731 |
+
|
| 732 |
+
# Blend
|
| 733 |
+
result = cv2.addWeighted(result, 0.8, overlay, 0.5, 0)
|
| 734 |
+
|
| 735 |
+
return result
|
| 736 |
+
|
| 737 |
+
|
| 738 |
+
def process_frame_scnn(frame):
|
| 739 |
+
"""
|
| 740 |
+
SCNN-inspired (Spatial CNN) method.
|
| 741 |
+
Uses spatial message passing for lane detection.
|
| 742 |
+
Implements slice-by-slice convolutions in four directions.
|
| 743 |
+
"""
|
| 744 |
+
height, width = frame.shape[:2]
|
| 745 |
+
|
| 746 |
+
# Preprocessing
|
| 747 |
+
hls = cv2.cvtColor(frame, cv2.COLOR_BGR2HLS)
|
| 748 |
+
l_channel = hls[:, :, 1]
|
| 749 |
+
s_channel = hls[:, :, 2]
|
| 750 |
+
|
| 751 |
+
# Enhanced preprocessing with CLAHE
|
| 752 |
+
clahe = cv2.createCLAHE(clipLimit=2.5, tileGridSize=(8, 8))
|
| 753 |
+
l_enhanced = clahe.apply(l_channel)
|
| 754 |
+
|
| 755 |
+
# Multi-scale edge detection
|
| 756 |
+
sobel_x = cv2.Sobel(l_enhanced, cv2.CV_64F, 1, 0, ksize=5)
|
| 757 |
+
sobel_y = cv2.Sobel(l_enhanced, cv2.CV_64F, 0, 1, ksize=5)
|
| 758 |
+
|
| 759 |
+
# Gradient magnitude and direction
|
| 760 |
+
magnitude = np.sqrt(sobel_x**2 + sobel_y**2)
|
| 761 |
+
magnitude = np.uint8(255 * magnitude / np.max(magnitude))
|
| 762 |
+
|
| 763 |
+
direction = np.arctan2(sobel_y, sobel_x)
|
| 764 |
+
|
| 765 |
+
# Focus on near-vertical edges (lane lines)
|
| 766 |
+
vertical_mask = np.zeros_like(magnitude)
|
| 767 |
+
vertical_mask[(np.abs(direction) > 0.6) & (np.abs(direction) < 1.5)] = 255
|
| 768 |
+
|
| 769 |
+
# Combine with color thresholding
|
| 770 |
+
s_binary = cv2.inRange(s_channel, 90, 255)
|
| 771 |
+
l_binary = cv2.inRange(l_enhanced, 180, 255)
|
| 772 |
+
|
| 773 |
+
combined = cv2.bitwise_or(s_binary, l_binary)
|
| 774 |
+
combined = cv2.bitwise_and(combined, magnitude)
|
| 775 |
+
combined = cv2.bitwise_and(combined, vertical_mask)
|
| 776 |
+
|
| 777 |
+
# Simulate spatial message passing with directional filtering
|
| 778 |
+
# Horizontal message passing (left-to-right and right-to-left)
|
| 779 |
+
kernel_h = np.ones((1, 15), np.uint8)
|
| 780 |
+
horizontal_pass = cv2.morphologyEx(combined, cv2.MORPH_CLOSE, kernel_h)
|
| 781 |
+
|
| 782 |
+
# Vertical message passing (top-to-bottom and bottom-to-top)
|
| 783 |
+
kernel_v = np.ones((15, 1), np.uint8)
|
| 784 |
+
spatial_features = cv2.morphologyEx(horizontal_pass, cv2.MORPH_CLOSE, kernel_v)
|
| 785 |
+
|
| 786 |
+
# Apply ROI
|
| 787 |
+
vertices = np.array([[
|
| 788 |
+
(int(width * 0.1), height),
|
| 789 |
+
(int(width * 0.45), int(height * 0.6)),
|
| 790 |
+
(int(width * 0.55), int(height * 0.6)),
|
| 791 |
+
(int(width * 0.9), height)
|
| 792 |
+
]], dtype=np.int32)
|
| 793 |
+
|
| 794 |
+
spatial_features = region_of_interest(spatial_features, vertices)
|
| 795 |
+
|
| 796 |
+
# Lane fitting with sliding window
|
| 797 |
+
histogram = np.sum(spatial_features[spatial_features.shape[0]//2:, :], axis=0)
|
| 798 |
+
midpoint = len(histogram) // 2
|
| 799 |
+
|
| 800 |
+
leftx_base = np.argmax(histogram[:midpoint])
|
| 801 |
+
rightx_base = np.argmax(histogram[midpoint:]) + midpoint
|
| 802 |
+
|
| 803 |
+
# Sliding window parameters
|
| 804 |
+
nwindows = 12
|
| 805 |
+
window_height = spatial_features.shape[0] // nwindows
|
| 806 |
+
margin = 80
|
| 807 |
+
minpix = 40
|
| 808 |
+
|
| 809 |
+
nonzero = spatial_features.nonzero()
|
| 810 |
+
nonzeroy = np.array(nonzero[0])
|
| 811 |
+
nonzerox = np.array(nonzero[1])
|
| 812 |
+
|
| 813 |
+
leftx_current = leftx_base
|
| 814 |
+
rightx_current = rightx_base
|
| 815 |
+
|
| 816 |
+
left_lane_inds = []
|
| 817 |
+
right_lane_inds = []
|
| 818 |
+
|
| 819 |
+
for window in range(nwindows):
|
| 820 |
+
win_y_low = spatial_features.shape[0] - (window + 1) * window_height
|
| 821 |
+
win_y_high = spatial_features.shape[0] - window * window_height
|
| 822 |
+
|
| 823 |
+
win_xleft_low = leftx_current - margin
|
| 824 |
+
win_xleft_high = leftx_current + margin
|
| 825 |
+
win_xright_low = rightx_current - margin
|
| 826 |
+
win_xright_high = rightx_current + margin
|
| 827 |
+
|
| 828 |
+
good_left_inds = ((nonzeroy >= win_y_low) & (nonzeroy < win_y_high) &
|
| 829 |
+
(nonzerox >= win_xleft_low) & (nonzerox < win_xleft_high)).nonzero()[0]
|
| 830 |
+
good_right_inds = ((nonzeroy >= win_y_low) & (nonzeroy < win_y_high) &
|
| 831 |
+
(nonzerox >= win_xright_low) & (nonzerox < win_xright_high)).nonzero()[0]
|
| 832 |
+
|
| 833 |
+
left_lane_inds.append(good_left_inds)
|
| 834 |
+
right_lane_inds.append(good_right_inds)
|
| 835 |
+
|
| 836 |
+
if len(good_left_inds) > minpix:
|
| 837 |
+
leftx_current = int(np.mean(nonzerox[good_left_inds]))
|
| 838 |
+
if len(good_right_inds) > minpix:
|
| 839 |
+
rightx_current = int(np.mean(nonzerox[good_right_inds]))
|
| 840 |
+
|
| 841 |
+
left_lane_inds = np.concatenate(left_lane_inds)
|
| 842 |
+
right_lane_inds = np.concatenate(right_lane_inds)
|
| 843 |
+
|
| 844 |
+
leftx = nonzerox[left_lane_inds]
|
| 845 |
+
lefty = nonzeroy[left_lane_inds]
|
| 846 |
+
rightx = nonzerox[right_lane_inds]
|
| 847 |
+
righty = nonzeroy[right_lane_inds]
|
| 848 |
+
|
| 849 |
+
result = frame.copy()
|
| 850 |
+
overlay = np.zeros_like(frame)
|
| 851 |
+
|
| 852 |
+
if len(leftx) > 0 and len(rightx) > 0:
|
| 853 |
+
left_fit = np.polyfit(lefty, leftx, 2)
|
| 854 |
+
right_fit = np.polyfit(righty, rightx, 2)
|
| 855 |
+
|
| 856 |
+
ploty = np.linspace(0, spatial_features.shape[0] - 1, spatial_features.shape[0])
|
| 857 |
+
left_fitx = left_fit[0] * ploty**2 + left_fit[1] * ploty + left_fit[2]
|
| 858 |
+
right_fitx = right_fit[0] * ploty**2 + right_fit[1] * ploty + right_fit[2]
|
| 859 |
+
|
| 860 |
+
left_fitx = np.clip(left_fitx, 0, width - 1)
|
| 861 |
+
right_fitx = np.clip(right_fitx, 0, width - 1)
|
| 862 |
+
|
| 863 |
+
# Draw lane area
|
| 864 |
+
pts_left = np.array([np.transpose(np.vstack([left_fitx, ploty]))])
|
| 865 |
+
pts_right = np.array([np.flipud(np.transpose(np.vstack([right_fitx, ploty])))])
|
| 866 |
+
pts = np.hstack((pts_left, pts_right))
|
| 867 |
+
|
| 868 |
+
cv2.fillPoly(overlay, np.int32([pts]), (0, 255, 0))
|
| 869 |
+
|
| 870 |
+
# Draw lane lines
|
| 871 |
+
cv2.polylines(overlay, np.int32([pts_left]), False, (0, 0, 255), 12)
|
| 872 |
+
cv2.polylines(overlay, np.int32([pts_right]), False, (0, 0, 255), 12)
|
| 873 |
+
|
| 874 |
+
result = cv2.addWeighted(result, 0.8, overlay, 0.5, 0)
|
| 875 |
+
|
| 876 |
+
return result
|
| 877 |
+
|
| 878 |
+
|
| 879 |
def process_frame(frame, method="advanced", use_enhanced=True, use_segmented=False):
|
| 880 |
"""
|
| 881 |
Process a single frame for lane detection.
|
| 882 |
+
method: "basic", "basic_segmented", "advanced", "yolop", "ufld", "scnn"
|
| 883 |
use_enhanced: Use enhanced thresholding for better accuracy (advanced method only)
|
| 884 |
use_segmented: Use segmented lines for curve representation (basic method only)
|
| 885 |
"""
|
| 886 |
+
if method == "basic" or method == "basic_standard":
|
| 887 |
+
return process_frame_basic(frame, use_segmented=False)
|
| 888 |
+
elif method == "basic_segmented":
|
| 889 |
+
return process_frame_basic(frame, use_segmented=True)
|
| 890 |
elif method == "advanced":
|
| 891 |
return process_frame_advanced(frame, use_enhanced)
|
| 892 |
+
elif method == "yolop":
|
| 893 |
+
return process_frame_yolop(frame)
|
| 894 |
+
elif method == "ufld":
|
| 895 |
+
return process_frame_ufld(frame)
|
| 896 |
+
elif method == "scnn":
|
| 897 |
+
return process_frame_scnn(frame)
|
| 898 |
else:
|
| 899 |
+
raise ValueError(f"Unknown method: {method}. Use 'basic', 'basic_segmented', 'advanced', 'yolop', 'ufld', or 'scnn'")
|
| 900 |
|
| 901 |
|
| 902 |
def process_frame_advanced(frame, use_enhanced=True):
|
|
|
|
| 922 |
return result
|
| 923 |
|
| 924 |
|
| 925 |
+
def process_video(input_path, output_path, method="advanced", use_enhanced=True, use_segmented=False, progress_callback=None):
|
| 926 |
"""
|
| 927 |
Process the video and create side-by-side comparison.
|
| 928 |
+
method: "basic", "basic_segmented", "advanced", "yolop", "ufld", "scnn"
|
| 929 |
use_enhanced: Use enhanced thresholding for better accuracy (advanced method only)
|
| 930 |
use_segmented: Use segmented lines for curve representation (basic method only)
|
| 931 |
+
progress_callback: Optional callback function to report progress (value between 0 and 1)
|
| 932 |
Returns True if successful, False otherwise.
|
| 933 |
"""
|
| 934 |
# Open the video
|
|
|
|
| 944 |
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
| 945 |
|
| 946 |
# Video writer for output (side-by-side, so width is doubled)
|
| 947 |
+
# Use mp4v codec for better compatibility
|
| 948 |
+
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
|
| 949 |
out = cv2.VideoWriter(output_path, fourcc, fps, (width * 2, height))
|
| 950 |
|
| 951 |
frame_count = 0
|
|
|
|
| 973 |
frame_count += 1
|
| 974 |
|
| 975 |
# Progress indicator
|
| 976 |
+
if progress_callback and frame_count % 10 == 0:
|
| 977 |
+
progress = frame_count / total_frames if total_frames > 0 else 0
|
| 978 |
+
progress_callback(progress, f"Processing frame {frame_count}/{total_frames}")
|
| 979 |
+
elif frame_count % 30 == 0:
|
| 980 |
progress = (frame_count / total_frames) * 100 if total_frames > 0 else 0
|
| 981 |
print(f"Progress: {frame_count}/{total_frames} frames ({progress:.1f}%)")
|
| 982 |
|
|
|
|
| 984 |
cap.release()
|
| 985 |
out.release()
|
| 986 |
|
| 987 |
+
if progress_callback:
|
| 988 |
+
progress_callback(1.0, "Completed!")
|
| 989 |
+
|
| 990 |
print(f"✓ Completed! Processed {frame_count} frames using {method} method.")
|
| 991 |
|
| 992 |
return frame_count > 0
|