From ae6574d3ecf009dece1ebeb3fed8ce3cf701f7a5 Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Fri, 7 Nov 2025 12:06:18 +0100 Subject: [PATCH] jfjoch_viewer: Combine Simple image viewer and DiffractionImageViewer into one parent class --- viewer/CMakeLists.txt | 2 + viewer/widgets/JFJochImage.cpp | 86 ++++++++++++++ viewer/widgets/JFJochImage.h | 43 +++++++ viewer/widgets/JFJochSimpleImageViewer.cpp | 125 +++++---------------- viewer/widgets/JFJochSimpleImageViewer.h | 30 +---- viewer/widgets/JFJochViewerImage.cpp | 79 +------------ viewer/widgets/JFJochViewerImage.h | 27 +---- viewer/windows/JFJochCalibrationWindow.cpp | 3 + 8 files changed, 175 insertions(+), 220 deletions(-) create mode 100644 viewer/widgets/JFJochImage.cpp create mode 100644 viewer/widgets/JFJochImage.h diff --git a/viewer/CMakeLists.txt b/viewer/CMakeLists.txt index df165015..0043b3da 100644 --- a/viewer/CMakeLists.txt +++ b/viewer/CMakeLists.txt @@ -71,6 +71,8 @@ ADD_EXECUTABLE(jfjoch_viewer jfjoch_viewer.cpp JFJochViewerWindow.cpp JFJochView windows/JFJochCalibrationWindow.h windows/JFJochHelperWindow.cpp windows/JFJochHelperWindow.h + widgets/JFJochImage.cpp + widgets/JFJochImage.h ) TARGET_LINK_LIBRARIES(jfjoch_viewer Qt6::Core Qt6::Gui Qt6::Widgets Qt6::Charts Qt6::DBus diff --git a/viewer/widgets/JFJochImage.cpp b/viewer/widgets/JFJochImage.cpp new file mode 100644 index 00000000..bc684124 --- /dev/null +++ b/viewer/widgets/JFJochImage.cpp @@ -0,0 +1,86 @@ +// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute +// SPDX-License-Identifier: GPL-3.0-only + +#include "JFJochImage.h" + +#include +#include + +JFJochImage::JFJochImage(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, &JFJochImage::onScroll); + + // Connect the vertical scrollbar's valueChanged signal + connect(verticalScrollBar(), &QScrollBar::valueChanged, this, &JFJochImage::onScroll); +} + +void JFJochImage::onScroll(int value) { + updateOverlay(); +} + +void JFJochImage::changeBackground(float val) { + background = val; + Redraw(); +} + +void JFJochImage::changeForeground(float val) { + foreground = val; + Redraw(); +} + +void JFJochImage::setColorMap(int color_map) { + try { + color_scale.Select(static_cast(color_map)); + Redraw(); + } catch (...) { + } +} + +void JFJochImage::setFeatureColor(QColor input) { + feature_color = input; + Redraw(); +} + +void JFJochImage::wheelEvent(QWheelEvent *event) { + if (!scene()) 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(); + } +} diff --git a/viewer/widgets/JFJochImage.h b/viewer/widgets/JFJochImage.h new file mode 100644 index 00000000..b4d0cc6e --- /dev/null +++ b/viewer/widgets/JFJochImage.h @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute +// SPDX-License-Identifier: GPL-3.0-only + +#ifndef JFJOCH_JFJOCHIMAGE_H +#define JFJOCH_JFJOCHIMAGE_H + +#include +#include "../../common/ColorScale.h" + +class JFJochImage : public QGraphicsView { + Q_OBJECT + + virtual void updateOverlay() = 0; + virtual void Redraw() = 0; + + void wheelEvent(QWheelEvent* event) override; + +protected: + QColor feature_color = Qt::magenta; + float foreground = 10.0; + float background = 0.0; + double scale_factor = 1.0; + ColorScale color_scale; + std::vector image_rgb; + QPixmap pixmap; + +signals: + void foregroundChanged(float v); + void backgroundChanged(float v); + void writeStatusBar(QString string, int timeout_ms = 0); + +private slots: + void onScroll(int value); +public slots: + void setFeatureColor(QColor input); + void setColorMap(int color_map); + void changeForeground(float val); + void changeBackground(float val); +public: + explicit JFJochImage(QWidget *parent = nullptr); +}; + +#endif //JFJOCH_JFJOCHIMAGE_H \ No newline at end of file diff --git a/viewer/widgets/JFJochSimpleImageViewer.cpp b/viewer/widgets/JFJochSimpleImageViewer.cpp index 03a2608e..6f11a701 100644 --- a/viewer/widgets/JFJochSimpleImageViewer.cpp +++ b/viewer/widgets/JFJochSimpleImageViewer.cpp @@ -21,26 +21,15 @@ static inline double normalize_to_unit(double raw, double bg, double fg) { } JFJochSimpleImageViewer::JFJochSimpleImageViewer(QWidget *parent) - : QGraphicsView(parent) { - setDragMode(QGraphicsView::NoDrag); - setTransformationAnchor(QGraphicsView::AnchorUnderMouse); - setRenderHint(QPainter::Antialiasing); - setRenderHint(QPainter::SmoothPixmapTransform); - setFocusPolicy(Qt::ClickFocus); - + : JFJochImage(parent) { auto *scn = new QGraphicsScene(this); setScene(scn); // Optional: a sensible default colormap - color_scale_.Select(ColorScaleEnum::Indigo); + color_scale.Select(ColorScaleEnum::Indigo); // Keep overlays in pixel units independent of zoom (for labels font sizing) setViewportUpdateMode(QGraphicsView::FullViewportUpdate); - - // QTimer for smoother movement/updates - repaint_timer_.setInterval(16); // ~60 FPS - repaint_timer_.setSingleShot(false); - connect(&repaint_timer_, &QTimer::timeout, this, &JFJochSimpleImageViewer::onRepaintTimer); } void JFJochSimpleImageViewer::clear() { @@ -55,69 +44,14 @@ void JFJochSimpleImageViewer::setImage(std::shared_ptr img) { image_ = std::move(img); has_image_ = true; renderImage(); - updateScene(); + updateOverlay(); } -void JFJochSimpleImageViewer::setBackground(float v) { - background_ = v; - emit backgroundChanged(background_); +void JFJochSimpleImageViewer::Redraw() { if (has_image_) { renderImage(); // Preserve current transform while updating - updateScene(); - } -} - -void JFJochSimpleImageViewer::setForeground(float v) { - foreground_ = std::max(1e-6f, v); - emit foregroundChanged(foreground_); - if (has_image_) { - renderImage(); - updateScene(); - } -} - -void JFJochSimpleImageViewer::setColorMap(int colorMapEnumValue) { - try { - color_scale_.Select(static_cast(colorMapEnumValue)); - if (has_image_) { - renderImage(); - updateScene(); - } - } catch (...) { - // ignore invalid value - } -} - -void JFJochSimpleImageViewer::wheelEvent(QWheelEvent *event) { - if (!scene() || !has_image_) return; - - const double zoomFactor = 1.15; - const QPointF targetScenePos = mapToScene(event->position().toPoint()); - - if (event->modifiers() == Qt::ShiftModifier) { - // Shift + wheel adjusts foreground (like your main viewer) - float new_fg = foreground_ + event->angleDelta().y() / 120.0f; - if (new_fg < 1.0f) new_fg = 1.0f; - setForeground(new_fg); - } else { - 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); - } - } - // Keep focus under mouse - const QPointF updatedCenter = mapToScene(viewport()->rect().center()); - const QPointF delta = targetScenePos - updatedCenter; - translate(delta.x(), delta.y()); - // Only labels depend on redraw; pixmap stays. Rebuild scene to re-place labels - updateScene(); + updateOverlay(); } } @@ -130,7 +64,7 @@ void JFJochSimpleImageViewer::mousePressEvent(QMouseEvent* event) { // With Shift -> ROI interactions (draw/resize/move); without -> pan if (event->modifiers() & Qt::Modifier::SHIFT) { // Determine if we are over an existing ROI handle - active_handle_ = hitTestROIHandle(scenePos, 4.0 / std::sqrt(std::max(1e-4, scale_factor_))); + active_handle_ = hitTestROIHandle(scenePos, 4.0 / std::sqrt(std::max(1e-4, scale_factor))); if (active_handle_ != ResizeHandle::None && active_handle_ != ResizeHandle::Inside) { mouse_event_type_ = MouseEventType::ResizingROI; roi_start_pos_ = roi_box_.topLeft(); @@ -167,7 +101,7 @@ void JFJochSimpleImageViewer::mouseMoveEvent(QMouseEvent* event) { const QPointF delta = mapToScene(event->pos()) - mapToScene(last_mouse_pos_); last_mouse_pos_ = event->pos(); translate(delta.x(), delta.y()); - updateScene(); + updateOverlay(); break; } case MouseEventType::DrawingROI: { @@ -254,7 +188,7 @@ void JFJochSimpleImageViewer::renderImage() { // Prepare destination QImage (RGB888) QImage qimg(int(W), int(H), QImage::Format_RGB888); - image_rgb_.resize(W * H); + image_rgb.resize(W * H); image_values_.resize(W * H); std::vector image_buffer; @@ -271,7 +205,7 @@ void JFJochSimpleImageViewer::renderImage() { dstRow[idx * 3 + 0] = c.r; dstRow[idx * 3 + 1] = c.g; dstRow[idx * 3 + 2] = c.b; - image_rgb_[idx] = c; + image_rgb[idx] = c; }; // Fast path: packed RGB (assumed 3 bytes per pixel) @@ -317,16 +251,16 @@ void JFJochSimpleImageViewer::renderImage() { } // Build pixmap - pixmap_ = QPixmap::fromImage(qimg); + pixmap = QPixmap::fromImage(qimg); } -void JFJochSimpleImageViewer::updateScene() { +void JFJochSimpleImageViewer::updateOverlay() { if (!scene()) return; scene()->clear(); if (!has_image_) return; // Add base image - scene()->addItem(new QGraphicsPixmapItem(pixmap_)); + 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). @@ -344,8 +278,8 @@ void JFJochSimpleImageViewer::updateScene() { const int endY = std::min(H, static_cast(std::ceil(visibleRect.bottom()))); // Only when zoomed in and region small (performance) - if (scale_factor_ > 20.0 && (endX - startX + 1) * (endY - startY + 1) < 500) { - const qreal f = std::clamp(scale_factor_, 0.5, 50.0); + if (scale_factor > 20.0 && (endX - startX + 1) * (endY - startY + 1) < 500) { + const qreal f = std::clamp(scale_factor, 0.5, 50.0); QFont font("Arial", 1); font.setPixelSize(1); // 1 px font; we use scaling below as needed @@ -355,7 +289,7 @@ void JFJochSimpleImageViewer::updateScene() { const size_t idx = base + size_t(x); const double raw = image_values_[idx]; - const rgb c = image_rgb_[idx]; + const rgb c = image_rgb[idx]; // If no scalar was stored (e.g., RGB source), you can skip or compute some alt text if (std::isnan(raw)) continue; @@ -398,19 +332,19 @@ void JFJochSimpleImageViewer::renderImage(QImage &qimg, const uint8_t *input) { if (auto_fgbg) { auto vec1(image_values_); std::sort(vec1.begin(), vec1.end()); - background_ = vec1[0]; - foreground_ = vec1[vec1.size() - 1]; - emit backgroundChanged(background_); - emit foregroundChanged(foreground_); + background = vec1[0]; + foreground = vec1[vec1.size() - 1]; + emit backgroundChanged(background); + emit foregroundChanged(foreground); } for (int y = 0; y < H; ++y) { uchar *scanLine = qimg.scanLine(y); // Get writable pointer to the row for (int x = 0; x < W; ++x) { - image_rgb_[y * W + x] = color_scale_.Apply(image_values_[y * W + x], background_, foreground_); - scanLine[x * 3 + 0] = image_rgb_[y * W + x].r; // Red - scanLine[x * 3 + 1] = image_rgb_[y * W + x].g; // Green - scanLine[x * 3 + 2] = image_rgb_[y * W + x].b; // Blue + image_rgb[y * W + x] = color_scale.Apply(image_values_[y * W + x], background, foreground); + scanLine[x * 3 + 0] = image_rgb[y * W + x].r; // Red + scanLine[x * 3 + 1] = image_rgb[y * W + x].g; // Green + scanLine[x * 3 + 2] = image_rgb[y * W + x].b; // Blue } } } @@ -480,7 +414,7 @@ void JFJochSimpleImageViewer::drawROI(QGraphicsScene* scn) { scn->addRect(roi_box_, pen); // Small corner handles - const qreal handleSize = 3.0 / std::sqrt(std::max(1e-4, scale_factor_)); + const qreal handleSize = 3.0 / std::sqrt(std::max(1e-4, scale_factor)); auto addHandle = [&](const QPointF& p) { scn->addRect(QRectF(p.x() - handleSize, p.y() - handleSize, 2 * handleSize, 2 * handleSize), QPen(feature_color, 1), QBrush(feature_color)); @@ -508,11 +442,11 @@ void JFJochSimpleImageViewer::drawROI(QGraphicsScene* scn) { const int H = int(image_->image.GetHeight()); int px = std::clamp(roi_px.left(), 0, std::max(0, W - 1)); int py = std::clamp(roi_px.top(), 0, std::max(0, H - 1)); - const rgb bgc = image_rgb_[size_t(py) * size_t(W) + size_t(px)]; + const rgb bgc = image_rgb[size_t(py) * size_t(W) + size_t(px)]; const bool darkText = luminance(bgc) > 128.0; QFont font("Arial"); - const qreal f = std::clamp(scale_factor_, 0.5, 50.0); + const qreal f = std::clamp(scale_factor, 0.5, 50.0); font.setPointSizeF(14.0 / std::sqrt(f)); auto* t = scn->addText(text, font); @@ -579,7 +513,7 @@ void JFJochSimpleImageViewer::onRepaintTimer() { // Rebuild scene if requested if (needs_scene_update_) { - updateScene(); + updateOverlay(); needs_scene_update_ = false; } @@ -600,8 +534,3 @@ void JFJochSimpleImageViewer::scheduleSceneUpdate() { if (!repaint_timer_.isActive()) repaint_timer_.start(); } - -void JFJochSimpleImageViewer::setFeatureColor(QColor input) { - feature_color = input; - scheduleSceneUpdate(); -} diff --git a/viewer/widgets/JFJochSimpleImageViewer.h b/viewer/widgets/JFJochSimpleImageViewer.h index d6e25c4c..5f935dab 100644 --- a/viewer/widgets/JFJochSimpleImageViewer.h +++ b/viewer/widgets/JFJochSimpleImageViewer.h @@ -3,7 +3,7 @@ #pragma once -#include +#include "JFJochImage.h" #include #include #include @@ -14,27 +14,16 @@ #include "../SimpleImage.h" #include "../../common/ColorScale.h" -class JFJochSimpleImageViewer : public QGraphicsView { +class JFJochSimpleImageViewer : public JFJochImage { Q_OBJECT - QColor feature_color = Qt::magenta; + void Redraw() override; public: explicit JFJochSimpleImageViewer(QWidget* parent = nullptr); void setImage(std::shared_ptr img); void clear(); -public slots: - void setBackground(float v); - void setForeground(float v); - void setColorMap(int colorMapEnumValue); - void setFeatureColor(QColor input); -signals: - void foregroundChanged(float v); - void backgroundChanged(float v); - void writeStatusBar(QString string, int timeout_ms = 0); - protected: - void wheelEvent(QWheelEvent* event) override; void mousePressEvent(QMouseEvent* event) override; void mouseMoveEvent(QMouseEvent* event) override; void mouseReleaseEvent(QMouseEvent* event) override; @@ -57,19 +46,17 @@ private: }; void renderImage(); // builds pixmap_ from current image_ + settings - void updateScene(); // clears scene, adds pixmap + labels + ROI (if any) + void updateOverlay() override; // clears scene, adds pixmap + labels + ROI (if any) void drawROI(QGraphicsScene* scn); // draw box and stats ROIStats computeROIStats(const QRect& roi_px) const; // Image and rendering state std::shared_ptr image_; bool has_image_ = false; - QPixmap pixmap_; - std::vector image_rgb_; // intermediate RGB buffer to feed QImage + std::vector image_values_; // original scalar values per pixel (for labels and min/max) // View/interaction - double scale_factor_ = 1.0; QPoint last_mouse_pos_; bool panning_ = false; @@ -81,13 +68,6 @@ private: void scheduleSceneUpdate(); void onRepaintTimer(); - // Settings - float background_ = 0.0f; - float foreground_ = 10.0f; - ColorScale color_scale_; - bool show_labels_ = false; - std::vector