835 lines
30 KiB
C++
835 lines
30 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 <QPainter>
|
|
|
|
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) {
|
|
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 adjustForeground =
|
|
(event->modifiers() == Qt::ShiftModifier) || m_adjustForegroundWithWheel;
|
|
|
|
if (adjustForeground) {
|
|
float new_foreground = foreground + event->angleDelta().y() / 120.0f;
|
|
if (new_foreground < 1)
|
|
new_foreground = 1.0;
|
|
foreground = new_foreground;
|
|
emit foregroundChanged(foreground);
|
|
|
|
GeneratePixmap();
|
|
Redraw();
|
|
} 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_RGB888);
|
|
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.0);
|
|
|
|
for (int y = 0; y < H; ++y) {
|
|
uchar *scanLine = qimg.scanLine(y); // Get writable pointer to the row
|
|
for (int x = 0; x < W; ++x) {
|
|
float fp = image_fp[y * W + x];
|
|
if (std::isnan(fp))
|
|
image_rgb[y * W + x] = color_scale.Apply(ColorScaleSpecial::Gap);
|
|
else if (std::isinf(fp)) {
|
|
if (std::signbit(fp))
|
|
image_rgb[y * W + x] = bad_color;
|
|
else
|
|
image_rgb[y * W + x] = sat_color;
|
|
} else
|
|
image_rgb[y * W + x] = color_scale.Apply(image_fp[y * W + x], background, foreground);
|
|
scanLine[x * 3 + 0] = image_rgb[y * W + x].r; // Red
|
|
scanLine[x * 3 + 1] = image_rgb[y * W + x].g; // Green
|
|
scanLine[x * 3 + 2] = image_rgb[y * W + x].b; // Blue
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
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 QString* pText = nullptr;
|
|
if (std::isnan(val)) {
|
|
pText = &kGap;
|
|
} else if (std::isinf(val)) {
|
|
pText = std::signbit(val) ? &kErr : &kSat;
|
|
} else {
|
|
// Fixed format reduces overhead and string length variability
|
|
numBuf = QString::number(val, 'g', 4);
|
|
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() {}
|