feat: area cap, Cellpose tuning knobs, and the scale-dependence finding - #13
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 areaEvery 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_thresholddid not exist on the segmenter at all.diameterexisted but — likemin_areabefore it andcheckpointin the SAM backend — was never passed bymake_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_MODELpoints 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". ItsIG2_full_set_cp_SAMweights load unchanged (verified:CellposeModelon cuda:0, 304.6 M parameters)..env.examplewarns 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_modeland SAM'scheckpointare now omitted when unset rather than passed asNone, with tests in the module that runs without the heavy extras.ADR-0002 — the finding
diameterwas assumed to be a sensitivity knob. It is a scale selector.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_modelaxis 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
ruffclean,mypyclean on 26 files, 64 tests passing (was 58). The area cap and the knob threading are each covered by a test that runs without thesam/cellposeextras — the blind spot that hid the SAM checkpoint bug.🤖 Generated with Claude Code