Skip to content

Stitching artifact correction: fix inverted background mask + add evaluation/sample tooling (WIP)#86

Open
satra wants to merge 5 commits into
lincbrain:james/spool_zarrfrom
satra:fix/stitching-artifact-correction
Open

Stitching artifact correction: fix inverted background mask + add evaluation/sample tooling (WIP)#86
satra wants to merge 5 commits into
lincbrain:james/spool_zarrfrom
satra:fix/stitching-artifact-correction

Conversation

@satra

@satra satra commented Jun 2, 2026

Copy link
Copy Markdown

Summary

Work toward correcting LSM stitching artifacts (intensity seams + stripe/shadow artifacts) on james/spool_zarr, evaluated on real sub-MF283 data. This PR contributes a bug fix, evaluation + sample tooling, and a characterized baseline. The full artifact correction is in progress (see "Remaining work") — opening now to share the fix and findings and to get input on a domain question.

What's included

  • Fix: inverted background-threshold mask (convert_spool_or_zarr.py). The background suppression was data * (data < threshold), which keeps background and zeroes foreground signal. Changed to data * (data >= threshold), gated by background_threshold (default None → behavior unchanged). Found by inspection — please confirm the intent. The existing test_lsm_stitch.py golden test still passes.
  • Evaluation harness (scripts/eval/): evaluate_stitch.py computes a seam-discontinuity metric, a stripe-prominence metric (along Y, the per-(z,y)-line direction the stripe correction targets), and in-strip preservation; compare.py checks before/after reduction gates.
  • Sample extraction (scripts/sample/extract_sample.py): crops a few adjacent strips (full Y, cropped Z/X) from the real strips into a small OME-Zarr sample for fast iteration. Extracted data is gitignored (restricted) — only the script is committed.
  • Investigation notes (docs/stitching_artifact_correction_notes.md): baseline metrics and findings.

Baseline findings (3 strips, slice036, tissue region X≈12000–16000)

  • Seam discontinuity 4.03; stripe prominence (Y) 22.1.
  • Per-strip body means 127.8 / 204.2 / 265.3 — strips get progressively brighter. The visible seam is a whole-strip brightness offset, not a local edge (the raised-cosine blend already smooths the local transition).

Open question for maintainers

Is the progressive inter-strip brightness ramp an illumination artifact to normalize, or real signal? This determines whether equalization belongs in the stripe/white-matter-normalization path (my current hypothesis) vs. elsewhere. A naive in-blend gain-match of overlap means overcorrects (compounding) and was not included.

Remaining work (not in this PR)

  • Per-strip illumination normalization for seams (validate ≥90% seam reduction).
  • mip.py writes {name}.tiff but stripes.py reads {name}_proc-mip.tiff — the mip → stripes chain is broken; fix before producing a stripe before/after.
  • Harden stripe correction maps (fully-masked (z,y) lines currently hit a 9999999 sentinel → wm/corr ≈ 0 → black lines); add logged fallback.
  • Stripe suppression validation (≥75% prominence reduction) + full-slice run + tests.

🤖 Generated with Claude Code

satra and others added 3 commits June 2, 2026 14:54
…rection

- scripts/sample/extract_sample.py: crop 3 adjacent strips (full Y, Z/X cropped)
  from real strips_raw OME-Zarr into scratch for fast iteration (FR-014).
- scripts/eval/{evaluate_stitch,compare}.py: seam-discontinuity + stripe-prominence
  + in-strip metrics with SC-002/003/004 gates.
- .gitignore: never commit extracted samples / eval outputs (restricted data).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…otes

- convert_spool_or_zarr.py: background_threshold mask was data*(data<threshold),
  which kept background and zeroed foreground (silent data loss). Now
  data*(data>=threshold), gated by background_threshold (default unchanged).
- scripts/eval/evaluate_stitch.py: stripe prominence now measured along Y (the
  per-(z,y)-line direction the correction targets), not X.
- docs/stitching_artifact_correction_notes.md: baseline metrics on a representative
  tissue sample + investigation findings (seam = whole-strip brightness offset;
  naive in-blend gain equalization regressed and was reverted; mip->stripes naming
  mismatch blocks the stripe path).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…eduction

- R5: mip.py writes {name}_proc-mip.tiff (what stripes.py reads); drop the
  slice0->slice rewrite that corrupted names like sample-slice036.
