Files
Jungfraujoch/tests/ROIIntegrationCPUTest.cpp
T
leonarski_f 58910274bf image_analysis: compute ROI statistics in the non-FPGA path
MXAnalysisWithoutFPGA never filled DataMessage.roi, so ROI integrals were
only available on the FPGA path. Add a software ROI engine that mirrors the
FPGA roi_calc kernel: per-ROI sum, sum of squares, good-pixel count, max and
intensity-weighted centre of mass, with each pixel carrying a 16-bit mask so
it can contribute to any subset of up to 16 ROIs.

New image_analysis/roi/ library (JFJochROIIntegration), structured like azint:
a base that precomputes the per-pixel mask and names, a templated CPU engine
(generic over pixel type for a future 16-bit path), and a GPU kernel using
per-block shared-memory atomics for the STXM case (half-detector ROIs).

Masked pixels are excluded entirely; saturated pixels are excluded from the
sums but still count towards the max, matching roi_calc exactly. The engine is
only constructed when at least one ROI is defined. Downstream CBOR/HDF5 already
consume message.roi, so no further changes are needed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 21:15:54 +02:00

96 lines
3.5 KiB
C++

// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include <catch2/catch_all.hpp>
#include "../image_analysis/roi/ROIIntegrationCPU.h"
#include "../common/DiffractionExperiment.h"
TEST_CASE("ROIIntegrationCPU") {
DiffractionExperiment experiment(DetJF(1));
experiment.ROI().SetROI(ROIDefinition{.boxes = {
ROIBox("roiA", 10, 20, 30, 40),
ROIBox("roiB", 15, 25, 35, 45)
}});
ROIIntegrationCPU roi(experiment);
REQUIRE(roi.Count() == 2);
REQUIRE_FALSE(roi.empty());
const auto roi_map = experiment.ExportROIMap();
const size_t width = experiment.GetXPixelsNumConv();
const size_t npixel = roi_map.size();
ImagePreprocessorBuffer image(npixel);
for (size_t i = 0; i < npixel; i++)
image[i] = static_cast<int32_t>((i * 7 + 3) % 251); // deterministic, well below INT32_MAX
// Inject one saturated pixel into roiA (bit 0) and one masked pixel into roiB (bit 1)
int64_t saturated_index = -1;
int64_t masked_index = -1;
for (size_t i = 0; i < npixel && (saturated_index < 0 || masked_index < 0); i++) {
if (saturated_index < 0 && (roi_map[i] & (1 << 0)))
saturated_index = static_cast<int64_t>(i);
else if (masked_index < 0 && (roi_map[i] & (1 << 1)))
masked_index = static_cast<int64_t>(i);
}
REQUIRE(saturated_index >= 0);
REQUIRE(masked_index >= 0);
image[saturated_index] = INT32_MAX;
image[masked_index] = INT32_MIN;
// Independent reference matching the documented semantics (masked excluded
// entirely; saturated excluded from sums but counted towards the max)
struct Ref { int64_t sum = 0; uint64_t sum2 = 0; uint64_t pixels = 0;
int64_t xw = 0; int64_t yw = 0; int64_t max = INT64_MIN; };
Ref ref[2];
for (size_t i = 0; i < npixel; i++) {
const uint16_t mask = roi_map[i];
if (mask == 0)
continue;
const int32_t v = image[i];
if (v == INT32_MIN)
continue;
const bool saturated = (v == INT32_MAX);
const int64_t val = v;
const int64_t x = static_cast<int64_t>(i % width);
const int64_t y = static_cast<int64_t>(i / width);
for (int r = 0; r < 2; r++) {
if (!(mask & (1 << r)))
continue;
if (!saturated) {
ref[r].sum += val;
ref[r].sum2 += static_cast<uint64_t>(val * val);
ref[r].pixels += 1;
ref[r].xw += val * x;
ref[r].yw += val * y;
}
if (val > ref[r].max)
ref[r].max = val;
}
}
std::map<std::string, ROIMessage> out;
roi.Run(image, out);
REQUIRE(out.size() == 2);
REQUIRE(out.contains("roiA"));
REQUIRE(out.contains("roiB"));
const std::pair<std::string, int> roi_ids[2] = {{"roiA", 0}, {"roiB", 1}};
for (const auto &[name, r] : roi_ids) {
const auto &msg = out.at(name);
CHECK(msg.sum == ref[r].sum);
CHECK(msg.sum_square == ref[r].sum2);
CHECK(msg.pixels == ref[r].pixels);
CHECK(msg.x_weighted == ref[r].xw);
CHECK(msg.y_weighted == ref[r].yw);
CHECK(msg.max_count == ref[r].max);
CHECK(msg.pixels_masked == 0);
}
// Targeted checks: saturated pixel sets the max for roiA but is not summed
CHECK(out.at("roiA").max_count == INT32_MAX);
CHECK(ref[0].pixels > 0);
}