SpeckLock

See the drone,
then hit it.

Find a drone that occupies 4–15 pixels in 720p video โ€” then fly into it, using nothing but that camera. No radar, no datalink, no GPS on the target.

Detection — measured on real hand-labelled video Interception — measured in closed-loop Isaac Sim, no flight test
One engagement, start to finish (played 2×). The interceptor holds over a rendered town watching all four quarters at once. An intruder arrives on bearing 105° at 12 m/s, 170 m out and 2.7 pixels across, diving on a plaza block. All four camera feeds are shown; the outlined one owns the track. Green is the truth, the yellow ring is what the seeker believes, and the magnified inset โ€” contrast-stretched, because at playback size three pixels of drone against bright sky are hard to see โ€” is the target itself. The map shows the buildings, both flight paths, and a red cross on the wall the intruder was aiming at. It never got there: intercepted at 7.0 s with 3.3 s to spare, passing 0.13 m from it.
0.809
AP, ARD-MAV official split
15 held-out videos, 3 seeds, IoU 0.5 — the benchmark’s own protocol. A competitor retrained from its own code scores 0.834 under the same evaluator.
real video
0.159 โ†’ 0.895
AP, one frame vs three moments
Same network and settings, one real video from a near-static camera. Only the temporal arm also trained on pasted instances — a confound the README names.
real video
24 / 24
intruders intercepted with a perfect sensor
0 buildings hit, across 24 arrival bearings โ€” one every 15° of the compass. This run was flown on detector: oracle, the simulator's own bounding box with zero latency: it measures the guidance, not the seeker. Flying the same mission on the seeker's own detections gives 0 / 3. The closed-loop number with a real detector is 54 / 62.
Isaac Sim ยท oracle sensor
0.080 m
mean closest approach
About a sixth of the airframe's own 0.47 m rotor span. It does not pass near the target; it hits it.
Isaac Sim

01The problem

A drone crossing a field at 4–11 pixels is in the frame, but in one frame it is hard to tell from everything else. The controlled single-frame detector does put a box on it in 57 % of labelled frames when all its detections are kept — about 96 % of those in scored frames are false — and in the other 43 % it finds nothing at all. At its best-F1 threshold, chosen on the same video, it finds 20 %. It fails twice: it misses the drone outright in many frames, and where it does find it, it ranks it below its clutter.

Left: a single video frame in which the drone is hard to pick out. Right: three grayscale moments stacked as the three colour channels, in which the drone appears as three coloured blips.
Same spot, same moment. Left: find the drone. It is there, but hard to pick out; the controlled single-frame detector scores AP 0.159 on 10_06. Right: the detector's actual input. Stabilise the video, then stack three grayscale moments as the colour channels of one image, oldest in blue and newest in red: everything the stabiliser aligns cancels to grey, and anything that moved leaves coloured blips โ€” one per moment in time. Yellow = where the drone was 12 frames ago, magenta = 6 ago, cyan (circled) = now. The trail even shows its direction of flight.
Same network, same settings, same video: single-frame input scores AP 0.159, the temporal stack scores 0.895. Only the temporal arm also trained on pasted instances, so the gap is the representation together with that augmentation.

It also formalises exactly how the human labeller found the drone in the first place: flip between frames and watch what moves. Nearly every design decision downstream follows from this โ€” including the ones about what cannot work.

02The whole method, in one picture

Two halves of one problem, sharing a coordinate and geometry vocabulary. The first half turns pixels into a bearing. The second turns a bearing into a collision. They meet at one small dataclass, and that boundary is what makes the accuracy attributable.

End-to-end system diagram: four lanes covering detection on real video, the four-camera seeker, proportional-navigation guidance, and how each number was measured.
The full pipeline. Click for the vector version. Green chips mark what was measured on real video; amber chips mark what was measured in the simulator.
1 · See

Stabilise, then stack three moments

