Inactive pixels get a pixel mask bit of their own (bit 11)

The pixels rugnux measures as outside a CCD's active area shared bit 1 with the detector's own bad
pixels, so they could not be told from the device mask and pixel_sum left them out. They now have bit 11,
a run-determined bit like the shadow and the hot pixels: they stay masked in the image and the other
statistics, and pixel_sum counts them. The beam-stop finder still reads them as data.

A file written with this mask no longer shows them as bit 1. The jfjoch XDS plugin and DIALS mask any
bit, so they still mask them; Durin and Neggia, which ignore the bits from 8 up, no longer do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
leonarski_fandClaude Opus 5.5 committed 2026-11-07 15:19:34 +01:00
1 parent 15af14357d
commit e6baecda58
10 files changed
+33 -15

No files matched your search

+1 -1
View File
@@ -275,7 +275,7 @@ void PixelMask::AddDeadPixelMask(const DiffractionExperiment& experiment, const
"Size of input dead pixel mask invalid"); "Size of input dead pixel mask invalid");
for (size_t i = 0; i < mask.size(); i++) { for (size_t i = 0; i < mask.size(); i++) {
if (in_mask[i] != 0) if (in_mask[i] != 0)
mask[i] |= (1 << ErrorPixelBit); mask[i] |= (1 << InactivePixelBit);
} }
UpdateDerived(experiment); UpdateDerived(experiment);
} }
+5 -2
View File
@@ -49,10 +49,13 @@ public:
// Found bad on the run's own frames (rugnux pre-scan, HotPixelFinder): lit above its ring on too // Found bad on the run's own frames (rugnux pre-scan, HotPixelFinder): lit above its ring on too
// many frames, or holding the error value on most of them. // many frames, or holding the error value on most of them.
constexpr static const uint8_t HotPixelBit = 10; constexpr static const uint8_t HotPixelBit = 10;
// Outside a CCD's active area, measured on the run's own frames (RugnuxMeasuredGain::constant).
constexpr static const uint8_t InactivePixelBit = 11;
constexpr static const uint8_t ChipGapPixelBit = 31; constexpr static const uint8_t ChipGapPixelBit = 31;
constexpr static const uint8_t ModuleEdgePixelBit = 30; constexpr static const uint8_t ModuleEdgePixelBit = 30;
// What a run determines on its own frames, as against the detector's and the calibration's mask. // What a run determines on its own frames, as against the detector's and the calibration's mask.
constexpr static const uint32_t RunMaskBits = (1u << BeamStopPixelBit) | (1u << HotPixelBit); constexpr static const uint32_t RunMaskBits = (1u << BeamStopPixelBit) | (1u << HotPixelBit)
| (1u << InactivePixelBit);
// The values of GetBinaryMask(): masked by the device mask, or only by what the run determined. // The values of GetBinaryMask(): masked by the device mask, or only by what the run determined.
constexpr static const uint8_t BinaryMaskDevice = 1; constexpr static const uint8_t BinaryMaskDevice = 1;
constexpr static const uint8_t BinaryMaskRunOnly = 2; constexpr static const uint8_t BinaryMaskRunOnly = 2;
@@ -70,7 +73,7 @@ public:
// from a file that carries one starts clear. The user mask (bit 8) is deliberately left alone. // from a file that carries one starts clear. The user mask (bit 8) is deliberately left alone.
void ClearBeamStopMask(const DiffractionExperiment& experiment); void ClearBeamStopMask(const DiffractionExperiment& experiment);
void LoadHotPixelMask(const DiffractionExperiment& experiment, const std::vector<uint32_t>& mask); void LoadHotPixelMask(const DiffractionExperiment& experiment, const std::vector<uint32_t>& mask);
// Pixels that record nothing (NXmx bit 1, "dead"), added to whatever the bit already holds. // Pixels that record nothing (InactivePixelBit), added to whatever the bit already holds.
void AddDeadPixelMask(const DiffractionExperiment& experiment, const std::vector<uint32_t>& mask); void AddDeadPixelMask(const DiffractionExperiment& experiment, const std::vector<uint32_t>& mask);
void LoadDECTRISBadPixelMask(const std::vector<uint32_t>& mask); void LoadDECTRISBadPixelMask(const std::vector<uint32_t>& mask);
void LoadDarkBadPixelMask(const DiffractionExperiment& experiment, const std::vector<uint32_t>& mask); void LoadDarkBadPixelMask(const DiffractionExperiment& experiment, const std::vector<uint32_t>& mask);
+1 -1
View File
@@ -454,7 +454,7 @@ many pixels were masked and why.
CCD, an image plate or another integrating detector read through an amplifier) has read noise on every CCD, an image plate or another integrating detector read through an amplifier) has read noise on every
live pixel, so no live pixel reads the same value on all 16 frames the detector gain is live pixel, so no live pixel reads the same value on all 16 frames the detector gain is
measured on. A pixel that does lies outside the active area - the corners of a round CCD, a border or seam measured on. A pixel that does lies outside the active area - the corners of a round CCD, a border or seam
the detector's corrections leave at zero - and is masked as dead (bit 1) before the run starts. A test that the detector's corrections leave at zero - and is masked as inactive (bit 11) before the run starts. A test that
compares a pixel with its own resolution ring cannot see such an area: beyond the circle of compares a pixel with its own resolution ring cannot see such an area: beyond the circle of
a round CCD the whole ring reads zero. It is not made where the pixels that do vary read zero on 1 % or a round CCD the whole ring reads zero. It is not made where the pixels that do vary read zero on 1 % or
more of their readings (counted photons, or a file cut at a threshold), since a pixel at zero on every more of their readings (counted photons, or a file cut at a threshold), since a pixel at zero on every
+1 -1
View File
@@ -80,7 +80,7 @@ Valid pixels are not masked, not saturated, not error pixels.
These are not specific to the FPGA path: the CPU and GPU image preprocessors of the shared These are not specific to the FPGA path: the CPU and GPU image preprocessors of the shared
`image_analysis/` library collect the same statistics per image, for detectors driven through `image_analysis/` library collect the same statistics per image, for detectors driven through
DECTRIS SIMPLON (e.g. EIGER) and for rugnux. There the sum excludes only the device mask (detector and DECTRIS SIMPLON (e.g. EIGER) and for rugnux. There the sum excludes only the device mask (detector and
calibration), not the hot pixels and the beam-stop shadow a run finds on its own frames. calibration), not the hot pixels, the beam-stop shadow and the inactive pixels a run finds on its own frames.
## Square root compression ## Square root compression
Jungfraujoch FPGA includes lossy compression preserving counting statistic properties of X-ray image, while reducing bit width of an image. Jungfraujoch FPGA includes lossy compression preserving counting statistic properties of X-ray image, while reducing bit width of an image.
+1 -1
View File
@@ -544,7 +544,7 @@ every pixel belonging to that ROI, so an ROI's footprint can be recovered exactl
`error_pixels`, `saturated_pixels`, `pixel_sum`. Surfaced in the master file under `/entry/image`. `error_pixels`, `saturated_pixels`, `pixel_sum`. Surfaced in the master file under `/entry/image`.
`pixel_sum` is the signed sum of the pixels that are not error, not saturated and not under the `pixel_sum` is the signed sum of the pixels that are not error, not saturated and not under the
device mask (detector and calibration); a mask rugnux determines on the run's own frames (hot pixels, device mask (detector and calibration); a mask rugnux determines on the run's own frames (hot pixels,
beam-stop shadow) does not change it. The FPGA computes it per module, and CPU/GPU analysis (e.g. beam-stop shadow, inactive pixels) does not change it. The FPGA computes it per module, and CPU/GPU analysis (e.g.
EIGER) per image, on the assembled image, to the same definition. EIGER) per image, on the assembled image, to the same definition.
### 4.6 `/entry/profiling` — per-image timing ### 4.6 `/entry/profiling` — per-image timing
+3
View File
@@ -25,6 +25,9 @@ Bit 10 - defective pixel found on the run's own frames (rotation data from a cou
[CPU data analysis §1.6](CPU_DATA_ANALYSIS_IMAGE.md)): lit above its resolution ring on more frames [CPU data analysis §1.6](CPU_DATA_ANALYSIS_IMAGE.md)): lit above its resolution ring on more frames
than one reflection or chance explains, or holding the detector's error value on most frames. than one reflection or chance explains, or holding the detector's error value on most frames.
Bit 11 - outside a CCD's active area, measured on the run's own frames (see
[CPU data analysis §1.6](CPU_DATA_ANALYSIS_IMAGE.md)).
Bit 30 - module edge (only for PSI systems) Bit 30 - module edge (only for PSI systems)
Bit 31 - chip edge interpolated pixel (multipixel) Bit 31 - chip edge interpolated pixel (multipixel)
+1 -1
View File
@@ -74,7 +74,7 @@ Measured with the Durin and Neggia builds used in Jungfraujoch CI:
|---|---|---|---|---|---| |---|---|---|---|---|---|
| 0 | module gap | yes | yes | yes | yes | | 0 | module gap | yes | yes | yes | yes |
| 1–4 | error, noisy | yes | yes | yes | yes | | 1–4 | error, noisy | yes | yes | yes | yes |
| 8, 9, 10 | user mask, beam stop, defective | yes | **no** | **no** | yes | | 8, 9, 10, 11 | user mask, beam stop, defective, inactive | yes | **no** | **no** | yes |
| 30 | module edge | yes | **no** | **no** | yes | | 30 | module edge | yes | **no** | **no** | yes |
| 31 | chip edge | yes | yes | **no** | yes | | 31 | chip edge | yes | yes | **no** | yes |
+1 -1
View File
@@ -349,7 +349,7 @@ void Rugnux::PreScan(int start_image, int images_to_process, int frame_count, Ru
std::vector<uint32_t> m = pixel_mask_.GetMask(); std::vector<uint32_t> m = pixel_mask_.GetMask();
for (size_t i = 0; i < m.size(); i++) for (size_t i = 0; i < m.size(); i++)
if (config_.inactive_pixels[i] != 0) if (config_.inactive_pixels[i] != 0)
m[i] &= ~(1u << PixelMask::ErrorPixelBit); m[i] &= ~((1u << PixelMask::ErrorPixelBit) | (1u << PixelMask::InactivePixelBit));
beam_stop_mask = PixelMask(m); beam_stop_mask = PixelMask(m);
} }
std::optional<ShadowFinder> finder; std::optional<ShadowFinder> finder;
+17 -5
View File
@@ -99,24 +99,29 @@ TEST_CASE("ImagePreprocessorCPU_PixelSum_NoOverflow", "[ImagePreprocessor]") {
CHECK(stats.pixel_sum > INT32_MAX); CHECK(stats.pixel_sum > INT32_MAX);
} }
// A mask a run determines (a beam-stop shadow, hot pixels) takes pixels out of the image, not out of the sum. // A mask a run determines (a beam-stop shadow, hot pixels, inactive pixels) takes pixels out of the image, not out of the sum.
TEST_CASE("ImagePreprocessorCPU_PixelSum_RunMask", "[ImagePreprocessor]") { TEST_CASE("ImagePreprocessorCPU_PixelSum_RunMask", "[ImagePreprocessor]") {
DiffractionExperiment x(DetDECTRIS(64, 4, "Test", {}).BitDepthImage(32)); DiffractionExperiment x(DetDECTRIS(64, 4, "Test", {}).BitDepthImage(32));
x.PixelSigned(true); x.PixelSigned(true);
std::vector<uint32_t> user_mask(x.GetPixelsNum(), 0), shadow(x.GetPixelsNum(), 0), hot(x.GetPixelsNum(), 0); std::vector<uint32_t> user_mask(x.GetPixelsNum(), 0), shadow(x.GetPixelsNum(), 0), hot(x.GetPixelsNum(), 0),
inactive(x.GetPixelsNum(), 0);
user_mask[1] = 1; // the device's user_mask[1] = 1; // the device's
shadow[1] = 1; // and the run's: the device mask decides shadow[1] = 1; // and the run's: the device mask decides
shadow[2] = 1; shadow[2] = 1;
shadow[3] = 1; // error pixel under the shadow shadow[3] = 1; // error pixel under the shadow
hot[4] = 1; hot[4] = 1;
hot[5] = 1; // saturated pixel, hot hot[5] = 1; // saturated pixel, hot
inactive[6] = 1;
inactive[7] = 1; // error pixel, inactive
inactive[1] = 1; // the device mask decides here too
PixelMask device(x); PixelMask device(x);
device.LoadUserMask(x, user_mask); device.LoadUserMask(x, user_mask);
PixelMask with_run = device; PixelMask with_run = device;
with_run.LoadBeamStopMask(x, shadow); with_run.LoadBeamStopMask(x, shadow);
with_run.LoadHotPixelMask(x, hot); with_run.LoadHotPixelMask(x, hot);
with_run.AddDeadPixelMask(x, inactive);
std::vector<int32_t> image(x.GetPixelsNum(), 0); std::vector<int32_t> image(x.GetPixelsNum(), 0);
image[0] = 11; image[0] = 11;
@@ -125,18 +130,21 @@ TEST_CASE("ImagePreprocessorCPU_PixelSum_RunMask", "[ImagePreprocessor]") {
image[3] = INT32_MIN; image[3] = INT32_MIN;
image[4] = -33; image[4] = -33;
image[5] = INT32_MAX; image[5] = INT32_MAX;
image[6] = 44;
image[7] = INT32_MIN;
const auto input = reinterpret_cast<const uint8_t *>(image.data()); const auto input = reinterpret_cast<const uint8_t *>(image.data());
ImagePreprocessorBuffer buffer(x.GetPixelsNum()); ImagePreprocessorBuffer buffer(x.GetPixelsNum());
ImagePreprocessorCPU device_only(x, device); ImagePreprocessorCPU device_only(x, device);
CHECK(device_only.Analyze(buffer, input, CompressedImageMode::Int32).pixel_sum == 11 + 22 - 33); CHECK(device_only.Analyze(buffer, input, CompressedImageMode::Int32).pixel_sum == 11 + 22 - 33 + 44);
ImagePreprocessorCPU run(x, with_run); ImagePreprocessorCPU run(x, with_run);
const ImageStatistics stats = run.Analyze(buffer, input, CompressedImageMode::Int32); const ImageStatistics stats = run.Analyze(buffer, input, CompressedImageMode::Int32);
CHECK(stats.pixel_sum == 11 + 22 - 33); CHECK(stats.pixel_sum == 11 + 22 - 33 + 44);
CHECK(stats.masked_pixel_count == 5); CHECK(stats.masked_pixel_count == 7);
CHECK(stats.max_value == 11); CHECK(stats.max_value == 11);
CHECK(buffer[2] == INT32_MIN); CHECK(buffer[2] == INT32_MIN);
CHECK(buffer[6] == INT32_MIN);
} }
// CPU or GPU, whichever this machine runs, from the image to the vector the master file is written from. // CPU or GPU, whichever this machine runs, from the image to the vector the master file is written from.
@@ -210,6 +218,10 @@ TEST_CASE("ImagePreprocessorGPU_PixelSum", "[ImagePreprocessorGPU]") {
PixelMask mask(x); PixelMask mask(x);
mask.LoadUserMask(x, user_mask); mask.LoadUserMask(x, user_mask);
mask.LoadBeamStopMask(x, shadow); mask.LoadBeamStopMask(x, shadow);
std::vector<uint32_t> inactive(npixels, 0);
for (size_t i = 0; i < npixels; i += 1003)
inactive[i] = 1;
mask.AddDeadPixelMask(x, inactive);
// Mostly near the limit, so the sum needs far more than 32 bits; negative pixels and markers among them. // Mostly near the limit, so the sum needs far more than 32 bits; negative pixels and markers among them.
std::mt19937 rng(11); std::mt19937 rng(11);
+2 -2
View File
@@ -4933,8 +4933,8 @@ TEST_CASE("RugnuxMeasureGain_ConstantPixels", "[portable]") {
reader.AddDeadPixelMask(g.constant); reader.AddDeadPixelMask(g.constant);
const auto &mask = reader.GetDataset()->pixel_mask->GetMask(); const auto &mask = reader.GetDataset()->pixel_mask->GetMask();
CHECK(mask[0] == (1u << PixelMask::ErrorPixelBit)); CHECK(mask[0] == (1u << PixelMask::InactivePixelBit));
CHECK(mask[5 * f.nx + 10] == (1u << PixelMask::ErrorPixelBit)); CHECK(mask[5 * f.nx + 10] == (1u << PixelMask::InactivePixelBit));
CHECK(mask[5 * f.nx + 11] == 0); CHECK(mask[5 * f.nx + 11] == 0);
reader.Close(); reader.Close();
for (int frame = 1; frame <= frames; frame++) { for (int frame = 1; frame <= frames; frame++) {