← Home

Zoom as a Function of Distance

The Insta360 Link has optical zoom up to 4×. My first tracker build left it fixed at 1×. That kept the control path simple, but it produced poor framing across the range I cared about. Aeroplanes at 15km looked like specks. Aeroplanes at 2km filled the frame uncomfortably and clipped at the edges mid-manoeuvre.

Making zoom a function of distance looked like the obvious fix. Zoom in for distant aircraft and zoom out for close ones. That sounds physically correct. In practice it produced the wrong control behaviour.

The Obvious Implementation

zoom command policy over distance

Apparent size of an object scales inversely with distance. An aircraft at 10km looks half the size it does at 5km. To keep it the same apparent size in frame you’d zoom in by 2× when the distance doubles. So the natural policy is:

zoom = k / distance

for some constant k chosen to fit the aircraft nicely at a reference distance.

I tried this. It produced a zoom level that changed constantly, because distance changes constantly. An aircraft on approach, descending steadily toward me, would be driving the zoom down the whole time. An aircraft in cruise at roughly constant distance would still jitter as ADS-B measurements fluctuated by a few hundred metres between polls.

The zoom HUD on the dashboard made the problem obvious because it was always moving. The camera handles zoom commands smoothly, so this was not a mechanical problem. It was a stability problem. The zoom never settled, which made the framing feel busy even when the aircraft motion itself was simple.

What I Actually Wanted

Zoom serves a simpler purpose here than I was trying to give it. A distant aircraft needs help being visible at all. A close aircraft does not need any help. It is already large in frame, and zooming in just risks clipping it.

The actual requirement is simpler. I need distant aircraft to stay legible and close ones to stay inside the frame, which makes this a threshold problem rather than a continuous tracking problem.

So instead of trying to track apparent size continuously, I reduced the problem to three bands, with a near region at minimum zoom, a far region at maximum zoom, and a ramp between them. That gives the behaviour I want while staying simple enough to reason about.

ZOOM_MIN     = 300    # 3× zoom
ZOOM_MAX     = 400    # 4× zoom
ZOOM_NEAR_KM = 2.0    # closer than this → minimum zoom
ZOOM_FAR_KM  = 15.0   # farther than this → maximum zoom

def _zoom_for_distance(dist_km: float) -> int:
    if dist_km <= ZOOM_NEAR_KM:
        return ZOOM_MIN
    if dist_km >= ZOOM_FAR_KM:
        return ZOOM_MAX
    t = (dist_km - ZOOM_NEAR_KM) / (ZOOM_FAR_KM - ZOOM_NEAR_KM)
    return int(ZOOM_MIN + t * (ZOOM_MAX - ZOOM_MIN))

Inside 2km, always 3×. Beyond 15km, always 4×. Between them, linear interpolation. t is the normalised position in the range.

Why the Flat Regions Matter

The key property is that zoom is constant in two large regions. Inside 2km and outside 15km, ADS-B distance fluctuations of a few hundred metres do nothing once you’re well inside either flat region.

The jitter problem only exists in the ramp, and in the ramp it’s mild. A 200m ADS-B fluctuation at 8.5km, roughly the midpoint of the ramp, moves the zoom by about 200/13000 * 100 ≈ 1.5 API units. That’s essentially invisible on a 300-unit zoom range.

Compare this to the k / distance curve. If k is chosen to give 4× at 15km, the same 200m fluctuation at 8km works out to about 1.8 API units, similar in the cruise. The difference is close in, where the 1/distance curve gets steep. At 2km, that same 200m fluctuation becomes about 27 units. Noticeable.

The flat-region approach tolerates noise uniformly at the cost of not tracking apparent size exactly. For this application, that is the right trade.

The Units

One confusing thing I sat with for longer than I should have is that ZOOM_MIN = 300 means 3× zoom, and ZOOM_MAX = 400 means 4×. The camera’s zoom range runs from 100 (1×) to 400 (4×), so it’s a linear scale where each 100 units is one additional optical zoom step. I originally had this comment wrong after misreading the UVC GET_MAX response, then later tuned the values upward. The snippet here is corrected. The code path itself ramps from 3× to 4× based on distance. The 1× end of the camera’s range is never used in practice because it makes distant aircraft genuinely hard to see.

Where It’s Called

Zoom is computed as part of _camera_angles, alongside pan and tilt, using the same haversine distance already calculated for the geometry.

def _camera_angles(lat, lon, alt_m):
    dist_m  = _haversine_m(OBS_LAT, OBS_LON, lat, lon)
    bearing = _azimuth_deg(OBS_LAT, OBS_LON, lat, lon)
    elev    = math.degrees(math.atan2(alt_m - OBS_ALT_M, max(dist_m, 1.0)))
    tilt    = max(TILT_MIN, min(TILT_MAX, elev + CAM_TILT_OFFSET))
    pan_max = _pan_max_for_tilt(tilt)
    pan     = max(PAN_MIN, min(pan_max, _bearing_to_pan(bearing)))
    zoom    = _zoom_for_distance(dist_m / 1000)
    return pan, tilt, zoom

Pan, tilt, and zoom are sent to the camera on every tick of the 40 Hz camera controller loop. Pan and tilt change smoothly with position prediction. Zoom steps discretely when the aircraft crosses a distance threshold, but because the transitions happen in the flat regions or very slowly in the ramp, you don’t see it.

What I’d Do Differently

The ramp is linear in distance. Apparent size is inversely proportional to distance, so a linear ramp slightly underzooms in the near half of the ramp and overzooms in the far half, relative to constant apparent size. A 1/distance ramp clamped to the same flat regions would be more principled.

I’d also tie the thresholds to altitude rather than just horizontal distance. A helicopter at 1.5km horizontal distance but 500m altitude is further from the camera in 3D than the distance figure suggests, and could use more zoom than a flat-terrain 2km threshold gives it. The slant range, sqrt(dist_m² + alt_diff_m²), would be a more honest input.

But that is a refinement, not a missing capability. The current policy gives me the trade I actually need. Distant aircraft stay visible, close aircraft stop blowing out the frame, and the zoom mostly stays put. It is less physically pure than k / distance, and much more useful.


Get notified when I publish new writeups and progress updates.

← Back