diff --git a/viewer/image_viewer/JFJochDiffractionImage.cpp b/viewer/image_viewer/JFJochDiffractionImage.cpp index 8811985b..130aa865 100644 --- a/viewer/image_viewer/JFJochDiffractionImage.cpp +++ b/viewer/image_viewer/JFJochDiffractionImage.cpp @@ -128,9 +128,35 @@ void JFJochDiffractionImage::LoadImageInternal() { W = image->Dataset().experiment.GetXPixelsNum(); H = image->Dataset().experiment.GetYPixelsNum(); - image_fp.resize(W*H); + pixel_values_valid_ = false; // image_fp is filled on demand, see EnsurePixelValues +} + +void JFJochDiffractionImage::ColorRow(size_t y, const PixelColorMap &map, QRgb *out) const { + const int32_t *row = &image->Image()[y * W]; + + for (size_t x = 0; x < W; ++x) { + const int32_t v = row[x]; + + // The three sentinels are the extremes of the int32 range, so one range test + // separates them from every real pixel value + rgb c; + if (v > GAP_PXL_VALUE && v < SATURATED_PXL_VALUE) + c = map.Apply(static_cast(v)); + else if (v == GAP_PXL_VALUE) + c = map.gap; + else + c = (v == ERROR_PXL_VALUE) ? map.bad : map.saturated; + + out[x] = qRgb(c.r, c.g, c.b); + } +} + +void JFJochDiffractionImage::EnsurePixelValues() { + if (pixel_values_valid_ || !image) + return; const auto &img = image->Image(); + image_fp.resize(W*H); QVector rows; rows.reserve(H); @@ -150,6 +176,8 @@ void JFJochDiffractionImage::LoadImageInternal() { image_fp[pxl] = static_cast(val); } }); + + pixel_values_valid_ = true; } void JFJochDiffractionImage::DrawSpots() { diff --git a/viewer/image_viewer/JFJochDiffractionImage.h b/viewer/image_viewer/JFJochDiffractionImage.h index 8c2f3c3b..e11e84ae 100644 --- a/viewer/image_viewer/JFJochDiffractionImage.h +++ b/viewer/image_viewer/JFJochDiffractionImage.h @@ -31,6 +31,11 @@ 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). + 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); diff --git a/viewer/image_viewer/JFJochImage.cpp b/viewer/image_viewer/JFJochImage.cpp index 1f7a9479..22f3f728 100644 --- a/viewer/image_viewer/JFJochImage.cpp +++ b/viewer/image_viewer/JFJochImage.cpp @@ -695,6 +695,48 @@ void JFJochImage::Redraw() { updateOverlay(); } +PixelColorMap JFJochImage::MakeColorMap() const { + // 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)}; + + const auto &lut_data = color_scale.LUTData(); + const auto lutSize = static_cast(lut_data.size()); + const float lutScale = static_cast(lutSize - 1); + const float range = foreground - background; + + return PixelColorMap{ + .lut = lut_data.data(), + .lut_size = lutSize, + .minv = background, + .range = range, + .inv_range = (range > 0) ? (lutScale / range) : 0.0f, + .inv_range_log = (range > 0) ? (lutScale / std::log1p(range)) : 0.0f, + .hdr = hdr_mode, + .gap = color_scale.Apply(ColorScaleSpecial::Gap), + .bad = bad_color, + // Saturation color + .saturated = show_saturation ? bad_color : color_scale.Apply(1.0f), + }; +} + +void JFJochImage::ColorRow(size_t y, const PixelColorMap &map, QRgb *out) const { + const float *row = &image_fp[y * W]; + + for (size_t x = 0; x < W; ++x) { + const float fp = row[x]; + + rgb c; + if (!std::isfinite(fp)) + c = std::isnan(fp) ? map.gap : (std::signbit(fp) ? map.bad : map.saturated); + else + c = map.Apply(fp); + + out[x] = qRgb(c.r, c.g, c.b); + } +} + void JFJochImage::RenderImage() { if (qimg_buffer_.width() != int(W) || qimg_buffer_.height() != int(H)) qimg_buffer_ = QImage(int(W), int(H), QImage::Format_RGB32); @@ -704,73 +746,14 @@ void JFJochImage::RenderImage() { uchar *const bits = qimg_buffer_.bits(); const qsizetype stride = qimg_buffer_.bytesPerLine(); - // 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.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(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); + const PixelColorMap map = MakeColorMap(); QVector rows; rows.reserve(H); for (int y = 0; y < H; ++y) rows.push_back(y); QtConcurrent::blockingMap(rows, [&](int y) { - QRgb *scanLine = reinterpret_cast(bits + y * stride); - const float *row = &image_fp[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(f + 0.5f); - if (idx <= 0) idx = 0; - else if (idx >= lutSize) idx = lutSize - 1; - c = lut_data[idx]; - } - - scanLine[x] = qRgb(c.r, c.g, c.b); - } + ColorRow(y, map, reinterpret_cast(bits + y * stride)); }); image_dirty_ = true; @@ -831,6 +814,8 @@ void JFJochImage::writePixelLabels() { constexpr float kMaxFixed = 1e5; if (visW * visH <= maxLabels) { + EnsurePixelValues(); + QString numBuf; // reused buffer for (int y = startY; y < endY; y ++) { @@ -981,6 +966,10 @@ void JFJochImage::CalcROI() { 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(); diff --git a/viewer/image_viewer/JFJochImage.h b/viewer/image_viewer/JFJochImage.h index 7bf2f6e3..e94ccb85 100644 --- a/viewer/image_viewer/JFJochImage.h +++ b/viewer/image_viewer/JFJochImage.h @@ -3,6 +3,8 @@ #pragma once +#include + #include #include #include @@ -15,6 +17,42 @@ // Q_DECLARE_METATYPE(ROIMessage) +// Maps one pixel value to a colour. Shared by the generic float path and by subclasses that +// colour straight out of their own buffer, so the two cannot drift apart. Apply() takes a +// real value; the callers handle their own gap/bad/saturated encoding. +struct PixelColorMap { + const rgb *lut = nullptr; + int lut_size = 0; + float minv = 0.0f; + float range = 0.0f; + float inv_range = 0.0f; + float inv_range_log = 0.0f; + bool hdr = false; + rgb gap{}, bad{}, saturated{}; + + [[nodiscard]] rgb Apply(float v) const { + float f; + const float v_minv = v - minv; + + if (hdr) { + if (v_minv <= 0.0f) + f = 0.0f; + else if (v_minv >= range) + f = static_cast(lut_size); + else + f = std::log1p(v_minv) * inv_range_log; + } else + f = v_minv * inv_range; + + if (f < 0.0f) f = 0.0f; + + auto idx = static_cast(f + 0.5f); + if (idx <= 0) idx = 0; + else if (idx >= lut_size) idx = lut_size - 1; + return lut[idx]; + } +}; + // Draws the rendered frame straight out of JFJochImage::qimg_buffer_. A QGraphicsPixmapItem // would mean converting the whole image into a QPixmap on every recolour, which costs one // extra allocation and a full pass over the pixels. @@ -119,6 +157,14 @@ protected: void updateOverlay(); void RenderImage(); + PixelColorMap MakeColorMap() const; + // Colour one row into `out`. Called from worker threads, so it must stay const. The base + // 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.