From 7d6f79a1dab821a38a33cfa7dca04823bc4802b2 Mon Sep 17 00:00:00 2001 From: leonarski_f Date: Fri, 19 Jun 2026 11:40:24 +0200 Subject: [PATCH] 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 --- viewer/JFJochViewerSidePanel.cpp | 10 ++ viewer/JFJochViewerSidePanel.h | 2 + viewer/JFJochViewerWindow.cpp | 4 + .../image_viewer/JFJochDiffractionImage.cpp | 112 ++++++++++++++++-- viewer/image_viewer/JFJochDiffractionImage.h | 10 ++ 5 files changed, 126 insertions(+), 12 deletions(-) diff --git a/viewer/JFJochViewerSidePanel.cpp b/viewer/JFJochViewerSidePanel.cpp index 40677d48..70c94acf 100644 --- a/viewer/JFJochViewerSidePanel.cpp +++ b/viewer/JFJochViewerSidePanel.cpp @@ -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); diff --git a/viewer/JFJochViewerSidePanel.h b/viewer/JFJochViewerSidePanel.h index 6eabf1bd..f4652fa0 100644 --- a/viewer/JFJochViewerSidePanel.h +++ b/viewer/JFJochViewerSidePanel.h @@ -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(); diff --git a/viewer/JFJochViewerWindow.cpp b/viewer/JFJochViewerWindow.cpp index 7763b9e1..c49599be 100644 --- a/viewer/JFJochViewerWindow.cpp +++ b/viewer/JFJochViewerWindow.cpp @@ -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); diff --git a/viewer/image_viewer/JFJochDiffractionImage.cpp b/viewer/image_viewer/JFJochDiffractionImage.cpp index 435cfceb..f82efa42 100644 --- a/viewer/image_viewer/JFJochDiffractionImage.cpp +++ b/viewer/image_viewer/JFJochDiffractionImage.cpp @@ -4,7 +4,10 @@ #include "JFJochDiffractionImage.h" #include "../../common/DiffractionGeometry.h" #include "../../common/JFJochMath.h" +#include "../../common/ROIAzimuthal.h" +#include +#include #include #include #include @@ -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(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(i) / static_cast(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(PI); // unwrap the sector + int steps = std::max(8, static_cast((phi1 - phi0) * 180.0f / static_cast(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(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(); } diff --git a/viewer/image_viewer/JFJochDiffractionImage.h b/viewer/image_viewer/JFJochDiffractionImage.h index 49ba0172..612f8058 100644 --- a/viewer/image_viewer/JFJochDiffractionImage.h +++ b/viewer/image_viewer/JFJochDiffractionImage.h @@ -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);