Build Packages / Unit tests (push) Successful in 1h28m28s
Build Packages / build:windows:nocuda (push) Successful in 14m45s
Build Packages / build:windows:cuda (push) Successful in 13m13s
Build Packages / build:viewer-tgz:cpu (push) Successful in 6m47s
Build Packages / build:viewer-tgz:cuda (push) Successful in 7m22s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 13m52s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 14m16s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 13m19s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 12m50s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 14m40s
Build Packages / build:rpm (rocky8) (push) Successful in 11m18s
Build Packages / build:rpm (rocky9) (push) Successful in 12m4s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 11m55s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 11m22s
Build Packages / DIALS test (push) Successful in 13m37s
Build Packages / XDS test (durin plugin) (push) Successful in 8m47s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 9m4s
Build Packages / XDS test (neggia plugin) (push) Successful in 7m45s
Build Packages / Generate python client (push) Successful in 34s
Build Packages / Build documentation (push) Successful in 1m4s
Build Packages / Create release (push) Skipped
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 7m16s
This is an UNSTABLE release. It includes many experimental features, as well as many AI generated fixes. We recommend using rc.152 for production use. * rugnux: Rebrand the offline data-processing subsystem as `rugnux` and consolidate all offline analysis into the single `rugnux` binary - `jfjoch_process` is now `rugnux`, the former `jfjoch_azint` is now `rugnux --azint-only`, and `jfjoch_scale` is now `rugnux --scale` (see the new docs/NAMING.md and docs/RUGNUX.md). Scaling and merging are on by default for rotation and stills (`--no-merge` disables them), replacing the previous opt-in `-M, --scale-merge`. * rugnux: CLI fixes - default `-N` to all hardware threads, parse numeric option arguments strictly (reject non-numeric or trailing input instead of silently yielding 0), require `--wavelength > 0`, and correct the reproduced command line and `--scale` reference-cell handling. * rugnux: De-novo space-group improvements - recover genuine high symmetry and centred Bravais lattices from intensities, add an automatic CC1/2 high-resolution cutoff, and report L-test twinning statistics. * rugnux: Index weakly-diffracting low-resolution rotation data that previously failed (e.g. F-cubic crystals that diffract only to ~4 A on a detector reaching ~1.5 A). The per-frame indexing gate now measures the indexed fraction only within the resolution range the lattice actually diffracts to, so the many sub-diffraction ice/noise spots no longer make the fraction floor unreachable; the two-pass first pass tries several image-sampling schemes (spread across the whole rotation vs a consecutive wedge whose native stride keeps a reflection's rocking curve continuous, letting the FFT resolve a long axis) and keeps the one that indexes the most frames; and the de-novo space-group search no longer discards all reflections (and crashes) when every resolution shell falls below <I/sigma> = 1. * rugnux: Lower the low-resolution R-meas for strongly-diffracting rotation data - drop edge-of-sweep truncated fulls whose rocking curve was captured below `--min-captured-fraction` (default 0.7 for rotation), and report R-meas only over the observations kept by outlier rejection (matching XDS). The 0.7 default also strips the partiality-extrapolated fulls that dominate the intensity second moment on weakly-diffracting crystals, so the de-novo space-group search is no longer starved by the error-model I/sigma floor and recovers the correct symmetry (e.g. the F-cubic Benas crystals: Benas_3 -> F432, Benas_7 -> P6122, instead of P4/P1); on the reference battery every other crystal keeps its space group. * rugnux: Write the refined geometry (beam, tilt, axis) to _process.h5 and place non-standard mmCIF items under a reserved `jfjoch` prefix. * jfjoch_broker: Ordinary acquisition failures (receiver/writer/analysis problems, missed packets, writer disconnect) now return to the Idle state with an Error-severity message, so a run can be retried without an expensive re-initialisation; only failures that leave the detector in an undefined state (new JFJochCriticalException, e.g. PCIe/FPGA faults) go to the Error state and force re-initialisation. * jfjoch_broker: A synchronous /start now reports its failure to the HTTP caller instead of returning HTTP 200, and an incomplete or truncated dataset (missing packets, writer disconnect) is reported as an error rather than a "reduce frame rate" warning. * jfjoch_broker: Drop uncollected placeholder rows (number = -1) from the scan_result REST endpoint. * jfjoch_broker: Fix the inverted per-image compression ratio reported by the Lite receiver (was compressed/uncompressed instead of uncompressed/compressed). * jfjoch_broker: Bragg integration adds a quantization-noise variance floor with a box-sum fallback, and treats the type-maximum marker as an invalid pixel for unsigned image types. * jfjoch_writer: Detect file-overwrite conflicts at start for back-channel transports, and reset the writer when end-of-collection finalisation fails. * jfjoch_viewer: Preview overlays follow the geometry (resolution/ROI arcs, true beam centre, predictions, coral secondary-lattice spots, legend), add save-as-JPEG, and fix an HTTP live-follow memory leak. * Frontend: Improved aesthetics and usability, and added in-browser pixel-mask and JUNGFRAU-pedestal visualisation. * CI: Name the Windows installer jfjoch-viewer-* instead of jfjoch-*.Reviewed-on: #67 Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
1010 lines
36 KiB
C++
1010 lines
36 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 <QGraphicsSimpleTextItem>
|
|
#include <QScrollBar>
|
|
#include <QWheelEvent>
|
|
#include <QMouseEvent>
|
|
#include <QTimer>
|
|
#include <QMenu>
|
|
#include <QContextMenuEvent>
|
|
#include <QClipboard>
|
|
#include <QGuiApplication>
|
|
#include <QMimeData>
|
|
#include <QBuffer>
|
|
#include <QFileDialog>
|
|
#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();
|
|
emitViewportChanged();
|
|
}
|
|
}
|
|
|
|
void JFJochImage::resizeEvent(QResizeEvent *event) {
|
|
QGraphicsView::resizeEvent(event);
|
|
|
|
if (scene())
|
|
scene()->setSceneRect(QRectF(0, 0, static_cast<qreal>(W), static_cast<qreal>(H)));
|
|
|
|
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());
|
|
|
|
if (roiEditPress(scenePos)) {
|
|
mouse_event_type = MouseEventType::EditingExternalROI;
|
|
event->accept();
|
|
return;
|
|
}
|
|
|
|
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);
|
|
emit hoverScenePos(scenePos);
|
|
QPointF delta;
|
|
|
|
switch (mouse_event_type) {
|
|
case MouseEventType::EditingExternalROI:
|
|
roiEditMove(scenePos);
|
|
return;
|
|
case MouseEventType::Panning: {
|
|
const QPoint viewDelta = event->pos() - lastMousePos;
|
|
lastMousePos = event->pos();
|
|
|
|
horizontalScrollBar()->setValue(horizontalScrollBar()->value() - viewDelta.x());
|
|
verticalScrollBar()->setValue(verticalScrollBar()->value() - viewDelta.y());
|
|
|
|
updateOverlay();
|
|
emitViewportChanged();
|
|
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::EditingExternalROI) {
|
|
roiEditRelease();
|
|
} else {
|
|
const bool drawn = (mouse_event_type == MouseEventType::DrawingROI);
|
|
if (drawn)
|
|
roiEndPos = RoundPoint(mapToScene(event->pos()));
|
|
updateROI();
|
|
if (drawn)
|
|
roiScratchDrawn(); // turn the drawn scratch box/circle into a persistent ROI
|
|
}
|
|
}
|
|
|
|
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 *saveImageAct = menu.addAction(tr("Save image as JPEG..."));
|
|
QAction *saveWithOverlayAct = menu.addAction(tr("Save image with overlay as JPEG..."));
|
|
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());
|
|
saveImageAct->setEnabled(hasImage);
|
|
saveWithOverlayAct->setEnabled(hasImage && scene());
|
|
|
|
QAction *chosen = menu.exec(event->globalPos());
|
|
if (!chosen) return;
|
|
|
|
if (chosen == copyImageAct) {
|
|
copyImageToClipboard();
|
|
} else if (chosen == copyWithOverlayAct) {
|
|
copyImageWithOverlayToClipboard();
|
|
} else if (chosen == saveImageAct) {
|
|
saveImageToFile(false);
|
|
} else if (chosen == saveWithOverlayAct) {
|
|
saveImageToFile(true);
|
|
} 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);
|
|
|
|
}
|
|
|
|
QImage JFJochImage::renderToImage(bool with_overlay) {
|
|
QImage img;
|
|
if (with_overlay && scene()) {
|
|
// Render the entire scene (image + overlay) at native image resolution
|
|
img = QImage(int(W), int(H), QImage::Format_ARGB32_Premultiplied);
|
|
img.fill(Qt::transparent);
|
|
QPainter p(&img);
|
|
const QRectF rect(0, 0, qreal(W), qreal(H));
|
|
scene()->render(&p, rect, rect);
|
|
p.end();
|
|
} else {
|
|
// The underlying rendered image (no overlay)
|
|
img = pixmap.toImage();
|
|
}
|
|
// Ensure 1:1 pixel ratio and 96 DPI metadata to avoid rescaling in consumer apps
|
|
img.setDevicePixelRatio(1.0);
|
|
constexpr int dotsPerMeter96DPI = 3780;
|
|
img.setDotsPerMeterX(dotsPerMeter96DPI);
|
|
img.setDotsPerMeterY(dotsPerMeter96DPI);
|
|
return img;
|
|
}
|
|
|
|
void JFJochImage::copyImageToClipboard() {
|
|
if (W == 0 || H == 0 || pixmap.isNull()) return;
|
|
|
|
setClipboardAsJpegAndImage(renderToImage(false), 95);
|
|
emit writeStatusBar(tr("Image copied to clipboard"), 2000);
|
|
}
|
|
|
|
void JFJochImage::copyImageWithOverlayToClipboard() {
|
|
if (W == 0 || H == 0 || !scene()) return;
|
|
|
|
setClipboardAsJpegAndImage(renderToImage(true), 95);
|
|
emit writeStatusBar(tr("Image with overlay copied to clipboard"), 2000);
|
|
}
|
|
|
|
void JFJochImage::saveImageToFile(bool with_overlay) {
|
|
if (W == 0 || H == 0 || pixmap.isNull()) return;
|
|
if (with_overlay && !scene()) return;
|
|
|
|
const QString caption = with_overlay ? tr("Save image with overlay as JPEG")
|
|
: tr("Save image as JPEG");
|
|
QString file_name = QFileDialog::getSaveFileName(this, caption, QString(),
|
|
tr("JPEG image (*.jpg *.jpeg)"));
|
|
if (file_name.isEmpty())
|
|
return;
|
|
if (!file_name.endsWith(".jpg", Qt::CaseInsensitive) && !file_name.endsWith(".jpeg", Qt::CaseInsensitive))
|
|
file_name += ".jpg";
|
|
|
|
// JPEG cannot store alpha; flatten to RGB before encoding.
|
|
const QImage img = renderToImage(with_overlay).convertToFormat(QImage::Format_RGB32);
|
|
if (img.save(file_name, "JPEG", 95))
|
|
emit writeStatusBar(tr("Saved %1").arg(file_name), 3000);
|
|
else
|
|
emit writeStatusBar(tr("Failed to save %1").arg(file_name), 3000);
|
|
}
|
|
|
|
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::addOverlayItem(QGraphicsItem *item) {
|
|
overlay_items_.append(item);
|
|
}
|
|
|
|
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
|
|
addOverlayItem(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());
|
|
addOverlayItem(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
|
|
addOverlayItem(scn->addLine(QLineF(c, c + QPointF(rad + arrowLen, 0)), apen));
|
|
// Inward arrow
|
|
addOverlayItem(scn->addLine(QLineF(c, c + QPointF(rad - arrowLen, 0)), apen));
|
|
}
|
|
} else {
|
|
// Box
|
|
addOverlayItem(scn->addRect(roiBox, pen));
|
|
|
|
// Corner handles
|
|
auto addHandle = [&](const QPointF& p) {
|
|
addOverlayItem(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) {
|
|
addOverlayItem(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;
|
|
|
|
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;
|
|
pixmap_item_ = nullptr; // new scene, old pointer invalid
|
|
}
|
|
|
|
// 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);
|
|
emitViewportChanged();
|
|
}
|
|
|
|
void JFJochImage::emitViewportChanged() {
|
|
if (m_applyingViewport || !scene())
|
|
return;
|
|
emit viewportChanged(transform(), mapToScene(viewport()->rect().center()));
|
|
}
|
|
|
|
void JFJochImage::applyViewport(QTransform transform, QPointF center) {
|
|
if (m_applyingViewport || !scene())
|
|
return;
|
|
m_applyingViewport = true;
|
|
setTransform(transform);
|
|
scale_factor = transform.m11();
|
|
centerOn(center);
|
|
updateOverlay();
|
|
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;
|
|
|
|
// 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', label_decimals_);
|
|
else
|
|
numBuf = QString::number(val, 'f', std::min(label_decimals_, 2));
|
|
pText = &numBuf;
|
|
} else {
|
|
numBuf = QString::number(val, 'e', 1);
|
|
pText = &numBuf;
|
|
}
|
|
|
|
auto *textItem = new QGraphicsSimpleTextItem(*pText);
|
|
textItem->setFont(font);
|
|
if (luminance(image_rgb[idx]) > 128.0)
|
|
textItem->setBrush(Qt::black);
|
|
else
|
|
textItem->setBrush(Qt::white);
|
|
|
|
textItem->setPos(x + 0.3, y + 0.2);
|
|
textItem->setTransform(QTransform::fromScale(0.2, 0.2));
|
|
scene()->addItem(textItem);
|
|
addOverlayItem(textItem);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void JFJochImage::resetScenePointers() {
|
|
pixmap_item_ = nullptr;
|
|
overlay_items_.clear();
|
|
}
|
|
|
|
void JFJochImage::updateOverlay() {
|
|
if (!scene() || W * H <= 0) return;
|
|
|
|
beforeOverlayCleared();
|
|
|
|
// Remove only overlay items, keep the pixmap item persistent
|
|
for (auto *item : overlay_items_)
|
|
scene()->removeItem(item);
|
|
qDeleteAll(overlay_items_);
|
|
overlay_items_.clear();
|
|
|
|
// Ensure pixmap item exists and is up-to-date
|
|
if (!pixmap_item_) {
|
|
pixmap_item_ = scene()->addPixmap(pixmap);
|
|
pixmap_item_->setZValue(0);
|
|
} else {
|
|
pixmap_item_->setPixmap(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() {}
|
|
|
|
double JFJochImage::GetScaleFactor() const {
|
|
return scale_factor;
|
|
}
|
|
|
|
void JFJochImage::setZoom(double input) {
|
|
if (std::isfinite(input) && input > 0) {
|
|
scale_factor = input;
|
|
if (!scene())
|
|
return;
|
|
setTransform(QTransform::fromScale(input, input));
|
|
updateOverlay();
|
|
emitViewportChanged();
|
|
}
|
|
}
|