From d31063ca3f87aca8287afdc43b135ff60c12a6bf Mon Sep 17 00:00:00 2001 From: leonarski_f Date: Thu, 11 Jun 2026 21:25:05 +0200 Subject: [PATCH] PixelRefine: Some improvements --- common/Reflection.h | 3 +- .../pixel_refinement/PixelRefine.cpp | 156 ++++++++++++------ image_analysis/pixel_refinement/PixelRefine.h | 12 ++ viewer/CMakeLists.txt | 2 + viewer/JFJochImageReadingWorker.cpp | 38 +++++ viewer/JFJochImageReadingWorker.h | 3 + viewer/JFJochViewerWindow.cpp | 9 + viewer/windows/JFJochPixelRefineWindow.cpp | 6 + viewer/windows/JFJochPixelRefineWindow.h | 2 + viewer/windows/PixelRefineParams.h | 25 +++ 10 files changed, 206 insertions(+), 50 deletions(-) diff --git a/common/Reflection.h b/common/Reflection.h index 08959c62..b46e38e6 100644 --- a/common/Reflection.h +++ b/common/Reflection.h @@ -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; diff --git a/image_analysis/pixel_refinement/PixelRefine.cpp b/image_analysis/pixel_refinement/PixelRefine.cpp index f41925b3..12540b3c 100644 --- a/image_analysis/pixel_refinement/PixelRefine.cpp +++ b/image_analysis/pixel_refinement/PixelRefine.cpp @@ -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 pixels; }; @@ -83,6 +84,23 @@ std::vector BuildSpotMask(const std::vector &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(std::lround(px)); + const int cy = static_cast(std::lround(py)); + return { + std::max(cx - radius, 0), + std::min(cx + radius, static_cast(xpixel) - 1), + std::max(cy - radius, 0), + std::min(cy + radius, static_cast(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(refl.predicted_y - radius, 0); - const int max_y = std::min(refl.predicted_y + radius, ypixel - 1); - const int min_x = std::max(refl.predicted_x - radius, 0); - const int max_x = std::min(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(std::lround(g.predicted_x)); + const int cy = static_cast(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(x), static_cast(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(xpixel) || y < 0 || y >= static_cast(ypixel)) + continue; + const size_t np = static_cast(xpixel) * y + x; + if (image[np] == std::numeric_limits::max()) + continue; + if (std::is_signed_v && image[np] == std::numeric_limits::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(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(radial_sum / radial_w) : 1.0f; + r.partiality = (den > 0.0) ? static_cast(radial_sum / den) : 1.0f; + r.completeness = (prof_full > 0.0) ? static_cast(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(r.partiality) * B_term * g.pol; // B, partiality & pol r.bkg = static_cast(bkg_sum / static_cast(n)); r.observed = true; @@ -1010,13 +1074,10 @@ std::vector PixelRefine::PredictImage(const T *image, refl.predicted_x, refl.predicted_y, radius, bkg_outer_radius, Ibkg); - const int min_y = std::max(refl.predicted_y - radius, 0); - const int max_y = std::min(refl.predicted_y + radius, ypixel - 1); - const int min_x = std::max(refl.predicted_x - radius, 0); - const int max_x = std::min(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 PixelRefine::ChiSquaredImage(const T *image, radius, bkg_outer_radius, Ibkg)) continue; - const int min_y = std::max(refl.predicted_y - radius, 0); - const int max_y = std::min(refl.predicted_y + radius, ypixel - 1); - const int min_x = std::max(refl.predicted_x - radius, 0); - const int max_x = std::min(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. diff --git a/image_analysis/pixel_refinement/PixelRefine.h b/image_analysis/pixel_refinement/PixelRefine.h index 8d28e107..1a2535c1 100644 --- a/image_analysis/pixel_refinement/PixelRefine.h +++ b/image_analysis/pixel_refinement/PixelRefine.h @@ -3,6 +3,8 @@ #pragma once +#include + #include "../bragg_prediction/BraggPrediction.h" #include "../common/DiffractionExperiment.h" #include "../scale_merge/HKLKey.h" @@ -198,4 +200,14 @@ public: std::vector 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 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; + } }; diff --git a/viewer/CMakeLists.txt b/viewer/CMakeLists.txt index 70981081..6bfbd37e 100644 --- a/viewer/CMakeLists.txt +++ b/viewer/CMakeLists.txt @@ -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 diff --git a/viewer/JFJochImageReadingWorker.cpp b/viewer/JFJochImageReadingWorker.cpp index 17a71934..1e92cfed 100644 --- a/viewer/JFJochImageReadingWorker.cpp +++ b/viewer/JFJochImageReadingWorker.cpp @@ -70,6 +70,7 @@ JFJochImageReadingWorker::JFJochImageReadingWorker(const SpotFindingSettings &se indexing_settings(experiment.GetIndexingSettings()), azint_settings(experiment.GetAzimuthalIntegrationSettings()) { qRegisterMetaType("PixelRefineParams"); + qRegisterMetaType("PixelRefineReport"); qRegisterMetaType>("QVector"); spot_finding_settings = settings;; @@ -852,6 +853,41 @@ QVector 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(r.I) * static_cast(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 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(img32.data(), *pixel_pred_, d); emit pixelRefineResidual(d.final_cost, d.cc, static_cast(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(d.reflections.size())); + emit pixelRefineReport(BuildReport_i(d)); auto display = BuildDisplayImage_i(d, params.display_mode); emit predictedImageReady(WrapFloatImage_i(display)); diff --git a/viewer/JFJochImageReadingWorker.h b/viewer/JFJochImageReadingWorker.h index 501b557d..50cd77ca 100644 --- a/viewer/JFJochImageReadingWorker.h +++ b/viewer/JFJochImageReadingWorker.h @@ -80,6 +80,8 @@ private: void MaskMeasuredSentinels_i(std::vector &img) const; // Build the per-reflection shoebox rectangles for the last refine/preview. QVector 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 BuildDisplayImage_i(const PixelRefineData &data, int display_mode) const; @@ -148,6 +150,7 @@ signals: void predictedShoeboxes(QVector 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: diff --git a/viewer/JFJochViewerWindow.cpp b/viewer/JFJochViewerWindow.cpp index 9fa6a302..1eb2c3c7 100644 --- a/viewer/JFJochViewerWindow.cpp +++ b/viewer/JFJochViewerWindow.cpp @@ -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); diff --git a/viewer/windows/JFJochPixelRefineWindow.cpp b/viewer/windows/JFJochPixelRefineWindow.cpp index 7f7a64ec..0e8b4a16 100644 --- a/viewer/windows/JFJochPixelRefineWindow.cpp +++ b/viewer/windows/JFJochPixelRefineWindow.cpp @@ -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 diff --git a/viewer/windows/JFJochPixelRefineWindow.h b/viewer/windows/JFJochPixelRefineWindow.h index 99c628c2..9398389d 100644 --- a/viewer/windows/JFJochPixelRefineWindow.h +++ b/viewer/windows/JFJochPixelRefineWindow.h @@ -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 image); diff --git a/viewer/windows/PixelRefineParams.h b/viewer/windows/PixelRefineParams.h index 64f6e27b..e180847e 100644 --- a/viewer/windows/PixelRefineParams.h +++ b/viewer/windows/PixelRefineParams.h @@ -4,6 +4,8 @@ #pragma once #include +#include +#include #include // 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 rows; +}; + +Q_DECLARE_METATYPE(PixelRefineReport)