Choose min-pix-per-spot adaptively per image for serial-stills indexing

For stills indexing the minimum-pixels-per-spot filter is now chosen per image
instead of being fixed: the frame is indexed at min-pix 3/2/1 and the setting that
maximises indexed-spot count weighted by indexed fraction (n_indexed^2 / n_total)
is kept, then integrated once at that min-pix. The fraction factor keeps a smaller
min-pix's extra spots only when the lattice actually explains them, so strong frames
retain their real weak spots (extending resolution) while noise-flooded frames stay
strict.

The mode is selected by the presence of --min-pix-per-spot, now optional
(SpotFindingSettings::min_pix_per_spot is std::optional<int64_t>): omit it for the
adaptive per-image path, give a value to force a fixed min-pix. It applies only to
the stills indexing path -- rotation indexing builds one global lattice and keeps a
fixed min-pix, and the online receiver and the FPGA host path always carry a concrete
value, so neither changes. IndexAndRefine::ProcessImage now returns whether the frame
indexed, to drive the per-image selection.

Exposed in the jfjoch_viewer spot-finding settings (adaptive-threshold and
adaptive-min-pix checkboxes, each greying out the control it overrides); the broker
uses neither.

Validated on the full rotation regression battery (no regression) and the whole
serial-stills target battery at full image count.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-26 18:31:35 +02:00
co-authored by Claude Opus 4.8
parent b65691f312
commit 427f4b3fff
13 changed files with 115 additions and 31 deletions
+53 -13
View File
@@ -3,6 +3,8 @@
#include "MXAnalysisWithoutFPGA.h"
#include <algorithm>
#include "spot_finding/StrongPixelSet.h"
#include "../compression/JFJochDecompress.h"
@@ -117,30 +119,68 @@ void MXAnalysisWithoutFPGA::Analyze(DataMessage &output,
|| mask_low_res != spot_finding_settings.low_resolution_limit)
UpdateMaskResolution(spot_finding_settings);
const auto spot_finding_start_time = std::chrono::steady_clock::now();
ImageSpotFinder &finder = spot_finding_settings.adaptive_threshold
? static_cast<ImageSpotFinder &>(*adaptiveSpotFinder)
: *spotFinder;
const std::vector<DiffractionSpot> spots = finder.Run(*preprocessor_buffer, spot_finding_settings, mask_resolution);
SpotAnalyze(experiment, spot_finding_settings, spots, output);
const auto spot_finding_end_time = std::chrono::steady_clock::now();
output.spot_finding_time_s = std::chrono::duration<float>(spot_finding_end_time - spot_finding_start_time).count();
const auto integrate_fn = [this](const std::vector<Reflection> &predicted, size_t npredicted,
int64_t image_number) {
return bragg_engine->Run(*preprocessor_buffer, predicted, npredicted, image_number);
};
// A missing min-pix (std::nullopt) means "choose it per image". This applies only to the stills
// indexing path (each frame is indexed independently); rotation indexing builds one lattice from
// all frames, so it keeps the fixed min-pix and the single-pass finder.
const bool adaptive_min_pix = !spot_finding_settings.min_pix_per_spot.has_value()
&& spot_finding_settings.indexing
&& !experiment.IsRotationIndexing();
if (adaptive_min_pix) {
// Choose the per-image min-pix adaptively instead of a fixed one. min-pix filters
// connected components AFTER detection, so re-running the finder only re-does the cheap CCL +
// spot filter, not the reduction; the azimuthal profile is identical across attempts. Index
// at 3/2/1 (index-only, no integration/accumulation) and keep whichever maximises
// n_indexed^2 / n_total (indexed count weighted by indexed fraction), then integrate once at
// that min-pix. spot_finding_time_s covers the whole escalation.
const auto start_time = std::chrono::steady_clock::now();
SpotFindingSettings s = spot_finding_settings;
int best_mp = 0;
double best_score = -1.0;
for (int mp : {3, 2, 1}) {
s.min_pix_per_spot = mp;
const std::vector<DiffractionSpot> spots = finder.Run(*preprocessor_buffer, s, mask_resolution);
SpotAnalyze(experiment, s, spots, output);
if (indexer.IndexFrameOnly(output, s)) {
const double n_idx = static_cast<double>(output.spot_count_indexed.value_or(0));
const double n_tot = static_cast<double>(std::max<int64_t>(1, output.spot_count.value_or(1)));
const double score = n_idx * n_idx / n_tot;
if (score > best_score) { best_score = score; best_mp = mp; }
}
}
if (best_mp != 0) {
// Re-run spot finding + index at the winning min-pix and integrate there.
s.min_pix_per_spot = best_mp;
const std::vector<DiffractionSpot> spots = finder.Run(*preprocessor_buffer, s, mask_resolution);
SpotAnalyze(experiment, s, spots, output);
indexer.ProcessImage(output, s, *prediction, integrate_fn);
}
output.spot_finding_time_s = std::chrono::duration<float>(std::chrono::steady_clock::now() - start_time).count();
} else {
const auto spot_finding_start_time = std::chrono::steady_clock::now();
const std::vector<DiffractionSpot> spots = finder.Run(*preprocessor_buffer, spot_finding_settings, mask_resolution);
SpotAnalyze(experiment, spot_finding_settings, spots, output);
output.spot_finding_time_s = std::chrono::duration<float>(std::chrono::steady_clock::now() - spot_finding_start_time).count();
if (spot_finding_settings.indexing)
indexer.ProcessImage(output, spot_finding_settings, *prediction, integrate_fn);
}
#ifdef JFJOCH_USE_CUDA
if (fused) {
// Lift the azimuthal profile the fused engine computed in the same pass; its azint cost is
// folded into spot_finding_time_s above.
// Lift the azimuthal profile the fused engine computed in the same pass (identical across
// any min-pix retries); its azint cost is folded into spot_finding_time_s above.
profile.Clear(integration);
profile += fused_adaptive->GetProfile();
output.azint_time_s = 0.0f;
}
#endif
if (spot_finding_settings.indexing)
indexer.ProcessImage(output, spot_finding_settings, *prediction,
[this](const std::vector<Reflection> &predicted, size_t npredicted, int64_t image_number) {
return bragg_engine->Run(*preprocessor_buffer, predicted, npredicted, image_number);
});
}
output.max_viable_pixel_value = ret.max_value;