Work · 02 of 9

OilTrace

U-Net oil-slick detection on Sentinel-1 radar, Smart India Hackathon 2026

Role
ML detection service, investigation dashboard, technical report
Team
Ved Kumar Singh, Parth Kshirsagar, Nimit Jain
Period
Aug to Sep 2026
Status
hackathon
Stack
  • PyTorch
  • U-Net
  • FastAPI
  • Modal
  • React
  • Leaflet
  • OpenDrift
trueman08.github.io/Team-AlgoRise_Oil_Detection_Report
The OilTrace technical report, captured 2026-09-15
The OilTrace technical report. Captured from the live site on 2026-09-15.

Measured

MetricValueWindowSource
Dice, vs 0.06 for the classical dark-spot threshold, 6× better0.35validation, traps includedML-service/runs/E6_focal_iter/history.json
Object precision, up from 0.28 after the focal-loss fine-tune; raw was 0.0450.47validationML-service/docs/judge_card.html
Object recall, slicks of 10 ha or more, max-IoU matching67%validationML-service/docs/judge_card.html
Centroid error, about 8 px at 10 m resolution85 mvalidationML-service/docs/judge_card.html
Inference, full Sentinel-1 scene on CPU10 sML-service/service/app.py, measured on the demo scene

Winner, internal round, Smart India Hackathon 2026 (problem SIH26143); national round pending

Problem

Problem statement SIH26143, set by NTRO, asks for oil-spill detection and vessel attribution from satellite data. The detection half sounds easy: oil damps the small waves that scatter radar back to the satellite, so a slick shows up as a dark patch on a Sentinel-1 scene. The trouble is that the sea is full of dark things that are not oil: calm wind, algae blooms, rain cells, ship wakes, the lee of an island. A detector that flags every dark patch is useless to an investigator, because the failure mode of a coastguard tool is not missing obvious oil; it is crying wolf on an algae bloom. The exam that matters is rejecting the look-alikes.

My module is the detector and everything downstream of its output that a human sees: the FastAPI service, the six-stage investigation dashboard, and the technical report. Ved built the backward hindcast, Parth the backend orchestration.

From the repository

14Commitsall mine
20,093Lines of code120 source files
19Test filesin the tree
Counted from Team_AlgoRise_OilSpill_detection + OilTrace-Frontend on 2026-09-15, commits 2026-08-31 to 2026-09-07. Source lines only (no vendored, generated or built files). The main repository is pushed from my work identity (TrueMan08); the separate frontend repository is the dashboard.

ML-service holds the training code (train.py, model.py, a sharded NPZ patch_dataset.py), five evaluation scripts (threshold sweep, confidence sweep, test-time augmentation, object-level matching, the classical baseline), eight recorded runs under runs/ each with a manifest and a per-epoch history, the FastAPI service, and the judge card that fixes how every number may be quoted. The backend has 23 tests.

Pipeline

Sentinel-1 sceneVV + VH GeoTIFFU-Net detectiontiled 512 px, TTASlick GeoJSONpolygon, centre, areaBackward hindcastOpenDrift, OpenOilAIS attributionvessels in windowCounterfactualforward run per vessel
Detection is my module. The hindcast and attribution stages take its GeoJSON.

A scene arrives as a two-channel GeoTIFF (VV and VH polarisation, sigma-nought in decibels). The service tiles it into 512-pixel patches, runs the U-Net on each with test-time augmentation, stitches the probability map back together, thresholds it, drops components below the minimum area or confidence, vectorises what is left, and returns WGS84 GeoJSON with a polygon, centroid, area and confidence per slick. The hindcast takes that polygon backward in time through OpenDrift to a probable source region; AIS vessels in that region and window are ranked; a forward counterfactual run per candidate checks whether that vessel could have produced the observed slick.

Training the detector

Data. About 2,500 real Sentinel-1 scenes, of which 685 are deliberate trap scenes with no oil. Scenes were split at scene level, never at patch level, so no patch of a validation scene ever appears in training. They were sharded into 16,926 training and 4,335 validation patches of 512 pixels, with an inventory and an error log per shard, because the first pipeline silently dropped scenes the reader could not open. A test set was sealed before the first experiment and has still not been evaluated.

