Skip to content

feat: area cap, Cellpose tuning knobs, and the scale-dependence finding - #13

Merged
leiverkus merged 3 commits into
mainfrom
feat/max-area-and-cellpose-knobs
Jul 26, 2026
Merged

feat: area cap, Cellpose tuning knobs, and the scale-dependence finding#13
leiverkus merged 3 commits into
mainfrom
feat/max-area-and-cellpose-knobs

Conversation

@leiverkus

Copy link
Copy Markdown
Owner

Three commits, all driven by looking at real output from the Tiberias trench in QGIS. Two are small code changes; the third records what they revealed.

STRUCTURA_MAX_AREA — an upper bound on polygon area

Every backend emits a background mask, and with no cap it reaches the output and swamps the layer.

The diagnosis is measured, not guessed. SAM's largest polygon on the full trench was 634 m² — its bounding box matches the raster extent to 0.000 m on all four edges, five vertices in the exterior ring, 1790 holes punched out where it had segmented something else. It is one whole-image mask.

It is explicitly not a tiling artefact: only 2 of 814 SAM polygons have two or more edges on the 512 px tile grid, and none is near-rectangular by area/bbox. The Otsu watershed does the same thing at 29 m².

With the cap at 5 m² the watershed run goes from 109 features to 108, and its largest survivor from 419.8 m² to 0.139 m². The filter sits in geo.mask_to_polygons, so it applies to all three backends.

Cellpose tuning is reachable from configuration

STRUCTURA_CELLPOSE_DIAMETER, _CELLPROB, _FLOW, _MODEL.

cellprob_threshold did not exist on the segmenter at all. diameter existed but — like min_area before it and checkpoint in the SAM backend — was never passed by make_segmenter. That is the fourth instance of the same failure mode this week: a parameter the library accepts, that Structura does not forward, invisible until someone runs it on real data.

STRUCTURA_CELLPOSE_MODEL points at a fine-tuned checkpoint. The motivating case is ImageGrains 2.0 (Mair et al. 2026, CC-BY-4.0) — Cellpose-SAM fine-tuned on sediment grains, which the paper's own literature guide calls "Cellpose for stones". Its IG2_full_set_cp_SAM weights load unchanged (verified: CellposeModel on cuda:0, 304.6 M parameters). .env.example warns that the record's 26 MB checkpoints are Cellpose-2 U-Nets and will not load into Cellpose 4, so nobody downloads the wrong file.

Both pretrained_model and SAM's checkpoint are now omitted when unset rather than passed as None, with tests in the module that runs without the heavy extras.

ADR-0002 — the finding

diameter was assumed to be a sensitivity knob. It is a scale selector.

shared only in A only in B of A recovered
auto (30 px) vs 60 px 151 177 254 46 %
auto vs 120 px 130 198 403 40 %

Not boundary jitter: relaxing IoU from 0.5 to 0.3 moves the auto-vs-120 overlap from 130 to 136. What is lost is systematically smaller — the 198 instances found only at 30 px have a median of 511 cm² against 1042 cm² for the shared set, p10 of 40 cm² against 272.

The union across four diameters holds 856 objects against 533 for the best single run — 61 % more than any one setting can produce. So no default diameter is set; there is no value that dominates.

The pretrained_model axis behaves the same way and more strongly, and reverses direction: raising the diameter takes cpsam_v2 from 328 to 533 objects but takes ImageGrains from 3137 down to 1090. Two models of the same architecture responding oppositely to the same knob is what a band selector looks like.

The record proposes a multi-scale pass mirroring dem.relief.multiscale_relief, which already does this for the 2.5D track, and is explicit that the merge needs containment handling rather than IoU dedup alone — which is why 856 is an upper bound on distinct detections, not a count of stones.

Consequence for v0.9: the evaluation has to state which scale configuration it scores, or it measures the setting as much as the model.

What this is not

No ground truth, so "more objects" is not "better" anywhere above. Nothing here bears on H_A.

