58910274bf
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>
96 lines
3.5 KiB
C++
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);
|
|
}
|