Model. A plain U-Net, base width 32, batch normalisation, batch 6 with gradient accumulation of 4, Adam at 1e-4 for fine-tunes and 1e-3 from scratch. About eleven minutes per epoch on the one GPU we had.

Loss, and the runs that failed. The first runs used binary cross-entropy plus Dice. On a dataset that is 96% background, that combination has a stable attractor: predict nothing. E0_partial reported a Dice of 0.70 after its first epoch and then collapsed to 0.00, with precision 1.00 and recall 0.00, which is what “an empty mask is always safe” looks like in numbers. Fine-tuning from the checkpoint before the collapse gave a model that drew too much: recall 0.80, precision 0.17. Object-level error analysis showed why. The false positives were confidently wrong: look-alike regions predicted as oil at 80% confidence, which cross-entropy barely penalises once averaged over millions of pixels. Focal loss amplifies the gradient on exactly those pixels. The E5_focal_v2 fine-tune raised object precision from 0.28 to 0.47 while slick recall stayed within three points. A longer focal run, E6_focal_iter, collapsed the other way, precision 0.60 with recall 0.03, and was abandoned. All of it is in the git history, next to the run that worked.

Reporting. Scored the way most papers score, on oil-only patches, the model reaches about 0.75 Dice. We report 0.35 because that is the number with the traps in the validation set, and it is the one that predicts deployed behaviour. Every operating parameter (threshold, minimum area, confidence floor) was chosen on validation only.

Try it

Every recorded segmentation run, plotted from its own history file. The collapses are left in.

Experiment explorer7 segmentation runs · 62 epochs · 12.2 GPU hours, from the run histories
0.000.250.500.751.001234567891011121314151617epochE0_partial epoch 1: dice 0.701E0_partial epoch 2: dice 0.712E0_partial epoch 3: dice 0.000E0_partial epoch 4: dice 0.746E0_partial epoch 5: dice 0.662E0_partial epoch 6: dice 0.661E0_partial epoch 7: dice 0.702E0_partial epoch 8: dice 0.660E0_partial epoch 9: dice 0.000E0_partial epoch 10: dice 0.000E0_finetune epoch 1: dice 0.302E0_finetune epoch 2: dice 0.341E0_finetune epoch 3: dice 0.044E0_finetune epoch 4: dice 0.287E0_finetune epoch 5: dice 0.346E0_finetune epoch 6: dice 0.290E0_finetune epoch 7: dice 0.269E0_finetune epoch 8: dice 0.251E0_finetune epoch 9: dice 0.269E0_finetune epoch 10: dice 0.219E0_finetune epoch 11: dice 0.281E5_focal_v2 epoch 1: dice 0.324E5_focal_v2 epoch 2: dice 0.000E5_focal_v2 epoch 3: dice 0.059E5_focal_v2 epoch 4: dice 0.277E5_focal_v2 epoch 5: dice 0.244E5_focal_v2 epoch 6: dice 0.000E5_focal_v2 epoch 7: dice 0.294E5_focal_v2 epoch 8: dice 0.191E5_focal_v2 epoch 9: dice 0.270E5_focal_v2 epoch 10: dice 0.003E5_focal_v2 epoch 11: dice 0.303E6_focal_iter epoch 1: dice 0.293E6_focal_iter epoch 2: dice 0.302E6_focal_iter epoch 3: dice 0.056E6_focal_iter epoch 4: dice 0.254E6_focal_iter epoch 5: dice 0.335E6_focal_iter epoch 6: dice 0.263E6_focal_iter epoch 7: dice 0.283E6_focal_iter epoch 8: dice 0.086E6_focal_iter epoch 9: dice 0.346E6_focal_iter epoch 10: dice 0.323E6_focal_iter epoch 11: dice 0.342E6_focal_iter epoch 12: dice 0.029E6_focal_iter epoch 13: dice 0.059E6_focal_iter epoch 14: dice 0.087E6_focal_iter epoch 15: dice 0.072E6_focal_iter epoch 16: dice 0.320E6_focal_iter epoch 17: dice 0.051
E0_partial · BCE+Dice(1.0) · BCE plus Dice, 10 epochs. Epoch 1 reported Dice 0.70, then the run collapsed to Dice 0.00 with precision 1.00 and recall 0.00: the network learned that an empty mask is a safe answer.
E0_finetune · BCE(pos_weight=None)+Dice(1.0) · Fine-tune from the best E0_partial checkpoint (epoch 1, before the collapse) with plain BCE plus Dice at a tenth of the learning rate. Stable around Dice 0.28 to 0.30, recall high, precision low: it drew too much.
E5_focal_v2 · Focal(g=2)+Dice · Focal plus Dice from the E0_finetune checkpoint with a corrected schedule. The run behind the reported numbers: Dice 0.32 at epoch 1, 0.30 at the end, recall held near 0.70 while object precision rose to 0.47.
E6_focal_iter · Focal(g=2)+Dice · A longer iterative focal run from the E5_focal checkpoint, 17 epochs at half the learning rate. Collapsed the other way: precision 0.60 with recall 0.03. Abandoned.

