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:
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ void JFJochGridScanImage::loadData(const std::vector<float> &data, const GridSca
|
||||
background = minv;
|
||||
foreground = maxv;
|
||||
|
||||
GeneratePixmap();
|
||||
RenderImage();
|
||||
Redraw();
|
||||
CalcROI();
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user