VixitAi Labs ← Solutions
Capability note · Working prototype · Run on real sensor data

SPARSE

Sparse Photon-domain Asynchronous RSO Sensing Engine

Detecting and tracking resident space objects directly in a neuromorphic camera’s event stream — never reconstructing a frame, and therefore never paying a frame’s latency. The whole pipeline is NumPy and SciPy on two CPU cores. There is no GPU in it, and nothing in it was trained.

0.17 ms
mean end-to-end on real sensor data
57×
faster than real time, 2 CPU cores
1.2 M
events / second
F1 0.063
accuracy on real data — weak, and why
Explore the engine → Generate a scene or feed your own recording and watch the pipeline run — in the browser, on two CPU cores.
01 · The thesis

The sensor has already done the hard part

An event camera on a sidereally-tracked telescope sees a nearly-static star field. The sensor is differential — a pixel only speaks when its log-intensity changes — so after noise rejection, almost every surviving event came from something that moved. The motion segmentation is free, in hardware, before any software runs.

The conventional move is to render those events back into frames and run a convolutional detector over them. That throws the free segmentation away and buys a per-frame compute bill that commodity CPU hardware cannot absorb inside a 40 ms window. It also applies a general object detector to a target that is two to five pixels across — an unresolved point source with no shape and no texture to recognise.

SPARSE stays in the event domain from decode to track. Doing so turns the latency budget from a constraint into a non-issue: the measured end-to-end cost is about one millisecond, which leaves the remaining thirty-nine for whatever an operator wants to do next.

02 · Pipeline

Five stages, all linear in the event count

01Decode Prophesee RAW/DAT, HDF5, AEDAT4, NPZ and CSV into one canonical (t, x, y, p) array. once, at load
02Filter Hot-pixel mask, then a refractory period, then a nearest-neighbour temporal-coherence gate. Cheapest first. 0.45 ms
03FEAST Unsupervised spiking feature layer with adaptive thresholds. Implemented, measured, and off by default — see below. 1.04 ms · disabled
04Cluster O(N) grid clustering, 8-connected, event-count-weighted sub-pixel centroid. One pass, no k, no neighbour graph. 0.22 ms
05Track Constant-velocity Kalman filter with probabilistic data association and m-of-n confirmation. 0.34 ms

Every stage is linear in the number of events. Nothing is quadratic in anything, which is why throughput degrades gracefully as the noise rate climbs rather than falling off a cliff.

Why probabilistic data association, and not nearest neighbour

Inside the association gate, PDA blends candidate measurements by likelihood instead of committing to the closest one. That is what keeps a faint target alive when a star scintillates beside it — nearest-neighbour association hands the track to the star. The filter returns sub-pixel position and angular rate, which is what a space-situational-awareness operator actually wants out of a tracker.

03 · Measured

Latency, with the hardware named

“Real-time capable” is not a claim. These are wall-clock measurements per processed 10 ms window on an AMD EPYC with cores and threads pinned, at 640 × 480 over 245,161 events.

Metric1 core2 cores
Mean end-to-end latency1.32 ms1.01 ms
p952.11 ms1.33 ms
p993.91 ms1.46 ms
Throughput0.93 M ev/s1.21 M ev/s
Windows over a 40 ms budget0 %0 %
Precision / recall / F11.000 / 0.638 / 0.7791.000 / 0.638 / 0.779
Localisation error0.96 px0.96 px

Accuracy is identical on one core and two, because every random number generator is seeded. A single core still meets a 40 ms budget ten times over.

Every number in the table above is from a synthetic scene, not from a sensor. They are produced by the DVS contrast model in our own scene generator, which gives exact sub-pixel ground truth and a clean magnitude ladder — the two things a fixed recording will not give you. They are honest measurements of real code doing real work, and they are not evidence of performance on a real telescope.

The section immediately below is what happened when we ran the same code on a real one.

04 · Real sensor data

What happened when we ran it on a telescope

We ran the pipeline against the Western Sydney EBSSA set — real neuromorphic recordings from a real telescope, openly published. The recording is an ATIS sensor at 304 × 240, 5.8 million events over 198 seconds, twelve human-labelled objects.

Tuned as it was for synthetic scenes, the pipeline scored F1 0.000 and found 0 of 12 objects. Latency looked superb the whole time. The failure was completely silent — which is exactly the failure a synthetic-only benchmark is built to hide.

The diagnosis is the useful part. On the synthetic scene roughly 5% of events belong to a target. On the real recording it is 0.081% — prior odds of one to twelve hundred. And the noise gate was asking the wrong question. It keeps an event only if one of its eight neighbours fired within 5 ms, but a tracked space object is an unresolved point source emitting about fifteen events a second onto one or two pixels, so its own events arrive 65 ms apart. Inside windows that contained a labelled object, one event in 273 survived the filter. The pipeline was working perfectly on an empty stream.

Integration time is a signal-to-noise choice

