// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute // SPDX-License-Identifier: GPL-3.0-only #pragma once #include #include #include #include #include // The background noise of a detector that reports ADU rather than counted photons. For a photon // counter a background pixel's variance equals its mean; a CCD's pixel value is a gain times the // photons plus read noise, so its variance is g*mean (+ a constant). The gain is not in the headers // (an SMV file states none, every marCCD file states the writer's default of 1 ADU/photon), so it is // measured: the variance per unit mean of the difference between two pixels two columns apart, which // share the background, but are far enough apart that the detector's point spread does not correlate // them. Bragg peaks and zingers are clipped per level. Taken per level (bins of the pair's mean, a // factor 1.25 wide) and summarised as the median ratio over all pairs, i.e. over the background that // holds most of the pixels. // // The variance/mean of pixels in a neighbourhood is how XDS fills its GAIN table (INIT step, // Kabsch (2010) Acta Cryst. D66, 125-132). namespace background_gain { constexpr int N_BINS = 64; // levels 1.25^0 .. 1.25^63, i.e. up to ~1.3e6 ADU constexpr size_t MIN_PAIRS_PER_BIN = 1000; constexpr size_t MIN_BINS = 3; // Frames of the pre-scan sample it is measured on, spread over the sample. The answer moves by under // 1 % between frames of a sweep, so more only cost time. constexpr size_t GAIN_MAX_FRAMES = 16; // Per level, over the pairs that survived the clip: the count, the sum of the pair means and the sum // of the squared differences. struct Levels { std::array n{}; std::array sum_mean{}; std::array sum_diff2{}; }; // One frame. A pixel takes part if the mask is zero there and 1 <= value < saturation (a CCD reads // its bias in the dark, so a zero is a pixel outside the active area). Levels MeasureFrame(const std::vector &image, size_t width, size_t height, const std::vector &mask, int64_t saturation); // The gain over frames: the pair-weighted median of the per-level variance/mean, or nothing with // fewer than MIN_BINS levels holding MIN_PAIRS_PER_BIN pairs. std::optional Gain(const std::vector &frames); // The gain of a detector whose background resolves single photons (a JUNGFRAU with its pedestal // subtracted): the histogram of pixel values has a peak at zero photons, one at a single photon and // another at two, a gain apart. The variance per unit mean above does not see it on a sparse background - // the empty pixels it leaves out are most of it - so the quantum is read off the histogram instead. The // gain is the spacing of the peaks, not the single photon's value, so a frame whose baseline was never // subtracted - its zero-photon peak well above zero - reads the same gain, and its baseline besides. constexpr size_t PEAK_HISTOGRAM_BINS = 16384; // Adds the frame's unmasked pixels with 0 <= value < min(saturation, PEAK_HISTOGRAM_BINS) to histogram // (PEAK_HISTOGRAM_BINS long, one bin per value). void AddToHistogram(const std::vector &image, const std::vector &mask, int64_t saturation, std::vector &histogram); struct GainEstimate { float gain; // detector units per photon (photon peaks: from the zero-photon to the single-photon peak) float pedestal; // where the zero-photon peak sits; 0 where the baseline was subtracted or no peak shows }; // The zero-photon peak is the highest bin; the single-photon peak the highest past its descent, standing out // of the valley before it, falling to half its height before the midpoint to the next, and repeated by a // two-photon peak near one more gain on. Nothing where the histogram shows no such pair - integer photon // counts, whose single photon is the bin next to the zero peak, or a detector whose noise hides the photons. std::optional PhotonPeak(const std::vector &histogram); // The gain of any input that reports detector units, rotation or stills, from the same frames: the photon // peaks where the histogram shows them, else the variance per unit mean of the levels. std::optional Estimate(const std::vector &frames, const std::vector &histogram); } // namespace background_gain