Files
Jungfraujoch/image_analysis/bragg_integration/BackgroundAperture.cpp
T
leonarski_fandClaude Opus 5.5 e8145d8d2f Stills integration: background ring reach measured on empty apertures, radial curvature from the whole frame
Two changes to the stills integration background, both stills-only (rotation keeps its path bit for bit):

1. The r2..r3 background ring's outer radius is measured per run. The pre-scan lays apertures on a
   grid over its frames, away from every found spot, and for each candidate r3 (r2+2 .. 14) takes the
   mean square difference between the r1 disk mean and the clipped ring mean. With nothing in the
   aperture that is exactly the background error the ring makes (noise + bias); the run keeps the
   minimum. On a photon-counting pixel detector the noise wins and the shipped r3=14 is kept; on a
   binned CCD the background structure wins and the narrowest ring is taken. r1 stays 6: a smaller
   disk (4,6,14) costs 0.0013 dimple R_free on a JUNGFRAU set, so it does not pass the
   photon-counting gate. --adaptive-integration-radius now defaults on for stills too (=off restores
   the fixed 6/8/14). The pre-scan bandwidth estimate is not applied to stills.

2. The radial background-curvature correction on stills takes its curve from the adaptive spot
   finder's peak-excluded per-ring background (whole frame) instead of the predicted reflections'
   own ring pixels, and auto applies it to every stills image (no ice-score gate: a solvent/carrier
   hump curves the background as much as ice). New engine call RadialBackgroundFromRings; CPU, GPU
   and Metal read the handed-over curve. The GPU box sum now sizes the radial bin index separately
   from accumulating, so a handed-over curve is read at the right radius.

Measured (dimple R_free, deposited free set, battery arguments, all frames unless noted):
  pal86  (Rayonix 4x4, 156 um): 0.354 -> 0.218   (ring 10; at 4,6,10 0.213)
  lcls222 (Rayonix 4x4, 177 um): 0.205 -> 0.196  (ring 10)
  sacla236 (MPCCD): 0.2588 -> 0.2558             (ring 11)
  alvra213_int16 (JUNGFRAU): 0.1986 -> 0.1986    (ring 14, unchanged)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CeEGgpKdfkCTJb3YNMJcu
2026-10-21 18:10:08 +02:00

123 lines
4.6 KiB
C++

// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include "BackgroundAperture.h"
#include <algorithm>
#include <cmath>
std::vector<float> background_aperture::CandidateR3(float r2, float max_r3) {
std::vector<float> out;
for (float r3 = r2 + 2.0f; r3 <= max_r3 + 1e-3f; r3 += 1.0f)
out.push_back(r3);
return out;
}
void background_aperture::MeasureFrame(ImagePreprocessorBuffer &image, int width, int height,
const std::vector<DiffractionSpot> &spots, float r1, float r2,
const std::vector<float> &r3, float clip_nsigma, float gain,
Tally &tally) {
if (r3.empty())
return;
tally.sum_d2.resize(r3.size(), 0.0);
const float r_max = r3.back();
const int half = static_cast<int>(std::ceil(r_max));
const int side = 2 * half + 1;
// A grid of centres, at least one aperture apart, kept clear of every found spot.
const double area = static_cast<double>(width) * height;
const int step = std::max(side, static_cast<int>(std::ceil(std::sqrt(area / MAX_APERTURES_PER_FRAME))));
const float clear = r_max + SPOT_CLEARANCE_PX;
std::vector<int32_t> cx, cy;
for (int y = half; y + half < height; y += step)
for (int x = half; x + half < width; x += step) {
bool near_spot = false;
for (const auto &s : spots) {
const Coord c = s.RawCoord();
if ((c.x - x) * (c.x - x) + (c.y - y) * (c.y - y) < clear * clear) {
near_spot = true;
break;
}
}
if (!near_spot) {
cx.push_back(x);
cy.push_back(y);
}
}
if (cx.empty())
return;
const int32_t *tiles = image.GatherTiles(cx, cy, width, half);
std::vector<double> ring_sum(r3.size());
std::vector<int> ring_n(r3.size());
std::vector<int32_t> ring_px;
std::vector<float> ring_d2;
for (size_t a = 0; a < cx.size(); ++a) {
const int32_t *t = tiles + a * static_cast<size_t>(side) * side;
double disk_sum = 0.0;
int disk_n = 0;
bool readable = true;
ring_px.clear();
ring_d2.clear();
for (int dy = -half; dy <= half && readable; ++dy)
for (int dx = -half; dx <= half; ++dx) {
const float d2 = static_cast<float>(dx * dx + dy * dy);
if (d2 >= r_max * r_max)
continue;
const int32_t v = t[(dy + half) * side + dx + half];
if (v == INT32_MIN || v == INT32_MAX) {
readable = false;
break;
}
if (d2 < r1 * r1) {
disk_sum += v;
++disk_n;
} else if (d2 >= r2 * r2) {
ring_px.push_back(v);
ring_d2.push_back(d2);
}
}
if (!readable || disk_n == 0)
continue;
// Every candidate ring as the integrator reads it: its mean, then one high-side clip pass.
bool measured = true;
for (size_t k = 0; k < r3.size() && measured; ++k) {
double sum = 0.0;
int n = 0;
for (size_t p = 0; p < ring_px.size(); ++p)
if (ring_d2[p] < r3[k] * r3[k]) {
sum += ring_px[p];
++n;
}
if (n == 0) {
measured = false;
break;
}
const double mean = sum / n;
const double thr = mean + clip_nsigma * std::sqrt(gain * std::max(mean, 1.0));
ring_sum[k] = 0.0;
ring_n[k] = 0;
for (size_t p = 0; p < ring_px.size(); ++p)
if (ring_d2[p] < r3[k] * r3[k] && (clip_nsigma <= 0.0f || ring_px[p] <= thr)) {
ring_sum[k] += ring_px[p];
++ring_n[k];
}
measured = ring_n[k] > 0;
}
if (!measured)
continue;
const double disk = disk_sum / disk_n;
for (size_t k = 0; k < r3.size(); ++k) {
const double d = disk - ring_sum[k] / ring_n[k];
tally.sum_d2[k] += d * d;
}
++tally.apertures;
}
}
int background_aperture::Best(const Tally &tally) {
if (tally.apertures == 0 || tally.sum_d2.empty())
return -1;
return static_cast<int>(std::min_element(tally.sum_d2.begin(), tally.sum_d2.end()) - tally.sum_d2.begin());
}