332 lines
11 KiB
C++
332 lines
11 KiB
C++
// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#include "JFJochSimpleImageViewer.h"
|
|
#include <QGraphicsScene>
|
|
#include <QGraphicsPixmapItem>
|
|
#include <QWheelEvent>
|
|
#include <QScrollBar>
|
|
#include <QtMath>
|
|
#include <cstring>
|
|
#include <algorithm>
|
|
|
|
#include "../../common/JFJochException.h"
|
|
|
|
static inline double normalize_to_unit(double raw, double bg, double fg) {
|
|
const double denom = std::max(1e-12, double(fg) - double(bg));
|
|
double v = (raw - bg) / denom;
|
|
if (v < 0.0) v = 0.0;
|
|
if (v > 1.0) v = 1.0;
|
|
return v;
|
|
}
|
|
|
|
JFJochSimpleImageViewer::JFJochSimpleImageViewer(QWidget *parent)
|
|
: QGraphicsView(parent) {
|
|
setDragMode(QGraphicsView::NoDrag);
|
|
setTransformationAnchor(QGraphicsView::AnchorUnderMouse);
|
|
setRenderHint(QPainter::Antialiasing);
|
|
setRenderHint(QPainter::SmoothPixmapTransform);
|
|
setFocusPolicy(Qt::ClickFocus);
|
|
|
|
auto *scn = new QGraphicsScene(this);
|
|
setScene(scn);
|
|
|
|
// Optional: a sensible default colormap
|
|
color_scale_.Select(ColorScaleEnum::Indigo);
|
|
|
|
// Keep overlays in pixel units independent of zoom (for labels font sizing)
|
|
setViewportUpdateMode(QGraphicsView::FullViewportUpdate);
|
|
}
|
|
|
|
void JFJochSimpleImageViewer::clear() {
|
|
has_image_ = false;
|
|
image_.reset();
|
|
|
|
if (scene())
|
|
scene()->clear();
|
|
}
|
|
|
|
void JFJochSimpleImageViewer::setImage(const std::shared_ptr<const SimpleImage> &img) {
|
|
image_ = img;
|
|
has_image_ = true;
|
|
renderImage();
|
|
updateScene();
|
|
}
|
|
|
|
void JFJochSimpleImageViewer::setBackground(float v) {
|
|
background_ = v;
|
|
emit backgroundChanged(background_);
|
|
if (has_image_) {
|
|
renderImage();
|
|
// Preserve current transform while updating
|
|
updateScene();
|
|
}
|
|
}
|
|
|
|
void JFJochSimpleImageViewer::setForeground(float v) {
|
|
foreground_ = std::max(1e-6f, v);
|
|
emit foregroundChanged(foreground_);
|
|
if (has_image_) {
|
|
renderImage();
|
|
updateScene();
|
|
}
|
|
}
|
|
|
|
void JFJochSimpleImageViewer::setColorMap(int colorMapEnumValue) {
|
|
try {
|
|
color_scale_.Select(static_cast<ColorScaleEnum>(colorMapEnumValue));
|
|
if (has_image_) {
|
|
renderImage();
|
|
updateScene();
|
|
}
|
|
} catch (...) {
|
|
// ignore invalid value
|
|
}
|
|
}
|
|
|
|
void JFJochSimpleImageViewer::wheelEvent(QWheelEvent *event) {
|
|
if (!scene() || !has_image_) return;
|
|
|
|
const double zoomFactor = 1.15;
|
|
const QPointF targetScenePos = mapToScene(event->position().toPoint());
|
|
|
|
if (event->modifiers() == Qt::ShiftModifier) {
|
|
// Shift + wheel adjusts foreground (like your main viewer)
|
|
float new_fg = foreground_ + event->angleDelta().y() / 120.0f;
|
|
if (new_fg < 1.0f) new_fg = 1.0f;
|
|
setForeground(new_fg);
|
|
} else {
|
|
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);
|
|
}
|
|
}
|
|
// Keep focus under mouse
|
|
const QPointF updatedCenter = mapToScene(viewport()->rect().center());
|
|
const QPointF delta = targetScenePos - updatedCenter;
|
|
translate(delta.x(), delta.y());
|
|
// Only labels depend on redraw; pixmap stays. Rebuild scene to re-place labels
|
|
updateScene();
|
|
}
|
|
}
|
|
|
|
void JFJochSimpleImageViewer::mousePressEvent(QMouseEvent *event) {
|
|
if (!scene() || !has_image_) return;
|
|
if (event->button() == Qt::LeftButton) {
|
|
panning_ = true;
|
|
setCursor(Qt::ClosedHandCursor);
|
|
last_mouse_pos_ = event->pos();
|
|
}
|
|
QGraphicsView::mousePressEvent(event);
|
|
}
|
|
|
|
void JFJochSimpleImageViewer::mouseMoveEvent(QMouseEvent *event) {
|
|
if (!scene() || !has_image_) return;
|
|
if (panning_) {
|
|
const QPointF delta = mapToScene(event->pos()) - mapToScene(last_mouse_pos_);
|
|
last_mouse_pos_ = event->pos();
|
|
translate(delta.x(), delta.y());
|
|
// labels positions are in scene coords, scene items re-used; no special handling
|
|
} else {
|
|
auto coord = mapToScene(event->pos());
|
|
if ((coord.x() >= 0)
|
|
&& (coord.x() < image_->image.GetWidth())
|
|
&& (coord.y() >= 0)
|
|
&& (coord.y() < image_->image.GetHeight())) {
|
|
emit writeStatusBar(QString("x=%1 y=%2 %3 ")
|
|
.arg(coord.x(), 0, 'f', 1)
|
|
.arg(coord.y(), 0, 'f', 1)
|
|
.arg(image_values_[int(coord.x()) + int(coord.y()) * image_->image.GetWidth()]
|
|
), 6000);
|
|
} else
|
|
emit writeStatusBar("", 6000);
|
|
}
|
|
QGraphicsView::mouseMoveEvent(event);
|
|
}
|
|
|
|
void JFJochSimpleImageViewer::mouseReleaseEvent(QMouseEvent *event) {
|
|
if (!scene() || !has_image_) return;
|
|
if (event->button() == Qt::LeftButton && panning_) {
|
|
panning_ = false;
|
|
setCursor(Qt::ArrowCursor);
|
|
}
|
|
QGraphicsView::mouseReleaseEvent(event);
|
|
}
|
|
|
|
void JFJochSimpleImageViewer::resizeEvent(QResizeEvent *event) {
|
|
QGraphicsView::resizeEvent(event);
|
|
// Nothing special; keep current transform and scene
|
|
}
|
|
|
|
void JFJochSimpleImageViewer::renderImage() {
|
|
const size_t W = image_->image.GetWidth();
|
|
const size_t H = image_->image.GetHeight();
|
|
if (W == 0 || H == 0) return;
|
|
|
|
// Prepare destination QImage (RGB888)
|
|
QImage qimg(int(W), int(H), QImage::Format_RGB888);
|
|
image_rgb_.resize(W * H);
|
|
image_values_.resize(W * H);
|
|
|
|
std::vector<uint8_t> image_buffer;
|
|
|
|
// Access uncompressed data
|
|
const uint8_t *src = image_->image.GetUncompressedPtr(image_buffer);
|
|
const auto mode = image_->image.GetMode();
|
|
|
|
auto write_rgb_row = [&](int y) -> uchar * {
|
|
return qimg.scanLine(y);
|
|
};
|
|
|
|
auto write_pixel = [&](size_t idx, uchar *dstRow, const rgb &c) {
|
|
dstRow[idx * 3 + 0] = c.r;
|
|
dstRow[idx * 3 + 1] = c.g;
|
|
dstRow[idx * 3 + 2] = c.b;
|
|
image_rgb_[idx] = c;
|
|
};
|
|
|
|
// Fast path: packed RGB (assumed 3 bytes per pixel)
|
|
if (mode == CompressedImageMode::RGB) {
|
|
for (int y = 0; y < int(H); ++y) {
|
|
const rgb *row = reinterpret_cast<const rgb *>(src + y * W * sizeof(rgb));
|
|
uchar *dst = write_rgb_row(y);
|
|
for (size_t x = 0; x < W; ++x) {
|
|
const rgb c = row[x];
|
|
write_pixel(x, dst, c);
|
|
}
|
|
}
|
|
} else {
|
|
switch (mode) {
|
|
case CompressedImageMode::Uint8:
|
|
renderImage<uint8_t>(qimg, src);
|
|
break;
|
|
case CompressedImageMode::Int8:
|
|
renderImage<int8_t>(qimg, src);
|
|
break;
|
|
case CompressedImageMode::Uint16:
|
|
renderImage<uint16_t>(qimg, src);
|
|
break;
|
|
case CompressedImageMode::Int16:
|
|
renderImage<int16_t>(qimg, src);
|
|
break;
|
|
case CompressedImageMode::Uint32:
|
|
renderImage<uint32_t>(qimg, src);
|
|
break;
|
|
case CompressedImageMode::Int32:
|
|
renderImage<int32_t>(qimg, src);
|
|
break;
|
|
case CompressedImageMode::Float32:
|
|
renderImage<float>(qimg, src);
|
|
break;
|
|
case CompressedImageMode::Float64:
|
|
renderImage<double>(qimg, src);
|
|
break;
|
|
case CompressedImageMode::Float16:
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, "Float16 not supported");
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Build pixmap
|
|
pixmap_ = QPixmap::fromImage(qimg);
|
|
}
|
|
|
|
void JFJochSimpleImageViewer::updateScene() {
|
|
if (!scene()) return;
|
|
scene()->clear();
|
|
if (!has_image_) return;
|
|
|
|
// Add base image
|
|
scene()->addItem(new QGraphicsPixmapItem(pixmap_));
|
|
|
|
QFont font("Arial", 1); // Font for pixel value text
|
|
font.setPixelSize(1); // This will render very small text (1-pixel high).
|
|
|
|
// Get the visible area in the scene coordinates
|
|
QRectF visibleRect = mapToScene(viewport()->geometry()).boundingRect();
|
|
|
|
// Calculate the range of pixels to process
|
|
const int W = int(image_->image.GetWidth());
|
|
const int H = int(image_->image.GetHeight());
|
|
|
|
const int startX = std::max(0, static_cast<int>(std::floor(visibleRect.left())));
|
|
const int endX = std::min(W, static_cast<int>(std::ceil(visibleRect.right())));
|
|
const int startY = std::max(0, static_cast<int>(std::floor(visibleRect.top())));
|
|
const int endY = std::min(H, static_cast<int>(std::ceil(visibleRect.bottom())));
|
|
|
|
// Only when zoomed in and region small (performance)
|
|
if (scale_factor_ > 20.0 && (endX - startX + 1) * (endY - startY + 1) < 500) {
|
|
const qreal f = std::clamp(scale_factor_, 0.5, 50.0);
|
|
QFont font("Arial", 1);
|
|
font.setPixelSize(1); // 1 px font; we use scaling below as needed
|
|
|
|
for (int y = startY; y < endY; ++y) {
|
|
const size_t base = size_t(y) * size_t(W);
|
|
for (int x = startX; x < endX; ++x) {
|
|
const size_t idx = base + size_t(x);
|
|
|
|
const double raw = image_values_[idx];
|
|
const rgb c = image_rgb_[idx];
|
|
|
|
// If no scalar was stored (e.g., RGB source), you can skip or compute some alt text
|
|
if (std::isnan(raw)) continue;
|
|
|
|
// Text content from original value
|
|
const QString pixelText = QString::number(raw);
|
|
|
|
// Contrast against colored pixel
|
|
QGraphicsTextItem *textItem = scene()->addText(pixelText, font);
|
|
if (luminance(c) > 128.0)
|
|
textItem->setDefaultTextColor(Qt::black);
|
|
else
|
|
textItem->setDefaultTextColor(Qt::white);
|
|
|
|
textItem->setPos(x - 0.7, y - 0.8);
|
|
textItem->setScale(0.2);
|
|
textItem->setZValue(10.0);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
template<class T>
|
|
void JFJochSimpleImageViewer::renderImage(QImage &qimg, const uint8_t *input) {
|
|
const size_t W = image_->image.GetWidth();
|
|
const size_t H = image_->image.GetHeight();
|
|
|
|
auto ptr = reinterpret_cast<const T *>(input);
|
|
int64_t val;
|
|
for (int i = 0; i < W * H; i++) {
|
|
if (std::is_floating_point_v<T>)
|
|
val = std::lround(ptr[i]);
|
|
else
|
|
val = static_cast<int64_t>(ptr[i]);
|
|
image_values_[i] = val;
|
|
}
|
|
|
|
if (auto_fgbg) {
|
|
auto vec1(image_values_);
|
|
std::sort(vec1.begin(), vec1.end());
|
|
background_ = vec1[0];
|
|
foreground_ = vec1[vec1.size() - 1];
|
|
emit backgroundChanged(background_);
|
|
emit foregroundChanged(foreground_);
|
|
}
|
|
|
|
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) {
|
|
image_rgb_[y * W + x] = color_scale_.Apply(image_values_[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
|
|
}
|
|
}
|
|
}
|