// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute // SPDX-License-Identifier: GPL-3.0-only #pragma once #include #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 (~12 photons on one serial-stills set, ~5 on a // weaker one). // // 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 at ~12 photons on the first // set and ~5 on the weaker one 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 // Exact integers: a preprocessed pixel is an int32 and the sentinels are skipped, so v and v*v // are exact in 64 bits. That is what lets the GPU engine reproduce these bit for bit - integer // addition is associative, so its block atomics can arrive in any order. std::vector ring_sum; std::vector ring_sum2; std::vector ring_cnt; std::vector ring_mean; std::vector ring_sigma; std::vector ring_thr; // ring_mean of the last Detect(), NaN where the ring holds too few pixels to be its own background. // Kept separately because ring_mean carries the previous frame's value for an empty ring. std::vector ring_bkg; void AccumulateRings(const ImagePreprocessorBuffer &image, float clip_k); public: explicit AdaptiveSpotFinderCPU(const AzimuthalIntegrationMapping &mapping); void Detect(const ImagePreprocessorBuffer &image, const SpotFindingSettings &settings) override; [[nodiscard]] const std::vector &GetRingBackground() const override { return ring_bkg; } };