Marshall Frith

Projects

Crop Health From Orbit, Checked to a 25th of a Pixel

Draw a field, pick a Sentinel-2 pass, get NDVI you can walk to. The georectification is proven by six checks, because a misplaced overlay looks perfect.

Draw a polygon on a map, pick a Sentinel-2 pass, and get a georectified NDVI overlay with stats, a histogram, and a GeoTIFF export. From there it scouts the field into five to ten ranked walking waypoints, tracks which weak spots recur across passes, compares two dates, plots the season against last year, and exports the lot as a single-file HTML report that works in aeroplane mode, a KMZ, a GPX, or an ATAK data package.

It runs on your own machine with no account anywhere. The Sentinel-2 archive on AWS reads anonymously, and nothing meters it.

One fact shapes the whole tool

Sentinel-2 arrives as 10 metre pixels inside 110 km granules on a public bucket, readable by HTTP range requests. So the tool never downloads a scene: it reprojects your polygon into the scene's UTM zone, reads only the pixel window that polygon covers, computes the index in native UTM at native resolution, masks cloud, and only then warps the finished picture for display.

The order matters twice over. Computing before warping, because resampling reflectance and then dividing is not the same as dividing and then resampling. And the window is why the AOI has a floor and a ceiling: below 5 km² a STAC query plus two COG opens buys a few hundred pixels, and above 1,500 km² the window stops fitting in memory at native resolution, about 0.4 GB there and 9.6 GB at the 40,000 km² some services advertise. Those services cap pixels too; they just spend them on coverage and hand you 80 m pixels. This tool keeps 10 m and says no instead.

Trust nothing that looks right

Every bug worth writing down here produced output that looked correct.

  • The archive's metadata publishes a reflectance offset of -0.1 and, in a separate field, a flag saying the offset is already applied. Applying the published offset, the obvious reading, double-corrected every pixel: 26.8% went out of range and a healthy field's median NDVI read 0.000. The flag wins.
  • The first ever run returned 4.9% valid pixels on a scene advertising 0.0% cloud. A STAC intersects query happily returns a granule that clips one corner of your area; everything outside its footprint reads as nodata while the cloud figure stays perfect. Coverage under 99% is now dropped before cloud is even consulted.
  • Sen2Cor draws its cloud edge tight, and the ring just outside it measured NDVI 0.471 against 0.530 for clear field. Low, compact, clustered, exactly what the scout hunts, so an unbuffered mask manufactures scouting targets out of haze. The mask is dilated 30 metres.

The georectification itself is guarded by six numbered checks in one script, because a subtly misplaced overlay is invisible in a screenshot. Output GeoTIFF against source COGs at 400 points: max difference 9.7e-08. PNG footprint against the drawn polygon: IoU 0.998, centroid shift 0.39 metres, a 25th of a pixel. The same checks caught the upload path and the satellite path disagreeing by 10 pixels out of 341,254, an off-by-one window pad that no screenshot would ever show. The app also opens on Garden City, Kansas deliberately: centre-pivot circles in a square-mile grid make any alignment error obvious to the naked eye.

Scouting that respects the farmer

Ranking weak areas by area times deficit is the honest metric, and its first run filled all ten stops with the farmstead, the roads, and a fallow quarter the farmer already knows about. Bare ground is off by default now and reported as a fraction instead. Thresholds are relative to the field's own 90th percentile, never absolute, and cropping to a named field first matters: scouting a 1,535 hectare window found ten stops, none in the field of interest; cropping to the 73 hectare field surfaced a 37-acre patch sitting 52% down that the wide scan never listed. Your problem loses to your neighbour's bigger one unless you crop.

Change detection between passes more than 30 days apart gets a warning, because winter wheat being harvested reads as a 0.6 NDVI collapse across a hundred acres and drowns every real problem.

Run it yourself

bash
cd ndvi_scope && .venv/bin/python app.py

Open http://127.0.0.1:8770, draw, pick a pass. The alignment regression is:

bash
.venv/bin/python check_align.py

Two gotchas from the desktop launcher, both of which look like network failures: a process launched from Finder inherits a 256 open-file limit where a shell gets over a million, and panning the map fires about a hundred tile requests, so the app raises its own limit at startup. And quitting the app does not stop the server, deliberately; the PID file under ~/.ndvi-scope does.

What I would tell you before you start: build the alignment checks before the features. Every feature on top of a raster, scouting, persistence, change, inherits the raster's honesty, and a georectification bug passes every unit test while lying to everyone.

Comments

Plain text only. Held for review before it appears.