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>
123 lines
4.2 KiB
C++
123 lines
4.2 KiB
C++
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#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);
|
|
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
|
setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
|
setTransform(QTransform::fromScale(zoom_, zoom_));
|
|
}
|
|
|
|
void JFJochFollowerImage::SetFrame(std::shared_ptr<const QImage> frame) {
|
|
const bool same_pointer = (frame_ == frame);
|
|
const bool same_size = frame_ && frame && frame_->size() == frame->size();
|
|
frame_ = std::move(frame);
|
|
|
|
if (!frame_ || frame_->isNull()) {
|
|
scene()->clear();
|
|
item_ = nullptr;
|
|
return;
|
|
}
|
|
|
|
// The producer reuses one buffer, so normally only the pixels changed and the item stays.
|
|
if (!item_ || !same_pointer) {
|
|
scene()->clear();
|
|
item_ = new JFJochImageItem(frame_);
|
|
scene()->addItem(item_);
|
|
}
|
|
|
|
if (!same_size) {
|
|
item_->refresh();
|
|
scene()->setSceneRect(0, 0, frame_->width(), frame_->height());
|
|
}
|
|
|
|
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;
|
|
centerOn(scenePos);
|
|
}
|
|
|
|
void JFJochFollowerImage::wheelEvent(QWheelEvent *event) {
|
|
constexpr double step = 1.15;
|
|
zoom_ *= (event->angleDelta().y() > 0) ? step : 1.0 / step;
|
|
zoom_ = std::clamp(zoom_, 1.0, 200.0);
|
|
|
|
const QPointF center = mapToScene(viewport()->rect().center());
|
|
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();
|
|
}
|