The Insta360 Link has optical zoom up to 4×. My first tracker build ignored it entirely - fixed zoom at 1×, all the time. Aeroplanes at 15km looked like specks. Aeroplanes at 2km filled the frame uncomfortably, clipped at the edges mid-manoeuvre. Both felt wrong.
The obvious fix is to zoom in for distant aircraft and zoom out for close ones. It doesn’t work well in practice.
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. That’s the physically correct relationship:
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 the cruise at roughly constant distance would still jitter as ADS-B measurements fluctuated by a few hundred metres between polls.
Watching the zoom HUD display on the dashboard: flickering, always moving. Not a problem in itself - the camera handles zoom commands smoothly - but it meant the zoom was always in transition, never settled, and I spent a lot of attention watching it instead of the aircraft.
What I Actually Wanted
Zoom serves a simpler purpose here than I was trying to make it serve. A distant aircraft needs help being visible at all. A close aircraft doesn’t need any help - it’s already large in frame, and zooming in just risks clipping it.
The actual requirement is simpler: don’t let distant aircraft become invisible, and don’t let close ones overflow the frame. Those are threshold conditions, not a continuous tracking problem.
So instead of a curve, I used two flat regions with a linear ramp between them:
ZOOM_MIN = 300 # 1× 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 1×. Beyond 15km, always 4×. Between them, linear interpolation. t is just the normalised position in the range - 0 at the near threshold, 1 at the far threshold.
A note on the zoom values: the camera’s zoom API takes an integer in the range 100–400, where 100 is 1× and 400 is 4×. I’m using 300 as ZOOM_MIN rather than 100, which means I’m starting at 3× rather than 1×. That’s not a mistake - it reflects that most aircraft I see are already at moderate distance and benefit from some zoom even at close range. The labels in the comments are slightly wrong. I’ve left them as a historical artifact of tuning and then not updating the comment.
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 don’t move the zoom at all 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 8km (midpoint of the ramp) produces a zoom change of 200/13000 * 100 ≈ 1.5 units - essentially invisible, since the camera’s zoom range is 300 units wide.
Compare this to the k / distance curve. At 8km, a 200m fluctuation changes k/8000 - k/8200 ≈ k * 0.000003. With k chosen to give 4× at 15km (so k = 400 * 15 = 6000), that’s a fluctuation of 6000 * 0.000003 * 100 ≈ 1.8 units. Similar in the cruise, but much worse at close range where the 1/distance curve is steep. At 2km, the same 200m fluctuation moves the zoom by 6000 * (1/2000 - 1/2200) * 100 ≈ 27 units. Noticeable.
The flat-region approach tolerates noise uniformly at the cost of not tracking apparent size exactly. For this application, that’s the right trade.
The Units
One confusing thing I sat with for longer than I should have: ZOOM_MIN = 300 doesn’t mean 3× zoom, and ZOOM_MAX = 400 doesn’t mean 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. ZOOM_MIN = 300 is 3×. I’d written the comment as # 1× initially based on a misread of the UVC GET_MAX response, then tuned the values up and forgot to update the comment. What the code actually does is ramp from 3× to 4× based on distance. The 1× end of the camera’s range is never used in practice - 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. The error is small enough that I’ve never noticed it visually, but a 1/distance ramp clamped to the flat regions would be more principled.
I’d also tie the near threshold to altitude rather than just 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. Something to come back to.