Emergent-seam cut-cell detection for calculate_tiling_qc (dense tissue / wide gaps / single-sided cuts) - #1255
Open
timtreis wants to merge 3 commits into
Open
Emergent-seam cut-cell detection for calculate_tiling_qc (dense tissue / wide gaps / single-sided cuts)#1255timtreis wants to merge 3 commits into
timtreis wants to merge 3 commits into
Conversation
…ing_qc The MAD-based `is_outlier` gate flags cells with unusually straight boundaries, but in dense tissue with a wide inter-FOV gap -- and when a cut leaves only one segmentable half -- that signal is swamped: most real cut cells are missed and interior cells are flagged instead (recall ~0.1 on a dense/wide-gap synthetic). Add a complementary, geometry-only detector (`detect_seams=True`, on by default) that needs neither the FOV size nor tile overlap and works on single-sided cuts: 1. per cell, find the longest cardinal flat boundary run, keeping only runs with a wide background gap beyond them (a real cut faces a gap; a dense-tissue facet faces a neighbour ~1px away); 2. seam bands emerge as the coordinates where many such edges align, with the band width read off from the peak spread (recovers the seam grid from the data -- no known FOV size); 3. flag cells whose edge lies on and faces a seam as `is_seam_cut`. New: `SeamDetectionParams`, `detect_seams` / `seam_params` args, obs columns `is_seam_cut` / `seam_dist`, and `uns["tiling_qc"]["seams"]`. Existing columns and behaviour are unchanged (additive). On a dense + wide-gap + single-sided synthetic fixture, `is_seam_cut` reaches F1 ~0.85 (recall ~0.95) where `is_outlier` sits at F1 ~0.2. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the absolute-pixel thresholds (which would not transfer across resolution / cell size / FOV pitch) with data-derived ones, and fix two recall/precision failures found on real CosMx breast data: - All length thresholds are now fractions of the data's own length scale D (median cell equivalent diameter); flat_tol / bin_width remain small pixel-grid constants (rasterisation / histogram resolution), which are genuinely resolution-independent. - Keep ALL cardinal flat edges per cell (dominant-edge-only dropped a wide cell's on-seam cut edge in favour of an off-seam facet), and find each side's dominant flat *line* at any coordinate (not just the extreme), so a partial cut plateau on a large cell is captured. - Two stages: (1) locate seams from wide-gap edges only -- a lone straight membrane between two touching cells has a ~1px gap and is excluded, so it cannot define a seam; (2) flag any cell with a cardinal edge on a detected seam, gap no longer required, catching partial-edge and close-gap two-sided cuts. The wide-gap threshold derives from the observed membrane width. - Seam peaks must be strong relative to the strongest peak on their axis (rejects stray off-grid membranes whether there is one seam or many). On a real breast 2x2 subset this recovers only the true seam (no spurious seams) with better seam-concentration than before, and on the dense/wide-gap/ single-sided synthetic keeps recall ~0.97-1.0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
assign_stitch_groups gated candidates on the MAD `is_outlier` flag, which fires tissue-wide in dense tissue -> facing-edge pairing merged touching interior cells far from any FOV seam. Add a `candidates` argument (default "auto") that gates on `is_seam_cut` when present -- cells whose straight edge lies on a detected FOV seam -- so the candidate pool is seam-localized and pairs form across the seam, not in FOV interiors. Falls back to `is_outlier` when seam detection wasn't run. Also fixes a variable-shadowing bug in calculate_tiling_qc where the new SeamScale object was bound to `scale`, overwriting the multi-scale level argument recorded in uns (broke assign_stitch_groups on multi-scale labels). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #1255 +/- ##
==========================================
+ Coverage 77.24% 77.69% +0.45%
==========================================
Files 63 64 +1
Lines 9378 9587 +209
Branches 1579 1617 +38
==========================================
+ Hits 7244 7449 +205
- Misses 1532 1536 +4
Partials 602 602
🚀 New features to boost your workflow:
|
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.
Motivation
calculate_tiling_qc'sis_outliergate flags cells whose boundary is unusually straight (MAD oncut_score). On dense tissue with a wide inter-FOV gap — and when a cut leaves only one segmentable half (common in CosMx: the other half is too small to segment in the adjacent FOV) — that signal is swamped:On a dense + wide-gap + single-sided synthetic fixture with ground truth,
is_outlierrecalls only ~10% of true cut cells (F1 ≈ 0.2), andassign_stitch_groupsrecovers ≈ 0 (it merges touching interior cells instead).What this adds
A complementary, geometry-only detector (
detect_seams=True, on by default) that needs neither the FOV size nor tile overlap, and works on single-sided cuts:is_seam_cut.API (additive, backward-compatible)
calculate_tiling_qc:detect_seams: bool = True,seam_params: SeamDetectionParams | Mapping | None = None.SeamDetectionParamsdataclass (exported fromsquidpy.experimental.tl).is_seam_cut(bool),seam_dist(px, NaN where not flagged).uns["tiling_qc"]["seams"].cut_score,is_outlier, …) and default behaviour are unchanged. Settingdetect_seams=Falsereproduces the old output exactly.Integrated into the existing tiled/lazy path (per-cell edges are computed inside
_score_tilewith the tile origin threaded through; the seam histogram is aggregated globally), so it scales the same way and is validated to detect seams across tile boundaries.Results (ground-truth synthetic, dense + single-sided)
is_outlierF1is_seam_cutF1Recall is a stable 0.94–0.96 across gaps/seeds (vs 0.06–0.27), and the detected seam-band width auto-adapts to the true gap. On a real CosMx breast 2×2-FOV subset the detector recovers the true seam at the correct coordinate with no FOV size given, and its flags are ~3× better localized to the seam than
is_outlier.Tests
tests/experimental/test_seam.py(unit tests for edge extraction / seam detection / flagging + integration on a new dense-seam ground-truth fixture inconftest.py).is_seam_cutrecall ≫is_outlieron the hard fixture, seams recovered near the true border,detect_seams=Falsereproduces the old schema, and param validation.test_tiling_qc.py/test_tiling.py/test_tiling_stitch.pypass unchanged.Notes / follow-ups
assign_stitch_groupsconsumeuns["seams"]to restrict pairing to same-seam facing cells (removing the interior-cell false merges).🤖 Generated with Claude Code