Two changes to the stills integration background, both stills-only (rotation keeps its path bit for bit): 1. The r2..r3 background ring's outer radius is measured per run. The pre-scan lays apertures on a grid over its frames, away from every found spot, and for each candidate r3 (r2+2 .. 14) takes the mean square difference between the r1 disk mean and the clipped ring mean. With nothing in the aperture that is exactly the background error the ring makes (noise + bias); the run keeps the minimum. On a photon-counting pixel detector the noise wins and the shipped r3=14 is kept; on a binned CCD the background structure wins and the narrowest ring is taken. r1 stays 6: a smaller disk (4,6,14) costs 0.0013 dimple R_free on a JUNGFRAU set, so it does not pass the photon-counting gate. --adaptive-integration-radius now defaults on for stills too (=off restores the fixed 6/8/14). The pre-scan bandwidth estimate is not applied to stills. 2. The radial background-curvature correction on stills takes its curve from the adaptive spot finder's peak-excluded per-ring background (whole frame) instead of the predicted reflections' own ring pixels, and auto applies it to every stills image (no ice-score gate: a solvent/carrier hump curves the background as much as ice). New engine call RadialBackgroundFromRings; CPU, GPU and Metal read the handed-over curve. The GPU box sum now sizes the radial bin index separately from accumulating, so a handed-over curve is read at the right radius. Measured (dimple R_free, deposited free set, battery arguments, all frames unless noted): pal86 (Rayonix 4x4, 156 um): 0.354 -> 0.218 (ring 10; at 4,6,10 0.213) lcls222 (Rayonix 4x4, 177 um): 0.205 -> 0.196 (ring 10) sacla236 (MPCCD): 0.2588 -> 0.2558 (ring 11) alvra213_int16 (JUNGFRAU): 0.1986 -> 0.1986 (ring 14, unchanged) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015CeEGgpKdfkCTJb3YNMJcu
73 lines
2.7 KiB
C++
73 lines
2.7 KiB
C++
// 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 <cmath>
|
|
#include <random>
|
|
|
|
#include "../image_analysis/bragg_integration/BackgroundAperture.h"
|
|
#include "../common/JFJochMath.h"
|
|
|
|
namespace {
|
|
|
|
constexpr int W = 600, H = 600;
|
|
|
|
// A background of `level` counts a pixel with Poisson noise, modulated by `ripple` (relative) with a
|
|
// period of 20 px along x - the structure a ring further from the disk reads wrong.
|
|
ImagePreprocessorBuffer Frame(double level, double ripple, uint32_t seed) {
|
|
std::mt19937 rng(seed);
|
|
ImagePreprocessorBuffer img(static_cast<size_t>(W) * H);
|
|
for (int y = 0; y < H; y++)
|
|
for (int x = 0; x < W; x++) {
|
|
std::poisson_distribution<int> p(level * (1.0 + ripple * std::sin(2.0 * PI * x / 20.0)));
|
|
img[static_cast<size_t>(y) * W + x] = p(rng);
|
|
}
|
|
return img;
|
|
}
|
|
|
|
int BestR3(double level, double ripple) {
|
|
const auto r3 = background_aperture::CandidateR3(8.0f, 14.0f);
|
|
background_aperture::Tally tally;
|
|
for (uint32_t seed = 1; seed <= 4; seed++) {
|
|
auto img = Frame(level, ripple, seed);
|
|
background_aperture::MeasureFrame(img, W, H, {}, 6.0f, 8.0f, r3, 4.0f, 1.0f, tally);
|
|
}
|
|
REQUIRE(tally.apertures > 100);
|
|
return static_cast<int>(r3[background_aperture::Best(tally)]);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("BackgroundAperture_CandidateR3", "[Integration]") {
|
|
const auto r3 = background_aperture::CandidateR3(8.0f, 14.0f);
|
|
REQUIRE(r3.size() == 5);
|
|
CHECK(r3.front() == 10.0f);
|
|
CHECK(r3.back() == 14.0f);
|
|
}
|
|
|
|
// A few counts a pixel on a flat background: the noise wins, and the widest ring is kept.
|
|
TEST_CASE("BackgroundAperture_FlatNoisyKeepsWidestRing", "[Integration]") {
|
|
CHECK(BestR3(0.5, 0.0) == 14);
|
|
}
|
|
|
|
// Thousands of counts a pixel on a background that varies over the stencil's own scale: the structure
|
|
// wins, and the narrowest ring is kept.
|
|
TEST_CASE("BackgroundAperture_StructuredBrightTakesNarrowestRing", "[Integration]") {
|
|
CHECK(BestR3(2000.0, 0.05) == 10);
|
|
}
|
|
|
|
// An aperture over an unreadable pixel is not used.
|
|
TEST_CASE("BackgroundAperture_SkipsUnreadablePixels", "[Integration]") {
|
|
const auto r3 = background_aperture::CandidateR3(8.0f, 14.0f);
|
|
auto img = Frame(1.0, 0.0, 7);
|
|
background_aperture::Tally all;
|
|
background_aperture::MeasureFrame(img, W, H, {}, 6.0f, 8.0f, r3, 4.0f, 1.0f, all);
|
|
for (size_t i = 0; i < static_cast<size_t>(W) * H; i += 97)
|
|
img[i] = INT32_MIN;
|
|
background_aperture::Tally masked;
|
|
background_aperture::MeasureFrame(img, W, H, {}, 6.0f, 8.0f, r3, 4.0f, 1.0f, masked);
|
|
CHECK(masked.apertures == 0);
|
|
CHECK(all.apertures > 0);
|
|
}
|