Files
Jungfraujoch/image_analysis/geom_refinement/BeamCenterFFT.h
T
leonarski_f 9aae0c2ba7
Build Packages / Create release (push) Successful in 21s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 9m40s
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 9m49s
Build Packages / build:viewer-tgz:cpu (push) Successful in 11m37s
Build Packages / build:viewer-tgz:cuda (push) Successful in 12m40s
Build Packages / build:windows:nocuda (push) Successful in 17m44s
Build Packages / build:windows:cuda (push) Successful in 20m13s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 14m41s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 25m59s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 15m5s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 14m35s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 15m53s
Build Packages / build:rugnux:windows (push) Successful in 11m29s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 18m51s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 18m43s
Build Packages / Generate python client (push) Successful in 51s
Build Packages / build:rpm (rocky8) (push) Successful in 18m51s
Build Packages / Build documentation (push) Successful in 1m21s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 18m38s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 18m24s
Build Packages / build:rpm (rocky9) (push) Successful in 19m19s
Build Packages / Unit tests (push) Successful in 1h37m15s
v1.0.0-rc.169 (#79)
* Building Jungfraujoch no longer needs zlib or Eigen installed on the machine, and the dependencies the build fetches are pinned and updated to current releases.
* rugnux: improvements in indexing, lattice selection and geometry post-refinement, which index crystals that previously returned no lattice and keep the better of the two geometries a run measures.
* rugnux: improvements in beam-centre measurement, beam-stop detection and space-group determination.
* rugnux: the unit cell reported with a determined space group now obeys that group - a cell whose symmetry was confirmed from the intensities is re-refined under it, and a cell the group cannot describe is reported with a warning rather than as it stands.
* rugnux drops the stretches of a rotation sweep whose removal measurably improves the merged intensities and reports what became of every frame, and decides the resolution cut on the crystal's own diffraction rather than on its ice rings.
* The rugnux results report is machine-readable - every line that is not `KEY= value` data starts with `#` - and states the build it was written by, its authorship and its terms of use (`REPORT_VERSION= 8`).
* `jfjoch_viewer`: improvements in the file manager (CBF frames beside HDF5 datasets, a remembered root), the dataset plots, the inspector and the image statistics, plus a settable font size, a view of the rugnux results report, usable performance over a remote display (`ssh -X`) and a reset of all settings to defaults; the reciprocal-space window is removed.
* Broker fixes around DECTRIS collections and dark-mask calibration: re-initialising after a run that never started no longer freezes the broker, a cancelled calibration is abandoned instead of reported as done, and a collection whose start message never arrives ends by itself.

Reviewed-on: #79
Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
2026-09-15 17:09:31 +02:00

82 lines
4.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 <vector>
#include "../../common/DiffractionExperiment.h"
#include "../../common/PixelMask.h"
// Whole-detector beam-centre capture from the symmetry of the scattered image, via FFT.
//
// The centrosymmetry score of the image about a candidate centre c, sum_x I(x) I(2c - x), is the
// self-convolution (I * I)(2c), so one FFT pair scores EVERY candidate centre on the detector at
// half-pixel spacing in a single transform set - the cost is O(N log N) and independent of how far
// the true centre is from the value in the file. Three surfaces are computed:
//
// * the 2D point-inversion score - a measurement of the isotropic background (air and solvent
// scatter), point-symmetric about the beam whatever the crystal does. It shares its physics
// with FindBeamCenterFromBackground, so their agreement is not two votes;
// * two 1D line-mirror scores, one per detector axis, mirroring that coordinate only. For the
// coordinate along the spindle this is exact Bragg physics: a reflection's Friedel mate
// half a turn later lands mirrored in the line through the beam across the spindle
// (R_m(180)(-q) = q - 2(q.m)m), so accumulating frames over the sweep makes this score sharp
// where the indexing count is blind.
//
// The score is a CAPTURE device, not a decision device: measured on 17 datasets its shortlist
// holds the true centre in the top candidates every time (three gross header errors of 71-351 px
// captured at rank 1 within 1 px), but the surface can be locally flat, so the result is a
// shortlist plus a margin - never a single centre. The margin (best minus runner-up, as a
// fraction of best, after suppression) is the self-diagnostic: measured, ambiguous surfaces sit
// below ~3 % and clean ones above ~5 %. Callers must verify a candidate (index at it) before
// believing it, and must not average candidates.
//
// The input is the mean-projection the pre-scan already accumulates for the beam-stop search
// (ShadowFinder::GetMeanProjection), NAN where nothing was counted - so this costs no new I/O.
//
// The transforms run on the GPU where there is one with room for them and on fftw3f otherwise
// (BeamCenterFFTGPU / BeamCenterFFTCPU); everything that decides anything is shared between the
// two. Whole-detector reach is what this buys, and on a 16 Mpixel detector the transforms are the
// whole of its cost, so which engine runs them is the difference between seconds and milliseconds.
struct BeamCenterFFTCandidate {
float beam_x_pxl = 0.0f; // NAN in a line_y candidate
float beam_y_pxl = 0.0f; // NAN in a line_x candidate
float score = 0.0f; // masked Pearson r of the image with its mirror about this centre
};
struct BeamCenterFFTResult {
// Non-maximum-suppressed shortlists, strongest first.
std::vector<BeamCenterFFTCandidate> point; // 2D point-inversion score
std::vector<BeamCenterFFTCandidate> line_x; // beam-x from the x line mirror
std::vector<BeamCenterFFTCandidate> line_y; // beam-y from the y line mirror
// (best - runner_up) / best per surface; 0 when fewer than two peaks survive.
float margin_point = 0.0f;
float margin_line_x = 0.0f;
float margin_line_y = 0.0f;
};
struct BeamCenterFFTSettings {
float nms_radius_pxl = 25.0f; // peak separation; tracks the score's plateau width
int candidates_point = 8;
int candidates_line = 4;
// Fraction of the maximum mirror-pair count below which a centre is not scored - centres so
// far out that almost no pixel has a valid mirror partner correlate noise with noise.
float min_pair_fraction = 0.25f;
};
// Core entry: `mean` is a width x height row-major per-pixel projection, NAN (or any non-finite
// value) where no valid pixel exists. Kept free of DiffractionExperiment so it can be tested
// against stored arrays.
[[nodiscard]] BeamCenterFFTResult
BeamCenterFFTScore(int64_t width, int64_t height, const std::vector<float> &mean,
const BeamCenterFFTSettings &settings = {});
// Convenience wrapper over the pre-scan projection.
[[nodiscard]] BeamCenterFFTResult
FindBeamCenterFFT(const DiffractionExperiment &experiment, const std::vector<float> &mean,
const BeamCenterFFTSettings &settings = {});