Remove --soft-weight and --local-snr spot-finder options
Both were opt-in adaptive-spot refinements that did not help. Soft per-spot weighting was index-rate neutral across the battery (re-ranking only bites when spots exceed the max-spot cap, which weak serial data does not reach). The local-SNR gate was neutral on index rate and degraded merged CC1/2 on flooded XFEL data. Drops the flags, ApplyWeights/FilterByLocalSNR, the per-spot weight field, and the by-weight FilterSpotsByCount branch (now strongest-first only). --adaptive-spots itself is unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -195,83 +195,5 @@ std::vector<DiffractionSpot> AdaptiveSpotFinderCPU::Run(const ImagePreprocessorB
|
||||
output_buffer[OutputSize() - 1] = out.to_ulong();
|
||||
|
||||
// --- 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();
|
||||
const float READ = 1.0f;
|
||||
for (auto &s : spots) {
|
||||
const Coord c = s.RawCoord(); // flux-weighted centroid (col, row)
|
||||
const int col = std::min(std::max(static_cast<int>(std::lround(c.x)), 0), width - 1);
|
||||
const int row = std::min(std::max(static_cast<int>(std::lround(c.y)), 0), height - 1);
|
||||
const uint16_t b = pixel_to_bin[static_cast<size_t>(row) * width + col];
|
||||
const float mu = (b < nbins) ? ring_mean[b] : 0.0f;
|
||||
const double N = std::max<int64_t>(s.PixelCount(), 1);
|
||||
const double tot = std::max<int64_t>(s.Count(), 0);
|
||||
const double signal = tot - N * mu;
|
||||
const double noise = std::sqrt(std::max(1.0, tot + N * static_cast<double>(READ) * READ));
|
||||
const double snr = signal / noise;
|
||||
// Dimensionless gates (sigma, pixels): high SNR -> keep; a reasonable pixel count -> keep, while
|
||||
// 1-pixel noise (rising edge) and oversized ice/salt/streak blobs (falling edge) -> ~0.
|
||||
const float w_snr = 1.0f / (1.0f + std::exp(-static_cast<float>(snr - 4.0) / 1.5f));
|
||||
const float w_size = (1.0f / (1.0f + std::exp(-(static_cast<float>(N) - 1.5f) / 0.7f)))
|
||||
* (1.0f / (1.0f + std::exp(-(40.0f - static_cast<float>(N)) / 8.0f)));
|
||||
s.SetWeight(std::min(std::max(w_snr * w_size, 0.0f), 1.0f));
|
||||
}
|
||||
return ExtractSpots(image, settings, res_mask);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user