shoalrun
Finds the rocks in an uncharted Maine lake from aerial imagery, and warns a phone when it is heading at one.
Live. 4,908 candidates published for Millinocket Lake, with an offline phone app that alerts on approach. It is a navigation aid, not a chart. 3,549 of those candidates are unverified, and that figure is a measurement rather than a disclaimer.
Why it exists
Coastal waters are surveyed. Inland Maine lakes are not. There is no chart, and the hazards are learned by hitting them or by being told. Aerial imagery covers the whole lake several times a decade at 0.3 m, and a rock that shows up in flight after flight is a rock.
The output is 4,908 candidates, sorted by what stands behind each one rather than by what it is: 48 confirmed above the waterline and cross-checked at 0.3 m, 1,311 that return near-infrared, which only a dry surface does, and 3,549 that persisted across six flights and mean something nobody has established.
The app draws the first two by default. All of them set off the alarm regardless of what is drawn, because hidden from view is not the same as hidden from the alarm.
The mistake worth reading about
The first version had two classes, shoal and exposed, and it called 150 things shoals. Nate checked them against the lake he grew up on and said they looked like rocks. He was right, and the cause was a resolution limit dressed up as physics.
At 10 metres per pixel, a rock smaller than a pixel can never be classified as land. The pixel's brightness is dominated by the water around it, so a small dry rock can only ever present as brighter water — which is indistinguishable from a shallow bottom. The boundary the classifier had actually learned was bigger than a pixel versus smaller than a pixel, which is the wrong axis entirely, and it stuffed the dangerous invisible-hazard class with things you can see from the boat.
Near-infrared settles it physically. Water absorbs NIR almost completely, so a submerged rock returns the water background while any dry surface in the pixel reflects it. Measured against the original set: 74% of those so-called shoals carried an NIR excess that nothing underwater can produce.
The result that closed a route
The original plan was to read depth straight out of the imagery, which would have mapped the submerged hazards rather than only the ones that break the surface. It does not work on this lake, and that is a measurement rather than a guess: scored against the 260 MDIFW soundings, satellite-derived depth came out at an AUC of 0.507, which is a coin flip. Near-infrared, which cannot physically carry depth information at all, correlated better than the green band did.
The water is too stained. The photons carry no bottom signal, so submerged hazards are not reliably mapped here and, from imagery, cannot be. A second route was tried and also measured null: photograph the rocks in air at low water and infer depth from the level difference. Every aerial flight of this lake is July to September, and a regulated lake is held near full pool right through the recreation season, so no flight exists at low water.
That is why the app labels 72% of its own map unverified instead of quietly shipping it. The two things it does show with confidence are rocks somebody has confirmed, and water a boat has already driven through without hitting anything.
The trap in the robustness check
What nearly shipped was a threshold sweep that looked like a robustness check and could not have caught the actual error. It moved one cutoff across every flight together, so a per-flight radiometric bias slid straight through it untouched.
The test that caught it was permuting the input order, which has nothing to do with thresholds. The shuffled control beat the real ordering 49 times out of 50. Always run the shuffle.
Reading untrusted bytes
The sonar-log parser reads binary files produced by consumer fishfinders — a format with no guarantees and every incentive to lie about its own record counts. It is covered by property tests that throw arbitrary bytes at it, so a malformed log fails loudly instead of being coerced into a plausible-looking depth reading.