viewer: draw azimuthal ROI wedges, with optional fill and labels

Azimuthal ROIs now render on the diffraction image as annular sectors
(or full-ring annuli), sampled through DiffractionGeometry::ResPhiToPxl
so the outline follows the ROI footprint, including wrap-around sectors.

Add two side-panel toggles (both default off): a translucent fill for
every ROI (helpful when outline colours clash with the image colour map,
and with many ROIs) and ROI name labels (constant on-screen size). Wired
side panel -> window -> diffraction image like the existing feature
toggles.

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 35f6ffdde9
commit 7d6f79a1da
5 changed files with 126 additions and 12 deletions
+10
View File
@@ -113,6 +113,16 @@ JFJochViewerSidePanel::JFJochViewerSidePanel(QWidget *parent) : QWidget(parent)
layout->addWidget(new TitleLabel("ROI", this));
auto roiLabelsCheckBox = new QCheckBox("Show ROI labels", this);
roiLabelsCheckBox->setCheckState(Qt::CheckState::Unchecked);
connect(roiLabelsCheckBox, &QCheckBox::toggled, this, &JFJochViewerSidePanel::showROILabels);
layout->addWidget(roiLabelsCheckBox);
auto roiFillCheckBox = new QCheckBox("Translucent ROI fill", this);
roiFillCheckBox->setCheckState(Qt::CheckState::Unchecked);
connect(roiFillCheckBox, &QCheckBox::toggled, this, &JFJochViewerSidePanel::showROIFill);
layout->addWidget(roiFillCheckBox);
roi = new JFJochViewerImageROIStatistics(this);
layout->addWidget(roi);
+2
View File
@@ -29,6 +29,8 @@ signals:
void showSaturatedPixels(bool input);
void showPredictions(bool input);
void highlightIceRings(bool input);
void showROILabels(bool input);
void showROIFill(bool input);
void findBeamCenter(const UnitCell &input, bool guess);
void analyze();
+4
View File
@@ -281,6 +281,10 @@ JFJochViewerWindow::JFJochViewerWindow(QWidget *parent, bool dbus, const QString
viewer, &JFJochDiffractionImage::showSpots);
connect(side_panel, &JFJochViewerSidePanel::showPredictions,
viewer, &JFJochDiffractionImage::showPredictions);
connect(side_panel, &JFJochViewerSidePanel::showROILabels,
viewer, &JFJochDiffractionImage::showROILabels);
connect(side_panel, &JFJochViewerSidePanel::showROIFill,
viewer, &JFJochDiffractionImage::showROIFill);
connect(side_panel, &JFJochViewerSidePanel::setFeatureColor,
viewer, &JFJochDiffractionImage::setFeatureColor);
+100 -12
View File
@@ -4,7 +4,10 @@
#include "JFJochDiffractionImage.h"
#include "../../common/DiffractionGeometry.h"
#include "../../common/JFJochMath.h"
#include "../../common/ROIAzimuthal.h"
#include <QPainterPath>
#include <QBrush>
#include <QGraphicsPixmapItem>
#include <QGraphicsSimpleTextItem>
#include <QGraphicsScene>
@@ -332,31 +335,116 @@ void JFJochDiffractionImage::DrawROIs() {
return;
const auto &rois = image->Dataset().experiment.ROI().GetROIDefinition();
auto geom = image->Dataset().experiment.GetDiffractionGeometry();
// Distinct colours per ROI; loaded ROIs use solid lines (the interactively
// drawn scratch ROI keeps its dashed feature_color).
// TODO: align this palette with the ROI colours in the bottom-panel plots.
static const QColor palette[] = {Qt::cyan, Qt::yellow, QColor(0xff, 0x57, 0x22),
Qt::green, Qt::magenta, QColor(0x21, 0x96, 0xf3)};
const int palette_size = sizeof(palette) / sizeof(palette[0]);
int color_index = 0;
auto roi_pen = [&]() {
QPen pen(palette[color_index % palette_size], 2);
pen.setCosmetic(true);
color_index++;
return pen;
auto fill_brush = [&](const QColor &c) {
return show_roi_fill ? QBrush(QColor(c.red(), c.green(), c.blue(), 60)) : QBrush(Qt::NoBrush);
};
auto label = [&](const std::string &name, const QColor &c, float px, float py) {
if (!show_roi_labels)
return;
auto *text = scene()->addText(QString::fromStdString(name));
text->setDefaultTextColor(c);
text->setFlag(QGraphicsItem::ItemIgnoresTransformations); // constant on-screen size
text->setPos(px, py);
addOverlayItem(text);
};
for (const auto &b : rois.boxes) {
auto *rect = scene()->addRect(b.GetXMin(), b.GetYMin(), b.GetWidth(), b.GetHeight(), roi_pen());
addOverlayItem(rect);
QColor c = palette[color_index++ % palette_size];
QPen pen(c, 2); pen.setCosmetic(true);
addOverlayItem(scene()->addRect(b.GetXMin(), b.GetYMin(), b.GetWidth(), b.GetHeight(), pen, fill_brush(c)));
label(b.GetName(), c, b.GetXMin(), b.GetYMin());
}
for (const auto &c : rois.circles) {
const float r = c.GetRadius_pxl();
auto *ell = scene()->addEllipse(c.GetX() - r, c.GetY() - r, 2 * r, 2 * r, roi_pen());
addOverlayItem(ell);
for (const auto &c_roi : rois.circles) {
QColor c = palette[color_index++ % palette_size];
QPen pen(c, 2); pen.setCosmetic(true);
const float r = c_roi.GetRadius_pxl();
addOverlayItem(scene()->addEllipse(c_roi.GetX() - r, c_roi.GetY() - r, 2 * r, 2 * r, pen, fill_brush(c)));
label(c_roi.GetName(), c, c_roi.GetX(), c_roi.GetY());
}
// Azimuthal ROIs are drawn as wedges in a later step.
for (const auto &az : rois.azimuthal)
DrawAzimuthalROI(az, palette[color_index++ % palette_size], geom);
}
void JFJochDiffractionImage::DrawAzimuthalROI(const ROIAzimuthal &az, const QColor &color,
const DiffractionGeometry &geom) {
QPen pen(color, 2); pen.setCosmetic(true);
QBrush brush = show_roi_fill ? QBrush(QColor(color.red(), color.green(), color.blue(), 60))
: QBrush(Qt::NoBrush);
const float d_inner = az.GetDMax_A(); // larger d -> smaller radius
const float d_outer = az.GetDMin_A();
auto deg2rad = [](float d) { return d * static_cast<float>(PI) / 180.0f; };
// Sample the boundary through the geometry so the wedge matches the ROI footprint.
// ResPhiToPxl throws when the resolution is too high for the wavelength; skip such ROIs.
auto add_arc = [&](QPainterPath &path, float d, float phi_a, float phi_b, int steps) -> bool {
for (int i = 0; i <= steps; i++) {
float phi = phi_a + (phi_b - phi_a) * static_cast<float>(i) / static_cast<float>(steps);
try {
auto [px, py] = geom.ResPhiToPxl(d, phi);
if (path.elementCount() == 0)
path.moveTo(px, py);
else
path.lineTo(px, py);
} catch (...) { return false; }
}
return true;
};
QPainterPath path;
if (az.HasPhi()) {
float phi0 = deg2rad(az.GetPhiMin_deg());
float phi1 = deg2rad(az.GetPhiMax_deg());
if (phi1 < phi0) phi1 += 2.0f * static_cast<float>(PI); // unwrap the sector
int steps = std::max(8, static_cast<int>((phi1 - phi0) * 180.0f / static_cast<float>(PI) / 2.0f));
if (!add_arc(path, d_outer, phi0, phi1, steps)) return; // outer arc
if (!add_arc(path, d_inner, phi1, phi0, steps)) return; // inner arc back
path.closeSubpath();
} else {
path.setFillRule(Qt::OddEvenFill); // annulus
const float two_pi = 2.0f * static_cast<float>(PI);
if (!add_arc(path, d_outer, 0, two_pi, 180)) return;
path.closeSubpath();
QPainterPath inner;
if (!add_arc(inner, d_inner, 0, two_pi, 180)) return;
inner.closeSubpath();
path.addPath(inner);
}
addOverlayItem(scene()->addPath(path, pen, brush));
if (show_roi_labels) {
try {
auto [px, py] = geom.ResPhiToPxl(d_outer, az.HasPhi() ? deg2rad(az.GetPhiMin_deg()) : 0.0f);
auto *text = scene()->addText(QString::fromStdString(az.GetName()));
text->setDefaultTextColor(color);
text->setFlag(QGraphicsItem::ItemIgnoresTransformations);
text->setPos(px, py);
addOverlayItem(text);
} catch (...) {}
}
}
void JFJochDiffractionImage::showROILabels(bool input) {
show_roi_labels = input;
updateOverlay();
}
void JFJochDiffractionImage::showROIFill(bool input) {
show_roi_fill = input;
updateOverlay();
}
@@ -6,6 +6,9 @@
#include "JFJochImage.h"
#include "../../reader/JFJochReaderImage.h"
class ROIAzimuthal;
class DiffractionGeometry;
class JFJochDiffractionImage : public JFJochImage {
Q_OBJECT
@@ -26,6 +29,7 @@ private:
void LoadImageInternal();
void DrawResolutionRings();
void DrawROIs();
void DrawAzimuthalROI(const ROIAzimuthal &az, const QColor &color, const DiffractionGeometry &geom);
void DrawSpots();
void DrawPredictions();
void DrawBeamCenter();
@@ -49,6 +53,9 @@ private:
bool show_spots = false;
bool show_predictions = false;
bool show_roi_labels = false;
bool show_roi_fill = false;
bool highlight_ice_rings = true;
float ice_ring_width_Q_recipA = 0.01;
@@ -64,6 +71,9 @@ public slots:
void showSpots(bool input);
void showPredictions(bool input);
void showROILabels(bool input);
void showROIFill(bool input);
void setSpotColor(QColor input);
void setPredictionColor(QColor input);