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:
@@ -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};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user