Files
Jungfraujoch/viewer/image_viewer/JFJochImage.cpp
Filip Leonarski 07fe4dd3bb
All checks were successful
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 11m23s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 10m32s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 9m15s
Build Packages / Generate python client (push) Successful in 19s
Build Packages / Build documentation (push) Successful in 49s
Build Packages / Create release (push) Has been skipped
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 9m13s
Build Packages / build:rpm (rocky8) (push) Successful in 9m10s
Build Packages / build:rpm (rocky9) (push) Successful in 9m58s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 8m52s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 8m42s
Build Packages / Unit tests (push) Successful in 1h12m44s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 11m30s
v1.0.0-rc.124 (#31)
This is an UNSTABLE release. This version significantly rewrites code to predict reflection position and integrate them,
especially in case of rotation crystallography. If things go wrong with analysis, it is better to revert to 1.0.0-rc.123.

* jfjoch_broker: Improve refection position prediction and Bragg integration code.
* jfjoch_broker: Align with XDS way of calculating Lorentz correction and general notation.
* jfjoch_writer: Fix saving mosaicity properly in HDF5 file.
* jfjoch_viewer: Introduce high-dynamic range mode for images
* jfjoch_viewer: Ctrl+mouse wheel has exponential change in foreground (+/-15%)
* jfjoch_viewer: Zoom-in numbers have better readability

Reviewed-on: #31
Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
Co-committed-by: Filip Leonarski <filip.leonarski@psi.ch>
2026-02-01 13:29:33 +01:00

903 lines
32 KiB
C++

// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include "JFJochImage.h"
#include <QGraphicsPixmapItem>
#include <QScrollBar>
#include <QWheelEvent>
#include <QMouseEvent>
#include <QTimer>
#include <QMenu>
#include <QContextMenuEvent>
#include <QClipboard>
#include <QGuiApplication>
#include <QMimeData>
#include <QBuffer>
#include <QElapsedTimer>
#include <QPainter>
#include <QtConcurrent/QtConcurrent>
JFJochImage::JFJochImage(QWidget *parent) : QGraphicsView(parent) {
setDragMode(QGraphicsView::NoDrag); // Disable default drag mode
setTransformationAnchor(QGraphicsView::AnchorUnderMouse); // Zoom anchors
setRenderHint(QPainter::Antialiasing); // Enable smooth rendering
setRenderHint(QPainter::SmoothPixmapTransform);
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
setFocusPolicy(Qt::ClickFocus);
// Connect the horizontal scrollbar's valueChanged signal
connect(horizontalScrollBar(), &QScrollBar::valueChanged, this, &JFJochImage::onScroll);
// Connect the vertical scrollbar's valueChanged signal
connect(verticalScrollBar(), &QScrollBar::valueChanged, this, &JFJochImage::onScroll);
// Optional: a sensible default colormap
color_scale.Select(ColorScaleEnum::Indigo);
}
void JFJochImage::onScroll(int value) {
updateOverlay();
}
void JFJochImage::changeBackground(float val) {
background = val;
GeneratePixmap();
Redraw();
}
void JFJochImage::changeForeground(float val) {
auto_fg = false;
emit autoForegroundChanged(false);
foreground = val;
// Regenerate the image
GeneratePixmap();
Redraw();
}
void JFJochImage::setColorMap(int color_map) {
try {
color_scale.Select(static_cast<ColorScaleEnum>(color_map));
// Regenerate the image
GeneratePixmap();
Redraw();
} catch (...) {
}
}
void JFJochImage::setFeatureColor(QColor input) {
feature_color = input;
GeneratePixmap();
Redraw();
}
void JFJochImage::wheelEvent(QWheelEvent *event) {
if (!scene()) return;
const double zoomFactor = 1.15; // Zoom factor
// Get the position of the mouse in scene coordinates
QPointF targetScenePos = mapToScene(event->position().toPoint());
const bool exp_fg_adjust = (event->modifiers() & Qt::ControlModifier);
const bool lin_fg_adjust = (event->modifiers() & Qt::ShiftModifier) || m_adjustForegroundWithWheel;
if (exp_fg_adjust || lin_fg_adjust) {
float new_foreground = foreground;
if (exp_fg_adjust) {
const float step = (event->angleDelta().y() > 0) ? zoomFactor : (1.0 / zoomFactor);
new_foreground = foreground * step;
} else {
new_foreground = foreground + event->angleDelta().y() / 120.0f;
}
if (new_foreground < 1.0f)
new_foreground = 1.0f;
changeForeground(new_foreground);
emit foregroundChanged(foreground);
} else {
// Perform zooming
if (event->angleDelta().y() > 0) {
if (scale_factor * zoomFactor < 500.0) {
scale_factor *= zoomFactor;
scale(zoomFactor, zoomFactor);
}
} else {
if (scale_factor > 0.2) {
scale_factor *= 1.0 / zoomFactor;
scale(1.0 / zoomFactor, 1.0 / zoomFactor);
}
}
// Adjust the view's center to keep the zoom focused on the mouse position
QPointF updatedViewportCenter = mapToScene(viewport()->rect().center());
QPointF delta = targetScenePos - updatedViewportCenter;
translate(delta.x(), delta.y()); // Shift the view
updateOverlay();
}
}
void JFJochImage::resizeEvent(QResizeEvent *event) {
QGraphicsView::resizeEvent(event);
updateOverlay();
}
QPointF JFJochImage::RoundPoint(const QPointF &input) {
return QPointF(qRound(input.x()), qRound(input.y()));
}
void JFJochImage::SetROIBox(QRect box) {
roi_type = RoiType::RoiBox;
roiBox= box;
roiStartPos = roiBox.topLeft();
roiEndPos = roiBox.bottomRight();
Redraw();
}
void JFJochImage::SetROICircle(double x, double y, double radius) {
roi_type = RoiType::RoiCircle;
roiBox= QRectF(x - radius, y - radius, 2 * radius, 2 * radius).normalized();
roiStartPos = roiBox.topLeft();
roiEndPos = roiBox.bottomRight();
Redraw();
}
void JFJochImage::mousePressEvent(QMouseEvent *event) {
if (!scene()) return;
if (event->button() == Qt::LeftButton) {
const QPointF scenePos = mapToScene(event->pos());
active_handle_ = hitTestROIHandle(scenePos, 4.0 / std::sqrt(std::max(1e-4, scale_factor)));
if (active_handle_ != ResizeHandle::None && active_handle_ != ResizeHandle::Inside) {
mouse_event_type = MouseEventType::ResizingROI;
roiStartPos = roiBox.topLeft();
roiEndPos = roiBox.bottomRight();
setCursor(Qt::SizeAllCursor);
} else if (roiBox.contains(scenePos)) {
mouse_event_type = MouseEventType::MovingROI;
lastMousePos = event->pos();
setCursor(Qt::ClosedHandCursor);
} else if (event->modifiers() & Qt::Modifier::SHIFT) {
mouse_event_type = MouseEventType::DrawingROI;
roiStartPos = RoundPoint(scenePos);
roiEndPos = roiStartPos;
roi_type = (event->modifiers() & Qt::Modifier::CTRL) ? RoiType::RoiCircle : RoiType::RoiBox;
setCursor(Qt::CrossCursor);
} else {
mouse_event_type = MouseEventType::Panning;
setCursor(Qt::ClosedHandCursor);
lastMousePos = event->pos();
}
}
QGraphicsView::mousePressEvent(event);
}
void JFJochImage::mouseMoveEvent(QMouseEvent *event) {
if (!scene())
return;
const QPointF scenePos = mapToScene(event->pos());
mouseHover(event);
QPointF delta;
switch (mouse_event_type) {
case MouseEventType::Panning:
delta = mapToScene(event->pos()) - mapToScene(lastMousePos);
lastMousePos = event->pos();
translate(delta.x(), delta.y());
updateOverlay();
break;
case MouseEventType::DrawingROI:
roiEndPos = RoundPoint(scenePos);
updateROI();
break;
case MouseEventType::MovingROI:
delta = mapToScene(event->pos()) - mapToScene(lastMousePos);
lastMousePos = event->pos();
roiBox.translate(delta);
updateROI();
break;
case MouseEventType::ResizingROI: {
// Modify the corresponding edges based on active_handle_
if (roi_type == RoiType::RoiCircle) {
// Resize circle by radius only, keep center fixed
const QPointF c = roiBox.center();
const qreal dx = scenePos.x() - c.x();
const qreal dy = scenePos.y() - c.y();
qreal r = std::hypot(dx, dy);
const qreal rMin = 1.0; // clamp tiny radii
if (r < rMin) r = rMin;
roiBox = QRectF(c.x() - r, c.y() - r, 2*r, 2*r);
} else {
// Box: modify edges based on active handle
QRectF r = roiBox;
switch (active_handle_) {
case ResizeHandle::Left: r.setLeft(scenePos.x()); break;
case ResizeHandle::Right: r.setRight(scenePos.x()); break;
case ResizeHandle::Top: r.setTop(scenePos.y()); break;
case ResizeHandle::Bottom: r.setBottom(scenePos.y()); break;
case ResizeHandle::TopLeft: r.setTop(scenePos.y()); r.setLeft(scenePos.x()); break;
case ResizeHandle::TopRight: r.setTop(scenePos.y()); r.setRight(scenePos.x()); break;
case ResizeHandle::BottomLeft: r.setBottom(scenePos.y()); r.setLeft(scenePos.x()); break;
case ResizeHandle::BottomRight:r.setBottom(scenePos.y()); r.setRight(scenePos.x()); break;
default: break;
}
roiBox = r.normalized();
}
updateROI();
break;
}
case MouseEventType::None: {
const qreal tol = 4.0 / std::sqrt(std::max(1e-4, scale_factor));
ResizeHandle h = hitTestROIHandle(scenePos, tol);
// Update hover state so overlay can draw arrows/handles accordingly
if (h != hover_handle_) {
hover_handle_ = h;
updateOverlay();
}
// Set an informative cursor
switch (h) {
case ResizeHandle::Left:
case ResizeHandle::Right:
setCursor(Qt::SizeHorCursor); break;
case ResizeHandle::Top:
case ResizeHandle::Bottom:
setCursor(Qt::SizeVerCursor); break;
case ResizeHandle::TopLeft:
case ResizeHandle::BottomRight:
setCursor(Qt::SizeFDiagCursor); break;
case ResizeHandle::TopRight:
case ResizeHandle::BottomLeft:
setCursor(Qt::SizeBDiagCursor); break;
case ResizeHandle::Inside:
setCursor(Qt::OpenHandCursor); break;
case ResizeHandle::None:
setCursor(Qt::ArrowCursor); break;
}
break;
}
}
QGraphicsView::mouseMoveEvent(event);
}
void JFJochImage::mouseReleaseEvent(QMouseEvent *event) {
if (!scene()) return;
if (event->button() == Qt::LeftButton) {
if (mouse_event_type == MouseEventType::DrawingROI) {
roiEndPos = RoundPoint(mapToScene(event->pos()));
}
updateROI();
}
mouse_event_type = MouseEventType::None;
active_handle_ = ResizeHandle::None;
setCursor(Qt::ArrowCursor);
QGraphicsView::mouseReleaseEvent(event);
}
void JFJochImage::contextMenuEvent(QContextMenuEvent *event) {
QMenu menu(this);
QAction *copyImageAct = menu.addAction(tr("Copy image"));
QAction *copyWithOverlayAct = menu.addAction(tr("Copy image with overlay"));
menu.addSeparator();
QAction *fitAct = menu.addAction(tr("Fit image to view"));
QAction *clearRoiAct = menu.addAction(tr("Clear ROI"));
const bool hasImage = (W > 0 && H > 0 && !pixmap.isNull());
copyImageAct->setEnabled(hasImage);
copyWithOverlayAct->setEnabled(hasImage && scene());
QAction *chosen = menu.exec(event->globalPos());
if (!chosen) return;
if (chosen == copyImageAct) {
copyImageToClipboard();
} else if (chosen == copyWithOverlayAct) {
copyImageWithOverlayToClipboard();
} else if (chosen == fitAct) {
fitToView();
} else if (chosen == clearRoiAct) {
clearROIInternal();
}
}
static void setClipboardAsJpegAndImage(const QImage &img, int quality = 95) {
// Provide both "image/jpeg" and generic image flavors for better compatibility
QByteArray ba;
ba.reserve(img.width() * img.height() * 3 / 2);
QBuffer buf(&ba);
buf.open(QIODevice::WriteOnly);
QImage toSave = img;
// Force 1:1 pixel ratio and standard DPI (96) to avoid scaling in consumer apps
toSave.setDevicePixelRatio(1.0);
constexpr int dotsPerMeter96DPI = 3780; // 96 DPI
toSave.setDotsPerMeterX(dotsPerMeter96DPI);
toSave.setDotsPerMeterY(dotsPerMeter96DPI);
toSave = toSave.convertToFormat(QImage::Format_ARGB32); // ensure a known format for encoding
toSave.save(&buf, "JPEG", quality);
auto *mime = new QMimeData();
mime->setData("image/jpeg", ba);
mime->setImageData(toSave); // also set as generic bitmap
QGuiApplication::clipboard()->setMimeData(mime);
}
void JFJochImage::copyImageToClipboard() {
if (W == 0 || H == 0 || pixmap.isNull()) return;
// Use the underlying rendered image (no overlay)
QImage img = pixmap.toImage();
// Ensure 1:1 pixel ratio and 96 DPI metadata
img.setDevicePixelRatio(1.0);
constexpr int dotsPerMeter96DPI = 3780;
img.setDotsPerMeterX(dotsPerMeter96DPI);
img.setDotsPerMeterY(dotsPerMeter96DPI);
setClipboardAsJpegAndImage(img, 95);
emit writeStatusBar(tr("Image copied to clipboard"), 2000);
}
void JFJochImage::copyImageWithOverlayToClipboard() {
if (W == 0 || H == 0 || !scene()) return;
// Render the entire scene (image + overlay) at native image resolution
QImage img(int(W), int(H), QImage::Format_ARGB32_Premultiplied);
img.fill(Qt::transparent);
// Ensure 1:1 and 96 DPI before rendering to avoid metadata-based rescaling
img.setDevicePixelRatio(1.0);
constexpr int dotsPerMeter96DPI = 3780;
img.setDotsPerMeterX(dotsPerMeter96DPI);
img.setDotsPerMeterY(dotsPerMeter96DPI);
QPainter p(&img);
// Ensure we render the same logical rect as the scene/image coordinates
const QRectF sourceRect(0, 0, qreal(W), qreal(H));
scene()->render(&p, QRectF(0, 0, qreal(W), qreal(H)), sourceRect);
p.end();
setClipboardAsJpegAndImage(img, 95);
emit writeStatusBar(tr("Image with overlay copied to clipboard"), 2000);
}
void JFJochImage::clearROIInternal() {
roiBox = QRectF(); // clear any ROI
// Keep current roi_type; ROI simply becomes empty
CalcROI(); // will emit a zeroed ROI message
updateOverlay();
emit writeStatusBar(tr("ROI cleared"), 1500);
}
JFJochImage::ResizeHandle
JFJochImage::hitTestROIHandle(const QPointF& scenePos, qreal tol) const {
if (roiBox.isNull() || roiBox.width() <= 0 || roiBox.height() <= 0)
return ResizeHandle::None;
const QRectF r = roiBox;
if (roi_type == RoiType::RoiCircle) {
// Circle hit test: near perimeter -> resize, inside -> move
const QPointF c = r.center();
const qreal rx = r.width() * 0.5;
const qreal ry = r.height() * 0.5;
// Enforce circular assumption: use average radius
const qreal rad = 0.5 * (rx + ry);
const qreal dx = scenePos.x() - c.x();
const qreal dy = scenePos.y() - c.y();
const qreal d = std::hypot(dx, dy);
if (std::abs(d - rad) <= tol) {
// generic "edge" resize handle for circle
return ResizeHandle::Right;
}
if (d < rad) return ResizeHandle::Inside;
return ResizeHandle::None;
}
// Box hit test (corners first)
const QPointF tl = r.topLeft();
const QPointF tr = r.topRight();
const QPointF bl = r.bottomLeft();
const QPointF br = r.bottomRight();
auto nearPt = [&](const QPointF& a, const QPointF& b, qreal t) {
return std::abs(a.x() - b.x()) <= t && std::abs(a.y() - b.y()) <= t;
};
if (nearPt(scenePos, tl, tol)) return ResizeHandle::TopLeft;
if (nearPt(scenePos, tr, tol)) return ResizeHandle::TopRight;
if (nearPt(scenePos, bl, tol)) return ResizeHandle::BottomLeft;
if (nearPt(scenePos, br, tol)) return ResizeHandle::BottomRight;
// Edges
if (std::abs(scenePos.x() - r.left()) <= tol && scenePos.y() >= r.top() - tol && scenePos.y() <= r.bottom() + tol)
return ResizeHandle::Left;
if (std::abs(scenePos.x() - r.right()) <= tol && scenePos.y() >= r.top() - tol && scenePos.y() <= r.bottom() + tol)
return ResizeHandle::Right;
if (std::abs(scenePos.y() - r.top()) <= tol && scenePos.x() >= r.left() - tol && scenePos.x() <= r.right() + tol)
return ResizeHandle::Top;
if (std::abs(scenePos.y() - r.bottom()) <= tol && scenePos.x() >= r.left() - tol && scenePos.x() <= r.right() + tol)
return ResizeHandle::Bottom;
if (r.contains(scenePos)) return ResizeHandle::Inside;
return ResizeHandle::None;
}
void JFJochImage::updateROI() {
if (roi_type == RoiType::RoiBox) {
if (mouse_event_type == MouseEventType::DrawingROI) {
// While drawing: construct box from start/end
QRectF rect = QRectF(RoundPoint(roiStartPos), RoundPoint(roiEndPos)).normalized();
roiBox = rect;
} else {
// While moving/resizing: keep roiBox as modified, just sync corners
roiStartPos = roiBox.topLeft();
roiEndPos = roiBox.bottomRight();
}
emit roiBoxUpdated(roiBox.toRect());
} else {
double radius;
if (mouse_event_type == MouseEventType::DrawingROI) {
// Center at roiStartPos, radius from start->end
QPointF delta = roiStartPos - roiEndPos;
radius = std::sqrt(delta.x() * delta.x() + delta.y() * delta.y());
roiBox = QRectF(roiStartPos.x() - radius, roiStartPos.y() - radius,
2 * radius, 2 * radius).normalized();
} else {
// Moving/resizing: infer center/radius from roiBox
const QPointF c = roiBox.center();
radius = 0.5 * std::min(roiBox.width(), roiBox.height());
roiStartPos = c; // treat start as center for consistency
roiEndPos = QPointF(c.x() + radius, c.y()); // arbitrary point on radius
}
emit roiCircleUpdated(roiStartPos.x(), roiStartPos.y(), radius);
}
CalcROI();
updateOverlay();
}
void JFJochImage::DrawROI() {
if (roiBox.isNull() || roiBox.width() <= 0 || roiBox.height() <= 0) return;
auto scn = scene();
if (!scn)
return;
QPen pen(feature_color, 2);
pen.setStyle(Qt::DashLine);
pen.setCosmetic(true);
const qreal f = std::clamp(scale_factor, 0.5, 50.0);
const qreal handleSize = 3.0 / std::sqrt(std::max(1e-4, f));
if (roi_type == RoiType::RoiCircle) {
// Draw circle
scn->addEllipse(roiBox, pen);
// A single handle on the circle at the rightmost point
const QPointF c = roiBox.center();
const qreal rad = 0.5 * (roiBox.width() + roiBox.height()) * 0.5; // average, should be equal
QPointF hpos = QPointF(roiBox.right(), c.y());
scn->addRect(QRectF(hpos.x() - handleSize, hpos.y() - handleSize, 2 * handleSize, 2 * handleSize),
QPen(feature_color, 1), QBrush(feature_color));
// On hover near perimeter: draw in/out arrows along radius at handle
if (hover_handle_ != ResizeHandle::None && hover_handle_ != ResizeHandle::Inside) {
QPen apen(feature_color, 1);
apen.setCosmetic(true);
const qreal arrowLen = 8.0 / std::sqrt(std::max(1e-4, f));
// Outward arrow
scn->addLine(QLineF(c, c + QPointF(rad + arrowLen, 0)), apen);
// Inward arrow
scn->addLine(QLineF(c, c + QPointF(rad - arrowLen, 0)), apen);
}
} else {
// Box
scn->addRect(roiBox, pen);
// Corner handles
auto addHandle = [&](const QPointF& p) {
scn->addRect(QRectF(p.x() - handleSize, p.y() - handleSize, 2 * handleSize, 2 * handleSize),
QPen(feature_color, 1), QBrush(feature_color));
};
addHandle(roiBox.topLeft());
addHandle(roiBox.topRight());
addHandle(roiBox.bottomLeft());
addHandle(roiBox.bottomRight());
// On hover over a resizable edge/corner: draw small arrows indicating resize direction
if (hover_handle_ != ResizeHandle::None && hover_handle_ != ResizeHandle::Inside) {
QPen apen(feature_color, 1);
apen.setCosmetic(true);
const qreal arrowLen = 6.0 / std::sqrt(std::max(1e-4, f));
const qreal off = 10.0 / std::sqrt(std::max(1e-4, f));
auto drawArrow = [&](const QPointF& a, const QPointF& b) {
scn->addLine(QLineF(a, b), apen);
};
const QRectF r = roiBox;
switch (hover_handle_) {
case ResizeHandle::Left:
drawArrow(QPointF(r.left(), r.center().y() - off), QPointF(r.left() - arrowLen, r.center().y() - off));
drawArrow(QPointF(r.left(), r.center().y() + off), QPointF(r.left() - arrowLen, r.center().y() + off));
break;
case ResizeHandle::Right:
drawArrow(QPointF(r.right(), r.center().y() - off), QPointF(r.right() + arrowLen, r.center().y() - off));
drawArrow(QPointF(r.right(), r.center().y() + off), QPointF(r.right() + arrowLen, r.center().y() + off));
break;
case ResizeHandle::Top:
drawArrow(QPointF(r.center().x() - off, r.top()), QPointF(r.center().x() - off, r.top() - arrowLen));
drawArrow(QPointF(r.center().x() + off, r.top()), QPointF(r.center().x() + off, r.top() - arrowLen));
break;
case ResizeHandle::Bottom:
drawArrow(QPointF(r.center().x() - off, r.bottom()), QPointF(r.center().x() - off, r.bottom() + arrowLen));
drawArrow(QPointF(r.center().x() + off, r.bottom()), QPointF(r.center().x() + off, r.bottom() + arrowLen));
break;
case ResizeHandle::TopLeft:
case ResizeHandle::TopRight:
case ResizeHandle::BottomLeft:
case ResizeHandle::BottomRight:
// For corners, show arrows on both axes (simple version)
drawArrow(QPointF(r.right(), r.center().y()), QPointF(r.right() + arrowLen, r.center().y()));
drawArrow(QPointF(r.left(), r.center().y()), QPointF(r.left() - arrowLen, r.center().y()));
drawArrow(QPointF(r.center().x(), r.top()), QPointF(r.center().x(), r.top() - arrowLen));
drawArrow(QPointF(r.center().x(), r.bottom()), QPointF(r.center().x(), r.bottom() + arrowLen));
break;
default: break;
}
}
}
}
void JFJochImage::Redraw() {
if (W*H <= 0)
return;
// Save the current transformation (zoom state)
QTransform currentTransform = this->transform();
QGraphicsScene *currentScene = scene();
if (!currentScene) {
// First time - create a new scene
currentScene = new QGraphicsScene(this);
setScene(currentScene);
// Reset initial-fit state for a new scene
initial_fit_done_ = false;
}
// Restore the zoom level
this->setTransform(currentTransform, false); // "false" prevents resetting the view
// Perform initial fit only once per image size
fitToViewShorterSideOnce();
updateOverlay();
}
void JFJochImage::GeneratePixmap() {
QImage qimg(int(W), int(H), QImage::Format_RGB32);
image_rgb.resize(W * H);
// Bad pixel color
int r, g, b, a;
feature_color.getRgb(&r, &g, &b, &a);
auto bad_color = rgb{.r = static_cast<uint8_t>(r), .g = static_cast<uint8_t>(g), .b = static_cast<uint8_t>(b)};
// Saturation color
rgb sat_color{};
if (show_saturation) {
sat_color = bad_color;
} else
sat_color = color_scale.Apply(1.0f);
// Precompute once
const float minv = background;
const float maxv = foreground;
const auto &lut_data = color_scale.LUTData();
const int64_t lutSize = lut_data.size();
const float lutScale = static_cast<float>(lutSize - 1);
const float range = maxv - minv;
const float invRange = (range > 0) ? (lutScale / range) : 0.0f;
const float invRangeLog = (range > 0) ? (lutScale / std::log1p(range)) : 0.0f;
rgb gap_color = color_scale.Apply(ColorScaleSpecial::Gap);
QVector<int> rows;
rows.reserve(H);
for (int y = 0; y < H; ++y) rows.push_back(y);
QtConcurrent::blockingMap(rows, [&](int y) {
QRgb *scanLine = reinterpret_cast<QRgb*>(qimg.scanLine(y));
const float *row = &image_fp[y * W];
rgb *out = &image_rgb[y * W];
for (int x = 0; x < W; ++x) {
const float fp = row[x];
rgb c;
if (!std::isfinite(fp)) {
if (std::isnan(fp)) {
c = gap_color;
} else {
c = std::signbit(fp) ? bad_color : sat_color;
}
} else {
float f;
const float fp_minv = fp - minv;
if (hdr_mode) {
if (fp_minv <= 0.0f)
f = 0.0f;
else if (fp_minv >= range)
f = lutSize;
else
f = std::log1p(fp_minv) * invRangeLog;
} else
f = fp_minv * invRange;
if (f < 0.0f) f = 0.0f;
auto idx = static_cast<int>(f + 0.5f);
if (idx <= 0) idx = 0;
else if (idx >= lutSize) idx = lutSize - 1;
c = lut_data[idx];
}
out[x] = c;
scanLine[x] = qRgb(c.r, c.g, c.b);
}
});
pixmap = QPixmap::fromImage(qimg);
pixmap.setDevicePixelRatio(1.0);
}
void JFJochImage::centerOnSpot(QPointF point) {
// If W or H = 0, then conditions are never satisfied
if (point.x() >= 0 && point.x() < W && point.y() >= 0 && point.y() < H)
centerOn(point);
}
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 = 1000;
// Choose thresholds that fit your UI width
constexpr float kMinFixed = 1e-3;
constexpr float kMaxFixed = 1e5;
if (visW * visH <= maxLabels) {
QString numBuf; // reused buffer
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];
const auto absVal = std::abs(val);
const auto nearest = std::nearbyint(val);
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', 3);
else
numBuf = QString::number(val, 'f', 2);
pText = &numBuf;
} else {
numBuf = QString::number(val, 'e', 1);
pText = &numBuf;
}
QGraphicsTextItem* textItem = scene()->addText(*pText, font);
if (luminance(image_rgb[idx]) > 128.0)
textItem->setDefaultTextColor(Qt::black);
else
textItem->setDefaultTextColor(Qt::white);
textItem->setPos(x - 0.7, y - 0.8);
textItem->setScale(0.2);
}
}
}
}
void JFJochImage::updateOverlay() {
if (!scene() || W * H <= 0) return;
beforeOverlayCleared();
scene()->clear();
scene()->addItem(new QGraphicsPixmapItem(pixmap));
if (scale_factor > 30.0)
writePixelLabels();
DrawROI();
addCustomOverlay();
}
void JFJochImage::addCustomOverlay() {}
ROIMessage JFJochImage::accumulateROI(
int64_t xmin, int64_t xmax,
int64_t ymin, int64_t ymax,
const std::function<bool(int64_t, int64_t)> &inside) {
int64_t roi_val = 0;
uint64_t roi_val_2 = 0;
int64_t roi_max = INT64_MIN;
uint64_t roi_npixel = 0;
uint64_t roi_npixel_masked = 0;
float x_weighted = 0.0f;
float y_weighted = 0.0f;
// Clamp bounds defensively to the image
xmin = std::max<int64_t>(0, xmin);
ymin = std::max<int64_t>(0, ymin);
xmax = std::min<int64_t>(W, xmax);
ymax = std::min<int64_t>(H, ymax);
for (int64_t y = ymin; y < ymax; ++y) {
for (int64_t x = xmin; x < xmax; ++x) {
if (!inside(x, y)) continue;
float val = image_fp[x + W * y];
if (std::isinf(val)) {
roi_npixel_masked++;
} else if (std::isfinite(val)) {
x_weighted += val * x;
y_weighted += val * y;
roi_val += val;
roi_val_2 += val * val;
if (val > roi_max) roi_max = val;
roi_npixel++;
}
}
}
return ROIMessage{
.sum = roi_val,
.sum_square = roi_val_2,
.max_count = roi_max,
.pixels = roi_npixel,
.pixels_masked = roi_npixel_masked,
.x_weighted = std::lroundf(x_weighted),
.y_weighted = std::lroundf(y_weighted),
};
}
void JFJochImage::CalcROI() {
if (W*H == 0) {
auto msg = ROIMessage{
.pixels = 0,
.pixels_masked = 0};
emit roiCalculated(msg);
return;
}
auto box_norm = roiBox.normalized();
// Using the rectangle as-is; you can adjust inclusivity if needed
int64_t xmin = box_norm.left();
int64_t xmax = box_norm.right();
int64_t ymin = box_norm.top();
int64_t ymax = box_norm.bottom();
ROIMessage msg{};
if (roi_type == RoiType::RoiBox)
msg = accumulateROI(
xmin, xmax, ymin, ymax,
[](int64_t, int64_t) { return true; } // everything in the rectangle
);
else {
QPointF delta = roiStartPos - roiEndPos;
double radius2 = delta.x() * delta.x() + delta.y() * delta.y();
const float cx = static_cast<float>(roiStartPos.x());
const float cy = static_cast<float>(roiStartPos.y());
const float r2 = static_cast<float>(radius2);
msg = accumulateROI(
xmin, xmax, ymin, ymax,
[cx, cy, r2](int64_t x, int64_t y) {
const float dx = static_cast<float>(x) - cx;
const float dy = static_cast<float>(y) - cy;
const float dist2 = dx * dx + dy * dy;
return dist2 <= r2;
}
);
}
emit roiCalculated(msg);
}
void JFJochImage::fitToView() {
initial_fit_done_ = false;
Redraw();
}
void JFJochImage::fitToViewShorterSideOnce() {
if (initial_fit_done_ && prev_H == H && prev_W == W)
return;
if (W == 0 || H == 0 || !viewport())
return;
prev_H = H;
prev_W = W;
// Guard against tiny or zero viewport (happens before layout settles)
const QSize vp = viewport()->size();
if (vp.width() < 8 || vp.height() < 8) {
// remember last tried size; resizeEvent/showEvent will retry
last_fit_viewport_ = vp;
return;
}
if (scene())
scene()->setSceneRect(QRectF(0, 0, static_cast<qreal>(W), static_cast<qreal>(H)));
const auto oldAnchor = transformationAnchor();
setTransformationAnchor(QGraphicsView::AnchorViewCenter);
setTransform(QTransform());
fitInView(QRectF(0, 0, static_cast<qreal>(W), static_cast<qreal>(H)), Qt::KeepAspectRatio);
scale_factor = transform().m11();
centerOn(QPointF(static_cast<qreal>(W) * 0.5, static_cast<qreal>(H) * 0.5));
setTransformationAnchor(oldAnchor);
initial_fit_done_ = true;
last_fit_viewport_ = vp;
}
void JFJochImage::adjustForeground(bool input) {
m_adjustForegroundWithWheel = input;
}
void JFJochImage::beforeOverlayCleared() {}