Files
Jungfraujoch/viewer/widgets/JFJochSimpleImageViewer.cpp

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
}
}
}