Pixel metrics on the held-out validation split with look-alike traps included, one point per epoch, exactly as logged by train.py. The headline Dice of 0.35 and object precision of 0.47 come from the evaluation sweeps on the E5-focal checkpoint (threshold_sweep.py, confidence_sweep.py, tta_eval.py, object_eval.py), which choose an operating point on validation only; the test set stays sealed. Hover a point for its full row.

Timeline

  1. E0_partial starts at 14:16: BCE plus Dice Dice 0.70 after epoch 1, then collapse to empty masks over the next nine.
  2. 00:21 E0, 01:26 E0_finetune fine-tune from the pre-collapse checkpoint; stable at Dice 0.28 to 0.30, recall high, precision low.
  3. 12:28 E5_focal, 19:13 E5_focal_v2: focal loss object precision 0.28 to 0.47, recall within three points.
  4. 21:17 E6_focal_iter seventeen epochs, collapsed to precision 0.60 and recall 0.03; abandoned.
  5. Classical baseline measured at Dice 0.06; judge card written every number fixed with its baseline and its caveat.
  6. Repository pushed: ML service, backend, dashboard, report FastAPI on Modal, React and Leaflet investigation UI.
  7. Dashboard replaced with the final investigation UI
Run start times from the manifests, commit dates from the git log. The whole detector was trained in one 31-hour window.

Before and after

BeforeAfter
DetectorClassical dark-spot threshold, Dice 0.06U-Net with focal loss, Dice 0.35, 6× better
Object precision0.045 raw, 0.28 after the first fine-tune0.47 after the focal-loss fine-tune, recall within three points
ValidationOil-only patches, the easier examTraps included, so the number means what an investigator needs it to mean
OutputA probability mapPolygons in WGS84 with centre, area and confidence, 10 s per scene on CPU
Language"Culprit vessel", "91% chance it is oil""Probable source region", "detection confidence"; scores are uncalibrated and the UI says so

Service and dashboard

The detector runs behind FastAPI: POST /detect takes a GeoTIFF, tiles it, stitches the probability map, applies the threshold and minimum-area filters, and returns GeoJSON; GET / reports the checkpoint, epoch and full validation metrics so any result can be traced to a model version; GET /detect/demo serves a precomputed run so the dashboard works without a GPU. It was deployed on Modal for the hackathon and runs locally now.

The investigation dashboard, React and Leaflet, walks the six stages with map-synced particle clouds and a timeline driven only by backend timestamps, so the animation can never claim a time the model did not compute. The interface never says “culprit”; it says “probable source region”, because that is what the evidence supports. On the demo scene the measured slick area was 266.9 km² against 267.9 km² ground truth, and the centroid error across validation is a median of 85 metres, about eight pixels, which is enough to seed the hindcast.

One error class remains and the demo panel shows it rather than hiding it: of ten demo scenes, nine are clean or correct, and one very dark, structured look-alike still produces confident false positives. Curriculum training on that class is the first thing on the roadmap.

What I would do next

Evaluate the sealed test set exactly once when the design freezes, and publish that number here next to the validation one. Add a per-scene report so a judge can see which traps were rejected and why. Calibrate the confidence scores, so that “0.8” can mean something. Wire the detector back onto Modal with a warm-up so the demo does not cold start.

Learned: Put the look-alikes in the validation set or the number is a lie.

keysKeyboard
j / k
next / previous row
Enter
open the focused row
⌘K or /
search, or ask this site
g then h w a c
go home, work, about, contact
t
toggle light and dark
Esc
close the palette or this map
?
this map