CrystFEL geometry: read the pixel masks and bad regions instead of ignoring them

The .geom parser now takes the masking information the format defines:
- mask arrays: mask/mask_file/mask_good/mask_bad (mask 0) and maskN_data/maskN_file/maskN_goodbits/
  maskN_badbits for N = 0..7; global keys hold for the panels after them, a panel may override. A mask
  that is the same on every panel is one whole-frame mask, otherwise one per panel, applied on that
  panel's pixels only.
- a relative mask_file is looked for in the working directory (as CrystFEL does), then beside the .geom.
- bad regions (bad*): a lab-plane box (min_x..max_y) or a fs/ss range on a panel (or, old syntax, on
  the whole frame); mask_edge_pixels and no_index.

JFJochXFELReader already had the static pixel mask and the per-frame mask path for these; the masks
go through them unchanged. A mask whose path holds % (one per shot group) is applied frame by frame
like a 3D one. Where the geometry names mask arrays, the data file's own Cheetah masks (CXI
mask_shared and per-shot mask with Cheetah's generic bad bits, /metadata/pixelmask) are not added:
the geometry says which bits matter (e.g. an AGIPD mask_bad that includes bit 32). --mask-good /
--mask-bad still add their masks to these.

Tests: [CrystFELGeometry] (new keys, per-panel override, relative mask file), JFJochXFELReader_*
(new GeometryMasks case on a synthetic CXI + .geom), [JFJochMultiReader], JFJochXTCReader*.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CeEGgpKdfkCTJb3YNMJcu
This commit is contained in:
leonarski_fandClaude Opus 5.5 committed 2026-10-21 12:42:08 +02:00
1 parent 30ea607686
commit 7c23b4befe
10 files changed
+380 -49

No files matched your search

