Everything sparse that took a radial quantity from an image coordinate now takes it from the reflection's (or spot's) position on the detector plane, through its panel (DetectorModules). With an empty module table every changed expression reduces to the old one in the same floating-point order, so flat detectors - every rotation set, EIGER, CCDs, assembled JUNGFRAU - integrate bit for bit as before. - Prediction (CPU, GPU, rotation CPU and GPU): bounds test against PlaneBounds instead of the image rectangle. - IntegrateOnImage: a prediction in a gap or off every panel is dropped (PlaneToPanel), no longer passed through onto an unrelated stored pixel. The panel id stays local: the engine and the way back find it from the image position (the panel boxes tile the image), and a prediction that rounding to float put on another panel's edge is dropped so the two agree. Nothing is added to Reflection. - Bragg engines (CPU, CUDA, Metal) take a BraggPanel per reflection (BraggStencil.h): its plane position, the panel's pixel axes on the plane and its box. The stencil's radial direction is taken on the plane and expressed in the panel's pixel axes; r0, the radial background curve, the Gaussian's elongation, the learned moments, tan(2theta) and Finalize's polarisation use the plane position; r_max / n_rad come from PlaneBounds. The neighbour mask and the owner map are clamped to the reflection's own panel; inside the walked box a pixel off the reflection's panel is read as a gap (unreadable, statically masked - not an overload), which is what the assembled image has there, so a disk cut by a panel edge is partial rather than silently "full". - Risk R4: the ProfileEmpirical grid is learned and applied in the plane's axes (the panel axes taken to the nearest quarter turn, BraggProfileCell), so turned panels pool one shape. Without it the stacked layout read up to 0.3 % off the assembled one in the harness. - SpotWidth / SpotFootprint: aperture bounds against the spot's panel box, beam distance, resolution and the shape's 2theta Jacobian on the plane, moments read in the panel's axes. - GetDetectorMaxQ_recipA, the far-corner radii (first-pass tilt report, metric-symmetry arm, footprint table range) from PlaneBounds; pre-scan spot resolution on the plane; LogHeaderBeamCenter: geometric centre of the panels' plane box, and the masked-pixel check on the panel the beam lands on (none when it is in a gap). Harness: PanelLayout_BraggIntegration is now a real [PanelLayout] check, over six settings (default, bandwidth-elongated ring, radial background, overlap exclude, box sum, empirical profile, all with polarisation), stacked vs assembled to 1e-5; PanelLayout_BraggIntegrationGPU runs the same on the CUDA engine. Its spots avoid exact half pixels: the grid centres on the rounded position, and a half rounds to the other neighbour on a turned panel. The Metal engine (.metal/.mm) mirrors the CPU change exactly but is UNCOMPILED here (no macOS): it needs a build and BraggIntegrationEngineMetal_BitIdenticalToCPU on a Mac. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015CeEGgpKdfkCTJb3YNMJcu
192 lines
9.0 KiB
C++
192 lines
9.0 KiB
C++
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#include "SpotFootprint.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <map>
|
|
#include <tuple>
|
|
|
|
namespace {
|
|
|
|
// The window around a spot is BRAGG_FOOTPRINT_NSIGMA-like: three of its standard deviations, at least
|
|
// a few pixels and at most this many, which is wider than any spot the integrator could hold.
|
|
constexpr float WINDOW_NSIGMA = 3.0f;
|
|
constexpr float WINDOW_MIN_PX = 3.0f;
|
|
constexpr float WINDOW_MAX_PX = 24.0f;
|
|
// The background is the median of an elliptical annulus between these multiples of the window.
|
|
constexpr float BKG_INNER = 1.5f;
|
|
constexpr float BKG_OUTER = 2.2f;
|
|
constexpr int ITERATIONS = 8;
|
|
|
|
inline bool valid(int32_t v) { return v != INT32_MIN && v != INT32_MAX; }
|
|
|
|
float median_of(std::vector<float> &v) {
|
|
const size_t m = v.size() / 2;
|
|
std::nth_element(v.begin(), v.begin() + static_cast<std::ptrdiff_t>(m), v.end());
|
|
return v[m];
|
|
}
|
|
|
|
} // namespace
|
|
|
|
FootprintOutcome MeasureFootprintSpot(const FootprintTile &tile, const BraggPanel &pn, float beam_x, float beam_y,
|
|
float x, float y, FootprintSpot &spot) {
|
|
const int half = static_cast<int>(std::ceil(BKG_OUTER * WINDOW_MAX_PX)) + 1;
|
|
std::vector<float> ring;
|
|
// The distance from the beam on the plane, and the radial direction in the panel's pixel axes.
|
|
const float rx = pn.plane_x - beam_x, ry = pn.plane_y - beam_y;
|
|
const float r = std::sqrt(rx * rx + ry * ry);
|
|
if (!(r > 1.0f)) return FootprintOutcome::LeftOut;
|
|
const float ux = rx / r * pn.fx + ry / r * pn.fy, uy = rx / r * pn.sx + ry / r * pn.sy;
|
|
const int ix = static_cast<int>(std::lround(x)), iy = static_cast<int>(std::lround(y));
|
|
if (ix - half < pn.x0 || iy - half < pn.y0 || ix + half > pn.x1 || iy + half > pn.y1)
|
|
return FootprintOutcome::LeftOut;
|
|
|
|
// Start from a compact spot at the prediction; each round re-centres on the signal and takes
|
|
// the window to three of the widths just measured.
|
|
float cx = x, cy = y;
|
|
float s2r = 1.0f, s2t = 1.0f;
|
|
for (int it = 0; it < ITERATIONS; ++it) {
|
|
const float wr = std::clamp(WINDOW_NSIGMA * std::sqrt(s2r), WINDOW_MIN_PX, WINDOW_MAX_PX);
|
|
const float wt = std::clamp(WINDOW_NSIGMA * std::sqrt(s2t), WINDOW_MIN_PX, WINDOW_MAX_PX);
|
|
const float reach = BKG_OUTER * std::max(wr, wt);
|
|
const int x0 = static_cast<int>(std::floor(cx - reach)), x1 = static_cast<int>(std::ceil(cx + reach));
|
|
const int y0 = static_cast<int>(std::floor(cy - reach)), y1 = static_cast<int>(std::ceil(cy + reach));
|
|
if (x0 < pn.x0 || y0 < pn.y0 || x1 > pn.x1 || y1 > pn.y1) return FootprintOutcome::LeftOut;
|
|
// The re-centring is not bounded, so the window can walk off a tile the frame still holds.
|
|
if (x0 < tile.x0 || y0 < tile.y0 || x1 >= tile.x0 + tile.w || y1 >= tile.y0 + tile.h)
|
|
return FootprintOutcome::OffTile;
|
|
const auto pixel = [&](int px, int py) {
|
|
return tile.pixels[static_cast<size_t>(py - tile.y0) * tile.w + (px - tile.x0)];
|
|
};
|
|
|
|
ring.clear();
|
|
for (int py = y0; py <= y1; ++py)
|
|
for (int px = x0; px <= x1; ++px) {
|
|
const float dx = px - cx, dy = py - cy;
|
|
const float rad = dx * ux + dy * uy, tn = -dx * uy + dy * ux;
|
|
const float e = rad * rad / (wr * wr) + tn * tn / (wt * wt);
|
|
const int32_t v = pixel(px, py);
|
|
if (e >= BKG_INNER * BKG_INNER && e < BKG_OUTER * BKG_OUTER && valid(v))
|
|
ring.push_back(static_cast<float>(v));
|
|
}
|
|
if (ring.size() < 10) return FootprintOutcome::LeftOut;
|
|
const double bkg = median_of(ring);
|
|
|
|
double w = 0.0, mr = 0.0, mt = 0.0, m2r = 0.0, m2t = 0.0;
|
|
for (int py = y0; py <= y1; ++py)
|
|
for (int px = x0; px <= x1; ++px) {
|
|
const float dx = px - cx, dy = py - cy;
|
|
const float rad = dx * ux + dy * uy, tn = -dx * uy + dy * ux;
|
|
if (rad * rad / (wr * wr) + tn * tn / (wt * wt) >= 1.0f) continue;
|
|
const int32_t v = pixel(px, py);
|
|
if (!valid(v)) return FootprintOutcome::LeftOut;
|
|
const double net = static_cast<double>(v) - bkg;
|
|
w += net;
|
|
mr += net * rad;
|
|
mt += net * tn;
|
|
m2r += net * rad * rad;
|
|
m2t += net * tn * tn;
|
|
}
|
|
if (!(w > 0.0)) return FootprintOutcome::LeftOut;
|
|
const double cr = mr / w, ct = mt / w;
|
|
s2r = static_cast<float>(std::max(0.25, m2r / w - cr * cr));
|
|
s2t = static_cast<float>(std::max(0.25, m2t / w - ct * ct));
|
|
cx += static_cast<float>(cr * ux - ct * uy);
|
|
cy += static_cast<float>(cr * uy + ct * ux);
|
|
}
|
|
spot = {r, std::sqrt(s2r), std::sqrt(s2t)};
|
|
return FootprintOutcome::Measured;
|
|
}
|
|
|
|
void MeasureFootprintSpots(const int32_t *img, int width, int height, float beam_x, float beam_y,
|
|
const std::vector<float> &x, const std::vector<float> &y,
|
|
std::vector<FootprintSpot> &out) {
|
|
const FootprintTile frame{img, 0, 0, width, height};
|
|
for (size_t s = 0; s < x.size(); ++s) {
|
|
FootprintSpot spot;
|
|
const BraggPanel pn = {x[s], y[s], 1.0f, 0.0f, 0.0f, 1.0f, 0, 0, width - 1, height - 1};
|
|
if (MeasureFootprintSpot(frame, pn, beam_x, beam_y, x[s], y[s], spot) == FootprintOutcome::Measured)
|
|
out.push_back(spot);
|
|
}
|
|
}
|
|
|
|
SpotFootprint FootprintFromSpots(const std::vector<FootprintSpot> &spots, float r_max) {
|
|
SpotFootprint fp;
|
|
if (!(r_max > 0.0f)) return fp;
|
|
const float bin = r_max / FOOTPRINT_BINS;
|
|
std::vector<std::vector<float>> rad(FOOTPRINT_BINS), tan(FOOTPRINT_BINS);
|
|
for (const auto &s : spots) {
|
|
const int b = std::clamp(static_cast<int>(s.r_px / bin), 0, FOOTPRINT_BINS - 1);
|
|
rad[b].push_back(s.sigma_rad);
|
|
tan[b].push_back(s.sigma_tan);
|
|
}
|
|
std::vector<int> filled;
|
|
std::vector<float> mr(FOOTPRINT_BINS, 0.0f), mt(FOOTPRINT_BINS, 0.0f);
|
|
for (int b = 0; b < FOOTPRINT_BINS; ++b)
|
|
if (static_cast<int>(rad[b].size()) >= FOOTPRINT_MIN_SPOTS_PER_BIN) {
|
|
mr[b] = median_of(rad[b]);
|
|
mt[b] = median_of(tan[b]);
|
|
filled.push_back(b);
|
|
}
|
|
if (filled.empty()) return fp;
|
|
fp.bin_px = bin;
|
|
for (int b = 0; b < FOOTPRINT_BINS; ++b) {
|
|
// The nearest bin that has enough spots; the inner one on a tie.
|
|
int best = filled.front();
|
|
for (int f : filled)
|
|
if (std::abs(f - b) < std::abs(best - b)) best = f;
|
|
fp.sigma_rad.push_back(mr[best]);
|
|
fp.sigma_tan.push_back(mt[best]);
|
|
}
|
|
return fp;
|
|
}
|
|
|
|
void MeasureFootprintOffsets(const std::vector<SpotToSave> &spots, const std::vector<Reflection> &reflections,
|
|
float beam_x, float beam_y, std::vector<FootprintOffset> &out) {
|
|
std::map<std::tuple<int, int, int>, const Reflection *> predicted;
|
|
for (const auto &r : reflections)
|
|
predicted[{r.h, r.k, r.l}] = &r;
|
|
for (const auto &s : spots) {
|
|
if (!s.indexed || s.lattice != 0) continue;
|
|
const auto it = predicted.find({static_cast<int>(s.h), static_cast<int>(s.k), static_cast<int>(s.l)});
|
|
if (it == predicted.end()) continue;
|
|
const float px = it->second->predicted_x, py = it->second->predicted_y;
|
|
const float rx = px - beam_x, ry = py - beam_y;
|
|
const float r = std::sqrt(rx * rx + ry * ry);
|
|
if (!(r > 1.0f)) continue;
|
|
const float ux = rx / r, uy = ry / r;
|
|
const float dx = s.x - px, dy = s.y - py;
|
|
out.push_back({r, dx * ux + dy * uy, -dx * uy + dy * ux});
|
|
}
|
|
}
|
|
|
|
SpotFootprint FootprintWithOffsets(const SpotFootprint &widths, std::vector<FootprintOffset> offsets) {
|
|
if (widths.empty()) return widths;
|
|
// Sorted, so the sums below do not depend on the order the frames were measured in.
|
|
std::sort(offsets.begin(), offsets.end());
|
|
const int n = static_cast<int>(widths.sigma_rad.size());
|
|
std::vector<double> s2r(n, 0.0), s2t(n, 0.0);
|
|
std::vector<int> cnt(n, 0);
|
|
for (const auto &o : offsets) {
|
|
const int b = std::clamp(static_cast<int>(o.r_px / widths.bin_px), 0, n - 1);
|
|
s2r[b] += static_cast<double>(o.off_rad) * o.off_rad;
|
|
s2t[b] += static_cast<double>(o.off_tan) * o.off_tan;
|
|
++cnt[b];
|
|
}
|
|
std::vector<int> filled;
|
|
for (int b = 0; b < n; ++b)
|
|
if (cnt[b] >= FOOTPRINT_MIN_SPOTS_PER_BIN) filled.push_back(b);
|
|
if (filled.empty()) return widths;
|
|
SpotFootprint fp = widths;
|
|
for (int b = 0; b < n; ++b) {
|
|
int best = filled.front();
|
|
for (int f : filled)
|
|
if (std::abs(f - b) < std::abs(best - b)) best = f;
|
|
fp.sigma_rad[b] = static_cast<float>(std::sqrt(widths.sigma_rad[b] * widths.sigma_rad[b] + s2r[best] / cnt[best]));
|
|
fp.sigma_tan[b] = static_cast<float>(std::sqrt(widths.sigma_tan[b] * widths.sigma_tan[b] + s2t[best] / cnt[best]));
|
|
}
|
|
return fp;
|
|
}
|