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 {