+120
View File
@@ -2,6 +2,8 @@
// SPDX-License-Identifier: GPL-3.0-only
#include <catch2/catch_all.hpp>
#include <algorithm>
#include <climits>
#include <cstdint>
#include <cstring>
#include <filesystem>
@@ -245,6 +247,124 @@ TEST_CASE("JFJochXFELReader_CXI_Int16", "[portable]") {
std::filesystem::remove(name);
}
TEST_CASE("JFJochXFELReader_GeometryMasks", "[portable]") {
// A Cheetah CXI file of two 3-column x 4-row panels side by side in a 4 x 6 frame, read through a
// CrystFEL geometry file that names the masks: the per-frame mask with its own bad bits, a mask in a
// file beside the geometry file, a box in the laboratory plane and a pixel range on panel p1. The
// file's own mask_shared is not taken (the geometry names the masks); --mask-bad still adds its own.
const auto dir = std::filesystem::path("xfel_reader_geom_masks");
std::filesystem::create_directories(dir);
const std::string name = (dir / "frames.cxi").string();
{
const hid_t f = H5Fcreate(name.c_str(), H5F_ACC_TRUNC, H5P_DEFAULT, H5P_DEFAULT);
for (const char *g: {"entry_1", "entry_1/data_1", "entry_1/instrument_1", "entry_1/instrument_1/detector_1",
"entry_1/instrument_1/source_1"})
H5Gclose(H5Gcreate2(f, g, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT));
std::vector<int32_t> frames(2 * 24);
for (size_t i = 0; i < frames.size(); i++)
frames[i] = static_cast<int32_t>(i % 24) + 100; // every pixel its own value, in both frames
WriteFrames(f, "entry_1/data_1/data", H5T_NATIVE_INT32, frames.data(), {2, 4, 6});
std::vector<uint16_t> mask(2 * 24, 0);
mask[1 * 6 + 1] = 0x20; // frame 0 only, (fs 1, ss 1): a bit Cheetah's own set does not include
WriteFrames(f, "entry_1/data_1/mask", H5T_NATIVE_UINT16, mask.data(), {2, 4, 6});
const std::string det = "entry_1/instrument_1/detector_1/";
std::vector<uint16_t> shared(24, 0);
shared[3 * 6 + 5] = 8; // (fs 5, ss 3) dead to Cheetah
WriteFrames(f, det + "mask_shared", H5T_NATIVE_UINT16, shared.data(), {4, 6});
WriteVector(f, "entry_1/instrument_1/source_1/energy", std::vector<double>(2, 9000.0 * 1.602176634e-19));
H5Fclose(f);
}
{
// Good where bit 0 is set; bit 1 is what --mask-bad below looks at.
const hid_t f = H5Fcreate((dir / "pixels.h5").string().c_str(), H5F_ACC_TRUNC, H5P_DEFAULT, H5P_DEFAULT);
std::vector<uint16_t> pixels(24, 1);
pixels[0 * 6 + 4] = 0; // (fs 4, ss 0)
pixels[2 * 6 + 3] = 3; // (fs 3, ss 2)
WriteFrames(f, "mask", H5T_NATIVE_UINT16, pixels.data(), {4, 6});
H5Fclose(f);
}
const std::string geom = (dir / "frames.geom").string();
std::ofstream(geom) << R"(
clen = 0.1
res = 10000
dim0 = %
dim1 = ss
dim2 = fs
data = /entry_1/data_1/data
mask = /entry_1/data_1/mask
mask_good = 0x0
mask_bad = 0xffff
mask1_data = /mask
mask1_file = pixels.h5
mask1_goodbits = 0x1
p0/min_fs = 0
p0/max_fs = 2
p0/min_ss = 0
p0/max_ss = 3
p0/fs = x
p0/ss = y
p0/corner_x = -3
p0/corner_y = -2
p1/min_fs = 3
p1/max_fs = 5
p1/min_ss = 0
p1/max_ss = 3
p1/fs = x
p1/ss = y
p1/corner_x = 0
p1/corner_y = -2
badbox/min_x = -3
badbox/max_x = -2
badbox/min_y = -2
badbox/max_y = -1
bad_p1/min_fs = 5
bad_p1/max_fs = 5
bad_p1/min_ss = 0
bad_p1/max_ss = 0
bad_p1/panel = p1
)";
JFJochXFELReader reader;
reader.ExtraMasks({{"/mask", (dir / "pixels.h5").string(), 0, 2}});
REQUIRE_NOTHROW(reader.ReadFiles({name}, geom));
REQUIRE(reader.GetNumberOfImages() == 2);
// Where each stored pixel lands, found by its value in frame 1 (which the per-frame mask leaves alone).
auto raw = reader.GetRawImage(1);
REQUIRE(raw);
const auto *frame1 = reinterpret_cast<const int32_t *>(raw->image_buffer.data());
const size_t n = raw->image.GetWidth() * raw->image.GetHeight();
auto where = [&](size_t fs, size_t ss) {
const auto it = std::find(frame1, frame1 + n, static_cast<int32_t>(ss * 6 + fs) + 100);
REQUIRE(it != frame1 + n);
return static_cast<size_t>(it - frame1);
};
const auto &m = reader.GetDataset()->pixel_mask->GetMask();
CHECK(m[where(2, 2)] == 0);
CHECK(m[where(4, 0)] != 0); // mask 1, beside the geometry file: lacks the good bit
CHECK(m[where(3, 2)] != 0); // --mask-bad: bit 1
CHECK(m[where(5, 3)] == 0); // the file's mask_shared: not taken
CHECK(m[where(5, 1)] == 0);
CHECK(m[where(0, 0)] != 0); // the lab box: p0's first pixel (centre at x -2.5, y -1.5) and no other
CHECK(m[where(1, 0)] == 0);
CHECK(m[where(0, 1)] == 0);
CHECK(m[where(5, 0)] != 0); // bad_p1
size_t usable = 0;
for (size_t ss = 0; ss < 4; ss++)
for (size_t fs = 0; fs < 6; fs++)
usable += m[where(fs, ss)] == 0;
CHECK(usable == 24 - 4);
// The per-frame mask: (1, 1) is no measurement in frame 0 only.
auto raw0 = reader.GetRawImage(0);
REQUIRE(raw0);
const auto *frame0 = reinterpret_cast<const int32_t *>(raw0->image_buffer.data());
CHECK(frame0[where(1, 1)] == INT32_MIN);
CHECK(frame0[where(2, 1)] == 1 * 6 + 2 + 100);
reader.Close();
std::filesystem::remove_all(dir);
}
namespace {
// An LCLS-I XTC stream of the shape JFJochXTCReader reads: a Configure datagram, then one event (an