A plain-language console over synthetic-aperture radar scenes. Ask "are there storage tanks?" or "how many vessels?" and get an answer, a confidence value, the evidence drawn on the image and every raw score behind it. The real SARCLIP ViT-L-14 radar-language model runs locally by default, over ten real open radar scenes, and a capability gate refuses questions the sensor cannot answer before the model runs.
Console prototype · SARCLIP ViT-L-14, local · 10 real open scenes · 190 tests passing · cached answers 0.13 to 0.34 s, counts about 1 s · upstream: fetch runs, analysis designed
At a glance
What
A console for asking plain questions of radar satellite images. SAR (synthetic-aperture radar) sees through cloud and darkness, and its pictures look like textured noise. The console turns a question into a scoring run by a radar-trained model, or into a detector run the model then checks, and returns the answer with a confidence value, boxes on the image and every raw score behind it.
Why it matters
Satellite radar is now cheap and frequent, and the slow part is reading it. A generic vision-language model (VLM, a model that matches images to text) will invent a count the sensor could never support. This project refuses such questions from the scene metadata, answers the rest from a radar-native model, and writes down where that model fails.
How it was built
Python with PyTorch, transformers, FastAPI and rasterio, plus a Next.js console in TypeScript. Ten real scenes: eight high-resolution small-satellite scenes at 0.65 to 1.5 m and two wide-area scenes at 10 m, fetched from open data programmes. The 1.7 GB of model weights are downloaded and SHA-256 checked, never committed. Image embeddings are precomputed per scene, so a cached answer takes 0.13 to 0.34 s. 190 tests pass.
Capture · count · real model
Scene Viewer
S0201 · X-band · 1.5 m/px · VV · 31.1°
Contains ICEYE SAR Open Data, ICEYE 2025, licensed under CC BY 4.0.
Response
2.1 s · mps · ViT-L-14
How many vessels are in this scene?
17 vessel(s)
Confidence · low
0.050
intent: count · target = vessel capability: ok · target supported at this resolution method: CFAR detector proposes, SARCLIP verifies each chip candidates: 17 proposed, 17 kept confidence: count_v2 = 0.053 mean kept p × 0.941 decided model_variant: ViT-L-14 · sha256 e8d163b69d73…
All 13 hand-labelled ships get a box. The four extra boxes are small or ambiguous returns, and one bright ship is split in two by its sidelobes. The confidence reads low by design: the detector found the ships, but the model does not back ships on open sea at chip scale, and the card says so instead of hiding it.
Capture · presence · real model
Scene Viewer
S0202 · X-band · 0.75 m/px · VV · 35.5°
Contains ICEYE SAR Open Data, ICEYE 2026, licensed under CC BY 4.0.
Response
0.16 s · mps · ViT-L-14
Are there storage tanks?
present
Confidence · high
0.808
intent: presence · target = storage_tank capability: ok · target supported at this resolution tiles: 204 scored, 18 at or above 0.55 scoring: target vs 6 contrast classes confidence: presence_v2 = best tile 0.808 model_variant: ViT-L-14 · sha256 e8d163b69d73…
Storage-tank presence is the model's strongest skill on real data: correct on all four tank scenes, with no false alarm on 79 non-tank patches. The outlined tiles are the 18 that scored as tanks; the solid one is the best tile. The answer came from cached image embeddings, so only the text side ran for this question.
Capture · refused query · real model
Scene Viewer
S0210 · C-band · 10 m/px · VV · wide-area
Contains modified Copernicus Sentinel data 2025, processed by ESA; accessed via Microsoft Planetary Computer.
Response
62 ms · mps · ViT-L-14
How many aircraft are visible?
Unable to answer
target 'aircraft' is not supported on scene S0210 (C-band, wide-area, 10.0m). Supported on this scene: ['large_vessel', 'port_activity'].
capability: reject · supported_targets check answer: null tiles: 0 scored, the model never ran confidence: 0.000 · capability_reject model_variant: ViT-L-14 · sha256 e8d163b69d73…
An aircraft is a few pixels at 10 m, so the gate refuses before any score is computed. Aircraft questions are answered only on airfield scenes; the gate refuses them on every other scene, whatever the resolution. Asked "how many vessels?" on the same scene, the gate downgrades the question to large vessels and says why: "at 10.0m only large vessels are reliably detectable; small craft will be missed."
All three cards are re-drawn from JSON captured from the running FastAPI app with the real SARCLIP ViT-L-14 weights (SHA-256 e8d163b6…9e68f64) on an Apple GPU. Answers, scores, boxes, runtimes and the reject rationale are the captured values; only the timestamps were removed from the files. The images are downscaled for the web (S0201 from 3,333 px, S0202 from 3,833 px, S0210 from 3,329 px wide) and the overlays are drawn in full-scene pixel coordinates. On S0201 the rings mark each box, because many boxes are only a few pixels tall at this size. Raw files:
health.json ·
scenes.json ·
query_S0201_vessels.json ·
query_S0202_tanks.json ·
query_S0206_tanks.json ·
query_S0210_aircraft.json ·
query_S0204_scene.json ·
audit.json
Problem
Radar is on many satellites. Few people can read it.
Small radar satellites now revisit chosen areas daily or faster at metre and sub-metre resolution, by day or night and through cloud. Radar backscatter does not look like a photograph. It looks like static with structure. The people who need the data (port managers, defence analysts, commodity desks, insurers) want to ask questions of it.
Two failure modes are equally bad. A specialist takes days to answer, or a generic vision-language model produces a plausible count with no grounding in what the sensor can resolve at its band and pixel size.
What it does
Ten real scenes, three task types, one gate before the model runs.
The console lists the scenes with their band, pixel size and supported targets. An analyst picks a scene and asks a question. A rule-based router turns it into a task (count, presence, classify) and a target class. A capability gate compares the target with the scene metadata and allows, downgrades or rejects; rejected questions never reach the model. Presence and scene-type questions are answered from tile scores. Counts come from a radar detector whose candidates the model checks. Every response lands in a local SQLite table with the model variant and the weights checksum.
Routes free-text questions to one of three task types and a target class, with length qualifiers such as "longer than 100 m". routing/router.py, tests/test_router.py
Rejects or downgrades a query from scene metadata alone, with a rationale naming band, regime and pixel size, in a fraction of a second because the encoder is skipped. Aircraft questions are allowed only on airfield scenes. routing/capability.py, tests/test_capability.py
Cuts the scene into tiles sized in metres for the target and scores each tile against seven class descriptions: the target and six competing contents (open water, land, dense buildings, container terminal and the two other targets). pipeline/tiler.py, scoring
Counts by detect then verify: a two-parameter CFAR detector proposes up to 64 bright objects, SARCLIP scores a chip of each, and every kept candidate becomes one box. pipeline/detect.py, tests/test_detect.py
Caches image embeddings per scene, so only the text side runs per question: 0.13 to 0.34 s once cached (counts about 1 s, mostly the detector), against about 30 to 60 s for the first question on a scene without the cache on a MacBook Air M4. scripts/precompute_embeddings.py
Persists every response, including rejections, as one SQLite row with the full payload, the model variant and the weights SHA-256, and shows them on an audit page with filters and read-only replays. audit/store.py, api/main.py
Fetches all ten real scenes from open data programmes in one command, with no account. scripts/fetch_real_scenes.py
Solution
Refuse from physics first. Answer from a radar-native model. Count with a detector the model checks.
A question is parsed into a (task, target) intent. The capability gate (a metadata check that compares the requested target with the scene's band, regime and pixel size) decides ok, downgrade or reject before the model is touched. Allowed questions go to SARCLIP, a version of CLIP (the well known model that matches images to text) retrained on radar image and caption pairs, here in its ViT-L-14 size. Each tile is scored against the target and six competing classes. A "with a vessel" against "without a vessel" prompt pair scores at or below chance, because CLIP does not understand negation; the contrast set separates vessels and tanks clearly.
Tile classification cannot count: one tile holds at most one counted object, and a ship covering a few percent of a sea tile loses to "open water". Counting therefore uses the classic radar approach. A CFAR detector (a threshold that adapts to the local background) proposes bright objects, merges fragments and filters them by physical size; SARCLIP verifies a chip around each candidate. No language model writes the answer, so a number can only come from detector output and model scores, and the audit row holds both, with the formula that produced the confidence.
Result
The gate and tank presence hold on real data. Counting works on open sea.
The aircraft question on the 10 m scene is refused in 62 ms, before any tile is scored. Storage tanks are found on the tank-farm scene with a best tile of 0.81, from cached embeddings in 0.16 s, and a second tank scene answers the same way at 0.85. The vessel count on the open-sea anchorage boxes all 13 labelled ships, with four extra boxes and a confidence that honestly reads low. Every capture carries the same model variant and weights checksum, and every one of them is in the audit file.
Real scenes
10
Patch accuracy
0.38 vs 0.14 chance
Tank AUC
0.97
Cached answer
0.13 to 0.34 scounts about 1 s
The design stance in one figure. The gate reads only scene metadata and decides before the encoder runs, which is why the refusal above takes 62 ms. Both outcomes are written to the audit store as one row with the full payload, the model variant and the weights checksum.Why scoring uses a contrast set. AUC is the chance that a random patch with the target outscores a random patch without it, so 0.5 is guessing. The negation pair asks the model to understand negation and sits at chance for vessels and tanks. The aircraft number is not trustworthy either way: all nine aircraft patches come from one airport scene.
Evaluation on real data
What works, and what does not yet.
A sanity check on a small hand-labelled set, not a benchmark: 94 patches from all ten scenes, and hand-labelled object centres on four scenes, all labelled by one non-specialist. Counting settings were tuned on two scenes and checked on two held out.
Capability
Result
Read
Patch classification
0.38 vs 0.14
Accuracy on 7 classes against chance. About 2.7x chance. Tanks, land and urban are recognised; container terminals never are, they read as buildings or ships.
Storage-tank presence
4 of 4 scenes
Patch AUC 0.97, no false alarm on 79 non-tank patches. The strongest skill.
Capability gate
as designed
Aircraft questions answered only on airfield scenes and refused everywhere else, including both 10 m scenes; vessel questions downgraded to large vessels on 10 m; answers in a fraction of a second.
Vessel count, open sea
13 of 13 ships
17 boxes on S0201: three extra on ambiguous small returns, one ship split by sidelobes. Usable.
Vessel count, port
about half right
Only about half of the boxes sit on a labelled ship. Show the boxes, not the number on its own.
Vessel count, 10 m
over-counts
Nearly every large ship is found, but small craft and sidelobe fragments on open water are counted too. Needs a stricter threshold tuned on another 10 m scene.
Aircraft count
not usable yet
On the airport scene, the only scene where the gate allows aircraft questions, the detector finds about half of the countable aircraft; verification rejects every one of them. Kept visible as a failing test.
Scene type
3 of 10
Right on the clear cases (open sea, airport). Port and industrial scenes come back as airfield or urban.
Upstream: the intelligence pipeline
Where the scenes come from: one fetch command runs. The analysis chain is designed, not run.
The console answers questions about a scene. Upstream of it sits the chain that produces those scenes and the first analysis layers: fetch, then change maps, ship contacts and building damage, merged into one GeoJSON file. The two halves are one product: open data goes in, and a plain-language answer with its evidence comes out.
The first stage runs. One command fetches all ten real scenes directly from open data programmes. The high-resolution scenes are partial reads of public cloud-optimised GeoTIFFs, so HTTP range requests pull only the window that is needed; the two wide-area scenes come from an open catalogue with an anonymous access token. No account and nothing to buy. Each scene is cropped, block-averaged, written with a neutral descriptor and verified, and its credit line is kept with it.
The method chain after it follows published work. Change uses the amplitude log-ratio and an InSAR coherence score between two passes. Ships use a CFAR detector and would be matched against AIS transponder tracks to flag vessels that hide. Damage follows the pixel-wise t-test approach on the coherence drop. Those notebooks are implemented in part and have not run. One piece runs inside the product: a two-parameter CFAR ship detector, with a dark-surface mask and a censored second pass, is the proposal step of every count in the console.
Fetch scenes from open data programmes in one command: windowed reads of public cloud-optimised files for the high-resolution set, catalogue items for the wide-area set, cropped, block-averaged and verified, with licence and credit line per scene. Runs. Calibration to sigma0, the standard measure of radar brightness, is not done: the console works on uncalibrated amplitudes. scripts/fetch_real_scenes.py
Compute log-ratio and coherence change maps, threshold them with CFAR, and stack detections into a temporal heatmap. Implemented, never run. notebook 02
Mask land, detect ship candidates with a two-parameter CFAR, and cut 64-pixel chips. Implemented, never run in the notebook; a separate detector implementation runs in the console. Classify each chip with a CNN and match contacts against AIS. Designed, no code yet. notebook 03, pipeline/detect.py
Build a coherence stack, run a pixel-wise t-test for the pre-to-post coherence drop, and assign one of five damage levels per building footprint. Implemented, never run. Load OpenStreetMap footprints. Designed, no code yet. notebook 04
Layer the three results on a Folium map and emit a structured JSON summary. Implemented, never run. Export the merged GeoJSON. Designed, no code yet. notebook 05
The chain as designed. One fetch stage fans out to three analysis notebooks, which fan in to one map and report. Blue marks what runs: the open-data fetch, and the CFAR ship detector inside the console. Solid text marks functions that exist in the notebooks, dotted boxes mark steps with no code. Stages 02 to 05 have not run.The sensor geometry the design rests on. The satellite flies along the azimuth direction and looks sideways across the range direction, so each scene is a strip lit from one side. Its own microwave pulse is the light source, so cloud and night make no difference. Amplitude products (GRD) serve change and ship detection, phase products (SLC) serve coherence and damage. All ten scenes in the console are amplitude products.
The smallest honest next step: fetch two wide-area passes over the same port with the same command, run the change detection on them, and hand the detections to this console as evidence next to its own boxes.
Solution architecture
What it stands on, and what this project adds.
Inputs and datascenes and metadata
10 real open scenes8 high-res at 0.65 to 1.5 m, 2 wide-area at 10 m. CC BY 4.0 or free and open satellite data terms
open-data fetcherwindowed range reads of cloud-optimised files and an open catalogue, no account
scene manifestband, regime, pixel size, polarisation, supported targets. A lint blocks provider and place names outside the attribution file
5 synthetic scenesseeded, for tests only
GeoTIFF amplitude and one scene object per query
Models and algorithmswhat reads the scene
SARCLIP ViT-L-14code MIT, weights research only, 1.7 GB, SHA-256 checked at setup, loaded by default
CFAR detectortwo-parameter, censored second pass, dark-surface mask, size filter in metres
regex intent router"ships" to vessel, "how many" to count
capability gatesupported targets and pixel-size rules. ok, downgrade or reject with a rationale
intent, verdict, tile embeddings and candidate chips
Engineown code, Python
tiler + contrast scorertiles sized in metres, up to 256 per query, target vs 6 contrast classes
embedding cachetile embeddings per scene on disk in float16, precomputed at bootstrap
detect-then-verify counterchips of 2.5 x object length, keep rule, count_v2 confidence
PyTorch + transformersBSD 3 and Apache 2.0
numpy, scipy, rasterioBSD 3, arrays and GeoTIFF I/O
answer, boxes, raw scores, confidence inputs
Outputs and interfaceswhere a reader lands
FastAPI REST APIhealth with model variant and weights SHA, scenes, scene image, query, audit
SQLite audit storeone row per query with the full response, model variant and weights SHA-256
Next.js consolescene browser, viewer with boxes, chat, raw-score inspector with CSV export, audit page with replays
CLI query.pyone question from the terminal, text or JSON
Built in this projectThird party
Sources: SARCLIP code is MIT and pinned as a git submodule; its weights are released for research and non-commercial use only, are downloaded and checksum-verified at setup, and are never committed. The SARCAP dataset behind it is non-commercial research only and is not used here. The ten real scenes are open data under CC BY 4.0 or free and open satellite data terms, cropped and downsampled, with the credit line for each published image directly under it. PyTorch, numpy, scipy and rasterio are BSD 3, transformers is Apache 2.0. FastAPI, Next.js, React and Tailwind are MIT. Everything else is this project, MIT.
Why it matters
A refusal contract with a real model behind it.
The answer path contains no language model, so a count can only come from detector output and model scores, and the audit row holds those scores, the threshold, the prompt strings and the checksum of the weights that produced them. The capability gate rejects a question from scene metadata before the model runs. With real weights on real scenes the project also shows where a radar-native model helps (tank presence, separating ships from port clutter) and where it does not (ships on open sea at chip scale, aircraft, scene type in ports), measured and written down instead of hidden behind a confident number.
In five years
Analysts will ask radar constellations questions in plain language and expect the system to say when the sensor cannot answer. That refusal logic, the per-tile evidence and the audit trail will be regulated surfaces in defence and maritime compliance. This console is a small, inspectable early version of that answer contract.
How it was built
A product surface, a real model, and an evaluation behind each method choice.
Python
9,155lines
Tests
190passing
Commits
51
TypeScript
2,042lines
Test code
2,809lines
Runtime deps
18
Spec, architecture, thresholds, labelling and review by hand. Implementation with Claude Code agents. Python lines include the test code. The TypeScript console spans 16 files. Measured from the repository.
The product surface (router, gate, tiler, SARCLIP wrapper, scene generator, SQLite store, API and console) also runs on a deterministic stand-in for the model, which the tests use. The real side: weights fetched and checksum-verified, ten open scenes fetched, a 94-patch hand-labelled set and a gating test that fails when the model stops clearly beating chance. The evaluation decides three method choices. The negation prompt pair scores at chance, so scoring uses a contrast set. Tile classification cannot count, so counting uses a CFAR detector with SARCLIP as verifier. An uncached scored question takes about 30 to 60 s, so image embeddings are precomputed per scene.
Algorithms and mathTwo-parameter CFAR with a censored second pass, an Otsu dark-surface mask, component merging and size filtering in metres, contrast-set softmax scoring, and versioned confidence formulas. No training of its own.
Systems integrationA 1.7 GB checkpoint loaded through transformers on MPS, CUDA or CPU, windowed range reads of remote GeoTIFFs, an anonymous catalogue token, a float16 embedding cache, FastAPI with lifespan loading, SQLite and a typed Next.js client.
Data and domainReal radar from four open sources at two resolutions, uncalibrated and display-scaled amplitudes, band and regime semantics, target visibility per pixel size, and per-scene licences and credit lines.
Evaluation rigor94 labelled patches, labelled object centres on four scenes, two held out, a gating test on every run with weights, and the failures kept visible. Small set, one labeller, no benchmark.
Interface and productAn operator console with grounding overlays, a confidence chip with its formula, a raw-score inspector with CSV export, reject cards and an audit page with replays, plus a REST API and a CLI.
Difficulty 18 of 25 · AdvancedRelative to one competent engineer building it alone
Success criteria
When this counts as working.
Never emit a number for a query the scene metadata cannot support. This is the point of the system. Met: rejected questions never reach the model.
Every answer is backed by per-tile or per-candidate scores, a threshold, the prompt strings, the model variant and the weights checksum that the user can inspect. Met.
Under 10 s per question on the reference laptop. Met once a scene's embeddings are cached (0.13 to 0.34 s, counts about 1 s); the first question on a scene takes about 30 to 60 s on a MacBook Air M4 without the cache, which is why setup precomputes the embeddings.
The model clearly beats chance on labelled real radar patches, checked on every test run with weights. Met: 0.38 against a gate of 0.29.
Every query and response, including rejections, is persisted to a local audit store. Met.
Runs on a laptop with no cloud inference. Met: the only network use is the one-time fetch of weights and open scenes.
Metrics, KPIs, evals
Numbers from the captures above.
KPI
Value
What it means
Gate verdicts
4 ok / 1 downgrade / 1 reject
Six captured queries. The aircraft question on the 10 m scene was refused, the vessel question there was downgraded to large vessels, and the rest were allowed. The gate tests and the end-to-end reject test are the better evidence.
Count confidence
0.050 · low
S0201 vessels. Formula count_v2: mean model probability over the kept candidates (0.053) times the share the model decided clearly (0.941). It says how much the model agrees, not how likely the count is to be right.
Presence confidence
0.808 / 0.849
S0202 and S0206 storage tanks, both present and high. Formula presence_v2: the best tile's target-class probability.
Runtime, single samples
0.06 to 11.6 s
Refusal 62 ms. Presence 0.16 and 0.19 s, scene type 0.25 s, from cached embeddings. Counts are slower because they also run the detector and encode each candidate chip at query time: 2.1 s for 17 chips on S0201, 11.6 s for 63 chips on the downgraded 10 m query. Apple GPU, one sample each.
Audit completeness
6 / 6 queries
Every captured response, including the refusal and the downgrade, persisted to SQLite with the model variant and weights SHA-256. See audit.json above.
Model provenance
1 checksum
Health endpoint, every capture and every audit row name ViT-L-14 and the same weights SHA-256, so a reader can see which model answered.
Tests
190 passing
Python tests run on a deterministic stand-in without weights; tests marked weights load the real model and run the labelled real-patch set. CI runs ruff, pytest, shellcheck, a neutrality check, and the frontend type check and build.
What this does not do yet.
Count reliably beyond open sea. Port counts are about half right, 10 m counts include small craft, and aircraft counts are not usable.
Tell port scenes apart. Scene type is right on 3 of 10 scenes; ports come back as airfield or urban.
Use calibrated radar brightness. The scenes are uncalibrated amplitudes, and three arrive already display-scaled.
Use the phase channel. The toggle is visible and inactive.
Claim a benchmark. The evaluation is a small hand-labelled sanity set from one non-specialist labeller.
Run the downstream analysis chain. Change, damage and AIS matching are designed, not run.
Ship container images. The Docker setup has not yet been built on a Docker host.