-
-
The operator console. Five sample sites are generated on first run and run through the same pipeline as an uploaded capture.
-
A rescan in progress: the second visit is registered into the first, then normalised, differenced and resolved, with per-stage timings.
-
The aligned comparison. Both visits sit in one canonical frame, so the slider compares one place at two moments, not two photographs.
-
Four typed changes: departed, arrived, moved 211 px, altered. Each carries its measurement and confidence; the 47% one is labelled 47%.
-
What the system did not see. 30% of the frame was comparable, so the rest becomes three capture requests instead of "nothing changed".
-
The approval gate. A scan stops before writing; nothing enters the site's history until a person decides, and that decision is recorded.
-
The architecture: console, HTTP API, agent loop and OpenCV 5 core, plus the managed AWS Graviton storage path and the COOL benchmark.
What it does
Photograph a place. Photograph it again later. STRATA tells you what changed, in the sense an engineer would accept: which object left, which arrived, which moved, which was altered — each with a location, a measurement, a confidence, and the reasoning that produced it.
Two visits of the same place are never the same photograph. The camera moves, the light shifts, the exposure drifts. Subtract them and you get the difference between two cameras, not the difference between two moments. So the problem is solved in the order it has to be solved: register, normalise the photometry, detect what genuinely differs, then resolve regions of difference into typed events.
It then does the part most systems skip. It reports what it did not see. Regions it could not observe well enough to compare are marked unresolved and turned into a specific request for another capture, rather than folded into a confident "nothing changed". Nothing enters a site's history until a person approves it.
The result has the properties engineers already trust in a repository: an immutable baseline, a diff against it, typed changes, provenance for every claim, and an append-only history that includes the scans that found nothing.
OpenCV 5
The vision core is written against OpenCV 5.0.0 (opencv-contrib-python==5.0.0.93)
and uses APIs that do not exist in 4.x:
cv::ALIKED— the CNN keypoint detector and descriptor introduced in OpenCV 5, run at up to 2000 keypoints with 128-dimensional descriptors.cv::LightGlueMatcher— attention-based matching with a learned confidence per correspondence, given spatial context throughsetPairInfo()beforematch().cv2.USAC_MAGSAC— robust estimation under OpenCV 5's USAC framework. Registration lands at 0.665 px mean corner error across the benchmark.- The classical path (
SIFT+ FLANN) is a working fallback, not a stub: with no weights on disk the whole product still runs, and every result reports which matcher actually executed.
Two guards exist because of measured failures rather than intuition. LightGlue
divides by zero on a featureless image, and on Windows that is a fatal C-level
exception — no Python handler runs, and the service process dies with it. A
single frame of a blank wall would take a deployment down. The condition is
invisible in the features (ALIKED returns 2000 well-spread keypoints with normal
descriptors on a uniform frame), so the guard tests the image, using mean
absolute Laplacian as a structure energy, and it sits before the call because
try/except cannot survive a failure that never unwinds the stack. Measured
separation: uniform grey 0.0000, a single small square 0.43, faint sensor noise
5.03, a real scene 15.6–20.2.
The agent
A five-phase loop — observe → decide → act → gate → finish — where the vision output changes what happens next. From a live run, verbatim from the persisted trace:
[observe] scan by=pipeline "4 changes; coverage 30%; registration confidence 0.92"
[decide ] request_view by=policy-rules "Only 30% of the frame is well observed, against a 60% bar"
[act ] request_view by=loop "Requested a further capture, but none was available"
[decide ] mark_uncertain by=policy-rules "…rather than accept a scan that implies the unseen areas were unchanged"
[decide ] finalize by=policy-rules "Committing this as the canonical record — which requires your approval"
[gate ] approval by=human
The decision layer is a deterministic policy by default. A model may be consulted at exactly one call site, to justify why another view is wanted; if it fails for any reason the run degrades to the rules with the error recorded in the trace. Nothing in the product requires a model to be reachable — which is why the demo runs with no configuration and no API key at all.
A requested capture changes what the system observes: each further frame of the second visit is matched, registered with the same estimator, warped into the canonical frame, and credited to coverage — and a frame that will not register is reported as attempted rather than counted. Coverage accumulates as a union, not a mean, so a second look can only ever add evidence.
AWS
The vision workload runs on AWS Graviton (arm64): Lambda, S3 for session objects and the ONNX feature weights, DynamoDB for the append-only ledger, CloudWatch for logs and per-stage metrics. The hybrid split is stated rather than hidden — development and the interactive console on x86, the vision workload on the Arm path — and the reason is technical, not decorative: the dominant cost is image processing (resize, warp, Sobel gradients, morphology, contour extraction), which is what OpenCV 5 accelerates on AArch64 through the KleidiCV HAL, and the workload is embarrassingly parallel across frame pairs.
Two deployment routes are committed. deploy/deploy.sh builds an arm64 container
with docker buildx; deploy/deploy.py ships the same handler to the same
architecture as a zip resolved from PyPI's aarch64 wheels, which is the route this
machine could actually run. Both are idempotent and have a --dry-run that prints
every command without touching an account.
It has been run. deploy/verify.py --record against a live account on
2026-10-20, receipt committed at deploy/verify-report.json, verdict PASS:
| Execution environment | arm64 (Graviton), python3.12, 3008 MB |
| Vision stack on the box | neural-lightglue — ALIKED + LightGlue, weights fetched from S3 into /tmp/models |
| Registration | homography, confidence 0.9231, 1174 inliers of 1210 matches |
| Change set | 4 typed events (added, missing, state_changed) |
| Latency | 8779 ms inside the function, 9156 ms round trip from outside; match 4941 ms is the bulk |
| Ledger | row read back from DynamoDB: lambda/ledger/verify-bay-4 / 20261020-012803-…json |
| Public API | POST /api/sessions then /rescan over HTTPS: 4 events, storage: s3 |
| Live endpoint | https://c3rdzrdswf.execute-api.us-east-1.amazonaws.com/api/health |
What has not run is the Graviton4 benchmark on EC2, so no Arm-versus-x86 ratio
is published. The harness that would produce it is committed and the protocol is
documented; publishing a number that was not measured would contradict the point of
the project. Two findings that only a real deployment could produce are recorded in
docs/DEPLOYMENT.md §2: opencv-contrib-python does not import on Lambda at all
(it links libxcb, which the runtime has no copy of — the deployment uses the
headless wheel at the same pinned version), and an anonymous Lambda Function URL is
refused in this account even with the canonical public resource policy, which is why
the endpoint sits behind API Gateway instead.
Evaluation
24 synthetic scenes at 960×720 generated from seeds, each with known mutations and
a known camera pose, plus 12 still pairs where nothing changed. Every figure below
is regenerated from the committed artefact by scripts/figures.py, and a test
fails if a document stops matching the run.
| Metric | Result |
|---|---|
| Registration error | 0.665 px mean corner error |
| Registration confidence | 0.9008 |
| Precision | 0.9375 (macro; micro 0.9318) |
| Recall | 0.8542 (micro 0.8542) |
| F1 | 0.8875 (micro 0.8913) |
| Event-kind accuracy | 0.6979 |
| Confident false positives on unchanged pairs | 0.0 per pair, 12 pairs |
A reported box counts as the same change as an expected one at IoU ≥ 0.25.
The still-pair number is the one that decides whether the system is usable. A detector that finds things is not hard; one that reliably finds nothing when nothing changed is what makes it deployable, and it is the number most systems do not report.
Reproduced independently. Installing requirements.lock in a clean
environment and running python scripts/evaluate.py --cases 24 reproduces the
committed artefact case for case, aggregate and still-pair block identical
(392.2 s against the recorded 387.5 s). The suite — 481 tests, 79 of which run the
real pipeline end to end — passes on the locked set, and CI re-runs the benchmark
and gates it against floors on every push.
What does not work
- Event labels are weaker than event locations. Kind accuracy is 0.6979 against F1 0.8875. The two are reported separately rather than blended.
- Ten of 24 cases merge two nearby changes into one region. The change is not lost; it is not split. Documented and pinned by a test that fails if the behaviour changes.
- One seed fragments a move destination into two regions, costing precision on that case, because the polarity guard correctly refuses to fuse opposite-polarity evidence but splits a genuine arrival whose fragments disagree locally.
- The second geometric model is not exercised by the benchmark.
prefer_planarshipsTrue, so all 24 cases registered as homographies and no model-comparison note was recorded. The fundamental-matrix branch is real and is covered by tests that turn it on; the benchmark contains no scene with enough depth structure to need it. - No Arm-versus-x86 measurement. The workload genuinely runs on Arm today - the receipt above is from a Graviton function - but the COOL benchmark on a Graviton4 EC2 instance has not been run, so no speed-up ratio is published.
- The evaluation data is synthetic. Real captures can be uploaded and go through the same code path, but they carry no answer key, so no real-world accuracy is claimed.
- No metric scale. Output is in canonical pixels: it says an object moved and roughly how far, not how many centimetres.
Responsible use
STRATA observes physical spaces, so consent and data handling are part of the
design rather than a disclaimer. It has no face model, no person
identification, no biometric template and no behavioural inference — these are
structural properties, not policy promises: the schema has nowhere to put a
person-level signal. Capture guidance is an instruction to a human holding a
camera, not autonomous acquisition. Every write is human-gated and attributed
(decision_made_by: human), and the history is append-only, so the record of what
was observed is itself observable. Uncertainty is a first-class output: "I could
not see this well enough" is preferred over a confident guess, because a confident
negative over ground nobody photographed is actionable and wrong.
Run it
Or don't install anything: the app is live at https://strata-console-rouge.vercel.app. Pick a sample site, press Run rescan, and the pipeline registers the two visits on AWS Graviton and returns the typed change set, the coverage it could not see, and the approval gate.
git clone https://github.com/JinYi123456/strata.git
cd strata
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
python scripts/serve.py # → http://127.0.0.1:8000
No configuration, no API keys, no model download to start: five sample sites are generated on first run and go through the same pipeline as an uploaded capture.
pip install -r requirements.lock # the exact measured set (Python 3.12+)
python scripts/fetch_models.py # ~52 MB, digest-verified
python scripts/evaluate.py --cases 24 # regenerate every published figure
pytest # 402 fast tests
pytest -m slow # 79 end-to-end tests
bash scripts/verify_ui.sh # drive the live app in a real browser
Built With
- amazon-dynamodb
- amazon-web-services
- aws-lambda
- cloudwatch
- docker
- fastapi
- next.js
- opencv
- opencv-5
- python
- typescript
Log in or sign up for Devpost to join the conversation.