Files
Jungfraujoch/viewer/image_viewer/JFJochDiffractionImage.cpp
T
leonarski_fandClaude Opus 4.8 142cb88aa8 viewer: shift/ctrl-drag creates list ROIs; remove the old scratch panel
The interactive shift-drag (box) / shift+ctrl-drag (circle) now creates a
new persistent ROI in the list instead of feeding the old single-ROI
scratch panel. The base emits a roiScratchDrawn hook on release; the
diffraction image turns the drawn shape into a named ROI committed via
SetROIDefinition.

The old JFJochViewerImageROIStatistics scratch panel and all its wiring
(box/circle configuration, single-ROI result, add/subtract user mask) are
removed from the side panel and window; the ROI list is now the single
source.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 14:31:23 +02:00

996 lines
37 KiB
C++

// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include <set>
#include "JFJochDiffractionImage.h"
#include "../../common/DiffractionGeometry.h"
#include "../../common/JFJochMath.h"
#include "../../common/ROIAzimuthal.h"
#include <QPainterPath>
#include <QBrush>
#include <QKeyEvent>
#include <QGraphicsPixmapItem>
#include <QGraphicsSimpleTextItem>
#include <QGraphicsScene>
#include <QWheelEvent>
#include <QScrollBar>
#include <QMenu>
#include <cmath>
#include <QMouseEvent>
#include "JFJochSimpleImage.h"
// Constructor
static bool InPhiSector(float phi, float phi_min, float phi_max) {
if (phi_min <= phi_max)
return phi >= phi_min && phi <= phi_max;
return phi >= phi_min || phi <= phi_max;
}
JFJochDiffractionImage::JFJochDiffractionImage(QWidget *parent) : JFJochImage(parent) {
setFocusPolicy(Qt::StrongFocus); // so the Delete key reaches the view
}
JFJochImage::ResizeHandle JFJochDiffractionImage::hitTestBoxHandle(const QRectF &r, const QPointF &p, qreal tol) const {
auto on = [&](qreal a, qreal b) { return std::abs(a - b) <= tol; };
const bool L = on(p.x(), r.left()), R = on(p.x(), r.right());
const bool T = on(p.y(), r.top()), B = on(p.y(), r.bottom());
const bool inX = p.x() >= r.left() - tol && p.x() <= r.right() + tol;
const bool inY = p.y() >= r.top() - tol && p.y() <= r.bottom() + tol;
if (L && T) return ResizeHandle::TopLeft;
if (R && T) return ResizeHandle::TopRight;
if (L && B) return ResizeHandle::BottomLeft;
if (R && B) return ResizeHandle::BottomRight;
if (L && inY) return ResizeHandle::Left;
if (R && inY) return ResizeHandle::Right;
if (T && inX) return ResizeHandle::Top;
if (B && inX) return ResizeHandle::Bottom;
if (r.contains(p)) return ResizeHandle::Inside;
return ResizeHandle::None;
}
void JFJochDiffractionImage::azimuthalHandles(const ROIAzimuthal &az, const DiffractionGeometry &geom,
QPointF &inner, QPointF &outer, QPointF &phimin, QPointF &phimax) const {
const float d2r = static_cast<float>(PI) / 180.0f;
const float phi0 = az.GetPhiMin_deg();
const float phi1 = az.GetPhiMax_deg();
const float mid_phi = az.HasPhi()
? (phi1 >= phi0 ? (phi0 + phi1) / 2.0f : std::fmod((phi0 + phi1 + 360.0f) / 2.0f, 360.0f))
: 0.0f;
const float r_inner = geom.ResToPxl(az.GetDMax_A());
const float r_outer = geom.ResToPxl(az.GetDMin_A());
const float d_mid = geom.PxlToRes((r_inner + r_outer) / 2.0f);
auto pt = [&](float d, float phi_deg) -> QPointF {
try {
auto [x, y] = geom.ResPhiToPxl(d, phi_deg * d2r);
return QPointF(x, y);
} catch (...) {
return QPointF(-1e9, -1e9); // off-image: never matches a handle hit-test
}
};
inner = pt(az.GetDMax_A(), mid_phi);
outer = pt(az.GetDMin_A(), mid_phi);
phimin = pt(d_mid, phi0);
phimax = pt(d_mid, phi1);
}
void JFJochDiffractionImage::mouseHover(QMouseEvent *event) {
auto coord = mapToScene(event->pos());
if (image && (coord.x() >= 0)
&& (coord.x() < image->Dataset().experiment.GetXPixelsNum())
&& (coord.y() >= 0)
&& (coord.y() < image->Dataset().experiment.GetYPixelsNum())) {
float res = image->Dataset().experiment.GetDiffractionGeometry().PxlToRes(coord.x(), coord.y());
int32_t intensity = image->Image()[std::floor(coord.x()) +
std::floor(coord.y()) * image->Dataset().experiment.GetXPixelsNum()];
QString intensity_str = QString("I=%1").arg(intensity, 9);
if (intensity == SATURATED_PXL_VALUE)
intensity_str = "I=Saturated";
else if (intensity == GAP_PXL_VALUE)
intensity_str = " Gap ";
else if (intensity == ERROR_PXL_VALUE)
intensity_str = " Bad pxl ";
emit writeStatusBar(QString("x=%1 y=%2 %3 d=%4 Å")
.arg(coord.x(), 0, 'f', 1)
.arg(coord.y(), 0, 'f', 1)
.arg(intensity_str)
.arg(res, 0, 'f', 2));
// Update hovered resolution text without rebuilding the whole overlay
hover_resolution = res;
DrawResolutionText();
} else {
emit writeStatusBar("");
// Clear hover resolution text when outside image
if (std::isfinite(hover_resolution)) {
hover_resolution = NAN;
DrawResolutionText();
}
}
}
void JFJochDiffractionImage::LoadImageInternal() {
if (!image)
return;
W = image->Dataset().experiment.GetXPixelsNum();
H = image->Dataset().experiment.GetYPixelsNum();
image_fp.resize(W*H);
auto img = image->Image();
// Fill the QImage with pixel data from the array
for (int pxl = 0; pxl < W * H; pxl++) {
auto val = img[pxl];
if (val == GAP_PXL_VALUE)
image_fp[pxl] = NAN;
else if (val == ERROR_PXL_VALUE)
image_fp[pxl] = -INFINITY;
else if (val == SATURATED_PXL_VALUE)
image_fp[pxl] = INFINITY;
else
image_fp[pxl] = static_cast<float>(val);
}
}
void JFJochDiffractionImage::DrawSpots() {
// Compute current visible area in scene coordinates
const QRectF visibleRect = mapToScene(viewport()->geometry()).boundingRect();
for (const auto &s: image->ImageData().spots) {
// Skip reflections outside the viewport
if (!visibleRect.contains(QPointF{s.x, s.y}))
continue;
const qreal desired_half_px = 8.0;
const qreal spot_size = desired_half_px / std::sqrt(std::max(0.0001, scale_factor));
QColor pen_color = spot_color;
if (s.indexed)
pen_color = feature_color;
else if (highlight_ice_rings && s.ice_ring)
pen_color = ice_ring_color;
QPen pen(pen_color, 3);
pen.setCosmetic(true);
auto *rect = scene()->addRect(s.x - spot_size + 0.5,
s.y - spot_size + 0.5,
2 * spot_size,
2 * spot_size,
pen);
addOverlayItem(rect);
}
}
void JFJochDiffractionImage::DrawPredictions() {
QFont font("Arial", 2); // Font for pixel value text
font.setPixelSize(2); // This will render very small text (1-pixel high).
const qreal desired_half_px = 8.0;
const qreal spot_size = desired_half_px / std::sqrt(std::max(0.0001, scale_factor));
QColor pen_color = prediction_color;
QPen pen(pen_color, 3);
pen.setCosmetic(true);
// Compute current visible area in scene coordinates
const QRectF visibleRect = mapToScene(viewport()->geometry()).boundingRect();
for (const auto &s: image->ImageData().reflections) {
// Skip reflections outside the viewport
if (!visibleRect.contains(QPointF{s.predicted_x, s.predicted_y}))
continue;
auto *ellipse = scene()->addEllipse(s.predicted_x - spot_size + 0.5f,
s.predicted_y - spot_size + 0.5f,
2.0f * spot_size,
2.0f * spot_size,
pen);
addOverlayItem(ellipse);
// When zoomed in enough, draw "h k l" above the box
if (scale_factor >= 10.0) {
// Format label
QString label = QString("%1, %2, %3").arg(s.h).arg(s.k).arg(s.l);
// Position slightly above the top side of the box
const qreal text_x = s.predicted_x - 5.5f;
const qreal text_y = s.predicted_y - 10.0f;
// Use QGraphicsSimpleTextItem for much better performance
auto *textItem = new QGraphicsSimpleTextItem(label);
textItem->setFont(font);
textItem->setBrush(pen_color);
textItem->setPos(text_x, text_y);
scene()->addItem(textItem);
addOverlayItem(textItem);
}
}
}
void JFJochDiffractionImage::DrawResolutionRings() {
if (ring_mode == RingMode::None)
return;
// Get the visible area in the scene coordinates
QRectF visibleRect = mapToScene(viewport()->geometry()).boundingRect();
int startX = std::max(0, static_cast<int>(std::floor(visibleRect.left())));
int endX = std::min(static_cast<int>(image->Dataset().experiment.GetXPixelsNum()),
static_cast<int>(std::ceil(visibleRect.right())));
int startY = std::max(0, static_cast<int>(std::floor(visibleRect.top())));
int endY = std::min(static_cast<int>(image->Dataset().experiment.GetYPixelsNum()),
static_cast<int>(std::ceil(visibleRect.bottom())));
auto geom = image->Dataset().experiment.GetDiffractionGeometry();
geom.PoniRot3_rad(0.0);
QColor ring_color = feature_color;
if (ring_mode == RingMode::IceRings) {
ring_color = ice_ring_color;
res_ring = QVector<float>{ICE_RING_RES_A.begin(), ICE_RING_RES_A.end()};
} else if (ring_mode == RingMode::Auto) {
float radius_x_0 = geom.GetBeamX_pxl() - startX;
float radius_x_1 = endX - geom.GetBeamX_pxl();
float radius_x = std::max(radius_x_0, radius_x_1);
float radius_y_0 = geom.GetBeamY_pxl() - startY;
float radius_y_1 = endY - geom.GetBeamY_pxl();
float radius_y = std::max(radius_y_0, radius_y_1);
float radius = std::min(radius_x, radius_y);
if (radius_x <= 0)
radius = radius_y;
if (radius_y <= 0)
radius = radius_x;
if (radius > 0)
res_ring = {
geom.PxlToRes(radius / 2.0f),
geom.PxlToRes(radius / 1.02f)
};
else
res_ring = {};
} else if (ring_mode == RingMode::Estimation) {
if (image
&& image->ImageData().resolution_estimate
&& std::isfinite(image->ImageData().resolution_estimate.value())
&& image->ImageData().resolution_estimate.value() > 0.0)
res_ring = {*image->ImageData().resolution_estimate};
else
res_ring = {};
}
if (res_ring.empty())
return;
QPen pen(ring_color, 5);
pen.setCosmetic(true);
QVector<qreal> dashPattern = {10, 15};
pen.setDashPattern(dashPattern);
float phi_offset = 0;
float res1 = geom.PxlToRes(0,0);
float res2 = geom.PxlToRes(image->Dataset().experiment.GetXPixelsNum(),0);
float res3 = geom.PxlToRes(image->Dataset().experiment.GetXPixelsNum(),image->Dataset().experiment.GetYPixelsNum());
float res4 = geom.PxlToRes(0,image->Dataset().experiment.GetYPixelsNum());
float min_res = std::min({res1, res2, res3, res4});
for (const auto &d: res_ring) {
if (d < min_res)
continue;
auto [x1,y1] = geom.ResPhiToPxl(d, 0);
auto [x2,y2] = geom.ResPhiToPxl(d, PI / 2);
auto [x3,y3] = geom.ResPhiToPxl(d, PI);
auto [x4,y4] = geom.ResPhiToPxl(d, 3.0 * PI / 2);
auto x_min = std::min({x1, x2, x3, x4});
auto x_max = std::max({x1, x2, x3, x4});
auto y_min = std::min({y1, y2, y3, y4});
auto y_max = std::max({y1, y2, y3, y4});
QRectF boundingRect(x_min, y_min, x_max - x_min, y_max - y_min);
addOverlayItem(scene()->addEllipse(boundingRect, pen));
auto [x5,y5] = geom.ResPhiToPxl(d, phi_offset + 0);
auto [x6,y6] = geom.ResPhiToPxl(d, phi_offset + PI / 2);
auto [x7,y7] = geom.ResPhiToPxl(d, phi_offset + PI);
auto [x8,y8] = geom.ResPhiToPxl(d, phi_offset + 3.0 * PI / 2);
QPointF point_1(x5, y5);
QPointF point_2(x6, y6);
QPointF point_3(x7, y7);
QPointF point_4(x8, y8);
std::optional<QPointF> point;
if (visibleRect.contains(point_1))
point = point_1;
else if (visibleRect.contains(point_2))
point = point_2;
else if (visibleRect.contains(point_3))
point = point_3;
else if (visibleRect.contains(point_4))
point = point_4;
if (point) {
QFont font("Arial", 16);
const qreal f = std::clamp(scale_factor, 0.5, 50.0);
font.setPointSizeF(16.0 / sqrt(f)); // base 12pt around scale_factor ~10
auto *textItem = new QGraphicsSimpleTextItem(
QString("%1 Å").arg(QString::number(d, 'f', 2)));
textItem->setFont(font);
textItem->setBrush(ring_color);
textItem->setPos(point.value());
scene()->addItem(textItem);
addOverlayItem(textItem);
}
phi_offset += 4.0 / 180.0 * PI;
}
}
void JFJochDiffractionImage::DrawBeamCenter() {
auto geom = image->Dataset().experiment.GetDiffractionGeometry();
auto [beam_x, beam_y] = geom.GetDirectBeam_pxl();
DrawCross(beam_x, beam_y, 25, 5, 2);
}
void JFJochDiffractionImage::DrawTopPixels() {
int i = 0;
for (const auto& p : image->GetTopPixels()) {
if (i >= show_highest_pixels)
break;
const int32_t idx = p.second;
DrawCross(idx % image->Dataset().experiment.GetXPixelsNum() + 0.5,
idx / image->Dataset().experiment.GetXPixelsNum() + 0.5, 15, 3);
i++;
}
}
void JFJochDiffractionImage::addCustomOverlay() {
DrawResolutionRings();
DrawROIs();
DrawTopPixels();
DrawBeamCenter();
if (show_spots)
DrawSpots();
if (show_predictions)
DrawPredictions();
if (show_saturation)
DrawSaturation();
DrawResolutionText();
}
void JFJochDiffractionImage::DrawROIs() {
if (!image)
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 fill_brush = [&](const QColor &c) {
return show_roi_fill ? QBrush(QColor(c.red(), c.green(), c.blue(), 60)) : QBrush(Qt::NoBrush);
};
auto draw_handle = [&](const QPointF &p, const QColor &c) {
const qreal s = 4.0 / std::sqrt(std::max(1e-4, scale_factor));
addOverlayItem(scene()->addRect(QRectF(p.x() - s, p.y() - s, 2 * s, 2 * s), QPen(c, 1), QBrush(c)));
};
for (const auto &b : rois.boxes) {
QColor c = palette[color_index++ % palette_size];
const bool selected = (QString::fromStdString(b.GetName()) == selected_roi_);
const bool editing = b.GetName() == edit_name_.toStdString()
&& (roi_edit_ == RoiEdit::MoveBox || roi_edit_ == RoiEdit::ResizeBox);
QPen pen(c, selected ? 3 : 2);
pen.setCosmetic(true);
if (selected) pen.setStyle(Qt::DashLine); // highlight the editable ROI
const QRectF rect = editing ? edit_box_
: QRectF(b.GetXMin(), b.GetYMin(), b.GetWidth(), b.GetHeight());
addOverlayItem(scene()->addRect(rect, pen, fill_brush(c)));
AddROILabel(b.GetName(), c, rect.left(), rect.top());
if (selected) {
draw_handle(rect.topLeft(), c); draw_handle(rect.topRight(), c);
draw_handle(rect.bottomLeft(), c); draw_handle(rect.bottomRight(), c);
draw_handle({rect.center().x(), rect.top()}, c);
draw_handle({rect.center().x(), rect.bottom()}, c);
draw_handle({rect.left(), rect.center().y()}, c);
draw_handle({rect.right(), rect.center().y()}, c);
}
}
for (const auto &c_roi : rois.circles) {
QColor c = palette[color_index++ % palette_size];
const bool selected = (QString::fromStdString(c_roi.GetName()) == selected_roi_);
const bool editing = c_roi.GetName() == edit_name_.toStdString()
&& (roi_edit_ == RoiEdit::MoveCircle || roi_edit_ == RoiEdit::ResizeCircle);
QPen pen(c, selected ? 3 : 2);
pen.setCosmetic(true);
if (selected) pen.setStyle(Qt::DashLine);
const QPointF center = editing ? edit_center_ : QPointF(c_roi.GetX(), c_roi.GetY());
const double r = editing ? edit_radius_ : c_roi.GetRadius_pxl();
addOverlayItem(scene()->addEllipse(center.x() - r, center.y() - r, 2 * r, 2 * r, pen, fill_brush(c)));
AddROILabel(c_roi.GetName(), c, center.x(), center.y());
if (selected) {
draw_handle({center.x() + r, center.y()}, c);
draw_handle({center.x() - r, center.y()}, c);
draw_handle({center.x(), center.y() + r}, c);
draw_handle({center.x(), center.y() - r}, c);
}
}
for (const auto &az_committed : rois.azimuthal) {
QColor c = palette[color_index++ % palette_size];
const bool selected = (QString::fromStdString(az_committed.GetName()) == selected_roi_);
const bool editing = az_committed.GetName() == edit_name_.toStdString()
&& (roi_edit_ == RoiEdit::AzimInner || roi_edit_ == RoiEdit::AzimOuter
|| roi_edit_ == RoiEdit::RotatePhiMin || roi_edit_ == RoiEdit::RotatePhiMax);
const ROIAzimuthal az = editing
? (edit_has_phi_ ? ROIAzimuthal(az_committed.GetName(), edit_d_min_, edit_d_max_, edit_phi_min_, edit_phi_max_)
: ROIAzimuthal(az_committed.GetName(), edit_d_min_, edit_d_max_))
: az_committed;
DrawAzimuthalROI(az, c, geom);
if (selected) {
QPointF inner, outer, pmin, pmax;
azimuthalHandles(az, geom, inner, outer, pmin, pmax);
draw_handle(inner, c);
draw_handle(outer, c);
if (az.HasPhi()) {
draw_handle(pmin, c);
draw_handle(pmax, c);
}
}
}
}
void JFJochDiffractionImage::AddROILabel(const std::string &name, const QColor &color, float px, float py) {
if (!show_roi_labels)
return;
// 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);
addOverlayItem(text);
}
void JFJochDiffractionImage::DrawAzimuthalROI(const ROIAzimuthal &az, const QColor &color,
const DiffractionGeometry &geom) {
const bool selected = (QString::fromStdString(az.GetName()) == selected_roi_);
QPen pen(color, selected ? 3 : 2); pen.setCosmetic(true);
if (selected) pen.setStyle(Qt::DashLine);
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.
// move_to_start == true begins a new subpath (no connecting line); false continues
// the current one (used for the radial edge between a sector's outer and inner arc).
auto add_arc = [&](QPainterPath &path, float d, float phi_a, float phi_b, int steps, bool move_to_start) -> 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 (move_to_start && i == 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, true)) return; // outer arc
if (!add_arc(path, d_inner, phi1, phi0, steps, false)) return; // inner arc; radial edges close it
path.closeSubpath();
} else {
path.setFillRule(Qt::OddEvenFill); // annulus: two concentric rings
const float two_pi = 2.0f * static_cast<float>(PI);
if (!add_arc(path, d_outer, 0, two_pi, 180, true)) return;
path.closeSubpath();
if (!add_arc(path, d_inner, 0, two_pi, 180, true)) return;
path.closeSubpath();
}
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);
AddROILabel(az.GetName(), color, px, py);
} catch (...) {}
}
}
void JFJochDiffractionImage::showROILabels(bool input) {
show_roi_labels = input;
updateOverlay();
}
void JFJochDiffractionImage::showROIFill(bool input) {
show_roi_fill = input;
updateOverlay();
}
void JFJochDiffractionImage::setSelectedROI(QString name) {
selected_roi_ = name;
updateOverlay();
}
bool JFJochDiffractionImage::roiEditPress(const QPointF &scenePos) {
if (!image)
return false;
const auto &rois = image->Dataset().experiment.ROI().GetROIDefinition();
auto geom = image->Dataset().experiment.GetDiffractionGeometry();
const qreal tol = 6.0 / std::sqrt(std::max(1e-4, scale_factor));
auto start = [&](const std::string &name) {
edit_name_ = QString::fromStdString(name);
selected_roi_ = edit_name_;
move_last_ = scenePos;
emit roiSelected(edit_name_);
setCursor(Qt::ClosedHandCursor);
};
// Box: corners/edges resize, interior moves.
for (const auto &b : rois.boxes) {
const QRectF r(QPointF(b.GetXMin(), b.GetYMin()), QPointF(b.GetXMax(), b.GetYMax()));
const ResizeHandle h = hitTestBoxHandle(r, scenePos, tol);
if (h == ResizeHandle::None)
continue;
start(b.GetName());
edit_box_ = r;
if (h == ResizeHandle::Inside) {
roi_edit_ = RoiEdit::MoveBox;
} else {
roi_edit_ = RoiEdit::ResizeBox;
box_handle_ = h;
}
return true;
}
// Circle: perimeter resizes, interior moves.
for (const auto &c : rois.circles) {
const QPointF center(c.GetX(), c.GetY());
const double dist = QLineF(center, scenePos).length();
if (dist > c.GetRadius_pxl() + tol)
continue;
start(c.GetName());
edit_center_ = center;
edit_radius_ = c.GetRadius_pxl();
roi_edit_ = (std::abs(dist - c.GetRadius_pxl()) <= tol) ? RoiEdit::ResizeCircle : RoiEdit::MoveCircle;
return true;
}
// Azimuthal: grab one of the discrete handles to resize Q/d (inner/outer arc) or
// rotate a phi edge. Larger tolerance than the thin arcs would give.
const qreal tol_h = 9.0 / std::sqrt(std::max(1e-4, scale_factor));
for (const auto &az : rois.azimuthal) {
QPointF inner, outer, pmin, pmax;
azimuthalHandles(az, geom, inner, outer, pmin, pmax);
auto grab = [&](const QPointF &h) { return QLineF(h, scenePos).length() <= tol_h; };
RoiEdit mode = RoiEdit::None;
if (grab(inner)) mode = RoiEdit::AzimInner;
else if (grab(outer)) mode = RoiEdit::AzimOuter;
else if (az.HasPhi() && grab(pmin)) mode = RoiEdit::RotatePhiMin;
else if (az.HasPhi() && grab(pmax)) mode = RoiEdit::RotatePhiMax;
if (mode == RoiEdit::None)
continue;
start(az.GetName());
edit_d_min_ = az.GetDMin_A();
edit_d_max_ = az.GetDMax_A();
edit_has_phi_ = az.HasPhi();
edit_phi_min_ = az.GetPhiMin_deg();
edit_phi_max_ = az.GetPhiMax_deg();
roi_edit_ = mode;
return true;
}
// Inside an azimuthal ROI but not on a handle: select it and let the base pan
// (these ROIs are large and should not capture the panning gesture).
for (const auto &az : rois.azimuthal) {
const auto [bx, by] = geom.GetDirectBeam_pxl();
const double cursor_r = QLineF(QPointF(bx, by), scenePos).length();
const float phi = geom.Phi_rad(scenePos.x(), scenePos.y()) * 180.0f / static_cast<float>(PI);
if (cursor_r < geom.ResToPxl(az.GetDMax_A()) || cursor_r > geom.ResToPxl(az.GetDMin_A()))
continue;
if (az.HasPhi() && !InPhiSector(phi, az.GetPhiMin_deg(), az.GetPhiMax_deg()))
continue;
selected_roi_ = QString::fromStdString(az.GetName());
emit roiSelected(selected_roi_);
break;
}
return false;
}
void JFJochDiffractionImage::roiEditMove(const QPointF &scenePos) {
if (!image)
return;
auto geom = image->Dataset().experiment.GetDiffractionGeometry();
const auto [bx, by] = geom.GetDirectBeam_pxl();
const float cursor_r = QLineF(QPointF(bx, by), scenePos).length();
switch (roi_edit_) {
case RoiEdit::MoveBox:
edit_box_.translate(scenePos - move_last_);
move_last_ = scenePos;
break;
case RoiEdit::ResizeBox: {
QRectF r = edit_box_;
switch (box_handle_) {
case ResizeHandle::Left: r.setLeft(scenePos.x()); break;
case ResizeHandle::Right: r.setRight(scenePos.x()); break;
case ResizeHandle::Top: r.setTop(scenePos.y()); break;
case ResizeHandle::Bottom: r.setBottom(scenePos.y()); break;
case ResizeHandle::TopLeft: r.setTopLeft(scenePos); break;
case ResizeHandle::TopRight: r.setTopRight(scenePos); break;
case ResizeHandle::BottomLeft: r.setBottomLeft(scenePos); break;
case ResizeHandle::BottomRight: r.setBottomRight(scenePos); break;
default: break;
}
edit_box_ = r.normalized();
break;
}
case RoiEdit::MoveCircle:
edit_center_ += (scenePos - move_last_);
move_last_ = scenePos;
break;
case RoiEdit::ResizeCircle:
edit_radius_ = std::max(1.0, QLineF(edit_center_, scenePos).length());
break;
case RoiEdit::AzimInner:
edit_d_max_ = geom.PxlToRes(cursor_r);
break;
case RoiEdit::AzimOuter:
edit_d_min_ = geom.PxlToRes(cursor_r);
break;
case RoiEdit::RotatePhiMin:
edit_phi_min_ = geom.Phi_rad(scenePos.x(), scenePos.y()) * 180.0f / static_cast<float>(PI);
break;
case RoiEdit::RotatePhiMax:
edit_phi_max_ = geom.Phi_rad(scenePos.x(), scenePos.y()) * 180.0f / static_cast<float>(PI);
break;
default:
return; // None: select only, nothing to drag
}
updateOverlay();
// Live recompute, but keep at most one in flight (cleared in loadImage) so the
// worker is not flooded with edits faster than it can recompute them.
if (!live_pending_) {
live_pending_ = true;
emit roiGeometryEdited(BuildEditedROIDefinition());
}
}
void JFJochDiffractionImage::roiEditRelease() {
if (roi_edit_ == RoiEdit::None)
return;
const ROIDefinition rois = BuildEditedROIDefinition();
roi_edit_ = RoiEdit::None;
setCursor(Qt::ArrowCursor);
emit roiGeometryEdited(rois); // final, exact geometry
}
ROIDefinition JFJochDiffractionImage::BuildEditedROIDefinition() const {
ROIDefinition rois;
if (image)
rois = image->Dataset().experiment.ROI().GetROIDefinition();
const std::string sel = edit_name_.toStdString();
switch (roi_edit_) {
case RoiEdit::MoveBox:
case RoiEdit::ResizeBox:
for (auto &b : rois.boxes)
if (b.GetName() == sel) {
b = ROIBox(sel, std::lround(edit_box_.left()), std::lround(edit_box_.right()),
std::lround(edit_box_.top()), std::lround(edit_box_.bottom()));
break;
}
break;
case RoiEdit::MoveCircle:
case RoiEdit::ResizeCircle:
for (auto &c : rois.circles)
if (c.GetName() == sel) {
c = ROICircle(sel, edit_center_.x(), edit_center_.y(), edit_radius_);
break;
}
break;
case RoiEdit::AzimInner:
case RoiEdit::AzimOuter:
case RoiEdit::RotatePhiMin:
case RoiEdit::RotatePhiMax:
for (auto &a : rois.azimuthal)
if (a.GetName() == sel) {
a = edit_has_phi_
? ROIAzimuthal(sel, edit_d_min_, edit_d_max_, edit_phi_min_, edit_phi_max_)
: ROIAzimuthal(sel, edit_d_min_, edit_d_max_);
break;
}
break;
default:
break;
}
return rois;
}
void JFJochDiffractionImage::roiScratchDrawn() {
// The base just drew a scratch box/circle (roiBox/roi_type, in pixel coords);
// turn it into a new persistent ROI in the list.
if (!image || roiBox.isNull() || roiBox.width() <= 0 || roiBox.height() <= 0)
return;
ROIDefinition rois = image->Dataset().experiment.ROI().GetROIDefinition();
if (rois.boxes.size() + rois.circles.size() + rois.azimuthal.size() >= 16)
return;
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());
std::string name;
for (int i = 1; ; i++) {
name = "roi" + std::to_string(i);
if (!used.count(name)) break;
}
if (roi_type == RoiType::RoiBox) {
const QRectF r = roiBox.normalized();
rois.boxes.emplace_back(name, std::lround(r.left()), std::lround(r.right()),
std::lround(r.top()), std::lround(r.bottom()));
} else {
const QPointF c = roiBox.center();
const double rad = 0.5 * std::min(roiBox.width(), roiBox.height());
rois.circles.emplace_back(name, c.x(), c.y(), std::max(0.1, rad));
}
roiBox = QRectF(); // clear the scratch overlay
selected_roi_ = QString::fromStdString(name);
emit roiSelected(selected_roi_);
emit roiGeometryEdited(rois);
}
void JFJochDiffractionImage::keyPressEvent(QKeyEvent *event) {
if (event->key() == Qt::Key_Delete && image && !selected_roi_.isEmpty()) {
ROIDefinition rois = image->Dataset().experiment.ROI().GetROIDefinition();
const std::string sel = selected_roi_.toStdString();
auto erase = [&sel](auto &vec) {
for (auto it = vec.begin(); it != vec.end(); ++it)
if (it->GetName() == sel) { vec.erase(it); return true; }
return false;
};
if (erase(rois.boxes) || erase(rois.circles) || erase(rois.azimuthal)) {
selected_roi_.clear();
emit roiGeometryEdited(rois);
}
event->accept();
return;
}
QGraphicsView::keyPressEvent(event);
}
void JFJochDiffractionImage::UpdateForeground() {
if (!image || !auto_fg)
return;
if (hdr_mode) {
const auto val_range = image->ValidMinMax();
if (val_range.has_value())
foreground = val_range->second;
} else {
foreground = image->GetAutoContrastValue();
}
emit foregroundChanged(foreground);
}
void JFJochDiffractionImage::setHDRMode(bool input) {
hdr_mode = input;
UpdateForeground();
GeneratePixmap();
Redraw();
}
void JFJochDiffractionImage::loadImage(std::shared_ptr<const JFJochReaderImage> in_image) {
live_pending_ = false; // a live ROI edit (if any) has now been recomputed
if (in_image) {
image = in_image;
UpdateForeground();
LoadImageInternal();
GeneratePixmap();
Redraw();
CalcROI();
} else {
image.reset();
W = 0; H = 0;
if (scene())
scene()->clear();
resetScenePointers();
hover_resolution = NAN;
hover_resolution_item = nullptr;
CalcROI();
}
}
void JFJochDiffractionImage::setAutoForeground(bool input) {
auto_fg = input;
// If auto_foreground is not set, then view stays with the current settings till these are explicitly changed
UpdateForeground();
GeneratePixmap();
Redraw();
emit autoForegroundChanged(auto_fg);
}
void JFJochDiffractionImage::setResolutionRing(QVector<float> v) {
res_ring = v;
ring_mode = RingMode::Manual;
updateOverlay();
}
void JFJochDiffractionImage::showSpots(bool input) {
show_spots = input;
updateOverlay();
}
void JFJochDiffractionImage::showPredictions(bool input) {
show_predictions = input;
updateOverlay();
}
void JFJochDiffractionImage::setSpotColor(QColor input) {
spot_color = input;
updateOverlay();
}
void JFJochDiffractionImage::setPredictionColor(QColor input) {
prediction_color = input;
updateOverlay();
}
void JFJochDiffractionImage::showHighestPixels(int32_t v) {
show_highest_pixels = v;
updateOverlay();
}
void JFJochDiffractionImage::DrawSaturation() {
for (const auto &iter: image->SaturatedPixels())
DrawCross(iter % image->Dataset().experiment.GetXPixelsNum() + 0.5,
iter / image->Dataset().experiment.GetXPixelsNum() + 0.5, 20, 4);
}
void JFJochDiffractionImage::DrawCross(float x, float y, float size, float width, float z) {
float sc_size = size / sqrt(scale_factor);
QPen pen(feature_color, width);
pen.setCosmetic(true);
QGraphicsLineItem *horizontalLine = scene()->addLine(x - sc_size, y, x + sc_size, y, pen);
QGraphicsLineItem *verticalLine = scene()->addLine(x, y - sc_size, x, y + sc_size, pen);
horizontalLine->setZValue(z); // Ensure it appears above other items
verticalLine->setZValue(z); // Ensure it appears above other items
addOverlayItem(horizontalLine);
addOverlayItem(verticalLine);
}
void JFJochDiffractionImage::showSaturation(bool input) {
show_saturation = input;
GeneratePixmap();
updateOverlay();
}
void JFJochDiffractionImage::highlightIceRings(bool input) {
highlight_ice_rings = input;
updateOverlay();
}
void JFJochDiffractionImage::setResolutionRingMode(RingMode mode) {
ring_mode = mode;
updateOverlay();
}
void JFJochDiffractionImage::DrawResolutionText() {
auto scn = scene();
if (!scn) {
hover_resolution_item = nullptr; // scene gone
return;
}
// Hide item if no valid hover resolution
if (!image || !std::isfinite(hover_resolution) || hover_resolution <= 0.0f) {
if (hover_resolution_item)
hover_resolution_item->setVisible(false);
return;
}
const QRectF visibleRect = mapToScene(viewport()->geometry()).boundingRect();
// Fixed on-screen font size (no dependence on scale_factor)
QFont font("Arial");
font.setPixelSize(32); // big, constant size on screen
const QString label =
QString("d = %1 Å").arg(QString::number(hover_resolution, 'f', 2));
// Create the item if it does not exist yet; otherwise reuse it
// NOTE: hover_resolution_item is NOT tracked in overlay_items_ — it is persistent
if (!hover_resolution_item) {
hover_resolution_item = scn->addText(label, font);
hover_resolution_item->setZValue(10.0);
// Make the text ignore zooming / view transforms
hover_resolution_item->setFlag(QGraphicsItem::ItemIgnoresTransformations, true);
} else {
hover_resolution_item->setFont(font);
hover_resolution_item->setPlainText(label);
}
hover_resolution_item->setDefaultTextColor(feature_color);
// Keep a roughly constant ~10 px margin by compensating with scale_factor
const qreal margin_px = 10.0;
const qreal margin_scene = margin_px / std::max(0.0001, scale_factor);
QPointF topLeft(visibleRect.left() + margin_scene,
visibleRect.top() + margin_scene);
hover_resolution_item->setPos(topLeft);
hover_resolution_item->setVisible(true);
}
void JFJochDiffractionImage::beforeOverlayCleared() {
// hover_resolution_item is NOT in overlay_items_, so the selective clear won't touch it.
// However, if scene()->clear() is ever called (e.g. on loadImage(nullptr)),
// the caller must also set hover_resolution_item = nullptr separately.
}
void JFJochDiffractionImage::leaveEvent(QEvent *event) {
// Mouse left the view: clear hover resolution and hide text
if (std::isfinite(hover_resolution)) {
hover_resolution = NAN;
DrawResolutionText();
}
JFJochImage::leaveEvent(event);
}