Spaces:
Sleeping
Sleeping
Enhance lane detection functionality with method selection and GPU support
Browse files- .vscode/launch.json +11 -0
- app.py +72 -21
- cli.py +33 -11
- lane_detection.py +491 -48
.vscode/launch.json
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"configurations": [
|
| 3 |
+
{
|
| 4 |
+
"name": "Python ๋๋ฒ๊ฑฐ: ํ์ฌ ํ์ผ",
|
| 5 |
+
"type": "debugpy",
|
| 6 |
+
"request": "launch",
|
| 7 |
+
"program": "${file}",
|
| 8 |
+
"console": "integratedTerminal"
|
| 9 |
+
}
|
| 10 |
+
]
|
| 11 |
+
}
|
app.py
CHANGED
|
@@ -3,22 +3,22 @@ import tempfile
|
|
| 3 |
from lane_detection import process_video as process_video_file
|
| 4 |
|
| 5 |
|
| 6 |
-
def process_video(video_path):
|
| 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
|
| 13 |
-
|
| 14 |
# Create temporary output file
|
| 15 |
temp_output = tempfile.NamedTemporaryFile(delete=False, suffix='.mp4')
|
| 16 |
output_path = temp_output.name
|
| 17 |
temp_output.close()
|
| 18 |
-
|
| 19 |
-
# Process the video
|
| 20 |
-
success = process_video_file(video_path, output_path)
|
| 21 |
-
|
| 22 |
if success:
|
| 23 |
return output_path
|
| 24 |
else:
|
|
@@ -28,34 +28,85 @@ def process_video(video_path):
|
|
| 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.
|
| 32 |
-
|
| 33 |
with gr.Row():
|
| 34 |
with gr.Column():
|
| 35 |
video_input = gr.Video(label="Upload Video")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
process_btn = gr.Button("Process Video", variant="primary")
|
| 37 |
-
|
| 38 |
with gr.Column():
|
| 39 |
video_output = gr.Video(label="Result (Original | Lane Detection)")
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
process_btn.click(
|
| 42 |
fn=process_video,
|
| 43 |
-
inputs=video_input,
|
| 44 |
outputs=video_output
|
| 45 |
)
|
| 46 |
-
|
| 47 |
gr.Markdown("""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
### How it works:
|
| 49 |
1. Upload a video file containing road scenes
|
| 50 |
-
2.
|
| 51 |
-
|
| 52 |
-
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
|
|
|
|
|
|
| 59 |
""")
|
| 60 |
|
| 61 |
|
|
|
|
| 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
|
| 13 |
+
|
| 14 |
# Create temporary output file
|
| 15 |
temp_output = tempfile.NamedTemporaryFile(delete=False, suffix='.mp4')
|
| 16 |
output_path = temp_output.name
|
| 17 |
temp_output.close()
|
| 18 |
+
|
| 19 |
+
# Process the video with selected method
|
| 20 |
+
success = process_video_file(video_path, output_path, method, use_enhanced, use_segmented)
|
| 21 |
+
|
| 22 |
if success:
|
| 23 |
return output_path
|
| 24 |
else:
|
|
|
|
| 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 basic and advanced methods.")
|
| 32 |
+
|
| 33 |
with gr.Row():
|
| 34 |
with gr.Column():
|
| 35 |
video_input = gr.Video(label="Upload Video")
|
| 36 |
+
|
| 37 |
+
with gr.Row():
|
| 38 |
+
method_selector = gr.Radio(
|
| 39 |
+
choices=["basic", "advanced"],
|
| 40 |
+
value="advanced",
|
| 41 |
+
label="Detection Method",
|
| 42 |
+
info="Basic: Fast Hough Transform | Advanced: Accurate polynomial fitting"
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
enhanced_checkbox = gr.Checkbox(
|
| 46 |
+
value=True,
|
| 47 |
+
label="Enhanced Thresholding",
|
| 48 |
+
info="Better accuracy but slightly slower (Advanced method only)",
|
| 49 |
+
visible=True
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
segmented_checkbox = gr.Checkbox(
|
| 53 |
+
value=False,
|
| 54 |
+
label="Segmented Lines",
|
| 55 |
+
info="Multiple segments for better curve representation (Basic method only)",
|
| 56 |
+
visible=False
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
process_btn = gr.Button("Process Video", variant="primary")
|
| 60 |
+
|
| 61 |
with gr.Column():
|
| 62 |
video_output = gr.Video(label="Result (Original | Lane Detection)")
|
| 63 |
+
|
| 64 |
+
# Update checkbox visibility based on method
|
| 65 |
+
def update_checkboxes(method):
|
| 66 |
+
enhanced_visible = (method == "advanced")
|
| 67 |
+
segmented_visible = (method == "basic")
|
| 68 |
+
return gr.Checkbox(visible=enhanced_visible), gr.Checkbox(visible=segmented_visible)
|
| 69 |
+
|
| 70 |
+
method_selector.change(
|
| 71 |
+
fn=update_checkboxes,
|
| 72 |
+
inputs=method_selector,
|
| 73 |
+
outputs=[enhanced_checkbox, segmented_checkbox]
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
process_btn.click(
|
| 77 |
fn=process_video,
|
| 78 |
+
inputs=[video_input, method_selector, enhanced_checkbox, segmented_checkbox],
|
| 79 |
outputs=video_output
|
| 80 |
)
|
| 81 |
+
|
| 82 |
gr.Markdown("""
|
| 83 |
+
### Detection Methods:
|
| 84 |
+
|
| 85 |
+
**๐น Basic Method (Hough Transform):**
|
| 86 |
+
- Fast and lightweight
|
| 87 |
+
- Good for straight lanes
|
| 88 |
+
- **New: Segmented Mode** - Draws multiple line segments for better curve representation
|
| 89 |
+
- Lower accuracy on sharp curves and dashed lines
|
| 90 |
+
|
| 91 |
+
**๐น Advanced Method (Perspective Transform + Polynomial):**
|
| 92 |
+
- Perspective transform to bird's eye view
|
| 93 |
+
- Polynomial fitting with sliding windows
|
| 94 |
+
- Excellent for curved and dashed lanes
|
| 95 |
+
- **Enhanced mode** uses CLAHE and gradient direction filtering for best accuracy
|
| 96 |
+
|
| 97 |
### How it works:
|
| 98 |
1. Upload a video file containing road scenes
|
| 99 |
+
2. Select detection method and options:
|
| 100 |
+
- For **Basic**: Enable "Segmented Lines" for curves
|
| 101 |
+
- For **Advanced**: Enable "Enhanced Thresholding" for better accuracy
|
| 102 |
+
3. Click "Process Video" button
|
| 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 |
+
- Use **Basic + Segmented** for fastest processing with decent curve handling
|
| 108 |
+
- Use **Advanced + Enhanced** for best accuracy but slower processing
|
| 109 |
+
- Adjust based on your video quality and road conditions
|
| 110 |
""")
|
| 111 |
|
| 112 |
|
cli.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
"""
|
| 3 |
Command-line interface for lane detection
|
| 4 |
-
Usage: python cli.py <input_video> <output_video>
|
| 5 |
"""
|
| 6 |
import sys
|
| 7 |
import os
|
|
@@ -9,31 +9,53 @@ from lane_detection import process_video
|
|
| 9 |
|
| 10 |
|
| 11 |
def main():
|
| 12 |
-
if len(sys.argv)
|
| 13 |
-
print("Usage: python cli.py <input_video> <output_video>")
|
| 14 |
-
print("\
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
print(" python cli.py road_video.mp4 output_result.mp4")
|
|
|
|
|
|
|
|
|
|
| 16 |
sys.exit(1)
|
| 17 |
-
|
| 18 |
input_path = sys.argv[1]
|
| 19 |
output_path = sys.argv[2]
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
# Check if input file exists
|
| 22 |
if not os.path.exists(input_path):
|
| 23 |
print(f"Error: Input file '{input_path}' not found!")
|
| 24 |
sys.exit(1)
|
| 25 |
-
|
| 26 |
print(f"Processing video: {input_path}")
|
| 27 |
print(f"Output will be saved to: {output_path}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
print("\nProcessing...")
|
| 29 |
-
|
| 30 |
try:
|
| 31 |
-
success = process_video(input_path, output_path)
|
| 32 |
-
|
| 33 |
if success:
|
| 34 |
print("\nโ Video processing completed successfully!")
|
| 35 |
print(f"โ Result saved to: {output_path}")
|
| 36 |
-
|
| 37 |
if os.path.exists(output_path):
|
| 38 |
size = os.path.getsize(output_path)
|
| 39 |
print(f"โ File size: {size:,} bytes")
|
|
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
"""
|
| 3 |
Command-line interface for lane detection
|
| 4 |
+
Usage: python cli.py <input_video> <output_video> [method] [enhanced] [segmented]
|
| 5 |
"""
|
| 6 |
import sys
|
| 7 |
import os
|
|
|
|
| 9 |
|
| 10 |
|
| 11 |
def main():
|
| 12 |
+
if len(sys.argv) < 3 or len(sys.argv) > 6:
|
| 13 |
+
print("Usage: python cli.py <input_video> <output_video> [method] [enhanced] [segmented]")
|
| 14 |
+
print("\nArguments:")
|
| 15 |
+
print(" input_video: Path to input video file")
|
| 16 |
+
print(" output_video: Path to output video file")
|
| 17 |
+
print(" method: 'basic' or 'advanced' (default: advanced)")
|
| 18 |
+
print(" enhanced: 'true' or 'false' for enhanced thresholding (default: true, advanced only)")
|
| 19 |
+
print(" segmented: 'true' or 'false' for segmented lines (default: false, basic only)")
|
| 20 |
+
print("\nExamples:")
|
| 21 |
print(" python cli.py road_video.mp4 output_result.mp4")
|
| 22 |
+
print(" python cli.py road_video.mp4 output_result.mp4 basic")
|
| 23 |
+
print(" python cli.py road_video.mp4 output_result.mp4 basic false true")
|
| 24 |
+
print(" python cli.py road_video.mp4 output_result.mp4 advanced true false")
|
| 25 |
sys.exit(1)
|
| 26 |
+
|
| 27 |
input_path = sys.argv[1]
|
| 28 |
output_path = sys.argv[2]
|
| 29 |
+
method = sys.argv[3] if len(sys.argv) >= 4 else "advanced"
|
| 30 |
+
enhanced = sys.argv[4].lower() == "true" if len(sys.argv) >= 5 else True
|
| 31 |
+
segmented = sys.argv[5].lower() == "true" if len(sys.argv) >= 6 else False
|
| 32 |
+
|
| 33 |
+
# Validate method
|
| 34 |
+
if method not in ["basic", "advanced"]:
|
| 35 |
+
print(f"Error: Invalid method '{method}'. Use 'basic' or 'advanced'")
|
| 36 |
+
sys.exit(1)
|
| 37 |
+
|
| 38 |
# Check if input file exists
|
| 39 |
if not os.path.exists(input_path):
|
| 40 |
print(f"Error: Input file '{input_path}' not found!")
|
| 41 |
sys.exit(1)
|
| 42 |
+
|
| 43 |
print(f"Processing video: {input_path}")
|
| 44 |
print(f"Output will be saved to: {output_path}")
|
| 45 |
+
print(f"Method: {method}")
|
| 46 |
+
if method == "advanced":
|
| 47 |
+
print(f"Enhanced thresholding: {'enabled' if enhanced else 'disabled'}")
|
| 48 |
+
if method == "basic":
|
| 49 |
+
print(f"Segmented lines: {'enabled' if segmented else 'disabled'}")
|
| 50 |
print("\nProcessing...")
|
| 51 |
+
|
| 52 |
try:
|
| 53 |
+
success = process_video(input_path, output_path, method, enhanced, segmented)
|
| 54 |
+
|
| 55 |
if success:
|
| 56 |
print("\nโ Video processing completed successfully!")
|
| 57 |
print(f"โ Result saved to: {output_path}")
|
| 58 |
+
|
| 59 |
if os.path.exists(output_path):
|
| 60 |
size = os.path.getsize(output_path)
|
| 61 |
print(f"โ File size: {size:,} bytes")
|
lane_detection.py
CHANGED
|
@@ -1,10 +1,26 @@
|
|
| 1 |
"""
|
| 2 |
Lane detection module using OpenCV
|
|
|
|
| 3 |
This module contains the core lane detection logic without UI dependencies.
|
| 4 |
"""
|
| 5 |
import cv2
|
| 6 |
import numpy as np
|
| 7 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
def region_of_interest(img, vertices):
|
| 10 |
"""
|
|
@@ -16,61 +32,61 @@ def region_of_interest(img, vertices):
|
|
| 16 |
return masked_image
|
| 17 |
|
| 18 |
|
| 19 |
-
def
|
| 20 |
"""
|
| 21 |
-
Draw lines on the image with filled lane area.
|
| 22 |
- Lane lines: Red color
|
| 23 |
- Lane interior: Green semi-transparent fill
|
| 24 |
"""
|
| 25 |
if lines is None:
|
| 26 |
return img
|
| 27 |
-
|
| 28 |
line_img = np.zeros_like(img)
|
| 29 |
-
|
| 30 |
# Separate left and right lane lines
|
| 31 |
left_lines = []
|
| 32 |
right_lines = []
|
| 33 |
-
|
| 34 |
for line in lines:
|
| 35 |
x1, y1, x2, y2 = line[0]
|
| 36 |
if x2 == x1:
|
| 37 |
continue
|
| 38 |
slope = (y2 - y1) / (x2 - x1)
|
| 39 |
-
|
| 40 |
# Filter by slope to separate left and right lanes
|
| 41 |
if slope < -0.5: # Left lane (negative slope)
|
| 42 |
left_lines.append(line[0])
|
| 43 |
elif slope > 0.5: # Right lane (positive slope)
|
| 44 |
right_lines.append(line[0])
|
| 45 |
-
|
| 46 |
# Average lines for left and right lanes
|
| 47 |
def average_lines(lines, img_shape):
|
| 48 |
if len(lines) == 0:
|
| 49 |
return None
|
| 50 |
-
|
| 51 |
x_coords = []
|
| 52 |
y_coords = []
|
| 53 |
-
|
| 54 |
for line in lines:
|
| 55 |
x1, y1, x2, y2 = line
|
| 56 |
x_coords.extend([x1, x2])
|
| 57 |
y_coords.extend([y1, y2])
|
| 58 |
-
|
| 59 |
# Fit a polynomial to the points
|
| 60 |
poly = np.polyfit(y_coords, x_coords, 1)
|
| 61 |
-
|
| 62 |
# Calculate line endpoints
|
| 63 |
y1 = img_shape[0]
|
| 64 |
y2 = int(img_shape[0] * 0.6)
|
| 65 |
x1 = int(poly[0] * y1 + poly[1])
|
| 66 |
x2 = int(poly[0] * y2 + poly[1])
|
| 67 |
-
|
| 68 |
return [x1, y1, x2, y2]
|
| 69 |
-
|
| 70 |
# Draw averaged lines
|
| 71 |
left_line = average_lines(left_lines, img.shape)
|
| 72 |
right_line = average_lines(right_lines, img.shape)
|
| 73 |
-
|
| 74 |
# Fill the lane area with green color
|
| 75 |
if left_line is not None and right_line is not None:
|
| 76 |
# Create polygon points for the lane area
|
|
@@ -80,38 +96,166 @@ def draw_lines(img, lines, color=[0, 255, 0], thickness=3):
|
|
| 80 |
(right_line[2], right_line[3]), # Right top
|
| 81 |
(right_line[0], right_line[1]) # Right bottom
|
| 82 |
]], dtype=np.int32)
|
| 83 |
-
|
| 84 |
# Fill the lane area with green (semi-transparent)
|
| 85 |
cv2.fillPoly(line_img, lane_polygon, (0, 255, 0))
|
| 86 |
-
|
| 87 |
# Draw the lane lines in red with thicker lines
|
| 88 |
if left_line is not None:
|
| 89 |
-
cv2.line(line_img, (left_line[0], left_line[1]), (left_line[2], left_line[3]),
|
| 90 |
(0, 0, 255), thickness * 2) # Red color (BGR format)
|
| 91 |
-
|
| 92 |
if right_line is not None:
|
| 93 |
-
cv2.line(line_img, (right_line[0], right_line[1]), (right_line[2], right_line[3]),
|
| 94 |
(0, 0, 255), thickness * 2) # Red color (BGR format)
|
| 95 |
-
|
| 96 |
# Blend with original image (make the overlay semi-transparent)
|
| 97 |
return cv2.addWeighted(img, 0.8, line_img, 0.5, 0)
|
| 98 |
|
| 99 |
|
| 100 |
-
def
|
| 101 |
"""
|
| 102 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
"""
|
| 104 |
height, width = frame.shape[:2]
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
# Define region of interest (ROI)
|
| 116 |
vertices = np.array([[
|
| 117 |
(int(width * 0.1), height),
|
|
@@ -119,10 +263,10 @@ def process_frame(frame):
|
|
| 119 |
(int(width * 0.55), int(height * 0.6)),
|
| 120 |
(int(width * 0.9), height)
|
| 121 |
]], dtype=np.int32)
|
| 122 |
-
|
| 123 |
# Apply ROI mask
|
| 124 |
masked_edges = region_of_interest(edges, vertices)
|
| 125 |
-
|
| 126 |
# Apply Hough transform to detect lines
|
| 127 |
lines = cv2.HoughLinesP(
|
| 128 |
masked_edges,
|
|
@@ -132,54 +276,353 @@ def process_frame(frame):
|
|
| 132 |
minLineLength=40,
|
| 133 |
maxLineGap=100
|
| 134 |
)
|
| 135 |
-
|
| 136 |
# Draw detected lanes on the original frame
|
| 137 |
-
|
| 138 |
-
|
|
|
|
|
|
|
|
|
|
| 139 |
return result
|
| 140 |
|
| 141 |
|
| 142 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
"""
|
| 144 |
Process the video and create side-by-side comparison.
|
|
|
|
|
|
|
|
|
|
| 145 |
Returns True if successful, False otherwise.
|
| 146 |
"""
|
| 147 |
# Open the video
|
| 148 |
cap = cv2.VideoCapture(input_path)
|
| 149 |
-
|
| 150 |
if not cap.isOpened():
|
| 151 |
return False
|
| 152 |
-
|
| 153 |
# Get video properties
|
| 154 |
fps = int(cap.get(cv2.CAP_PROP_FPS))
|
| 155 |
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
| 156 |
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
| 157 |
-
|
|
|
|
| 158 |
# Video writer for output (side-by-side, so width is doubled)
|
| 159 |
# Use H264 codec for better web browser compatibility
|
| 160 |
fourcc = cv2.VideoWriter_fourcc(*'avc1')
|
| 161 |
out = cv2.VideoWriter(output_path, fourcc, fps, (width * 2, height))
|
| 162 |
-
|
| 163 |
frame_count = 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
# Process each frame
|
| 165 |
while True:
|
| 166 |
ret, frame = cap.read()
|
| 167 |
if not ret:
|
| 168 |
break
|
| 169 |
-
|
| 170 |
# Process frame for lane detection
|
| 171 |
-
processed_frame = process_frame(frame)
|
| 172 |
-
|
| 173 |
# Create side-by-side comparison
|
| 174 |
# Original on left, processed on right
|
| 175 |
combined = np.hstack((frame, processed_frame))
|
| 176 |
-
|
| 177 |
# Write the combined frame
|
| 178 |
out.write(combined)
|
| 179 |
frame_count += 1
|
| 180 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
# Release resources
|
| 182 |
cap.release()
|
| 183 |
out.release()
|
| 184 |
-
|
|
|
|
|
|
|
| 185 |
return frame_count > 0
|
|
|
|
| 1 |
"""
|
| 2 |
Lane detection module using OpenCV
|
| 3 |
+
Advanced lane detection with multiple methods and GPU acceleration.
|
| 4 |
This module contains the core lane detection logic without UI dependencies.
|
| 5 |
"""
|
| 6 |
import cv2
|
| 7 |
import numpy as np
|
| 8 |
|
| 9 |
+
# GPU acceleration setup - prioritize NVIDIA GPU
|
| 10 |
+
USE_GPU = False
|
| 11 |
+
GPU_TYPE = "none"
|
| 12 |
+
try:
|
| 13 |
+
if cv2.cuda.getCudaEnabledDeviceCount() > 0:
|
| 14 |
+
USE_GPU = True
|
| 15 |
+
GPU_TYPE = "nvidia"
|
| 16 |
+
cv2.cuda.setDevice(0) # Use first GPU
|
| 17 |
+
print(f"โ NVIDIA CUDA enabled! Using GPU acceleration on device: {cv2.cuda.printShortCudaDeviceInfo(cv2.cuda.getDevice())}")
|
| 18 |
+
else:
|
| 19 |
+
print("โน CUDA not available. Using CPU.")
|
| 20 |
+
except Exception as e:
|
| 21 |
+
print(f"โน GPU acceleration not available: {e}. Using CPU.")
|
| 22 |
+
GPU_TYPE = "none"
|
| 23 |
+
|
| 24 |
|
| 25 |
def region_of_interest(img, vertices):
|
| 26 |
"""
|
|
|
|
| 32 |
return masked_image
|
| 33 |
|
| 34 |
|
| 35 |
+
def draw_lines_basic(img, lines, color=[0, 255, 0], thickness=3):
|
| 36 |
"""
|
| 37 |
+
Draw lines on the image with filled lane area (Basic method).
|
| 38 |
- Lane lines: Red color
|
| 39 |
- Lane interior: Green semi-transparent fill
|
| 40 |
"""
|
| 41 |
if lines is None:
|
| 42 |
return img
|
| 43 |
+
|
| 44 |
line_img = np.zeros_like(img)
|
| 45 |
+
|
| 46 |
# Separate left and right lane lines
|
| 47 |
left_lines = []
|
| 48 |
right_lines = []
|
| 49 |
+
|
| 50 |
for line in lines:
|
| 51 |
x1, y1, x2, y2 = line[0]
|
| 52 |
if x2 == x1:
|
| 53 |
continue
|
| 54 |
slope = (y2 - y1) / (x2 - x1)
|
| 55 |
+
|
| 56 |
# Filter by slope to separate left and right lanes
|
| 57 |
if slope < -0.5: # Left lane (negative slope)
|
| 58 |
left_lines.append(line[0])
|
| 59 |
elif slope > 0.5: # Right lane (positive slope)
|
| 60 |
right_lines.append(line[0])
|
| 61 |
+
|
| 62 |
# Average lines for left and right lanes
|
| 63 |
def average_lines(lines, img_shape):
|
| 64 |
if len(lines) == 0:
|
| 65 |
return None
|
| 66 |
+
|
| 67 |
x_coords = []
|
| 68 |
y_coords = []
|
| 69 |
+
|
| 70 |
for line in lines:
|
| 71 |
x1, y1, x2, y2 = line
|
| 72 |
x_coords.extend([x1, x2])
|
| 73 |
y_coords.extend([y1, y2])
|
| 74 |
+
|
| 75 |
# Fit a polynomial to the points
|
| 76 |
poly = np.polyfit(y_coords, x_coords, 1)
|
| 77 |
+
|
| 78 |
# Calculate line endpoints
|
| 79 |
y1 = img_shape[0]
|
| 80 |
y2 = int(img_shape[0] * 0.6)
|
| 81 |
x1 = int(poly[0] * y1 + poly[1])
|
| 82 |
x2 = int(poly[0] * y2 + poly[1])
|
| 83 |
+
|
| 84 |
return [x1, y1, x2, y2]
|
| 85 |
+
|
| 86 |
# Draw averaged lines
|
| 87 |
left_line = average_lines(left_lines, img.shape)
|
| 88 |
right_line = average_lines(right_lines, img.shape)
|
| 89 |
+
|
| 90 |
# Fill the lane area with green color
|
| 91 |
if left_line is not None and right_line is not None:
|
| 92 |
# Create polygon points for the lane area
|
|
|
|
| 96 |
(right_line[2], right_line[3]), # Right top
|
| 97 |
(right_line[0], right_line[1]) # Right bottom
|
| 98 |
]], dtype=np.int32)
|
| 99 |
+
|
| 100 |
# Fill the lane area with green (semi-transparent)
|
| 101 |
cv2.fillPoly(line_img, lane_polygon, (0, 255, 0))
|
| 102 |
+
|
| 103 |
# Draw the lane lines in red with thicker lines
|
| 104 |
if left_line is not None:
|
| 105 |
+
cv2.line(line_img, (left_line[0], left_line[1]), (left_line[2], left_line[3]),
|
| 106 |
(0, 0, 255), thickness * 2) # Red color (BGR format)
|
| 107 |
+
|
| 108 |
if right_line is not None:
|
| 109 |
+
cv2.line(line_img, (right_line[0], right_line[1]), (right_line[2], right_line[3]),
|
| 110 |
(0, 0, 255), thickness * 2) # Red color (BGR format)
|
| 111 |
+
|
| 112 |
# Blend with original image (make the overlay semi-transparent)
|
| 113 |
return cv2.addWeighted(img, 0.8, line_img, 0.5, 0)
|
| 114 |
|
| 115 |
|
| 116 |
+
def draw_lines_segmented(img, lines, color=[0, 255, 0], thickness=3):
|
| 117 |
"""
|
| 118 |
+
Draw multiple short line segments to represent curves.
|
| 119 |
+
Better curve representation with Hough Transform.
|
| 120 |
+
- Lane lines: Red segmented lines
|
| 121 |
+
- Lane interior: Green semi-transparent fill
|
| 122 |
+
"""
|
| 123 |
+
if lines is None:
|
| 124 |
+
return img
|
| 125 |
+
|
| 126 |
+
line_img = np.zeros_like(img)
|
| 127 |
+
fill_img = np.zeros_like(img)
|
| 128 |
+
|
| 129 |
+
# Separate left and right lane lines
|
| 130 |
+
left_lines = []
|
| 131 |
+
right_lines = []
|
| 132 |
+
|
| 133 |
+
for line in lines:
|
| 134 |
+
x1, y1, x2, y2 = line[0]
|
| 135 |
+
if x2 == x1:
|
| 136 |
+
continue
|
| 137 |
+
slope = (y2 - y1) / (x2 - x1)
|
| 138 |
+
|
| 139 |
+
# Filter by slope to separate left and right lanes
|
| 140 |
+
if slope < -0.5: # Left lane (negative slope)
|
| 141 |
+
left_lines.append(line[0])
|
| 142 |
+
elif slope > 0.5: # Right lane (positive slope)
|
| 143 |
+
right_lines.append(line[0])
|
| 144 |
+
|
| 145 |
+
# Extract left and right lane boundaries
|
| 146 |
+
left_x = []
|
| 147 |
+
left_y = []
|
| 148 |
+
right_x = []
|
| 149 |
+
right_y = []
|
| 150 |
+
|
| 151 |
+
for line in left_lines:
|
| 152 |
+
x1, y1, x2, y2 = line
|
| 153 |
+
left_x.extend([x1, x2])
|
| 154 |
+
left_y.extend([y1, y2])
|
| 155 |
+
|
| 156 |
+
for line in right_lines:
|
| 157 |
+
x1, y1, x2, y2 = line
|
| 158 |
+
right_x.extend([x1, x2])
|
| 159 |
+
right_y.extend([y1, y2])
|
| 160 |
+
|
| 161 |
+
# Initialize sorted lists
|
| 162 |
+
left_x_sorted = []
|
| 163 |
+
left_y_sorted = []
|
| 164 |
+
right_x_sorted = []
|
| 165 |
+
right_y_sorted = []
|
| 166 |
+
|
| 167 |
+
# Sort by y coordinate to maintain order
|
| 168 |
+
if len(left_x) > 0:
|
| 169 |
+
left_coords = sorted(zip(left_y, left_x))
|
| 170 |
+
left_y_sorted = [c[0] for c in left_coords]
|
| 171 |
+
left_x_sorted = [c[1] for c in left_coords]
|
| 172 |
+
|
| 173 |
+
# Draw all individual line segments for left lane
|
| 174 |
+
for line in left_lines:
|
| 175 |
+
x1, y1, x2, y2 = line
|
| 176 |
+
cv2.line(line_img, (x1, y1), (x2, y2), (0, 0, 255), thickness + 2)
|
| 177 |
+
|
| 178 |
+
if len(right_x) > 0:
|
| 179 |
+
right_coords = sorted(zip(right_y, right_x))
|
| 180 |
+
right_y_sorted = [c[0] for c in right_coords]
|
| 181 |
+
right_x_sorted = [c[1] for c in right_coords]
|
| 182 |
+
|
| 183 |
+
# Draw all individual line segments for right lane
|
| 184 |
+
for line in right_lines:
|
| 185 |
+
x1, y1, x2, y2 = line
|
| 186 |
+
cv2.line(line_img, (x1, y1), (x2, y2), (0, 0, 255), thickness + 2)
|
| 187 |
+
|
| 188 |
+
# Fill the area between left and right lanes
|
| 189 |
+
if len(left_y_sorted) > 0 and len(right_y_sorted) > 0:
|
| 190 |
+
# Create a polygon by combining left and right points
|
| 191 |
+
min_y = max(min(left_y_sorted), min(right_y_sorted))
|
| 192 |
+
max_y = min(max(left_y_sorted), max(right_y_sorted))
|
| 193 |
+
|
| 194 |
+
if max_y > min_y:
|
| 195 |
+
# Interpolate to get matching y-coordinates
|
| 196 |
+
y_range = np.arange(int(min_y), int(max_y), 10)
|
| 197 |
+
|
| 198 |
+
poly_points = []
|
| 199 |
+
|
| 200 |
+
# Left points
|
| 201 |
+
for y in y_range:
|
| 202 |
+
if y >= min(left_y_sorted) and y <= max(left_y_sorted):
|
| 203 |
+
idx = np.searchsorted(left_y_sorted, y)
|
| 204 |
+
if idx > 0 and idx < len(left_x_sorted):
|
| 205 |
+
x = left_x_sorted[idx]
|
| 206 |
+
poly_points.append([x, y])
|
| 207 |
+
|
| 208 |
+
# Right points (reverse order for polygon)
|
| 209 |
+
for y in reversed(y_range):
|
| 210 |
+
if y >= min(right_y_sorted) and y <= max(right_y_sorted):
|
| 211 |
+
idx = np.searchsorted(right_y_sorted, y)
|
| 212 |
+
if idx > 0 and idx < len(right_x_sorted):
|
| 213 |
+
x = right_x_sorted[idx]
|
| 214 |
+
poly_points.append([x, y])
|
| 215 |
+
|
| 216 |
+
if len(poly_points) >= 3:
|
| 217 |
+
poly_points = np.array(poly_points, dtype=np.int32)
|
| 218 |
+
cv2.fillPoly(fill_img, [poly_points], (0, 255, 0))
|
| 219 |
+
|
| 220 |
+
# Combine filled area and lines
|
| 221 |
+
result_img = cv2.addWeighted(line_img, 0.6, fill_img, 0.7, 0)
|
| 222 |
+
|
| 223 |
+
# Blend with original image
|
| 224 |
+
return cv2.addWeighted(img, 0.8, result_img, 0.5, 0)
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
def process_frame_basic(frame, use_segmented=False):
|
| 228 |
+
"""
|
| 229 |
+
Process a single frame for lane detection using basic Hough Transform method.
|
| 230 |
+
use_segmented: If True, draw multiple line segments for better curve representation.
|
| 231 |
+
If False, draw averaged single line (default).
|
| 232 |
"""
|
| 233 |
height, width = frame.shape[:2]
|
| 234 |
+
|
| 235 |
+
if USE_GPU and GPU_TYPE == "nvidia":
|
| 236 |
+
# Upload frame to GPU
|
| 237 |
+
gpu_frame = cv2.cuda_GpuMat()
|
| 238 |
+
gpu_frame.upload(frame)
|
| 239 |
+
|
| 240 |
+
# Convert to grayscale on GPU
|
| 241 |
+
gpu_gray = cv2.cuda.cvtColor(gpu_frame, cv2.COLOR_BGR2GRAY)
|
| 242 |
+
|
| 243 |
+
# Apply Gaussian blur on GPU
|
| 244 |
+
gpu_blur = cv2.cuda.createGaussianFilter(cv2.CV_8UC1, cv2.CV_8UC1, (5, 5), 0)
|
| 245 |
+
gpu_blurred = gpu_blur.apply(gpu_gray)
|
| 246 |
+
|
| 247 |
+
# Apply Canny edge detection on GPU
|
| 248 |
+
gpu_canny = cv2.cuda.createCannyEdgeDetector(50, 150)
|
| 249 |
+
gpu_edges = gpu_canny.detect(gpu_blurred)
|
| 250 |
+
|
| 251 |
+
# Download edges from GPU
|
| 252 |
+
edges = gpu_edges.download()
|
| 253 |
+
else:
|
| 254 |
+
# CPU processing
|
| 255 |
+
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
| 256 |
+
blur = cv2.GaussianBlur(gray, (5, 5), 0)
|
| 257 |
+
edges = cv2.Canny(blur, 50, 150)
|
| 258 |
+
|
| 259 |
# Define region of interest (ROI)
|
| 260 |
vertices = np.array([[
|
| 261 |
(int(width * 0.1), height),
|
|
|
|
| 263 |
(int(width * 0.55), int(height * 0.6)),
|
| 264 |
(int(width * 0.9), height)
|
| 265 |
]], dtype=np.int32)
|
| 266 |
+
|
| 267 |
# Apply ROI mask
|
| 268 |
masked_edges = region_of_interest(edges, vertices)
|
| 269 |
+
|
| 270 |
# Apply Hough transform to detect lines
|
| 271 |
lines = cv2.HoughLinesP(
|
| 272 |
masked_edges,
|
|
|
|
| 276 |
minLineLength=40,
|
| 277 |
maxLineGap=100
|
| 278 |
)
|
| 279 |
+
|
| 280 |
# Draw detected lanes on the original frame
|
| 281 |
+
if use_segmented:
|
| 282 |
+
result = draw_lines_segmented(frame.copy(), lines)
|
| 283 |
+
else:
|
| 284 |
+
result = draw_lines_basic(frame.copy(), lines)
|
| 285 |
+
|
| 286 |
return result
|
| 287 |
|
| 288 |
|
| 289 |
+
def calibrate_perspective(img):
|
| 290 |
+
"""
|
| 291 |
+
Apply perspective transform to get bird's eye view.
|
| 292 |
+
Converts trapezoidal ROI to rectangular view for easier lane detection.
|
| 293 |
+
"""
|
| 294 |
+
height, width = img.shape[:2]
|
| 295 |
+
|
| 296 |
+
# Define source points (trapezoid in original image)
|
| 297 |
+
src = np.float32([
|
| 298 |
+
[width * 0.45, height * 0.65], # Bottom left
|
| 299 |
+
[width * 0.55, height * 0.65], # Bottom right
|
| 300 |
+
[width * 0.9, height], # Top right
|
| 301 |
+
[width * 0.1, height] # Top left
|
| 302 |
+
])
|
| 303 |
+
|
| 304 |
+
# Define destination points (rectangle in bird's eye view)
|
| 305 |
+
dst = np.float32([
|
| 306 |
+
[width * 0.25, 0], # Top left
|
| 307 |
+
[width * 0.75, 0], # Top right
|
| 308 |
+
[width * 0.75, height], # Bottom right
|
| 309 |
+
[width * 0.25, height] # Bottom left
|
| 310 |
+
])
|
| 311 |
+
|
| 312 |
+
# Calculate perspective transform matrix
|
| 313 |
+
M = cv2.getPerspectiveTransform(src, dst)
|
| 314 |
+
# Calculate inverse perspective transform matrix
|
| 315 |
+
Minv = cv2.getPerspectiveTransform(dst, src)
|
| 316 |
+
|
| 317 |
+
# Apply perspective transform
|
| 318 |
+
warped = cv2.warpPerspective(img, M, (width, height), flags=cv2.INTER_LINEAR)
|
| 319 |
+
|
| 320 |
+
return warped, M, Minv
|
| 321 |
+
|
| 322 |
+
|
| 323 |
+
def color_and_gradient_threshold(img, use_enhanced=True):
|
| 324 |
+
"""
|
| 325 |
+
Apply color and gradient thresholding to isolate lane lines.
|
| 326 |
+
Enhanced version with better accuracy for various conditions.
|
| 327 |
+
Returns binary image with lane pixels set to 255.
|
| 328 |
+
"""
|
| 329 |
+
# Convert to HLS color space
|
| 330 |
+
hls = cv2.cvtColor(img, cv2.COLOR_BGR2HLS)
|
| 331 |
+
|
| 332 |
+
# Extract channels
|
| 333 |
+
h_channel = hls[:, :, 0]
|
| 334 |
+
l_channel = hls[:, :, 1]
|
| 335 |
+
s_channel = hls[:, :, 2]
|
| 336 |
+
|
| 337 |
+
# Enhanced thresholding for better lane detection
|
| 338 |
+
if use_enhanced:
|
| 339 |
+
# Adaptive thresholding for saturation channel
|
| 340 |
+
s_thresh = (90, 255) # Lower threshold for yellow lanes
|
| 341 |
+
s_binary = np.zeros_like(s_channel)
|
| 342 |
+
s_binary[(s_channel >= s_thresh[0]) & (s_channel <= s_thresh[1])] = 255
|
| 343 |
+
|
| 344 |
+
# Adaptive thresholding for lightness channel
|
| 345 |
+
l_thresh = (180, 255) # Lower threshold for white lanes
|
| 346 |
+
l_binary = np.zeros_like(l_channel)
|
| 347 |
+
l_binary[(l_channel >= l_thresh[0]) & (l_channel <= l_thresh[1])] = 255
|
| 348 |
+
|
| 349 |
+
# Apply CLAHE (Contrast Limited Adaptive Histogram Equalization) for better contrast
|
| 350 |
+
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
|
| 351 |
+
l_channel_enhanced = clahe.apply(l_channel.astype(np.uint8))
|
| 352 |
+
|
| 353 |
+
# Apply Sobel operator on enhanced channel
|
| 354 |
+
sobelx = cv2.Sobel(l_channel_enhanced, cv2.CV_64F, 1, 0, ksize=3)
|
| 355 |
+
abs_sobelx = np.absolute(sobelx)
|
| 356 |
+
scaled_sobel = np.uint8(255 * abs_sobelx / np.max(abs_sobelx))
|
| 357 |
+
|
| 358 |
+
# More sensitive gradient threshold
|
| 359 |
+
sobel_thresh = (15, 255) # Lower threshold for better edge detection
|
| 360 |
+
sobel_binary = np.zeros_like(scaled_sobel)
|
| 361 |
+
sobel_binary[(scaled_sobel >= sobel_thresh[0]) & (scaled_sobel <= sobel_thresh[1])] = 255
|
| 362 |
+
|
| 363 |
+
# Direction threshold to focus on vertical edges
|
| 364 |
+
sobely = cv2.Sobel(l_channel_enhanced, cv2.CV_64F, 0, 1, ksize=3)
|
| 365 |
+
abs_sobely = np.absolute(sobely)
|
| 366 |
+
scaled_sobely = np.uint8(255 * abs_sobely / np.max(abs_sobely))
|
| 367 |
+
|
| 368 |
+
# Calculate gradient direction
|
| 369 |
+
grad_dir = np.arctan2(abs_sobely, abs_sobelx)
|
| 370 |
+
dir_thresh = (0.7, 1.3) # Focus on near-vertical edges (lane lines)
|
| 371 |
+
dir_binary = np.zeros_like(scaled_sobel)
|
| 372 |
+
dir_binary[(grad_dir >= dir_thresh[0]) & (grad_dir <= dir_thresh[1])] = 255
|
| 373 |
+
|
| 374 |
+
# Combine all binary images with direction filter
|
| 375 |
+
combined_binary = np.zeros_like(s_binary)
|
| 376 |
+
combined_binary[((s_binary == 255) | (l_binary == 255) | (sobel_binary == 255)) & (dir_binary == 255)] = 255
|
| 377 |
+
|
| 378 |
+
# Apply morphological operations to reduce noise
|
| 379 |
+
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
|
| 380 |
+
combined_binary = cv2.morphologyEx(combined_binary, cv2.MORPH_CLOSE, kernel)
|
| 381 |
+
combined_binary = cv2.morphologyEx(combined_binary, cv2.MORPH_OPEN, kernel)
|
| 382 |
+
|
| 383 |
+
else:
|
| 384 |
+
# Basic thresholding
|
| 385 |
+
s_thresh = (100, 255)
|
| 386 |
+
s_binary = np.zeros_like(s_channel)
|
| 387 |
+
s_binary[(s_channel >= s_thresh[0]) & (s_channel <= s_thresh[1])] = 255
|
| 388 |
+
|
| 389 |
+
l_thresh = (200, 255)
|
| 390 |
+
l_binary = np.zeros_like(l_channel)
|
| 391 |
+
l_binary[(l_channel >= l_thresh[0]) & (l_channel <= l_thresh[1])] = 255
|
| 392 |
+
|
| 393 |
+
sobelx = cv2.Sobel(l_channel, cv2.CV_64F, 1, 0, ksize=3)
|
| 394 |
+
abs_sobelx = np.absolute(sobelx)
|
| 395 |
+
scaled_sobel = np.uint8(255 * abs_sobelx / np.max(abs_sobelx))
|
| 396 |
+
|
| 397 |
+
sobel_thresh = (20, 255)
|
| 398 |
+
sobel_binary = np.zeros_like(scaled_sobel)
|
| 399 |
+
sobel_binary[(scaled_sobel >= sobel_thresh[0]) & (scaled_sobel <= sobel_thresh[1])] = 255
|
| 400 |
+
|
| 401 |
+
combined_binary = np.zeros_like(s_binary)
|
| 402 |
+
combined_binary[(s_binary == 255) | (l_binary == 255) | (sobel_binary == 255)] = 255
|
| 403 |
+
|
| 404 |
+
return combined_binary
|
| 405 |
+
|
| 406 |
+
|
| 407 |
+
def fit_polynomial_lanes(binary_warped):
|
| 408 |
+
"""
|
| 409 |
+
Fit 2nd degree polynomials to lane lines using sliding window approach.
|
| 410 |
+
Returns left and right lane polynomial coefficients.
|
| 411 |
+
"""
|
| 412 |
+
# Take a histogram of the bottom half of the image
|
| 413 |
+
histogram = np.sum(binary_warped[binary_warped.shape[0]//2:, :], axis=0)
|
| 414 |
+
|
| 415 |
+
# Find the peak of the left and right halves of the histogram
|
| 416 |
+
midpoint = len(histogram) // 2
|
| 417 |
+
leftx_base = np.argmax(histogram[:midpoint])
|
| 418 |
+
rightx_base = np.argmax(histogram[midpoint:]) + midpoint
|
| 419 |
+
|
| 420 |
+
# Sliding window parameters
|
| 421 |
+
nwindows = 9
|
| 422 |
+
window_height = binary_warped.shape[0] // nwindows
|
| 423 |
+
margin = 100
|
| 424 |
+
minpix = 50
|
| 425 |
+
|
| 426 |
+
# Find nonzero pixels
|
| 427 |
+
nonzero = binary_warped.nonzero()
|
| 428 |
+
nonzeroy = np.array(nonzero[0])
|
| 429 |
+
nonzerox = np.array(nonzero[1])
|
| 430 |
+
|
| 431 |
+
# Current positions
|
| 432 |
+
leftx_current = leftx_base
|
| 433 |
+
rightx_current = rightx_base
|
| 434 |
+
|
| 435 |
+
# Lists to store lane pixel indices
|
| 436 |
+
left_lane_inds = []
|
| 437 |
+
right_lane_inds = []
|
| 438 |
+
|
| 439 |
+
# Step through windows
|
| 440 |
+
for window in range(nwindows):
|
| 441 |
+
# Window boundaries
|
| 442 |
+
win_y_low = binary_warped.shape[0] - (window + 1) * window_height
|
| 443 |
+
win_y_high = binary_warped.shape[0] - window * window_height
|
| 444 |
+
|
| 445 |
+
win_xleft_low = leftx_current - margin
|
| 446 |
+
win_xleft_high = leftx_current + margin
|
| 447 |
+
win_xright_low = rightx_current - margin
|
| 448 |
+
win_xright_high = rightx_current + margin
|
| 449 |
+
|
| 450 |
+
# Find pixels within windows
|
| 451 |
+
good_left_inds = ((nonzeroy >= win_y_low) & (nonzeroy < win_y_high) &
|
| 452 |
+
(nonzerox >= win_xleft_low) & (nonzerox < win_xleft_high)).nonzero()[0]
|
| 453 |
+
good_right_inds = ((nonzeroy >= win_y_low) & (nonzeroy < win_y_high) &
|
| 454 |
+
(nonzerox >= win_xright_low) & (nonzerox < win_xright_high)).nonzero()[0]
|
| 455 |
+
|
| 456 |
+
left_lane_inds.append(good_left_inds)
|
| 457 |
+
right_lane_inds.append(good_right_inds)
|
| 458 |
+
|
| 459 |
+
# Recenter windows
|
| 460 |
+
if len(good_left_inds) > minpix:
|
| 461 |
+
leftx_current = int(np.mean(nonzerox[good_left_inds]))
|
| 462 |
+
if len(good_right_inds) > minpix:
|
| 463 |
+
rightx_current = int(np.mean(nonzerox[good_right_inds]))
|
| 464 |
+
|
| 465 |
+
# Concatenate indices
|
| 466 |
+
left_lane_inds = np.concatenate(left_lane_inds)
|
| 467 |
+
right_lane_inds = np.concatenate(right_lane_inds)
|
| 468 |
+
|
| 469 |
+
# Extract pixel positions
|
| 470 |
+
leftx = nonzerox[left_lane_inds]
|
| 471 |
+
lefty = nonzeroy[left_lane_inds]
|
| 472 |
+
rightx = nonzerox[right_lane_inds]
|
| 473 |
+
righty = nonzeroy[right_lane_inds]
|
| 474 |
+
|
| 475 |
+
# Fit polynomial (2nd degree)
|
| 476 |
+
left_fit = None
|
| 477 |
+
right_fit = None
|
| 478 |
+
|
| 479 |
+
if len(leftx) > 0:
|
| 480 |
+
left_fit = np.polyfit(lefty, leftx, 2)
|
| 481 |
+
if len(rightx) > 0:
|
| 482 |
+
right_fit = np.polyfit(righty, rightx, 2)
|
| 483 |
+
|
| 484 |
+
return left_fit, right_fit
|
| 485 |
+
|
| 486 |
+
|
| 487 |
+
def draw_poly_lines(img, binary_warped, left_fit, right_fit, Minv):
|
| 488 |
+
"""
|
| 489 |
+
Draw polynomial lane lines on the original image using inverse perspective transform.
|
| 490 |
+
"""
|
| 491 |
+
if left_fit is None or right_fit is None:
|
| 492 |
+
return img
|
| 493 |
+
|
| 494 |
+
# Create an image to draw on
|
| 495 |
+
warp_zero = np.zeros_like(binary_warped).astype(np.uint8)
|
| 496 |
+
color_warp = np.dstack((warp_zero, warp_zero, warp_zero))
|
| 497 |
+
|
| 498 |
+
# Generate y values
|
| 499 |
+
ploty = np.linspace(0, binary_warped.shape[0] - 1, binary_warped.shape[0])
|
| 500 |
+
|
| 501 |
+
# Calculate x values using polynomial
|
| 502 |
+
left_fitx = left_fit[0] * ploty**2 + left_fit[1] * ploty + left_fit[2]
|
| 503 |
+
right_fitx = right_fit[0] * ploty**2 + right_fit[1] * ploty + right_fit[2]
|
| 504 |
+
|
| 505 |
+
# Ensure values are within image bounds
|
| 506 |
+
left_fitx = np.clip(left_fitx, 0, binary_warped.shape[1] - 1)
|
| 507 |
+
right_fitx = np.clip(right_fitx, 0, binary_warped.shape[1] - 1)
|
| 508 |
+
|
| 509 |
+
# Create points for the lane area
|
| 510 |
+
pts_left = np.array([np.transpose(np.vstack([left_fitx, ploty]))])
|
| 511 |
+
pts_right = np.array([np.flipud(np.transpose(np.vstack([right_fitx, ploty])))])
|
| 512 |
+
pts = np.hstack((pts_left, pts_right))
|
| 513 |
+
|
| 514 |
+
# Fill the lane area with green
|
| 515 |
+
cv2.fillPoly(color_warp, np.int_([pts]), (0, 255, 0))
|
| 516 |
+
|
| 517 |
+
# Draw the lane lines in red
|
| 518 |
+
cv2.polylines(color_warp, np.int32([pts_left]), isClosed=False, color=(0, 0, 255), thickness=15)
|
| 519 |
+
cv2.polylines(color_warp, np.int32([pts_right]), isClosed=False, color=(0, 0, 255), thickness=15)
|
| 520 |
+
|
| 521 |
+
# Apply inverse perspective transform to project back to original image
|
| 522 |
+
newwarp = cv2.warpPerspective(color_warp, Minv, (img.shape[1], img.shape[0]))
|
| 523 |
+
|
| 524 |
+
# Combine with original image
|
| 525 |
+
result = cv2.addWeighted(img, 0.8, newwarp, 0.5, 0)
|
| 526 |
+
|
| 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" or "advanced"
|
| 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' or 'advanced'")
|
| 543 |
+
|
| 544 |
+
|
| 545 |
+
def process_frame_advanced(frame, use_enhanced=True):
|
| 546 |
+
"""
|
| 547 |
+
Process a single frame for lane detection using advanced pipeline.
|
| 548 |
+
1. Perspective transform to bird's eye view
|
| 549 |
+
2. Enhanced color and gradient thresholding
|
| 550 |
+
3. Polynomial fitting with sliding windows
|
| 551 |
+
4. Draw lanes with inverse perspective transform
|
| 552 |
+
"""
|
| 553 |
+
# Step 1: Apply perspective transform to get bird's eye view
|
| 554 |
+
warped, M, Minv = calibrate_perspective(frame)
|
| 555 |
+
|
| 556 |
+
# Step 2: Apply enhanced color and gradient thresholding
|
| 557 |
+
binary_warped = color_and_gradient_threshold(warped, use_enhanced)
|
| 558 |
+
|
| 559 |
+
# Step 3: Fit polynomial lanes using sliding window approach
|
| 560 |
+
left_fit, right_fit = fit_polynomial_lanes(binary_warped)
|
| 561 |
+
|
| 562 |
+
# Step 4: Draw polynomial lines on original image
|
| 563 |
+
result = draw_poly_lines(frame, binary_warped, left_fit, right_fit, Minv)
|
| 564 |
+
|
| 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" or "advanced"
|
| 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
|
| 577 |
cap = cv2.VideoCapture(input_path)
|
| 578 |
+
|
| 579 |
if not cap.isOpened():
|
| 580 |
return False
|
| 581 |
+
|
| 582 |
# Get video properties
|
| 583 |
fps = int(cap.get(cv2.CAP_PROP_FPS))
|
| 584 |
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
| 585 |
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
| 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 H264 codec for better web browser compatibility
|
| 590 |
fourcc = cv2.VideoWriter_fourcc(*'avc1')
|
| 591 |
out = cv2.VideoWriter(output_path, fourcc, fps, (width * 2, height))
|
| 592 |
+
|
| 593 |
frame_count = 0
|
| 594 |
+
print(f"Processing {total_frames} frames using {method} method...")
|
| 595 |
+
if method == "advanced" and use_enhanced:
|
| 596 |
+
print("Enhanced thresholding enabled for better accuracy")
|
| 597 |
+
if method == "basic" and use_segmented:
|
| 598 |
+
print("Segmented line mode enabled for better curve representation")
|
| 599 |
+
|
| 600 |
# Process each frame
|
| 601 |
while True:
|
| 602 |
ret, frame = cap.read()
|
| 603 |
if not ret:
|
| 604 |
break
|
| 605 |
+
|
| 606 |
# Process frame for lane detection
|
| 607 |
+
processed_frame = process_frame(frame, method, use_enhanced, use_segmented)
|
| 608 |
+
|
| 609 |
# Create side-by-side comparison
|
| 610 |
# Original on left, processed on right
|
| 611 |
combined = np.hstack((frame, processed_frame))
|
| 612 |
+
|
| 613 |
# Write the combined frame
|
| 614 |
out.write(combined)
|
| 615 |
frame_count += 1
|
| 616 |
+
|
| 617 |
+
# Progress indicator
|
| 618 |
+
if frame_count % 30 == 0:
|
| 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 |
+
|
| 622 |
# Release resources
|
| 623 |
cap.release()
|
| 624 |
out.release()
|
| 625 |
+
|
| 626 |
+
print(f"โ Completed! Processed {frame_count} frames using {method} method.")
|
| 627 |
+
|
| 628 |
return frame_count > 0
|