diff --git a/viewer/image_viewer/JFJochAzIntImage.cpp b/viewer/image_viewer/JFJochAzIntImage.cpp index a9905f24..7ccf3ce4 100644 --- a/viewer/image_viewer/JFJochAzIntImage.cpp +++ b/viewer/image_viewer/JFJochAzIntImage.cpp @@ -23,7 +23,6 @@ void JFJochAzIntImage::Clear() { void JFJochAzIntImage::imageLoaded(std::shared_ptr in_image) { if (!in_image) { Clear(); - CalcROI(); return; } @@ -71,7 +70,6 @@ void JFJochAzIntImage::imageLoaded(std::shared_ptr in_i // Render the image and redraw using base class functionality RenderImage(); Redraw(); - CalcROI(); } else { Clear(); } diff --git a/viewer/image_viewer/JFJochDiffractionImage.cpp b/viewer/image_viewer/JFJochDiffractionImage.cpp index 7c12ab74..45b6d56e 100644 --- a/viewer/image_viewer/JFJochDiffractionImage.cpp +++ b/viewer/image_viewer/JFJochDiffractionImage.cpp @@ -127,7 +127,6 @@ void JFJochDiffractionImage::LoadImageInternal() { W = image->Dataset().experiment.GetXPixelsNum(); H = image->Dataset().experiment.GetYPixelsNum(); - pixel_values_valid_ = false; // image_fp is filled on demand, see EnsurePixelValues } void JFJochDiffractionImage::ColorRow(size_t y, const PixelColorMap &map, QRgb *out) const { @@ -150,34 +149,6 @@ void JFJochDiffractionImage::ColorRow(size_t y, const PixelColorMap &map, QRgb * } } -void JFJochDiffractionImage::EnsurePixelValues() { - if (pixel_values_valid_ || !image) - return; - - const auto &img = image->Image(); - image_fp.resize(W*H); - - QVector rows; - rows.reserve(H); - for (int y = 0; y < H; ++y) rows.push_back(y); - - // Fill the float image with pixel data from the array - QtConcurrent::blockingMap(rows, [&](int y) { - for (size_t pxl = y * W; pxl < (y + 1) * W; pxl++) { - auto val = img[pxl]; - if (val == GAP_PXL_VALUE) - image_fp[pxl] = NAN; - else if (val == ERROR_PXL_VALUE) - image_fp[pxl] = -INFINITY; - else if (val == SATURATED_PXL_VALUE) - image_fp[pxl] = INFINITY; - else - image_fp[pxl] = static_cast(val); - } - }); - - pixel_values_valid_ = true; -} void JFJochDiffractionImage::DrawSpots() { // Compute current visible area in scene coordinates @@ -1011,6 +982,89 @@ QString JFJochDiffractionImage::HoverResolutionLabel() const { return QString("d = %1 \u00C5").arg(QString::number(hover_resolution, 'f', 2)); } +ROIMessage JFJochDiffractionImage::AccumulateROI( + int64_t xmin, int64_t xmax, int64_t ymin, int64_t ymax, + const std::function &inside) const { + 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); + + const auto &pixels = image->Image(); + + for (int64_t y = ymin; y < ymax; ++y) { + for (int64_t x = xmin; x < xmax; ++x) { + if (!inside(x, y)) continue; + + const int32_t val = pixels[x + W * y]; + + if (val == SATURATED_PXL_VALUE || val == ERROR_PXL_VALUE) { + roi_npixel_masked++; + } else if (val != GAP_PXL_VALUE) { + x_weighted += static_cast(val) * x; + y_weighted += static_cast(val) * y; + roi_val += val; + roi_val_2 += static_cast(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 JFJochDiffractionImage::CalcROI() { + if (!image || 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 + const int64_t xmin = box_norm.left(); + const int64_t xmax = box_norm.right(); + const int64_t ymin = box_norm.top(); + const 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 { + const QPointF delta = roiStartPos - roiEndPos; + const float cx = static_cast(roiStartPos.x()); + const float cy = static_cast(roiStartPos.y()); + const float r2 = static_cast(delta.x() * delta.x() + delta.y() * delta.y()); + 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; + return dx * dx + dy * dy <= r2; + }); + } + emit roiCalculated(msg); +} + QString JFJochDiffractionImage::PixelLabel(int x, int y) const { if (!image) return {}; diff --git a/viewer/image_viewer/JFJochDiffractionImage.h b/viewer/image_viewer/JFJochDiffractionImage.h index 3ba3884e..4d717001 100644 --- a/viewer/image_viewer/JFJochDiffractionImage.h +++ b/viewer/image_viewer/JFJochDiffractionImage.h @@ -3,6 +3,8 @@ #pragma once +#include + #include #include "JFJochImage.h" @@ -29,6 +31,12 @@ Q_OBJECT // Counts are exact integers: label them from the int32 image rather than from a float copy, // which both avoids materialising that copy and cannot round large summed values. [[nodiscard]] QString PixelLabel(int x, int y) const override; + + // This is the view that has detector counts, so it is the one that offers an ROI + [[nodiscard]] bool AllowROI() const override { return true; } + void CalcROI() override; + [[nodiscard]] ROIMessage AccumulateROI(int64_t xmin, int64_t xmax, int64_t ymin, int64_t ymax, + const std::function &inside) const; void drawForeground(QPainter *painter, const QRectF &rect) override; public: enum class RingMode {Auto, Estimation, Manual, None, IceRings}; @@ -39,11 +47,9 @@ private: void addCustomOverlay() override; void LoadImageInternal(); - // Colour straight from the int32 detector image; image_fp is only materialised when the - // base class actually needs pixel values (ROI statistics, per-pixel labels). + // Colour straight from the int32 detector image; no float copy of it is ever built void ColorRow(size_t y, const PixelColorMap &map, QRgb *out) const override; - void EnsurePixelValues() override; - bool pixel_values_valid_ = false; + void DrawResolutionRings(); void DrawROIs(); void DrawAzimuthalROI(const ROIAzimuthal &az, const QColor &color, const DiffractionGeometry &geom); @@ -113,6 +119,7 @@ private: void mouseHover(const QPointF &scenePos, Qt::KeyboardModifiers modifiers) override; signals: + void roiCalculated(ROIMessage &output); void roiGeometryEdited(ROIDefinition rois); void roiSelected(QString name); // user picked an ROI by clicking it on the image public slots: diff --git a/viewer/image_viewer/JFJochGridScanImage.cpp b/viewer/image_viewer/JFJochGridScanImage.cpp index 7bb761fd..c4bfc1bd 100644 --- a/viewer/image_viewer/JFJochGridScanImage.cpp +++ b/viewer/image_viewer/JFJochGridScanImage.cpp @@ -14,7 +14,6 @@ void JFJochGridScanImage::clear() { if (scene()) scene()->clear(); resetScenePointers(); - CalcROI(); } void JFJochGridScanImage::loadData(const std::vector &data, const GridScanSettings &settings, bool in_one_over_d2) { @@ -60,7 +59,6 @@ void JFJochGridScanImage::loadData(const std::vector &data, const GridSca RenderImage(); Redraw(); - CalcROI(); } void JFJochGridScanImage::mouseHover(const QPointF &pt, Qt::KeyboardModifiers modifiers) { diff --git a/viewer/image_viewer/JFJochImage.cpp b/viewer/image_viewer/JFJochImage.cpp index 3ed3b9ed..47dc7a04 100644 --- a/viewer/image_viewer/JFJochImage.cpp +++ b/viewer/image_viewer/JFJochImage.cpp @@ -238,18 +238,20 @@ void JFJochImage::mousePressEvent(QMouseEvent *event) { return; } - active_handle_ = hitTestROIHandle(scenePos, 4.0 / std::sqrt(std::max(1e-4, scale_factor))); + active_handle_ = AllowROI() + ? hitTestROIHandle(scenePos, 4.0 / std::sqrt(std::max(1e-4, scale_factor))) + : ResizeHandle::None; 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)) { + } else if (AllowROI() && roiBox.contains(scenePos)) { mouse_event_type = MouseEventType::MovingROI; lastMousePos = event->pos(); setCursor(Qt::ClosedHandCursor); - } else if (event->modifiers() & Qt::Modifier::SHIFT) { + } else if (AllowROI() && (event->modifiers() & Qt::Modifier::SHIFT)) { mouse_event_type = MouseEventType::DrawingROI; roiStartPos = RoundPoint(scenePos); roiEndPos = roiStartPos; @@ -333,6 +335,8 @@ void JFJochImage::mouseMoveEvent(QMouseEvent *event) { break; } case MouseEventType::None: { + if (!AllowROI()) + break; 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 @@ -400,7 +404,7 @@ void JFJochImage::contextMenuEvent(QContextMenuEvent *event) { 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")); + QAction *clearRoiAct = AllowROI() ? menu.addAction(tr("Clear ROI")) : nullptr; const bool hasImage = (W > 0 && H > 0 && !frame_->isNull()); copyImageAct->setEnabled(hasImage); @@ -421,7 +425,7 @@ void JFJochImage::contextMenuEvent(QContextMenuEvent *event) { saveImageToFile(true); } else if (chosen == fitAct) { fitToView(); - } else if (chosen == clearRoiAct) { + } else if (clearRoiAct && chosen == clearRoiAct) { clearROIInternal(); } } @@ -607,6 +611,8 @@ void JFJochImage::addOverlayItem(QGraphicsItem *item) { } void JFJochImage::DrawROI() { + if (!AllowROI()) + return; if (roiBox.isNull() || roiBox.width() <= 0 || roiBox.height() <= 0) return; auto scn = scene(); @@ -924,99 +930,7 @@ void JFJochImage::updateOverlay() { 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(); - - // accumulateROI only reads pixel values inside the box, so an empty ROI needs none - if (box_norm.width() > 0 && box_norm.height() > 0) - EnsurePixelValues(); - - // 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; diff --git a/viewer/image_viewer/JFJochImage.h b/viewer/image_viewer/JFJochImage.h index acc6ea09..ef83e2f6 100644 --- a/viewer/image_viewer/JFJochImage.h +++ b/viewer/image_viewer/JFJochImage.h @@ -91,9 +91,10 @@ class JFJochImage : public QGraphicsView { void mouseMoveEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; - ROIMessage accumulateROI(int64_t xmin, int64_t xmax, int64_t ymin, int64_t ymax, - const std::function& inside); protected: + // Only the view that owns detector counts offers a region of interest; for the others a + // shift-drag would draw a box that means nothing. + [[nodiscard]] virtual bool AllowROI() const { return false; } virtual void beforeOverlayCleared(); bool show_saturation = false; @@ -184,6 +185,9 @@ protected: ResizeHandle hitTestROIHandle(const QPointF& scenePos, qreal tol = 3.0) const; + // Statistics over roiBox, for the view that has pixel values to accumulate + virtual void CalcROI() {} + void updateOverlay(); void RenderImage(); PixelColorMap MakeColorMap() const; @@ -191,9 +195,6 @@ protected: // maps image_fp; a subclass whose source is already a compact buffer can map that directly // and skip materialising the float image. virtual void ColorRow(size_t y, const PixelColorMap &map, QRgb *out) const; - // Fill image_fp, which the base class reads for ROI statistics and per-pixel value labels. - // Subclasses that colour without it fill it on demand here rather than on every frame. - virtual void EnsurePixelValues() {} // Re-render once the event queue drains. The foreground slider and the wheel emit far // faster than a large image can be recoloured, so intermediate values are dropped // instead of queueing a full recolour per event. @@ -205,7 +206,6 @@ protected: // once per scrollbar; the gesture rebuilds it once itself. bool suppress_overlay_update_ = false; void Redraw(); - void CalcROI(); // Invalidate image_item_ and overlay tracking after scene()->clear() void resetScenePointers(); @@ -228,7 +228,6 @@ signals: void writeStatusBar(QString string, int timeout_ms = 0); void roiBoxUpdated(QRect box); void roiCircleUpdated(double x, double y, double radius); - void roiCalculated(ROIMessage &output); void viewportChanged(QTransform transform, QPointF center); void hoverScenePos(QPointF scenePos); // A new frame has been rendered into Frame(). Follower views repaint on this. diff --git a/viewer/image_viewer/JFJochSimpleImage.cpp b/viewer/image_viewer/JFJochSimpleImage.cpp index 5e561cfa..2b899f9a 100644 --- a/viewer/image_viewer/JFJochSimpleImage.cpp +++ b/viewer/image_viewer/JFJochSimpleImage.cpp @@ -30,14 +30,12 @@ void JFJochSimpleImage::setImage(std::shared_ptr img) { loadImageInternal(); RenderImage(); Redraw(); - CalcROI(); } else { image_.reset(); W = 0; H = 0; if (scene()) scene()->clear(); resetScenePointers(); - CalcROI(); } } diff --git a/viewer/windows/JFJoch2DAzintImageWindow.cpp b/viewer/windows/JFJoch2DAzintImageWindow.cpp index 062328a1..287fafc4 100644 --- a/viewer/windows/JFJoch2DAzintImageWindow.cpp +++ b/viewer/windows/JFJoch2DAzintImageWindow.cpp @@ -3,7 +3,6 @@ #include #include "JFJoch2DAzintImageWindow.h" -#include "../widgets/JFJochViewerROIResult.h" JFJoch2DAzintImageWindow::JFJoch2DAzintImageWindow(QWidget *parent) : JFJochHelperWindow(parent) { QWidget *centralWidget = new QWidget(this); @@ -25,12 +24,9 @@ JFJoch2DAzintImageWindow::JFJoch2DAzintImageWindow(QWidget *parent) : JFJochHelp foreground_row->addWidget(new QLabel("Foreground:")); foreground_row->addWidget(foreground_slider); - auto roi_result = new JFJochViewerROIResult(this); - grid_layout->addLayout(background_row, 0, 0, 1, 2); grid_layout->addLayout(foreground_row, 1, 0, 1, 2); grid_layout->addWidget(viewer, 2, 0, 1, 2); - grid_layout->addWidget(roi_result, 3, 0, 1, 2); centralWidget->setLayout(grid_layout); connect(viewer, &JFJochAzIntImage::backgroundChanged, @@ -45,8 +41,6 @@ JFJoch2DAzintImageWindow::JFJoch2DAzintImageWindow(QWidget *parent) : JFJochHelp foreground_slider->setValue(val); }); - connect(viewer, &JFJochAzIntImage::roiCalculated, roi_result, &JFJochViewerROIResult::SetROIResult); - connect(background_slider, &SliderPlusBox::valueChanged, viewer, &JFJochAzIntImage::changeBackground); connect(foreground_slider, &SliderPlusBox::valueChanged, viewer, &JFJochAzIntImage::changeForeground); diff --git a/viewer/windows/JFJochCalibrationWindow.cpp b/viewer/windows/JFJochCalibrationWindow.cpp index 05ffc281..2033f45d 100644 --- a/viewer/windows/JFJochCalibrationWindow.cpp +++ b/viewer/windows/JFJochCalibrationWindow.cpp @@ -7,7 +7,6 @@ #include #include -#include "../widgets/JFJochViewerROIResult.h" JFJochCalibrationWindow::JFJochCalibrationWindow(QWidget *parent) : JFJochHelperWindow(parent) { QWidget *centralWidget = new QWidget(this); @@ -37,14 +36,11 @@ JFJochCalibrationWindow::JFJochCalibrationWindow(QWidget *parent) : JFJochHelper foreground_row->addWidget(new QLabel("Foreground:")); foreground_row->addWidget(foreground_slider); - auto roi_result = new JFJochViewerROIResult(this); - grid_layout->addWidget(calibration_option, 0, 0); grid_layout->addWidget(color_map_select, 0, 1); grid_layout->addLayout(background_row, 1, 0, 1, 2); grid_layout->addLayout(foreground_row, 2, 0, 1, 2); grid_layout->addWidget(viewer, 3, 0, 1, 2); - grid_layout->addWidget(roi_result, 4, 0, 1, 2); connect(viewer, &JFJochSimpleImage::backgroundChanged, [this] (float val) { @@ -57,8 +53,6 @@ JFJochCalibrationWindow::JFJochCalibrationWindow(QWidget *parent) : JFJochHelper QSignalBlocker blocker(foreground_slider); foreground_slider->setValue(val); }); - connect(viewer, &JFJochSimpleImage::roiCalculated, roi_result, &JFJochViewerROIResult::SetROIResult); - connect(background_slider, &SliderPlusBox::valueChanged, viewer, &JFJochSimpleImage::changeBackground); connect(foreground_slider, &SliderPlusBox::valueChanged, viewer, &JFJochSimpleImage::changeForeground);