// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute // SPDX-License-Identifier: GPL-3.0-only #include "JFJochImage.h" #include #include #include #include #include #include #include #include #include #include #include #include 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(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()); if (event->modifiers() == Qt::ShiftModifier) { 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(r), .g = static_cast(g), .b = static_cast(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(std::floor(visibleRect.left()))); const int endX = std::min(static_cast(W), static_cast(std::ceil(visibleRect.right()))); const int startY = std::max(0, static_cast(std::floor(visibleRect.top()))); const int endY = std::min(static_cast(H), static_cast(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; 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 &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(0, xmin); ymin = std::max(0, ymin); xmax = std::min(W, xmax); ymax = std::min(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(roiStartPos.x()); const float cy = static_cast(roiStartPos.y()); const float r2 = static_cast(radius2); msg = accumulateROI( xmin, xmax, ymin, ymax, [cx, cy, r2](int64_t x, int64_t y) { const float dx = static_cast(x) - cx; const float dy = static_cast(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(W), static_cast(H))); const auto oldAnchor = transformationAnchor(); setTransformationAnchor(QGraphicsView::AnchorViewCenter); setTransform(QTransform()); fitInView(QRectF(0, 0, static_cast(W), static_cast(H)), Qt::KeepAspectRatio); scale_factor = transform().m11(); centerOn(QPointF(static_cast(W) * 0.5, static_cast(H) * 0.5)); setTransformationAnchor(oldAnchor); initial_fit_done_ = true; last_fit_viewport_ = vp; }