The per-ring sums were floats reduced by atomics, so the ring sigma - and with it the detection threshold - depended on the order the blocks happened to arrive in. Detection compares an INTEGER pixel value against that threshold, so a threshold that drifts across an integer flips every pixel of that value in the ring at once, which is how a last-bit difference turned into a different spot list. A preprocessed pixel is an exact int32 and the masked and saturated sentinels are skipped, so v and v*v are exact in 64 bits, and integer addition is associative: the sums no longer care about arrival order. Both engines now accumulate the same way, so they agree exactly rather than approximately, and the GPU spot list is bit-identical across runs. The corrected sums that feed the reported azimuthal profile stay float - a pixel value times a float correction has no exact integer form - but they do not enter the detection decision. Cost: the ring reduction needs 28 bytes per bin instead of 20 in the plain pass, which drops it from eight co-resident blocks per SM to seven and costs about 11% of that kernel (0.582 -> 0.650 ms/frame on a 4.5 Mpx frame). End to end it does not show: alternating runs on three rotation crystals came out the same or slightly faster, and the battery is unchanged in every number. The CPU engine got 30% faster (32.2 -> 22.6 ms/frame), integers being cheaper than doubles. Tests: exact CPU/GPU agreement on the spot list, and 50 repeats of bit-identical output where there were four. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
127 lines
5.1 KiB
C++
127 lines
5.1 KiB
C++
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#include <algorithm>
|
|
#include <bitset>
|
|
#include <cmath>
|
|
|
|
#include "AdaptiveSpotFinderCPU.h"
|
|
#include "AdaptiveThreshold.h"
|
|
|
|
AdaptiveSpotFinderCPU::AdaptiveSpotFinderCPU(const AzimuthalIntegrationMapping &in_mapping)
|
|
: ImageSpotFinder(static_cast<int32_t>(in_mapping.GetWidth()),
|
|
static_cast<int32_t>(in_mapping.GetHeight())),
|
|
mapping(in_mapping) {
|
|
const size_t nbins = mapping.GetBinNumber();
|
|
ring_sum.assign(nbins, 0);
|
|
ring_sum2.assign(nbins, 0);
|
|
ring_cnt.assign(nbins, 0);
|
|
ring_mean.assign(nbins, 0.0f);
|
|
ring_sigma.assign(nbins, 0.0f);
|
|
ring_thr.assign(nbins, 0.0f);
|
|
}
|
|
|
|
// Accumulate per-ring mean/variance from the raw (photon) image. clip_k <= 0 -> use every valid
|
|
// pixel (first pass); clip_k > 0 -> keep only pixels within clip_k sigma of the current ring mean,
|
|
// which removes the Bragg peaks from the background estimate.
|
|
void AdaptiveSpotFinderCPU::AccumulateRings(const ImagePreprocessorBuffer &image, float clip_k) {
|
|
const auto &pixel_to_bin = mapping.GetPixelToBin();
|
|
const size_t nbins = ring_sum.size();
|
|
const size_t npix = static_cast<size_t>(width) * height;
|
|
|
|
std::fill(ring_sum.begin(), ring_sum.end(), 0);
|
|
std::fill(ring_sum2.begin(), ring_sum2.end(), 0);
|
|
std::fill(ring_cnt.begin(), ring_cnt.end(), 0);
|
|
|
|
for (size_t pxl = 0; pxl < npix; ++pxl) {
|
|
const int32_t v = image[pxl];
|
|
if (v == INT32_MIN || v == INT32_MAX) continue; // bad / saturated
|
|
const uint16_t b = pixel_to_bin[pxl];
|
|
if (b >= nbins) continue; // masked / out of range (UINT16_MAX)
|
|
if (clip_k > 0.0f) {
|
|
const float lo = ring_mean[b] - clip_k * ring_sigma[b];
|
|
const float hi = ring_mean[b] + clip_k * ring_sigma[b];
|
|
if (v < lo || v > hi) continue; // exclude peaks / outliers
|
|
}
|
|
ring_sum[b] += v;
|
|
ring_sum2[b] += static_cast<uint64_t>(static_cast<int64_t>(v) * v);
|
|
ring_cnt[b] += 1;
|
|
}
|
|
|
|
for (size_t b = 0; b < nbins; ++b) {
|
|
if (ring_cnt[b] > 0) {
|
|
const double m = static_cast<double>(ring_sum[b]) / ring_cnt[b];
|
|
const double var = std::max(0.0, static_cast<double>(ring_sum2[b]) / ring_cnt[b] - m * m);
|
|
ring_mean[b] = static_cast<float>(m);
|
|
ring_sigma[b] = static_cast<float>(std::sqrt(var));
|
|
}
|
|
}
|
|
}
|
|
|
|
void AdaptiveSpotFinderCPU::Detect(const ImagePreprocessorBuffer &image,
|
|
const SpotFindingSettings &settings) {
|
|
const auto &pixel_to_bin = mapping.GetPixelToBin();
|
|
const size_t nbins = ring_sum.size();
|
|
const size_t npix = static_cast<size_t>(width) * height;
|
|
|
|
// --- Stage A: robust per-ring background (one plain pass + two sigma-clip passes) ---
|
|
AccumulateRings(image, 0.0f);
|
|
AccumulateRings(image, 3.0f);
|
|
AccumulateRings(image, 3.0f);
|
|
|
|
// --- Stage B: per-ring threshold from the single portable knob E (false pixels / frame) ---
|
|
int64_t n_total = 0;
|
|
double g_sum = 0.0, g_sum2 = 0.0;
|
|
for (size_t b = 0; b < nbins; ++b) {
|
|
n_total += ring_cnt[b];
|
|
g_sum += static_cast<double>(ring_sum[b]);
|
|
g_sum2 += static_cast<double>(ring_sum2[b]);
|
|
}
|
|
if (n_total == 0) {
|
|
// Nothing valid to threshold against: leave no strong pixels for ExtractSpots to build on.
|
|
std::fill(output_buffer.begin(), output_buffer.end(), 0);
|
|
return;
|
|
}
|
|
|
|
const double E = std::max(1.0f, settings.false_pixels_per_frame);
|
|
double p = E / static_cast<double>(n_total);
|
|
p = std::min(std::max(p, 1e-9), 0.1);
|
|
const float z = static_cast<float>(adaptive_threshold::NormalQuantile(1.0 - p));
|
|
|
|
// whole-frame fallback background for rings too sparse to trust on their own
|
|
const double g_mean = g_sum / n_total;
|
|
const double g_sigma = std::sqrt(std::max(0.0, g_sum2 / n_total - g_mean * g_mean));
|
|
const float g_thr = adaptive_threshold::RingThreshold(static_cast<float>(g_mean),
|
|
static_cast<float>(g_sigma), p, z);
|
|
|
|
for (size_t b = 0; b < nbins; ++b)
|
|
ring_thr[b] = (ring_cnt[b] < adaptive_threshold::MIN_RING_PIXELS)
|
|
? g_thr
|
|
: adaptive_threshold::RingThreshold(ring_mean[b], ring_sigma[b], p, z);
|
|
|
|
// --- Stage C: flag strong pixels into the bit buffer (value >= ring threshold) ---
|
|
for (size_t i = 0; i < OutputSize(); ++i)
|
|
output_buffer[i] = 0;
|
|
|
|
std::bitset<32> out = 0;
|
|
for (size_t pxl = 0; pxl < npix; ++pxl) {
|
|
const int32_t v = image[pxl];
|
|
const uint16_t b = pixel_to_bin[pxl];
|
|
bool strong = false;
|
|
if (v == INT32_MAX)
|
|
strong = true;
|
|
else if (v != INT32_MIN && b < nbins && v >= ring_thr[b])
|
|
strong = true;
|
|
|
|
const int32_t bit = pxl % 32;
|
|
if (strong)
|
|
out.set(bit);
|
|
if (bit == 31) {
|
|
output_buffer[pxl / 32] = out.to_ulong();
|
|
out.reset();
|
|
}
|
|
}
|
|
if (npix % 32 != 0)
|
|
output_buffer[OutputSize() - 1] = out.to_ulong();
|
|
}
|