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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UqXYdL4qBGYRiSXSGXfCLu
This commit is contained in:
leonarski_fandClaude Opus 5.5 committed 2026-11-04 17:35:21 +01:00
1 parent 79a80f086c
commit c76304d899
3 files changed
+20 -5

No files matched your search

+15 -3
View File
@@ -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<int>(in_experiment.GetXPixelsNumConv()),
static_cast<int>(in_experiment.GetYPixelsNumConv()))),
mask_high_res(-1),
mask_low_res(-1) {
#ifdef JFJOCH_USE_CUDA
@@ -535,7 +537,7 @@ std::vector<Reflection> 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<Reflection> &reflections) {
DropShadowClipped(reflections, mask.GetMask(), static_cast<int>(experiment.GetXPixelsNumConv()),
DropShadowClipped(reflections, mask.GetMask(), shadow_rows, static_cast<int>(experiment.GetXPixelsNumConv()),
static_cast<int>(experiment.GetYPixelsNumConv()), experiment.GetBraggIntegrationSettings().GetR1());
};
if (modules.empty()) {
@@ -584,15 +586,25 @@ std::vector<Reflection> 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<std::pair<int, int>> ShadowRows(const std::vector<uint32_t> &mask, int W, int H) {
std::vector<std::pair<int, int>> rows(H, {W, -1});
for (int y = 0; y < H; y++)
for (int x = 0; x < W; x++)
if (mask[static_cast<size_t>(y) * W + x] & (1u << PixelMask::BeamStopPixelBit))
rows[y] = {std::min(rows[y].first, x), x};
return rows;
}
void DropShadowClipped(std::vector<Reflection> &reflections, const std::vector<uint32_t> &mask,
int W, int H, float r1) {
const std::vector<std::pair<int, int>> &shadow_rows, int W, int H, float r1) {
std::erase_if(reflections, [&](const Reflection &r) {
const int x0 = std::max(0, static_cast<int>(std::floor(r.predicted_x - r1)));
const int x1 = std::min(W - 1, static_cast<int>(std::ceil(r.predicted_x + r1)));
const int y0 = std::max(0, static_cast<int>(std::floor(r.predicted_y - r1)));
const int y1 = std::min(H - 1, static_cast<int>(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<float>(x) - r.predicted_x, dy = static_cast<float>(y) - r.predicted_y;
if (dx * dx + dy * dy < r1 * r1 && (mask[static_cast<size_t>(y) * W + x] & (1u << PixelMask::BeamStopPixelBit)))
return true;
+4 -1
View File
@@ -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<Reflection> &reflections, const std::vector<uint32_t> &mask,
int width, int height, float r1);
const std::vector<std::pair<int, int>> &shadow_rows, int width, int height, float r1);
std::vector<std::pair<int, int>> ShadowRows(const std::vector<uint32_t> &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<ImagePreprocessorBuffer> preprocessor_buffer;
const PixelMask &mask;
const std::vector<std::pair<int, int>> 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.
+1 -1
View File
@@ -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);