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 c25f9c7b51
commit 8cfc820767
6 changed files with 71 additions and 34 deletions
+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)