Global camera motion is removed by phase correlation, three grayscale moments (t−12, t−6, t) become the blue, green and red channels of one image, and a YOLO with a P2 head and a tiny-object loss reads that. Labels are deliberately inflated to a fixed 24 px, because a 1 px shift on a 6 px box swings IoU wildly.

2 · Decide

A track, not a detection

Per-frame detection tops out around AP 0.9 โ€” the last mile is temporal. A Kalman tracker links detections across frames and coasts through the ones the detector missed; a track-level classifier then aggregates the per-frame scores, and a track is announced as a drone at its 8th verifier-confirmed detection, about a quarter of a second after birth.

3 · Seek

Four cameras, and one drone among fifty contacts

On an interceptor, pointing was part of the mission: a target outside a 76° cone did not exist. Four 96° cameras 90° apart remove the mechanism rather than tune it. A stationary background model then finds a 3 px contact at 140 m โ€” along with ~50 pieces of clutter a frame, which is the actual hard part.

4 · Hit

Only bearing steers

A pixel is a ray, so bearing is essentially exact; monocular range is f·S/span and its error grows with range². So range only schedules speed and triggers the terminal commit, and steering is proportional navigation โ€” which needs nothing but the rotation rate of the line of sight, and drives it to zero.

A line of sight that does not rotate while the range shrinks is a collision course โ€” whatever the target does, and whatever the range actually is.

03Results

Every number below has an artifact behind it in the repository. The two halves are scored apart because they fail for different reasons and are fixed by different work.

Real video Detection

Both shipped models are scored on 10_06.mp4 โ€” never trained on, but six track-classifier constants were tuned against it, so it is a development video rather than an unseen one. Matching is by centre distance (τ = 12 px), because IoU is meaningless on a 4-pixel box.

modelwhat it is07_05 val (hardest) 10_06 (development)fps
PC-MAX3-stream fusion + tracker + track classifier 0.995 / 0.9951.000 / 1.0004
EDGE-RTone YOLOv8-nano on the temporal stack, TensorRT FP16 0.995 / 0.9951.000 / 1.00058.9

These are the causal numbers โ€” frame by frame, no look-ahead, which is the only mode an aircraft can run. On the full training video PC-MAX scores 0.998 / 0.998 causally. A surveillance mode allowed to interpolate across gaps using later frames reaches 1.000 across the board for both models, and it is reported next to the causal row in round 3 rather than in the headline. Every labelled frame is hit within 12 px, and the score is 1.000 because AP is score-weighted per frame — at the track, PC-MAX raises four sustained false drone tracks on 10_06 (README §7), so this is not a zero-false-alarm result. PC-MAX misses at most 1 labelled frame in 885. PC-MAX fps is from an RTX 5070 laptop; EDGE-RT was re-measured on an RTX 4090 with a rebuilt engine (the earlier 74 fps was not reproduced). The fps column is not from the passes behind the accuracy columns: EDGE-RT's 58.9 is the detector alone (TensorRT FP16, 1280 px), in a pass that scored AP 0.876 on 10_06 (README §8), not the tracked pipeline whose 1.000 / 1.000 it sits beside. The honest scope of that 1.000: it is one location, one drone, two videos. The generalisation work is separate and scored separately โ€” see below.

Against a strong conventional baseline

A YOLO26n trained on a real multi-scene drone dataset (imgsz 1760, 300 epochs), on the same video, 10_06:

Side-by-side: the baseline single-frame detector finds nothing over ground clutter, while the temporal pipeline holds a continuous track.
Baseline YOLO26n (left) against this pipeline (right), same video, same moments.
flight coveragewhere it works
Baseline YOLO26n (single frame)12.5% only the final second, drone against open sky: 39 of its 40 detections fall there, and it finds the drone in 37 frames at precision 0.925 (3 of the 40 are false; top score 0.861)
This pipelinecontinuous track the whole flight, including 300 frames of ground clutter in which the baseline emits a single detection, at conf 0.022
That gap motivates the method; it is not the ablation. This baseline differs from the pipeline in architecture, training corpus and resolution, so the controlled comparison is the one above. What it does show: this model handles sky silhouettes, and below the treeline it emits a single detection, at conf 0.022. That is a property of this model, not of single frames: the controlled single-frame arm above puts a box on the drone in 57 % of labelled frames when all of its detections are kept.

