// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute // SPDX-License-Identifier: GPL-3.0-only #include "JFJochViewerImage.h" #include "../common/DiffractionGeometry.h" #include #include #include #include #include // Constructor JFJochViewerImage::JFJochViewerImage(QWidget *parent) : QGraphicsView(parent) { setDragMode(QGraphicsView::NoDrag); // Disable default drag mode setTransformationAnchor(QGraphicsView::AnchorUnderMouse); // Zoom anchors setRenderHint(QPainter::Antialiasing); // Enable smooth rendering setRenderHint(QPainter::SmoothPixmapTransform); setFocusPolicy(Qt::ClickFocus); // Connect the horizontal scrollbar's valueChanged signal connect(horizontalScrollBar(), &QScrollBar::valueChanged, this, &JFJochViewerImage::onScroll); // Connect the vertical scrollbar's valueChanged signal connect(verticalScrollBar(), &QScrollBar::valueChanged, this, &JFJochViewerImage::onScroll); } // Handle zooming with mouse wheel void JFJochViewerImage::wheelEvent(QWheelEvent *event) { if (!scene() || !image) return; const double zoomFactor = 1.15; // Zoom factor // Get the position of the mouse in scene coordinates QPointF targetScenePos = mapToScene(event->position().toPoint()); if (event->modifiers() == Qt::ShiftModifier) { float new_foreground = foreground + event->angleDelta().y() / 120.0f; if (new_foreground < 1) new_foreground = 1.0; foreground = new_foreground; emit foregroundChanged(foreground); Redraw(); } else { // Perform zooming if (event->angleDelta().y() > 0) { if (scale_factor * zoomFactor < 500.0) { scale_factor *= zoomFactor; scale(zoomFactor, zoomFactor); } } else { if (scale_factor > 0.2) { scale_factor *= 1.0 / zoomFactor; scale(1.0 / zoomFactor, 1.0 / zoomFactor); } } // Adjust the view's center to keep the zoom focused on the mouse position QPointF updatedViewportCenter = mapToScene(viewport()->rect().center()); QPointF delta = targetScenePos - updatedViewportCenter; translate(delta.x(), delta.y()); // Shift the view updateOverlay(); } } QPointF RoundPoint(const QPointF &input) { return QPointF(qRound(input.x()), qRound(input.y())); } void JFJochViewerImage::mousePressEvent(QMouseEvent *event) { if (!scene() || !image) return; mouse_event_type = MouseEventType::None; bool mouse_inside_roi = false; if (roiStartPos != roiEndPos) { if (roi_type == RoiType::RoiBox) { QRectF roiRect = QRectF(roiStartPos, roiEndPos).normalized(); mouse_inside_roi = roiRect.contains(mapToScene(event->pos())); } else { auto curr_position = mapToScene(event->pos()); auto delta = roiStartPos - curr_position; auto radius_vec = roiStartPos - roiEndPos; auto radius_2 = radius_vec.x() * radius_vec.x() + radius_vec.y() * radius_vec.y(); mouse_inside_roi = (delta.x() * delta.x() + delta.y() * delta.y() < radius_2); } } if (event->button() == Qt::LeftButton) { setCursor(Qt::ClosedHandCursor); // Change cursor to indicate panning lastMousePos = event->pos(); if (mouse_inside_roi) mouse_event_type = MouseEventType::MovingROI; else mouse_event_type = MouseEventType::Panning; } else if (event->buttons() & Qt::RightButton) { setCursor(Qt::CursorShape::CrossCursor); if (mouse_inside_roi) roiEndPos = RoundPoint(mapToScene(event->pos())); else { roiStartPos = RoundPoint(mapToScene(event->pos())); roi_type = (event->modifiers() == Qt::Modifier::SHIFT) ? RoiType::RoiCircle : RoiType::RoiBox; } mouse_event_type = MouseEventType::DrawingROI; } QGraphicsView::mousePressEvent(event); // Call base implementation } // Handle panning while moving the mouse void JFJochViewerImage::mouseMoveEvent(QMouseEvent *event) { if (!scene() || !image) return; 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()); float q = 2 * M_PI / res; if (res <= 0) q = 0; 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)); } else emit writeStatusBar(""); QPointF delta; switch (mouse_event_type) { case MouseEventType::Panning: delta = coord - mapToScene(lastMousePos); lastMousePos = event->pos(); translate(delta.x(), delta.y()); // Adjust the view's pan updateOverlay(); break; case MouseEventType::DrawingROI: roiEndPos = RoundPoint(coord); updateROI(); break; case MouseEventType::MovingROI: delta = coord - mapToScene(lastMousePos); roiStartPos += delta; roiEndPos += delta; lastMousePos = event->pos(); updateROI(); break; default: break; } QGraphicsView::mouseMoveEvent(event); } void JFJochViewerImage::mouseReleaseEvent(QMouseEvent *event) { if (!scene() || !image) return; setCursor(Qt::CursorShape::ArrowCursor); if (mouse_event_type == MouseEventType::MovingROI) { if (roi_type == RoiType::RoiBox) { roiStartPos = RoundPoint(roiStartPos); roiEndPos = RoundPoint(roiEndPos); } updateROI(); } mouse_event_type = MouseEventType::None; QGraphicsView::mouseReleaseEvent(event); } void JFJochViewerImage::LoadImageInternal() { if (!image || (image->Dataset().experiment.GetXPixelsNum() <= 0)) return; // Create a QImage with RGB format QImage qimage(image->Dataset().experiment.GetXPixelsNum(), image->Dataset().experiment.GetYPixelsNum(), QImage::Format_RGB888); image_rgb.resize(image->Dataset().experiment.GetXPixelsNum() * image->Dataset().experiment.GetYPixelsNum()); rgb sat_color{}; int r,g,b,a; feature_color.getRgb(&r, &g, &b, &a); auto bad_color = rgb{.r = static_cast(r), .g = static_cast(g), .b= static_cast(b)}; if (show_saturation) { sat_color = bad_color; } else sat_color = color_scale.Apply(1.0); // Fill the QImage with pixel data from the array for (int y = 0; y < image->Dataset().experiment.GetYPixelsNum(); ++y) { uchar *scanLine = qimage.scanLine(y); // Get writable pointer to the row for (int x = 0; x < image->Dataset().experiment.GetXPixelsNum(); ++x) { auto pxl = x + y * image->Dataset().experiment.GetXPixelsNum(); const double val_orig = (image->Image()[pxl] - background); double val = val_orig / (foreground - background); if (image->Image()[pxl] == GAP_PXL_VALUE) image_rgb[pxl] = color_scale.Apply(ColorScaleSpecial::Gap); else if (image->Image()[pxl] == ERROR_PXL_VALUE) image_rgb[pxl] = bad_color; else if (image->Image()[pxl] == SATURATED_PXL_VALUE) image_rgb[pxl] = sat_color; else if (val >= 1.0) image_rgb[pxl] = color_scale.Apply(1.0); else if (val >= 0.0) image_rgb[pxl] = color_scale.Apply(val); else image_rgb[pxl] = color_scale.Apply(0.0); scanLine[x * 3 + 0] = image_rgb[pxl].r; // Red scanLine[x * 3 + 1] = image_rgb[pxl].g; // Green scanLine[x * 3 + 2] = image_rgb[pxl].b; // Blue } } pixmap = QPixmap::fromImage(qimage); QGraphicsScene* currentScene = scene(); if (!currentScene) { // First time - create a new scene currentScene = new QGraphicsScene(this); setScene(currentScene); } } void JFJochViewerImage::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); } } void JFJochViewerImage::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 rect = 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); // 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; // Add or update text in the scene QGraphicsTextItem *textItem = scene()->addText(label, font); textItem->setDefaultTextColor(pen_color); textItem->setPos(text_x, text_y); // Position the text over the pixel // textItem->setScale(1.0); // Scale down to 10% of the original size } } } void JFJochViewerImage::DrawResolutionRings() { // Get the visible area in the scene coordinates QRectF visibleRect = mapToScene(viewport()->geometry()).boundingRect(); int startX = std::max(0, static_cast(std::floor(visibleRect.left()))); int endX = std::min(static_cast(image->Dataset().experiment.GetXPixelsNum()), static_cast(std::ceil(visibleRect.right()))); int startY = std::max(0, static_cast(std::floor(visibleRect.top()))); int endY = std::min(static_cast(image->Dataset().experiment.GetYPixelsNum()), static_cast(std::ceil(visibleRect.bottom()))); auto geom = image->Dataset().experiment.GetDiffractionGeometry(); geom.PoniRot3_rad(0.0); if (res_ring_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 = {}; } QPen pen(feature_color, 5); pen.setCosmetic(true); QVector dashPattern = {10, 15}; pen.setDashPattern(dashPattern); float phi_offset = 0; for (const auto &d: res_ring) { auto [x1,y1] = geom.ResPhiToPxl(d, 0); auto [x2,y2] = geom.ResPhiToPxl(d, M_PI_2); auto [x3,y3] = geom.ResPhiToPxl(d, M_PI); auto [x4,y4] = geom.ResPhiToPxl(d, 3.0 * M_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); scene()->addEllipse(boundingRect, pen); auto [x5,y5] = geom.ResPhiToPxl(d, phi_offset + 0); auto [x6,y6] = geom.ResPhiToPxl(d, phi_offset + M_PI_2); auto [x7,y7] = geom.ResPhiToPxl(d, phi_offset + M_PI); auto [x8,y8] = geom.ResPhiToPxl(d, phi_offset + 3.0 * M_PI_2); QPointF point_1(x5, y5); QPointF point_2(x6,y6); QPointF point_3(x7, y7); QPointF point_4(x8, y8); std::optional 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 QGraphicsTextItem *textItem = scene()->addText( QString("%1 Å").arg(QString::number(d, 'f', 2)), font); textItem->setDefaultTextColor(feature_color); textItem->setPos(point.value()); } phi_offset += 4.0 / 180.0 * M_PI; } } void JFJochViewerImage::DrawBeamCenter() { auto geom = image->Dataset().experiment.GetDiffractionGeometry(); auto [beam_x, beam_y] = geom.GetDirectBeam_pxl(); DrawCross(beam_x, beam_y, 25, 5, 2); } void JFJochViewerImage::DrawTopPixels() { int i = 0; for (auto iter = image->ValidPixels().crbegin(); iter != image->ValidPixels().rend() && i < show_highest_pixels; iter++, i++) DrawCross(iter->second % image->Dataset().experiment.GetXPixelsNum() + 0.5, iter->second / image->Dataset().experiment.GetXPixelsNum() + 0.5, 15, 3); } void JFJochViewerImage::updateROI() { if (roi_type == RoiType::RoiBox) { QRect roi_box_int = QRectF(RoundPoint(roiStartPos), RoundPoint(roiEndPos)).normalized().toRect(); roi_box = roi_box_int; emit roiBoxUpdated(roi_box_int); } else { QPointF delta = roiStartPos - roiEndPos; double radius = std::sqrt(delta.x() * delta.x() + delta.y() * delta.y()); roi_box = QRectF(roiStartPos.x() - radius, roiStartPos.y() - radius, 2 * radius, 2 * radius).normalized(); emit roiCircleUpdated(roiStartPos.x(), roiStartPos.y(), radius); } updateOverlay(); } void JFJochViewerImage::updateOverlay() { if (!scene() || !image) return; scene()->clear(); scene()->addItem(new QGraphicsPixmapItem(pixmap)); QFont font("Arial", 1); // Font for pixel value text font.setPixelSize(1); // This will render very small text (1-pixel high). // Get the visible area in the scene coordinates QRectF visibleRect = mapToScene(viewport()->geometry()).boundingRect(); // Calculate the range of pixels to process int startX = std::max(0, static_cast(std::floor(visibleRect.left()))); int endX = std::min(static_cast(image->Dataset().experiment.GetXPixelsNum()), static_cast(std::ceil(visibleRect.right()))); int startY = std::max(0, static_cast(std::floor(visibleRect.top()))); int endY = std::min(static_cast(image->Dataset().experiment.GetYPixelsNum()), static_cast(std::ceil(visibleRect.bottom()))); if (scale_factor > 20.0 && (endX - startX + 1) * (endY - startY + 1) < 500) { // Iterate through the pixels within the visible range for (int y = startY; y < endY; ++y) { for (int x = startX; x < endX; ++x) { QString pixelText; int32_t val = image->Image()[x + y * image->Dataset().experiment.GetXPixelsNum()]; if (val == SATURATED_PXL_VALUE) pixelText = "Sat"; else if (val == GAP_PXL_VALUE) pixelText = "Gap"; else if (val == ERROR_PXL_VALUE) pixelText = "Err"; else pixelText = QString("%1").arg(val); // Add or update text in the scene QGraphicsTextItem *textItem = scene()->addText(pixelText, font); if (luminance(image_rgb[x + y * image->Dataset().experiment.GetXPixelsNum()]) > 128.0) textItem->setDefaultTextColor(Qt::black); // Text color else textItem->setDefaultTextColor(Qt::white); // Text color textItem->setPos(x-0.7, y-0.8); // Position the text over the pixel textItem->setScale(0.2); // Scale down to 10% of the original size } } } DrawBeamCenter(); if (show_spots) DrawSpots(); if (show_predictions) DrawPredictions(); DrawResolutionRings(); DrawTopPixels(); if (show_saturation) DrawSaturation(); if (roi_box.width() * roi_box.height() > 0) { QPen pen(feature_color, 3); pen.setStyle(Qt::DashLine); pen.setCosmetic(true); if (roi_type == RoiType::RoiBox) { scene()->addRect(roi_box, pen); } else { scene()->addEllipse(roi_box, pen); double pointRadius = 0.5; QRectF pointBoundingBox = QRectF(roiStartPos.x() - pointRadius,roiStartPos.y() - pointRadius, 2 * pointRadius, 2 * pointRadius); scene()->addEllipse(pointBoundingBox, pen); } } } void JFJochViewerImage::resizeEvent(QResizeEvent *event) { // Call the base class implementation first QGraphicsView::resizeEvent(event); updateOverlay(); } void JFJochViewerImage::onScroll(int value) { updateOverlay(); } void JFJochViewerImage::Redraw() { if (!image) return; // Save the current transformation (zoom state) QTransform currentTransform = this->transform(); // Regenerate the image LoadImageInternal(); // Restore the zoom level this->setTransform(currentTransform, false); // "false" prevents resetting the view updateOverlay(); } void JFJochViewerImage::loadImage(std::shared_ptr in_image) { if (in_image) { image = in_image; Redraw(); } else { image.reset(); if (scene()) scene()->clear(); } } void JFJochViewerImage::changeBackground(float val) { background = val; Redraw(); } void JFJochViewerImage::changeForeground(float val) { foreground = val; Redraw(); } void JFJochViewerImage::setColorMap(int color_map) { try { color_scale.Select(static_cast(color_map)); Redraw(); } catch (...) {} } void JFJochViewerImage::setResolutionRing(QVector v) { res_ring = v; res_ring_auto = false; Redraw(); } void JFJochViewerImage::setResolutionRingAuto() { res_ring_auto = true; Redraw(); } void JFJochViewerImage::showSpots(bool input) { show_spots = input; Redraw(); } void JFJochViewerImage::showPredictions(bool input) { show_predictions = input; Redraw(); } void JFJochViewerImage::setFeatureColor(QColor input) { feature_color = input; Redraw(); } void JFJochViewerImage::setSpotColor(QColor input) { spot_color = input; Redraw(); } void JFJochViewerImage::setPredictionColor(QColor input) { prediction_color = input; Redraw(); } void JFJochViewerImage::showHighestPixels(int32_t v) { show_highest_pixels = v; Redraw(); } void JFJochViewerImage::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 JFJochViewerImage::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 } void JFJochViewerImage::showSaturation(bool input) { show_saturation = input; Redraw(); } void JFJochViewerImage::centerOnSpot(double x, double y) { if (!image) return; int imgW = image->Dataset().experiment.GetXPixelsNum(); int imgH = image->Dataset().experiment.GetYPixelsNum(); if (x >= 0 && x < imgW && y >= 0 && y < imgH) centerOn(x, y); } void JFJochViewerImage::highlightIceRings(bool input) { highlight_ice_rings = input; Redraw(); }