Viewer: colour the diffraction image straight from int32
image_fp is the base class's one pixel representation, and it earns that for
three of the four image widgets: the azimuthal image is already float, the grid
scan holds computed 1/sigma^2 floats, and the calibration viewer accepts eight
source types from uint8 to float64. The diffraction image is the odd one out --
its source is a large int32 buffer -- and it is the one paying: a full
int32 -> float pass plus a second resident copy of the image, on every frame.
Split the mapping from the source. PixelColorMap holds the precomputed LUT
constants and does value -> colour; a virtual ColorRow() picks the pixels out of
whatever buffer the subclass has. Both paths now go through the same Apply(), so
only the gap/bad/saturated dispatch differs, and it lines up exactly with the
encoding LoadImageInternal used:
GAP_PXL_VALUE -> NAN -> gap
ERROR_PXL_VALUE -> -INF -> bad
SATURATED_PXL_VALUE -> +INF -> saturated
The base class still needs real pixel values for ROI statistics and per-pixel
labels, so image_fp is filled on demand instead of per frame -- and only when
something reads it: a non-empty scratch ROI, or labels above 30x zoom. Neither
happens while simply looking at frames, and nothing else routinely sets roiBox
(the named ROIs are computed in the reading worker, not here).
18.1 Mpx: 10.5 -> 5.9 ms per frame and 72 MB less resident. 4.5 Mpx: 1.8 -> 1.0 ms
and 18 MB.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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<float>(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<int> rows;
|
||||
rows.reserve(H);
|
||||
@@ -150,6 +176,8 @@ void JFJochDiffractionImage::LoadImageInternal() {
|
||||
image_fp[pxl] = static_cast<float>(val);
|
||||
}
|
||||
});
|
||||
|
||||
pixel_values_valid_ = true;
|
||||
}
|
||||
|
||||
void JFJochDiffractionImage::DrawSpots() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<uint8_t>(r), .g = static_cast<uint8_t>(g), .b = static_cast<uint8_t>(b)};
|
||||
|
||||
const auto &lut_data = color_scale.LUTData();
|
||||
const auto lutSize = static_cast<int>(lut_data.size());
|
||||
const float lutScale = static_cast<float>(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<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);
|
||||
const PixelColorMap map = MakeColorMap();
|
||||
|
||||
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*>(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<int>(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<QRgb *>(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();
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include <QGraphicsView>
|
||||
#include <QGraphicsItem>
|
||||
#include <QImage>
|
||||
@@ -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<float>(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<int>(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.
|
||||
|
||||
Reference in New Issue
Block a user