From c76304d8995a0fd000a3ffa6cdba768ff26a7fb7 Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Wed, 4 Nov 2026 14:42:30 +0100 Subject: [PATCH] Integration: the shadow-clip test reads only each row's span of shadow pixels DropShadowClipped scanned the whole signal disk of every predicted reflection in the mask, ~10% of all cycles of a rotation run. The analysis now takes, once per object, the first and last beam-stop pixel of each mask row (ShadowRows), and the test reads only that part of the disk's rows; no shadow pixel lies outside it, so the same reflections are dropped. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UqXYdL4qBGYRiSXSGXfCLu --- image_analysis/MXAnalysisWithoutFPGA.cpp | 18 +++++++++++++++--- image_analysis/MXAnalysisWithoutFPGA.h | 5 ++++- tests/BraggIntegrationEngineCPUTest.cpp | 2 +- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/image_analysis/MXAnalysisWithoutFPGA.cpp b/image_analysis/MXAnalysisWithoutFPGA.cpp index b5a576bef..f6bdee7e1 100644 --- a/image_analysis/MXAnalysisWithoutFPGA.cpp +++ b/image_analysis/MXAnalysisWithoutFPGA.cpp @@ -48,6 +48,8 @@ MXAnalysisWithoutFPGA::MXAnalysisWithoutFPGA(const DiffractionExperiment &in_exp indexer(in_indexer), prediction(CreateBraggPrediction(experiment.IsRotationIndexing())), mask(in_mask), + shadow_rows(ShadowRows(in_mask.GetMask(), static_cast(in_experiment.GetXPixelsNumConv()), + static_cast(in_experiment.GetYPixelsNumConv()))), mask_high_res(-1), mask_low_res(-1) { #ifdef JFJOCH_USE_CUDA @@ -535,7 +537,7 @@ std::vector MXAnalysisWithoutFPGA::IntegrateOnImage(const std::vecto size_t npredicted, int64_t image_number) { const auto &modules = experiment.GetModules(); const auto drop_shadow_clipped = [this](std::vector &reflections) { - DropShadowClipped(reflections, mask.GetMask(), static_cast(experiment.GetXPixelsNumConv()), + DropShadowClipped(reflections, mask.GetMask(), shadow_rows, static_cast(experiment.GetXPixelsNumConv()), static_cast(experiment.GetYPixelsNumConv()), experiment.GetBraggIntegrationSettings().GetR1()); }; if (modules.empty()) { @@ -584,15 +586,25 @@ std::vector MXAnalysisWithoutFPGA::IntegrateOnImage(const std::vecto // does not end where the background's does, so the rest of such a disk is attenuated too. Measured against // their symmetry mates, these observations came out 12-60 % low on the sweeps whose shadow edge crosses // busy low-resolution reflections (CCD and pixel detectors alike), and level with them elsewhere. +std::vector> ShadowRows(const std::vector &mask, int W, int H) { + std::vector> rows(H, {W, -1}); + for (int y = 0; y < H; y++) + for (int x = 0; x < W; x++) + if (mask[static_cast(y) * W + x] & (1u << PixelMask::BeamStopPixelBit)) + rows[y] = {std::min(rows[y].first, x), x}; + return rows; +} + void DropShadowClipped(std::vector &reflections, const std::vector &mask, - int W, int H, float r1) { + const std::vector> &shadow_rows, int W, int H, float r1) { std::erase_if(reflections, [&](const Reflection &r) { const int x0 = std::max(0, static_cast(std::floor(r.predicted_x - r1))); const int x1 = std::min(W - 1, static_cast(std::ceil(r.predicted_x + r1))); const int y0 = std::max(0, static_cast(std::floor(r.predicted_y - r1))); const int y1 = std::min(H - 1, static_cast(std::ceil(r.predicted_y + r1))); for (int y = y0; y <= y1; y++) - for (int x = x0; x <= x1; x++) { + // Only the part of the row between its first and last shadow pixel can hold one. + for (int x = std::max(x0, shadow_rows[y].first); x <= std::min(x1, shadow_rows[y].second); x++) { const float dx = static_cast(x) - r.predicted_x, dy = static_cast(y) - r.predicted_y; if (dx * dx + dy * dy < r1 * r1 && (mask[static_cast(y) * W + x] & (1u << PixelMask::BeamStopPixelBit))) return true; diff --git a/image_analysis/MXAnalysisWithoutFPGA.h b/image_analysis/MXAnalysisWithoutFPGA.h index 27cdff2e5..8d3e0708a 100644 --- a/image_analysis/MXAnalysisWithoutFPGA.h +++ b/image_analysis/MXAnalysisWithoutFPGA.h @@ -29,8 +29,10 @@ class AdaptiveSpotFinderGPU; // Leave out the reflections whose signal disk - radius r1 about the predicted position, in image // coordinates - reaches a pixel of the beam-stop shadow (PixelMask::BeamStopPixelBit of `mask`, a // width x height image). +// `shadow_rows` is ShadowRows of the same mask: per row, the first and last x of a shadow pixel. void DropShadowClipped(std::vector &reflections, const std::vector &mask, - int width, int height, float r1); + const std::vector> &shadow_rows, int width, int height, float r1); +std::vector> ShadowRows(const std::vector &mask, int width, int height); // MXAnalysisWithoutFPGA is not thread safe - it has to owned by a single thread class MXAnalysisWithoutFPGA { @@ -81,6 +83,7 @@ class MXAnalysisWithoutFPGA { bool count_crowding = false; std::unique_ptr preprocessor_buffer; const PixelMask &mask; + const std::vector> shadow_rows; // ShadowRows of mask // Decompress the image into decompression_buffer (or read it straight from the message, when it is // not compressed) and return where it landed. diff --git a/tests/BraggIntegrationEngineCPUTest.cpp b/tests/BraggIntegrationEngineCPUTest.cpp index 46d2f30b5..845f73072 100644 --- a/tests/BraggIntegrationEngineCPUTest.cpp +++ b/tests/BraggIntegrationEngineCPUTest.cpp @@ -565,7 +565,7 @@ TEST_CASE("DropShadowClipped_SignalDiskTouchingTheShadow", "[Integration][portab at(33.9f, 5.0f), // 3.9 px on the other side: dropped at(34.0f, 5.0f), // 4 px, the disk's own edge: kept at(1.0f, 1.0f)}; // near the image corner: kept - DropShadowClipped(refl, mask, W, H, r1); + DropShadowClipped(refl, mask, ShadowRows(mask, W, H), W, H, r1); REQUIRE(refl.size() == 4); CHECK(refl[0].predicted_x == 10.0f); CHECK(refl[1].predicted_x == 25.0f);