Generalising: one model, many datasets, a moving camera

Two near-perfect models on one location leave an obvious question open. Public tiny-drone datasets (ARD-MAV and NPS-Drones โ€” air-to-air, moving cameras) were merged with the project's own footage into one combined training set, and a round-7 model learns RGB + motion fusion (a 4-channel [R,G,B,ego-motion] input) with an NWD tiny-object loss. Round 7 scored it on one held-out clip per dataset, by centre distance (τ = 12 px) — not on the benchmarks' test splits.

the one fusion model, round 7ARD-MAV (phantom16)NPS (Clip_19)black drone (10_06)
per-frame detection APvoid0.8010.685
tracked coveragevoid0.9900.875

The ARD-MAV cells are void. They read 0.994 and 0.971, but combined_splits() ignored ARD-MAV's published 15-video test list and re-split by position, so rounds 5–7 trained on most of the official test set. This model was not re-measured on the official split; the 0.809 quoted elsewhere on this page is a different model — the per-dataset temporal arm, trained and scored on that split (round 8). The NPS and black-drone cells are unaffected, and each is one clip.

Combined training takes the held-out NPS clip to AP 0.80 and the low-contrast black drone from 0.00 → tracked. Honest limit: on that black drone the earlier regime-adaptive pipeline still tracks marginally better (coverage 1.000 vs 0.875) using classical motion, and fusion's motion channel produces more false tracks on moving cameras. So there is no single winner โ€” the regime pipeline stays the pick for the black drone, and fusion is the pick for one-model generalisation, on the evidence of one clip per dataset.

Against the published state of the art

Every published leader on these benchmarks is a specialist โ€” one dataset, one set of weights, scored at home โ€” and off home turf they collapse: TransVisDrone holds NPS-Drones at 0.95 and drops to 0.15 on ARD100. This project's own round-4 specialist did exactly the same (ARD-MAV 0.76 → NPS 0.15 → this repo's drone 0.00) โ€” measuring that collapse is what forced the combined training. The 2025 anti-UAV survey (arXiv 2504.11967) lists no unified multi-dataset model; that is the niche this generalist was built for, on the one-clip evidence above.

