Range from apparent size
A known object gives a range, and the range gets worse as the square of itself.
Do this lesson in the simulatorA camera gives bearings. To get a position you need a range as well, and the cheapest way to get one is to know how big the thing is.
The relation
An object of size S at range d, seen square on, covers
w = f * S / d pixels
so
d = f * S / w
That is the whole method, and it is used everywhere: apparent height of a car in a dashcam, apparent width of a face in a camera's autofocus, apparent size of a tag in the AprilTag library. It is how the tag detector in this simulator produces the dist_cm it reports, which is worth knowing before you trust that number too far.
How bad it gets, and how fast
Differentiate:
dd/dw = -f * S / (w * w) = -d * d / (f * S)
The range error grows as the square of the range. One pixel of error in the width costs you d^2 / (f * S) centimetres of range. Put numbers on that for a 8 cm ball on this camera, f * S = 92.4 * 8 = 739:
| range | apparent width | one pixel is worth |
|---|---|---|
| 25 cm | 30 px | 0.8 cm |
| 50 cm | 15 px | 3.4 cm |
| 100 cm | 7 px | 14 cm |
| 150 cm | 5 px | 30 cm |
At arm's length it is a precision instrument. At a metre and a half it is a guess. Nothing about the algorithm changed between those two rows: the geometry did.
That quadratic law is not special to apparent size. Stereo has it too, with the baseline in place of the object size, and so does triangulating from two positions along a path. Any time depth comes from an angle, the error goes as the square of the distance, and knowing this saves you from designing a system whose specification is impossible.
Detection versus measurement
The detector says there is a red ball at a certain width. That is a detection: a fact about the image, and quite reliable. Converting it to a range makes it a measurement, and now it carries an uncertainty that depends on range, on how accurately the width was found, and on whether the object really is the size you assumed.
Three ways that assumption breaks, all of them real:
- The object is not the size you think. A slightly smaller ball at a shorter range produces exactly the same image. Nothing in the picture can tell them apart.
- The object is not square on. The width of a flat tag seen at 60 degrees is half its true width, so the range reads double. Tag libraries solve the full pose to avoid this; a bounding box cannot.
- The bounding box is quantised. A box edge is an integer. At 5 pixels wide, one pixel is 20 percent.
The third is measurable right now.
from bugbot import *
connect()
F, R = 92.4, 4.0
set_cv("blob", "red")
wait(0.3)
for cx, cy, area, x0, y0, x1, y1, aspect in blobs():
w_box = x1 - x0
w_area = 2 * (area / 3.14159) ** 0.5 # width implied by the area, which is not rounded to a pixel
print("box width", w_box, "-> range", round(2 * R * F / w_box, 1), "cm")
print("area width", round(w_area, 2), "-> range", round(2 * R * F / w_area, 1), "cm")
print("one pixel of width is worth", round((2 * R * F / w_box) ** 2 / (2 * R * F), 1), "cm here")
The two estimates disagree by several centimetres on the far ball and by less on the near one, and neither of them is noisy in the usual sense. This is quantisation, not noise, and averaging repeated frames will not remove it. Using the area instead of the box is an easy win, because area is a sum over many pixels and so is quantised far more finely than an edge is.
Where this is the right tool anyway
It sounds discouraging and it should not. Apparent size is often the correct engineering answer, because:
- it needs one camera and no extra hardware;
- the error is honest and computable, so a filter can be told about it (this is precisely the
Rthat grows with range from U6.6); - at the ranges where robots actually manipulate things, under half a metre, it is accurate to a centimetre or two.
Use it close in, distrust it far out, and always report the uncertainty alongside the number.
Task: range from apparent size
Two red balls are on the mat, both 8 cm across. Standing still, print near: and far:, the range to each in centimetres worked out from how wide it looks, and per pixel:, how many centimetres of range one pixel of width is worth at the further ball.
from bugbot import *
connect()
F = 92.4
R = 4.0
set_cv("blob", "red")
wait(0.3)
Challenges
- Do it again using the area rather than the box width. How much do the two answers differ at each ball?
- At what range does one pixel become worth more than 10 percent of the range itself, for this ball and this camera?
- The depth sensor is also on board. Read it and compare. At which range does each instrument win?