Files
Jungfraujoch/image_analysis/MXAnalysisWithoutFPGA.h
T
leonarski_fandClaude Opus 4.8 0ed50d91c9 Add self-calibrating adaptive spot detection for offline stills
The offline CPU spot finder marks a pixel strong when it clears a fixed photon
count AND a local-window SNR. The fixed photon floor forces per-dataset tuning:
its sweet spot tracks the background level (weak sets want a low threshold,
strong or high-background sets a high one) and the usable window is narrow, so
users hand-tune --spot-threshold/--spot-sigma per dataset.

Add an opt-in --adaptive-spots mode (AdaptiveSpotFinderCPU) that replaces the
fixed floor with a per-resolution-ring threshold derived from each image's own
noise. Per ring it computes a peak-excluded background mean and sigma (one plain
pass + two sigma-clip passes over the assembled photon image, binned by the
azimuthal-integration ring index) and sets

    thr = max( PoissonTail(mean, p), mean + z * sqrt(sigma^2 + read^2) )

with p = false_pixels_per_frame / n_pixels the single portable knob (default
100) and z = Phi^-1(1 - p). The Poisson arm is the correct significance where
the background is countable (it carries the sqrt(mean) shot noise, so a bright
low-resolution ring gets a high threshold); the read-noise-floored Gaussian arm
keeps the threshold physical where the background vanishes (empty high-resolution
rings), without which those rings flood. read is a detector-level constant, not
a per-dataset knob. Both arms are needed: Poisson alone floods near-zero
background, Gaussian alone drops the shot-noise term and under-thresholds bright
rings.

One --adaptive-spots setting then adapts across a wide range of serial datasets
with no per-dataset threshold, matching or beating hand-tuned thresholds and the
peakfinder8/xgandalf reference on both weak large-cell and strong serial data,
with equal merged R-free.

The finder runs on the CPU (offline/viewer path) and reads the host image, which
the GPU pipeline already keeps in sync, so it works in either build. The default
(non-adaptive) path and the online/FPGA path are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 17:23:19 +02:00

74 lines
2.9 KiB
C++

// SPDX-FileCopyrightText: 2024 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <mutex>
#include "../common/JFJochMessages.h"
#include "../common/DiffractionExperiment.h"
#include "../common/AzimuthalIntegrationMapping.h"
#include "../common/PixelMask.h"
#include "../common/AzimuthalIntegrationProfile.h"
#include "bragg_prediction/BraggPrediction.h"
#include "bragg_integration/BraggIntegrationEngine.h"
#include "spot_finding/ImageSpotFinder.h"
#include "spot_finding/AdaptiveSpotFinderCPU.h"
#include "indexing/IndexerThreadPool.h"
#include "azint/AzIntEngine.h"
#include "roi/ROIIntegration.h"
#include "IndexAndRefine.h"
#include "image_preprocessing/ImagePreprocessor.h"
#include "image_preprocessing/ImagePreprocessorBuffer.h"
class CudaStream;
// MXAnalysisWithoutFPGA is not thread safe - it has to owned by a single thread
class MXAnalysisWithoutFPGA {
const DiffractionExperiment &experiment;
const AzimuthalIntegrationMapping &integration;
std::vector<uint8_t> decompression_buffer;
std::unique_ptr<ImagePreprocessor> preprocessor;
size_t npixels;
size_t xpixels;
std::unique_ptr<AzIntEngine> azint;
std::unique_ptr<ROIIntegration> roi;
std::unique_ptr<ImageSpotFinder> spotFinder;
// Self-calibrating CPU finder, used when spot settings request adaptive detection. Kept alongside
// the default finder because the choice arrives with the per-image settings, not at construction.
// It reads the host preprocessed image (populated on the GPU path too), so it works in either build.
std::unique_ptr<AdaptiveSpotFinderCPU> adaptiveSpotFinder;
IndexAndRefine &indexer;
std::unique_ptr<BraggPrediction> prediction;
std::unique_ptr<BraggIntegrationEngine> bragg_engine;
std::unique_ptr<ImagePreprocessorBuffer> preprocessor_buffer;
const PixelMask &mask;
std::vector<bool> mask_resolution;
float mask_high_res;
float mask_low_res;
void UpdateMaskResolution(const SpotFindingSettings& settings);
#ifdef JFJOCH_USE_CUDA
std::shared_ptr<CudaStream> stream; // kept so RebuildROI() can recreate the GPU ROI engine
#endif
public:
MXAnalysisWithoutFPGA(const DiffractionExperiment &experiment, const AzimuthalIntegrationMapping &integration,
const PixelMask &mask, IndexAndRefine &indexer);
void Analyze(DataMessage &output, AzimuthalIntegrationProfile &profile, const SpotFindingSettings &spot_finding_settings);
// Surgical ROI-only paths used when a full re-analysis is not wanted: rebuild the
// ROI engine after the ROI set changes, recompute ROIs after preprocessing a new
// image (reanalyze off), or just rerun ROIs on the current preprocessed image (an
// interactive ROI move). A full Analyze() already computes ROIs, so needs nothing.
void RebuildROI();
void AnalyzeROIOnly(DataMessage &output);
void RunROIOnly(DataMessage &output);
};