Add opt-in local-SNR spot gate and acceptance-fraction knob (serial stills)
Two opt-in tools for weak serial-stills tuning; both default-off, so the default pipeline is bit-identical (verified: a serial-stills reference run reproduces HEAD's 7.85% indexing rate exactly). --local-snr <sigma> (AdaptiveSpotFinderCPU::FilterByLocalSNR): after the loose per-ring adaptive threshold builds connected-component spots, drop any spot that does not stand this many sigmas above its OWN LOCAL background (robust median/MAD of a square annulus), not just the azimuthal ring mean. On structured-background (XFEL) frames the ring mean underestimates the local diffuse level in some sectors, so the ring threshold floods; a real Bragg peak still stands many local sigmas proud. Validated on XFEL stills to separate real peaks from flood at the pixel level (real median local-SNR ~70 vs flood ~2.6; SNR>=5 keeps ~99.8% of real peaks, ~14% of flood). GPU-portable (a per-spot local reduction). NOTE: on the current serial-stills battery it is index-rate/CC1/2 neutral -- the flood that survives as CC clusters overlaps weak-real spots, and only lattice-fit separates those -- but it is the correct tool for genuinely floody data (ice/jet/loosened detector) and the right substrate for the online FPGA path. --min-indexed-fraction <f>: exposes the previously hardcoded 0.20 minimum indexed-spot fraction (AnalyzeIndexing) as a per-run setting. Lowering it admits weaker/sparser crystals; on flooded XFEL data the extra lattices are spurious (pair with --min-image-cc to gate them), on clean synchrotron data there are no marginal frames so it is a no-op -- useful as a gating-experiment primitive. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -200,11 +200,62 @@ std::vector<DiffractionSpot> AdaptiveSpotFinderCPU::Run(const ImagePreprocessorB
|
||||
|
||||
// --- Stage D: connected components + resolution mask + min/max-pix (shared with classic path) ---
|
||||
auto spots = ExtractSpots(image, settings, res_mask);
|
||||
if (settings.local_snr > 0.0f)
|
||||
FilterByLocalSNR(image, spots, settings.local_snr);
|
||||
if (settings.soft_weight)
|
||||
ApplyWeights(spots);
|
||||
return spots;
|
||||
}
|
||||
|
||||
// Reject spots that do not stand out against their LOCAL background. The loose per-ring threshold
|
||||
// keeps ~100% of real Bragg peaks but, on structured-background (XFEL) frames, also floods with
|
||||
// spurious pixels: the azimuthal ring mean underestimates the diffuse level in some sectors, so a
|
||||
// locally-high background pixel clears it. A real peak stands many sigmas above the background in
|
||||
// its IMMEDIATE neighbourhood, a flood pixel does not. For each spot the background mean and scatter
|
||||
// are measured from a square annulus around its centroid (robust median / MAD, so a neighbouring
|
||||
// peak in the annulus cannot bias it), and the spot is kept only if its integrated signal exceeds
|
||||
// k local sigmas. k is in sigma units -- self-calibrating, no photon threshold.
|
||||
void AdaptiveSpotFinderCPU::FilterByLocalSNR(const ImagePreprocessorBuffer &image,
|
||||
std::vector<DiffractionSpot> &spots, float k) const {
|
||||
constexpr int RIN = 3; // half-width of the excluded core (7x7)
|
||||
constexpr int ROUT = 6; // half-width of the background annulus (13x13)
|
||||
std::vector<float> bg;
|
||||
bg.reserve((2 * ROUT + 1) * (2 * ROUT + 1));
|
||||
std::vector<DiffractionSpot> kept;
|
||||
kept.reserve(spots.size());
|
||||
for (const auto &s : spots) {
|
||||
const Coord c = s.RawCoord();
|
||||
const int col = static_cast<int>(std::lround(c.x));
|
||||
const int row = static_cast<int>(std::lround(c.y));
|
||||
bg.clear();
|
||||
for (int dr = -ROUT; dr <= ROUT; ++dr) {
|
||||
const int rr = row + dr;
|
||||
if (rr < 0 || rr >= height) continue;
|
||||
for (int dc = -ROUT; dc <= ROUT; ++dc) {
|
||||
if (std::max(std::abs(dr), std::abs(dc)) <= RIN) continue; // skip the peak core
|
||||
const int cc = col + dc;
|
||||
if (cc < 0 || cc >= width) continue;
|
||||
const int32_t v = image[static_cast<size_t>(rr) * width + cc];
|
||||
if (v == INT32_MIN || v == INT32_MAX) continue; // masked / saturated
|
||||
bg.push_back(static_cast<float>(v));
|
||||
}
|
||||
}
|
||||
if (bg.size() < 8) { kept.push_back(s); continue; } // too few bg pixels to judge
|
||||
const size_t mid = bg.size() / 2;
|
||||
std::nth_element(bg.begin(), bg.begin() + mid, bg.end());
|
||||
const float bg_med = bg[mid];
|
||||
for (auto &v : bg) v = std::fabs(v - bg_med);
|
||||
std::nth_element(bg.begin(), bg.begin() + mid, bg.end());
|
||||
const float sigma = std::max(1.4826f * bg[mid], 1.0f);
|
||||
const double npix = static_cast<double>(std::max<int64_t>(s.PixelCount(), 1));
|
||||
const double signal = static_cast<double>(s.Count()) - bg_med * npix;
|
||||
const double snr = signal / (sigma * std::sqrt(npix));
|
||||
if (snr >= static_cast<double>(k))
|
||||
kept.push_back(s);
|
||||
}
|
||||
spots.swap(kept);
|
||||
}
|
||||
|
||||
void AdaptiveSpotFinderCPU::ApplyWeights(std::vector<DiffractionSpot> &spots) const {
|
||||
const auto &pixel_to_bin = mapping.GetPixelToBin();
|
||||
const size_t nbins = ring_mean.size();
|
||||
@@ -338,6 +389,8 @@ std::vector<DiffractionSpot> AdaptiveSpotFinderCPU::RunPersistence(const ImagePr
|
||||
std::vector<DiffractionSpot> out;
|
||||
out.reserve(spots.size());
|
||||
for (auto &kv : spots) out.push_back(kv.second);
|
||||
if (settings.local_snr > 0.0f)
|
||||
FilterByLocalSNR(image, out, settings.local_snr);
|
||||
if (settings.soft_weight)
|
||||
ApplyWeights(out);
|
||||
return out;
|
||||
|
||||
Reference in New Issue
Block a user