Files
Jungfraujoch/image_analysis/beam_stop/ShadowFinder.h
T
leonarski_fandClaude Opus 4.8 9bfecaadf5 beam_stop: add ShadowFinder detection class (unwired)
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>
2026-07-18 12:05:05 +02:00

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;
};