A GUI for exploring circle detection (the OpenCV Hough Circle Transform) on uploaded images or a live camera feed. Adjust every parameter with sliders and watch the whole pipeline — grayscale → blur → edges → detected circles — update in real time.
This is a sibling of the HSV Picker GUI: same layout, same workflow, but for finding circles instead of filtering colours.
- Python 3.12.3 or higher
Before diving into the algorithms, make sure you have the required tools and libraries installed.
- Setup your development environment.
- Run the command to start the GUI:
Use the run script to start the GUI:
./runOr run the following command directly:
streamlit run ./gui/main.py --server.port 8503- Open the link in your browser: http://localhost:8503/
Circle detection uses cv2.HoughCircles. Each slider maps directly to one of its arguments:
| Parameter | What it does |
|---|---|
| Median blur kernel | Smooths the image before detection. Larger removes more noise but softens edges. Must be odd. |
| dp | Inverse ratio of accumulator resolution to image resolution. 1 = same resolution; larger finds coarser circles faster. |
| minDist | Minimum distance between the centres of two detected circles. Too small produces many overlapping false circles. |
| param1 | Upper threshold for the internal Canny edge detector (the lower one is half of it). Use the Edges panel to tune it visually. |
| param2 | Accumulator vote threshold for circle centres. Smaller = more circles (including false ones); larger = only strong circles. |
| minRadius / maxRadius | Restrict the size of circles to detect, in pixels. 0 means unbounded. |
- Start with the Edges panel and adjust param1 until the circle outlines you care about are clean and continuous.
- Lower param2 until your circles appear, then raise it just enough to drop the false positives.
- Set minRadius / maxRadius to ignore blobs that are the wrong size, and raise minDist if you get clusters of overlapping detections.
Detected circles are drawn in green with a red centre dot, and the current parameters can be exported as JSON from the sidebar.
Tuning by hand is fine for one image. For a folder of them, tuning/ does it
automatically: tell it how many circles are in each image and it searches for the
parameters that find exactly that many.
The count goes in the filename, as the last _n<count> (or -n<count>) token:
data/images/coins_n7.jpg -> 7 circles
data/images/wafer-n12.png -> 12 circles
data/images/2024_batch_n3.jpg -> 3 circles
data/images/IMG_0042.jpg -> unlabelled; reported, not guessed at
Filename labels fail silently when mistyped, so check what was parsed before committing to a long run:
python -m tuning labelspython -m tuning searchEach image is searched independently and the winning parameters recorded. Common options:
python -m tuning search --dir data/samples # a different folder
python -m tuning search --image coins_n7.jpg # one image (repeatable)
python -m tuning search --quick # small grid, for a first look
python -m tuning search --jobs 4 # cap the worker processes
python -m tuning search --force # re-search images already recorded
python -m tuning search --grid my_grid.json # custom search spaceImages already matched in results/best_params.json are skipped, so adding a
new batch later only searches the new files. A recorded miss is tried again —
the grid may have changed since.
| File | Contents |
|---|---|
results/best_params.json |
The winning config per image, in both pixel and fractional form |
results/global_params.json |
The single config that fits the most images, with a per-image hit/miss list |
results/search_log.csv |
Every parameter set that produced the right count, ranked |
results/overlays/*.png |
The winning detections drawn on each image, named <image filename>.png |
Look at the overlays. Finding the right number of circles is not the same as finding the right circles, and the overlay is what tells the two apart.
Copy the config_pixels values from best_params.json straight into the GUI
sliders — the keys are the same.
param2 is the expensive axis: 200 values, and it interacts with everything
else. But circle count is near-monotone in it — a higher accumulator threshold
admits fewer circles — so it is searched rather than enumerated:
- Bracket. Try the loosest
param2. If even that finds fewer circles than the target, no threshold can reach it, so the combination is discarded after one detector call. Most of the grid dies here. - Bisect. About eight calls to find where the count crosses the target.
- Scan. Walk the integers around the crossing, because the monotonicity is only approximate and bisection can step over an exact hit.
Everything else — blur kernel, dp, minDist, param1, radius range — is a
plain grid, and lengths are stored as fractions of the image's shorter side so a
config means the same thing on a 640 px image and a 4000 px one.
Usually many parameter sets produce the right count and most of them are coincidences. Two things separate the real ones:
- Run width — how many consecutive
param2values give the target count. A config that works acrossparam234–41 is an operating point; one that works only at exactly 52 is a fluke. - Stability — whether nudging
dp,minDistorparam1one grid step still gives the target count.
python tools/make_synthetic.py
python -m tuning search --dir data/samplesThis writes images with a known number of circles drawn into them, so the whole pipeline can be exercised end to end and the overlays checked against an answer that is right by construction.
pip install -r requirements-dev.txt
pytest