A classical vision pipeline

This project detects road lanes using C++14 and OpenCV. It works directly with image colors, edges, and line geometry: each video frame passes through a sequence of filters before the estimated left and right lane boundaries are drawn back onto the image.

The demonstration below comes from the repository’s lane_detection.gif, converted to video for playback.

Lane detection and lane-region overlay from the project repository.

The processing sequence

The LaneDetection function makes the pipeline explicit. Color filtering retains likely lane markings; a region-of-interest mask focuses on the road; Canny extracts edges; the Hough transform finds line segments; and a weighted blend combines the resulting overlay with the original frame.

Full frame-processing pipelinemain.cpp ↗
cv::Mat LaneDetection(cv::Mat src){
    cv::Mat color_masked, roi_img, canny_img, hough_img, final_img;
    color_masked = colorFilter(src);
    roi_img = ROI(color_masked);
    canny_img = canny(roi_img);
    hough_img = lineDetect(canny_img);
    final_img = weighted_img(hough_img, src);
    return final_img;
}

Finding yellow and white markings

The color filter converts the image to HLS and constructs separate masks for yellow and white. The yellow mask uses lower and upper bounds of (10, 0, 90) and (50, 255, 255). The white mask selects high lightness with a lower bound of 190. Combining the masks retains candidate markings before the geometric stages.

HLS color masks · source excerptmain.cpp ↗
cv::Mat colorFilter(cv::Mat src){
    cv::Mat hls, yellowmask, whitemask, mask, masked;
    cv::cvtColor(src, hls, cv::COLOR_RGB2HLS);
    cv::inRange(hls, cv::Scalar(10,0,90), cv::Scalar(50,255,255), yellowmask);
    cv::inRange(hls, cv::Scalar(0,190,0), cv::Scalar(255,255,255), whitemask);
    cv::bitwise_or(yellowmask, whitemask, mask);
    cv::bitwise_and(src, src, masked, mask=mask);
    return masked;
}

Focusing on the road

The region of interest is a trapezoid. Its lower edge spans the image width, while its upper edge runs from 45% to 55% of the image width at 60% of the image height. The implementation fills this polygon into a mask and applies it with bitwise_and, removing image regions outside the expected road area.

Road-mask vertices · reformatted excerptmain.cpp ↗
int x = src.cols;
int y = src.rows;
cv::Point polygon_vertices[1][4];
polygon_vertices[0][0] = cv::Point(0,y);
polygon_vertices[0][1] = cv::Point(x,y);
polygon_vertices[0][2] = cv::Point(
    (int)std::round(0.55*x), (int)std::round(0.6*y));
polygon_vertices[0][3] = cv::Point(
    (int)std::round(0.45*x), (int)std::round(0.6*y));

From edges to line segments

Canny runs on the grayscale masked image with thresholds of 50 and 120. The probabilistic Hough transform then uses a one-pixel distance resolution, a one-degree angular resolution, a vote threshold of 10, a minimum line length of 20 pixels, and a maximum gap of 100 pixels.

Edge thresholds and Hough parametersmain.cpp ↗
cv::Mat canny(cv::Mat img){
    cv::Mat edges;
    cv::Canny(grayscale(img), edges, 50, 120);
    return edges;
}

cv::Mat lineDetect(cv::Mat img){
    return hough_lines(img, 1, CV_PI/180, 10, 20, 100);
}
Detecting and drawing line segmentsmain.cpp ↗
cv::Mat hough_lines(cv::Mat img, double rho, double theta, int threshold, double min_line_len,double max_line_gap){
    std::vector<cv::Vec4f> lines;
    cv::Mat line_img(img.rows, img.cols, CV_8UC3, cv::Scalar(0,0,0));
    cv::HoughLinesP(img, lines, rho, theta, threshold, min_line_len, max_line_gap);
    drawLines(line_img, lines);
    return line_img;
}

Estimating the lane boundaries

The drawing stage separates candidate segments by slope and position. Right-lane candidates require a slope above 0.3 and an initial x-coordinate above 500 pixels. Left-lane candidates require a slope below −0.3 and an initial x-coordinate below 600 pixels.

For each side, the code averages slopes and intercepts, then extrapolates a boundary from 65% of the image height to the bottom. It fills the polygon between the boundaries and draws the two lane lines. The excerpt shows the averages and left-boundary calculation; the right boundary uses the same approach.

Averaging and extrapolation · source excerptmain.cpp ↗
float left_intercept_avg = vectorAverage(leftIntercept);
float right_intercept_avg = vectorAverage(rightIntercept);
float left_slope_avg = vectorAverage(leftSlope);
float right_slope_avg = vectorAverage(rightSlope);

int left_line_x1 = (int)std::round(
    (0.65*img.rows - left_intercept_avg)/left_slope_avg);
int left_line_x2 = (int)std::round(
    (img.rows - left_intercept_avg)/left_slope_avg);

The overlay is combined with the source image using addWeighted, with default weights of 0.8 for the overlay and 1.0 for the original frame.

Building and running the project

The CMake configuration requires CMake 3.17.2 or newer, selects C++14, and links the LaneDetection executable against OpenCV. Run the program on a machine with OpenCV installed and a graphical display. It prompts for a video path, processes frames until the stream ends, and also exits when Escape is pressed.

Build and run · shell commandsCMakeLists.txt ↗
git clone https://github.com/hamptonjc/lane-detection-CPP.git
cd lane-detection-CPP
cmake -S . -B build
cmake --build build
./build/LaneDetection

# At the video-path prompt, enter:
# test_videos/solidWhiteRight.mp4

Scope and next steps

The implementation is a compact, frame-by-frame lane detector. Its color thresholds, slope filters, and fixed horizontal cutoffs encode assumptions about the scene. The boundary model is straight-line based, and the code does not carry lane estimates forward between frames.

Useful next steps would be handling frames with no valid candidates before computing averages, replacing pixel-specific cutoffs with normalized coordinates, and adding temporal smoothing. These are possible extensions to the current code.

Code and demonstration

Browse the repository ↗
Read the complete implementation ↗
README and original demonstration ↗