ShadowFinder: add beam-stop holder arms that let part of the beam through
After the existing mask is complete, a second step takes the pixels it left out that are
significantly dimmer than their ring, joins them through a 6 px bridge and across module gaps of
any width, and adds a piece whole when it holds >= 2000 dim pixels of which >= 200 are deep. The
existing mask is never touched, so a sweep with no such piece keeps its mask bit for bit.
Quick subset tests (battery --only, no model check) against the rc173 a0518abe6 full battery:
152 of 233 masks identical; 10 of 10 controls (including the sets where earlier shadow changes
regressed through marginal decisions) give byte-identical merges. On the 21 sets that gain a
piece the space group never changes, merge outlier rejections fall on 19, R_meas falls and ISa
rises on 17, and the shell-scaled model R improves on 16 of 18; a transmitting arm with an
over-subtracted background strip is recovered (R_meas 13.3 -> 12.0 %, ISa 13.4 -> 15.5).
Known costs: on one set with background bumps at ring radii the low-resolution agreement with
the model falls (CC 0.89 -> 0.85) while its internal statistics improve, and two sets cut
slightly coarser (1.50 -> 1.55 A, 1.72 -> 1.80 A) with a better model R.
Squashed from branch hq-beamstop (cfa080a59..60b6c6822), whose history records the variants
tried and dropped: a straight port of the earlier arm finder moved every mask, a two-shape
construction filled almost a whole sweep through hole filling, and a deep-fraction floor
rejected a real arm that is dim along its whole length.
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:
@@ -4,6 +4,7 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <climits>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <optional>
|
||||
@@ -103,10 +104,54 @@ TEST_CASE("ShadowFinder_FindsAnInjectedBeamStop", "[ShadowFinder]") {
|
||||
|
||||
// Pinned from the serial implementation. A rewrite of the dilation, the hole fill or the ring
|
||||
// median that moves the mask by one pixel fails here, rather than in a merging statistic
|
||||
// several stages downstream.
|
||||
// several stages downstream. Nothing here lets part of the beam through, so the transmitting
|
||||
// shape may redraw the stop's edge but must not add to the mask: the count is the opaque one.
|
||||
CHECK(std::count(mask.begin(), mask.end(), 1u) == 2612);
|
||||
}
|
||||
|
||||
// A holder arm that lets part of the beam through, on a background bright enough that every pixel
|
||||
// reaches the reflection guard's count, crossing a module gap wider than the bridge. Each of the
|
||||
// three hid the arm on its own: the partly transmitting stretch never reaches SHADOW_RATIO, the gap
|
||||
// cut what did into pieces too small to be believed, and a count threshold read the attenuated
|
||||
// background under the arm as recorded reflections and gave every pixel of it back.
|
||||
TEST_CASE("ShadowFinder_FindsAnArmThatLetsPartOfTheBeamThrough", "[ShadowFinder]") {
|
||||
constexpr int32_t BRIGHT = 50; // even the arm reaches MIN_REFLECTION
|
||||
constexpr int ARM_HALF_WIDE = 15, GAP_X0 = 200, GAP_X1 = 216, OPAQUE_FROM_X = 232;
|
||||
|
||||
const DiffractionExperiment x = TestExperiment();
|
||||
const PixelMask pixel_mask(x);
|
||||
ShadowFinder finder(x, pixel_mask);
|
||||
|
||||
std::vector<std::vector<int32_t>> frames;
|
||||
std::vector<uint8_t> buffer;
|
||||
for (int f = 0; f < NFRAMES; f++) {
|
||||
frames.emplace_back(static_cast<size_t>(W) * H, BRIGHT);
|
||||
auto &frame = frames.back();
|
||||
for (int y = 0; y < H; y++)
|
||||
for (int xi = 0; xi < W; xi++) {
|
||||
const int dx = xi - C, dy = y - C;
|
||||
if (dx * dx + dy * dy <= STOP_R * STOP_R)
|
||||
frame[I(xi, y)] = 0;
|
||||
else if (dx >= 0 && std::abs(dy) <= ARM_HALF_WIDE)
|
||||
frame[I(xi, y)] = xi >= OPAQUE_FROM_X ? 0 : BRIGHT * 6 / 10;
|
||||
if (xi >= GAP_X0 && xi <= GAP_X1)
|
||||
frame[I(xi, y)] = INT32_MIN; // no data
|
||||
}
|
||||
DataMessage msg{};
|
||||
msg.image = CompressedImage(frame, W, H);
|
||||
finder.AddImage(msg, buffer);
|
||||
}
|
||||
|
||||
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_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);
|
||||
}
|
||||
|
||||
// The per-pixel passes are split across threads, so where the split falls must not be visible in the
|
||||
// answer. The x pass and the y pass of the dilation and of the pooled sum are written differently -
|
||||
// one a plain scan, the other blocked by column - so an x/y asymmetry is the plausible regression.
|
||||
|
||||
Reference in New Issue
Block a user