The second cause was the 10 ms window. For a counting detector against a Poisson background the figure of merit is signal over the square root of background, not signal over background. We read the wrong column first, and the correction moved the optimum from 100 ms to about a second.

IntegrationSignal eventsBackgroundS/√BObject smear
10 ms — the old default0.230.090.80.06 px
100 ms3.910.864.20.63 px
250 ms9.042.126.21.57 px
1000 ms28.148.189.86.28 px
2000 ms41.8916.1910.412.57 px

That is why detection now runs at more than one scale at once. A scale is an integration depth with its own detector and its own tracker, because an object drifting at 6 px/s and one transiting at 640 px/s do not share an answer. A deep scale integrates without overlap: re-clustering an almost identical buffer every window makes a chance cluster reappear a hundred times, which quietly destroys the independence that m-of-n confirmation depends on.

Where that leaves the numbers

On the EBSSA recordingResult
Mean end-to-end latency0.17 ms
p99 latency1.23 ms
Speed against real time, 2 cores57×
Windows over a 40 ms budget0 %
Precision / recall / F1 on tracks0.052 / 0.079 / 0.063
Localisation error3.04 px
Objects found1 of 12
What an operator sees in 198 s4 tracks: 1 real, 3 false

That is a weak accuracy result and we are not going to dress it up. The pipeline finds and holds the primary tracked object on real data, to three pixels, while running fifty-seven times faster than real time on two cores — and it raises three false tracks in three and a half minutes. Ten of the twelve labelled objects are transits crossing at 244 to 641 px/s; they deposit fewer than two events in a 10 ms window, so there is nothing for a point-source clusterer to cluster. That is arithmetic rather than tuning, and reaching those objects needs a streak-aware detector we have not built.

Two smaller things the run settled. The same code handled a second sensor at a second resolution — a DAVIS at 180 × 240 — with no change at all, which until then had only been shown on synthetic scenes. And the synthetic profile is bit-for-bit unchanged: calibrating for real data adds a named parameter set, it never quietly retunes numbers already published.

05 · Limits

Where it stops working

F1 on confirmed tracks across a magnitude ladder, at three sensor resolutions. Higher magnitude means a fainter object.

F1 (confirmed tracks) 0.00 0.25 0.50 0.75 1.00 0.99 0.97 0.95 mag 6.0 0.99 0.93 0.25 mag 8.0 0.11 0 0 mag 10.0 0 0 0 mag 11.5
640 × 480 346 × 240 240 × 180

Read plainly, this is a cliff, not a slope. Between magnitude 8 and magnitude 10 at 640 × 480, F1 goes from 0.99 to 0.11. And the cliff moves brighter as the sensor shrinks: more pixels across the point-spread function means more contrast crossings per unit of flux, so a larger sensor reaches fainter.

The cause is not detector sensitivity. Recall is measured per window, and a faint target emits events intermittently — so in some windows it simply does not accumulate enough events to clear the cluster floor. The remedy is track continuity: predicting through the gaps rather than building a larger model. We publish the cliff because a stated limitation is worth more than a suspiciously clean result.

The neuromorphic layer is switched off, on purpose

FEAST — an unsupervised spiking feature layer with adaptive selection thresholds — is implemented and works. On this noise process it costs 2.4× the end-to-end latency and moves F1 from 0.779 to 0.776: inside noise, and in the wrong direction. So it ships disabled and is reported as a measured ablation rather than quietly dropped. It may still earn its place on real sensor noise, which is a different process from the synthetic model.

06 · At scale

How far it goes before it breaks

Every latency figure above comes from one scene at one input rate. That says the pipeline is fast on a small sensor; it says nothing about the ceiling. So we raised the input rate on a 1920 × 1080 sensor — larger than anything in the EBSSA set — until the pipeline broke, and recorded where.

The largest single run: 83,189,520 events, 1.08 GB of raw event data, a thousand consecutive 10 ms windows. It held all three objects at F1 0.978 with a mean of 28.25 ms and a p99 of 36.70 ms per window, sustaining 2.81 million events per second. Seven windows out of a thousand crossed the 40 ms budget.

Events / windowFilter DetectTrackTotal Over budgetF1
3,2452.39 ms0.580.613.58 ms0 %0.995
11,1264.80 ms0.650.626.07 ms0 %0.995
21,4978.23 ms0.690.689.60 ms0 %0.995
42,23614.68 ms0.740.7116.13 ms0 %0.995
83,71223.08 ms0.930.6224.63 ms0 %0.995
166,66047.52 ms28.7435.83112.09 ms100 %0.085
332,55790.18 ms21.9022.91135.00 ms100 %0.000

There are two ceilings, and only one of them is the 40 ms budget

Per-window latency stays inside 40 ms up to 8.4 million events per second. But sustained real time is a stricter test that the budget never states: a 10 ms window has to be processed in under 10 ms, or the queue grows. That line is crossed at ~2.2 million events per second. Between the two, the pipeline is simultaneously inside its latency budget and falling behind its sensor. Both numbers are true; they answer different questions, and a system quoting only the flattering one is not quoting a capability.