Verification

ruff clean, mypy clean on 26 files, 64 tests passing (was 58). The area cap and the knob threading are each covered by a test that runs without the sam/cellpose extras — the blind spot that hid the SAM checkpoint bug.

🤖 Generated with Claude Code

leiverkus and others added 3 commits July 26, 2026 18:13
Both found by looking at real output in QGIS.

STRUCTURA_MAX_AREA. Every backend emits a background mask, and with no
upper bound it reaches the output. On the Tiberias trench SAM produced a
single polygon of 634 m² — the full raster rectangle, matching the extent
to 0.000 m on all four edges, with 1790 holes punched out where it had
segmented something else. The Otsu watershed does the same at 29 m². This
is not a tiling artefact: only 2 of 814 SAM polygons touch the 512 px tile
grid and none is rectangular; it is one whole-image mask. With the cap at
5 m² the watershed run drops from 109 features to 108 and its largest
survivor from 419.8 m² to 0.139 m².

Cellpose knobs. `cellprob_threshold` did not exist on the segmenter at
all, and it is the recall knob — the one that matters when the backend
returns few but correct instances, which is what a real trench showed
(328 objects at a median of 834 cm², against SAM's 814). `diameter`
existed but was never passed by `make_segmenter`, the same failure mode
as `min_area` last time: a parameter that looks configurable and is not.

Optional numeric settings read through a shared `_opt_float`, so an unset
*and* a blanked line in `.env` both mean "no value" — otherwise
`STRUCTURA_MAX_AREA=` would crash on `float("")`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
STRUCTURA_CELLPOSE_MODEL points CellposeModel at custom weights. Unset
keeps Cellpose's generalist default (cpsam_v2), so behaviour is unchanged.

The motivating case is ImageGrains 2.0 (Mair et al. 2026,
zenodo.org/records/15728186, CC-BY-4.0): Cellpose-SAM fine-tuned on
sediment grains in orthophotos — the paper's literature guide calls it
"Cellpose for stones". Its `IG2_full_set_cp_SAM` checkpoint is the same
architecture as ours and loads unchanged; verified on the GPU host,
CellposeModel on cuda:0 with 304.6 M parameters. The record's 26 MB
checkpoints are Cellpose-2 U-Nets and will not load into Cellpose 4 —
noted in .env.example so nobody downloads the wrong file.

`pretrained_model` is omitted from the constructor call when unset rather
than passed as None. That is the same mistake that kept SamSegmenter from
ever running, so it gets the same shape of test, in the module that runs
without the heavy extras.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`diameter` was assumed to be a sensitivity knob. Measured on the Tiberias
M10 extent it is a scale *selector*: raising it from Cellpose's own
estimate (~30 px) to 120 px lifts the count from 328 to 533, but matching
the instances with structura.metrics shows only 130 in common — 40 % of
the original recovered. Relaxing IoU from 0.5 to 0.3 moves that to 136,
so these are different objects, not the same ones drawn differently.

What is lost is systematically smaller: the 198 instances that exist only
at 30 px have a median of 511 cm² against 1042 cm² for the shared set,
and a p10 of 40 cm² against 272.

The union across four diameters holds 856 objects against 533 for the
best single run — 61 % more than any one setting can produce. So there is
no "correct" diameter to pick, and no default is set. The record proposes
a multi-scale pass instead, mirroring dem.relief.multiscale_relief, which
already does this for the 2.5D track, and notes that the merge needs
containment handling rather than IoU dedup alone.

The pretrained_model axis behaves the same way and more strongly:
ImageGrains 2.0 returns 3130 objects at a median of 138 cm² where the
generalist cpsam_v2 returns 393 at 866 cm². Both are Cellpose-SAM, so the
fine-tuning material moves the band rather than raising recall.

Consequence for v0.9: the evaluation has to state which scale
configuration it scores, or it measures the setting as much as the model.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@leiverkus
leiverkus merged commit 8fca32c into main Jul 26, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant