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
123 lines
4.6 KiB
C++
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());
|
|
}
|