// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute // SPDX-License-Identifier: GPL-3.0-only #pragma once // Per-resolution-ring detection-threshold math shared by the CPU adaptive spot finder // (AdaptiveSpotFinderCPU) and its GPU fused counterpart (AdaptiveSpotFinderGPU). Both engines reduce // every pixel into resolution rings, take a robust per-ring background (mean, sigma), and turn it into // a strong-pixel threshold with the SAME formula - so keeping that formula in one place is what makes // the GPU engine reproduce the CPU one. These are plain host functions (the threshold is computed on // the host in both engines, once per frame, over the small per-ring arrays). #include #include #include namespace adaptive_threshold { // Number of background pixels a ring needs before its own statistics are trusted; sparser rings // (detector corners, heavily masked, innermost) fall back to the whole-frame background. constexpr int64_t MIN_RING_PIXELS = 40; // Detector-level excess-noise floor (photons). Near-zero-background rings scatter MORE than pure // Poisson (charge sharing / read noise / occasional spurious low counts), so a per-ring sigma alone // collapses toward zero on empty high-resolution rings and the threshold would flood. READ is a // photon-scale constant (the same for every dataset -- NOT the per-dataset knob), so the operating // point still self-calibrates through mean and sigma while staying physical where the background // vanishes. constexpr float READ = 1.0f; // Inverse standard-normal CDF (Acklam's rational approximation, ~1e-9 accuracy). Only called once // per frame, so accuracy over speed. inline double NormalQuantile(double p) { if (p <= 0.0) return -40.0; if (p >= 1.0) return 40.0; static const double a[] = {-3.969683028665376e+01, 2.209460984245205e+02, -2.759285104469687e+02, 1.383577518672690e+02, -3.066479806614716e+01, 2.506628277459239e+00}; static const double b[] = {-5.447609879822406e+01, 1.615858368580409e+02, -1.556989798598866e+02, 6.680131188771972e+01, -1.328068155288572e+01}; static const double c[] = {-7.784894002430293e-03, -3.223964580411365e-01, -2.400758277161838e+00, -2.549732539343734e+00, 4.374664141464968e+00, 2.938163982698783e+00}; static const double d[] = {7.784695709041462e-03, 3.224671290700398e-01, 2.445134137142996e+00, 3.754408661907416e+00}; const double plow = 0.02425, phigh = 1.0 - 0.02425; if (p < plow) { double q = std::sqrt(-2.0 * std::log(p)); return (((((c[0]*q+c[1])*q+c[2])*q+c[3])*q+c[4])*q+c[5]) / ((((d[0]*q+d[1])*q+d[2])*q+d[3])*q+1.0); } else if (p <= phigh) { double q = p - 0.5, r = q*q; return (((((a[0]*r+a[1])*r+a[2])*r+a[3])*r+a[4])*r+a[5])*q / (((((b[0]*r+b[1])*r+b[2])*r+b[3])*r+b[4])*r+1.0); } else { double q = std::sqrt(-2.0 * std::log(1.0 - p)); return -(((((c[0]*q+c[1])*q+c[2])*q+c[3])*q+c[4])*q+c[5]) / ((((d[0]*q+d[1])*q+d[2])*q+d[3])*q+1.0); } } // Smallest integer count whose Poisson(mu) upper tail P(X >= k) <= p. This is the correct // significance floor while the background is countable (it carries the sqrt(mu) shot-noise // implicitly, so a bright low-resolution ring gets a high threshold). It DEGENERATES at mu -> 0 // (a single photon on a zero background is "significant"), which is why it is max'd with a // read-noise-floored Gaussian arm by the caller. Short-circuits to Gaussian for large mu. inline float PoissonThreshold(double mu, double p, double z) { if (mu > 50.0) return static_cast(mu + z * std::sqrt(mu)); if (mu < 1e-6) mu = 1e-6; const double target = 1.0 - p; double pmf = std::exp(-mu); double cdf = pmf; int k = 0; while (cdf < target && k < 1000) { ++k; pmf *= mu / k; cdf += pmf; } return static_cast(k + 1); } // A ring's threshold is background mean + z sigmas, computed two ways and max'd: Poisson significance // (correct where the background is countable) floored by a read-noise-aware Gaussian arm (which alone // survives mean -> 0, where Poisson degenerates to "one photon is significant" and would flood the // empty high-resolution rings). p, z are the frame-wide operating point (p = E / N_pixels). inline float RingThreshold(float mean, float sigma, double p, float z) { const float gauss = mean + z * std::sqrt(sigma * sigma + READ * READ); const float poisson = PoissonThreshold(static_cast(mean), p, static_cast(z)); return std::max(gauss, poisson); } } // namespace adaptive_threshold