Files
Jungfraujoch/tests/ROIMapTest.cpp
T
leonarski_fandClaude Opus 5.5 dcad4104ce Dense per-pixel maps through the panel table (panel layout stage 1)
Every dense map that needs geometry now evaluates each image pixel at its
place on the detector plane (DetectorModules), so it is right for a stacked
panel layout and picks up refined module placements:

- AzimuthalIntegrationMapping: bins, resolution, solid-angle/polarisation
  corrections built per pixel through the table; a pixel on no panel is left
  as a masked one. The geometry cache key now includes the panel table, so a
  mapping built after the stills module placement is rebuilt rather than
  served from the cache. ResolutionMaskBits follows (it is a lookup).
- ROIMap / ExportROIMap: azimuthal ROIs on the plane (box and circle ROIs
  stay in image pixels).
- ROIIntegration CPU+GPU: the weighted sums are taken per ROI and panel in
  image pixels and placed on the plane at the end (exact; the affine commutes
  with a weighted mean).
- HotPixelFinder: ring/sector keys and the zeta of the persistence rule on
  the plane.
- Background beam-centre walk CPU+GPU: pixels binned at their plane position;
  host and device share BackgroundPlanePoint so they stay bit-identical.

New common/PanelRows.h: ForEachPixelInRows, ForEachPixel cut to a band of
rows in index order, for the row-parallel builders.

An empty table takes the old arithmetic in the old order: rotation sets and
every single-panel input are unchanged. [PanelLayout] AzintMapping and
ResolutionMask enabled (+ azimuthal ROI map and ROI centroid on the plane);
new BeamCenterFromBackground_BinsOnThePlane and
ROIIntegrationGPU_MatchesCPU_Panels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CeEGgpKdfkCTJb3YNMJcu
2026-10-21 10:26:30 +02:00

311 lines
11 KiB
C++

