From 56a28c6e41e0e6c1af88d9e4924486f70deb6a75 Mon Sep 17 00:00:00 2001 From: Prayas Dey Date: Tue, 11 Aug 2026 21:03:51 +0530 Subject: [PATCH 1/2] Fix path resolution, missing frame validation, Hough lines crash, and update README in Finding_Lanes --- Finding_Lanes/README.md | 2 +- Finding_Lanes/lanes.py | 48 ++++++++++++++++++++++++++++++----------- Finding_Lanes/sub.py | 11 ++++++++-- 3 files changed, 46 insertions(+), 15 deletions(-) diff --git a/Finding_Lanes/README.md b/Finding_Lanes/README.md index c6b08872..2e405fa8 100644 --- a/Finding_Lanes/README.md +++ b/Finding_Lanes/README.md @@ -7,7 +7,7 @@ ## 🛠️ Description -A short description about the script must be mentioned here. +Finding Lanes is a Python computer vision script built with OpenCV and NumPy to detect road lane markings in images and video streams. It performs RGB to Grayscale conversion, Gaussian blurring, Canny edge detection, Region of Interest (ROI) polygon masking, Hough Transform line detection, and linear regression slope/intercept averaging to compute and visualize clear lane boundaries. ## ⚙️ Languages or Frameworks Used Run the following command: diff --git a/Finding_Lanes/lanes.py b/Finding_Lanes/lanes.py index 159ca500..ee624fa3 100644 --- a/Finding_Lanes/lanes.py +++ b/Finding_Lanes/lanes.py @@ -1,3 +1,4 @@ +import os import cv2 import numpy as np @@ -14,18 +15,24 @@ def make_coordinate(image, line_parameters): slope, intercept = line_parameters + if slope == 0: + return None y1 = image.shape[0] - y2 = int(y1*(3/5)) - x1 = int((y1-intercept)/slope) - x2 = int((y2-intercept)/slope) + y2 = int(y1 * (3 / 5)) + x1 = int((y1 - intercept) / slope) + x2 = int((y2 - intercept) / slope) return np.array([x1, y1, x2, y2]) def average_lines_intercept(image, lines): left_fit = [] right_fit = [] + if lines is None: + return None for line in lines: x1, y1, x2, y2 = line.reshape(4) + if x1 == x2: + continue parameters = np.polyfit((x1, x2), (y1, y2), 1) slope = parameters[0] intercept = parameters[1] @@ -33,11 +40,21 @@ def average_lines_intercept(image, lines): left_fit.append((slope, intercept)) else: 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]) + + lines_list = [] + 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: + lines_list.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: + lines_list.append(right_line) + + return np.array(lines_list) if len(lines_list) > 0 else None def canny(image): @@ -66,13 +83,20 @@ def roi(image): return masked_image -cap = cv2.VideoCapture("Finding_Lanes/video.mp4") +dir_path = os.path.dirname(os.path.abspath(__file__)) +video_path = os.path.join(dir_path, "video.mp4") +if not os.path.exists(video_path): + video_path = "Finding_Lanes/video.mp4" -while(cap.isOpened()): - _, frame = cap.read() +cap = cv2.VideoCapture(video_path) + +while cap.isOpened(): + ret, frame = cap.read() + if not ret or frame is None: + break canny_image = canny(frame) cropped_image = roi(canny_image) - lines = cv2.HoughLinesP(cropped_image, 2, np.pi/180, + lines = cv2.HoughLinesP(cropped_image, 2, np.pi / 180, 100, np.array([]), minLineLength=40, maxLineGap=5) averaged_lines = average_lines_intercept(frame, lines) line_image = display_lines(frame, averaged_lines) diff --git a/Finding_Lanes/sub.py b/Finding_Lanes/sub.py index d819484d..a77ab2b6 100644 --- a/Finding_Lanes/sub.py +++ b/Finding_Lanes/sub.py @@ -1,5 +1,12 @@ +import os import matplotlib.pyplot as plt import matplotlib.image as img -img = img.imread("Finding_Lanes/picture.jpg") -plt.imshow(img) + +dir_path = os.path.dirname(os.path.abspath(__file__)) +image_path = os.path.join(dir_path, "picture.jpg") +if not os.path.exists(image_path): + image_path = "Finding_Lanes/picture.jpg" + +image = img.imread(image_path) +plt.imshow(image) plt.show() From 69e74da7c5ddfd8f530b6a1e39c957ee2018af92 Mon Sep 17 00:00:00 2001 From: Prayas Dey Date: Tue, 18 Aug 2026 23:47:32 +0530 Subject: [PATCH 2/2] fix(Finding_Lanes): Dynamic ROI, slope filtering, multi-source CLI, visualizer, and unit tests (fixes #467) --- Finding_Lanes/README.md | 135 +++++++++-- Finding_Lanes/lanes.py | 434 +++++++++++++++++++++++++++++++----- Finding_Lanes/sub.py | 118 +++++++++- Finding_Lanes/test_lanes.py | 153 +++++++++++++ 4 files changed, 753 insertions(+), 87 deletions(-) create mode 100644 Finding_Lanes/test_lanes.py diff --git a/Finding_Lanes/README.md b/Finding_Lanes/README.md index 2e405fa8..881760e3 100644 --- a/Finding_Lanes/README.md +++ b/Finding_Lanes/README.md @@ -2,31 +2,130 @@ ![Star Badge](https://img.shields.io/static/v1?label=%F0%9F%8C%9F&message=If%20Useful&style=style=flat&color=BC4E99) ![Open Source Love](https://badges.frapsoft.com/os/v1/open-source.svg?v=103) -# 🚘 Finding Lanes -### Let's go! - +# ?? Finding Lanes -## 🛠️ Description -Finding Lanes is a Python computer vision script built with OpenCV and NumPy to detect road lane markings in images and video streams. It performs RGB to Grayscale conversion, Gaussian blurring, Canny edge detection, Region of Interest (ROI) polygon masking, Hough Transform line detection, and linear regression slope/intercept averaging to compute and visualize clear lane boundaries. +> Real-time and static road lane boundary detection using OpenCV and NumPy. -## ⚙️ Languages or Frameworks Used -Run the following command: +