- stripes.py: harden correction-map generation. Empty (no-foreground) (z,y) lines
  no longer become the 9999999 sentinel (-> black holes after white_matter/corr);
  filled with the tile's finite-line median and LOGGED (sample chunk-0001: 49% of
  lines). Add Y-smoothing (smooth_y, Gaussian) so the noisy per-line map does not
  inject new stripes. Robust volume read (np.asarray(reader[:])).
- convert_spool_or_zarr.py: guard stripe-correction apply against non-finite values
  (log + set to 1.0), never silently zero/NaN data.
- scripts/eval/evaluate_stitch.py: seam metric now measures the strip-body brightness
  step across seams (the blend turns the junction into a gradient); stripe metric
  measures prominence along Y.

Result on the real-data sample: seam-body step 126.6% -> 13.6% (~89% reduction, SC-002).
Stripe prominence ~neutral (needs the FFT-notch destripe / re-targeted metric). See
docs/stitching_artifact_correction_notes.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@satra

satra commented Jun 2, 2026

Copy link
Copy Markdown
Author

Update: mip→stripes→stitch chain now works; ~89% seam reduction

Pushed a follow-up commit implementing the illumination-normalization path and hardening:

  • R5 fixedmip.py writes {name}_proc-mip.tiff; stripes.py reads it (removed the slice0slice rewrite that corrupted sample-slice036sample-slice36). The mip → stripes → stitch chain runs end-to-end.
  • Stripe-map hardening — empty (no-foreground) (z,y) lines no longer become the 9999999 sentinel (→ white_matter/corr ≈ 0 → black lines); filled with the tile's finite-line median and logged. On the eval sample, chunk-0001 had 49% empty lines — these would have been black stripes. Added a finiteness guard on apply.
  • Map Y-smoothing (smooth_y) — the per-line map is noisy (many fallback lines); smoothing keeps the illumination falloff but avoids injecting new stripes.
  • Corrected seam metric — measures the strip-body brightness step across seams (the raised-cosine blend turns the junction into a gradient, so a local row-jump metric missed it).

Result on the real-data sample (white_matter_intensity=200, smooth_y=25) vs. --blend-only baseline:

metric baseline after change
seam-body step (% of mean) 126.6% 13.6% −89% (≈ SC-002 target)
stripe prominence (Y) 22.1 23.9 ≈ neutral

Still open: stripe suppression (SC-003) is ~neutral — the per-line flat-field removes the low-frequency falloff that drives the seam but not the mid-frequency stripe prominence; needs a dedicated FFT-notch destripe (or a re-targeted metric). Also: adjacent strips' shared-overlap foreground medians disagree ~2.5× (153 vs 379), i.e. strong within-strip Y illumination falloff — so a single per-strip gain can't fix it (confirmed). Question still stands: is this variation purely illumination (normalize away) or partly real signal? That gates how aggressive to be.

The existing test_lsm_stitch.py golden test still passes. Details in docs/stitching_artifact_correction_notes.md.

satra and others added 2 commits June 2, 2026 16:17
…ndings

- convert_spool_or_zarr.py + stitch.py: optional correction_clip (default 0.0=off)
  bounds the per-line stripe-correction gain to [clip, 100-clip] percentiles so
  low-signal/background lines are not amplified into horizontal bands.
- docs: spectral + visual analysis shows there is NO narrow periodic stripe in the
  sample (broadband energy ~40px, low-freq dominated), so an FFT-notch is inappropriate
  and was deliberately not implemented. The residual banding is introduced by the
  multiplicative per-line correction on background; the right fix is a foreground-gated/
  additive correction (model change, needs maintainer intent). Seam fix (~89%) stands.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ing)

foreground_gated=True applies corrected = bg + (data - bg) * correction (bg = per-tile
5th percentile or background_level param), so the per-line gain normalizes signal above
background while leaving background flat. This removes the horizontal banding the blanket
multiplicative correction introduced in low-signal regions, while preserving output scale
(no ~5x amplification) and reducing stripe prominence (22.1->20.8). Visually the cleanest
result: strips equalized, tissue preserved, no background bands. Default False (no behavior
change). Wired through stitch.convert. See docs/stitching_artifact_correction_notes.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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