viewer: ROI list panel — select, add, rename, delete, per-ROI results

Add JFJochViewerROIList to the side panel: a list of the dataset's ROIs
with selection, add (box/circle/azimuthal), rename and delete, plus a
statistics readout (sum/mean/std/max/valid/masked/centre-of-mass) for the
selected ROI, taken from the analysis output for the current image. Edits
emit a full ROIDefinition, routed to the worker's SetROIDefinition.

Per-ROI statistics now live in this panel rather than the canvas labels;
the diffraction image's labels show only the ROI name, and the ad-hoc
ROIIntegrationCPU computation there is removed in favour of the analysis
pipeline. The result widget now reports std dev instead of variance.

The single-ROI scratch panel remains for now and will be retired once the
interactive canvas editing replaces it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-19 13:56:09 +02:00
co-authored by Claude Opus 4.8
parent 4e45680e96
commit 2f8d486b51
9 changed files with 240 additions and 61 deletions
+2 -47
View File
@@ -5,7 +5,6 @@
#include "../../common/DiffractionGeometry.h"
#include "../../common/JFJochMath.h"
#include "../../common/ROIAzimuthal.h"
#include "../../image_analysis/roi/ROIIntegrationCPU.h"
#include <QPainterPath>
#include <QBrush>
@@ -23,8 +22,6 @@
// Constructor
JFJochDiffractionImage::JFJochDiffractionImage(QWidget *parent) : JFJochImage(parent) {}
JFJochDiffractionImage::~JFJochDiffractionImage() = default;
void JFJochDiffractionImage::mouseHover(QMouseEvent *event) {
auto coord = mapToScene(event->pos());
@@ -371,53 +368,12 @@ void JFJochDiffractionImage::DrawROIs() {
DrawAzimuthalROI(az, palette[color_index++ % palette_size], geom);
}
namespace {
// Adapts the viewer image for ROIIntegrationCPU, whose masking convention is
// "min() == masked, max() == saturated". The reader uses two masked sentinels
// (ERROR_PXL_VALUE == INT32_MIN and GAP_PXL_VALUE == INT32_MIN+1), so fold the
// gap value onto the error value; saturated already maps to INT32_MAX.
struct MaskedImageView {
const std::vector<int32_t> &img;
[[nodiscard]] size_t size() const { return img.size(); }
int32_t operator[](size_t i) const {
const int32_t v = img[i];
return (v == GAP_PXL_VALUE) ? ERROR_PXL_VALUE : v;
}
};
}
void JFJochDiffractionImage::ComputeROIStats() {
roi_stats_.clear();
if (!image || image->Dataset().experiment.ROI().empty())
return;
// Rebuild the engine only when the dataset changes (it rasterizes the bitmap).
if (roi_integration_dataset_ != &image->Dataset()) {
roi_integration_ = std::make_unique<ROIIntegrationCPU>(image->Dataset().experiment);
roi_integration_dataset_ = &image->Dataset();
}
if (roi_integration_->empty())
return;
try {
roi_integration_->RunROI(MaskedImageView{image->Image()}, roi_stats_);
} catch (const std::exception &) {
roi_stats_.clear(); // e.g. image/bitmap size mismatch
}
}
void JFJochDiffractionImage::AddROILabel(const std::string &name, const QColor &color, float px, float py) {
if (!show_roi_labels)
return;
QString text_str = QString::fromStdString(name);
auto it = roi_stats_.find(name);
if (it != roi_stats_.end() && it->second.pixels > 0) {
const auto &m = it->second;
text_str += QString(" Σ=%1 max=%2 n=%3").arg(m.sum).arg(m.max_count).arg(m.pixels);
}
auto *text = scene()->addText(text_str);
// Just the name; per-ROI statistics are shown in the side-panel ROI list.
auto *text = scene()->addText(QString::fromStdString(name));
text->setDefaultTextColor(color);
text->setFlag(QGraphicsItem::ItemIgnoresTransformations); // constant on-screen size
text->setPos(px, py);
@@ -516,7 +472,6 @@ void JFJochDiffractionImage::loadImage(std::shared_ptr<const JFJochReaderImage>
image = in_image;
UpdateForeground();
LoadImageInternal();
ComputeROIStats();
GeneratePixmap();
Redraw();
CalcROI();