Viewer: label pixels from the int32 image, and paint them instead of building items

Two changes to the per-pixel value labels, which appear above 30x zoom.

They were up to 5000 QGraphicsSimpleTextItems created and destroyed on every
overlay rebuild - so on every pan step while zoomed in. Paint them in
drawForeground() instead: no item churn, no scene invalidation, and the text is
laid out in viewport pixels so it is a constant readable size rather than a
scene-space font scaled by 0.2. Same approach as the magnifier's labels.

The value text becomes a virtual, PixelLabel(). The base still formats from
image_fp, which is what the genuinely float-valued views hold (azimuthal
profile, grid-scan 1/sigma^2, the calibration viewer's eight source types).
JFJochDiffractionImage overrides it to read the int32 image directly: counts are
exact integers, so routing them through float32 is a detour that also cannot
represent summed values above 2^24 exactly.

Verified at 38 wheel clicks over a module edge: identical values and
gap/contrast handling to the previous float path, now centred in each pixel.
Fit-view 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:27:17 +02:00
co-authored by Claude Opus 5
parent 7a893bb1e7
commit 27615a8a1d
4 changed files with 81 additions and 72 deletions
@@ -1011,6 +1011,20 @@ QString JFJochDiffractionImage::HoverResolutionLabel() const {
return QString("d = %1 \u00C5").arg(QString::number(hover_resolution, 'f', 2));
}
QString JFJochDiffractionImage::PixelLabel(int x, int y) const {
if (!image)
return {};
const int32_t v = image->Image()[static_cast<size_t>(y) * W + x];
if (v == GAP_PXL_VALUE)
return QStringLiteral("Gap");
if (v == ERROR_PXL_VALUE)
return QStringLiteral("Err");
if (v == SATURATED_PXL_VALUE)
return QStringLiteral("Sat");
return QString::number(v);
}
void JFJochDiffractionImage::drawForeground(QPainter *painter, const QRectF &rect) {
JFJochImage::drawForeground(painter, rect);
@@ -26,6 +26,9 @@ Q_OBJECT
// currently sits, in viewport coordinates.
QRect hover_text_rect_;
[[nodiscard]] QString HoverResolutionLabel() const;
// Counts are exact integers: label them from the int32 image rather than from a float copy,
// which both avoids materialising that copy and cannot round large summed values.
[[nodiscard]] QString PixelLabel(int x, int y) const override;
void drawForeground(QPainter *painter, const QRectF &rect) override;
public:
enum class RingMode {Auto, Estimation, Manual, None, IceRings};
+57 -71
View File
@@ -817,86 +817,75 @@ void JFJochImage::applyViewport(QTransform transform, QPointF center) {
m_applyingViewport = false;
}
void JFJochImage::writePixelLabels() {
static QFont font([] {
QFont f("DejaVu Sans Mono");
f.setStyleHint(QFont::TypeWriter);
f.setPixelSize(1);
return f;
}());
static const QString kGap = QStringLiteral("Gap");
static const QString kErr = QStringLiteral("Err");
static const QString kSat = QStringLiteral("Sat");
QRectF visibleRect = mapToScene(viewport()->geometry()).boundingRect();
const int startX = std::max(0, static_cast<int>(std::floor(visibleRect.left())));
const int endX = std::min(static_cast<int>(W), static_cast<int>(std::ceil(visibleRect.right())));
const int startY = std::max(0, static_cast<int>(std::floor(visibleRect.top())));
const int endY = std::min(static_cast<int>(H), static_cast<int>(std::ceil(visibleRect.bottom())));
const int visW = std::max(0, endX - startX);
const int visH = std::max(0, endY - startY);
int maxLabels = 5000;
QString JFJochImage::PixelLabel(int x, int y) const {
// Choose thresholds that fit your UI width
constexpr float kMinFixed = 1e-3;
constexpr float kMaxFixed = 1e5;
if (visW * visH <= maxLabels) {
EnsurePixelValues();
const float val = image_fp[static_cast<size_t>(y) * W + x];
const auto absVal = std::abs(val);
const auto nearest = std::nearbyint(val);
QString numBuf; // reused buffer
if (std::isnan(val))
return QStringLiteral("Gap");
if (std::isinf(val))
return std::signbit(val) ? QStringLiteral("Err") : QStringLiteral("Sat");
if (val == 0.0f)
return QStringLiteral("0");
if (absVal >= kMinFixed && absVal < kMaxFixed) {
if (std::abs(val - nearest) < 1e-6)
return QString::number(static_cast<qint64>(val));
if (absVal < 1e4)
return QString::number(val, 'f', label_decimals_);
return QString::number(val, 'f', std::min(label_decimals_, 2));
}
return QString::number(val, 'e', 1);
}
for (int y = startY; y < endY; y ++) {
for (int x = startX; x < endX; x++) {
const int idx = y * W + x;
const float val = image_fp[idx];
void JFJochImage::drawPixelLabels(QPainter *painter) {
constexpr int kMaxLabels = 5000;
const auto absVal = std::abs(val);
const auto nearest = std::nearbyint(val);
const QRectF visibleRect = mapToScene(viewport()->rect()).boundingRect();
const int startX = std::max(0, static_cast<int>(std::floor(visibleRect.left())));
const int endX = std::min(static_cast<int>(W), static_cast<int>(std::ceil(visibleRect.right())));
const int startY = std::max(0, static_cast<int>(std::floor(visibleRect.top())));
const int endY = std::min(static_cast<int>(H), static_cast<int>(std::ceil(visibleRect.bottom())));
if (endX <= startX || endY <= startY)
return;
if ((endX - startX) * (endY - startY) > kMaxLabels)
return;
const QString* pText = nullptr;
if (std::isnan(val)) {
pText = &kGap;
} else if (std::isinf(val)) {
pText = std::signbit(val) ? &kErr : &kSat;
} else if (val == 0.0f) {
numBuf = QStringLiteral("0");
pText = &numBuf;
} else if (absVal >= kMinFixed && absVal < kMaxFixed) {
if (std::abs(val - nearest) < 1e-6)
numBuf = QString::number(static_cast<qint64>(val));
else if (absVal < 1e4)
numBuf = QString::number(val, 'f', label_decimals_);
else
numBuf = QString::number(val, 'f', std::min(label_decimals_, 2));
pText = &numBuf;
} else {
numBuf = QString::number(val, 'e', 1);
pText = &numBuf;
}
// Laid out in viewport pixels: a constant, readable size independent of the zoom
painter->save();
painter->resetTransform();
auto *textItem = new QGraphicsSimpleTextItem(*pText);
textItem->setFont(font);
// Read the colour back from the rendered image rather than keeping a
// full-size mirror of it around for the few pixels that get a label.
const QRgb pxl = frame_->pixel(x, y);
if (luminance(rgb{.r = static_cast<uint8_t>(qRed(pxl)),
.g = static_cast<uint8_t>(qGreen(pxl)),
.b = static_cast<uint8_t>(qBlue(pxl))}) > 128.0)
textItem->setBrush(Qt::black);
else
textItem->setBrush(Qt::white);
QFont font("DejaVu Sans Mono");
font.setStyleHint(QFont::TypeWriter);
font.setPixelSize(std::clamp(static_cast<int>(scale_factor * 0.3), 7, 16));
painter->setFont(font);
textItem->setPos(x + 0.3, y + 0.2);
textItem->setTransform(QTransform::fromScale(0.2, 0.2));
scene()->addItem(textItem);
addOverlayItem(textItem);
}
for (int y = startY; y < endY; y++) {
for (int x = startX; x < endX; x++) {
const QRect cell = mapFromScene(QRectF(x, y, 1, 1)).boundingRect();
// Read the colour back from the rendered image rather than keeping a full-size
// mirror of it around for the few pixels that get a label.
const QRgb pxl = frame_->pixel(x, y);
painter->setPen(luminance(rgb{.r = static_cast<uint8_t>(qRed(pxl)),
.g = static_cast<uint8_t>(qGreen(pxl)),
.b = static_cast<uint8_t>(qBlue(pxl))}) > 128.0
? Qt::black : Qt::white);
painter->drawText(cell, Qt::AlignCenter, PixelLabel(x, y));
}
}
painter->restore();
}
void JFJochImage::drawForeground(QPainter *painter, const QRectF &rect) {
QGraphicsView::drawForeground(painter, rect);
if (scale_factor > 30.0 && W * H > 0 && frame_ && !frame_->isNull())
drawPixelLabels(painter);
}
void JFJochImage::resetScenePointers() {
@@ -928,9 +917,6 @@ void JFJochImage::updateOverlay() {
image_dirty_ = false;
}
if (scale_factor > 30.0)
writePixelLabels();
DrawROI();
addCustomOverlay();
+7 -1
View File
@@ -82,7 +82,7 @@ class JFJochImage : public QGraphicsView {
void DrawROI();
virtual void addCustomOverlay();
void updateROI();
void writePixelLabels();
void drawPixelLabels(QPainter *painter);
void wheelEvent(QWheelEvent* event) override;
void resizeEvent(QResizeEvent *event) override;
void contextMenuEvent(QContextMenuEvent *event) override;
@@ -156,6 +156,12 @@ protected:
QPointF roiEndPos;
QRectF roiBox;
// Text for the per-pixel value label at (x, y). The base reads image_fp, which is what the
// float-valued views hold; a view whose source is exact integers overrides this so the label
// is exact and no float copy of the image has to exist at all.
[[nodiscard]] virtual QString PixelLabel(int x, int y) const;
void drawForeground(QPainter *painter, const QRectF &rect) override;
static QPointF RoundPoint(const QPointF& p);
virtual void mouseHover(const QPointF &scenePos, Qt::KeyboardModifiers modifiers) = 0;