// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute // SPDX-License-Identifier: GPL-3.0-only #pragma once #include #include #include #include "../../common/DetectorModules.h" #include "../../common/DiffractionGeometry.h" #include "../../common/DiffractionSpot.h" #include "../image_preprocessing/ImagePreprocessorBuffer.h" #include "RingBackground.h" // How far out the background ring should reach, measured on apertures with nothing in them. // // The ring mean stands in for the background under the signal disk. A wider ring averages more pixels, // so its own noise falls; but it also reaches further from the reflection, so whatever the background // does between the disk and the ring - a curved solvent ring, the tile structure of a fibre-optic CCD, // the tails of reflections nobody predicted - enters the intensity as a bias. Which of the two wins is // a property of the detector and the sample, not a constant: on a pixel detector counting a fraction of // a photon per pixel the noise wins and the widest ring is best, on a binned CCD reading hundreds of // ADU a pixel the structure wins and the narrowest is. // // So it is measured. Apertures are laid on a grid over pre-scan frames, away from every spot found; on // each, the mean of the r1 disk and the mean of the r2..r3 ring are taken for every candidate r3, and // the run keeps the r3 whose ring predicts the disk best - the smallest mean square of the difference. // With nothing in the aperture the difference is exactly the error the ring makes in the background of // a reflection: its noise plus its bias. The disk's own noise is the same for every candidate, so it // does not move the choice. The ring is clipped as the integrator clips it (high side, at // mean + nsigma * sqrt(gain * mean)). // // The same apertures also say whether the radial background-curvature correction helps: each difference // is taken a second time with the correction applied as the integrator applies it - the ring's clip // following the frame's own ring background (RingBackground), and that background averaged over the // ring's pixels minus over the disk's, which is what the integrator's kernel takes off a reflection. // Where the background really is a function of the radius the corrected ring predicts the disk better; // where it is not - a strongly polarised beam, panels of different response - it predicts it worse, and // the run measures which. On stills the ring is chosen on the plain differences first, so its reach does // not rest on the radial model; the correction is then kept where it improves that ring. Rotation measures // every stencil the spot width can give it, and reads the one it gets. namespace background_aperture { // The squared disk-minus-ring differences, summed per candidate r3 over every aperture measured: plain, // and with the radial correction (counted separately: a frame without a ring background adds none). struct Tally { std::vector sum_d2, sum_d2_radial; // Sum of the squared per-aperture change the correction makes to d^2, for the standard error of // the paired comparison (RadialZ). std::vector sum_dd2; size_t apertures = 0, apertures_radial = 0; void Add(const Tally &other); // The mean square difference of each candidate, plain or corrected; empty where nothing was measured. [[nodiscard]] std::vector MeanSquare(bool radial) const; // How many standard errors the correction lowers candidate k's mean square by, over the apertures // measured both ways (positive: the correction helps). [[nodiscard]] double RadialZ(size_t k) const; }; // A signal disk r1, the inner edge of its ring r2, and the candidate outer edges r3. struct Stencil { float r1 = 0.0f, r2 = 0.0f; std::vector r3; }; // The candidate outer radii: from r2 + 2 up to max_r3, one pixel apart. [[nodiscard]] std::vector CandidateR3(float r2, float max_r3); // The apertures of one frame, appended to `tally`. Centres lie on a grid over the frame, and an aperture // is used only where every pixel out to the largest candidate is readable and no found spot is within // that radius plus SPOT_CLEARANCE_PX of its centre. `rings` is this frame's ring background, or empty; // `geometry` and `modules` place the pixels on the plane to read it at their resolution. void MeasureFrame(ImagePreprocessorBuffer &image, int width, int height, const std::vector &spots, float r1, float r2, const std::vector &r3, float clip_nsigma, float gain, const RingBackground &rings, const DiffractionGeometry &geometry, const DetectorModules &modules, Tally &tally); // Whether the radial correction makes candidate k's ring predict the disk better - its mean square // difference lower by at least the fraction min_gain - on apertures that were all measured both ways. [[nodiscard]] bool RadialHelps(const Tally &tally, size_t k, double min_gain = 0.0); // The part of the background error the correction must take away on a rotation run. A still has no other // correction, so any gain is taken there; a rotation run already corrects the frames whose ice score shows // a smooth ring, and the frame's curve on every frame replaces that only for a clear gain. Measured: on // photon-counting sweeps the apertures gain 0.1-5% (z 3-4) and every image corrected costs ISa and // anomalous signal; where it pays - home-source CCD rings, a smooth ice ring - they gain 13-31%. constexpr double ROTATION_RADIAL_MIN_GAIN = 0.10; // The candidate with the smallest mean square difference, or -1 where nothing was measured. [[nodiscard]] int Best(const std::vector &mean_square); constexpr float SPOT_CLEARANCE_PX = 4.0f; // At most this many apertures a frame: a 16 Mpx detector would otherwise lay twenty thousand. constexpr size_t MAX_APERTURES_PER_FRAME = 2000; } // namespace background_aperture