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:
@@ -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
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()));
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user