Pre-scan masks a pixel at the saturation limit on most frames, as one holding the error value

A pixel stuck at the saturation limit was never masked: the defective-pixel scan counted only the
error value as "invalid on most frames", and a saturated pixel merely as lit, which the hot rule
does not judge on stills. A Rayonix stills set has 3,627 pixels at 65535 on every frame, at high
resolution; each flagged every reflection whose disk it fell in as overloaded, and the spot finder
found them on every frame. The saturation limit now counts with the error value (CPU and CUDA), so
such a pixel is masked like one holding the error value.

On that set the indexed fraction rises from 10% to 33% (and 7% to 30% on the second lattice of the
same run), R_split 0.120 -> 0.106 and the unrefined R-free against the deposition -0.001 / -0.024.
Rotation merges checked bit-identical (three in-house sets with overloads).

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-22 17:27:43 +02:00
1 parent 0cf62452cd
commit b7e4ea23a3
5 files changed
+19 -13

No files matched your search

+3 -2
View File
@@ -203,7 +203,8 @@ void HotPixelFinder::AddImage(const int32_t *image, std::vector<int32_t> &scratc
for (size_t i = begin; i < end; i++) {
const int32_t k = key[i], v = image[i];
if (k < 0) continue;
if (v == INT32_MIN) {
// The error value and the saturation limit alike: no count (see the class comment).
if (v == INT32_MIN || v == INT32_MAX) {
n_error[i]++;
if (ring_ok[k / SECTORS]) { // counted in its ring-sector's frames, but not valid here
n_error_ring_ok[i]++;
@@ -213,7 +214,7 @@ void HotPixelFinder::AddImage(const int32_t *image, std::vector<int32_t> &scratc
}
if (!ring_ok[k / SECTORS]) continue;
sum_value[i] += v;
if (v == INT32_MAX || static_cast<float>(v) > threshold[k])
if (static_cast<float>(v) > threshold[k])
n_lit[i]++;
}
}
+5 -3
View File
@@ -31,8 +31,10 @@
// is above the Poisson bound: a weaker one cannot make an outlier. Alone or as part of a patch - a
// patch lit through the whole sweep is stationary in the lab, so it is not a reflection of the
// rotating crystal, and a reflection crossing it reads the patch. A pixel holding the
// detector's error value on most frames is masked with it: every frame already treats it as invalid,
// but the static mask did not know.
// detector's error value or its saturation limit on most frames is masked with it: every frame already
// treats it as invalid, but the static mask did not know. A pixel stuck at the limit is no overload -
// a CCD can have thousands, at 65535 on every frame - and unmasked, every reflection whose disk it
// falls in is flagged as overloaded.
//
// A DEAD pixel is the mirror of a hot one: it reads on average at most half of its own sector's level,
// and its mean deficit is above the bound a hot pixel's excess has to clear. Nothing but a defect keeps
@@ -85,7 +87,7 @@ public:
struct Result {
std::vector<uint32_t> mask; // non-zero = masked (1 hot, 2 error value, 3 dead), converted geometry
size_t hot = 0; // persistent above the ring
size_t error = 0; // holding the error value on most frames
size_t error = 0; // holding the error value or the saturation limit on most frames
size_t dead = 0; // reading at most half of its sector's level
uint32_t frames = 0;
};
+2 -2
View File
@@ -132,7 +132,7 @@ __global__ void accumulate_kernel(const int32_t *__restrict__ image, const int32
i += static_cast<size_t>(blockDim.x) * gridDim.x) {
const int32_t k = key[i], v = image[i];
if (k < 0) continue;
if (v == INT32_MIN) {
if (v == INT32_MIN || v == INT32_MAX) { // the error value or the saturation limit: no count
n_error[i]++;
if (ring_ok[k / sectors]) {
n_error_ring_ok[i]++;
@@ -142,7 +142,7 @@ __global__ void accumulate_kernel(const int32_t *__restrict__ image, const int32
}
if (!ring_ok[k / sectors]) continue;
sum_value[i] += v;
if (v == INT32_MAX || static_cast<float>(v) > threshold[k])
if (static_cast<float>(v) > threshold[k])
n_lit[i]++;
}
}
+1 -1
View File
@@ -1218,7 +1218,7 @@ void Rugnux::MaskDefectivePixels(int start_image, const std::vector<int> &sample
pixel_mask_.LoadHotPixelMask(experiment_, mask);
const size_t masked = defects->hot + defects->error + defects->dead;
logger.Info("Defective pixels: {} masked from {} pre-scan frames - {} lit above their ring on more "
"frames than one reflection or chance explains, {} holding the detector's error value, {} dead",
"frames than one reflection or chance explains, {} holding the detector's error value or saturation limit, {} dead",
masked, defects->frames, defects->hot, defects->error, defects->dead);
Note(fmt::format("{} defective pixel{} masked", masked, masked == 1 ? "" : "s"));
};
+8 -5
View File
@@ -25,9 +25,10 @@ namespace {
// A Poisson background with a powder ring, and on it four kinds of pixel: one that reads 60 counts
// high on every frame, one only 20 high - persistent, but too weak to make an outlier - one that holds
// the error value on every frame, and one crossed by a genuine reflection. The last sits close to the rotation axis, where one reflection stays on a pixel for a
// the error value on every frame, one stuck at the saturation limit, and one crossed by a genuine
// reflection, saturated on a few frames. The last sits close to the rotation axis, where one reflection stays on a pixel for a
// long stretch of rotation - here nine consecutive sampled frames, more than the chance bound allows
// but fewer than the one-reflection bound at that zeta. Only the first and the third are masked - and
// but fewer than the one-reflection bound at that zeta. Only the first, third and fourth are masked - and
// a 3x3 patch reading high on every frame, which is stationary in the lab and so no reflection either.
// Three dark pixels besides: one dead on the powder ring, which is masked, one on the ring reading 70%
// of it, and one dead on the 5-count background, whose deficit is too small to make an outlier.
@@ -40,7 +41,7 @@ TEST_CASE("HotPixelFinder_PersistentPixelNotBragg", "[HotPixelFinder]") {
HotPixelFinder finder(x, pixel_mask, 4);
constexpr int HOT_X = 200, HOT_Y = 200, WARM_X = 190, WARM_Y = 60, ERR_X = 60, ERR_Y = 190;
constexpr int BRAGG_X = 40, BRAGG_Y = 115;
constexpr int BRAGG_X = 40, BRAGG_Y = 115, STUCK_X = 100, STUCK_Y = 30;
constexpr int PATCH_X = 150, PATCH_Y = 225;
constexpr int DEAD_X = C + 82, DEAD_Y = C, DIM_X = C, DIM_Y = C + 82, DEAD_BKG_X = 20, DEAD_BKG_Y = 20;
std::mt19937 rng(1);
@@ -57,11 +58,12 @@ TEST_CASE("HotPixelFinder_PersistentPixelNotBragg", "[HotPixelFinder]") {
frame[I(HOT_X, HOT_Y)] += 60;
frame[I(WARM_X, WARM_Y)] += 20;
frame[I(ERR_X, ERR_Y)] = INT32_MIN;
frame[I(STUCK_X, STUCK_Y)] = INT32_MAX;
for (int dy = -1; dy <= 1; dy++)
for (int dx = -1; dx <= 1; dx++)
frame[I(PATCH_X + dx, PATCH_Y + dy)] += 60;
if (f >= 20 && f < 29)
frame[I(BRAGG_X, BRAGG_Y)] += 500;
frame[I(BRAGG_X, BRAGG_Y)] = f < 23 ? INT32_MAX : frame[I(BRAGG_X, BRAGG_Y)] + 500;
frame[I(DEAD_X, DEAD_Y)] = 0;
frame[I(DIM_X, DIM_Y)] = static_cast<int32_t>(0.7 * frame[I(DIM_X, DIM_Y)]);
frame[I(DEAD_BKG_X, DEAD_BKG_Y)] = 0;
@@ -72,6 +74,7 @@ TEST_CASE("HotPixelFinder_PersistentPixelNotBragg", "[HotPixelFinder]") {
CHECK(result.frames == NFRAMES);
CHECK(result.mask[I(HOT_X, HOT_Y)] == 1); // 1 = hot, 2 = error value, 3 = dead
CHECK(result.mask[I(ERR_X, ERR_Y)] == 2);
CHECK(result.mask[I(STUCK_X, STUCK_Y)] == 2);
CHECK(result.mask[I(WARM_X, WARM_Y)] == 0);
CHECK(result.mask[I(BRAGG_X, BRAGG_Y)] == 0);
for (int dy = -1; dy <= 1; dy++)
@@ -81,7 +84,7 @@ TEST_CASE("HotPixelFinder_PersistentPixelNotBragg", "[HotPixelFinder]") {
CHECK(result.mask[I(DIM_X, DIM_Y)] == 0);
CHECK(result.mask[I(DEAD_BKG_X, DEAD_BKG_Y)] == 0);
CHECK(result.hot == 10);
CHECK(result.error == 1);
CHECK(result.error == 2);
CHECK(result.dead == 1);
}