SpeckLock

See the drone,
then hit it.

Find a drone that occupies 3–14 pixels in 720p video from a moving camera โ€” 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 three pixels of drone against bright sky are otherwise invisible โ€” 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.
1.000
AP and F1, detection
On a real video never trained on and never used to pick a model. Zero false positives.
real video
0.06 โ†’ 0.83
mAP50, one change
Same network, same recipe: single-frame input against a 3-moment temporal stack.
real video
24 / 24
intruders intercepted
0 buildings hit, across 24 arrival bearings โ€” one every 15° of the compass.
Isaac Sim
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–10 pixels is invisible in a single frame โ€” to a detector and to a human. That is not a tuning problem. There is no threshold that finds it, because in one frame there is nothing there to find.

Left: a single video frame in which the drone cannot be seen. Right: three grayscale moments stacked as R/G/B, in which the drone appears as three coloured blips.
Same spot, same moment, unseen video. Left: find the drone. You can't โ€” nor can any single-frame detector, at any confidence. Right: the detector's actual input. Stabilise the video, then stack three grayscale moments as the R/G/B channels of one image: everything static 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.
On identical held-out instances with an identical training recipe: single-frame input scores mAP50 0.06, the temporal stack scores 0.83. The representation is the breakthrough, not the network.

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 R/G/B 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 โ€” a video never trained on and never used for model selection. Matching is by centre distance (τ = 12 px), because IoU is meaningless on a 4-pixel box.

modelwhat it is07_05 val (hardest) 10_06 test (unseen)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.00074

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 with zero false positives; PC-MAX misses at most 1 labelled frame in 885. fps measured on an RTX 5070. 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 unseen video:

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 โ€” and excellent there (conf 0.6–0.84, zero FPs)
This pipelinecontinuous track the whole flight, including 300 frames of ground clutter where the baseline outputs nothing even at conf 0.02
That gap is the thesis: single-frame appearance handles sky silhouettes; everything below the treeline requires motion.

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.

the one fusion modelARD-MAVNPSblack drone (10_06)
per-frame detection AP0.9940.8010.69
tracked coverage0.9710.9900.875

Combined training takes NPS from 0.21 → 0.81 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.

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

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 generalistall sets at once 0.84 ARD-MAV ยท 0.81 NPS ยท black drone tracked 1.000 โ€” same weights, held-out

Published numbers are AP@0.5 IoU on each paper's own split; ours are centre-distance AP (τ = 12 px) on whole-video held-out splits โ€” IoU is unstable on a 6 px box, so read this as a class comparison, not a leaderboard entry. The claim is not that any specialist is beaten at home; it is that no published method holds specialist-class accuracy across several tiny-drone datasets with one set of weights โ€” and this one is real-time with it: 74 fps for the shipped edge model (TensorRT FP16), 107–122 fps for the generalist edge pipeline on a moving camera, against a published range of ~1 fps (Dogfight) to 147 (GLAD).

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 the same law is 120/120 on the stress matrix and 31/31 on the mission suite. Running both on identical scenarios is the point of the split: it settles attribution rather than arguing 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 detector208.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/ โ€” seven rounds, 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.