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:
@@ -87,6 +87,8 @@ ADD_EXECUTABLE(jfjoch_viewer jfjoch_viewer.cpp JFJochViewerWindow.cpp JFJochView
|
||||
windows/JFJoch2DAzintImageWindow.h
|
||||
widgets/JFJochViewerROIResult.cpp
|
||||
widgets/JFJochViewerROIResult.h
|
||||
widgets/JFJochViewerROIList.cpp
|
||||
widgets/JFJochViewerROIList.h
|
||||
widgets/ResolutionRingWidget.cpp
|
||||
widgets/ResolutionRingWidget.h
|
||||
image_viewer/JFJochGridScanImage.cpp
|
||||
|
||||
@@ -123,6 +123,12 @@ JFJochViewerSidePanel::JFJochViewerSidePanel(QWidget *parent) : QWidget(parent)
|
||||
connect(roiFillCheckBox, &QCheckBox::toggled, this, &JFJochViewerSidePanel::showROIFill);
|
||||
layout->addWidget(roiFillCheckBox);
|
||||
|
||||
roi_list = new JFJochViewerROIList(this);
|
||||
layout->addWidget(roi_list);
|
||||
connect(this, &JFJochViewerSidePanel::imageLoaded, roi_list, &JFJochViewerROIList::loadImage);
|
||||
connect(roi_list, &JFJochViewerROIList::roisChanged, this, &JFJochViewerSidePanel::roisChanged);
|
||||
connect(roi_list, &JFJochViewerROIList::selectedROIChanged, this, &JFJochViewerSidePanel::selectedROIChanged);
|
||||
|
||||
roi = new JFJochViewerImageROIStatistics(this);
|
||||
layout->addWidget(roi);
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <QCheckBox>
|
||||
|
||||
#include "widgets/JFJochViewerImageROIStatistics.h"
|
||||
#include "widgets/JFJochViewerROIList.h"
|
||||
#include "../reader/JFJochReader.h"
|
||||
#include "charts/JFJochSimpleChartView.h"
|
||||
#include "widgets/JFJochViewerSidePanelChart.h"
|
||||
@@ -19,6 +20,7 @@ class JFJochViewerSidePanel : public QWidget {
|
||||
|
||||
JFJochViewerSidePanelChart *chart = nullptr;
|
||||
JFJochViewerImageROIStatistics *roi = nullptr;
|
||||
JFJochViewerROIList *roi_list = nullptr;
|
||||
ResolutionRingWidget *res_rings = nullptr;
|
||||
|
||||
signals:
|
||||
@@ -31,6 +33,8 @@ signals:
|
||||
void highlightIceRings(bool input);
|
||||
void showROILabels(bool input);
|
||||
void showROIFill(bool input);
|
||||
void roisChanged(ROIDefinition rois);
|
||||
void selectedROIChanged(QString name);
|
||||
void findBeamCenter(const UnitCell &input, bool guess);
|
||||
void analyze();
|
||||
|
||||
|
||||
@@ -204,6 +204,9 @@ JFJochViewerWindow::JFJochViewerWindow(QWidget *parent, bool dbus, const QString
|
||||
connect(viewer, &JFJochDiffractionImage::roiCalculated,
|
||||
side_panel, &JFJochViewerSidePanel::SetROIResult);
|
||||
|
||||
connect(side_panel, &JFJochViewerSidePanel::roisChanged,
|
||||
reading_worker, &JFJochImageReadingWorker::SetROIDefinition);
|
||||
|
||||
connect(side_panel, &JFJochViewerSidePanel::ROIBoxConfigured,
|
||||
reading_worker, &JFJochImageReadingWorker::SetROIBox);
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -3,15 +3,11 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
|
||||
#include "JFJochImage.h"
|
||||
#include "../../reader/JFJochReaderImage.h"
|
||||
|
||||
class ROIAzimuthal;
|
||||
class DiffractionGeometry;
|
||||
class ROIIntegrationCPU;
|
||||
|
||||
class JFJochDiffractionImage : public JFJochImage {
|
||||
Q_OBJECT
|
||||
@@ -27,7 +23,6 @@ public:
|
||||
Q_ENUM(RingMode)
|
||||
|
||||
JFJochDiffractionImage(QWidget *parent = nullptr);
|
||||
~JFJochDiffractionImage() override; // out-of-line for the unique_ptr to an incomplete type
|
||||
private:
|
||||
|
||||
void addCustomOverlay() override;
|
||||
@@ -35,7 +30,6 @@ private:
|
||||
void DrawResolutionRings();
|
||||
void DrawROIs();
|
||||
void DrawAzimuthalROI(const ROIAzimuthal &az, const QColor &color, const DiffractionGeometry &geom);
|
||||
void ComputeROIStats();
|
||||
void AddROILabel(const std::string &name, const QColor &color, float px, float py);
|
||||
void DrawSpots();
|
||||
void DrawPredictions();
|
||||
@@ -63,13 +57,6 @@ private:
|
||||
bool show_roi_labels = false;
|
||||
bool show_roi_fill = false;
|
||||
|
||||
// Per-ROI statistics for the current image, computed with the shared CPU ROI
|
||||
// engine (the software counterpart of the FPGA roi_calc). The engine is rebuilt
|
||||
// only when the dataset changes, as it rasterizes the ROI bitmap on construction.
|
||||
std::map<std::string, ROIMessage> roi_stats_;
|
||||
std::unique_ptr<ROIIntegrationCPU> roi_integration_;
|
||||
const JFJochReaderDataset *roi_integration_dataset_ = nullptr;
|
||||
|
||||
bool highlight_ice_rings = true;
|
||||
|
||||
float ice_ring_width_Q_recipA = 0.01;
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#include "JFJochViewerROIList.h"
|
||||
|
||||
#include <set>
|
||||
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QPushButton>
|
||||
#include <QInputDialog>
|
||||
#include <QListWidgetItem>
|
||||
|
||||
JFJochViewerROIList::JFJochViewerROIList(QWidget *parent) : QWidget(parent) {
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
|
||||
list_ = new QListWidget(this);
|
||||
list_->setMaximumHeight(120);
|
||||
layout->addWidget(list_);
|
||||
connect(list_, &QListWidget::itemSelectionChanged, this, &JFJochViewerROIList::OnSelectionChanged);
|
||||
|
||||
auto *buttons = new QHBoxLayout();
|
||||
type_combo_ = new QComboBox(this);
|
||||
type_combo_->addItems({"Box", "Circle", "Azimuthal"});
|
||||
buttons->addWidget(type_combo_);
|
||||
auto *add = new QPushButton("+", this);
|
||||
auto *ren = new QPushButton("Rename", this);
|
||||
auto *del = new QPushButton("Delete", this);
|
||||
buttons->addWidget(add);
|
||||
buttons->addWidget(ren);
|
||||
buttons->addWidget(del);
|
||||
layout->addLayout(buttons);
|
||||
connect(add, &QPushButton::clicked, this, &JFJochViewerROIList::OnAdd);
|
||||
connect(ren, &QPushButton::clicked, this, &JFJochViewerROIList::OnRename);
|
||||
connect(del, &QPushButton::clicked, this, &JFJochViewerROIList::OnDelete);
|
||||
|
||||
result_ = new JFJochViewerROIResult(this);
|
||||
layout->addWidget(result_);
|
||||
}
|
||||
|
||||
void JFJochViewerROIList::loadImage(std::shared_ptr<const JFJochReaderImage> image) {
|
||||
image_ = image;
|
||||
const QString keep = SelectedName();
|
||||
rois_ = image_ ? image_->Dataset().experiment.ROI().GetROIDefinition() : ROIDefinition{};
|
||||
RebuildList(keep);
|
||||
ShowSelectedResult();
|
||||
}
|
||||
|
||||
void JFJochViewerROIList::RebuildList(const QString &keep_selected) {
|
||||
rebuilding_ = true;
|
||||
list_->clear();
|
||||
|
||||
auto add_item = [&](const std::string &name, const char *type) {
|
||||
auto *item = new QListWidgetItem(QString("%1 · %2").arg(QString::fromStdString(name), type));
|
||||
item->setData(Qt::UserRole, QString::fromStdString(name)); // name independent of display text
|
||||
list_->addItem(item);
|
||||
};
|
||||
for (const auto &b : rois_.boxes) add_item(b.GetName(), "box");
|
||||
for (const auto &c : rois_.circles) add_item(c.GetName(), "circle");
|
||||
for (const auto &a : rois_.azimuthal) add_item(a.GetName(), "azim");
|
||||
|
||||
if (!keep_selected.isEmpty()) {
|
||||
for (int i = 0; i < list_->count(); i++)
|
||||
if (list_->item(i)->data(Qt::UserRole).toString() == keep_selected) {
|
||||
list_->setCurrentRow(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
rebuilding_ = false;
|
||||
}
|
||||
|
||||
QString JFJochViewerROIList::SelectedName() const {
|
||||
auto *item = list_->currentItem();
|
||||
return item ? item->data(Qt::UserRole).toString() : QString();
|
||||
}
|
||||
|
||||
void JFJochViewerROIList::ShowSelectedResult() {
|
||||
const QString name = SelectedName();
|
||||
if (image_ && !name.isEmpty()) {
|
||||
const auto &roi = image_->ImageData().roi;
|
||||
auto it = roi.find(name.toStdString());
|
||||
if (it != roi.end()) {
|
||||
result_->SetROIResult(it->second);
|
||||
return;
|
||||
}
|
||||
}
|
||||
result_->SetROIResult(ROIMessage{}); // clears the display
|
||||
}
|
||||
|
||||
std::string JFJochViewerROIList::UniqueName() const {
|
||||
std::set<std::string> used;
|
||||
for (const auto &b : rois_.boxes) used.insert(b.GetName());
|
||||
for (const auto &c : rois_.circles) used.insert(c.GetName());
|
||||
for (const auto &a : rois_.azimuthal) used.insert(a.GetName());
|
||||
for (int i = 1; ; i++) {
|
||||
std::string n = "roi" + std::to_string(i);
|
||||
if (!used.count(n)) return n;
|
||||
}
|
||||
}
|
||||
|
||||
void JFJochViewerROIList::OnSelectionChanged() {
|
||||
if (rebuilding_)
|
||||
return;
|
||||
ShowSelectedResult();
|
||||
emit selectedROIChanged(SelectedName());
|
||||
}
|
||||
|
||||
void JFJochViewerROIList::OnAdd() {
|
||||
if (rois_.boxes.size() + rois_.circles.size() + rois_.azimuthal.size() >= 16)
|
||||
return; // bitmap allows at most 16 ROIs
|
||||
|
||||
const std::string name = UniqueName();
|
||||
int64_t cx = 100, cy = 100;
|
||||
if (image_) {
|
||||
cx = image_->Dataset().experiment.GetXPixelsNum() / 2;
|
||||
cy = image_->Dataset().experiment.GetYPixelsNum() / 2;
|
||||
}
|
||||
switch (type_combo_->currentIndex()) {
|
||||
case 0: rois_.boxes.emplace_back(name, cx - 100, cx + 100, cy - 100, cy + 100); break;
|
||||
case 1: rois_.circles.emplace_back(name, cx, cy, 100); break;
|
||||
default: rois_.azimuthal.emplace_back(name, 2.0f, 4.0f); break;
|
||||
}
|
||||
emit roisChanged(rois_);
|
||||
RebuildList(QString::fromStdString(name));
|
||||
ShowSelectedResult();
|
||||
}
|
||||
|
||||
void JFJochViewerROIList::OnDelete() {
|
||||
const std::string name = SelectedName().toStdString();
|
||||
if (name.empty())
|
||||
return;
|
||||
|
||||
auto erase_by_name = [&name](auto &vec) {
|
||||
for (auto it = vec.begin(); it != vec.end(); ++it)
|
||||
if (it->GetName() == name) { vec.erase(it); return true; }
|
||||
return false;
|
||||
};
|
||||
if (erase_by_name(rois_.boxes) || erase_by_name(rois_.circles) || erase_by_name(rois_.azimuthal)) {
|
||||
emit roisChanged(rois_);
|
||||
RebuildList(QString());
|
||||
ShowSelectedResult();
|
||||
}
|
||||
}
|
||||
|
||||
void JFJochViewerROIList::OnRename() {
|
||||
const QString old_name = SelectedName();
|
||||
if (old_name.isEmpty())
|
||||
return;
|
||||
|
||||
bool ok = false;
|
||||
const QString new_name = QInputDialog::getText(this, "Rename ROI", "New name:",
|
||||
QLineEdit::Normal, old_name, &ok);
|
||||
if (!ok || new_name.isEmpty() || new_name == old_name)
|
||||
return;
|
||||
|
||||
const std::string oldn = old_name.toStdString();
|
||||
const std::string newn = new_name.toStdString();
|
||||
|
||||
for (auto &b : rois_.boxes)
|
||||
if (b.GetName() == oldn) { b = ROIBox(newn, b.GetXMin(), b.GetXMax(), b.GetYMin(), b.GetYMax()); break; }
|
||||
for (auto &c : rois_.circles)
|
||||
if (c.GetName() == oldn) { c = ROICircle(newn, c.GetX(), c.GetY(), c.GetRadius_pxl()); break; }
|
||||
for (auto &a : rois_.azimuthal)
|
||||
if (a.GetName() == oldn) {
|
||||
a = a.HasPhi() ? ROIAzimuthal(newn, a.GetDMin_A(), a.GetDMax_A(), a.GetPhiMin_deg(), a.GetPhiMax_deg())
|
||||
: ROIAzimuthal(newn, a.GetDMin_A(), a.GetDMax_A());
|
||||
break;
|
||||
}
|
||||
|
||||
emit roisChanged(rois_);
|
||||
RebuildList(new_name);
|
||||
ShowSelectedResult();
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QWidget>
|
||||
#include <QListWidget>
|
||||
#include <QComboBox>
|
||||
|
||||
#include "JFJochViewerROIResult.h"
|
||||
#include "../../common/ROIDefinition.h"
|
||||
#include "../../reader/JFJochReaderImage.h"
|
||||
|
||||
// Manages the dataset's ROI list: select one, add (box/circle/azimuthal), rename or
|
||||
// delete, and show the selected ROI's statistics for the current image. Edits are
|
||||
// emitted as a complete ROIDefinition; the reading worker is the source of truth and
|
||||
// recomputes the per-image ROI results, which arrive back via loadImage().
|
||||
class JFJochViewerROIList : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
QListWidget *list_;
|
||||
QComboBox *type_combo_;
|
||||
JFJochViewerROIResult *result_;
|
||||
|
||||
ROIDefinition rois_;
|
||||
std::shared_ptr<const JFJochReaderImage> image_;
|
||||
bool rebuilding_ = false;
|
||||
|
||||
void RebuildList(const QString &keep_selected);
|
||||
void ShowSelectedResult();
|
||||
[[nodiscard]] QString SelectedName() const;
|
||||
[[nodiscard]] std::string UniqueName() const;
|
||||
public:
|
||||
explicit JFJochViewerROIList(QWidget *parent = nullptr);
|
||||
public slots:
|
||||
void loadImage(std::shared_ptr<const JFJochReaderImage> image);
|
||||
private slots:
|
||||
void OnSelectionChanged();
|
||||
void OnAdd();
|
||||
void OnDelete();
|
||||
void OnRename();
|
||||
signals:
|
||||
void roisChanged(ROIDefinition rois);
|
||||
void selectedROIChanged(QString name);
|
||||
};
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#include "JFJochViewerROIResult.h"
|
||||
#include <cmath>
|
||||
#include <QGridLayout>
|
||||
|
||||
JFJochViewerROIResult::JFJochViewerROIResult(QWidget *parent) : QWidget(parent) {
|
||||
@@ -40,10 +41,11 @@ void JFJochViewerROIResult::SetROIResult(ROIMessage roi) {
|
||||
auto roi_npixel_val = static_cast<double>(roi.pixels);
|
||||
double roi_mean_val = static_cast<double>(roi.sum) / roi_npixel_val;
|
||||
double variance = static_cast<double>(roi.sum_square) / roi_npixel_val - roi_mean_val * roi_mean_val;
|
||||
double std_dev = std::sqrt(std::max(0.0, variance));
|
||||
|
||||
roi_sum->setText(QString("Sum <b>%1</b>").arg(roi.sum));
|
||||
roi_mean->setText(QString("Mean <b>%1</b>").arg(QString::number(roi_mean_val, 'f', 3)));
|
||||
roi_var->setText(QString("Var <b>%1</b>").arg(QString::number(variance, 'f', 3)));
|
||||
roi_var->setText(QString("Std <b>%1</b>").arg(QString::number(std_dev, 'f', 3)));
|
||||
roi_max->setText(QString("Max <b>%1</b>").arg(roi.max_count));
|
||||
roi_npixel->setText(QString("Valid <b>%1</b>").arg(roi.pixels));
|
||||
roi_masked->setText(QString("Masked <b>%1</b>").arg(roi.pixels_masked));
|
||||
|
||||
Reference in New Issue
Block a user