Skip to content

smi_core.FindROI issue: multiple ROI candidates generated for single emitter #21

Open

Description

@dschodt

It seems that smi_core.FindROI is (sometimes) generating multiple boxes for the same emitter in a single frame.

Example where this does happen:

SMD = smi_core.SingleMoleculeData.createSMD();
SMD.X = 16;
SMD.Y = SMD.X;
SMD.FrameNum = 1:numel(SMD.X);
SMD.Photons = 1e3 * ones(size(SMD.X));
SMD.Bg = zeros(size(SMD.X));
SMD.YSize = 32;
SMD.XSize = 32;
SMD.NFrames = numel(SMD.X);
SMD.NDatasets = 1;
SMD.PSFSigma = 1.3;
[Model] = smi_sim.GaussBlobs.gaussBlobImage(SMD);
SMF = smi_core.SingleMoleculeFitting;
SMF.Fitting.PSFSigma = mean(SMD.PSFSigma);
LD = smi_core.LocalizeData(Model, SMF);
[SMD, SMDPreThresh] = LD.genLocalizations();

Looking at smi_core.LocalizeData.genLocalizations() line 30 there are two ROIs output from FindROI.findROI()

Example where this doesn't happen:

SMD = smi_core.SingleMoleculeData.createSMD();
SMD.X = 8;
SMD.Y = SMD.X;
SMD.FrameNum = 1:numel(SMD.X);
SMD.Photons = 1e3 * ones(size(SMD.X));
SMD.Bg = zeros(size(SMD.X));
SMD.YSize = 16;
SMD.XSize = 16;
SMD.NFrames = numel(SMD.X);
SMD.NDatasets = 1;
SMD.PSFSigma = 1.3;
[Model] = smi_sim.GaussBlobs.gaussBlobImage(SMD);
SMF = smi_core.SingleMoleculeFitting;
SMF.Fitting.PSFSigma = mean(SMD.PSFSigma);
LD = smi_core.LocalizeData(Model, SMF);
[SMD, SMDPreThresh] = LD.genLocalizations();

Activity

  1. kalidke commented on Dec 9, 2021

    @kalidke
    Member

    The box finder uses the operation pixel==maxpixel from a maximum pixel filter. In rare cases, much more likely in noise-free model data, there can be two identical pixels. Boxes will be returned for both. We should implement a localization combiner algorithm somewhere in LocalizeData.

  2. kalidke commented on Nov 4, 2025

    @kalidke
    Member

    Issue Status: Still Exists (Edge Case Only)

    Verified the issue still exists in current codebase (v1.3.1), but only in noise-free simulation data.

    Test Results

    Test Case Input Output Status
    32×32 noiseless 1 emitter 2 ROIs ✗ Duplicates
    16×16 noiseless 1 emitter 1 ROI ✓ Correct
    3 emitters noiseless 3 emitters 6 ROIs ✗ Duplicates
    32×32 with Poisson noise 1 emitter 1 ROI ✓ Correct

    Root Cause Confirmed

    As noted in the original comment:

    "The box finder uses the operation pixel==maxpixel from a maximum pixel filter. In rare cases, much more likely in noise-free model data, there can be two identical pixels."

    When simulated Gaussian blobs create perfectly symmetric intensity profiles on a discrete pixel grid, multiple pixels can have identical maximum values, causing FindROI to identify multiple candidate positions.

    Why It Doesn't Affect Real Data

    1. Real camera data always has noise (Poisson shot noise, readout noise)
    2. Breaking symmetry: Even small noise breaks the perfect symmetry that causes identical pixel values
    3. Test with Poisson noise passed: Single emitter → single ROI detected

    Recommendation

    Keep open as low-priority enhancement with labels:

    • enhancement - Not a critical bug
    • simulation - Only affects simulation workflows
    • good first issue - Solution is well-defined

    Proposed Solutions

    From original comment: "We should implement a localization combiner algorithm somewhere in LocalizeData."

    Option 1: Post-processing deduplication

    • In LocalizeData.genLocalizations(), merge localizations within distance threshold
    • Check if X, Y positions are identical or within ~0.5 pixels
    • Keep only one, average the parameters

    Option 2: FindROI-level fix

    • After identifying local maxima, check for duplicate positions
    • Remove duplicates before creating ROI boxes
    • More efficient but requires modifying FindROI algorithm

    Option 3: Add jitter to simulations

    • In GaussBlobs.gaussBlobImage(), add small random offset to positions
    • Prevents perfect pixel symmetry in simulations
    • Doesn't fix root cause but prevents issue in testing

    Impact Assessment

    • Severity: Low - doesn't affect real experimental data
    • Frequency: Rare - only noise-free simulations
    • Workaround: Add poissrnd() to simulated data
    • Fix effort: Medium - requires deduplication logic

    The issue is real but has minimal practical impact on actual SMLM workflows.

  3. kalidke commented on Nov 4, 2025

    @kalidke
    Member

    Fix Implemented ✓

    Pull request #52 has been created to fix this issue.

    Solution: Post-Processing Deduplication

    Implemented intelligent duplicate merging in LocalizeData after GaussMLE fitting:

    • New method: mergeDuplicateLocalizations()
    • Location: Called in genLocalizations() after coordinate transformation
    • Logic: Merges localizations < 0.1 pixels apart in same frame, keeping higher LogLikelihood
    • Approach: Post-fix (MATLAB) rather than prevention (CUDA kernel)

    Why Post-Fix?

    • No CUDA recompilation needed
    • Easy to test and maintain
    • Intelligent merging based on fit quality
    • Handles any source of duplicates

    Test Results

    All 5 comprehensive tests pass:

    • ✓ Original bug case (32×32): 2 duplicates → 1 merged
    • ✓ Multiple emitters: 6 duplicates → 3 merged
    • ✓ Preserves distinct emitters (8 pixels apart)
    • ✓ Multi-frame handling works correctly
    • ✓ No false positives on normal data

    Root Cause Confirmed

    When Gaussian blob centered at integer pixel coordinates (e.g., 16.0, 16.0), creates 2×2 block of identical pixel values:

    (15,15) = 77.9090042114
    (15,16) = 77.9090042114  ← IDENTICAL (to 10 decimals)
    (16,15) = 77.9090042114
    (16,16) = 77.9090042114
    

    CUDA kernel tolerance (< 1e-6) flags all four, creating 4 ROI boxes (reduced to 2 after fitting).

    See PR #52 for full implementation details and testing documentation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions