Range from apparent size

A known object gives a range, and the range gets worse as the square of itself.

U11.3VisionUniversity35 min

Do this lesson in the simulator

A 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 a real tag detector produces the dist_cm it reports, which is worth knowing before you trust that number too far. (The simulator's tag detector cheats and reports the true range, rounded to a millimetre, so the errors below are ones you would meet on the real robot, not in apriltags() here. The blob detector is honest: its box is drawn from the apparent size, so this lesson measures with balls.)

Similar triangles: an 8 cm ball at 25 cm and its image, which is f S / d pixels widew = 29.6 pxS = 8 cmf = 92.4 pxd = 25 cmpinhole
The two triangles have the same shape, so w / f = S / d. Here 8 / 25 = 29.6 / 92.4: the ball covers the same fraction of its range as its image covers of the focal length.

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 an 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.

The ranges an 8 cm ball's box can report, one for each whole number of pixels, crowding in close and spreading out far away020406080100120140160range, cm5 px6 px7 px8 px10 px15 px30 pxeach tick: a range the box width can reporttrue 70.0box says 73.9true 126.5box says 123.2
The box width is a whole number of pixels, so the range can only be one of these ticks (d = 739 / w). They are packed tight close in and 20 to 30 cm apart past a metre, so a true range is snapped to the nearest one.

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.
A smaller ball nearer looks the same as a bigger one further off; a flat tag turned 60 degrees looks half as wideSmaller and nearer6 cm at 52.5 cm8 cm at 70 cmboth 10.6 px wideTurned awaysquare onturned 60°, it looks 4 cm wide,so the range reads 140 cm, not 70
Two of the ways the size assumption breaks. The camera measures an angle, and a 6 cm ball at 52.5 cm fills exactly the same angle as an 8 cm ball at 70 cm. A flat tag turned 60 degrees is only 4 cm wide as the camera sees it, so its range comes out double.

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")

Run this in the simulator

The balls are really 70.0 cm and 126.5 cm away. The box says 73.9 and 123.2, the area says 69.8 and 126.1, so the box is out by 3.9 and 3.3 cm and the area by well under a centimetre at both. At the far ball one pixel of box width is worth 20.5 cm, which is why a box only 6 pixels wide cannot do better. Neither estimate 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 R that 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

  1. Do it again using the area rather than the box width. How much do the two answers differ at each ball?
  2. At what range does one pixel become worth more than 10 percent of the range itself, for this ball and this camera?
  3. The depth sensor is also on board. Read it and compare. At which range does each instrument win?