Skip to content

Add edge fill options for Spliner's padding margin - #73

Merged
HazenBabcock merged 1 commit into
ZhuangLab:masterfrom
HazenBabcock:edge-fill
Nov 4, 2026
Merged

HazenBabcock merged 1 commit into
ZhuangLab:masterfrom
HazenBabcock:edge-fill

Conversation

@HazenBabcock

Copy link
Copy Markdown
Member

Summary

A new Spliner parameter, opt-in, for how the padding margin around each frame is filled. mirror stays the default.

<edge_fill type="string">planar</edge_fill>          <!-- mirror (default) | planar | edge -->
<edge_fill_weight type="float">30.0</edge_fill_weight> <!-- margin pixel = 1/30 of a real pixel -->
 PeakFinderFitter.analyzeImage
   loadImage
-    padArray(frame, margin)                       # always mirror
+    peak_finder.padImage(frame)                   # mirror | planar plane fit | edge copy
   peak_finder.newImage
+  peak_fitter.setScmosTerm(marginVariance(image)) # planar/edge: (k - 1) x fill in the margin
   peak_fitter.newImage
   for each iteration
     backgroundEstimator
-      bg_filter.convolve(image)
+      bg_filter.convolve | PlanarBackground.estimate | realPixelsBackground
  • What the fills are. planar is a Gaussian-weighted plane (first-order normalized convolution, done with a MatchedFilterBank). edge copies the outermost pixels straight out.
  • What changes with either fill:
    • The background comes from the real pixels only.
    • The margin stays in the fit at 1/k weight. That weight goes in through the C fitter's sCMOS term (MultiFitter.setScmosTerm(), a ctypes write, with no C changes).
  • Scope. Only ParametersSplinerSTD has the parameter. The deconvolution finders keep mirroring.

Evidence

The 2016 SMLM challenge's AS-Exp N1 movies, Spliner alone, Jaccard:

  • Before (mirror): N1.LD 0.9194, N1.HD 0.7345. On N1.LD it finds only 49-77% of the defocused emitters 450-900 nm from the edge (813 and 519 emitters on the two kinds of side).
  • After:
    • planar: 0.9542 and 0.7638.
    • edge: 0.9540 and 0.7703.
    • Recall of those same edge emitters rises to 87-96%.

On 320 x 256 frames with a real cell's background (about twice the frame median along one edge):

  • planar keeps the mirror's false positives at the bright edge (53 against 52 on N1.LD); edge adds some (121).
  • On 64 x 64 and 320 x 256 frames, planar costs 1.3-1.4x the mirror's time per frame, interleaved on one core. edge costs nothing.

Tests:

  • test_edge_fill.py (new): 6 passed. It covers:
    • the edge copy;
    • the plane fit: exact on a plane wherever a fit window reaches, and a plain Gaussian filter in the interior;
    • parameter checks;
    • the margin variance reaching the C fitter each frame;
    • Spliner runs with each fill.
  • The full suite: 294 passed.
  • On test_spliner.dax, the default and an explicit mirror give byte-identical localizations to master.
  • Both fills give byte-identical localizations to the prototype the numbers above come from (with fftw_estimate 1).

Merge Danger

Door: two-way

The parameter is opt-in, the default path gives byte-identical output, and there are no C or file-format changes.

Blast Radius: small

PeakFinderFitter.loadImage() and loadBackgroundEstimate() now pad through peak_finder.padImage(), and analyzeImage() reads peak_finder.edge_fill. Every finder in the repository provides both. fitting.PeakFinder has them (mirror unless the parameter is set), and so does the Spliner deconvolution finder, which gained them in this PR. A custom finder outside the repository that doesn't inherit from fitting.PeakFinder, used with fitting.PeakFinderFitter, would need the same two additions.

🤖 Generated with Claude Code

Spliner pads each frame with a margin and fits the padded image. The
margin is filled with a mirror image of the frame, so an emitter near
the edge is fitted next to its own reflection, and defocused emitters
within about a micron of the edge are often missed. On the 2016 SMLM
challenge's astigmatic N1 movies the mirror finds 60% and 44% of the
defocused emitters 450-900 nm from the edge (LD and HD).

The new edge_fill parameter (ParametersSplinerSTD) offers two other
fills, alongside the default 'mirror':
- 'planar' fills the margin with a plane fitted to the nearby pixels,
  weighted by a Gaussian of background_sigma (first-order normalized
  convolution, PlanarBackground).
- 'edge' copies the frame's outermost pixels straight out.
With either, the background is estimated from the frame's own pixels
only, and the margin stays in the fit with each pixel counting for
1/edge_fill_weight (default 30) of a real one. That weight goes in as
a per-pixel variance through the C fitter's sCMOS term, which is now
set before each frame (MultiFitter.setScmosTerm()). Leaving the margin
out of the fit entirely made edge fits unstable.

On the challenge's N1 movies both fills raise the Jaccard index by
0.03 to 0.036. On frames the size of the real Tubulin-A647-3D movie,
with its background (about twice the frame median along one edge), they
raise edge recall, but 'edge' adds false positives at the bright edge
and 'planar' doesn't. With the analysis interleaved on one core,
'planar' takes 1.3 to 1.4 times as long per frame as the mirror on
64x64 and 320x256 pixel frames, and 'edge' costs nothing extra.

The default is unchanged: on the Spliner test movie the default and
an explicit 'mirror' give byte-identical localizations to master.
The two fills reproduce the prototype they were tested as exactly
(with fftw_estimate so the FFT plans are deterministic). The
deconvolution finders only mirror.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@HazenBabcock
HazenBabcock merged commit 75db8e1 into ZhuangLab:master Nov 4, 2026
5 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