+ Finding Lanes Demo +

+ +## ??? Description + +**Finding Lanes** is a modular Python computer vision project that detects and highlights road lane boundaries from static images, video files, or live camera feeds. + +The pipeline processes each frame through several standard computer vision stages: +1. **Grayscale Conversion & Gaussian Blur**: Reduces image noise and gradient variance. +2. **Canny Edge Detection**: Identifies sharp brightness transitions indicating potential lane boundaries. +3. **Dynamic Region of Interest (ROI)**: Dynamically computes a triangular polygon mask proportional to frame dimensions to eliminate irrelevant road surroundings and sky. +4. **Hough Transform Line Detection (`cv2.HoughLinesP`)**: Identifies line segments from edge pixels. +5. **Slope-Intercept Regression & Extrapolation**: Separates left (negative slope) and right (positive slope) line segments, filters out horizontal noise markings, and extrapolates continuous lane boundary lines. +6. **Alpha Blending (`cv2.addWeighted`)**: Overlays highlighted lane boundaries onto the original frame. + +--- + +## ? Features + +- **Dynamic ROI**: Adapts automatically to arbitrary image/video resolutions and aspect ratios. +- **Robust Line Filtering**: Ignores horizontal markings and guards against zero-division, `NaN`, or out-of-bound coordinates. +- **Multi-Source Support**: Seamlessly processes static images (`picture.jpg`), video files (`video.mp4`), or live camera streams (`--camera 0`). +- **Headless & Batch Support**: `--no-show` flag and `--output` options for saving results in automated/CI pipelines. +- **Pipeline Visualizer (`sub.py`)**: 4-panel subplot showing Original, Canny Edges, ROI Masked, and Lane Detection stages side-by-side. +- **Comprehensive Unit Tests (`test_lanes.py`)**: 100% test coverage over all core pipeline functions and edge cases. + +--- + +## ?? Requirements & Installation + +Ensure you have Python 3.8+ installed. Install the required dependencies: + +```sh +pip install -r requirements.txt +``` + +Or install dependencies manually: + +```sh +pip install opencv-python numpy matplotlib +``` + +--- + +## ?? How to Run + +Navigate to the project folder: + +```sh +cd Finding_Lanes +``` + +### 1. Run Lane Detection on Video (Default) +```sh +python lanes.py +``` +> Press **`q`** on the video window to quit. + +### 2. Run Lane Detection on a Static Image +```sh +python lanes.py --image picture.jpg +``` + +### 3. Run on Custom Video and Save Output +```sh +python lanes.py --video path/to/video.mp4 --output output.mp4 +``` + +### 4. Run on Live Webcam +```sh +python lanes.py --camera 0 +``` + +### 5. Multi-Stage Pipeline Visualizer (`sub.py`) +To inspect intermediate computer vision stages side-by-side using Matplotlib: +```sh +python sub.py +``` +Or with custom image and save output: ```sh -$ python -m pip install --upgrade pip -$ python -m pip install opencv-python +python sub.py --image picture.jpg --output pipeline_stages.png ``` -## How to run -open a terminal in the folder where your script is located and run the following command: + +--- + +## ?? Running Unit Tests + +Run the test suite using Python's built-in `unittest` runner: + ```sh -$ python lanes.py +python -m unittest test_lanes.py -v +``` + +--- + +## ?? Project Structure + ``` -## How to close +Finding_Lanes/ +??? README.md # Project documentation and guide +??? lanes.py # Core lane detection engine and CLI +??? sub.py # 4-stage pipeline visualization utility +??? test_lanes.py # Unit test suite +??? picture.jpg # Sample input road image +??? video.mp4 # Sample input road driving video +??? capture.png # Demo output screenshot +``` + +--- + +## ?? Demo -Just press q +

