Viewer: the region of interest belongs to the diffraction view alone

Drawing an ROI only means something where there are detector counts to
accumulate. Gate the gesture on a virtual AllowROI(), true only for
JFJochDiffractionImage: shift-drag, the resize handles, the hover cursor and the
"Clear ROI" context entry now do nothing in the azimuthal, grid-scan and
calibration views, which cannot report anything about a box anyway.

The statistics move out of the base class into the diffraction view and read the
int32 image directly, so no float copy of the detector image is built for them
either. With the labels already converted, image_fp is now untouched by the
diffraction view, and the lazy EnsurePixelValues machinery it needed is gone.
image_fp stays as the base's representation for the views whose data really is
float: the azimuthal profile, the grid-scan 1/sigma^2 map, and the calibration
viewer's eight source types.

Removed with it: the ROI readouts in the calibration and 2D azimuthal windows,
which were the only two consumers of roiCalculated -- the diffraction view
emitted it and nothing listened. Nothing surfaces ROI statistics now; the
pixel-mask case wants rectangles counting excluded pixels and deserves its own
design. JFJochViewerROIResult is still used by the side-panel ROI list, so the
widget stays.

Verified in the GUI: shift-drag in the diffraction view still draws the box,
turns it into a named ROI and runs the statistics; fit-view panel remains
pixel-identical to the pre-series baseline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-26 21:32:47 +02:00
co-authored by Claude Opus 5
parent 327e2645cf
commit 85c4908afc
9 changed files with 111 additions and 155 deletions
+83 -29
View File
@@ -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<int> 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<float>(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<bool(int64_t, int64_t)> &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<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);
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<float>(val) * x;
y_weighted += static_cast<float>(val) * y;
roi_val += val;
roi_val_2 += static_cast<uint64_t>(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<float>(roiStartPos.x());
const float cy = static_cast<float>(roiStartPos.y());
const float r2 = static_cast<float>(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<float>(x) - cx;
const float dy = static_cast<float>(y) - cy;
return dx * dx + dy * dy <= r2;
});
}
emit roiCalculated(msg);
}
QString JFJochDiffractionImage::PixelLabel(int x, int y) const {
if (!image)
return {};