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) <noreply@anthropic.com>
This commit is contained in:
2026-07-26 21:18:20 +02:00
co-authored by Claude Opus 5
parent d2ce65f857
commit 7a893bb1e7
5 changed files with 88 additions and 2 deletions
+4
View File
@@ -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);
@@ -3,12 +3,26 @@
#include "JFJochFollowerImage.h"
#include "JFJochImage.h"
#include "../../common/ColorScale.h"
#include <algorithm>
#include <cmath>
#include <QGraphicsScene>
#include <QPainter>
#include <QWheelEvent>
// 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<const QImage> frame) {
viewport()->update();
}
void JFJochFollowerImage::SetPixelValues(std::shared_ptr<const JFJochReaderImage> 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<int64_t>(pixels.size()) < static_cast<int64_t>(W) * H)
return; // values belong to a different frame
const QRectF visible = mapToScene(viewport()->rect()).boundingRect();
const int x0 = std::max(0, static_cast<int>(std::floor(visible.left())));
const int x1 = std::min(W, static_cast<int>(std::ceil(visible.right())));
const int y0 = std::max(0, static_cast<int>(std::floor(visible.top())));
const int y1 = std::min(H, static_cast<int>(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<int>(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<uint8_t>(qRed(c)),
.g = static_cast<uint8_t>(qGreen(c)),
.b = static_cast<uint8_t>(qBlue(c))};
painter->setPen(luminance(col) > 128.0 ? Qt::black : Qt::white);
painter->drawText(cell, Qt::AlignCenter, PixelValueText(pixels[y * W + x]));
}
}
painter->restore();
}
+14 -2
View File
@@ -8,25 +8,37 @@
#include <QGraphicsView>
#include <QImage>
#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<const QImage> frame_;
std::shared_ptr<const JFJochReaderImage> 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<const QImage> frame);
void SetPixelValues(std::shared_ptr<const JFJochReaderImage> image);
void CenterAt(QPointF scenePos);
};
+4
View File
@@ -18,6 +18,10 @@ void JFJochMagnifierWindow::setFrame(std::shared_ptr<const QImage> frame) {
m_image->SetFrame(std::move(frame));
}
void JFJochMagnifierWindow::imageLoaded(std::shared_ptr<const JFJochReaderImage> image) {
m_image->SetPixelValues(std::move(image));
}
void JFJochMagnifierWindow::centerAt(QPointF scenePos) {
if (!isVisible())
return;
+4
View File
@@ -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<const JFJochReaderImage> image) override;
public slots:
void setFrame(std::shared_ptr<const QImage> frame);
void centerAt(QPointF scenePos);