Skip to content

About

Classical OpenCV pipeline that locates traffic signs in a webcam feed via HSV colour segmentation, Canny edges and contour filtering, then labels them by template matching. University project.

Topics

Resources

Stars

0 stars

Watchers

1 watching

Forks

Latest commit

 

History

3 Commits

Folders and files

Repository files navigation

Traffic Sign Detection with Classical Computer Vision

A classical OpenCV pipeline that locates traffic-sign candidates in a live webcam feed using HSV colour segmentation, Canny edge detection and contour filtering, then labels each region by template matching against four reference signs. No machine learning is involved.

Overview

The program opens the default camera and processes every frame in turn. Traffic signs are visually distinctive — strong reds and blues, high-contrast borders, compact convex shapes — and this project exploits exactly those properties with hand-tuned image processing instead of a learned model. Colour segmentation isolates the regions that could plausibly be a sign, edge and contour analysis reduces them to a handful of bounding boxes, and each box is compared against four template images to decide which sign it resembles.

Originally developed as a university computer-vision project. It uses classical image processing throughout — there is no neural network, no training step and no learned features.

Scope, stated plainly: input is a live camera stream only. There is no code path for reading image files or video files, the recognised vocabulary is four signs, and the project contains no labelled evaluation. See Evaluation and Limitations.

Pipeline

Webcam frame (640x480 requested)
        │
        ▼
HSV colour segmentation
  red   (H 0-10 and 160-179, S>=100, V>=100)
  blue  (H 100-140, S>=100, V>=100)
  white (S<=50, V>=180)
  masks OR-combined, then applied back to the colour frame
        │
        ▼
Gaussian blur  (7x7, sigma 2.0)
        │
        ▼
Greyscale  →  Canny edge detection  (100, 200)
        │
        ▼
Contour extraction  (RETR_TREE, CHAIN_APPROX_NONE)
        │
        ▼
Candidate filtering, per contour
  polygon approximation (epsilon = 0.02 x perimeter) → bounding box
  keep if  aspect ratio < 3  and  area > 1500  and  contour is outermost
        │
        ▼
Template matching  (TM_CCOEFF_NORMED, edge map vs edge map)
  each of the 4 templates resized to the box, best score wins
        │
        ▼
Temporal vote  →  bounding box + label + FPS overlay

Templates are preprocessed once at startup with the same kind of transform applied to frames: Gaussian blur (7×7, σ 2.6) → greyscale → Canny (210, 220). Matching therefore compares an edge map against an edge map, not raw pixels, which is what makes it tolerant of the colour and brightness differences between a printed reference sign and a camera image.

Computer Vision Techniques

Technique Where Why it is used
HSV colour thresholding colorSegmentation Hue separates sign colours from the scene far more stably than RGB does under changing brightness. Red wraps around the hue circle, so it needs two ranges (0–10 and 160–179).
Mask combination (bitwise OR / AND) colorSegmentation Red, blue and white masks are unioned, then applied to the frame so that only sign-coloured pixels survive into the edge stage.
Gaussian blur frame loop, loadTemplate Suppresses sensor noise and mask speckle so Canny produces continuous contours instead of fragments.
Canny edge detection frame loop, loadTemplate Reduces both frames and templates to thin binary outlines, giving a representation that is comparable across lighting conditions.
Contour extraction detectSign Turns the edge image into discrete closed regions that can be measured and filtered individually.
Contour hierarchy filtering detectSign Keeping only outermost contours (hierarchy[…,3] == -1) discards the inner pictogram outlines so each sign yields one box, not several nested ones.
Polygon approximation detectSign approxPolyDP smooths each contour before its bounding box is taken, so noisy outlines do not inflate the box.
Geometric filtering detectSign Signs are compact, so candidates with an aspect ratio ≥ 3 or an area ≤ 1500 px are rejected before the expensive matching step.
Template matching templateMatching Normalised cross-correlation (TM_CCOEFF_NORMED) between the candidate's edges and each resized template's edges assigns the class.
Temporal majority vote templateMatching A rolling buffer of recent best matches is reduced with statistics.mode, intended to stop the label flickering between frames.

Note what is not used: there are no morphological operations, no Hough transforms, no vertex-count shape classification (the polygon approximation feeds only the bounding box), and no learned model of any kind.

Supported Signs / Classes

Exactly four, fixed in main.py and named after their template files:

Class Template Sign shape and colour
no_enter no_enter.png Red circle, white bar
bumps bumps.png Red-bordered triangle, black pictogram
give_way give_way.png Red-bordered inverted triangle
pedestrian pedestrian.png Blue square, white triangle and pictogram

Adding a class means dropping a PNG into the repository root and adding its filename to template_paths — the label is derived from the filename with the extension stripped.

Example Results

Figures produced by tools/pipeline_demo.py, which imports the functions from main.py unmodified and runs them on a synthetic frame built by compositing the repository's own four template images onto a flat background.

This is an illustration of the processing stages, not a measure of accuracy. The input is the template images themselves, which is the easiest possible case — nothing about real-world performance can be inferred from it.

Pipeline stages

All four signs survive colour segmentation, produce clean closed contours, and pass the geometric filter. The per-region template scores behind panel 4 are:

Candidate region Best match Score Runner-up
Red circle no_enter 0.221 give_way (0.021)
Inverted triangle give_way 0.233 no_enter (0.016)
Blue square pedestrian 0.181 bumps (0.001)
Red triangle bumps 0.121 no_enter (0.007)

