Trainable segmentation: paint, train, and label images, movies and 4-D scans - #154
Open
CSSFrancis wants to merge 2 commits into
Open
CSSFrancis wants to merge 2 commits into
CSSFrancis wants to merge 2 commits into
Conversation
The core of the quarry branch (#117 / #122), rebuilt on main in 3k lines instead of 34k: paint a few strokes, train a linear per-pixel classifier over a small feature bank, and label every field — one image, each frame of an in-situ movie, or the real-space navigator of a 4-D scan, where a region is a set of scan positions. spyde/segmentation/ is the compute package (no session, no plots): features (separable Gaussian bank + a 4x-decimated sigma=25 background reference), labels (scribbles as flat indices with a stroke id, so every stroke weighs the same in the fit), classifier (one 1x1-conv head, trained on the stroke bounding boxes only, predicted band by band with a halo, logits smoothed), instances (boundary-stroke route = connected components + a flat-elevation watershed to give the seam back; otherwise the distance-transform watershed), measure (scikit-image regionprops, calibrated once), fields (one accessor for the three dataset shapes, reading one frame at a time through spyde.drift.frame_source). Regions(RaggedStore) is the result table: one row per instance indexed by field, with `space` recording whether a pixel is an image pixel or a scan position. The label movie is a lazy view recomputed from the classifier, the way a drift-corrected node is warped — nothing is stored. What was left behind, deliberately: the 1,300-line numba region-property kernels (scikit-image does it; numba was never a declared dependency and miscompiles on two Python versions), the two dead engines, tracking, the overlay editor, the table dock, the dual-lane batch dispatcher, the autolabel test door, the nm/px slider conversions. A band and a training crop now start on the decimation grid and the background reference resamples by its exact factor, so a band reproduces the full field bit for bit; without that, training saw features that prediction never produced. Verified in the app (electron/tests/segment_wizard.spec.ts): a real Shift+drag paints, Train overlays the mask, Run opens the label movie with a regions-per-frame navigator.
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.
The core of #117 / #122, rebuilt on
main: trainable segmentation that runs on a single image, on every frame of an in-situ movie, and on the real-space navigator of a 4-D scan (where a region is a set of scan positions). ~3k lines including tests, against the quarry's 34k.Drift correction, the other half of #117, already landed as #130; nothing here touches it beyond reusing
spyde.drift.frame_sourceto read frames.What is in
spyde/segmentation/— the compute package. Nothing in it knows about sessions or plots, and nothing materialises a stack.features.pylabels.pyclassifier.pyinstances.pymin_sizefloormeasure.pyregionprops_table, calibrated once; intensity stats over finite pixels only (drift borders)fields.pyspyde/signals/regions.py—Regions(RaggedStore): one row per instance, indexed by field.spacerecords whether a pixel is an image pixel or a scan position, so per-region diffraction on a 4-D scan is well-defined later. The label movie is not stored: it is a lazy view recomputed from the classifier, the way a drift-corrected node is warped.spyde/actions/segment_action.py+SegmentWizard.tsx— the caret: class strip (particle / background / boundary / erase), brush size, Train, Run; the split parameters behind Advanced. Strokes go anyplotlib brush →pointer_up→ label store; a trained classifier retrains on every new stroke and repaints its mask. One result door for every shape (open_result_tree),tree.regionsattached throughattach_container.What was left behind, deliberately
props/contours/hull/intensity): scikit-image does it, numba was never a declared dependency, and feat(seg): the fast segmentation engine, non-rigid drift, and a test suite that can fail #122's own CI note says it miscompiles on cp310/cp311 and macOSscribble.pyScribbleClassifier,scribble_cnn.py), the classical engine remnants, the SAM bootstrapautolabeltest door, the nm↔px slider layerTwo things the reviewers flagged and this PR adopts: the container is named
Regionsrather thanParticles(right for grains on a scan and for a still image;lifecycle.wait_for_particleshad no callers and is nowwait_for_regions), and the per-stroke weighting the quarry documented but never implemented (hasattr(store, "stroke_ids")was always False) is real here.Two findings worth your eyes
Band / crop alignment. The decimated background reference only equals its full-field value if a band or a training crop starts on the decimation grid and the upsample uses the exact factor. The quarry did neither, so training saw features prediction never produced. Pinned by
test_a_band_reproduces_the_full_field_exactly(array_equal, notallclose) andtest_a_stroke_crop_features_match_the_full_field.Mask dilation on small particles. On the synthetic movie (particles r = 3–9 px, soft 1 px edge) the classifier's mask is larger than the disc, and the logit smoothing widens it further — measured area / true area at frame 0:
Every particle is still found within 0.6 px and the unpainted faint probe is found from what the classifier learnt. The σ = 3 default is the quarry's, measured on real 15 px / CNR 0.17 data where it halved fragmentation; the fixture is the opposite regime. Finer discriminative scales (0.7, 1.4, 2.8) bring the fixture to 1.25. I kept the real-data defaults and left both as constructor parameters, not caret controls — this wants a measurement on your data before moving.
Verification
test_segmentation.py(25),test_regions.py(2),test_segment_wizard.py(11, movie / image / 4-D navigator through the real brush widget), conformance + schema + drift + lifecycle files green,tsccleanelectron/tests/segment_wizard.spec.ts— a real Shift+drag paints (the class picked on the caret is the one painted), Train overlays the mask, Run opens the label movie with a regions-per-frame navigator. Screenshots below.