N. BLATTNER
Personal Project · Software · Computer Vision

Visual Sky Radar

Distributed multi-camera system for real-time aerial tracking

2025C++PythonOpenCVComputer VisionCNN
The Simulation Environment

Two cameras, one shared patch of sky

Every detection and triangulation is validated in a simulated 3D environment before it runs on real hardware: ground cameras with known positions and orientations watch simulated aircraft cross their overlapping fields of view. Drag the model to look around.

loading 3D scene…
interactive — drag to rotate

Recreation of the simulation environment: two to three ground cameras with overlapping fields of view over satellite terrain, tracking aircraft crossing the shared airspace.

01 — The Idea

Can two cheap phone cameras triangulate an aircraft?

Every plane overhead already broadcasts its position over ADS-B — services like OpenSky make that data public. But I wanted to know whether I could independently see and locate aircraft myself, from the ground, using nothing but two ordinary phone cameras and some geometry.

The goal: point two fixed, calibrated cameras at the sky from two different locations, detect the same aircraft (or its contrail) in both feeds, and triangulate a 3D position — then cross-check it against the real ADS-B track for that flight.

ConceptADS-BOpenSky
Raw sky photo showing a jet contrail
Raw frame from a fixed sky camera — the kind of input the whole pipeline starts from.
02 — Multi-Camera Concept

Two ground stations, one triangulated track

The system runs two independent camera stations roughly 4.3 km apart. Each one watches a wide slice of sky, detects candidate targets locally, and reports bearing (azimuth/elevation) toward anything it finds.

Where two bearings from two known locations cross, that intersection is a 3D position estimate — classic optical triangulation, just aimed upward instead of across a lab bench.

TriangulationGeometry
triangulated positionbaseline ≈ 4.3 kmSkyCam-1 (Xiaomi)FOV 34.0°SkyCam-2 (Vivo)FOV 35.6°

Recreated schematic from camera_specs.json — real FOV and baseline geometry, redrawn (site does not include on-site photos yet).

03 — Camera Alignment

Turning a phone camera into a measuring instrument

A phone's stated field of view is a marketing number, not a calibration certificate — I needed the real horizontal and vertical FOV of each sensor, in its actual mounted orientation, to trust any angle I derived from a pixel position.

I measured it the direct way: camera fixed at a known distance from a flat wall marked at even intervals, sighting the extreme visible marks and solving the subtended angle from simple trigonometry. That gave real fov_horizontal_deg / fov_vertical_deg values per camera — 34.0°×57.0° for one unit, 35.6°×58.2° for the other — which now live in the app's camera_specs.json as the source of truth for every bearing calculation downstream.

Each camera also needed a yaw/tilt orientation ('cone' in the code) so its pixel grid maps onto real compass bearings and elevation angles — set once during setup and stored in cone_orientations.json.

CalibrationTrigonometrycamera_specs.json
wall, marks at known spacing scamerameasured distance Dθ = atan((n·s / 2) / D)

Recreated schematic of the wall-calibration method — sighting evenly spaced marks at a measured distance to solve for each camera's true horizontal/vertical field of view.

04 — Detection Pipeline

Finding a thin white line in a noisy blue sky

A contrail is a faint, thin, high-contrast streak against a sky that has its own gradients, sensor noise, and JPEG artefacts. A naive brightness threshold picks up compression noise as readily as an actual contrail — so detection became a staged pipeline, each stage narrowing down what's left.

Bright-region extraction → sky masking → edge detection → frame-to-frame motion → morphological top-hat filtering to isolate thin linear structures → dark-sky suppression → line-bank scanning to confirm a real elongated streak rather than a blob of noise.

Every stage is independently visualised during development, which made it possible to see exactly where a false positive was entering the pipeline instead of just staring at a final yes/no.

OpenCVImage ProcessingMorphology
Bright-region extraction
1 · Bright
Sky masking
2 · Sky mask
Edge detection
3 · Edges
Motion detection
4 · Motion
Morphological top-hat filter
5 · Top-hat
Final contrail mask
7 · Contrail mask
05 — Filter Optimisation

Sweeping parameters until the noise disappears

Each stage has a free parameter or two — the Gaussian brightness sigma, the morphological kernel size, the line-opening length — and none of them have an obvious 'correct' value on paper. I swept them systematically against real recorded footage and compared outputs side by side.

Below: the same brightness-extraction stage at four different sigma values. Too small and the contrail fragments into noise; too large and it smears into the surrounding sky gradient. The working value sits in a narrow band in between — the kind of thing you only find by testing, not by deriving.

Parameter SweepGaussian Filtering
Sigma 8
σ = 8 — too fragmented
Sigma 15
σ = 15
Sigma 25
σ = 25 — usable
Sigma 40
σ = 40 — over-smoothed
06 — Line Confirmation

A bank of line detectors, not a single threshold

The last confirmation stage runs a bank of directional line-opening filters at different lengths and orientations, then combines them — a single filter length either missed short contrail segments or let short noise streaks through, so I settled on running several in parallel and taking their union.

Line DetectionFilter Banks
Line opening length 60
opening length 60
Line opening length 80
opening length 80
Combined multi-line bank result
combined multi-line bank
07 — Synthetic Test Feeds

Testing without waiting for a plane to fly over

Real contrail sightings are intermittent and weather-dependent, which is a slow feedback loop for iterating on a detector. I built synthetic feeds with injected aircraft and contrail shapes to test the pipeline on demand and validate against a known ground truth before ever pointing it at the sky.

Synthetic DataTesting
Synthetic test feed frame
08 — The Application

From research script to a live Qt6 application

The working pipeline moved from a Python prototype into a native C++17 / Qt6 application with FFmpeg-based RTSP ingestion, so it can run continuously against live camera feeds rather than pre-recorded clips. The Python side still hosts the terrain-cache rendering, OpenSky polling, and the analysis tooling used to validate results.

The two-camera setup, its FOV calibration, and its cone orientation all live in versioned JSON config files — camera_specs.json and cone_orientations.json — so re-pointing or re-calibrating a station doesn't require touching code, just re-running the wall calibration and updating a couple of numbers.

C++17Qt6FFmpegRTSP
09 — Where It Stands

A working two-station detection & tracking pipeline

The system reliably detects contrails and bright aircraft against a range of sky conditions, calibrates each camera's real field of view from a simple physical measurement, and cross-references detections with live ADS-B traffic. Triangulated 3D position estimates from the two stations are the current frontier — the geometry is in place; the next iteration is tightening detection timing sync between the two feeds to shrink triangulation error.

StatusNext Steps