7.27. Finding circles and rectangles¶
Lines and segments cover the straight edges in the captured frame, but plenty of real-world features the camera looks for are not straight. A coin lying on a desk is a circle. A printed label, a sticky note, the top of a box viewed off-axis is a quadrilateral. The image module exposes a dedicated detector for each: a Hough-style search for circles and an AprilTag-derived search for four-sided shapes.
Both methods follow the same template the
line detectors do – threshold controls
how many votes a detection needs, roi
narrows the search, and the returned objects
carry both a position and a confidence
magnitude – but the parameter spaces and
the right defaults differ enough to deserve
explicit coverage.
7.27.1. Hough circles¶
find_circles() runs the
circular variant of the Hough transform.
Each edge pixel from the Sobel pre-pass
votes for every circle that could pass
through it; circles that collect enough
votes are returned.
circles = img.find_circles(threshold=3500,
x_margin=10, y_margin=10, r_margin=10,
r_min=10, r_max=80, r_step=2)
for c in circles:
img.draw_circle((c.x, c.y, c.r), color=(255, 0, 0))
threshold is the minimum sum of Sobel
edge magnitudes along the candidate circle.
Bigger circles trace more pixels and so
need higher thresholds to pass; a value
that finds a 20-pixel-radius coin will
also fire on noise around a 100-pixel
edge, while a value tuned for the large
coin will miss the small one. When the
target radius range is known, the right
threshold scales with the circumference –
roughly threshold = 50 * 2 * pi * r
gives a reasonable starting point and the
right value follows from a brief tuning
pass.
r_min, r_max, and r_step set
the radius search. Without bounds, the
detector would search every radius from a
few pixels up to the half-width of the
image, which is both slow and a recipe for
false positives. Setting r_min and
r_max to bracket the expected target
size by a generous margin (e.g.
r_min=15, r_max=25 for a coin known to
be around 20 pixels) cuts the work
substantially and improves the signal-
to-noise ratio of the votes. r_step
controls the granularity of the search;
larger steps run faster and may miss a
circle whose true radius falls between two
sampled values. The default r_step=2 is
a reasonable compromise.
x_margin, y_margin, and r_margin
control merging of nearby detections, the
way theta_margin and rho_margin do
for line detection. A single physical circle
in the image votes for a cluster of
candidate circles whose centres and radii
agree to within a few pixels; the margins
collapse each cluster to its peak before the
result list is built. Larger margins return
fewer, more confident detections; smaller
margins return more detections with possible
near-duplicates.
x_stride and y_stride step the
voting scan, the same way they do in the
other detectors. The default of 2 and
1 is fine for most images; cranking
both up to 4 is the standard speed
trade-off for an image known to contain
large circles.
Each returned Circle
carries x, y, r (the centre and
radius) and magnitude (the vote total,
useful as a confidence score for sorting or
filtering). Drawing the detection back into
the frame is one call –
draw_circle() takes the
(x, y, r) 3-tuple, available as
(c.x, c.y, c.r) directly off the result.
7.27.2. Rectangles¶
find_rects() borrows the
quadrilateral detector from the AprilTag
pipeline – the same routine that locates
the black square around a tag is exposed
on its own as a general-purpose
rectangle finder.
rects = img.find_rects(threshold=12000)
for r in rects:
img.draw_rectangle(r.rect, color=(0, 255, 0))
for corner in r.corners:
img.draw_circle((corner[0], corner[1], 4),
color=(0, 255, 0))
threshold is the minimum sum of edge
magnitudes around the rectangle’s
perimeter. A printed black-on-white
rectangle in a well-lit frame easily clears
10000; a faint rectangle on a textured
background may need to drop to 2000 –
trading off false positives for sensitivity.
Like the circle detector, the right value
follows from a quick tuning pass with the
intended targets in view.
The detector is projective – it finds
quadrilaterals whose sides are straight but
not necessarily parallel or
axis-aligned. A label viewed off-axis
looks like a trapezoid in the image, and
the rectangle detector finds it
correctly; an axis-aligned rectangle is
just the degenerate case where the four
corners happen to form a right-angled box.
roi restricts the search; the rest of
the keyword arguments take their defaults
from the AprilTag pipeline and rarely need
tuning.
Each returned Rect
carries the axis-aligned bounding box –
x, y, w, h, plus the
rect 4-tuple that
draw_rectangle()
expects – and the four detected corners
as corners. The bounding box is what
the application uses for rough position and
size; the corners describe the projective
quadrilateral itself. When the
camera is viewing a flat target from an
angle and the application needs to undo
the keystone – read text on a label,
sample colour from a flat patch – the
corners feed directly into
rotation_corr() with the
corners= keyword (see lens and
perspective correction), and
the output is the rectified rectangle ready
for whatever analysis comes next.
Warning
Because the detector is tuned for what the AprilTag pipeline needs – quadrilaterals with strong, high-contrast borders, like a black tag outline on white paper – it is not a find-every-rectangle pass. Rectangles with soft contrast, textured edges, or busy surroundings can go undetected entirely. How well it works is situation dependent: test it against the real targets early, before building a pipeline around it.
7.27.3. When the detector misfires¶
Circles in particular benefit from a
pre-filter on the input. A noisy frame
yields lots of stray edge pixels that all
vote, and the resulting Hough space has
broad mushy peaks that the merger has a
hard time separating. A
gaussian() or
mean() pass before
find_circles() smooths
the noise away and leaves the true edges
intact; the detector returns cleaner peaks
in less time.
For rectangles, the common failure mode is
the opposite: low contrast between the
rectangle and its background means the
edge-magnitude sum never clears
threshold. An histeq()
pass to redistribute the brightness range
across the full 0-to-255 spread restores
the contrast the detector needs. (The
contrast must exist somewhere in the
image; histogram equalisation can only
amplify what is already there.)