// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute // SPDX-License-Identifier: GPL-3.0-only #pragma once #include #include #include #include "../../common/CompressedImage.h" #include "../../common/DiffractionExperiment.h" #include "../../common/JFJochMessages.h" // DataMessage // Tunable parameters for ShadowFinder. Plain struct with sensible defaults; when the // finder is wired into the workflow these can move onto DiffractionExperiment the way // DarkMaskSettings does. See SHADOW_FINDER.md for what each one does. struct ShadowFinderSettings { // A pixel is "shadow core" when its mean is below this fraction of the typical // (azimuthal-median) background at the same radius. float shadow_ratio = 0.35f; // The soft boundary grows outward into partially-shadowed pixels down to this // fraction of the background, but no further than penumbra_max_px from the core. float penumbra_ratio = 0.72f; int penumbra_max_px = 14; // Bridge module gaps / small breaks that the holder arm crosses (pixels). int bridge_px = 6; // A pixel whose max-projection reaches this value recorded a real reflection and is // never masked - a beam stop cannot block a reflection that was measured. This also // caps the central disk just inside the innermost such reflection. float min_reflection = 25.0f; }; // Detects the beam-stop shadow (central disk + holder arm) from a small number of // images, mirroring the accumulate-then-finalize shape of DarkMaskAnalysis: feed frames // with AddImage(), then read the mask once with GetMask(). The returned mask is in // converted geometry and is 1 where the beam stop shadows the detector. // // The shadow is treated as an azimuthal anomaly: a per-radius background baseline is // robust to the shadow, so a localized dip connected to the beam centre is the beam // stop. See SHADOW_FINDER.md for the full algorithm and the (deferred) wiring plan. // Thread-safe: AddImage may be called from several worker threads. class ShadowFinder { mutable std::mutex m; const int width; const int height; const double beam_x; const double beam_y; const ShadowFinderSettings settings; // Per-pixel projection over the frames added so far (converted geometry). std::vector max_value; // maximum over frames std::vector sum_value; // sum of valid values std::vector valid_count; // number of frames the pixel carried data uint32_t frames = 0; template void Add(const T *ptr); public: ShadowFinder(const DiffractionExperiment &experiment, ShadowFinderSettings settings = {}); // Accumulate one full converted-geometry image into the projection. Gap / masked // pixels (the pixel type's sentinel extreme) are skipped. `buffer` is scratch space // for decompression (mirrors DarkMaskAnalysis::AnalyzeImage). void AddImage(const DataMessage &data, std::vector buffer); // Compute the beam-stop shadow mask (1 = shadow, 0 = keep). Size is the converted // pixel count. Recomputed from the accumulators on each call - meant to be called // once at the end; not cheap (see SHADOW_FINDER.md). [[nodiscard]] std::vector GetMask() const; [[nodiscard]] uint32_t GetFrameCount() const; };