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.
Project site · Video gallery · The method in one diagram · Docs · Licence
🟢 Detection is measured on real hand-labelled video, on a clip never trained on. 🟡 Interception is measured closed-loop in NVIDIA Isaac Sim. There is no flight test here. Every table below says which.
One engagement, start to finish (2×). An intruder arrives on bearing 105° at 12 m/s, 170 m out
and 2.7 pixels across, committed to a building. Four camera feeds, the owning one outlined;
green is truth, yellow is what the seeker believes, the inset is contrast-stretched because three
pixels of drone against bright sky are otherwise invisible. Intercepted at 7.0 s with
3.3 s to spare, passing 0.13 m from it.
| result | measured on | |
|---|---|---|
| Detection, unseen test video | AP / F1 = 1.000, zero false positives | 🟢 real video, causal |
| The one change that mattered | mAP50 0.06 → 0.83 | 🟢 real video, identical recipe |
| Speed | 4 fps (PC-MAX) · 74 fps (EDGE-RT, TensorRT FP16) | 🟢 RTX 5070 |
| Generalist model, one for all datasets | ARD-MAV AP 0.994 · NPS AP 0.801 | 🟢 public data, moving cameras |
| Versus published SOTA | specialist-class on every set, one set of weights — no published method does both | 🟢 held-out splits |
| City defence | 24 / 24 intercepted, 0 buildings hit | 🟡 Isaac Sim, 24 bearings |
| How close | mean closest approach 0.080 m (airframe span 0.47 m) | 🟡 Isaac Sim |
| One-camera pursuit campaign | 54 / 62 — 87.1 %, CI [76.6, 93.3] | 🟡 Isaac Sim, 62 engagements |
| Seeing 3 pixels | reliable to 140 m, target ~3 px | 🟡 Isaac Sim, live town |
| Tests | 540 unit tests, 22 s | python -m pytest |
Full scorecards: city · pursuit campaign · statistics
A drone at 4 px is invisible in one frame, to a detector and to a human. Stabilise the video and stack three grayscale moments (t−12, t−6, t) as R/G/B: the static world cancels to grey, and anything that moved leaves a coloured trail.
Left: find the drone. You can't — nor can any single-frame detector, at any confidence.
Right: the detector's actual input. Yellow = 12 frames ago, magenta = 6 ago,
cyan (circled) = now. The trail even shows its direction of flight.
Same network, same recipe: single-frame input scores mAP50 0.06, the temporal stack scores 0.83. The representation is the breakthrough, not the network.
The two shipped models, scored on 10_06.mp4 — never trained on, never used to pick a model.
Matching is by centre distance (τ = 12 px); IoU is meaningless on a 4 px box. These are the causal
numbers, frame by frame with no look-ahead:
| model | what it is | 07_05 val (hardest) | 10_06 test (unseen) | fps |
|---|---|---|---|---|
| PC-MAX | 3 detection streams + tracker + track classifier | 0.995 | 1.000 | 4 |
| EDGE-RT | one YOLOv8-nano on the stack, TensorRT FP16 | 0.995 | 1.000 | 74 |
| on the same unseen video | flight coverage | where it works |
|---|---|---|
| Baseline YOLO26n, single frame | 12.5 % | only the last second, drone against open sky |
| This pipeline | continuous track | the whole flight, including 300 frames of ground clutter |
That gap is the thesis: single-frame appearance handles sky silhouettes; everything below the treeline requires motion.
One model for all datasets. Public tiny-drone data (ARD-MAV, NPS-Drones — air-to-air, moving
cameras) merged with our own, and a 4-channel [R,G,B,ego-motion] detector with an NWD tiny-object
loss: ARD-MAV AP 0.994, NPS 0.801, and the low-contrast black drone 0.00 → tracked.
Round 7 →
Every published leader on these benchmarks is a specialist — one dataset, one set of weights, scored at home — and the 2025 anti-UAV survey (arXiv 2504.11967) lists no unified multi-dataset model. Off home turf the specialists collapse, and ours did too, until the training corpus was combined:
| trained on | at home | off its home dataset | |
|---|---|---|---|
| Dogfight (2103.17242) | NPS | 0.89 | 0.50 on ARD100 · ~1 fps |
| TransVisDrone (2210.08423) | NPS | 0.95 | 0.15 on ARD100 |
| GLAD (2312.11008) | ARD-MAV | 0.80 | — |
| YOLOMG (2503.07115) | per dataset | 0.95 NPS · 0.85 ARD100 | separate weights per set |
| our round-4 specialist | ARD-MAV | 0.76 | 0.15 NPS · 0.00 on our drone |
| this generalist (rounds 5–7) | all sets at once | — | 0.84 ARD-MAV · 0.81 NPS · black drone tracked 1.000 — same weights, all held-out |
Published numbers are AP@0.5 IoU on each paper's own split; ours are centre-distance AP on whole-video held-out splits (τ = 12 px — IoU swings wildly on a 6 px box, which is why this repo never scores with it). 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 on several tiny-drone datasets with one set of weights — and this one does it in real time: 74 fps for the shipped edge model (TensorRT FP16, RTX 5070) and 107–122 fps for the generalist edge pipeline on a moving camera, where the published range runs from Dogfight's ~1 fps to GLAD's 147.
One forward camera made pointing part of the mission: a target outside its 76° cone did not exist, and a full sweep takes ten seconds. The interceptor carries four 96° cameras 90° apart — 384° of 360, 6° of overlap at every seam, same 16.1 px/deg.
Arriving 145° off the nose, in the cone a forward camera cannot see at all. It is in the
aft feed from the first frame. Watch the outline move aft → right → fwd — two
seam crossings in two seconds, no break in the track. Closest approach 3.1 cm.
It holds station, and that is the point: four stationary cameras see the target's whole contrast, not the sliver that changes between two frames.
Detection was never the hard part — discrimination was. That sky returns ~50 motion contacts a frame, and the drone is neither the brightest nor the most persistent one:
| gate statistic | clutter surviving at a 95 % true-keep |
|---|---|
| peak motion · mean motion · compactness | 100 % |
| local motion contrast — the best of four | 85 % |
No single-frame gate separates them (motion_gate.json). Physics does: an artefact sits still and a drone flies, so a fixed object seen from a fixed observer has a bearing rate of exactly zero. Every contact gets a running record, and the tracker is handed only one that has been watched flying.
Aiming at where the target is curves in behind it and never converges against a turn. So the closure law is chosen for what a camera can and cannot measure:
| quantity | quality | role |
|---|---|---|
| bearing | essentially exact — a pixel is a ray | steering |
| range | poor: fx·S/span, error grows with range² |
speed schedule and terminal trigger only |
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.
Against a perfect sensor the law is 120/120 on the stress matrix and 31/31 on the mission suite, which is what makes the attribution above possible: every remaining failure is perception.
Per-model architecture figures: PC-MAX · EDGE-RT
The full gallery — 21 clips with the facts from each run →
| clip | what it shows |
|---|---|
| Baseline | PC-MAX | EDGE-RT | three systems on the same unseen video, side by side |
| PC-MAX · EDGE-RT · baseline | each one full length |
| city_defence.mp4 | the headline engagement, four camera feeds and a map |
| city_astern.mp4 | an intruder arriving 145° off the nose |
docs/media/pursuit/city/ |
all ten recorded city engagements, around the compass |
docs/media/pursuit/chase/ |
six one-camera pursuits — including a failure shown in full |
git clone https://github.com/NadavCherry/SpeckLock.git && cd SpeckLock
python -m venv .venv && .venv/bin/pip install -r requirements.txtTorch needs the cu128 index on Blackwell GPUs — see requirements.txt.
Both shipped models are in the repo; nothing to download.
python final/run_final.py --video V.mp4 --profile pc-max --out out_pc # most accurate, ~4 fps
python final/run_final.py --video V.mp4 --profile edge-rt --out out_edge # real-time, ~74 fps
# the generalist, for other scenes and a moving camera
python tools/run_max.py --profile fusion \
--weights work/runs/combined-fusion-m-p2-2/weights/best.pt --video V.mp4 --out out_max--out gets annotated.mp4, tracks_drone.json, alarms.txt and per-frame dets.json.
Full guide: docs/guides/run-inference.md.
No simulator needed — the same closed loop, with arithmetic instead of a renderer:
python -m pursuit.sandbox --suite city --ring # 24/24, the whole city mission
python -m pursuit.sandbox --suite stress # 120/120 in 1.2 s
python -m pytest # 540 tests in ~22 sWith pixels, against Isaac Sim — which is not part of this repository, see
simulators/pegasus/README.md for what you must supply:
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
python -m pursuit.tools.city_report --search work/pursuit/citypython tools/make_arch_figure_system.py # the method diagram
python tools/make_result_charts.py # the three charts above, from work/pursuit/*.json
python tools/publish_showcase.py # re-encode the showcase clips
python tools/make_gallery.py # rebuild the gallery page
python tools/check_docs.py # every documented link resolves and is trackedWhat ships and what does not. The two deliverable models (
final/), the round-1..3 weights (work/models/,realtime/work/models/), the baseline (baseline/) and the round-7 fusion generalist (work/runs/combined-fusion-*) are in git. The round-4..6 combined weights and the simulator detectors are not — they are regenerable, and docs/guides/retrain.md is the recipe. TensorRT.enginefiles are architecture-specific and are never committed; build them on the target device. Without one,--profile edge-rtfalls back to the.pt— correct, just slow.
dronedet/ the core detection library — stabilise, motion, methods, track, evaluate
realtime/ the edge (Jetson-class) re-architecture, six pipelines compared
final/ the two shipped models + one-command runner
pursuit/ the interceptor — ring.py, city.py, perception.py, guidance.py, 540 tests
simulators/pegasus/ the Isaac Sim rig and the wire protocol both processes share
tools/ dataset builders, training, labelling UI, figures, reproduction scripts
docs/ reports, guides, media, and the project site
work/ artifacts: ground truth, weights, detections, tracks, pursuit scorecards
data/videos/ the two source videos (07_05 = train, 10_06 = unseen test)
| doc | what's in it |
|---|---|
| the project site | the illustrated version — diagrams, interactive charts, 21 videos |
| docs/guides/methods.md | every algorithm, its models, and its measured performance |
| docs/guides/run-inference.md · retrain.md | run it on a new video · relabel, rebuild datasets, retrain |
| pursuit/README.md | the interceptor in depth, and a 26-row table of every bug that shaped it |
| final/README.md · realtime/README.md | the two deliverables · the edge pipeline |
| docs/reports/ | the build story, seven rounds, including every negative result |
- Finding a 3 px drone in the rendered city is not solved. Closure is (24/24 with a perfect sensor); the full pipeline has 3 recorded engagements, all lost. Five threshold-level changes were tried and measured; none closed it. The fix is training on the failing domain, not more tuning.
- The perception loop is not real time. The ring runs at 4.4 FPS against a 50 ms budget, and it is now the classical motion stage (208 ms over four 2048×704 images), not the network.
- Latency must be calibrated on hardware. At 3 frames of latency, declaring it is worth 32/42 intercepts against 18/42 ignored.
- Drone-vs-bird is the frontier for detection. At a few pixels only appearance can separate them, and appearance is what is weakest at that scale.
- No flight test. Interception is Isaac Sim throughout.
Use the Cite this repository button in the sidebar, or:
@software{Cherry_See_the_drone_2026,
author = {Cherry, Nadav},
title = {{See the drone, then hit it (SpeckLock)}},
license = {AGPL-3.0-only},
year = {2026},
url = {https://github.com/NadavCherry/SpeckLock}
}If you quote a number from here, cite the artifact it came from as well — every one is linked from the table it appears in.
AGPL-3.0 — see LICENSE. That is what the dependency requires: every detector here
builds on Ultralytics YOLO, which is AGPL-3.0.
Third-party components, dataset terms and the exact scope of the results are in
NOTICE.md.