// SPDX-FileCopyrightText: 2024 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include <catch2/catch_all.hpp>
#include "../common/ROIMap.h"
#include "../common/DiffractionExperiment.h"
TEST_CASE("ROICircle", "[ROIMap]") {
std::unique_ptr<ROICircle> circle;
REQUIRE_THROWS(circle = std::make_unique<ROICircle>("roi1", 200,200,0));
REQUIRE_THROWS(circle = std::make_unique<ROICircle>("roi1", 200,200,-5.0));
REQUIRE_NOTHROW(circle = std::make_unique<ROICircle>("roi1", 200,220,2));
REQUIRE(circle->GetName() == "roi1");
REQUIRE(circle->GetX() == 200.0f);
REQUIRE(circle->GetY() == 220.0f);
REQUIRE(circle->GetRadius_pxl() == 2.0f);
}
TEST_CASE("ROICircle_MarkROI", "[ROIMap]") {
DiffractionExperiment x(DetJF(1));
ROICircle c1("roi1", 200,220,2);
ROICircle c2("roi1", 200,220,1);
std::vector<uint16_t> mask(x.GetPixelsNum(), 0);
std::vector<float> res(x.GetPixelsNum());
REQUIRE_NOTHROW(c1.MarkROI(mask, 15, x.GetXPixelsNum(), x.GetYPixelsNum(), res));
REQUIRE_NOTHROW(c2.MarkROI(mask, 1, x.GetXPixelsNum(), x.GetYPixelsNum(), res));
REQUIRE (mask[220 * x.GetXPixelsNum() + 200] == ((1<<15) | (1<<1)));
REQUIRE (mask[218 * x.GetXPixelsNum() + 200] == (1<<15));
REQUIRE (mask[220 * x.GetXPixelsNum() + 202] == (1<<15));
REQUIRE (mask[221 * x.GetXPixelsNum() + 201] == (1<<15));
REQUIRE (mask[218 * x.GetXPixelsNum() + 202] == 0);
}
TEST_CASE("ROIBox_SwapX", "[ROIMap]") {
std::unique_ptr<ROIBox> rectangle;
REQUIRE_NOTHROW(rectangle = std::make_unique<ROIBox>("roi1", 200, 199, 199, 221));
REQUIRE(rectangle->GetXMax() == 200);
REQUIRE(rectangle->GetXMin() == 199);
REQUIRE(rectangle->GetYMax() == 221);
REQUIRE(rectangle->GetYMin() == 199);
}
TEST_CASE("ROIBox_SwapY", "[ROIMap]") {
std::unique_ptr<ROIBox> rectangle;
REQUIRE_NOTHROW(rectangle = std::make_unique<ROIBox>("roi1", 200,220,199, 198));
REQUIRE(rectangle->GetXMax() == 220);
REQUIRE(rectangle->GetXMin() == 200);
REQUIRE(rectangle->GetYMax() == 199);
REQUIRE(rectangle->GetYMin() == 198);
}
TEST_CASE("ROIBox_NegativeBounds", "[ROIMap]") {
DiffractionExperiment x(DetJF(1));
std::unique_ptr<ROIBox> rectangle;
REQUIRE_NOTHROW(rectangle = std::make_unique<ROIBox>("roi1", -200,220,220, 221));
REQUIRE(rectangle->GetXMin() == 0);
REQUIRE(rectangle->GetXMax() == 220);
REQUIRE_NOTHROW(rectangle = std::make_unique<ROIBox>("roi1", 200,-220,220, 221));
REQUIRE(rectangle->GetXMin() == 0);
REQUIRE(rectangle->GetXMax() == 200);
REQUIRE_NOTHROW(rectangle = std::make_unique<ROIBox>("roi1", -200,-20,220, 221));
REQUIRE(rectangle->GetXMin() == 0);
REQUIRE(rectangle->GetXMax() == -1);
REQUIRE(rectangle->GetWidth() == 0);
REQUIRE(rectangle->GetArea() == 0);
REQUIRE_NOTHROW(rectangle = std::make_unique<ROIBox>("roi1", 220,221,-200, -20));
REQUIRE(rectangle->GetYMin() == 0);
REQUIRE(rectangle->GetYMax() == -1);
REQUIRE(rectangle->GetHeight() == 0);
REQUIRE(rectangle->GetArea() == 0);
}
TEST_CASE("ROIBox_MarkROI", "[ROIMap]") {
DiffractionExperiment x(DetJF(1));
ROIBox r1("roi1", 198,202,218, 222);
ROIBox r2("roi2", 198,202,222, 250);
std::vector<uint16_t> mask(x.GetPixelsNum(), 0);
std::vector<float> res(x.GetPixelsNum());
REQUIRE_NOTHROW(r1.MarkROI(mask, 12, x.GetXPixelsNum(), x.GetYPixelsNum(), res));
REQUIRE_NOTHROW(r2.MarkROI(mask, 13, x.GetXPixelsNum(), x.GetYPixelsNum(), res));
REQUIRE (mask[220 * x.GetXPixelsNum() + 202] == (1<<12));
REQUIRE (mask[218 * x.GetXPixelsNum() + 198] == (1<<12));
REQUIRE (mask[218 * x.GetXPixelsNum() + 202] == (1<<12));
REQUIRE (mask[218 * x.GetXPixelsNum() + 202] == (1<<12));
REQUIRE (mask[222 * x.GetXPixelsNum() + 201] == ((1<<12) | (1<<13)));
REQUIRE (mask[223 * x.GetXPixelsNum() + 200] == (1<<13));
REQUIRE (mask[218 * x.GetXPixelsNum() + 197] == 0);
REQUIRE (mask[218 * x.GetXPixelsNum() + 203] == 0);
REQUIRE (mask[217 * x.GetXPixelsNum() + 200] == 0);
}
TEST_CASE("ROIBox_MarkROI_OutOfBounds", "[ROIMap]") {
DiffractionExperiment x(DetJF(1));
ROIBox r1("roi1", x.GetXPixelsNum(),x.GetXPixelsNum() + 10,218, 222);
ROIBox r2("roi2", 198,202,x.GetYPixelsNum(), x.GetYPixelsNum() + 10);
std::vector<uint16_t> mask(x.GetPixelsNum() * 4, 0); // Set the array to much more to protect for eventual out-out-bounds access
std::vector<float> res(x.GetPixelsNum());
REQUIRE_NOTHROW(r1.MarkROI(mask, 12, x.GetXPixelsNum(), x.GetYPixelsNum(), res));
REQUIRE_NOTHROW(r2.MarkROI(mask, 13, x.GetXPixelsNum(), x.GetYPixelsNum(), res));
size_t err = 0;
for (const auto &i: mask) {
if (i != 0) err++;
}
REQUIRE(err == 0);
}
TEST_CASE("ROIBox", "[ROIMap]") {
std::unique_ptr<ROIBox> rectangle;
REQUIRE_NOTHROW(rectangle = std::make_unique<ROIBox>("roi1", 198,202,218, 222));
REQUIRE(rectangle->GetName() == "roi1");
REQUIRE(rectangle->GetXMin() == 198);
REQUIRE(rectangle->GetXMax() == 202);
REQUIRE(rectangle->GetYMin() == 218);
REQUIRE(rectangle->GetYMax() == 222);
REQUIRE(rectangle->GetWidth() == 5);
REQUIRE(rectangle->GetHeight() == 5);
REQUIRE(rectangle->GetArea() == 25);
}
TEST_CASE("ROIAzimuthal", "[ROIMap]") {
std::unique_ptr<ROIAzimuthal> roi;
REQUIRE_NOTHROW(roi = std::make_unique<ROIAzimuthal>("roi1", 3.0, 5.0));
REQUIRE(roi->GetDMin_A() == 3.0);
REQUIRE(roi->GetDMax_A() == 5.0);
}
TEST_CASE("ROIAzimuthal_Swap", "[ROIMap]") {
std::unique_ptr<ROIAzimuthal> roi;
REQUIRE_NOTHROW(roi = std::make_unique<ROIAzimuthal>("roi1", 3.0, 2.0));
REQUIRE(roi->GetDMin_A() == 2.0);
REQUIRE(roi->GetDMax_A() == 3.0);
}
TEST_CASE("ROIAzimuthal_neg_resolution", "[ROIMap]") {
std::unique_ptr<ROIAzimuthal> roi;
REQUIRE_THROWS(roi = std::make_unique<ROIAzimuthal>("roi1", -1.0, 5.0));
REQUIRE_THROWS(roi = std::make_unique<ROIAzimuthal>("roi1", 5.0, -5.0));
REQUIRE_THROWS(roi = std::make_unique<ROIAzimuthal>("roi1", 5.0, 0.0));
REQUIRE_THROWS(roi = std::make_unique<ROIAzimuthal>("roi1", 0.0, 5.0));
}
TEST_CASE("ROIAzimuthal_ROImark", "[ROIMap]") {
DiffractionExperiment x(DetJF(1));
std::vector<float> resolution_map(x.GetPixelsNum(), 3.0);
resolution_map[1] = 2.0;
resolution_map[2] = 1.0;
resolution_map[3] = 0.0;
resolution_map[4] = 5.0;
ROIAzimuthal r1("roi1", 2.0, 4.0);
ROIAzimuthal r2("roi2", 3.0, 6.0);
std::vector<uint16_t> mask(x.GetPixelsNum(), 0);
REQUIRE_NOTHROW(r1.MarkROI(mask, 15, x.GetXPixelsNum(), x.GetYPixelsNum(), resolution_map));
REQUIRE_NOTHROW(r2.MarkROI(mask, 1, x.GetXPixelsNum(), x.GetYPixelsNum(), resolution_map));
REQUIRE(mask[0] == ((1<<1) | (1 << 15)));
REQUIRE(mask[1] == (1<<15));
REQUIRE(mask[2] == 0);
REQUIRE(mask[3] == 0);
REQUIRE(mask[4] == (1<<1));
for (int i = 5; i < x.GetPixelsNum(); i++) {
REQUIRE(mask[i] == ((1<<1) | (1 << 15)));
}
}
TEST_CASE("ROIAzimuthal_Phi", "[ROIMap]") {
ROIAzimuthal full("roi1", 2.0, 4.0);
REQUIRE_FALSE(full.HasPhi());
// Full ring: phi is ignored, only d-range matters
REQUIRE(full.CheckROI(0, 0, 3.0, 123.0));
REQUIRE(full.CheckROI(0, 0, 3.0, 0.0));
REQUIRE_FALSE(full.CheckROI(0, 0, 1.0, 123.0));
ROIAzimuthal sector("roi2", 2.0, 4.0, 30.0f, 90.0f);
REQUIRE(sector.HasPhi());
REQUIRE(sector.GetPhiMin_deg() == 30.0f);
REQUIRE(sector.GetPhiMax_deg() == 90.0f);
REQUIRE(sector.CheckROI(0, 0, 3.0, 60.0));
REQUIRE(sector.CheckROI(0, 0, 3.0, 30.0)); // inclusive bounds
REQUIRE(sector.CheckROI(0, 0, 3.0, 90.0));
REQUIRE_FALSE(sector.CheckROI(0, 0, 3.0, 120.0));
REQUIRE_FALSE(sector.CheckROI(0, 0, 3.0, 0.0));
REQUIRE_FALSE(sector.CheckROI(0, 0, 1.0, 60.0)); // outside d-range
}
TEST_CASE("ROIAzimuthal_Phi_Wrap", "[ROIMap]") {
// Sector wrapping across 0 deg: 350 .. 10
ROIAzimuthal sector("roi1", 2.0, 4.0, 350.0f, 10.0f);
REQUIRE(sector.HasPhi());
REQUIRE(sector.CheckROI(0, 0, 3.0, 355.0));
REQUIRE(sector.CheckROI(0, 0, 3.0, 5.0));
REQUIRE(sector.CheckROI(0, 0, 3.0, 0.0));
REQUIRE_FALSE(sector.CheckROI(0, 0, 3.0, 180.0));
REQUIRE_FALSE(sector.CheckROI(0, 0, 3.0, 11.0));
}
TEST_CASE("ROIAzimuthal_Phi_Normalize", "[ROIMap]") {
// Out-of-range inputs are normalized to [0, 360)
ROIAzimuthal sector("roi1", 2.0, 4.0, -30.0f, 400.0f);
REQUIRE(sector.GetPhiMin_deg() == 330.0f);
REQUIRE(sector.GetPhiMax_deg() == 40.0f);
// Default (no phi bounds) and equal bounds are both the full ring
ROIAzimuthal full("roi2", 2.0, 4.0);
REQUIRE_FALSE(full.HasPhi());
ROIAzimuthal equal_bounds("roi3", 2.0, 4.0, 45.0f, 45.0f);
REQUIRE_FALSE(equal_bounds.HasPhi());
}
TEST_CASE("ROIAzimuthal_Phi_MarkROI", "[ROIMap]") {
DiffractionExperiment x(DetJF(1));
std::vector<float> resolution_map(x.GetPixelsNum(), 3.0);
std::vector<float> phi_map(x.GetPixelsNum(), 0.0);
phi_map[0] = 45.0;
phi_map[1] = 200.0;
phi_map[2] = 45.0;
resolution_map[2] = 1.0; // outside d-range
ROIAzimuthal sector("roi1", 2.0, 4.0, 30.0f, 90.0f);
std::vector<uint16_t> mask(x.GetPixelsNum(), 0);
REQUIRE_NOTHROW(sector.MarkROI(mask, 3, x.GetXPixelsNum(), x.GetYPixelsNum(), resolution_map, phi_map));
REQUIRE(mask[0] == (1<<3)); // d in range, phi 45 in [30, 90]
REQUIRE(mask[1] == 0); // phi 200 outside sector
REQUIRE(mask[2] == 0); // d outside range
for (int i = 3; i < x.GetPixelsNum(); i++)
REQUIRE(mask[i] == 0); // phi 0 outside sector
}
TEST_CASE("ROIMap") {
DiffractionExperiment x(DetJF(1));
ROIMap mask;
REQUIRE(mask.empty());
mask.SetROI({
.boxes = {
ROIBox("roi12", 100, 120, 240, 260),
ROIBox("roi11", 200, 220, 240, 260)
},
.circles = {ROICircle("roi7", 300, 400, 5)}
});
REQUIRE(!mask.empty());
mask.SetROI({
.boxes = {ROIBox("roi12", 100, 120, 240, 260)},
.circles = {ROICircle("roi7", 300, 400, 5)}
});
REQUIRE(!mask.empty());
// Check arrays are proper size
REQUIRE(mask.GetROINameMap().size() == 2);
auto map = mask.GetROIMap(x.GetDiffractionGeometry(), x.GetModules(),
x.GetXPixelsNum(),
x.GetYPixelsNum());
REQUIRE(map.size() == x.GetPixelsNum());
auto it1 = mask.GetROINameMap().find("roi12");
REQUIRE(it1 != mask.GetROINameMap().end());
REQUIRE(it1->second == 0);
REQUIRE(map[260 * x.GetXPixelsNum() + 100] == (1<<0));
auto it2 = mask.GetROINameMap().find("roi7");
REQUIRE(it2 != mask.GetROINameMap().end());
REQUIRE(it2->second == 1);
REQUIRE(map[405 * x.GetXPixelsNum() + 300] == (1<<1));
mask.SetROI({});
REQUIRE(mask.empty());
}
TEST_CASE("ROIMap_Empty") {
DiffractionExperiment x(DetJF(1));
REQUIRE(x.ROI().empty());
size_t err = 0;
for (const auto &i: x.ExportROIMap()) {
if (i != 0)
err++;
}
REQUIRE(err == 0);
}