+ Finding Lanes Demo Output +

-## 📺 Demo +--- - +## ?? 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 ee624fa3..5b7b8934 100644 --- a/Finding_Lanes/lanes.py +++ b/Finding_Lanes/lanes.py @@ -1,109 +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). -def make_coordinate(image, line_parameters): - slope, intercept = line_parameters - if slope == 0: + 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 - 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]) + slope = float(line_parameters[0]) + intercept = float(line_parameters[1]) -def average_lines_intercept(image, lines): - left_fit = [] - right_fit = [] - if lines is None: + # 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)) + + return np.array([x1, y1, x2, y2], dtype=np.int32) + + +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)) - lines_list = [] + 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: - lines_list.append(left_line) + 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: - lines_list.append(right_line) + 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. - return np.array(lines_list) if len(lines_list) > 0 else None + 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 -dir_path = os.path.dirname(os.path.abspath(__file__)) -video_path = os.path.join(dir_path, "video.mp4") -if not os.path.exists(video_path): - video_path = "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 -cap = cv2.VideoCapture(video_path) + 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. -while cap.isOpened(): - ret, frame = cap.read() - if not ret or frame is None: - break + 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 a77ab2b6..89f15730 100644 --- a/Finding_Lanes/sub.py +++ b/Finding_Lanes/sub.py @@ -1,12 +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 -dir_path = os.path.dirname(os.path.abspath(__file__)) -image_path = os.path.join(dir_path, "picture.jpg") -if not os.path.exists(image_path): - image_path = "Finding_Lanes/picture.jpg" +# 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) + -image = img.imread(image_path) -plt.imshow(image) -plt.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()