Rugnux: fit the beam centre without the transmitting beam-stop pieces

a1816e905 adds the partly transmitting holder-arm pieces to the beam-stop mask, and the
background beam-centre fit read that mask too. A thin arm running into the beam, masked, takes
cells out of the rings it crosses; which radial bins stay above the sector-coverage gate then
changes as the trial centre moves, and a walk seeded beside the arm (the FFT capture sat 48 px off)
limit-cycled on that edge and stopped 44 px short (sigma 0.34 -> 2.27 px). On an open-arm set whose
header centre is 171 px out, the measured centre no longer indexed (60/60 -> 0/60), the run kept the
file's centre and merged P1 at CC1/2 0.004 instead of P212121 at 0.995.

The arm's dimming is multiplicative in azimuth, which the fit's per-sector amplitude already
absorbs, so the fit now uses the mask without those pieces (ShadowFinder marks them TRANSMITTING);
integration still masks them. The fitted centre is back to its a0518abe6 value on every set checked
(the failing one plus four arm sets); the failing set merges P212121, CC1/2 0.995, ISa 22.6.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D1G8gJVAy6gp1K5Dz3NE5C
This commit is contained in:
2026-09-27 08:34:36 +02:00
co-authored by Claude Opus 5.5
parent e9da892790
commit a42b73ccfb
4 changed files with 28 additions and 9 deletions
+2 -2
View File
@@ -831,7 +831,7 @@ std::vector<uint32_t> ShadowFinder::GetMask(size_t nthreads) const {
region[i] = 0;
for (int i = 0; i < n_pixels; i++)
mask[i] = region[i] ? 1 : 0;
mask[i] = region[i] ? SHADOW : 0;
// Hardware that lets part of the beam through - a thin holder arm, or one whose shadow is blurred
// by its distance from the detector - sits between PENUMBRA_RATIO and SHADOW_RATIO along most of
@@ -878,7 +878,7 @@ std::vector<uint32_t> ShadowFinder::GetMask(size_t nthreads) const {
if (n_dim >= MIN_SHADOW_PIXELS && n_low >= MIN_CORE_PIXELS)
for (const int i : component)
if (dim[i])
mask[i] = 1;
mask[i] = TRANSMITTING;
}
return mask;
}
+6 -1
View File
@@ -98,6 +98,9 @@ class ShadowFinder {
[[nodiscard]] Projection Reduce() const;
public:
static constexpr uint32_t SHADOW = 1;
static constexpr uint32_t TRANSMITTING = 2;
ShadowFinder(const DiffractionExperiment &experiment, const PixelMask &mask);
// The centre the rings are drawn about. It starts as the file's, which is the only one there
@@ -116,7 +119,9 @@ public:
// decompression, reused across the calls of one worker.
void AddImage(const DataMessage &data, std::vector<uint8_t> &buffer, size_t shard = 0);
// Compute the shadow mask (1 = shadow, 0 = keep), of the converted pixel count.
// Compute the shadow mask (SHADOW, TRANSMITTING or 0 = keep), of the converted pixel count.
// TRANSMITTING marks the pieces of hardware that let part of the beam through, added after the
// shadow proper; both are masked, and a consumer that must not see those pieces can tell them apart.
// Recomputed from the accumulators on each call - meant to be called once at the end.
// nthreads = 0 asks for all hardware threads. The per-pixel passes over a 16M-pixel detector
// dominate this, and they are all exactly parallel.
+17 -3
View File
@@ -1245,6 +1245,7 @@ void Rugnux::PreScan(int start_image, int images_to_process, int frame_count, Ru
// Where the scattered background puts the beam, when the projection exists to say so.
std::optional<std::pair<float, float>> ring_centre;
std::vector<uint32_t> opaque_shadow;
if (want_shadow) {
// Every pixel is compared against the ring it sits on, so the rings have to be drawn about
// the beam. A centre far enough out draws them across the background's own radial fall-off
@@ -1272,8 +1273,12 @@ void Rugnux::PreScan(int start_image, int images_to_process, int frame_count, Ru
experiment_.GetBeamX_pxl(), experiment_.GetBeamY_pxl());
}
const auto shadow = finder.GetMask(config_.nthreads);
const auto shadowed = std::count(shadow.begin(), shadow.end(), 1u);
const auto shadowed = std::count_if(shadow.begin(), shadow.end(), [](uint32_t v) { return v != 0; });
pixel_mask_.LoadBeamStopMask(experiment_, shadow);
opaque_shadow = shadow;
for (auto &v : opaque_shadow)
if (v == ShadowFinder::TRANSMITTING)
v = 0;
// Not "this beamline has no beam stop": it is also what a comparison drawn about the wrong
// point returns, so an unexpected answer here is worth reading against the centre named
// above first.
@@ -1315,10 +1320,19 @@ void Rugnux::PreScan(int start_image, int images_to_process, int frame_count, Ru
if (want_hot)
MaskDefectivePixels(start_image, shadow_sample, ring_centre);
// The centre is fitted with the shadow masked but NOT the transmitting pieces the shadow finder
// adds after it. Their dimming is multiplicative in azimuth, which the fit's per-sector amplitude
// absorbs; masked, a thin arm running into the beam takes cells out of the rings it crosses, and
// which rings stay covered then changes as the trial centre moves - a walk seeded beside such an
// arm stalled on that edge 44 px from the centre it reaches with the arm left in.
PixelMask centre_mask = pixel_mask_;
if (!opaque_shadow.empty())
centre_mask.LoadBeamStopMask(experiment_, opaque_shadow);
if (want_shadow && config_.beam_center_check) {
const auto t0 = std::chrono::steady_clock::now();
BeamCenterFFTResult capture;
background_center_ = FindBeamCenter(experiment_, pixel_mask_, finder.GetMeanProjection(),
background_center_ = FindBeamCenter(experiment_, centre_mask, finder.GetMeanProjection(),
config_.nthreads, &capture);
measured_beam_center_ = background_center_;
const auto capture_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
@@ -1516,7 +1530,7 @@ void Rugnux::PreScan(int start_image, int images_to_process, int frame_count, Ru
// same geometry, same projection - so this asks for it again only where it was not run.
estimate = background_center_
? background_center_
: FindBeamCenter(experiment_, pixel_mask_, finder.GetMeanProjection(),
: FindBeamCenter(experiment_, centre_mask, finder.GetMeanProjection(),
config_.nthreads);
}
if (!estimate) {
+3 -3
View File
@@ -144,9 +144,9 @@ TEST_CASE("ShadowFinder_FindsAnArmThatLetsPartOfTheBeamThrough", "[ShadowFinder]
const auto mask = finder.GetMask();
CHECK(mask[I(C, C)] == 1); // the stop
CHECK(mask[I(GAP_X0 - 10, C)] == 1); // the arm where it transmits, before the gap
CHECK(mask[I(GAP_X1 + 10, C + ARM_HALF_WIDE - 3)] == 1); // ... and after it
CHECK(mask[I(W - 3, C)] == 1); // where it is opaque
CHECK(mask[I(GAP_X0 - 10, C)] == ShadowFinder::TRANSMITTING); // the arm where it transmits, before the gap
CHECK(mask[I(GAP_X1 + 10, C + ARM_HALF_WIDE - 3)] == ShadowFinder::TRANSMITTING); // ... and after it
CHECK(mask[I(W - 3, C)] != 0); // where it is opaque
CHECK(mask[I(GAP_X1 + 10, C + ARM_HALF_WIDE + 25)] == 0); // and nothing beside it
CHECK(mask[I(0, 0)] == 0);
CHECK(mask[I(C - 60, C)] == 0);