The matcher picks the correct template for all four, and by a wide relative margin. Two things are still worth noticing: the absolute correlation peaks are low (0.12–0.23) even in this ideal case, which is characteristic of sparse edge-map correlation; and every box in panel 4 is labelled pedestrian, because the drawn label is the mode of a buffer shared by all regions rather than each region's own best match. That behaviour comes from the original code, is reproduced here unchanged, and is explained under Known Implementation Limitations.

Evaluation

There is no evaluation in this project. No labelled test set, no ground-truth annotations, no accuracy, precision or recall figures — none were produced when the project was written, and none are invented here.

No FPS figure was recorded historically either. main.py computes and overlays a live FPS counter, but that value is only displayed on screen; it is not logged or stored anywhere in the repository.

Reproduced measurements — obtained now, on the synthetic frame described above, and clearly not a substitute for a real evaluation:

Measurement Value
Processing chain, 640×480 frame with 4 sign candidates median 8.1 ms (min 7.7, max 28.0 over 30 runs)
Environment Python 3.11, OpenCV 5.0.0, Apple Silicon
Excluded camera capture, window rendering

The cost scales with the number of candidate regions that survive filtering, since every surviving region is correlated against all four templates. A cluttered scene will therefore be slower than this figure suggests, and no claim of real-time performance is made on the basis of one synthetic frame.

Project Structure

.
├── main.py                  Entire implementation: segmentation, edges, contours, matching, UI
├── no_enter.png             Template — red circle with white bar
├── bumps.png                Template — uneven road warning triangle
├── give_way.png             Template — give way / yield triangle
├── pedestrian.png           Template — pedestrian crossing square
├── requirements.txt         Third-party dependencies
└── tools/
    └── pipeline_demo.py     Renders the README figures from the templates above

main.py is a single flat module of six functions: colorSegmentation, loadTemplate, drawRectangle, detectSign, templateMatching and main.

Getting Started

Requires Python 3 and a working webcam — the program calls cv2.VideoCapture(0) and has no file-input mode.

python3 -m pip install -r requirements.txt
python3 main.py

A window opens showing the camera feed with candidate boxes, labels and an FPS counter drawn on it. Press q to quit. Templates are loaded by bare filename, so run the command from the repository root.

To regenerate the README figures without a camera:

python3 tools/pipeline_demo.py

Technologies

  • Python 3
  • OpenCV (opencv-python) — colour conversion, blurring, Canny, contours, template matching, GUI
  • NumPy — HSV threshold bounds and array handling
  • Python standard library — statistics for the label vote, time for the FPS counter

Nothing else. There is no deep-learning framework, no training pipeline and no dataset loader in this project.

Limitations

Documented against the code as written. The original implementation is preserved unchanged, so the notes below describe it exactly as it behaves rather than as it might be rewritten.

Algorithmic Limits

Inherent to the classical approach itself:

  • Fixed, hand-tuned thresholds. Every constant — the HSV bounds, the Canny hysteresis pair, the 1500 px area floor, the aspect-ratio cap of 3 — was chosen by hand for particular conditions. Nothing adapts to the scene.
  • Colour segmentation gates everything. A sign whose colours fall outside the HSV bands never reaches the edge stage, so dusk, strong shadow, glare, colour cast or a faded sign removes it entirely.
  • Template matching is not scale- or rotation-invariant. Templates are rescaled to each candidate box, but a rotated or perspective-skewed sign correlates poorly against a fronto-parallel template.
  • Four classes only, all of them clean rasterised pictograms rather than photographs of real signs, which widens the gap between template and camera appearance.
  • No shape classification. approxPolyDP is computed but its vertex count is never used, so a triangle and a circle are separated only by the correlation score.
  • Any sufficiently large, sign-coloured, roughly square object is a candidate — the filter is purely geometric.

Known Implementation Limitations

These are characteristics of the historical implementation, retained as written:

  • The displayed label reflects a global vote, not the region's own match. templateMatching computes the correct best match per region, then draws statistics.mode(past) — the mode of a buffer shared across every region and every frame. All boxes in a frame consequently show the same label, visible in the figure above. The intent was to stabilise labels over time; the effect is that per-region identity is not shown.
  • The acceptance threshold provides almost no rejection. A match is accepted when bestScore > 0.001, a value almost any correlation clears, so weak matches still enter the vote buffer.
  • The vote buffer is cleared, not slid. When past exceeds 50 entries it is emptied outright rather than dropping its oldest element, so the label history restarts periodically.
  • Bounding-box width and height are transposed. cv2.boundingRect returns (x, y, w, h), while the historical code assigns the result to x, y, h, w. Because signs are near-square the drawn boxes usually look correct, and the aspect-ratio test is unaffected since it takes the larger dimension over the smaller; an elongated candidate, however, is cropped and boxed with its dimensions transposed.
  • No guard on frame capture. The return flag from cap.read() is ignored, so the program raises if the camera drops a frame or is unavailable.
  • import time appears three times.

Evaluation Limits

  • No labelled data and no measured accuracy exist for this project, so its real-world detection and recognition performance is simply unknown.

Asset provenance

The four template PNGs are 1024 px rasterisations of standard traffic-sign pictograms, carrying only ImageMagick creation timestamps from 2022 in their metadata — no author, source or copyright field. Their origin is not documented anywhere in the repository, and no licensing claim is made about them here.

For the same reason no LICENSE file has been added: the code and the bundled images would need to be licensed separately, and the images' rights are unclear.

Background

Originally developed as a university computer-vision project and retained as an example of classical image-processing and visual-recognition work.

About

Classical OpenCV pipeline that locates traffic signs in a webcam feed via HSV colour segmentation, Canny edges and contour filtering, then labels them by template matching. University project.

Topics

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages