PixelRefine: Some improvements
Build Packages / Generate python client (push) Successful in 23s
Build Packages / Create release (push) Skipped
Build Packages / build:rpm (rocky9) (push) Failing after 4m58s
Build Packages / Build documentation (push) Successful in 1m25s
Build Packages / XDS test (neggia plugin) (push) Successful in 13m57s
Build Packages / XDS test (durin plugin) (push) Successful in 15m44s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 16m9s
Build Packages / DIALS test (push) Successful in 22m45s
Build Packages / Unit tests (push) Successful in 1h44m30s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Failing after 3m0s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Failing after 3m4s
Build Packages / build:rpm (rocky8_nocuda) (push) Failing after 3m12s
Build Packages / build:rpm (rocky9_nocuda) (push) Failing after 3m58s
Build Packages / build:rpm (rocky8_sls9) (push) Failing after 4m20s
Build Packages / build:rpm (rocky8) (push) Failing after 4m18s
Build Packages / build:rpm (rocky9_sls9) (push) Failing after 4m55s
Build Packages / build:rpm (ubuntu2204) (push) Failing after 4m20s
Build Packages / build:rpm (ubuntu2404) (push) Failing after 4m17s

This commit is contained in:
2026-06-11 21:25:05 +02:00
parent 48d4fb0d0f
commit d31063ca3f
10 changed files with 206 additions and 50 deletions
+2 -1
View File
@@ -25,7 +25,8 @@ struct Reflection {
float sigma;
float dist_ewald;
float rlp;
float partiality;
float partiality; // fraction of the reflection recorded in the sampled (rocking) slice
float completeness = 1.0f; // fraction of the spot footprint on live pixels (1 = not clipped by edge/gap/mask)
float zeta;
float image_scale_corr; // I_true = scaling_correction * I; scaling_correction = rlp / (partiality * image_scale)
bool observed = false;
+107 -49
View File
@@ -33,6 +33,7 @@ struct ReflGroup {
double Itrue; // reference intensity (held fixed)
double R_bw_sq; // bandwidth radial-width^2 contribution (0 = monochromatic)
double pol; // per-reflection polarization correction (raw = true * pol)
double Ibkg; // local flat background (raw counts, constant over the shoebox)
double predicted_x, predicted_y;
std::vector<PixelObs> pixels;
};
@@ -83,6 +84,23 @@ std::vector<uint8_t> BuildSpotMask(const std::vector<Reflection> &predicted, int
return mask;
}
// Square shoebox bounds (inclusive) around a predicted spot, clamped to the
// detector. The centre is rounded to the nearest pixel with std::lround so the
// signal box is centred identically to the spot-core mask (BuildSpotMask) and
// the local-background ring (EstimateLocalBackground), which also lround. Used by
// Run and the diagnostic renderers so all three share one shoebox definition.
struct ShoeboxBox { int min_x, max_x, min_y, max_y; };
ShoeboxBox ShoeboxBounds(double px, double py, int radius, size_t xpixel, size_t ypixel) {
const int cx = static_cast<int>(std::lround(px));
const int cy = static_cast<int>(std::lround(py));
return {
std::max(cx - radius, 0),
std::min<int>(cx + radius, static_cast<int>(xpixel) - 1),
std::max(cy - radius, 0),
std::min<int>(cy + radius, static_cast<int>(ypixel) - 1)
};
}
// Local flat background around one shoebox, in raw detector counts. Samples the
// square ring shoebox_radius < max(|dx|,|dy|) <= bkg_outer_radius centred on the
// spot, dropping pixels that belong to any spot core (spot_mask) or carry a
@@ -633,16 +651,14 @@ void PixelRefine::Run(const T *image,
g.Itrue = reference_data[hkl];
g.R_bw_sq = bandwidth_radial_sq(refl.d);
g.pol = polarization(refl.predicted_x, refl.predicted_y);
g.Ibkg = Ibkg;
g.predicted_x = refl.predicted_x;
g.predicted_y = refl.predicted_y;
const int min_y = std::max<int>(refl.predicted_y - radius, 0);
const int max_y = std::min<int>(refl.predicted_y + radius, ypixel - 1);
const int min_x = std::max<int>(refl.predicted_x - radius, 0);
const int max_x = std::min<int>(refl.predicted_x + radius, xpixel - 1);
const auto box = ShoeboxBounds(refl.predicted_x, refl.predicted_y, radius, xpixel, ypixel);
for (int y = min_y; y <= max_y; ++y) {
for (int x = min_x; x <= max_x; ++x) {
for (int y = box.min_y; y <= box.max_y; ++y) {
for (int x = box.min_x; x <= box.max_x; ++x) {
const size_t npixel = xpixel * y + x;
// Skip sentinel (masked / saturated) pixels. We assume the pixel
@@ -826,52 +842,100 @@ void PixelRefine::Run(const T *image,
} // predict<->refine iterations
// ---- Extract integrated reflections ---------------------------------------
// Profile fitting gives the recorded amplitude (against the tangential profile
// P_t):
// J = sum_p[ P_t,p (Iobs_p - Ibkg_p)/v_p ] / sum_p[ P_t,p^2 / v_p ]
// Profile fitting gives the recorded amplitude (fitting the tangential profile
// P_t against the background-subtracted pixels):
// J = sum_p[ P_t,p (Iobs_p - Ibkg)/v_p ] / sum_p[ P_t,p^2 / v_p ]
// ~ G * Itrue * B_term * partiality * pol (recorded raw counts)
// var(J) = 1 / sum_p[ P_t,p^2 / v_p ]
//
// Two SEPARATE fractions reduce the full intensity to what these pixels record:
//
// partiality - the radial / rocking dimension that a still does NOT sample.
// Only the slice of the reflection that crosses the Ewald
// sphere on this shot is recorded; <= 1. We DIVIDE it out to
// recover the full intensity. = profile-weighted P_radial.
//
// completeness - the fraction of the spot's detector footprint that landed on
// live pixels (= profile captured by live pixels / profile over
// the whole shoebox). 1.0 when the spot sits fully on the
// detector; < 1.0 only when a detector edge, gap or mask clips
// it. Profile fitting already extrapolates over the missing
// pixels, so this is NOT applied to r.I - it is a quality flag.
//
// Output split (Merge multiplies r.I * image_scale_corr and weights by
// 1/(sigma*image_scale_corr)^2 - see Merge.cpp):
// r.I = J / (B_term * partiality * pol) = G * Itrue
// r.sigma = sqrt(var(J)) / (B_term * partiality * pol)
// r.partiality = profile-weighted peak radial factor in (0,1] (Merge filter only)
// r.partiality = profile-weighted P_radial in (0,1] (the rocking fraction)
// r.completeness = live/total tangential profile in (0,1] (detector clipping)
// r.image_scale_corr = 1/G (per-image scale ONLY)
// so r.I * image_scale_corr = Itrue. B, partiality and polarization live on the
// intensity, G lives on image_scale_corr - one clean meaning per field.
//
// We walk the full (unclamped) shoebox once: every grid point feeds the total
// tangential profile (completeness denominator); points that are real, live
// detector pixels also feed the profile fit and the captured profile.
data.reflections.reserve(groups.size());
for (const auto &g : groups) {
double num = 0.0, den = 0.0, bkg_sum = 0.0;
double radial_sum = 0.0, radial_w = 0.0;
const int cx = static_cast<int>(std::lround(g.predicted_x));
const int cy = static_cast<int>(std::lround(g.predicted_y));
// Debye-Waller factor for this reflection (constant over its shoebox).
const double B_term = std::exp(-data.B_factor / (4.0 * g.d * g.d));
double num = 0.0, den = 0.0, bkg_sum = 0.0, radial_sum = 0.0;
double prof_live = 0.0, prof_full = 0.0; // tangential profile: captured / total
size_t n = 0;
for (const auto &obs : g.pixels) {
PixelResidual pr(obs, 1.0, lambda, pixel_size, g.h, g.k, g.l, g.R_bw_sq, g.pol, data.crystal_system);
double q_sq, eps_r, eps_t_sq;
if (!pr.GeometryTerms(beam, &dist_mm, detector_rot, latt_vec0, latt_vec1, latt_vec2, q_sq,
eps_r, eps_t_sq))
continue;
if (!(data.R[0] > 0.0) || !(data.R[1] > 0.0))
continue;
for (int y = cy - radius; y <= cy + radius; ++y) {
for (int x = cx - radius; x <= cx + radius; ++x) {
// Geometry/profile for this grid point (valid even off the detector).
PixelObs probe{static_cast<double>(x), static_cast<double>(y), 0.0, g.Ibkg, 1.0};
PixelResidual pr(probe, 1.0, lambda, pixel_size, g.h, g.k, g.l,
g.R_bw_sq, g.pol, data.crystal_system);
double q_sq, eps_r, eps_t_sq;
if (!pr.GeometryTerms(beam, &dist_mm, detector_rot,
latt_vec0, latt_vec1, latt_vec2, q_sq, eps_r, eps_t_sq))
continue;
if (!(data.R[0] > 0.0) || !(data.R[1] > 0.0))
continue;
// Tangential profile shape -> fit weight (every pixel counts equally).
const double P_t = std::exp(-eps_t_sq / (data.R[1] * data.R[1]))
/ (M_PI * data.R[1] * data.R[1]);
// Peak-normalized radial factor (the partiality), in (0,1].
// Bandwidth-broadened radial width, matching the model in Model().
const double R0_eff_sq = data.R[0] * data.R[0] + g.R_bw_sq;
const double P_radial = std::exp(-eps_r * eps_r / R0_eff_sq);
// Tangential profile shape (area-normalized) -> the fit template.
const double P_t = std::exp(-eps_t_sq / (data.R[1] * data.R[1]))
/ (M_PI * data.R[1] * data.R[1]);
prof_full += P_t; // whole shoebox, on- or off-detector
const double v = SafeInv(obs.weight * obs.weight, 1.0); // pixel variance
const double signal = obs.Iobs - obs.Ibkg;
// Only real, unmasked detector pixels carry signal.
if (x < 0 || x >= static_cast<int>(xpixel) || y < 0 || y >= static_cast<int>(ypixel))
continue;
const size_t np = static_cast<size_t>(xpixel) * y + x;
if (image[np] == std::numeric_limits<T>::max())
continue;
if (std::is_signed_v<T> && image[np] == std::numeric_limits<T>::min())
continue;
num += P_t * signal / v;
den += P_t * P_t / v;
radial_sum += P_radial * P_t; // weight partiality by the spot core
radial_w += P_t;
bkg_sum += obs.Ibkg;
++n;
const double Iobs = static_cast<double>(image[np]); // raw counts
double v = std::max(Iobs, 0.0); // Poisson variance
if (!(v > 1.0))
v = 1.0;
// Peak-normalized radial factor (the partiality), in (0,1]. The
// bandwidth-broadened radial width matches the model in Model().
const double R0_eff_sq = data.R[0] * data.R[0] + g.R_bw_sq;
const double P_radial = std::exp(-eps_r * eps_r / R0_eff_sq);
// Profile-fit accumulators. The amplitude estimator weights pixels by
// P_t^2/v, so the partiality (which de-scales that amplitude) MUST use
// the SAME weights - otherwise an R0_eff-dependent (resolution-
// dependent) factor is left behind in r.I.
const double w = P_t * P_t / v;
num += P_t * (Iobs - g.Ibkg) / v;
den += w;
radial_sum += P_radial * w; // partiality weighted exactly like num/den
prof_live += P_t; // captured tangential profile
bkg_sum += g.Ibkg;
++n;
}
}
Reflection r{};
@@ -884,12 +948,12 @@ void PixelRefine::Run(const T *image,
r.observed_x = NAN;
r.observed_y = NAN;
r.rlp = 1.0f;
r.partiality = (radial_w > 0.0) ? static_cast<float>(radial_sum / radial_w) : 1.0f;
r.partiality = (den > 0.0) ? static_cast<float>(radial_sum / den) : 1.0f;
r.completeness = (prof_full > 0.0) ? static_cast<float>(prof_live / prof_full) : 1.0f;
if (den > 0.0 && n > 0) {
const double I_amp = num / den; // ~ G*Itrue*B_term*partiality*pol
const double sigma_amp = std::sqrt(1.0 / den);
const double B_term = std::exp(-data.B_factor / (4.0 * g.d * g.d));
const double corr = static_cast<double>(r.partiality) * B_term * g.pol; // B, partiality & pol
r.bkg = static_cast<float>(bkg_sum / static_cast<double>(n));
r.observed = true;
@@ -1010,13 +1074,10 @@ std::vector<float> PixelRefine::PredictImage(const T *image,
refl.predicted_x, refl.predicted_y,
radius, bkg_outer_radius, Ibkg);
const int min_y = std::max<int>(refl.predicted_y - radius, 0);
const int max_y = std::min<int>(refl.predicted_y + radius, ypixel - 1);
const int min_x = std::max<int>(refl.predicted_x - radius, 0);
const int max_x = std::min<int>(refl.predicted_x + radius, xpixel - 1);
const auto box = ShoeboxBounds(refl.predicted_x, refl.predicted_y, radius, xpixel, ypixel);
for (int y = min_y; y <= max_y; ++y) {
for (int x = min_x; x <= max_x; ++x) {
for (int y = box.min_y; y <= box.max_y; ++y) {
for (int x = box.min_x; x <= box.max_x; ++x) {
const size_t npixel = xpixel * y + x;
PixelObs obs{
@@ -1106,13 +1167,10 @@ std::vector<float> PixelRefine::ChiSquaredImage(const T *image,
radius, bkg_outer_radius, Ibkg))
continue;
const int min_y = std::max<int>(refl.predicted_y - radius, 0);
const int max_y = std::min<int>(refl.predicted_y + radius, ypixel - 1);
const int min_x = std::max<int>(refl.predicted_x - radius, 0);
const int max_x = std::min<int>(refl.predicted_x + radius, xpixel - 1);
const auto box = ShoeboxBounds(refl.predicted_x, refl.predicted_y, radius, xpixel, ypixel);
for (int y = min_y; y <= max_y; ++y) {
for (int x = min_x; x <= max_x; ++x) {
for (int y = box.min_y; y <= box.max_y; ++y) {
for (int x = box.min_x; x <= box.max_x; ++x) {
const size_t npixel = xpixel * y + x;
// Same gating as Run(): only pixels that actually enter the fit.
@@ -3,6 +3,8 @@
#pragma once
#include <optional>
#include "../bragg_prediction/BraggPrediction.h"
#include "../common/DiffractionExperiment.h"
#include "../scale_merge/HKLKey.h"
@@ -198,4 +200,14 @@ public:
std::vector<float> ChiSquaredImage(const T *image,
BraggPrediction &prediction,
const PixelRefineData &data) const;
// Reference (merged) intensity used as the fixed hypothesis for a reflection,
// or nullopt if this hkl is not in the reference. Lets callers show the fitted
// estimate next to the reference it was scaled against.
std::optional<double> ReferenceIntensity(const Reflection &r) const {
const auto it = reference_data.find(hkl_key_generator(r));
if (it == reference_data.end())
return std::nullopt;
return it->second;
}
};
+2
View File
@@ -88,6 +88,8 @@ ADD_EXECUTABLE(jfjoch_viewer jfjoch_viewer.cpp JFJochViewerWindow.cpp JFJochView
windows/JFJochViewerReciprocalSpaceWindow.h
windows/JFJochPixelRefineWindow.cpp
windows/JFJochPixelRefineWindow.h
windows/JFJochPixelRefineTableWindow.cpp
windows/JFJochPixelRefineTableWindow.h
windows/JFJochMagnifierWindow.cpp
windows/JFJochMagnifierWindow.h
windows/PixelRefineParams.h
+38
View File
@@ -70,6 +70,7 @@ JFJochImageReadingWorker::JFJochImageReadingWorker(const SpotFindingSettings &se
indexing_settings(experiment.GetIndexingSettings()),
azint_settings(experiment.GetAzimuthalIntegrationSettings()) {
qRegisterMetaType<PixelRefineParams>("PixelRefineParams");
qRegisterMetaType<PixelRefineReport>("PixelRefineReport");
qRegisterMetaType<QVector<QRect>>("QVector<QRect>");
spot_finding_settings = settings;;
@@ -852,6 +853,41 @@ QVector<QRect> JFJochImageReadingWorker::BuildShoeboxes_i(const PixelRefineData
return boxes;
}
PixelRefineReport JFJochImageReadingWorker::BuildReport_i(const PixelRefineData &data) const {
PixelRefineReport report;
report.pr_G = data.scale_factor;
report.pr_B = data.B_factor;
report.pr_cc = data.cc;
report.pr_cc_n = data.cc_n;
// Standard ScaleOnTheFly pipeline result for the same image, as a baseline.
if (current_image_ptr) {
const auto &d = current_image_ptr->ImageData();
if (d.image_scale_factor) report.pipe_G = d.image_scale_factor.value();
if (d.image_scale_b_factor) report.pipe_B = d.image_scale_b_factor.value();
if (d.image_scale_cc) report.pipe_cc = d.image_scale_cc.value();
}
report.rows.reserve(data.reflections.size());
for (const auto &r : data.reflections) {
if (!r.observed)
continue;
PixelRefineReport::Row row;
row.h = r.h; row.k = r.k; row.l = r.l;
row.d = r.d;
row.completeness = r.completeness;
row.partiality = r.partiality;
row.I = r.I;
row.sigma = r.sigma;
if (std::isfinite(r.image_scale_corr))
row.I_true_est = static_cast<double>(r.I) * static_cast<double>(r.image_scale_corr);
if (pixel_refine_)
row.I_true_ref = pixel_refine_->ReferenceIntensity(r).value_or(NAN);
report.rows.push_back(row);
}
return report;
}
std::vector<float> JFJochImageReadingWorker::BuildDisplayImage_i(const PixelRefineData &data,
int display_mode) const {
const auto &img32 = current_image_ptr->Image();
@@ -909,6 +945,7 @@ void JFJochImageReadingWorker::PixelRefinePreview(PixelRefineParams params) {
const auto &img32 = current_image_ptr->Image();
pixel_refine_->Run<int32_t>(img32.data(), *pixel_pred_, d);
emit pixelRefineResidual(d.final_cost, d.cc, static_cast<int64_t>(d.reflections.size()));
emit pixelRefineReport(BuildReport_i(d));
auto display = BuildDisplayImage_i(d, params.display_mode);
emit predictedImageReady(WrapFloatImage_i(display));
@@ -954,6 +991,7 @@ void JFJochImageReadingWorker::PixelRefineRun(PixelRefineParams params) {
out.beam_y = d.geom.GetBeamY_pxl();
emit pixelRefineParamsRefined(out);
emit pixelRefineResidual(d.final_cost, d.cc, static_cast<int64_t>(d.reflections.size()));
emit pixelRefineReport(BuildReport_i(d));
auto display = BuildDisplayImage_i(d, params.display_mode);
emit predictedImageReady(WrapFloatImage_i(display));
+3
View File
@@ -80,6 +80,8 @@ private:
void MaskMeasuredSentinels_i(std::vector<float> &img) const;
// Build the per-reflection shoebox rectangles for the last refine/preview.
QVector<QRect> BuildShoeboxes_i(const PixelRefineData &data) const;
// Assemble the per-reflection table + per-image summary for the table window.
PixelRefineReport BuildReport_i(const PixelRefineData &data) const;
// Build the float image to display for the given PixelRefineParams::DisplayMode.
std::vector<float> BuildDisplayImage_i(const PixelRefineData &data, int display_mode) const;
@@ -148,6 +150,7 @@ signals:
void predictedShoeboxes(QVector<QRect> boxes); // per-reflection optimization windows
void pixelRefineResidual(double cost, double cc, int64_t n_reflections);
void pixelRefineParamsRefined(PixelRefineParams params);
void pixelRefineReport(PixelRefineReport report); // per-reflection table + summary
void pixelRefineStatus(QString message);
public:
+9
View File
@@ -26,6 +26,7 @@
#include "windows/JFJoch2DAzintImageWindow.h"
#include "windows/JFJochAzIntWindow.h"
#include "windows/JFJochPixelRefineWindow.h"
#include "windows/JFJochPixelRefineTableWindow.h"
#include "windows/JFJochMagnifierWindow.h"
#include "image_viewer/JFJochImage.h"
#include "image_viewer/JFJochSimpleImage.h"
@@ -108,6 +109,7 @@ JFJochViewerWindow::JFJochViewerWindow(QWidget *parent, bool dbus, const QString
auto azintWindow = new JFJochAzIntWindow(experiment.GetAzimuthalIntegrationSettings(), this);
auto azintImageWindow = new JFJoch2DAzintImageWindow(this);
auto pixelRefineWindow = new JFJochPixelRefineWindow(this);
auto pixelRefineTableWindow = new JFJochPixelRefineTableWindow(this);
auto magnifierWindow = new JFJochMagnifierWindow(this);
menuBar->AddWindowEntry(tableWindow, "Image list");
@@ -120,6 +122,7 @@ JFJochViewerWindow::JFJochViewerWindow(QWidget *parent, bool dbus, const QString
menuBar->AddWindowEntry(azintWindow, "Azimuthal integration settings");
menuBar->AddWindowEntry(azintImageWindow, "Azimuthal integration 2D image");
menuBar->AddWindowEntry(pixelRefineWindow, "PixelRefine (experimental)");
menuBar->AddWindowEntry(pixelRefineTableWindow, "PixelRefine reflections");
menuBar->AddWindowEntry(magnifierWindow, "Magnifier");
if (dbus) {
@@ -361,6 +364,12 @@ JFJochViewerWindow::JFJochViewerWindow(QWidget *parent, bool dbus, const QString
connect(reading_worker, &JFJochImageReadingWorker::pixelRefineStatus,
pixelRefineWindow, &JFJochPixelRefineWindow::setStatus);
// Reflection-table window: refreshed on every preview/refine, raised by button.
connect(reading_worker, &JFJochImageReadingWorker::pixelRefineReport,
pixelRefineTableWindow, &JFJochPixelRefineTableWindow::setReport);
connect(pixelRefineWindow, &JFJochPixelRefineWindow::showTableRequested,
pixelRefineTableWindow, &JFJochHelperWindow::open);
// Lock the predicted-image viewport to the original image (both directions).
connect(viewer, &JFJochImage::viewportChanged,
pixelRefineWindow->imageView(), &JFJochImage::applyViewport);
@@ -82,8 +82,10 @@ JFJochPixelRefineWindow::JFJochPixelRefineWindow(QWidget *parent)
// --- buttons + readouts -------------------------------------------------
m_loadRef = new QPushButton(tr("Load reference MTZ…"), this);
m_refine = new QPushButton(tr("Refine"), this);
m_showTable = new QPushButton(tr("Reflection table…"), this);
controlsLayout->addWidget(m_loadRef);
controlsLayout->addWidget(m_refine);
controlsLayout->addWidget(m_showTable);
m_residual = new QLabel(tr("Residual: —"), this);
m_pipelineCC = new QLabel(tr("Pipeline CC (ref): —"), this);
@@ -121,6 +123,10 @@ JFJochPixelRefineWindow::JFJochPixelRefineWindow(QWidget *parent)
emit loadReferenceRequested(path);
});
connect(m_showTable, &QPushButton::clicked, this, [this] {
emit showTableRequested();
});
connect(m_refine, &QPushButton::clicked, this, [this] {
// Cancel any pending live-preview: otherwise a debounce armed by a slider
// move just before this click fires after the refine and overwrites the
+2
View File
@@ -49,6 +49,7 @@ class JFJochPixelRefineWindow : public JFJochHelperWindow {
QLabel *m_status;
QPushButton *m_loadRef;
QPushButton *m_refine;
QPushButton *m_showTable;
QTimer *m_debounce;
bool m_suppress = false; // guard while pushing refined params into sliders
@@ -68,6 +69,7 @@ signals:
void paramsChanged(PixelRefineParams params); // debounced live preview
void refineRequested(PixelRefineParams params); // "Refine" button
void loadReferenceRequested(QString path); // "Load reference" button
void showTableRequested(); // "Reflection table" button
public slots:
void setPredictedImage(std::shared_ptr<const SimpleImage> image);
+25
View File
@@ -4,6 +4,8 @@
#pragma once
#include <cmath>
#include <cstdint>
#include <vector>
#include <QMetaType>
// Parameters exchanged between the PixelRefine window (sliders/buttons) and the
@@ -39,3 +41,26 @@ struct PixelRefineParams {
};
Q_DECLARE_METATYPE(PixelRefineParams)
// One PixelRefine result, shipped from the worker to the reflection-table window:
// a per-image summary that puts PixelRefine's scale/B/CC next to the standard
// ScaleOnTheFly pipeline's, plus one row per matched reflection.
struct PixelRefineReport {
// Per-image summary (NaN = not available).
double pr_G = NAN, pr_B = NAN, pr_cc = NAN; // PixelRefine
int64_t pr_cc_n = 0;
double pipe_G = NAN, pipe_B = NAN, pipe_cc = NAN; // ScaleOnTheFly pipeline baseline
struct Row {
int h = 0, k = 0, l = 0;
double d = 0.0;
double completeness = 1.0; // spot footprint on live pixels (1 = not clipped)
double partiality = 1.0; // recorded rocking fraction
double I = 0.0, sigma = 0.0;
double I_true_est = NAN; // r.I * image_scale_corr (this image's estimate)
double I_true_ref = NAN; // reference (merged) intensity
};
std::vector<Row> rows;
};
Q_DECLARE_METATYPE(PixelRefineReport)