Add image_analysis/beam_stop/ShadowFinder.{h,cpp} and SHADOW_FINDER.md: a
self-contained beam-stop shadow detector that accumulates images via AddImage()
and returns a mask from GetMask(), mirroring the shape of DarkMaskAnalysis.
It detects the beam-stop shadow (central disk + holder arm) as an azimuthal
anomaly: an iterated radial-median background baseline, a ratio threshold, a
connectivity-to-beam-centre anchor with module-gap bridging, a central low-res
disk guard capped just inside the innermost reflection, and a reflection guard
that never masks a pixel that recorded real signal.
Not yet wired: not added to image_analysis/CMakeLists.txt and no PixelMask /
Rugnux / viewer changes. SHADOW_FINDER.md documents the algorithm and the
deferred offline (Rugnux bit 9 + viewer user-mask) integration plan.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
77 lines
3.3 KiB
C++
77 lines
3.3 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" // 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<int32_t> max_value; // maximum over frames
|
|
std::vector<int64_t> sum_value; // sum of valid values
|
|
std::vector<uint32_t> valid_count; // number of frames the pixel carried data
|
|
uint32_t frames = 0;
|
|
|
|
template<class T> 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<uint8_t> 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<uint32_t> GetMask() const;
|
|
|
|
[[nodiscard]] uint32_t GetFrameCount() const;
|
|
};
|