From 7a893bb1e76f66522c1c835c845687dd73e53d0b Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Sun, 26 Jul 2026 21:18:20 +0200 Subject: [PATCH] Viewer: per-pixel counts in the magnifier, read from the int32 image Users expect a magnifier to tell them the counts, which the follower view could not do: it has the rendered pixels but not the numbers behind them. Take them from the detector's int32 buffer directly, the same source the main view colours from, so no float copy of the image is needed - the magnifier still holds nothing full-size of its own, only a shared_ptr to the frame and one to the reader image. The labels are painted in drawForeground() rather than as scene items. The main view creates up to 5000 QGraphicsSimpleTextItems per overlay rebuild for this; here they are just drawn, so there is no item churn and no scene invalidation. Text is laid out in viewport pixels so it stays a constant readable size, and black/white is chosen from the luminance of the rendered pixel underneath, as the main view does. Threshold is the same 30x as the main view, so the default 12x magnification shows no labels until the user wheels in; a cap keeps pathological window sizes from drawing thousands of them. Verified in the GUI at 32x: counts drawn per pixel with white text over the dark centre of a Bragg peak and black elsewhere, and "Gap" across a module gap. Main image panel still pixel-identical to the pre-series baseline. Co-Authored-By: Claude Opus 5 (1M context) --- viewer/JFJochViewerWindow.cpp | 4 ++ viewer/image_viewer/JFJochFollowerImage.cpp | 62 +++++++++++++++++++++ viewer/image_viewer/JFJochFollowerImage.h | 16 +++++- viewer/windows/JFJochMagnifierWindow.cpp | 4 ++ viewer/windows/JFJochMagnifierWindow.h | 4 ++ 5 files changed, 88 insertions(+), 2 deletions(-) diff --git a/viewer/JFJochViewerWindow.cpp b/viewer/JFJochViewerWindow.cpp index 82fade34..56ea0a8f 100644 --- a/viewer/JFJochViewerWindow.cpp +++ b/viewer/JFJochViewerWindow.cpp @@ -400,6 +400,10 @@ JFJochViewerWindow::JFJochViewerWindow(QWidget *parent, bool dbus, const QString connect(viewer, &JFJochImage::frameRendered, magnifierWindow, [viewer, magnifierWindow] { magnifierWindow->setFrame(viewer->Frame()); }); + + // ... and the raw counts behind it, for the per-pixel labels. Stores a pointer, nothing more. + connect(this, &JFJochViewerWindow::imageReady, + magnifierWindow, &JFJochHelperWindow::imageLoaded); connect(viewer, &JFJochImage::hoverScenePos, magnifierWindow, &JFJochMagnifierWindow::centerAt); diff --git a/viewer/image_viewer/JFJochFollowerImage.cpp b/viewer/image_viewer/JFJochFollowerImage.cpp index 83d92126..95761db5 100644 --- a/viewer/image_viewer/JFJochFollowerImage.cpp +++ b/viewer/image_viewer/JFJochFollowerImage.cpp @@ -3,12 +3,26 @@ #include "JFJochFollowerImage.h" #include "JFJochImage.h" +#include "../../common/ColorScale.h" #include +#include #include +#include #include +// Same wording as the main view's per-pixel labels +static QString PixelValueText(int32_t value) { + if (value == GAP_PXL_VALUE) + return QStringLiteral("Gap"); + if (value == ERROR_PXL_VALUE) + return QStringLiteral("Err"); + if (value == SATURATED_PXL_VALUE) + return QStringLiteral("Sat"); + return QString::number(value); +} + JFJochFollowerImage::JFJochFollowerImage(QWidget *parent) : QGraphicsView(parent) { setScene(new QGraphicsScene(this)); setTransformationAnchor(QGraphicsView::AnchorViewCenter); @@ -43,6 +57,10 @@ void JFJochFollowerImage::SetFrame(std::shared_ptr frame) { viewport()->update(); } +void JFJochFollowerImage::SetPixelValues(std::shared_ptr image) { + values_ = std::move(image); +} + void JFJochFollowerImage::CenterAt(QPointF scenePos) { if (!frame_ || frame_->isNull()) return; @@ -58,3 +76,47 @@ void JFJochFollowerImage::wheelEvent(QWheelEvent *event) { setTransform(QTransform::fromScale(zoom_, zoom_)); centerOn(center); } + +void JFJochFollowerImage::drawForeground(QPainter *painter, const QRectF &rect) { + QGraphicsView::drawForeground(painter, rect); + + if (zoom_ < kLabelZoom || !values_ || !frame_ || frame_->isNull()) + return; + + const int W = frame_->width(); + const int H = frame_->height(); + const auto &pixels = values_->Image(); + if (static_cast(pixels.size()) < static_cast(W) * H) + return; // values belong to a different frame + + const QRectF visible = mapToScene(viewport()->rect()).boundingRect(); + const int x0 = std::max(0, static_cast(std::floor(visible.left()))); + const int x1 = std::min(W, static_cast(std::ceil(visible.right()))); + const int y0 = std::max(0, static_cast(std::floor(visible.top()))); + const int y1 = std::min(H, static_cast(std::ceil(visible.bottom()))); + if (x1 <= x0 || y1 <= y0 || (x1 - x0) * (y1 - y0) > kMaxLabels) + return; + + // Lay the text out in viewport pixels so it stays a constant, readable size + painter->save(); + painter->resetTransform(); + + QFont font("DejaVu Sans Mono"); + font.setStyleHint(QFont::TypeWriter); + font.setPixelSize(std::clamp(static_cast(zoom_ * 0.3), 7, 16)); + painter->setFont(font); + + for (int y = y0; y < y1; y++) { + for (int x = x0; x < x1; x++) { + const QRect cell = mapFromScene(QRectF(x, y, 1, 1)).boundingRect(); + const QRgb c = frame_->pixel(x, y); + const rgb col{.r = static_cast(qRed(c)), + .g = static_cast(qGreen(c)), + .b = static_cast(qBlue(c))}; + painter->setPen(luminance(col) > 128.0 ? Qt::black : Qt::white); + painter->drawText(cell, Qt::AlignCenter, PixelValueText(pixels[y * W + x])); + } + } + + painter->restore(); +} diff --git a/viewer/image_viewer/JFJochFollowerImage.h b/viewer/image_viewer/JFJochFollowerImage.h index 0c484f79..90bd5f5d 100644 --- a/viewer/image_viewer/JFJochFollowerImage.h +++ b/viewer/image_viewer/JFJochFollowerImage.h @@ -8,25 +8,37 @@ #include #include +#include "../../reader/JFJochReaderImage.h" + class JFJochImageItem; // A second view of a frame that a JFJochImage has already rendered: the same pixels, with its own // zoom and centre. Nothing is converted or coloured here and there is no second full-size buffer, // so following the main view costs a pointer assignment per frame rather than a whole render. // -// It shows only the image. Overlays, ROI tools and per-pixel value labels belong to the view that -// owns the data; a magnifier does not need them. +// Zoomed in far enough it writes the per-pixel counts over the image. Those are read straight from +// the detector's int32 buffer - the same source the main view colours from - so no float copy of +// the image is needed either. +// +// It draws no overlays and has no ROI tools: those belong to the view that owns the data. class JFJochFollowerImage : public QGraphicsView { Q_OBJECT + // Per-pixel counts are only legible once a detector pixel is a few tens of screen pixels + static constexpr double kLabelZoom = 30.0; + static constexpr int kMaxLabels = 2000; + JFJochImageItem *item_ = nullptr; std::shared_ptr frame_; + std::shared_ptr values_; double zoom_ = 12.0; void wheelEvent(QWheelEvent *event) override; + void drawForeground(QPainter *painter, const QRectF &rect) override; public: explicit JFJochFollowerImage(QWidget *parent = nullptr); void SetFrame(std::shared_ptr frame); + void SetPixelValues(std::shared_ptr image); void CenterAt(QPointF scenePos); }; diff --git a/viewer/windows/JFJochMagnifierWindow.cpp b/viewer/windows/JFJochMagnifierWindow.cpp index 52e6da7b..11c779eb 100644 --- a/viewer/windows/JFJochMagnifierWindow.cpp +++ b/viewer/windows/JFJochMagnifierWindow.cpp @@ -18,6 +18,10 @@ void JFJochMagnifierWindow::setFrame(std::shared_ptr frame) { m_image->SetFrame(std::move(frame)); } +void JFJochMagnifierWindow::imageLoaded(std::shared_ptr image) { + m_image->SetPixelValues(std::move(image)); +} + void JFJochMagnifierWindow::centerAt(QPointF scenePos) { if (!isVisible()) return; diff --git a/viewer/windows/JFJochMagnifierWindow.h b/viewer/windows/JFJochMagnifierWindow.h index 346be210..de01c7d6 100644 --- a/viewer/windows/JFJochMagnifierWindow.h +++ b/viewer/windows/JFJochMagnifierWindow.h @@ -23,6 +23,10 @@ class JFJochMagnifierWindow : public JFJochHelperWindow { public: explicit JFJochMagnifierWindow(QWidget *parent = nullptr); + // Raw counts for the per-pixel labels. This only stores the pointer - the pixels are + // displayed from the frame the main view rendered, nothing is converted here. + void imageLoaded(std::shared_ptr image) override; + public slots: void setFrame(std::shared_ptr frame); void centerAt(QPointF scenePos);