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:
2026-07-25 17:31:35 +02:00
co-authored by Claude Opus 4.8
parent f72b4484e2
commit 20bbcb1cd3
10 changed files with 7 additions and 143 deletions
-1
View File
@@ -85,7 +85,6 @@ std::optional<SpotToSave> DiffractionSpot::Export(const DiffractionGeometry &geo
.lattice = -1,
.image = image_num,
.d_A = d,
.weight = weight,
.ice_ring = false,
.indexed = false
};
-3
View File
@@ -15,10 +15,7 @@ class DiffractionSpot {
int64_t pixel_count = 0;
int64_t photons = 0; // total photon count
int64_t max_photons = INT64_MIN; // maximum number of counts per pixel in the spot
float weight = 1.0f; // soft quality weight in (0,1]; 1 = "no opinion" (default)
public:
void SetWeight(float w) { weight = w; }
float Weight() const { return weight; }
DiffractionSpot() = default;
DiffractionSpot(uint32_t col, uint32_t line, int64_t photons);
DiffractionSpot(const SpotToSave &save);
-1
View File
@@ -16,7 +16,6 @@ struct SpotToSave {
int64_t h = 0, k = 0, l= 0;
float d_A = 0.0;
float dist_ewald_sphere = 0.0;
float weight = 1.0f; // soft quality weight in (0,1]; used to rank the kept spots (see --soft-weight)
bool ice_ring = false;
bool indexed = false;
@@ -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);
}
@@ -36,15 +36,6 @@ class AdaptiveSpotFinderCPU : public ImageSpotFinder {
std::vector<float> ring_thr;
void AccumulateRings(const ImagePreprocessorBuffer &image, float clip_k);
// Assign each spot a soft quality weight (SNR against the per-ring background x a soft size band),
// consumed downstream to keep the best spots rather than the brightest. Needs the ring background,
// so it must run after AccumulateRings.
void ApplyWeights(std::vector<DiffractionSpot> &spots) const;
// Drop spots that do not stand k sigmas above their own LOCAL (annulus) background. The
// discriminator the per-ring threshold lacks on structured-background frames; see the header
// comment on SpotFindingSettings::local_snr.
void FilterByLocalSNR(const ImagePreprocessorBuffer &image,
std::vector<DiffractionSpot> &spots, float k) const;
public:
explicit AdaptiveSpotFinderCPU(const AzimuthalIntegrationMapping &mapping);
@@ -31,21 +31,4 @@ struct SpotFindingSettings {
// per frame (the threshold's operating point), ~100 for a multi-megapixel detector.
bool adaptive_threshold = false;
float false_pixels_per_frame = 100.0f;
// Soft per-spot weighting (implies adaptive detection): assign every detected spot a continuous
// quality weight from its SNR (against the per-ring background) and a soft size band (too few or
// too many pixels -> low), then keep the highest-WEIGHT spots rather than the brightest. Lets a
// deliberately loose detector self-clean -- bright ice/salt/jet blobs and single-pixel noise no
// longer evict faint clean Bragg spots. See AdaptiveSpotFinderCPU::ApplyWeights.
bool soft_weight = false;
// Local-background SNR gate (implies adaptive detection): after connected-component spots are
// built from the loose per-ring threshold, reject any spot whose integrated signal does not
// clear this many sigmas above its OWN LOCAL background (measured from a robust annulus around
// it), 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 with
// spurious pixels; a real Bragg peak still stands many local sigmas proud, so this recovers the
// clean spot list a global threshold cannot. In sigma units -> self-calibrating, no photon
// threshold. 0 disables. See AdaptiveSpotFinderCPU::FilterByLocalSNR.
float local_snr = 0.0f;
};
+5 -10
View File
@@ -37,19 +37,14 @@ void MarkIceRings(std::vector<SpotToSave> &spots, float tolerance_q_recipA) {
}
}
void FilterSpotsByCount(std::vector<SpotToSave> &input, int64_t count, bool by_weight) {
void FilterSpotsByCount(std::vector<SpotToSave> &input, int64_t count) {
size_t output_size = std::min<size_t>(input.size(), count);
std::ranges::partial_sort(input, input.begin() + output_size,
std::ranges::less{}, // comparator on the projected key
[by_weight](const SpotToSave &s) {
// projection: key to compare by. non-ice first (false < true), then
// by soft quality weight (higher first) when requested -- so a loose
// detector's bright junk cannot evict faint clean Bragg -- else by
// raw intensity. Intensity is the tie-breaker under the weight.
if (by_weight)
return std::tuple{s.ice_ring, -s.weight, -s.intensity};
return std::tuple{s.ice_ring, 0.0f, -s.intensity};
[](const SpotToSave &s) {
// projection: non-ice first (false < true), then strongest intensity first.
return std::tuple{s.ice_ring, -s.intensity};
});
input.resize(output_size);
}
@@ -156,7 +151,7 @@ void SpotAnalyze(const DiffractionExperiment &experiment,
output.resolution_estimate = GetResolution(spots_out);
FilterSpotsByCount(spots_out, experiment.GetMaxSpotCount(), spot_finding_settings.soft_weight);
FilterSpotsByCount(spots_out, experiment.GetMaxSpotCount());
output.spots = spots_out;
}
+1 -1
View File
@@ -18,7 +18,7 @@ void CountSpots(DataMessage &msg,
void MarkIceRings(std::vector<SpotToSave> &spots, float tolerance_q_recipA);
void FilterSpotsByCount(std::vector<SpotToSave> &input, int64_t count, bool by_weight = false);
void FilterSpotsByCount(std::vector<SpotToSave> &input, int64_t count);
void FilterSpuriousHighResolutionSpots(std::vector<SpotToSave> &spots, float threshold);
// Ignore high res. spots if there is a gap in (1/d) between two spots of dist_threshold (default: 0.25 A^-1)
-2
View File
@@ -87,8 +87,6 @@ std::string RugnuxCommandLine(const ProcessConfig &config,
add("--spot-threshold", std::to_string(sf.photon_count_threshold));
if (sf.adaptive_threshold)
add("--spot-false-pixels", num(sf.false_pixels_per_frame));
if (sf.local_snr > 0.0f)
add("--local-snr", num(sf.local_snr));
add("--spot-high-resolution", num(sf.high_resolution_limit));
add("--max-spots", std::to_string(experiment.GetMaxSpotCount()));
-20
View File
@@ -67,8 +67,6 @@ void print_usage() {
std::cout << " --min-pix-per-spot <num> Minimum connected strong pixels per spot (default: 2; serial data can index better with 1 + a higher --spot-threshold)" << std::endl;
std::cout << " --adaptive-spots Self-calibrating detection: replace the fixed --spot-threshold with a per-resolution-ring threshold set from each image's own noise, so one setting adapts across datasets (no per-dataset --spot-threshold/--spot-sigma tuning)" << std::endl;
std::cout << " --spot-false-pixels <num> Adaptive detection operating point: expected noise pixels tolerated per frame (default: 100; implies --adaptive-spots)" << std::endl;
std::cout << " --soft-weight With --adaptive-spots: weight each spot by SNR + a soft size band and keep the highest-quality (not brightest) spots, so bright ice/salt/noise cannot crowd out faint Bragg" << std::endl;
std::cout << " --local-snr <sigma> With --adaptive-spots: keep only spots standing this many sigma above their own LOCAL background (annulus), removing structured-background (XFEL) flood the ring threshold lets through (e.g. 5; implies --adaptive-spots)" << std::endl;
std::cout << " --min-indexed-fraction <f> Minimum fraction of in-resolution spots a lattice must index to be accepted (default 0.20); lower to admit weaker/sparser crystals (pair with --min-image-cc to gate the extra spurious ones)" << std::endl;
std::cout << " --spot-high-resolution <num> High resolution limit for spot finding (default: 1.5)" << std::endl;
std::cout << " --spot-low-resolution <num> Low resolution limit for spot finding, in A (default: 50; lower it, e.g. 24, to exclude the direct-beam halo on weakly-diffracting serial data)" << std::endl;
@@ -150,8 +148,6 @@ enum {
OPT_MIN_PIX_PER_SPOT,
OPT_ADAPTIVE_SPOTS,
OPT_SPOT_FALSE_PIXELS,
OPT_SOFT_WEIGHT,
OPT_LOCAL_SNR,
OPT_MIN_INDEXED_FRACTION,
OPT_SPOT_RESOLUTION,
OPT_SPOT_LOW_RESOLUTION,
@@ -264,8 +260,6 @@ static option long_options[] = {
{"min-pix-per-spot", required_argument, nullptr, OPT_MIN_PIX_PER_SPOT},
{"adaptive-spots", no_argument, nullptr, OPT_ADAPTIVE_SPOTS},
{"spot-false-pixels", required_argument, nullptr, OPT_SPOT_FALSE_PIXELS},
{"soft-weight", no_argument, nullptr, OPT_SOFT_WEIGHT},
{"local-snr", required_argument, nullptr, OPT_LOCAL_SNR},
{"min-indexed-fraction", required_argument, nullptr, OPT_MIN_INDEXED_FRACTION},
{"spot-high-resolution", required_argument, nullptr, OPT_SPOT_RESOLUTION},
{"spot-low-resolution", required_argument, nullptr, OPT_SPOT_LOW_RESOLUTION},
@@ -532,8 +526,6 @@ int main(int argc, char **argv) {
int64_t min_pix_per_spot = 2;
bool adaptive_spots = false;
float false_pixels_per_frame = 100.0f;
bool soft_weight_flag = false;
float local_snr = 0.0f;
std::optional<float> min_indexed_fraction;
bool refine_bfactor = false;
std::string ref_mtz;
@@ -773,16 +765,6 @@ int main(int argc, char **argv) {
adaptive_spots = true;
logger.Info("Adaptive spot detection: expected false pixels/frame set to {:.0f}", false_pixels_per_frame);
break;
case OPT_SOFT_WEIGHT:
adaptive_spots = true;
soft_weight_flag = true;
logger.Info("Soft per-spot weighting enabled (keep highest-quality spots, not brightest)");
break;
case OPT_LOCAL_SNR:
local_snr = parse_number_arg<float>(optarg, "--local-snr", logger, 0.0f);
adaptive_spots = true;
logger.Info("Local-background SNR gate enabled: keep spots >= {:.1f} sigma above their local background", local_snr);
break;
case OPT_MIN_INDEXED_FRACTION:
min_indexed_fraction = parse_number_arg<float>(optarg, "--min-indexed-fraction", logger, 0.0f);
logger.Info("Minimum indexed-spot fraction for acceptance set to {:.2f}", min_indexed_fraction.value());
@@ -1527,8 +1509,6 @@ int main(int argc, char **argv) {
spot_settings.min_pix_per_spot = min_pix_per_spot;
spot_settings.adaptive_threshold = adaptive_spots;
spot_settings.false_pixels_per_frame = false_pixels_per_frame;
spot_settings.soft_weight = soft_weight_flag;
spot_settings.local_snr = local_snr;
if (d_min_spot_finding > 0.0f)
spot_settings.high_resolution_limit = d_min_spot_finding;
if (d_max_spot_finding > 0.0f)