GetMask() was 3.28 s of the 4.78 s pre-scan on a 16M-pixel detector, all on one thread. Four changes, none of which alters the mask: dilate() was a multi-source BFS. On a full rectangle with no obstacles the 8-connected graph distance IS the Chebyshev distance - a path stepping towards the target never has to leave the frame - so the result is a dilation by the (2r+1) square clipped to the frame, which separates into a pass along x and a pass along y. That is O(1) per pixel whatever r is, with no queue and no 4-bytes-per-pixel distance array (72 MB, allocated and filled five times per call). The erode() case is the one that hurt: it dilates the COMPLEMENT, so on a detector whose shadow is under 1% of the pixels it seeded the BFS from essentially every pixel. fill_holes() floods the background from the border. It now floods the bounding box of the region grown by one: everything outside that box is background and the box's own ring is background, so the whole outside is one border-connected component and a background pixel inside the box is border-connected exactly when it reaches the ring. The three baseline iterations re-binned every pixel by radius and re-took a median each time. The iteration only ever excludes pixels whose background is below a cut, and dividing by a positive baseline is monotone, so a ring's excluded pixels are exactly its lowest ones and the next median is an order statistic of the same, unchanging ring. The rings are binned and sorted once; each iteration then picks a rank and counts a prefix. Nine full-image passes become one. box_sum's vertical pass walked one column at a time, striding a whole row per step and missing on every access; it now carries a strip of columns together. Each row's and each column's running sum keeps its terms in its order, so the floating-point rounding is unchanged - only the traversal differs. The pooled COUNT is a count of at most 25 pixels, so it is an exact integer box sum now rather than a floating-point one; the background itself stays in double, because its running sum adds and subtracts across a whole row and in float the two roundings would not cancel. The per-pixel passes then run on all threads, and GetMask takes a thread count. Measured on a 16M-pixel rotation dataset: GetMask 3.28 s -> 0.99 s, whole pre-scan 4.78 s -> 2.37 s, whole run 1m10s -> 1m03s. The mask is unchanged on both a 16M and a 2M-pixel dataset (139126 and 22143 shadow pixels), as are the space group, the merged reflection count and the merging statistics. Also corrected the comment on erode(): the dilation cannot seed outside the frame, so outside behaves as foreground, not as complement. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
88 lines
4.1 KiB
C++
88 lines
4.1 KiB
C++
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#pragma once
|
|
|
|
#include <cstdint>
|
|
#include <mutex>
|
|
#include <vector>
|
|
|
|
#include "../../common/CompressedImage.h"
|
|
#include "../../common/DiffractionExperiment.h"
|
|
#include "../../common/JFJochMessages.h"
|
|
#include "../../common/PixelMask.h"
|
|
|
|
// Finds the beam-stop shadow - the central disk and the holder arm - from a set of images,
|
|
// mirroring the accumulate-then-finalize shape of DarkMaskAnalysis: feed frames with
|
|
// AddImage(), then read the mask once with GetMask(). The mask is in converted geometry
|
|
// and is 1 where the beam stop shadows the detector.
|
|
//
|
|
// The shadow is a place where the background is missing, so it is found by comparing each
|
|
// pixel's mean against the typical background at the same radius - the median over its ring,
|
|
// taken over the pixels not already known to be shadowed. That comparison holds wherever the
|
|
// ring still has unshadowed pixels to measure. Where it does not - a ring lying wholly inside
|
|
// the stop - there is nothing to compare against, and such a ring is shadow in its entirety.
|
|
//
|
|
// The background belongs to the beam and the shadow to the stop, and the two are not concentric:
|
|
// the stop sits off the beam by a sizeable fraction of its own radius. Only the per-ring
|
|
// comparison is used, so nothing here assumes they share a centre.
|
|
//
|
|
// Frames are chosen by the caller; the detection needs enough of them that the background
|
|
// is counted rather than guessed (see MIN_EXPECTED_COUNTS in the .cpp).
|
|
// Thread-safe: workers call AddImage concurrently, each naming a shard of its own (see
|
|
// SetShardCount) - so no two threads touch the same accumulator and nothing is locked while
|
|
// an image is added. The shards are summed when the projection is read.
|
|
class ShadowFinder {
|
|
mutable std::mutex m;
|
|
|
|
const int width;
|
|
const int height;
|
|
const float beam_x;
|
|
const float beam_y;
|
|
|
|
std::vector<uint32_t> pixel_mask; // pixels already masked carry no background to test
|
|
|
|
// Per-pixel projection over the frames added so far (converted geometry). One set per shard:
|
|
// the sums and counts are integers, so summing the shards is exact and the result does not
|
|
// depend on how the frames were spread over them.
|
|
struct Projection {
|
|
std::vector<int64_t> max_value;
|
|
std::vector<int64_t> sum_value;
|
|
std::vector<uint32_t> valid_count;
|
|
uint32_t frames = 0;
|
|
};
|
|
std::vector<Projection> shards;
|
|
|
|
template<class T> void Add(const T *ptr, Projection &p);
|
|
|
|
// Sum the shards into one projection. max_value is only taken from a shard that actually
|
|
// counted the pixel - a shard that never saw it holds 0, which would beat a genuinely
|
|
// negative maximum.
|
|
[[nodiscard]] Projection Reduce() const;
|
|
|
|
public:
|
|
ShadowFinder(const DiffractionExperiment &experiment, const PixelMask &mask);
|
|
|
|
// Give each worker a shard to accumulate into. Must be called before the first AddImage,
|
|
// and costs 20 bytes per pixel per shard.
|
|
void SetShardCount(size_t n);
|
|
|
|
// Accumulate one full converted-geometry image into shard `shard`. Gap / masked pixels
|
|
// (the pixel type's sentinel extreme) are skipped. `buffer` is scratch space for
|
|
// 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.
|
|
// 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.
|
|
[[nodiscard]] std::vector<uint32_t> GetMask(size_t nthreads = 0) const;
|
|
|
|
// Mean counts per pixel over the frames added, NAN where nothing was counted. This is the
|
|
// projection GetMask() tests, so anything else that wants the background before indexing
|
|
// gets it without reading the frames a second time.
|
|
[[nodiscard]] std::vector<float> GetMeanProjection() const;
|
|
|
|
[[nodiscard]] uint32_t GetFrameCount() const;
|
|
};
|