diff --git a/Finding_Lanes/README.md b/Finding_Lanes/README.md
index c6b08872..881760e3 100644
--- a/Finding_Lanes/README.md
+++ b/Finding_Lanes/README.md
@@ -2,31 +2,130 @@


-# 🚘 Finding Lanes
-### Let's go!
-
+# ?? Finding Lanes
-## 🛠️ Description
-A short description about the script must be mentioned here.
+> Real-time and static road lane boundary detection using OpenCV and NumPy.
-## ⚙️ Languages or Frameworks Used
-Run the following command:
+
+
+
+
+
+## ?? Author
-## 🤖 Author
-zmdlw (https://github.com/zmdlw)
+- Original Script: **zmdlw** ([@zmdlw](https://github.com/zmdlw))
+- Enhancements & Tests: **Prayas Dey** ([@Prayas340](https://github.com/Prayas340))
diff --git a/Finding_Lanes/lanes.py b/Finding_Lanes/lanes.py
index 159ca500..5b7b8934 100644
--- a/Finding_Lanes/lanes.py
+++ b/Finding_Lanes/lanes.py
@@ -1,85 +1,421 @@
+"""Finding Lanes: Road Lane Detection using OpenCV & NumPy.
+
+This module provides a modular computer vision pipeline to detect road lane
+markings in static images, pre-recorded video files, or live camera streams.
+It uses Gaussian blurring, Canny edge detection, dynamic Region of Interest
+(ROI) masking, Hough Transform line detection, and slope-intercept linear
+regression averaging to render clear lane boundaries.
+"""
+
+import argparse
+import os
+import sys
+from typing import Dict, List, Optional, Tuple, Union
+
import cv2
import numpy as np
-# 1. convert the image to gray scale
-# 2. blur the image
-# 3. detect the edges
-# 4. create a mask
-# 5. apply the mask to the image
-# 6. detect the lines
-# 7. average the lines
-# 8. display the lines
+def make_coordinate(
+ image: np.ndarray,
+ line_parameters: Union[Tuple[float, float], np.ndarray, List[float]]
+) -> Optional[np.ndarray]:
+ """Calculate (x1, y1, x2, y2) pixel coordinates from slope and intercept.
+
+ Args:
+ image: Source image frame as a NumPy ndarray.
+ line_parameters: Tuple or array containing (slope, intercept).
+
+ Returns:
+ NumPy array [x1, y1, x2, y2] or None if line is invalid.
+ """
+ if line_parameters is None or len(line_parameters) < 2:
+ return None
+
+ slope = float(line_parameters[0])
+ intercept = float(line_parameters[1])
+
+ # Guard against zero slope, NaN, or non-finite values
+ if abs(slope) < 1e-4 or not np.isfinite(slope) or not np.isfinite(intercept):
+ return None
+
+ height, width = image.shape[:2]
+ y1 = height
+ y2 = int(height * 0.6)
+
+ try:
+ x1 = int((y1 - intercept) / slope)
+ x2 = int((y2 - intercept) / slope)
+ except (ValueError, OverflowError, ZeroDivisionError):
+ return None
+ # Clip coordinates within safe display bounds
+ x1 = max(-width, min(2 * width, x1))
+ x2 = max(-width, min(2 * width, x2))
-def make_coordinate(image, line_parameters):
- slope, intercept = line_parameters
- y1 = image.shape[0]
- y2 = int(y1*(3/5))
- x1 = int((y1-intercept)/slope)
- x2 = int((y2-intercept)/slope)
- return np.array([x1, y1, x2, y2])
+ return np.array([x1, y1, x2, y2], dtype=np.int32)
-def average_lines_intercept(image, lines):
- left_fit = []
- right_fit = []
+def average_lines_intercept(
+ image: np.ndarray,
+ lines: Optional[np.ndarray],
+ min_slope: float = 0.3
+) -> Optional[np.ndarray]:
+ """Average and extrapolate detected Hough line segments into left and right lane lines.
+
+ Args:
+ image: Source image frame.
+ lines: Array of line segments from cv2.HoughLinesP.
+ min_slope: Minimum absolute slope threshold to filter horizontal noise lines.
+
+ Returns:
+ NumPy array containing coordinates for left and right lanes, or None.
+ """
+ if lines is None or len(lines) == 0:
+ return None
+
+ left_fit: List[Tuple[float, float]] = []
+ right_fit: List[Tuple[float, float]] = []
+
for line in lines:
- x1, y1, x2, y2 = line.reshape(4)
+ coords = line.reshape(4)
+ x1, y1, x2, y2 = int(coords[0]), int(coords[1]), int(coords[2]), int(coords[3])
+
+ # Ignore purely vertical lines to prevent division by zero
+ if x1 == x2:
+ continue
+
parameters = np.polyfit((x1, x2), (y1, y2), 1)
- slope = parameters[0]
- intercept = parameters[1]
- if slope < 0:
+ slope = float(parameters[0])
+ intercept = float(parameters[1])
+
+ # Filter out near-horizontal noise lines (crosswalks, shadows)
+ if abs(slope) < min_slope:
+ continue
+
+ # In image coordinates, y increases downward:
+ # Left lane has a negative slope, Right lane has a positive slope
+ if slope < -min_slope:
left_fit.append((slope, intercept))
- else:
+ elif slope > min_slope:
right_fit.append((slope, intercept))
- left_fit_average = np.average(left_fit, axis=0)
- right_fit_average = np.average(right_fit, axis=0)
- left_line = make_coordinate(image, left_fit_average)
- right_line = make_coordinate(image, right_fit_average)
- return np.array([left_line, right_line])
+ lane_lines: List[np.ndarray] = []
+
+ if len(left_fit) > 0:
+ left_fit_average = np.average(left_fit, axis=0)
+ left_line = make_coordinate(image, left_fit_average)
+ if left_line is not None:
+ lane_lines.append(left_line)
+
+ if len(right_fit) > 0:
+ right_fit_average = np.average(right_fit, axis=0)
+ right_line = make_coordinate(image, right_fit_average)
+ if right_line is not None:
+ lane_lines.append(right_line)
+
+ return np.array(lane_lines, dtype=np.int32) if len(lane_lines) > 0 else None
+
+
+def canny(
+ image: np.ndarray,
+ low_threshold: int = 50,
+ high_threshold: int = 150,
+ kernel_size: int = 5
+) -> np.ndarray:
+ """Apply grayscale conversion, Gaussian blur, and Canny edge detection.
+
+ Args:
+ image: Input BGR image array.
+ low_threshold: Lower hysteresis threshold for Canny.
+ high_threshold: Upper hysteresis threshold for Canny.
+ kernel_size: Gaussian blur kernel size (odd integer).
+
+ Returns:
+ Binary edge map as a 2D NumPy array.
+ """
+ if len(image.shape) == 3:
+ gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
+ else:
+ gray = image
-def canny(image):
- gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
- blur = cv2.GaussianBlur(gray, (5, 5), 0)
- canny = cv2.Canny(blur, 50, 150)
- return canny
+ blur = cv2.GaussianBlur(gray, (kernel_size, kernel_size), 0)
+ edges = cv2.Canny(blur, low_threshold, high_threshold)
+ return edges
-def display_lines(image, lines):
+def display_lines(
+ image: np.ndarray,
+ lines: Optional[np.ndarray],
+ color: Tuple[int, int, int] = (0, 0, 255),
+ thickness: int = 10
+) -> np.ndarray:
+ """Render detected lane lines onto a blank black canvas matching image dimensions.
+
+ Args:
+ image: Reference image for dimensions.
+ lines: Array of lane line coordinates [[x1, y1, x2, y2], ...].
+ color: BGR color tuple for the drawn lines.
+ thickness: Line thickness in pixels.
+
+ Returns:
+ Image with rendered lane lines.
+ """
line_image = np.zeros_like(image)
- if lines is not None:
- for x1, y1, x2, y2 in lines:
- cv2.line(line_image, (x1, y1), (x2, y2), (0, 0, 255), 10)
+ if lines is not None and len(lines) > 0:
+ for line in lines:
+ x1, y1, x2, y2 = line.reshape(4)
+ cv2.line(line_image, (int(x1), int(y1)), (int(x2), int(y2)), color, thickness)
return line_image
-def roi(image):
- height = image.shape[0]
- polygons = np.array([
- [(200, height), (1100, height), (550, 250)]
- ])
+def roi(
+ image: np.ndarray,
+ polygons: Optional[np.ndarray] = None
+) -> np.ndarray:
+ """Apply a Region of Interest (ROI) polygon mask to isolate the road lane area.
+
+ Calculates dynamic vertices proportional to image width and height when
+ polygons are not explicitly provided.
+
+ Args:
+ image: Single-channel edge map or 3-channel image.
+ polygons: Custom polygon vertices array, or None for dynamic ROI.
+
+ Returns:
+ Masked image containing only the region of interest.
+ """
+ height, width = image.shape[:2]
+
+ if polygons is None:
+ # Dynamic triangular ROI proportional to image dimensions
+ polygons = np.array([
+ [
+ (int(width * 0.15), height),
+ (int(width * 0.88), height),
+ (int(width * 0.45), int(height * 0.35))
+ ]
+ ], dtype=np.int32)
+
mask = np.zeros_like(image)
cv2.fillPoly(mask, polygons, 255)
masked_image = cv2.bitwise_and(image, mask)
return masked_image
-cap = cv2.VideoCapture("Finding_Lanes/video.mp4")
+def process_frame(
+ frame: np.ndarray,
+ min_slope: float = 0.3,
+ return_intermediates: bool = False
+) -> Union[Tuple[np.ndarray, Optional[np.ndarray]], Tuple[np.ndarray, Optional[np.ndarray], Dict[str, np.ndarray]]]:
+ """Execute the full lane detection pipeline on a single frame.
+
+ Pipeline stages:
+ 1. Canny edge detection (Grayscale -> Gaussian Blur -> Canny)
+ 2. Dynamic Region of Interest (ROI) masking
+ 3. Hough Transform line detection
+ 4. Slope-intercept averaging & extrapolation
+ 5. Line rendering & alpha blending with the original frame
-while(cap.isOpened()):
- _, frame = cap.read()
+ Args:
+ frame: BGR input image frame.
+ min_slope: Minimum slope threshold to filter noise lines.
+ return_intermediates: If True, returns a dict of intermediate pipeline stages.
+
+ Returns:
+ (combo_image, averaged_lines) or (combo_image, averaged_lines, intermediates_dict)
+ """
canny_image = canny(frame)
cropped_image = roi(canny_image)
- lines = cv2.HoughLinesP(cropped_image, 2, np.pi/180,
- 100, np.array([]), minLineLength=40, maxLineGap=5)
- averaged_lines = average_lines_intercept(frame, lines)
+ lines = cv2.HoughLinesP(
+ cropped_image,
+ rho=2,
+ theta=np.pi / 180,
+ threshold=100,
+ lines=np.array([]),
+ minLineLength=40,
+ maxLineGap=5
+ )
+ averaged_lines = average_lines_intercept(frame, lines, min_slope=min_slope)
line_image = display_lines(frame, averaged_lines)
- combo_image = cv2.addWeighted(frame, 0.8, line_image, 1, 1)
- cv2.imshow("result", combo_image)
- if cv2.waitKey(10) == ord('q'):
- break
+ combo_image = cv2.addWeighted(frame, 0.8, line_image, 1.0, 1.0)
+
+ if return_intermediates:
+ intermediates = {
+ "canny": canny_image,
+ "roi": cropped_image,
+ "line_image": line_image,
+ "raw_lines": lines
+ }
+ return combo_image, averaged_lines, intermediates
+
+ return combo_image, averaged_lines
+
+
+def process_image(
+ image_path: str,
+ output_path: Optional[str] = None,
+ show: bool = True
+) -> Optional[np.ndarray]:
+ """Process a static image file and detect road lanes.
+
+ Args:
+ image_path: Path to the input image.
+ output_path: Optional path to save the annotated result image.
+ show: If True, displays the result window using OpenCV GUI.
+
+ Returns:
+ Annotated image array or None if file cannot be read.
+ """
+ if not os.path.exists(image_path):
+ print(f"Error: Image file '{image_path}' not found.", file=sys.stderr)
+ return None
+
+ image = cv2.imread(image_path)
+ if image is None:
+ print(f"Error: Unable to load image from '{image_path}'.", file=sys.stderr)
+ return None
+
+ combo_image, _ = process_frame(image)
+
+ if output_path:
+ cv2.imwrite(output_path, combo_image)
+ print(f"Saved processed image to: {output_path}")
+
+ if show:
+ cv2.imshow("Finding Lanes - Image Result", combo_image)
+ print("Press any key to close the window...")
+ cv2.waitKey(0)
+ cv2.destroyAllWindows()
+
+ return combo_image
+
+
+def process_video(
+ source: Union[str, int],
+ output_path: Optional[str] = None,
+ show: bool = True
+) -> None:
+ """Process a video file or live camera stream frame-by-frame.
+
+ Args:
+ source: File path to a video file, or integer webcam device index (e.g., 0).
+ output_path: Optional output video path (e.g. 'output.mp4').
+ show: If True, displays live video frames using OpenCV GUI.
+ """
+ if isinstance(source, str) and not os.path.exists(source):
+ print(f"Error: Video file '{source}' not found.", file=sys.stderr)
+ return
+
+ cap = cv2.VideoCapture(source)
+ if not cap.isOpened():
+ print(f"Error: Could not open video source '{source}'.", file=sys.stderr)
+ return
+
+ writer = None
+ if output_path:
+ 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) or 30.0
+ fourcc = cv2.VideoWriter_fourcc(*"mp4v")
+ writer = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
+
+ print("Processing video stream... Press 'q' to stop.")
+ try:
+ while cap.isOpened():
+ ret, frame = cap.read()
+ if not ret or frame is None:
+ break
+
+ combo_image, _ = process_frame(frame)
+
+ if writer:
+ writer.write(combo_image)
+
+ if show:
+ cv2.imshow("Finding Lanes - Video Stream", combo_image)
+ if cv2.waitKey(10) & 0xFF == ord("q"):
+ print("User interrupted video playback.")
+ break
+ finally:
+ cap.release()
+ if writer:
+ writer.release()
+ print(f"Saved processed video to: {output_path}")
+ if show:
+ cv2.destroyAllWindows()
+
+
+def resolve_asset_path(filename: str) -> str:
+ """Resolve file path relative to current script directory or workspace root."""
+ dir_path = os.path.dirname(os.path.abspath(__file__))
+ direct_path = os.path.join(dir_path, filename)
+ if os.path.exists(direct_path):
+ return direct_path
+
+ relative_path = os.path.join("Finding_Lanes", filename)
+ if os.path.exists(relative_path):
+ return relative_path
+
+ return filename
+
+
+def main() -> None:
+ """Parse CLI arguments and execute the lane finding pipeline."""
+ parser = argparse.ArgumentParser(
+ description="Finding Lanes: Detect and highlight road lane markings in images and video streams."
+ )
+ parser.add_argument(
+ "-i", "--image",
+ type=str,
+ default=None,
+ help="Path to an input image file to process."
+ )
+ parser.add_argument(
+ "-v", "--video",
+ type=str,
+ default=None,
+ help="Path to an input video file to process."
+ )
+ parser.add_argument(
+ "-c", "--camera",
+ type=int,
+ default=None,
+ help="Webcam device index (e.g. 0 for built-in camera)."
+ )
+ parser.add_argument(
+ "-o", "--output",
+ type=str,
+ default=None,
+ help="Path to save the processed image or video output."
+ )
+ parser.add_argument(
+ "--no-show",
+ action="store_true",
+ help="Run in headless mode without displaying GUI windows."
+ )
+
+ args = parser.parse_args()
+ show = not args.no_show
+
+ if args.image:
+ process_image(args.image, output_path=args.output, show=show)
+ elif args.camera is not None:
+ process_video(args.camera, output_path=args.output, show=show)
+ elif args.video:
+ process_video(args.video, output_path=args.output, show=show)
+ else:
+ # Default behavior: run bundled video.mp4 or fallback to picture.jpg
+ default_video = resolve_asset_path("video.mp4")
+ default_image = resolve_asset_path("picture.jpg")
+
+ if os.path.exists(default_video):
+ process_video(default_video, output_path=args.output, show=show)
+ elif os.path.exists(default_image):
+ process_image(default_image, output_path=args.output, show=show)
+ else:
+ print("Error: No input source provided and default assets (video.mp4, picture.jpg) not found.", file=sys.stderr)
+
-cap.release()
-cv2.destroyAllWindows()
+if __name__ == "__main__":
+ main()
diff --git a/Finding_Lanes/sub.py b/Finding_Lanes/sub.py
index d819484d..89f15730 100644
--- a/Finding_Lanes/sub.py
+++ b/Finding_Lanes/sub.py
@@ -1,5 +1,114 @@
+"""Finding Lanes - Pipeline Visualizer.
+
+This utility visualizes each stage of the computer vision lane detection pipeline
+(Original Image, Canny Edges, ROI Masked Edges, and Final Lane Overlay)
+side-by-side using Matplotlib.
+"""
+
+import argparse
+import os
+import sys
+
+import cv2
import matplotlib.pyplot as plt
-import matplotlib.image as img
-img = img.imread("Finding_Lanes/picture.jpg")
-plt.imshow(img)
-plt.show()
+
+# Import core lane detection functions
+try:
+ from lanes import process_frame, resolve_asset_path
+except ImportError:
+ from Finding_Lanes.lanes import process_frame, resolve_asset_path
+
+
+def visualize_pipeline(
+ image_path: str,
+ output_path: str = None,
+ show: bool = True
+) -> None:
+ """Visualize all intermediate stages of the lane detection pipeline.
+
+ Args:
+ image_path: Path to the input image.
+ output_path: Optional path to save the generated subplot figure.
+ show: Whether to display the plot interactively.
+ """
+ if not os.path.exists(image_path):
+ print(f"Error: Image '{image_path}' does not exist.", file=sys.stderr)
+ return
+
+ # Read image using OpenCV (BGR format)
+ bgr_img = cv2.imread(image_path)
+ if bgr_img is None:
+ print(f"Error: Failed to read image from '{image_path}'.", file=sys.stderr)
+ return
+
+ rgb_img = cv2.cvtColor(bgr_img, cv2.COLOR_BGR2RGB)
+
+ # Process frame and retrieve intermediate stages
+ combo_bgr, lanes, intermediates = process_frame(bgr_img, return_intermediates=True)
+ combo_rgb = cv2.cvtColor(combo_bgr, cv2.COLOR_BGR2RGB)
+
+ fig, axes = plt.subplots(2, 2, figsize=(14, 8))
+ fig.suptitle("Finding Lanes - Computer Vision Pipeline Stages", fontsize=16, fontweight="bold")
+
+ # 1. Original Image
+ axes[0, 0].imshow(rgb_img)
+ axes[0, 0].set_title("1. Original Image (RGB)", fontsize=12)
+ axes[0, 0].axis("off")
+
+ # 2. Canny Edge Detection
+ axes[0, 1].imshow(intermediates["canny"], cmap="gray")
+ axes[0, 1].set_title("2. Canny Edge Detection", fontsize=12)
+ axes[0, 1].axis("off")
+
+ # 3. Region of Interest (ROI) Masked
+ axes[1, 0].imshow(intermediates["roi"], cmap="gray")
+ axes[1, 0].set_title("3. Dynamic Region of Interest (ROI)", fontsize=12)
+ axes[1, 0].axis("off")
+
+ # 4. Final Lane Overlay
+ axes[1, 1].imshow(combo_rgb)
+ axes[1, 1].set_title("4. Hough Lines & Averaged Lane Detection", fontsize=12)
+ axes[1, 1].axis("off")
+
+ plt.tight_layout()
+
+ if output_path:
+ plt.savefig(output_path, dpi=200, bbox_inches="tight")
+ print(f"Pipeline stages figure saved to: {output_path}")
+
+ if show:
+ plt.show()
+ else:
+ plt.close(fig)
+
+
+def main() -> None:
+ """Parse CLI arguments and run visualization."""
+ parser = argparse.ArgumentParser(
+ description="Finding Lanes: Multi-stage Computer Vision Pipeline Visualizer."
+ )
+ parser.add_argument(
+ "-i", "--image",
+ type=str,
+ default=None,
+ help="Path to an input image (default: picture.jpg)."
+ )
+ parser.add_argument(
+ "-o", "--output",
+ type=str,
+ default=None,
+ help="Path to save the pipeline stages plot image."
+ )
+ parser.add_argument(
+ "--no-show",
+ action="store_true",
+ help="Run without displaying the Matplotlib window."
+ )
+
+ args = parser.parse_args()
+ image_path = args.image or resolve_asset_path("picture.jpg")
+ visualize_pipeline(image_path, output_path=args.output, show=not args.no_show)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/Finding_Lanes/test_lanes.py b/Finding_Lanes/test_lanes.py
new file mode 100644
index 00000000..8c7c9787
--- /dev/null
+++ b/Finding_Lanes/test_lanes.py
@@ -0,0 +1,153 @@
+"""Unit test suite for Finding Lanes computer vision pipeline."""
+
+import os
+import unittest
+import numpy as np
+import cv2
+
+# Support running directly or as package
+try:
+ from lanes import (
+ canny,
+ roi,
+ make_coordinate,
+ average_lines_intercept,
+ display_lines,
+ process_frame,
+ resolve_asset_path,
+ )
+except ImportError:
+ from Finding_Lanes.lanes import (
+ canny,
+ roi,
+ make_coordinate,
+ average_lines_intercept,
+ display_lines,
+ process_frame,
+ resolve_asset_path,
+ )
+
+
+class TestFindingLanes(unittest.TestCase):
+ """Test suite verifying all core functions of the Finding Lanes pipeline."""
+
+ def setUp(self):
+ """Create test image frames of various dimensions."""
+ self.height, self.width = 720, 1280
+ # Create a synthetic 3-channel BGR image
+ self.test_frame = np.zeros((self.height, self.width, 3), dtype=np.uint8)
+ # Draw synthetic left lane (negative slope in image coordinates)
+ cv2.line(self.test_frame, (300, 720), (580, 450), (255, 255, 255), 8)
+ # Draw synthetic right lane (positive slope in image coordinates)
+ cv2.line(self.test_frame, (1000, 720), (700, 450), (255, 255, 255), 8)
+
+ def test_canny_edge_detection(self):
+ """Verify Canny edge detector outputs a binary 2D edge map."""
+ edges = canny(self.test_frame)
+ self.assertEqual(edges.shape, (self.height, self.width))
+ self.assertEqual(edges.dtype, np.uint8)
+ self.assertTrue(np.any(edges > 0), "Canny edge detector should find drawn lines")
+
+ def test_canny_grayscale_input(self):
+ """Verify Canny handles single-channel 2D grayscale input gracefully."""
+ gray = cv2.cvtColor(self.test_frame, cv2.COLOR_BGR2GRAY)
+ edges = canny(gray)
+ self.assertEqual(edges.shape, (self.height, self.width))
+
+ def test_dynamic_roi_masking(self):
+ """Verify dynamic ROI preserves road area and masks out non-ROI regions."""
+ edges = canny(self.test_frame)
+ masked = roi(edges)
+ self.assertEqual(masked.shape, edges.shape)
+ # Top corners should be masked out (all zeros)
+ self.assertEqual(masked[0, 0], 0)
+ self.assertEqual(masked[0, self.width - 1], 0)
+
+ def test_roi_custom_polygon(self):
+ """Verify ROI accepts custom polygon coordinates."""
+ custom_poly = np.array([[(100, 700), (800, 700), (450, 300)]], dtype=np.int32)
+ edges = canny(self.test_frame)
+ masked = roi(edges, polygons=custom_poly)
+ self.assertEqual(masked.shape, edges.shape)
+
+ def test_make_coordinate_valid(self):
+ """Verify make_coordinate computes expected pixel coordinates from slope & intercept."""
+ # Slope = -1.0, Intercept = 1000
+ coords = make_coordinate(self.test_frame, (-1.0, 1000.0))
+ self.assertIsNotNone(coords)
+ self.assertEqual(len(coords), 4)
+ x1, y1, x2, y2 = coords
+ self.assertEqual(y1, self.height)
+ self.assertEqual(y2, int(self.height * 0.6))
+ self.assertEqual(x1, int((720 - 1000) / -1.0))
+
+ def test_make_coordinate_edge_cases(self):
+ """Verify make_coordinate handles zero slopes, NaNs, infinities, and None inputs."""
+ self.assertIsNone(make_coordinate(self.test_frame, None))
+ self.assertIsNone(make_coordinate(self.test_frame, []))
+ self.assertIsNone(make_coordinate(self.test_frame, (0.0, 500.0)))
+ self.assertIsNone(make_coordinate(self.test_frame, (np.nan, 500.0)))
+ self.assertIsNone(make_coordinate(self.test_frame, (-1.0, np.inf)))
+
+ def test_average_lines_intercept_none_and_empty(self):
+ """Verify average_lines_intercept handles None and empty line inputs."""
+ self.assertIsNone(average_lines_intercept(self.test_frame, None))
+ self.assertIsNone(average_lines_intercept(self.test_frame, np.array([])))
+
+ def test_average_lines_slope_filtering(self):
+ """Verify horizontal noise lines (near zero slope) are filtered out."""
+ # Horizontal line: (100, 500) to (900, 500) -> slope = 0
+ horizontal_line = np.array([[[100, 500, 900, 500]]])
+ result = average_lines_intercept(self.test_frame, horizontal_line, min_slope=0.3)
+ self.assertIsNone(result)
+
+ def test_average_lines_detection(self):
+ """Verify left and right lanes are correctly categorized and averaged."""
+ left_seg = [[300, 720, 580, 450]]
+ right_seg = [[1000, 720, 700, 450]]
+ lines = np.array([left_seg, right_seg])
+ lane_lines = average_lines_intercept(self.test_frame, lines)
+ self.assertIsNotNone(lane_lines)
+ self.assertEqual(len(lane_lines), 2)
+
+ def test_display_lines(self):
+ """Verify display_lines generates an overlay image with the correct shape."""
+ lanes = np.array([[300, 720, 580, 432], [1000, 720, 700, 432]])
+ line_img = display_lines(self.test_frame, lanes)
+ self.assertEqual(line_img.shape, self.test_frame.shape)
+ self.assertEqual(line_img.dtype, np.uint8)
+
+ def test_display_lines_none(self):
+ """Verify display_lines returns all-black canvas when lines is None."""
+ line_img = display_lines(self.test_frame, None)
+ self.assertEqual(line_img.shape, self.test_frame.shape)
+ self.assertTrue(np.all(line_img == 0))
+
+ def test_process_frame_end_to_end(self):
+ """Verify full process_frame pipeline runs successfully on synthetic frame."""
+ combo_image, lanes = process_frame(self.test_frame)
+ self.assertEqual(combo_image.shape, self.test_frame.shape)
+ self.assertIsNotNone(lanes)
+
+ def test_process_frame_intermediates(self):
+ """Verify process_frame returns intermediate dictionary when requested."""
+ combo, lanes, intermediates = process_frame(self.test_frame, return_intermediates=True)
+ self.assertIn("canny", intermediates)
+ self.assertIn("roi", intermediates)
+ self.assertIn("line_image", intermediates)
+ self.assertIn("raw_lines", intermediates)
+
+ def test_process_bundled_image_if_present(self):
+ """Verify pipeline execution on bundled picture.jpg asset."""
+ img_path = resolve_asset_path("picture.jpg")
+ if os.path.exists(img_path):
+ img = cv2.imread(img_path)
+ self.assertIsNotNone(img)
+ combo, lanes = process_frame(img)
+ self.assertEqual(combo.shape, img.shape)
+ self.assertIsNotNone(lanes)
+ self.assertEqual(len(lanes), 2, "Should detect both left and right lane lines")
+
+
+if __name__ == "__main__":
+ unittest.main()