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:
1 parent
30ea607686
commit
7c23b4befe
10 files changed
+380
-49
No files matched your search
@@ -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
|
||||
|
||||
Reference in new issue
Block a user