From 20bbcb1cd3a9c47bbfcdbf67b02398246caf8c10 Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Sat, 25 Jul 2026 17:31:35 +0200 Subject: [PATCH] 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 --- common/DiffractionSpot.cpp | 1 - common/DiffractionSpot.h | 3 - common/SpotToSave.h | 1 - .../spot_finding/AdaptiveSpotFinderCPU.cpp | 80 +------------------ .../spot_finding/AdaptiveSpotFinderCPU.h | 9 --- .../spot_finding/SpotFindingSettings.h | 17 ---- image_analysis/spot_finding/SpotUtils.cpp | 15 ++-- image_analysis/spot_finding/SpotUtils.h | 2 +- rugnux/RugnuxCommandLine.cpp | 2 - rugnux/rugnux_cli.cpp | 20 ----- 10 files changed, 7 insertions(+), 143 deletions(-) diff --git a/common/DiffractionSpot.cpp b/common/DiffractionSpot.cpp index 9d65d668..5e6a8857 100644 --- a/common/DiffractionSpot.cpp +++ b/common/DiffractionSpot.cpp @@ -85,7 +85,6 @@ std::optional DiffractionSpot::Export(const DiffractionGeometry &geo .lattice = -1, .image = image_num, .d_A = d, - .weight = weight, .ice_ring = false, .indexed = false }; diff --git a/common/DiffractionSpot.h b/common/DiffractionSpot.h index 43c00ea1..2614b4c9 100644 --- a/common/DiffractionSpot.h +++ b/common/DiffractionSpot.h @@ -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); diff --git a/common/SpotToSave.h b/common/SpotToSave.h index 491f7a96..d866acbc 100644 --- a/common/SpotToSave.h +++ b/common/SpotToSave.h @@ -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; diff --git a/image_analysis/spot_finding/AdaptiveSpotFinderCPU.cpp b/image_analysis/spot_finding/AdaptiveSpotFinderCPU.cpp index e891604d..a551f567 100644 --- a/image_analysis/spot_finding/AdaptiveSpotFinderCPU.cpp +++ b/image_analysis/spot_finding/AdaptiveSpotFinderCPU.cpp @@ -195,83 +195,5 @@ std::vector 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 &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 bg; - bg.reserve((2 * ROUT + 1) * (2 * ROUT + 1)); - std::vector kept; - kept.reserve(spots.size()); - for (const auto &s : spots) { - const Coord c = s.RawCoord(); - const int col = static_cast(std::lround(c.x)); - const int row = static_cast(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(rr) * width + cc]; - if (v == INT32_MIN || v == INT32_MAX) continue; // masked / saturated - bg.push_back(static_cast(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(std::max(s.PixelCount(), 1)); - const double signal = static_cast(s.Count()) - bg_med * npix; - const double snr = signal / (sigma * std::sqrt(npix)); - if (snr >= static_cast(k)) - kept.push_back(s); - } - spots.swap(kept); -} - -void AdaptiveSpotFinderCPU::ApplyWeights(std::vector &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(std::lround(c.x)), 0), width - 1); - const int row = std::min(std::max(static_cast(std::lround(c.y)), 0), height - 1); - const uint16_t b = pixel_to_bin[static_cast(row) * width + col]; - const float mu = (b < nbins) ? ring_mean[b] : 0.0f; - const double N = std::max(s.PixelCount(), 1); - const double tot = std::max(s.Count(), 0); - const double signal = tot - N * mu; - const double noise = std::sqrt(std::max(1.0, tot + N * static_cast(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(snr - 4.0) / 1.5f)); - const float w_size = (1.0f / (1.0f + std::exp(-(static_cast(N) - 1.5f) / 0.7f))) - * (1.0f / (1.0f + std::exp(-(40.0f - static_cast(N)) / 8.0f))); - s.SetWeight(std::min(std::max(w_snr * w_size, 0.0f), 1.0f)); - } + return ExtractSpots(image, settings, res_mask); } diff --git a/image_analysis/spot_finding/AdaptiveSpotFinderCPU.h b/image_analysis/spot_finding/AdaptiveSpotFinderCPU.h index 0faf7590..d75e937b 100644 --- a/image_analysis/spot_finding/AdaptiveSpotFinderCPU.h +++ b/image_analysis/spot_finding/AdaptiveSpotFinderCPU.h @@ -36,15 +36,6 @@ class AdaptiveSpotFinderCPU : public ImageSpotFinder { std::vector 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 &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 &spots, float k) const; public: explicit AdaptiveSpotFinderCPU(const AzimuthalIntegrationMapping &mapping); diff --git a/image_analysis/spot_finding/SpotFindingSettings.h b/image_analysis/spot_finding/SpotFindingSettings.h index 4d1dd099..b69b7072 100644 --- a/image_analysis/spot_finding/SpotFindingSettings.h +++ b/image_analysis/spot_finding/SpotFindingSettings.h @@ -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; }; diff --git a/image_analysis/spot_finding/SpotUtils.cpp b/image_analysis/spot_finding/SpotUtils.cpp index da082741..b682649b 100644 --- a/image_analysis/spot_finding/SpotUtils.cpp +++ b/image_analysis/spot_finding/SpotUtils.cpp @@ -37,19 +37,14 @@ void MarkIceRings(std::vector &spots, float tolerance_q_recipA) { } } -void FilterSpotsByCount(std::vector &input, int64_t count, bool by_weight) { +void FilterSpotsByCount(std::vector &input, int64_t count) { size_t output_size = std::min(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; } diff --git a/image_analysis/spot_finding/SpotUtils.h b/image_analysis/spot_finding/SpotUtils.h index f85441d5..9ba458be 100644 --- a/image_analysis/spot_finding/SpotUtils.h +++ b/image_analysis/spot_finding/SpotUtils.h @@ -18,7 +18,7 @@ void CountSpots(DataMessage &msg, void MarkIceRings(std::vector &spots, float tolerance_q_recipA); -void FilterSpotsByCount(std::vector &input, int64_t count, bool by_weight = false); +void FilterSpotsByCount(std::vector &input, int64_t count); void FilterSpuriousHighResolutionSpots(std::vector &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) diff --git a/rugnux/RugnuxCommandLine.cpp b/rugnux/RugnuxCommandLine.cpp index 7c258ddb..0b6b6bfa 100644 --- a/rugnux/RugnuxCommandLine.cpp +++ b/rugnux/RugnuxCommandLine.cpp @@ -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())); diff --git a/rugnux/rugnux_cli.cpp b/rugnux/rugnux_cli.cpp index a490bf93..94f66566 100644 --- a/rugnux/rugnux_cli.cpp +++ b/rugnux/rugnux_cli.cpp @@ -67,8 +67,6 @@ void print_usage() { std::cout << " --min-pix-per-spot 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 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 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 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 High resolution limit for spot finding (default: 1.5)" << std::endl; std::cout << " --spot-low-resolution 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 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(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(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)