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) <noreply@anthropic.com>
This commit is contained in:
2026-07-26 18:58:55 +02:00
co-authored by Claude Opus 5
parent 0cee55f654
commit a704a2cd33
6 changed files with 71 additions and 34 deletions
+2 -2
View File
@@ -68,8 +68,8 @@ void JFJochAzIntImage::imageLoaded(std::shared_ptr<const JFJochReaderImage> 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 {
@@ -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<const JFJochReaderImage>
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();
}
+1 -1
View File
@@ -58,7 +58,7 @@ void JFJochGridScanImage::loadData(const std::vector<float> &data, const GridSca
background = minv;
foreground = maxv;
GeneratePixmap();
RenderImage();
Redraw();
CalcROI();
}
+43 -21
View File
@@ -3,7 +3,6 @@
#include "JFJochImage.h"
#include <QGraphicsPixmapItem>
#include <QGraphicsSimpleTextItem>
#include <QScrollBar>
#include <QWheelEvent>
@@ -20,6 +19,31 @@
#include <QPainter>
#include <QtConcurrent/QtConcurrent>
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<ColorScaleEnum>(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)
+20 -5
View File
@@ -4,7 +4,10 @@
#pragma once
#include <QGraphicsView>
#include <QGraphicsItem>
#include <QImage>
#include <QMouseEvent>
#include <QPainterPath>
#include <QTransform>
#include <QPointF>
#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<float> 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<QGraphicsItem *> 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
+1 -1
View File
@@ -27,7 +27,7 @@ void JFJochSimpleImage::setImage(std::shared_ptr<const SimpleImage> img) {
if (img) {
image_ = std::move(img);
loadImageInternal();
GeneratePixmap();
RenderImage();
Redraw();
CalcROI();
} else {