Marshall Frith

Projects

Counting Parked Aircraft in Radar, and Why I Stopped

Airfield mode for the ship finder. Built in a day, worked on synthetic data, and parked the same day when real aprons answered back.

This is the tutorial for a feature I parked the same day I built it. It stays on the site because the reason it failed is more useful than most things that work, and because the code is still in the ship finder, dormant, waiting for the one test that would justify waking it.

The idea: point the ship detector's CFAR at airport aprons instead of water and count large parked aircraft by size class. Sharpen the guess with a prior from which airfield you are looking at, bundled from OurAirports as 5,682 fields in a 125 KB file, no network at runtime.

Same detector, three inverted assumptions

Sea mode's job is rejecting land. On an apron that logic rejects everything, so airfield mode inverts three things, and each inversion taught me something.

The background gate moves from -11 dB to +2 dB, because sea sits at -22 dB and concrete at -10 to -5. The land mask is off, because it would correctly mask the entire airport. And the breakwater filter is off, because six evenly spaced collinear widebodies on a terminal pier are exactly the dashed-line signature it was built to delete. Turn it on and an entire pier vanishes, looking like "found nothing" rather than a bug. The regression test's pier case exists specifically to catch that.

python
mean, std = _ring_stats(db, valid, guard=AIR_GUARD_PX, bg=AIR_BG_PX)
target = (
    valid
    & (db > mean + AIR_K_SIGMA * std)
    & (db > AIR_ABS_FLOOR_DB)
    & (mean < AIR_BACKGROUND_DB_MAX)
)

The subtle one: CFAR ring sizes track target spacing, not target size. Sea mode's 250 m guard and 600 m background rings are, on an apron, full of the neighbouring airframes. Local mean and deviation shoot up and nothing clears threshold, which looks exactly like a broken detector. It cost a full debugging cycle before the rings shrank to 100 and 220 metres, sized to stand spacing.

Confidently wrong is worse than absent

Two failures in this build produced answers that looked great.

The aspect gate started at 3.0, permissive, on the theory that permissive gates add recall. What it added was 96 by 34 metre merged blobs, two neighbours or an aircraft plus its shadow welded together, which the classifier faithfully reported as "very_large" aircraft. Real airframes run 0.8 to 1.3 because span roughly equals length. The gate is 1.8 now, and the median real detection came out at 1.2 to 1.3.

The nearest-airfield lookup sorted by airport category before distance, and returned Reagan National for the exact coordinates of Joint Base Andrews, 15.5 km away, because DCA is a large airport and Andrews is medium. A military field resolving to a civil one poisons precisely the prior the module exists to supply. Distance plus a small category penalty fixed it. Related trap: a boolean military flag would have put a heavy-lifter prior on Frankfurt, because its keywords still say "Rhein-Main Air Base", closed 2005. The flag is graded instead.

And the synthetic test scene was wrong before the detector was: I planted aircraft 7 dB over the apron and they sat below threshold. Real airframes return 12 to 15 dB over concrete, because wing roots and engine pylons are dihedral corner reflectors. When a synthetic harness disagrees with reality, suspect the scene first.

The verdict

On real data: LAX terminal aprons gave 3 detections. Roswell storage gave 14. Victorville gave 8. Those fields hold hundreds of airframes.

That is not a bug and no parameter fixes it. Aircraft parked closer than about 100 metres cannot be separated at 10 metre resolution; terminals and boneyards merge into one continuous bright mass, the same physical limit as a ship moored hard against a quay. A 787-9 and an A330-300 are the same object to this sensor, 63 by 60 metres both, which is why the output was only ever a size class. Sentinel-2 does not rescue any of this; its RGB is also 10 m.

So the honest scope shrank to aircraft parked far apart: military ramps, alert pads, revetments, open hardstands. That is untested, and it is the only experiment that would justify continuing. I stopped there. Ships are a better use of the same detector, and the plan file says exactly that.

Run the post-mortem yourself

The code is dormant but the harnesses run:

bash
.venv/bin/python check_airfield.py
bash
.venv/bin/python classify_air.py

The first plants a synthetic apron and demands 10 of 10 aircraft plus all six pier widebodies. The second shows why the prior matters: the same 48 by 51 metre measurement classified at three different airfields and with no context at all, and the ranking moves.

What I would tell you before you start something like this: write the verdict criteria before building, and honour them. The plan said how this would be judged. It was judged, it lost, and the loss cost one day instead of a month of UI on top of a detector that cannot see a terminal.

Comments

Plain text only. Held for review before it appears.