methodtrained onat homeoff its home dataset
Dogfight (CVPR '21)NPS 0.890.50 on ARD100 ยท ~1 fps
TransVisDrone (ICRA '23)NPS 0.950.15 on ARD100
GLAD (T-ITS '24)ARD-MAV 0.80—
YOLOMG (2025)per dataset 0.95 NPS ยท 0.85 ARD100separate weights per set
this projectper dataset0.809 ARD-MAV ยท 0.487 NPS separate weights per set, like YOLOMG — which, retrained by us on the same splits and scored by the same evaluator, gets 0.834 / 0.527

Published numbers are AP@0.5 IoU on each paper's own split. Ours on ARD-MAV and NPS are AP@0.5 IoU too, on held-out splits — ARD-MAV on its official list, NPS video-disjoint rather than the published split, and which videos are held out is the largest single term in the NPS discrepancy. Read this as a class comparison, not a leaderboard entry. Against that retrained competitor YOLOMG leads on point estimates on both benchmarks; under a Holm-corrected paired test the overall difference is significant on no seed. Speed: 58.9 fps at 1280 px for the edge model on an RTX 4090 (TensorRT FP16, in the pass that scored its AP of 0.876), against a published range of ~1 fps (Dogfight) to 147 (GLAD).

Against the closest prior art: Temporal-YOLOv8, reimplemented

Temporal-YOLOv8 (van Leeuwen et al., TNO, Sensors 2024) stacks three grayscale frames into a stock YOLOv8 โ€” the same basic move as this project, published two years earlier. It ships no code, no weights, and every number on a private dataset, so it is reimplemented from the paper โ€” 70 epochs, Adam, a non-causal t−15/t/t+15 window with no stabilisation โ€” trained on this repository's splits and scored by this repository's evaluator, the way YOLOMG is above. Full comparison, including what is and is not reproduced from the paper →

benchmarkours (100 ep)Temporal-YOLOv8 (70 ep)paired verdict
ARD-MAV, official split0.8090.807 no difference, any seed
NPS-Drones, video-disjoint0.4870.531 no difference, any seed
our 8 px task (fine-tuned)0.8400.809 ours leads 1 of 3 seeds; ties the other 2

No difference detected on either public benchmark, under the same two-test, Holm-corrected rule as the YOLOMG comparison above โ€” on ARD-MAV the two arms are numerically close as well as statistically indistinguishable; on NPS, Temporal-YOLOv8's point estimate leads and is markedly more stable across seeds, but that lead does not clear the significance bar. The one exception is the same one YOLOMG has: above 25 px on ARD-MAV, Temporal-YOLOv8 leads significantly on 2 of 3 seeds after Holm. Its window reads half a second of the future, which a deployed interceptor cannot have; that it still does not separate from this project's causal arm on either benchmark, and loses the one significant seed on the project's own task, is the finding โ€” not a win being talked down.

Isaac Sim Interception

The mission: hold over a city, an intruder arrives from any of 360°, commits to a surveyed building and does not break off when it is seen. There are two ways to lose and they are scored separately โ€” the interceptor can miss the drone, or it can arrive after the drone has already flown into the wall.

closure โ€” handed the true bounding box and nothing else
intruders intercepted24 / 24
buildings hit0
mean true closest approach0.080 m
median / best / worst0.073 / 0.001 / 0.419 m
inside 0.25 m / 0.5 m96% / 100%
vertical aim bias−0.12 cm (t = −0.27)
lateral aim bias+2.69 cm (t = +2.59)
median time to intercept6.68 s
median margin before the strike4.29 s (worst 2.82 s)

"Handed the true bounding box" means the box a perfect detector would draw โ€” no range, no position, no velocity. Everything downstream is the real thing: the same bearing extraction, the same monocular range from pixel span, the same proportional navigation, the same airframe limits, the same 20 Hz loop.

Two fixes were worth the trip and both were found by testing the aim bias for significance rather than by looking at the misses. A stubborn 3.5 cm low bias (t = −8.76) turned out to be the chaser's vertical speed limit being derived from the evader's climb rate. And a pinhole camera stretches an object 43° off boresight by sec² โ€” 15 px where the same drone measures 8 px on the nose โ€” which is a range error of tens of percent at the edge of a 96° field, on exactly the geometry a ring exists to exploit.

Every one of the 24 engagements, by arrival bearing

Closest approach against the two distances that matter: the airframe's own 0.47 m rotor span, and the 1.0 m radius that counts as a hit. Twenty-three of twenty-four are inside a quarter of a metre.

closest approach, one engagement
Show the numbers

The earlier mission โ€” one camera, chase and intercept

Before the ring and the city, the same guidance law was flown as a straight pursuit: one forward-facing camera, an intruder crossing or fleeing, and nothing to defend. 54 of 62 engagements intercepted โ€” 87.1%, 95% CI [76.6%, 93.3%] (Wilson), across two environments, twelve arrival directions and ten evasion policies. Mean true miss distance 0.239 m; 93% inside half a metre.

Only one thing predicts failure

Detection rate for each of the 62 engagements, split by outcome. It survives Holm correction across six tested factors (p = 0.0001). Environment does not (p = 0.74), nor arrival direction, start range, or how hard the target manoeuvred.

intercepted (54) missed (8) group mean

The eight failures are 3 that never acquired and 5 that acquired and did not close โ€” and since the aim bias is statistically centred on both axes (vertical −1.8 cm, t = −1.27; lateral −0.7 cm, t = −0.19), none of them is an aiming error. Against a perfect sensor, in the arithmetic sandbox, the same law is 120/120 on the stress matrix and 42/42 on the full suite (python -m pursuit.sandbox). Those are not the 62 flown scenarios, so they point the failures at perception rather than settle it.

04Seeing three pixels

At 140 m an intruder is 3.1 px across โ€” 2.9 px by 150 m โ€” and no appearance model will find it. It has to be found anyway, because detection range converts directly into how much ground one interceptor can defend.

With the interceptor 1.5× faster and both on the same line, it wins only if the intruder is acquired beyond d × (1 + vi/vc) โ€” so every metre of detection range buys 0.6 m of defended radius. Nothing else in the system trades that steeply.

What finds it is the other half of this repository. An interceptor on overwatch has four stationary cameras looking at a city that is not going anywhere โ€” so a per-pixel background model sees the target's whole contrast instead of the sliver of it that changes between two frames.

Detection range: frame differencing against a background model

Fraction of frames with a detection inside 1.2°, measured live on the rendered town with an intruder running in at 12 m/s. Half the frames is the threshold that matters, because below it a track cannot be held.

per-pixel background model ego-compensated frame differencing
Show the numbers

Reliable to 140 m against 100 m, which is a defended radius of 70 m against 46 m โ€” a 52% larger bubble from one change of detector. Both run at full resolution with no morphological opening, and that detail cost a day: an opening is an erosion followed by a dilation, so a 3×3 kernel deletes anything smaller than 3×3 โ€” and at 100 m the target is smaller than 3×3. The step that exists to remove speckle was removing the drone.

Finding it was never the hard part

That same empty sky returns about 50 motion contacts a frame, so the drone is one in fifty โ€” and it is neither the brightest nor the most persistent. It is the least of both, because it is 3 px across and detected on half the frames while a renderer artefact fires on nine tenths of them. A single-target tracker seeded on the first corroborated pair therefore locks onto clutter and, being single-target, never reconsiders. Measured live: the seeker held a confident track on 86–97% of frames and was on the drone for 0% of them, in every engagement.

No single-frame gate fixes that, and that is measured rather than assumed. Every detection on live Rivermark was scored โ€” 185 on the drone, 693 on clutter โ€” against four per-blob statistics, and asked the only question a gate cares about: at a threshold that keeps the true detections, how much clutter survives?

gate statisticclutter surviving at a 95% true-keep
peak motion in the box100%
mean motion in the box100%
compactness100%
local motion contrast โ€” the best of the four85%

You have to throw away 30% of the real detections before clutter falls to half. What separates them is not a property of one frame โ€” it is behaviour over seconds: an artefact sits still and a drone flies, and from an interceptor holding station that is unambiguous, because a fixed object's bearing is exactly constant. So every contact gets a cheap running record, and the Kalman filter is handed only one whose recent bearings fit a constant angular rate with a real rate and a small residual.

Against the real sensor on the rendered city, that is still not enough. The seeker holds a track on 41% of frames and is on something other than the drone for 60% of them (city_pipe, 3 engagements). The mechanism is right and the discrimination problem is not solved โ€” see what is not finished.

Where the time goes โ€” and it is no longer the network

Two configurations were timed, and the honest first sentence is that they are not a controlled comparison: they differ in the model, in the input, and in how many cameras the network sees.

stageone nose camerafour-camera ring
appearance model130.7 ms โ€” 25 M-param fusion model, whole 1440×840 frame16.2 ms โ€” 2.9 M-param nano, 640 px crop, 1–2 cameras
motion detector—208.0 ms (4 cameras, threaded on CPU)
tracker + guidance0.16 ms7.4 ms
perception total130.9 ms → 7.6 FPS231.5 ms → 4.4 FPS

Nose column: mean over the 62 recorded engagements, detector=fusion (METRICS.md). Ring column: mean over the 3 recorded live-Rivermark engagements, detector=yolo (city_pipe/results.json), spread 198–259 ms.

So the appearance stage did get about eight times cheaper โ€” but a network with a ninth of the parameters is doing most of that work, not the crop, and separating the two would take the same model run both ways, which is a measurement this repository does not have. What the numbers do settle is where the time goes now: the ring loop is 90% classical motion detection, four 2048×704 images a tick in a city that returns ~50 contacts a frame. The network is no longer the thing to optimise, and neither configuration meets the 50 ms budget.

The crop's other benefit is not about speed and is measured separately: it runs at native scale, where a full-frame pass has to fit 2048 px into the network's input and shrinks a 9 px drone to 8 โ€” the detector was always the thing that ran out of pixels first.

05What it looks like

Sixteen recorded engagements and four full-length detection runs. See the whole gallery →

Dead astern โ€” 180°
The exact bearing one nose camera cannot see. It is in the aft feed from the first frame; acquisition takes 0.20 s.
closest approach 0.036 m ยท 5.05 s to spare
Why the ring exists
The same arrival with one forward camera: 21.0 s to acquire and 30.5 s to intercept, nearly all of it spent turning to look.
the ring does this in 0.2 s
A failure, shown in full
Detection rate 0.00. The target crosses low against the town's roofline and is never acquired, so there is nothing to steer at. Every one of the eight failures looks like this.
perception, not guidance
It breaks; the law does not care
A hard break turn at close range โ€” the manoeuvre that defeats pure pursuit. Proportional navigation sees only a new bearing rate to null.
closest approach 0.198 m

06What is not finished

Stated here rather than buried, because a result you cannot see the edge of is not a result.

The full pipeline against the rendered city is not solved. Closure is finished (24/24 with a perfect sensor). Finding a 3 px drone among ~50 motion contacts a frame in that scene is not. Six bugs were found and fixed getting to that sentence, and five further changes were tried and measured โ€” uncapping the contact list, putting the intruder against sky rather than terrain, giving the interceptor 120 quiet frames to learn the city first, expiring a foreground freeze, and the candidate pool itself. Each is a real improvement and all are kept; together they take the drone from absent to detected on 2–4% of frames. The classical motion detector cannot separate a 3 px drone from this renderer's noise at these ranges โ€” that is a statement about the detector, not about the guidance law, the scenario or the ring. The fix is the one this repository's other half already demonstrates: train on the domain that is failing, rather than tune thresholds against it.

07Run it yourself

Detection runs on any video with no simulator. The mission needs Isaac Sim, but the same closed loop also runs headless with arithmetic instead of a renderer — 120 scenarios in 1.2 s. 540 unit tests cover the geometry, guidance, dynamics and ring.

# the two shipped detection models, on any video
python final/run_final.py --video V.mp4 --profile pc-max  --out out_pc    # most accurate
python final/run_final.py --video V.mp4 --profile edge-rt --out out_edge  # real-time

# the generalist multi-dataset pipeline
python tools/run_max.py --profile v1 --weights work/runs/combined-m-p2-640/weights/best.pt \
    --video V.mp4 --out out_max

# the whole mission, headless: 24 arrival bearings in seconds, no renderer
python -m pursuit.sandbox --suite city --ring

# the guidance law alone, against a perfect sensor
python -m pursuit.sandbox --suite stress      # 120/120 in 1.2 s

# the closed loop against Isaac Sim, four-camera ring
docker exec -d isaac-sim bash -c "cd /tmp/dev/dronedet && /isaac-sim/python.sh \
    simulators/pegasus/scripts/pursuit_server.py --scene rivermark --cameras ring"
python -m pursuit.tools.ring_probe --range 40         # 0 blind bearings of 120
python -m pursuit.tools.record_city --detector oracle # 24/24, 0 struck
Read next

How every algorithm works

docs/guides/methods.md โ€” every method, the models inside it, and its measured performance.

Read next

The build narrative

docs/reports/ โ€” eight rounds plus five focused investigations, including every negative result, in the order they were found.

Read next

The interceptor in depth

pursuit/README.md โ€” the bug-and-fix table, and the measured sensor-degradation budget.