The offline CPU spot finder marks a pixel strong when it clears a fixed photon
count AND a local-window SNR. The fixed photon floor forces per-dataset tuning:
its sweet spot tracks the background level (weak sets want a low threshold,
strong or high-background sets a high one) and the usable window is narrow, so
users hand-tune --spot-threshold/--spot-sigma per dataset.
Add an opt-in --adaptive-spots mode (AdaptiveSpotFinderCPU) that replaces the
fixed floor with a per-resolution-ring threshold derived from each image's own
noise. Per ring it computes a peak-excluded background mean and sigma (one plain
pass + two sigma-clip passes over the assembled photon image, binned by the
azimuthal-integration ring index) and sets
thr = max( PoissonTail(mean, p), mean + z * sqrt(sigma^2 + read^2) )
with p = false_pixels_per_frame / n_pixels the single portable knob (default
100) and z = Phi^-1(1 - p). The Poisson arm is the correct significance where
the background is countable (it carries the sqrt(mean) shot noise, so a bright
low-resolution ring gets a high threshold); the read-noise-floored Gaussian arm
keeps the threshold physical where the background vanishes (empty high-resolution
rings), without which those rings flood. read is a detector-level constant, not
a per-dataset knob. Both arms are needed: Poisson alone floods near-zero
background, Gaussian alone drops the shot-noise term and under-thresholds bright
rings.
One --adaptive-spots setting then adapts across a wide range of serial datasets
with no per-dataset threshold, matching or beating hand-tuned thresholds and the
peakfinder8/xgandalf reference on both weak large-cell and strong serial data,
with equal merged R-free.
The finder runs on the CPU (offline/viewer path) and reads the host image, which
the GPU pipeline already keeps in sync, so it works in either build. The default
(non-adaptive) path and the online/FPGA path are unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
45 lines
2.1 KiB
C++
45 lines
2.1 KiB
C++
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#pragma once
|
|
|
|
#include <vector>
|
|
|
|
#include "ImageSpotFinder.h"
|
|
#include "SpotFindingSettings.h"
|
|
#include "../../common/AzimuthalIntegrationMapping.h"
|
|
|
|
// Self-calibrating strong-pixel detector for the offline (rugnux/viewer) path.
|
|
//
|
|
// The classic finder (ImageSpotFinderCPU) marks a pixel strong when it clears a *fixed* photon
|
|
// count AND a local-box SNR. The fixed photon floor is what forces per-dataset tuning: it must sit
|
|
// above the background (wants high) yet not bury weak spots (wants low), and the background level
|
|
// differs per dataset, so the sweet spot is narrow (KR2 ~12 photons, weak OCP ~5).
|
|
//
|
|
// Here the floor is replaced by a per-resolution-ring threshold derived from a single portable
|
|
// number: E = the expected count of noise pixels tolerated per frame (default ~100). For a ring
|
|
// whose (peak-excluded) background mean is mu, the threshold is the smallest count whose Poisson
|
|
// upper tail is <= p = E / N_pixels, max'd with a Gaussian arm mu + z*sigma to absorb read/flat-field
|
|
// excess. Because it is set from the image's own noise, the SAME E lands ~12 photons on KR2 and ~5
|
|
// on OCP with no user input. Detection then is simply value > ring_threshold, fed to the same
|
|
// connected-component builder as the classic finder.
|
|
class AdaptiveSpotFinderCPU : public ImageSpotFinder {
|
|
const AzimuthalIntegrationMapping &mapping;
|
|
|
|
// per-ring scratch, sized to the mapping's bin count
|
|
std::vector<double> ring_sum;
|
|
std::vector<double> ring_sum2;
|
|
std::vector<int64_t> ring_cnt;
|
|
std::vector<float> ring_mean;
|
|
std::vector<float> ring_sigma;
|
|
std::vector<float> ring_thr;
|
|
|
|
void AccumulateRings(const ImagePreprocessorBuffer &image, float clip_k);
|
|
|
|
public:
|
|
explicit AdaptiveSpotFinderCPU(const AzimuthalIntegrationMapping &mapping);
|
|
std::vector<DiffractionSpot> Run(const ImagePreprocessorBuffer &image,
|
|
const SpotFindingSettings &settings,
|
|
const std::vector<bool> &res_mask) override;
|
|
};
|