Detection and tracking never notice

The noise gate costs 2.39 ms at three thousand events per window and 23.08 ms at eighty-four thousand — about 0.28 microseconds per event, linear, exactly what the complexity says. Meanwhile detection and tracking stay flat between 0.6 and 0.9 ms across a 26× change in input rate, because the gate hands them a near-constant workload however loud the input gets. That is the whole architecture in one row of numbers, and it is measured rather than argued.

Then it fails at both things at once

At 166,000 events per window the gate stops holding. Filtering goes from 23 to 48 ms, detection from 0.93 to 28.74 ms — a thirty-onefold jump — and F1 collapses from 0.995 to 0.085 in the same step. Compute and accuracy break together and for a single reason: enough noise survives to cluster like a target. It is a cliff rather than a slope, which is the same shape as the brightness limit above.

Up to that point accuracy is flat at F1 0.995 across a 26× input-rate change. That is the claim worth making from this: throughput costs latency, not correctness — until the gate gives way, when it costs both.

This is a synthetic stress test, and the honest detail is the tail. At the headline rate a two-second run reports zero windows over budget. The same rate over ten seconds reports seven in a thousand. The overruns only exist in the tail of a longer run, so a short benchmark would have shown a clean sweep and been wrong about the system. We report the ten-second number.

07 · Engineering decisions

What we left out, and why

A list of what a system uses shows assembly. A list of what it rejected shows judgement. Every component below was assessed against three filters: does it fit the latency budget on commodity CPU, is its licence permissive, and does it actually improve a result.

ComponentVerdictReasoning
Grid clusteringKEEP Single O(N) pass, no k, no neighbour graph. DBSCAN’s memory profile is the wrong shape for a streaming window.
Kalman + PDAKEEP Gives sub-pixel position and angular rate at 0.34 ms per window.
YOLOv8 / ultralyticsCUT Two independent reasons. Its AGPL-3.0 licence is copyleft with a network clause. And a general object detector is the wrong instrument for a two-to-five-pixel point source.
Pretrained COCO weightsCUT Weights carry their own terms, independent of the code that loads them. Nothing here is trained, so there is no checkpoint to encumber.
Sliced inference (SAHI)CUT Exists to find small objects inside large frames. We never build a frame.
Appearance re-ID (DeepSORT)SWAP An unresolved point source has no appearance to re-identify. Replaced with probabilistic data association.
PostGISCUT Wrong coordinate system. Detections live in pixel coordinates mapping to right ascension and declination on the celestial sphere — not latitude and longitude on the Earth’s surface.
Redis / message brokerCUT A broker between stages that each complete in under a millisecond adds latency, a process boundary and a failure mode, in exchange for nothing.
Trained SNN (snnTorch, Norse)CUT Backpropagation-through-time on two CPU cores. FEAST gives a genuine spiking mechanism with no training run at all.
Time-series store, 3D globeDEFER Defensible in production; worth nothing in a prototype. A celestial globe is actively misleading until astrometric plate-solving and TLE correlation exist to feed it.
08 · Deployment

One container, no accelerator

SPARSE ships as a multi-stage linux/amd64 image that runs as an unprivileged user, so nothing it writes into a mounted volume comes back root-owned. There is no CUDA base, no GPU runtime, no model weights and no dataset inside it.

It is published and pullable right now, from our own read-only registry. No account, no login, nothing to configure:

docker pull vixitai.com/sparse:1.0

docker run --rm -v $PWD/data:/data vixitai.com/sparse:1.0 \
  synth --out /data/scene.npz --shape 480x640 --duration 2.0 \
        --mags 6.0 7.5 9.0 --seed 0
docker run --rm -v $PWD/data:/data vixitai.com/sparse:1.0 \
  run --input /data/scene.npz --gt /data/scene_gt.npy --out /data/out

Those two commands land on F1 0.779, 3 of 3 objects and 0.96 px — the same canonical scene quoted in section 03, reproducible to four decimals on a machine that has never seen this project. Add --profile sparse-sky to run the same image against real sensor data.

Output is a JSONL track record, a per-stage latency report, and a single self-contained HTML viewer — no server, no CDN, no build step — that plays the events, detection boxes and track trails from inside the output volume with one double-click.

Dependencies, and why the list is short

NumPy and SciPy (BSD-3), expelliarmus (MIT) and h5py (BSD-3), on a Debian-based Python 3.11 image. Every version is pinned exactly, because a result you cannot reproduce is not a result: the same scene gives identical accuracy to four decimal places across two major NumPy releases and across one and two cores.

The whole stack is permissively licensed, with no copyleft anywhere in the tree and no encumbered weights — which is a deliberate position, not an accident of what happened to be convenient.

Where this goes next