From a704a2cd339019c6f4b4a9c247954fd3e80e3b83 Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Sun, 26 Jul 2026 18:58:55 +0200 Subject: [PATCH] Viewer: draw the rendered image directly instead of via a QPixmap Every recolour ended with QPixmap::fromImage(), which allocates a second full-size buffer and converts the whole image into the screen format. That conversion was the largest single cost left in the colouring path. Replace QGraphicsPixmapItem with a small item that paints qimg_buffer_ with QPainter::drawImage. The buffer is already what the raster engine wants, so nothing is converted or copied. The item declares its opaque area, as the pixmap item did, so the view still skips the background fill underneath it, and it turns SmoothPixmapTransform off before drawing to keep the nearest-neighbour sampling QGraphicsPixmapItem gave us by default -- zoomed-in detector pixels stay sharp squares. GeneratePixmap is renamed RenderImage: it no longer makes a pixmap. 18.1 Mpx recolour: 22 -> 5.6 ms (28.0 ms before this series). Co-Authored-By: Claude Opus 5 (1M context) --- viewer/image_viewer/JFJochAzIntImage.cpp | 4 +- .../image_viewer/JFJochDiffractionImage.cpp | 8 +-- viewer/image_viewer/JFJochGridScanImage.cpp | 2 +- viewer/image_viewer/JFJochImage.cpp | 64 +++++++++++++------ viewer/image_viewer/JFJochImage.h | 25 ++++++-- viewer/image_viewer/JFJochSimpleImage.cpp | 2 +- 6 files changed, 71 insertions(+), 34 deletions(-) diff --git a/viewer/image_viewer/JFJochAzIntImage.cpp b/viewer/image_viewer/JFJochAzIntImage.cpp index 4588de53..449ee039 100644 --- a/viewer/image_viewer/JFJochAzIntImage.cpp +++ b/viewer/image_viewer/JFJochAzIntImage.cpp @@ -68,8 +68,8 @@ void JFJochAzIntImage::imageLoaded(std::shared_ptr in_i emit backgroundChanged(background); emit foregroundChanged(foreground); - // Generate pixmap and redraw using base class functionality - GeneratePixmap(); + // Render the image and redraw using base class functionality + RenderImage(); Redraw(); CalcROI(); } else { diff --git a/viewer/image_viewer/JFJochDiffractionImage.cpp b/viewer/image_viewer/JFJochDiffractionImage.cpp index 536b24c2..9c535ad5 100644 --- a/viewer/image_viewer/JFJochDiffractionImage.cpp +++ b/viewer/image_viewer/JFJochDiffractionImage.cpp @@ -847,7 +847,7 @@ void JFJochDiffractionImage::UpdateForeground() { void JFJochDiffractionImage::setHDRMode(bool input) { hdr_mode = input; UpdateForeground(); - GeneratePixmap(); + RenderImage(); Redraw(); } @@ -857,7 +857,7 @@ void JFJochDiffractionImage::loadImage(std::shared_ptr image = in_image; UpdateForeground(); LoadImageInternal(); - GeneratePixmap(); + RenderImage(); Redraw(); CalcROI(); } else { @@ -877,7 +877,7 @@ 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(); + RenderImage(); Redraw(); emit autoForegroundChanged(auto_fg); } @@ -937,7 +937,7 @@ void JFJochDiffractionImage::DrawCross(float x, float y, float size, float width void JFJochDiffractionImage::showSaturation(bool input) { show_saturation = input; - GeneratePixmap(); + RenderImage(); updateOverlay(); } diff --git a/viewer/image_viewer/JFJochGridScanImage.cpp b/viewer/image_viewer/JFJochGridScanImage.cpp index a20b0ca8..a98121e6 100644 --- a/viewer/image_viewer/JFJochGridScanImage.cpp +++ b/viewer/image_viewer/JFJochGridScanImage.cpp @@ -58,7 +58,7 @@ void JFJochGridScanImage::loadData(const std::vector &data, const GridSca background = minv; foreground = maxv; - GeneratePixmap(); + RenderImage(); Redraw(); CalcROI(); } diff --git a/viewer/image_viewer/JFJochImage.cpp b/viewer/image_viewer/JFJochImage.cpp index a75b5840..62ef5b1c 100644 --- a/viewer/image_viewer/JFJochImage.cpp +++ b/viewer/image_viewer/JFJochImage.cpp @@ -3,7 +3,6 @@ #include "JFJochImage.h" -#include #include #include #include @@ -20,6 +19,31 @@ #include #include +QRectF JFJochImageItem::boundingRect() const { + return QRectF(0, 0, img_.width(), img_.height()); +} + +QPainterPath JFJochImageItem::opaqueArea() const { + // The buffer is RGB32, so the item fully covers its bounding rect + QPainterPath path; + path.addRect(boundingRect()); + return path; +} + +void JFJochImageItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) { + if (img_.isNull()) + return; + // QGraphicsPixmapItem defaults to Qt::FastTransformation and turned this hint off before + // drawing; keep doing that, so zoomed-in detector pixels stay sharp squares. + painter->setRenderHint(QPainter::SmoothPixmapTransform, false); + painter->drawImage(0, 0, img_); +} + +void JFJochImageItem::refresh() { + prepareGeometryChange(); + update(); +} + JFJochImage::JFJochImage(QWidget *parent) : QGraphicsView(parent) { setDragMode(QGraphicsView::NoDrag); // Disable default drag mode setTransformationAnchor(QGraphicsView::AnchorUnderMouse); // Zoom anchors @@ -45,7 +69,7 @@ void JFJochImage::onScroll(int value) { void JFJochImage::changeBackground(float val) { background = val; - GeneratePixmap(); + RenderImage(); Redraw(); } @@ -54,7 +78,7 @@ void JFJochImage::changeForeground(float val) { emit autoForegroundChanged(false); foreground = val; // Regenerate the image - GeneratePixmap(); + RenderImage(); Redraw(); } @@ -62,7 +86,7 @@ void JFJochImage::setColorMap(int color_map) { try { color_scale.Select(static_cast(color_map)); // Regenerate the image - GeneratePixmap(); + RenderImage(); Redraw(); } catch (...) { } @@ -70,7 +94,7 @@ void JFJochImage::setColorMap(int color_map) { void JFJochImage::setFeatureColor(QColor input) { feature_color = input; - GeneratePixmap(); + RenderImage(); Redraw(); } @@ -333,7 +357,7 @@ void JFJochImage::contextMenuEvent(QContextMenuEvent *event) { QAction *fitAct = menu.addAction(tr("Fit image to view")); QAction *clearRoiAct = menu.addAction(tr("Clear ROI")); - const bool hasImage = (W > 0 && H > 0 && !pixmap.isNull()); + const bool hasImage = (W > 0 && H > 0 && !qimg_buffer_.isNull()); copyImageAct->setEnabled(hasImage); copyWithOverlayAct->setEnabled(hasImage && scene()); saveImageAct->setEnabled(hasImage); @@ -393,7 +417,7 @@ QImage JFJochImage::renderToImage(bool with_overlay) { p.end(); } else { // The underlying rendered image (no overlay) - img = pixmap.toImage(); + img = qimg_buffer_; } // Ensure 1:1 pixel ratio and 96 DPI metadata to avoid rescaling in consumer apps img.setDevicePixelRatio(1.0); @@ -404,7 +428,7 @@ QImage JFJochImage::renderToImage(bool with_overlay) { } void JFJochImage::copyImageToClipboard() { - if (W == 0 || H == 0 || pixmap.isNull()) return; + if (W == 0 || H == 0 || qimg_buffer_.isNull()) return; setClipboardAsJpegAndImage(renderToImage(false), 95); emit writeStatusBar(tr("Image copied to clipboard"), 2000); @@ -418,7 +442,7 @@ void JFJochImage::copyImageWithOverlayToClipboard() { } void JFJochImage::saveImageToFile(bool with_overlay) { - if (W == 0 || H == 0 || pixmap.isNull()) return; + if (W == 0 || H == 0 || qimg_buffer_.isNull()) return; if (with_overlay && !scene()) return; const QString caption = with_overlay ? tr("Save image with overlay as JPEG") @@ -641,7 +665,7 @@ void JFJochImage::Redraw() { setScene(currentScene); // Reset initial-fit state for a new scene initial_fit_done_ = false; - pixmap_item_ = nullptr; // new scene, old pointer invalid + image_item_ = nullptr; // new scene, old pointer invalid } // Perform initial fit only once per image size @@ -650,7 +674,7 @@ void JFJochImage::Redraw() { updateOverlay(); } -void JFJochImage::GeneratePixmap() { +void JFJochImage::RenderImage() { if (qimg_buffer_.width() != int(W) || qimg_buffer_.height() != int(H)) qimg_buffer_ = QImage(int(W), int(H), QImage::Format_RGB32); @@ -727,9 +751,6 @@ void JFJochImage::GeneratePixmap() { scanLine[x] = qRgb(c.r, c.g, c.b); } }); - - pixmap = QPixmap::fromImage(qimg_buffer_); - pixmap.setDevicePixelRatio(1.0); } void JFJochImage::centerOnSpot(QPointF point) { @@ -837,7 +858,7 @@ void JFJochImage::writePixelLabels() { } void JFJochImage::resetScenePointers() { - pixmap_item_ = nullptr; + image_item_ = nullptr; overlay_items_.clear(); } @@ -846,18 +867,19 @@ void JFJochImage::updateOverlay() { beforeOverlayCleared(); - // Remove only overlay items, keep the pixmap item persistent + // Remove only overlay items, keep the image item persistent for (auto *item : overlay_items_) scene()->removeItem(item); qDeleteAll(overlay_items_); overlay_items_.clear(); - // Ensure pixmap item exists and is up-to-date - if (!pixmap_item_) { - pixmap_item_ = scene()->addPixmap(pixmap); - pixmap_item_->setZValue(0); + // Ensure the image item exists and is up-to-date + if (!image_item_) { + image_item_ = new JFJochImageItem(qimg_buffer_); + image_item_->setZValue(0); + scene()->addItem(image_item_); } else { - pixmap_item_->setPixmap(pixmap); + image_item_->refresh(); } if (scale_factor > 30.0) diff --git a/viewer/image_viewer/JFJochImage.h b/viewer/image_viewer/JFJochImage.h index 2d3199d3..a6f9e3f2 100644 --- a/viewer/image_viewer/JFJochImage.h +++ b/viewer/image_viewer/JFJochImage.h @@ -4,7 +4,10 @@ #pragma once #include +#include +#include #include +#include #include #include #include "../../common/ColorScale.h" @@ -12,6 +15,19 @@ // Q_DECLARE_METATYPE(ROIMessage) +// Draws the rendered frame straight out of JFJochImage::qimg_buffer_. A QGraphicsPixmapItem +// would mean converting the whole image into a QPixmap on every recolour, which costs one +// extra allocation and a full pass over the pixels. +class JFJochImageItem : public QGraphicsItem { + const QImage &img_; +public: + explicit JFJochImageItem(const QImage &img) : img_(img) {} + QRectF boundingRect() const override; + QPainterPath opaqueArea() const override; + void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; + void refresh(); // the buffer behind the item changed +}; + class JFJochImage : public QGraphicsView { Q_OBJECT @@ -63,11 +79,10 @@ protected: float background = 0.0; ColorScale color_scale; std::vector image_fp; - QPixmap pixmap; QImage qimg_buffer_; // reusable image buffer — avoids 64MB alloc per frame - // Persistent pixmap item — never destroyed/recreated on overlay update - QGraphicsPixmapItem *pixmap_item_ = nullptr; + // Persistent image item — never destroyed/recreated on overlay update + JFJochImageItem *image_item_ = nullptr; // Overlay items managed separately QList overlay_items_; @@ -103,11 +118,11 @@ protected: ResizeHandle hitTestROIHandle(const QPointF& scenePos, qreal tol = 3.0) const; void updateOverlay(); - void GeneratePixmap(); + void RenderImage(); void Redraw(); void CalcROI(); - // Invalidate pixmap_item_ and overlay tracking after scene()->clear() + // Invalidate image_item_ and overlay tracking after scene()->clear() void resetScenePointers(); // Perform initial fit-to-view (shorter direction), once per image size diff --git a/viewer/image_viewer/JFJochSimpleImage.cpp b/viewer/image_viewer/JFJochSimpleImage.cpp index bfa029e7..25156a6e 100644 --- a/viewer/image_viewer/JFJochSimpleImage.cpp +++ b/viewer/image_viewer/JFJochSimpleImage.cpp @@ -27,7 +27,7 @@ void JFJochSimpleImage::setImage(std::shared_ptr img) { if (img) { image_ = std::move(img); loadImageInternal(); - GeneratePixmap(); + RenderImage(); Redraw(); CalcROI(); } else {