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>
174 lines
6.1 KiB
C++
174 lines
6.1 KiB
C++
// 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();
|
|
}
|