rugnux finds the beam stop and its holder in a projection of 60 images and marks them in the pixel mask as bit 9 (--detect-beam-stop[=N|off], on by default). Reflections behind the stop are attenuated but not flagged, so they integrate low with a plausible sigma and nothing downstream catches them: the signal-box gate requires 100% valid pixels and shadow pixels are valid, the background clip is high-side only, and the |zeta| cut applies only to the space-group search merge. The detection compares each pixel's background against the typical background at the same radius on two channels. An azimuthal one (the ring median) finds the holder arm, which is a minority of its ring; a radial one (the background just outside) finds the disk, which the ring median cannot see because inside a fully blocked ring the median is the shadow itself. Pixels are pooled over a 5x5 box and tested only where the background has actually been counted, so low-background data no longer masks the whole detector. Recorded reflections are carved back out - a beam stop cannot block a reflection that was measured. Bit 9 belongs to the run that found it, not to the dataset: it is cleared when a run starts, so a mask read back from a file that carries one starts clear. The user mask (bit 8) is left alone. Scaling and merging gain a low-resolution limit, default 50 A (--scaling-low-resolution <num>, 0 removes it), applied per observation before scaling so it also protects the per-frame scale fit and the space-group search. 50 A is the value XDS configurations use; rugnux_vs_xds.py now matches both of XDS's resolution limits instead of only the high one, so the lowest shell is the same shell in the two programs. The viewer draws the detected shadow in coral with a "Show beam stop" switch in the side panel, exposes the low-resolution limit in the settings dock, and offers detection in its processing jobs. Adding an image marker meant giving the reader a MIN_REAL_PXL_VALUE, because several places classify a pixel by range rather than by equality and would otherwise read the new marker as a very negative intensity. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
330 lines
13 KiB
C++
330 lines
13 KiB
C++
// SPDX-FileCopyrightText: 2024 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#include "PixelMask.h"
|
|
#include "RawToConvertedGeometry.h"
|
|
#include "JFJochException.h"
|
|
#include "JFJochCompressor.h"
|
|
|
|
PixelMask::PixelMask() = default;
|
|
|
|
PixelMask::PixelMask(size_t width, size_t height)
|
|
: mask(width*height, 0) {}
|
|
|
|
PixelMask::PixelMask(const DiffractionExperiment &experiment)
|
|
: PixelMask(experiment.GetXPixelsNumConv(),
|
|
experiment.GetYPixelsNumConv()) {
|
|
CalcEdgePixels(experiment);
|
|
}
|
|
|
|
PixelMask::PixelMask(const std::vector<uint32_t> &in_mask) : mask(in_mask) {}
|
|
|
|
uint32_t PixelMask::LoadMask(const std::vector<uint32_t> &input_mask, uint8_t bit) {
|
|
uint32_t ret = 0;
|
|
if (input_mask.size() != mask.size())
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
|
"Input match doesn't fit the detector ");
|
|
|
|
for (int i = 0; i < mask.size(); i++) {
|
|
if (input_mask[i] != 0) {
|
|
mask[i] |= (1 << bit);
|
|
ret++;
|
|
} else
|
|
mask[i] &= ~(1 << bit);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
void PixelMask::UpdateRawMask(const DiffractionExperiment &experiment) {
|
|
switch (experiment.GetDetectorType()) {
|
|
case DetectorType::JUNGFRAU:
|
|
case DetectorType::EIGER:
|
|
raw_mask.resize(experiment.GetModulesNum() * RAW_MODULE_SIZE, 0);
|
|
ConvertedToRawGeometry(experiment, raw_mask.data(), mask.data());
|
|
break;
|
|
default:
|
|
raw_mask.clear();
|
|
break;
|
|
}
|
|
}
|
|
|
|
void PixelMask::CalcEdgePixels_i(const DiffractionExperiment &experiment) {
|
|
if (experiment.GetDetectorType() == DetectorType::DECTRIS)
|
|
return;
|
|
|
|
size_t nmodules = experiment.GetModulesNum();
|
|
auto settings = experiment.GetImageFormatSettings();
|
|
|
|
// Set module gaps to 1
|
|
std::vector<uint32_t> module_gaps(nmodules * RAW_MODULE_SIZE, 0);
|
|
std::vector<uint32_t> module_gaps_conv(experiment.GetPixelsNumConv(), 1);
|
|
RawToConvertedGeometry(experiment, module_gaps_conv.data(), module_gaps.data());
|
|
LoadMask(module_gaps_conv, ModuleGapPixelBit);
|
|
|
|
// Calculate module edges and chip edges
|
|
std::vector<uint32_t> module_edge(nmodules * RAW_MODULE_SIZE, 0);
|
|
std::vector<uint32_t> chip_edge(nmodules * RAW_MODULE_SIZE, 0);
|
|
for (int64_t module = 0; module < nmodules; module++) {
|
|
for (int64_t line = 0; line < RAW_MODULE_LINES; line++) {
|
|
for (int64_t col = 0; col < RAW_MODULE_COLS; col++) {
|
|
int64_t pixel = module * RAW_MODULE_SIZE + line * RAW_MODULE_COLS + col;
|
|
if ((line == 0)
|
|
|| (line == RAW_MODULE_LINES - 1)
|
|
|| (col == 0)
|
|
|| (col == RAW_MODULE_COLS - 1))
|
|
module_edge[pixel] = 1;
|
|
|
|
if ((col == 255) || (col == 256)
|
|
|| (col == 511) || (col == 512)
|
|
|| (col == 767) || (col == 768)
|
|
|| (line == 255) || (line == 256))
|
|
chip_edge[pixel] = 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
std::vector<uint32_t> module_edge_conv(experiment.GetPixelsNumConv(), 0);
|
|
if (experiment.GetMaskModuleEdges())
|
|
RawToConvertedGeometry(experiment, module_edge_conv.data(), module_edge.data());
|
|
LoadMask(module_edge_conv, ModuleEdgePixelBit);
|
|
|
|
std::vector<uint32_t> chip_edge_conv(experiment.GetPixelsNumConv(), 0);
|
|
if (experiment.GetMaskChipEdges())
|
|
RawToConvertedGeometry(experiment, chip_edge_conv.data(), chip_edge.data());
|
|
LoadMask(chip_edge_conv, ChipGapPixelBit);
|
|
}
|
|
|
|
void PixelMask::CalcEdgePixels(const DiffractionExperiment &experiment) {
|
|
CalcEdgePixels_i(experiment);
|
|
UpdateRawMask(experiment);
|
|
}
|
|
|
|
const std::vector<uint32_t> &PixelMask::GetMaskRaw() const {
|
|
if (raw_mask.empty())
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
|
"Raw format not available for this detector");
|
|
return raw_mask;
|
|
}
|
|
|
|
const std::vector<uint32_t> &PixelMask::GetMask() const {
|
|
return mask;
|
|
}
|
|
|
|
const std::vector<uint32_t> &PixelMask::GetMask(const DiffractionExperiment& experiment) const {
|
|
if (experiment.IsGeometryTransformed())
|
|
return GetMask();
|
|
else
|
|
return GetMaskRaw();
|
|
}
|
|
|
|
std::vector<uint32_t> PixelMask::GetUserMask() const {
|
|
std::vector<uint32_t> ret = GetMask();
|
|
for (auto &i: ret)
|
|
i = ((i & (1 << UserMaskedPixelBit)) != 0) ? 1 : 0;
|
|
return ret;
|
|
}
|
|
|
|
std::vector<uint32_t> PixelMask::GetUserMask(const DiffractionExperiment& experiment) const {
|
|
if (experiment.IsGeometryTransformed())
|
|
return GetUserMask();
|
|
else {
|
|
std::vector<uint32_t> tmp = GetUserMask();
|
|
std::vector<uint32_t> ret(experiment.GetModulesNum() * RAW_MODULE_SIZE, 0);
|
|
ConvertedToRawGeometry(experiment, ret.data(), tmp.data());
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
|
|
void PixelMask::LoadDetectorBadPixelMask(const DiffractionExperiment &experiment, const JFCalibration *calib) {
|
|
if (experiment.GetDetectorType() == DetectorType::DECTRIS)
|
|
return;
|
|
|
|
std::vector<uint32_t> input_mask(experiment.GetModulesNum() * RAW_MODULE_SIZE, 0);
|
|
std::vector<uint32_t> input_mask_rms(experiment.GetModulesNum() * RAW_MODULE_SIZE, 0);
|
|
|
|
if (calib != nullptr) {
|
|
for (int sc = 0; sc < experiment.GetStorageCellNumber(); sc++) {
|
|
// For multiple SC PixelMask is logical sum of all image masks
|
|
// (this can be too much, but better than too little)
|
|
auto pedestal_g0 = calib->GetPedestal(0, sc);
|
|
auto pedestal_g0_rms = calib->GetPedestalRMS(0, sc);
|
|
auto pedestal_g1 = calib->GetPedestal(1, sc);
|
|
auto pedestal_g2 = calib->GetPedestal(2, sc);
|
|
|
|
for (int i = 0; i < experiment.GetModulesNum() * RAW_MODULE_SIZE; i++) {
|
|
if (pedestal_g1[i] > 16383)
|
|
input_mask[i] = 1;
|
|
if (!experiment.IsFixedGainG1()) {
|
|
if (pedestal_g0[i] >= 16383) {
|
|
if (experiment.IsMaskPixelsWithoutG0())
|
|
input_mask[i] = 1;
|
|
} else if (pedestal_g0_rms[i] > experiment.GetImageFormatSettings().GetPedestalG0RMSLimit())
|
|
input_mask_rms[i] = 1;
|
|
|
|
if (pedestal_g2[i] >= 16383)
|
|
input_mask[i] = 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
std::vector<uint32_t> input_mask_conv(experiment.GetPixelsNumConv(), 0);
|
|
RawToConvertedGeometry(experiment, input_mask_conv.data(), input_mask.data());
|
|
|
|
std::vector<uint32_t> input_mask_rms_conv(experiment.GetPixelsNumConv(), 0);
|
|
RawToConvertedGeometry(experiment, input_mask_rms_conv.data(), input_mask_rms.data());
|
|
|
|
LoadMask(input_mask_conv, ErrorPixelBit);
|
|
LoadMask(input_mask_rms_conv, NoisyPixelBit);
|
|
|
|
CalcEdgePixels_i(experiment);
|
|
|
|
UpdateRawMask(experiment);
|
|
}
|
|
|
|
PixelMaskStatistics PixelMask::GetStatistics() const {
|
|
PixelMaskStatistics ret{};
|
|
for (const auto &i: mask) {
|
|
if (i & (1 << ModuleGapPixelBit))
|
|
ret.module_gap_pixel++;
|
|
else {
|
|
if (i != 0)
|
|
ret.total_masked++;
|
|
if (i & (1 << ErrorPixelBit))
|
|
ret.error_pixel++;
|
|
if (i & (1 << NoisyPixelBit))
|
|
ret.noisy_pixel++;
|
|
if (i & (1 << UserMaskedPixelBit))
|
|
ret.user_mask++;
|
|
if (i & ((1 << ChipGapPixelBit) | (1 << ModuleEdgePixelBit)))
|
|
ret.chip_gap_pixel++;
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
void PixelMask::LoadUserMask(const DiffractionExperiment& experiment, const std::vector<uint32_t> &in_mask) {
|
|
if (in_mask.size() == mask.size()) {
|
|
LoadMask(in_mask, UserMaskedPixelBit);
|
|
UpdateRawMask(experiment);
|
|
} else if (in_mask.size() == experiment.GetModulesNum() * RAW_MODULE_SIZE) {
|
|
std::vector<uint32_t> tmp(experiment.GetPixelsNumConv(), 0);
|
|
RawToConvertedGeometry(experiment, tmp.data(), in_mask. data());
|
|
LoadMask(tmp, UserMaskedPixelBit);
|
|
UpdateRawMask(experiment);
|
|
} else
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
|
"Size of input user mask invalid");
|
|
}
|
|
|
|
void PixelMask::LoadBeamStopMask(const DiffractionExperiment& experiment, const std::vector<uint32_t> &in_mask) {
|
|
if (in_mask.size() != mask.size())
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
|
"Size of input beam stop mask invalid");
|
|
LoadMask(in_mask, BeamStopPixelBit);
|
|
UpdateRawMask(experiment);
|
|
}
|
|
|
|
void PixelMask::ClearBeamStopMask(const DiffractionExperiment& experiment) {
|
|
for (auto &i: mask)
|
|
i &= ~(1u << BeamStopPixelBit);
|
|
UpdateRawMask(experiment);
|
|
}
|
|
|
|
void PixelMask::LoadUserMask(const DiffractionExperiment& experiment, const CompressedImage& image) {
|
|
const size_t width = image.GetWidth();
|
|
const size_t height = image.GetHeight();
|
|
|
|
// The image has to match one of the two layouts handled by the vector
|
|
// overload below: converted geometry, or raw stacked modules.
|
|
const bool converted = (width == static_cast<size_t>(experiment.GetXPixelsNumConv()))
|
|
&& (height == static_cast<size_t>(experiment.GetYPixelsNumConv()));
|
|
const bool raw = (width == static_cast<size_t>(RAW_MODULE_COLS))
|
|
&& (height == static_cast<size_t>(RAW_MODULE_LINES * experiment.GetModulesNum()));
|
|
if (!converted && !raw)
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
|
"User mask image size doesn't match the detector");
|
|
|
|
std::vector<uint8_t> buffer;
|
|
const uint8_t *bytes = image.GetUncompressedPtr(buffer);
|
|
|
|
// A pixel is masked when its value is non-zero. Read each pixel as an
|
|
// unsigned integer of the matching width - the sign is irrelevant when
|
|
// comparing against zero.
|
|
std::vector<uint32_t> mask(width * height);
|
|
auto binarize = [&](auto sample) {
|
|
using sample_t = decltype(sample);
|
|
const auto *typed = reinterpret_cast<const sample_t *>(bytes);
|
|
for (size_t i = 0; i < mask.size(); i++)
|
|
mask[i] = (typed[i] != 0) ? 1 : 0;
|
|
};
|
|
|
|
switch (image.GetMode()) {
|
|
case CompressedImageMode::Uint8:
|
|
case CompressedImageMode::Int8:
|
|
binarize(uint8_t{});
|
|
break;
|
|
case CompressedImageMode::Uint16:
|
|
case CompressedImageMode::Int16:
|
|
binarize(uint16_t{});
|
|
break;
|
|
case CompressedImageMode::Uint32:
|
|
case CompressedImageMode::Int32:
|
|
binarize(uint32_t{});
|
|
break;
|
|
default:
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
|
"User mask must be an 8-, 16- or 32-bit integer image");
|
|
}
|
|
|
|
LoadUserMask(experiment, mask);
|
|
}
|
|
|
|
void PixelMask::LoadDECTRISBadPixelMask(const std::vector<uint32_t> &input_mask) {
|
|
if (input_mask.size() != mask.size())
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
|
"Input match doesn't fit the detector ");
|
|
|
|
uint32_t user_bitmask = (1 << UserMaskedPixelBit);
|
|
uint32_t bad_pixel_bitmask = ~((1 << UserMaskedPixelBit) | (1 << ModuleGapPixelBit) | (1 << ChipGapPixelBit));
|
|
|
|
for (int i = 0; i < mask.size(); i++) {
|
|
if ((input_mask[i] & (1 << ModuleGapPixelBit)) != 0) {
|
|
mask[i] = (1 << ModuleGapPixelBit);
|
|
} else {
|
|
mask[i] = 0;
|
|
if (input_mask[i] & bad_pixel_bitmask) {
|
|
mask[i] |= (1 << ErrorPixelBit);
|
|
}
|
|
// User and chip gap are just transferred
|
|
if ((input_mask[i] & (1 << UserMaskedPixelBit)) != 0) {
|
|
mask[i] |= (1 << UserMaskedPixelBit);
|
|
}
|
|
if ((input_mask[i] & (1 << ChipGapPixelBit)) != 0) {
|
|
mask[i] |= (1 << ChipGapPixelBit);
|
|
}
|
|
}
|
|
}
|
|
raw_mask = {}; // For DECTRIS - there is no raw mask
|
|
}
|
|
|
|
void PixelMask::LoadDarkBadPixelMask(const DiffractionExperiment& experiment, const std::vector<uint32_t> &input_mask) {
|
|
if (input_mask.size() != mask.size())
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
|
"Input match doesn't fit the detector ");
|
|
|
|
for (int i = 0; i < mask.size(); i++) {
|
|
// Ignore module gap (doesn't matter) or bad pixels
|
|
if ((mask[i] & (1 << ModuleGapPixelBit | 1 << ErrorPixelBit)) != 0)
|
|
continue;
|
|
|
|
if (input_mask[i] != 0) {
|
|
mask[i] |= (1 << NoisyPixelBit);
|
|
} else {
|
|
mask[i] &= ~(1 << NoisyPixelBit);
|
|
}
|
|
}
|
|
UpdateRawMask(experiment);
|
|
}
|