rugnux: background clip scaled by the CCD gain measured in the pre-scan
The background ring's high-side clip, mean + 4*sqrt(mean), assumes counted photons. A CCD reports ADU: on a Rigaku Saturn a background pixel scatters 4-5x its mean, so the clip sat at ~2 sigma, cut the upper tail of every ring and added a positive offset to every reflection, compressing the intensity statistics (a false 422 on one twinned I41 crystal). No header carries a usable gain (SMV states none, every marCCD file the writer default of 1 ADU per photon), so it is measured: background_gain::MeasureFrame takes the variance per unit mean of the difference of pixels two columns apart, per level, clipped, on every fourth row of up to 16 of the pre-scan's frames; Gain is the pair-weighted median over the levels. The clip becomes mean + n*sqrt(max(g,1)*mean) (BraggIntegrationSettings::bkg_pixel_gain, CPU and GPU engines). Only for SMV/marCCD input (ProcessConfig::counts_in_adu, set from the file format); HDF5 and CBF keep g = 1, the identity. One log line reports the value. No new option. Measured g: Saturn 944+ 4.7-5.2, Saturn 944 2.7, A200 0.68, ADSC ~0.3, marCCD ~0.3 - so the ADSC/marCCD/A200 sets keep the photon clip exactly. Checks (reflection data via gemmi and p.hkl, against the rc175 battery outputs / base build): identical on 11if, 7qis, 8a1a, myob_x10sa, cytc_x10sa (photon counting), 4bwl, 5epe (ADSC, marCCD) and 6cee (A200). Saturn sets, battery baseline -> this change: 3r6o I4122 R_meas 19.3% ISa 4.4 <|L|> .321 -> I41 (correct) 12.1% 8.1 .384 3mc4 R_meas 10.2 -> 9.8 %, ISa 12.1 -> 12.7, d_min 1.79 -> 1.75 A, <|L|> .360 -> .391 5uth 12.6 -> 11.8 %, ISa 10.4 -> 10.1, <|L|> .426 -> .483, offset vs |Fc|^2 0.21 -> 0.00 5vml 10.2 -> 9.6 %, ISa 11.0 -> 11.6, <|L|> .426 -> .475, offset 0.15 -> 0.05 5cc8 8.1 -> 7.9 %, ISa 10.8 -> 11.2, <|L|> .469 -> .494 3meb 14.5 -> 14.6 %, ISa 13.2 -> 13.2, <|L|> .442 -> .457 Space groups unchanged except 3r6o. Pre-scan cost: +0.5 s on a 9 Mpx ADSC sweep, within noise on a 15 Mpx marCCD sweep, nothing on HDF5/CBF. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SVmAWnzCmRKAXVUCdc4iNi
This commit is contained in:
@@ -166,6 +166,17 @@ float BraggIntegrationSettings::GetBackgroundClipNSigma() const {
|
||||
return bkg_clip_nsigma;
|
||||
}
|
||||
|
||||
BraggIntegrationSettings &BraggIntegrationSettings::BackgroundPixelGain(float input) {
|
||||
check_finite("Background pixel gain", input);
|
||||
check_min("Background pixel gain", input, 1.0);
|
||||
bkg_pixel_gain = input;
|
||||
return *this;
|
||||
}
|
||||
|
||||
float BraggIntegrationSettings::GetBackgroundPixelGain() const {
|
||||
return bkg_pixel_gain;
|
||||
}
|
||||
|
||||
BraggIntegrationSettings &BraggIntegrationSettings::BackgroundRadialCorrection(std::optional<bool> input) {
|
||||
bkg_radial_correction = input;
|
||||
return *this;
|
||||
|
||||
@@ -97,6 +97,12 @@ class BraggIntegrationSettings {
|
||||
// engine applies (rugnux --background-clip); the front end picks the default, and rugnux lowers it
|
||||
// to 3 sigma for broadband (non-zero bandwidth) data, where longer spots leak further into the ring.
|
||||
float bkg_clip_nsigma = 4.0f;
|
||||
// Variance of a background pixel per unit of its mean, the "n*sqrt(mean)" of the clip above being
|
||||
// n*sqrt(this * mean). 1 for a detector that counts photons, which is what every HDF5 and CBF input
|
||||
// is taken to be. A CCD reports ADU instead, and on a Rigaku Saturn a pixel scatters 4-5x its mean:
|
||||
// there the photon clip cuts the background's upper tail at about 2 sigma and adds a positive offset
|
||||
// to every reflection. Set by rugnux from the pre-scan for SMV and marCCD input (BackgroundGain.h).
|
||||
float bkg_pixel_gain = 1.0f;
|
||||
// Radial background curvature correction. The signal disk and the background annulus are
|
||||
// concentric, so for ANY background linear in position their means are equal - a plane fit buys
|
||||
// nothing and the leading error is the CURVATURE of the radial background, which the flat annulus
|
||||
@@ -174,6 +180,7 @@ public:
|
||||
BraggIntegrationSettings& Integrator(IntegratorMode input);
|
||||
BraggIntegrationSettings& BackgroundTrimFraction(float input);
|
||||
BraggIntegrationSettings& BackgroundClipNSigma(float input);
|
||||
BraggIntegrationSettings& BackgroundPixelGain(float input);
|
||||
BraggIntegrationSettings& BackgroundRadialCorrection(std::optional<bool> input);
|
||||
BraggIntegrationSettings& MaxHKL(std::optional<int> input);
|
||||
BraggIntegrationSettings& Overlap(OverlapMode input);
|
||||
@@ -194,6 +201,7 @@ public:
|
||||
[[nodiscard]] float GetMinimumSigmaInRegardsToI() const;
|
||||
[[nodiscard]] float GetBackgroundTrimFraction() const;
|
||||
[[nodiscard]] float GetBackgroundClipNSigma() const;
|
||||
[[nodiscard]] float GetBackgroundPixelGain() const;
|
||||
// Unset = auto (gate per image on the smooth-ice score); see bkg_radial_correction.
|
||||
[[nodiscard]] std::optional<bool> GetBackgroundRadialCorrection() const;
|
||||
[[nodiscard]] std::optional<int> GetMaxHKL() const;
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#include "BackgroundGain.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <utility>
|
||||
|
||||
namespace background_gain {
|
||||
|
||||
namespace {
|
||||
constexpr int PAIR_DISTANCE = 2; // columns between the two pixels of a pair
|
||||
constexpr size_t ROW_STEP = 4; // every fourth row: a quarter of a frame is ample for one number
|
||||
constexpr double CLIP_NSIGMA = 4.0;
|
||||
constexpr int CLIP_ROUNDS = 3;
|
||||
const double LOG_BIN = std::log(1.25);
|
||||
}
|
||||
|
||||
Levels MeasureFrame(const std::vector<int32_t> &image, size_t width, size_t height,
|
||||
const std::vector<uint32_t> &mask, int64_t saturation) {
|
||||
const auto usable = [&](size_t i) {
|
||||
return mask[i] == 0 && image[i] >= 1 && image[i] < saturation;
|
||||
};
|
||||
// The level of every usable pair, worked out once; -1 where the pair is not usable.
|
||||
std::vector<int8_t> bin(width * height, -1);
|
||||
for (size_t y = 0; y < height; y += ROW_STEP)
|
||||
for (size_t x = 0; x + PAIR_DISTANCE < width; x++) {
|
||||
const size_t i = y * width + x;
|
||||
if (!usable(i) || !usable(i + PAIR_DISTANCE))
|
||||
continue;
|
||||
const double m = 0.5 * (static_cast<double>(image[i]) + image[i + PAIR_DISTANCE]);
|
||||
bin[i] = static_cast<int8_t>(std::min<int>(N_BINS - 1, static_cast<int>(std::log(m) / LOG_BIN)));
|
||||
}
|
||||
|
||||
// Clip about zero at CLIP_NSIGMA of the level's own scatter, starting from no clip at all: a few
|
||||
// rounds take the Bragg peaks and zingers out without needing any idea of the gain beforehand.
|
||||
std::array<double, N_BINS> limit2;
|
||||
limit2.fill(std::numeric_limits<double>::infinity());
|
||||
Levels out;
|
||||
for (int round = 0; round <= CLIP_ROUNDS; round++) {
|
||||
out = Levels{};
|
||||
for (size_t i = 0; i < bin.size(); i++) {
|
||||
const int b = bin[i];
|
||||
if (b < 0)
|
||||
continue;
|
||||
const double d = static_cast<double>(image[i]) - image[i + PAIR_DISTANCE];
|
||||
if (d * d > limit2[b])
|
||||
continue;
|
||||
out.n[b]++;
|
||||
out.sum_mean[b] += 0.5 * (static_cast<double>(image[i]) + image[i + PAIR_DISTANCE]);
|
||||
out.sum_diff2[b] += d * d;
|
||||
}
|
||||
for (int b = 0; b < N_BINS; b++)
|
||||
if (out.n[b] > 0)
|
||||
limit2[b] = CLIP_NSIGMA * CLIP_NSIGMA * out.sum_diff2[b] / static_cast<double>(out.n[b]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::optional<float> Gain(const std::vector<Levels> &frames) {
|
||||
// (variance/mean, pairs) per level
|
||||
std::vector<std::pair<double, uint64_t>> ratio;
|
||||
uint64_t total = 0;
|
||||
for (int b = 0; b < N_BINS; b++) {
|
||||
uint64_t n = 0;
|
||||
double sum_mean = 0.0, sum_diff2 = 0.0;
|
||||
for (const auto &f : frames) {
|
||||
n += f.n[b];
|
||||
sum_mean += f.sum_mean[b];
|
||||
sum_diff2 += f.sum_diff2[b];
|
||||
}
|
||||
// var(a - b) = 2 var(pixel)
|
||||
if (n >= MIN_PAIRS_PER_BIN && sum_mean > 0.0) {
|
||||
ratio.emplace_back(0.5 * sum_diff2 / sum_mean, n);
|
||||
total += n;
|
||||
}
|
||||
}
|
||||
if (ratio.size() < MIN_BINS)
|
||||
return std::nullopt;
|
||||
// The median over PAIRS, not over levels: the background holds most of the pixels in a few levels,
|
||||
// and the Bragg peaks spread a few pixels over many more - an unweighted median over the levels of
|
||||
// a weak-background marCCD frame lands on the peaks and reads 5.7 where the background reads 0.3.
|
||||
std::sort(ratio.begin(), ratio.end());
|
||||
uint64_t cum = 0;
|
||||
for (const auto &[r, n] : ratio) {
|
||||
cum += n;
|
||||
if (2 * cum >= total)
|
||||
return static_cast<float>(r);
|
||||
}
|
||||
return static_cast<float>(ratio.back().first);
|
||||
}
|
||||
|
||||
} // namespace background_gain
|
||||
@@ -0,0 +1,50 @@
|
||||
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
// 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<uint64_t, N_BINS> n{};
|
||||
std::array<double, N_BINS> sum_mean{};
|
||||
std::array<double, N_BINS> 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<int32_t> &image, size_t width, size_t height,
|
||||
const std::vector<uint32_t> &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<float> Gain(const std::vector<Levels> &frames);
|
||||
|
||||
} // namespace background_gain
|
||||
@@ -115,6 +115,7 @@ BraggIntegrationEngine::BraggIntegrationEngine(const DiffractionExperiment &expe
|
||||
// symmetric trimmed mean (rugnux --background-trim). The caller owns the choice - the engine no
|
||||
// longer overrides it for broadband data.
|
||||
bkg_clip_nsigma = settings.GetBackgroundClipNSigma();
|
||||
bkg_pixel_gain = settings.GetBackgroundPixelGain();
|
||||
bkg_trim = bkg_clip_nsigma > 0.0f ? 0.0f : settings.GetBackgroundTrimFraction();
|
||||
|
||||
// Overlap treatment. Ownership is decided out to the fit grid's half size, which is where the
|
||||
|
||||
@@ -175,6 +175,7 @@ protected:
|
||||
|
||||
double bw_sigma; // bandwidth sigma [dimensionless, * Rpx -> px]
|
||||
float bkg_clip_nsigma; // high-outlier background sigma-clip multiplier (0 = no clip)
|
||||
float bkg_pixel_gain = 1.0f; // background pixel variance per unit mean the clip scales by
|
||||
bool use_ellipse; // radially elongate the per-reflection Gaussian
|
||||
|
||||
double c_radial; // radial variance coefficient of tan^2(2theta): parallax + capture
|
||||
|
||||
@@ -311,8 +311,10 @@ std::vector<Reflection> BraggIntegrationEngineCPU::RunImpl(const Sampler &img,
|
||||
}
|
||||
} else if (do_clip) {
|
||||
// One high-outlier sigma-clip pass on the background ring: reject pixels above
|
||||
// mean + n*sqrt(mean) to strip a neighbour core or a zinger that biases the mean.
|
||||
const double thr = out.bkg + bkg_clip_nsigma * std::sqrt(std::max(out.bkg, 1.0));
|
||||
// mean + n*sqrt(gain*mean) to strip a neighbour core or a zinger that biases the mean.
|
||||
// The gain is 1 for counted photons, so this is then exactly the Poisson clip.
|
||||
const double thr = out.bkg + bkg_clip_nsigma * std::sqrt(static_cast<double>(bkg_pixel_gain)
|
||||
* std::max(out.bkg, 1.0));
|
||||
double s = 0.0;
|
||||
int n = 0;
|
||||
for (int y = y0; y <= y1; ++y)
|
||||
|
||||
@@ -19,6 +19,7 @@ struct BraggGpuParams {
|
||||
float r1_sq, r2, r2_sq, r3, r3_sq;
|
||||
int R, G, GG;
|
||||
float bkg_clip_nsigma; // high-side background sigma-clip multiplier (0 = no clip)
|
||||
float bkg_pixel_gain; // background pixel variance per unit mean (1 = counted photons)
|
||||
int empirical; // ProfileEmpirical vs ProfileGaussian
|
||||
int use_ellipse;
|
||||
float bw_sigma;
|
||||
@@ -284,7 +285,7 @@ __global__ void boxsum(const float *px_x, const float *px_y, const float *dd,
|
||||
if (s_nown < p.minpk * s_ndisk) atomicAdd(counts + COUNT_OVERLAPPED_MAJOR, 1ull);
|
||||
}
|
||||
s_bkg = s_accept ? ((double) (long long) s_bkgsum / (double) s_nbkg) : 0.0;
|
||||
s_thr = s_bkg + (double) p.bkg_clip_nsigma * sqrt(fmax(s_bkg, 1.0));
|
||||
s_thr = s_bkg + (double) p.bkg_clip_nsigma * sqrt((double) p.bkg_pixel_gain * fmax(s_bkg, 1.0));
|
||||
// The pixel is an integer, so comparing it against the floor of the threshold accepts
|
||||
// exactly the same set - and does it with an integer compare instead of a widening
|
||||
// conversion and a double comparison, per ring pixel.
|
||||
@@ -965,6 +966,7 @@ std::vector<Reflection> BraggIntegrationEngineGPU::Run(const ImagePreprocessorBu
|
||||
.r1_sq = r1_sq, .r2 = r2, .r2_sq = r2_sq, .r3 = r3, .r3_sq = r3_sq,
|
||||
.R = R, .G = G, .GG = GG,
|
||||
.bkg_clip_nsigma = mode != IntegratorMode::BoxSum ? bkg_clip_nsigma : 0.0f,
|
||||
.bkg_pixel_gain = bkg_pixel_gain,
|
||||
.empirical = empirical ? 1 : 0,
|
||||
.use_ellipse = use_ellipse ? 1 : 0,
|
||||
.bw_sigma = static_cast<float>(bw_sigma), .c_radial = static_cast<float>(c_radial),
|
||||
|
||||
@@ -11,6 +11,8 @@ ADD_LIBRARY(JFJochBraggIntegration STATIC
|
||||
SystematicAbsence.h
|
||||
SpotWidth.cpp
|
||||
SpotWidth.h
|
||||
BackgroundGain.cpp
|
||||
BackgroundGain.h
|
||||
)
|
||||
|
||||
TARGET_LINK_LIBRARIES(JFJochBraggIntegration JFJochImagePreprocessing JFJochCommon)
|
||||
|
||||
@@ -137,6 +137,12 @@ struct ProcessConfig {
|
||||
// profile the model cannot represent. Ignored where the radii were set by hand.
|
||||
bool adaptive_integration_radius = false;
|
||||
|
||||
// The input's pixel values are ADU, not counted photons (SMV and marCCD: CCDs). The pre-scan then
|
||||
// measures the background noise per unit level and the background clip is scaled by it
|
||||
// (BraggIntegrationSettings::bkg_pixel_gain, background_gain::Gain). Set by the front end from the
|
||||
// file format; HDF5 and CBF input never has it.
|
||||
bool counts_in_adu = false;
|
||||
|
||||
// Rotation two-pass geometry post-refinement (FullAnalysis, rotation only; on by default in the rugnux
|
||||
// CLI, --rotation-no-postrefine disables it). When set, a first pass integrates and post-refines the
|
||||
// detector distance + beam (from the observed spot positions) and the cell scale + rotation axis (from the
|
||||
@@ -754,6 +760,8 @@ class Rugnux {
|
||||
// The recorded spot width the pre-scan measured (config_.adaptive_integration_radius), kept so the
|
||||
// two-pass rotation run measures it once and both passes integrate at the same radius.
|
||||
bool spot_width_measured_ = false;
|
||||
// The background gain (config_.counts_in_adu) is measured once, on the first pre-scan.
|
||||
bool background_gain_measured_ = false;
|
||||
// The pixels the pre-scan's own frames show to be defective (HotPixelFinder) are measured once,
|
||||
// on the pre-scan that also measures the width, and stay in pixel_mask_ for every pass after it.
|
||||
bool hot_pixels_measured_ = false;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "../writer/WriteModel.h"
|
||||
#include "../image_analysis/indexing/SpindleCuspLoss.h"
|
||||
#include "../image_analysis/bragg_integration/SpotWidth.h"
|
||||
#include "../image_analysis/bragg_integration/BackgroundGain.h"
|
||||
#include "../image_analysis/bragg_integration/SpotFootprint.h"
|
||||
#include "../image_analysis/bragg_integration/BraggStencil.h" // BRAGG_FOOTPRINT_NSIGMA
|
||||
#include "../image_analysis/hot_pixels/HotPixels.h"
|
||||
@@ -167,7 +168,9 @@ void Rugnux::PreScan(int start_image, int images_to_process, int frame_count, Ru
|
||||
const auto goniometer = experiment_.GetGoniometer();
|
||||
const bool want_hot = !hot_pixels_measured_
|
||||
&& experiment_.IsRotationIndexing() && goniometer && goniometer->IsScanning();
|
||||
if (!want_shadow && !want_width && !want_hot)
|
||||
// The background noise per unit level of a CCD, read off the same frames (BackgroundGain.h).
|
||||
const bool want_gain = config_.counts_in_adu && !background_gain_measured_;
|
||||
if (!want_shadow && !want_width && !want_hot && !want_gain)
|
||||
return;
|
||||
|
||||
// The sample is taken from the frames given - on a rotation sweep its first part (PrepassEnd) -
|
||||
@@ -335,6 +338,9 @@ void Rugnux::PreScan(int start_image, int images_to_process, int frame_count, Ru
|
||||
const std::vector<int> ordinals(shadow_set.begin(), shadow_set.end());
|
||||
const size_t nworkers = std::min<size_t>(std::max<size_t>(config_.nthreads, 1),
|
||||
std::min(PRESCAN_MAX_WORKERS, ordinals.size()));
|
||||
// Per frame, in sample order, so the gain does not depend on how the workers interleaved.
|
||||
std::vector<background_gain::Levels> gain_levels(want_gain ? ordinals.size() : 0);
|
||||
const size_t gain_frame_stride = (ordinals.size() + background_gain::GAIN_MAX_FRAMES - 1) / background_gain::GAIN_MAX_FRAMES;
|
||||
{
|
||||
std::atomic<size_t> next{0};
|
||||
std::vector<std::future<void>> futures;
|
||||
@@ -342,6 +348,7 @@ void Rugnux::PreScan(int start_image, int images_to_process, int frame_count, Ru
|
||||
for (size_t t = 0; t < nworkers; t++)
|
||||
futures.emplace_back(std::async(std::launch::async, [&] {
|
||||
std::vector<uint8_t> shadow_buffer;
|
||||
std::vector<int32_t> gain_pixels;
|
||||
JFJochReaderRawImage raw_image;
|
||||
for (size_t i = next.fetch_add(1); i < ordinals.size(); i = next.fetch_add(1)) {
|
||||
const int ordinal = ordinals[i];
|
||||
@@ -367,11 +374,56 @@ void Rugnux::PreScan(int start_image, int images_to_process, int frame_count, Ru
|
||||
msg.original_number = image_idx;
|
||||
finder.AddImage(msg, shadow_buffer);
|
||||
}
|
||||
if (want_gain && i % gain_frame_stride == 0) {
|
||||
const auto &img = raw_image.image;
|
||||
const size_t n = img.GetWidth() * img.GetHeight();
|
||||
const uint8_t *ptr = img.GetUncompressedPtr(shadow_buffer);
|
||||
gain_pixels.resize(n);
|
||||
switch (img.GetMode()) {
|
||||
case CompressedImageMode::Int32:
|
||||
std::memcpy(gain_pixels.data(), ptr, n * sizeof(int32_t));
|
||||
break;
|
||||
case CompressedImageMode::Uint16:
|
||||
std::copy_n(reinterpret_cast<const uint16_t *>(ptr), n, gain_pixels.begin());
|
||||
break;
|
||||
case CompressedImageMode::Int16:
|
||||
std::copy_n(reinterpret_cast<const int16_t *>(ptr), n, gain_pixels.begin());
|
||||
break;
|
||||
default:
|
||||
continue; // the CCD readers give 16- or 32-bit integers
|
||||
}
|
||||
gain_levels[i] = background_gain::MeasureFrame(gain_pixels, img.GetWidth(),
|
||||
img.GetHeight(), pixel_mask_.GetMask(),
|
||||
experiment_.GetSaturationLimit());
|
||||
}
|
||||
}
|
||||
}));
|
||||
for (auto &f : futures) f.get();
|
||||
}
|
||||
|
||||
// Applied before anything below copies the integration settings (bragg_before_adaptive_), so every
|
||||
// pass and every fallback integrates with it. At or below 1 - an ADSC or a marCCD, whose pixel
|
||||
// scatters less than its mean - the photon clip is already looser than its nominal width and is left
|
||||
// exactly as it is.
|
||||
if (want_gain) {
|
||||
background_gain_measured_ = true;
|
||||
const auto g = background_gain::Gain(gain_levels);
|
||||
if (g) {
|
||||
BraggIntegrationSettings bis = experiment_.GetBraggIntegrationSettings();
|
||||
bis.BackgroundPixelGain(std::max(1.0f, *g));
|
||||
experiment_.ImportBraggIntegrationSettings(bis);
|
||||
}
|
||||
if (!g)
|
||||
logger.Info("Background gain: not measurable on {} frames; the background clip stays the photon "
|
||||
"clip", (ordinals.size() + gain_frame_stride - 1) / gain_frame_stride);
|
||||
else
|
||||
logger.Info("Background gain: a background pixel scatters {:.2f}x its mean in ADU ({} frames){}",
|
||||
*g, (ordinals.size() + gain_frame_stride - 1) / gain_frame_stride,
|
||||
*g > 1.0f ? fmt::format(" => background clip at mean + {:.1f}*sqrt({:.2f}*mean)",
|
||||
experiment_.GetBraggIntegrationSettings().GetBackgroundClipNSigma(), *g)
|
||||
: std::string(" - at or below 1, the photon clip is kept"));
|
||||
}
|
||||
|
||||
const auto spot_measurement = [&] {
|
||||
prescan_mapping = std::make_unique<AzimuthalIntegrationMapping>(prescan_x, prescan_mask,
|
||||
*azint_geometry_);
|
||||
|
||||
@@ -3030,6 +3030,7 @@ static int RunRugnux(int argc, char **argv) {
|
||||
config.detect_beam_stop = detect_beam_stop;
|
||||
config.beam_center_check = beam_center_check;
|
||||
config.adaptive_integration_radius = adaptive_integration_radius;
|
||||
config.counts_in_adu = input_is_smv || input_is_marccd;
|
||||
config.rotation_postrefine_geometry = rotation_postrefine_geometry;
|
||||
// Measure the spot budget from the data unless the user pinned it.
|
||||
config.measure_spot_budget = !max_spot_count_override.has_value();
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <catch2/matchers/catch_matchers_floating_point.hpp>
|
||||
|
||||
#include <cmath>
|
||||
#include <random>
|
||||
|
||||
#include "../image_analysis/bragg_integration/BackgroundGain.h"
|
||||
#include "../common/BraggIntegrationSettings.h"
|
||||
#include "../common/JFJochException.h"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr size_t W = 512, H = 512;
|
||||
|
||||
// A smooth background of 100..2000 photons per pixel, read out as bias + gain * photons + read noise,
|
||||
// with a grid of bright 5x5 "Bragg" spots and a masked strip.
|
||||
std::vector<int32_t> Frame(double gain, uint32_t seed, std::vector<uint32_t> &mask) {
|
||||
std::mt19937 rng(seed);
|
||||
std::normal_distribution<double> read_noise(0.0, 3.0);
|
||||
std::vector<int32_t> img(W * H);
|
||||
mask.assign(W * H, 0);
|
||||
for (size_t y = 0; y < H; y++)
|
||||
for (size_t x = 0; x < W; x++) {
|
||||
const double photons = 100.0 + 1900.0 * (x + y) / static_cast<double>(W + H);
|
||||
std::poisson_distribution<int> p(photons);
|
||||
double v = (gain > 1.0 ? 20.0 : 0.0) + gain * p(rng) + (gain > 1.0 ? read_noise(rng) : 0.0);
|
||||
if (x % 32 >= 14 && x % 32 < 19 && y % 32 >= 14 && y % 32 < 19)
|
||||
v += 50.0 * gain * photons;
|
||||
img[y * W + x] = static_cast<int32_t>(std::lround(v));
|
||||
if (y >= 200 && y < 210)
|
||||
mask[y * W + x] = 1;
|
||||
}
|
||||
return img;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
TEST_CASE("BackgroundGain_CCD_gain4") {
|
||||
std::vector<background_gain::Levels> frames;
|
||||
std::vector<uint32_t> mask;
|
||||
for (uint32_t seed = 1; seed <= 3; seed++) {
|
||||
const auto img = Frame(4.0, seed, mask);
|
||||
frames.push_back(background_gain::MeasureFrame(img, W, H, mask, 1 << 30));
|
||||
}
|
||||
const auto g = background_gain::Gain(frames);
|
||||
REQUIRE(g.has_value());
|
||||
CHECK_THAT(*g, Catch::Matchers::WithinAbs(4.0, 0.25));
|
||||
}
|
||||
|
||||
TEST_CASE("BackgroundGain_photon_counting") {
|
||||
std::vector<uint32_t> mask;
|
||||
const auto img = Frame(1.0, 7, mask);
|
||||
const auto g = background_gain::Gain({background_gain::MeasureFrame(img, W, H, mask, 1 << 30)});
|
||||
REQUIRE(g.has_value());
|
||||
CHECK_THAT(*g, Catch::Matchers::WithinAbs(1.0, 0.1));
|
||||
}
|
||||
|
||||
TEST_CASE("BackgroundGain_saturated_and_empty") {
|
||||
std::vector<uint32_t> mask;
|
||||
const auto img = Frame(4.0, 11, mask);
|
||||
// Everything at or above the saturation limit: nothing to measure.
|
||||
CHECK_FALSE(background_gain::Gain({background_gain::MeasureFrame(img, W, H, mask, 1)}).has_value());
|
||||
CHECK_FALSE(background_gain::Gain({}).has_value());
|
||||
}
|
||||
|
||||
TEST_CASE("BackgroundGain_settings") {
|
||||
BraggIntegrationSettings s;
|
||||
CHECK(s.GetBackgroundPixelGain() == 1.0f);
|
||||
s.BackgroundPixelGain(4.5f);
|
||||
CHECK(s.GetBackgroundPixelGain() == 4.5f);
|
||||
CHECK_THROWS_AS(s.BackgroundPixelGain(0.5f), JFJochException);
|
||||
}
|
||||
@@ -79,6 +79,7 @@ ADD_EXECUTABLE(jfjoch_test
|
||||
BraggIntegrationEngineGPUTest.cpp
|
||||
BraggIntegrationEngineCompressedImageTest.cpp
|
||||
BraggIntegrationEngineCPUTest.cpp
|
||||
BackgroundGainTest.cpp
|
||||
BraggStencilTest.cpp
|
||||
SpotFootprintTest.cpp
|
||||
LossyFilterTest.cpp
|
||||
|
||||
Reference in New Issue
Block a user