Files
Jungfraujoch/viewer/JFJochViewerImage.cpp
2025-03-02 13:15:28 +01:00

411 lines
14 KiB
C++

// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include "JFJochViewerImage.h"
#include "../common/DiffractionGeometry.h"
#include <QGraphicsPixmapItem>
#include <QGraphicsScene>
#include <QWheelEvent>
#include <QScrollBar>
#include <cmath>
// Constructor
JFJochViewerImage::JFJochViewerImage(JFJochReader &reader,
QWidget *parent) : reader(reader), QGraphicsView(parent) {
setDragMode(QGraphicsView::NoDrag); // Disable default drag mode
setTransformationAnchor(QGraphicsView::AnchorUnderMouse); // Zoom anchors
setRenderHint(QPainter::Antialiasing); // Enable smooth rendering
setRenderHint(QPainter::SmoothPixmapTransform);
setFocusPolicy(Qt::ClickFocus);
// Connect the horizontal scrollbar's valueChanged signal
connect(horizontalScrollBar(), &QScrollBar::valueChanged, this, &JFJochViewerImage::onScroll);
// Connect the vertical scrollbar's valueChanged signal
connect(verticalScrollBar(), &QScrollBar::valueChanged, this, &JFJochViewerImage::onScroll);
}
// Handle zooming with mouse wheel
void JFJochViewerImage::wheelEvent(QWheelEvent *event) {
const double zoomFactor = 1.15; // Zoom factor
// Get the position of the mouse in scene coordinates
QPointF targetScenePos = mapToScene(event->position().toPoint());
if (event->modifiers() == Qt::ShiftModifier) {
float new_foreground = foreground + event->angleDelta().y() / 120.0f;
if (new_foreground < 1)
new_foreground = 1.0;
foreground = new_foreground;
emit foregroundChanged(foreground);
Redraw();
} else {
// Perform zooming
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);
}
}
// Adjust the view's center to keep the zoom focused on the mouse position
QPointF updatedViewportCenter = mapToScene(viewport()->rect().center());
QPointF delta = targetScenePos - updatedViewportCenter;
translate(delta.x(), delta.y()); // Shift the view
updateOverlay();
}
}
// Handle start of panning on mouse press
void JFJochViewerImage::mousePressEvent(QMouseEvent *event) {
if (event->button() == Qt::LeftButton) {
setCursor(Qt::ClosedHandCursor); // Change cursor to indicate panning
lastMousePos = event->pos();
}
QGraphicsView::mousePressEvent(event); // Call base implementation
}
// Handle panning while moving the mouse
void JFJochViewerImage::mouseMoveEvent(QMouseEvent *event) {
if (event->buttons() & Qt::LeftButton) {
QPointF delta = mapToScene(event->pos()) - mapToScene(lastMousePos);
lastMousePos = event->pos();
translate(delta.x(), delta.y()); // Adjust the view's pan
updateOverlay();
}
QGraphicsView::mouseMoveEvent(event);
}
void JFJochViewerImage::LoadImageInternal() {
if (!image || (image->dataset->image_size_x <= 0))
return;
// Create a QImage with RGB format
QImage qimage(image->dataset->image_size_x, image->dataset->image_size_y, QImage::Format_RGB888);
image_rgb.resize(image->dataset->image_size_x * image->dataset->image_size_y);
rgb sat_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)};
if (show_saturation) {
sat_color = bad_color;
} else
sat_color = color_scale.Apply(1.0);
// Fill the QImage with pixel data from the array
for (int y = 0; y < image->dataset->image_size_y; ++y) {
uchar *scanLine = qimage.scanLine(y); // Get writable pointer to the row
for (int x = 0; x < image->dataset->image_size_x; ++x) {
auto pxl = x + y * image->dataset->image_size_x;
const double val_orig = (image->image[pxl] - background);
double val = val_orig / (foreground - background);
if (image->image[pxl] == GAP_PXL_VALUE)
image_rgb[pxl] = color_scale.Apply(ColorScaleSpecial::Gap);
else if (image->image[pxl] == ERROR_PXL_VALUE)
image_rgb[pxl] = bad_color;
else if (image->image[pxl] == SATURATED_PXL_VALUE)
image_rgb[pxl] = sat_color;
else if (val >= 1.0)
image_rgb[pxl] = color_scale.Apply(1.0);
else if (val >= 0.0)
image_rgb[pxl] = color_scale.Apply(val);
else
image_rgb[pxl] = color_scale.Apply(0.0);
scanLine[x * 3 + 0] = image_rgb[pxl].r; // Red
scanLine[x * 3 + 1] = image_rgb[pxl].g; // Green
scanLine[x * 3 + 2] = image_rgb[pxl].b; // Blue
}
}
// Convert QImage to QPixmap and return
auto pixmap = QPixmap::fromImage(qimage);
// Display the pixmap in a QGraphicsView
auto scene = new QGraphicsScene(this);
auto pixmapItem = new QGraphicsPixmapItem(pixmap);
scene->addItem(pixmapItem);
setScene(scene);
}
void JFJochViewerImage::DrawSpots() {
for (const auto &s: image->spots) {
size_t spot_size = 3;
QColor pen_color = spot_color;
if (s.indexed)
pen_color = feature_color;
QPen pen(pen_color, 10);
pen.setCosmetic(true);
auto rect = scene()->addRect(s.x - spot_size + 0.5,
s.y - spot_size + 0.5,
2 * spot_size,
2 * spot_size,
pen);
overlay.push_back(rect);
}
}
void JFJochViewerImage::DrawResolutionRings() {
// Get the visible area in the scene coordinates
QRectF visibleRect = mapToScene(viewport()->geometry()).boundingRect();
int startX = std::max(0, static_cast<int>(std::floor(visibleRect.left())));
int endX = std::min(static_cast<int>(image->dataset->image_size_x), static_cast<int>(std::ceil(visibleRect.right())));
int startY = std::max(0, static_cast<int>(std::floor(visibleRect.top())));
int endY = std::min(static_cast<int>(image->dataset->image_size_y), static_cast<int>(std::ceil(visibleRect.bottom())));
if (res_ring_auto) {
float radius_x_0 = image->dataset->geom.GetBeamX_pxl() - startX;
float radius_x_1 = endX - image->dataset->geom.GetBeamX_pxl();
float radius_x = std::max(radius_x_0, radius_x_1);
float radius_y_0 = image->dataset->geom.GetBeamY_pxl() - startY;
float radius_y_1 = endY - image->dataset->geom.GetBeamY_pxl();
float radius_y = std::max(radius_y_0, radius_y_1);
float radius = std::min(radius_x, radius_y);
if (radius_x <= 0)
radius = radius_y;
if (radius_y <= 0)
radius = radius_x;
if (radius > 0)
res_ring = {image->dataset->geom.PxlToRes(radius / 2.0f),
image->dataset->geom.PxlToRes(radius / 1.02f)};
else
res_ring = {};
}
QPen pen(feature_color, 5);
pen.setCosmetic(true);
for (const auto &d: res_ring) {
float r = image->dataset->geom.ResToPxl(d);
QRectF boundingRect(image->dataset->geom.GetBeamX_pxl() - r, image->dataset->geom.GetBeamY_pxl() - r, r * 2,r * 2);
auto circ = scene()->addEllipse(boundingRect, pen);
overlay.push_back(circ);
QPointF point_1(image->dataset->geom.GetBeamX_pxl(), image->dataset->geom.GetBeamY_pxl() - r + 3);
QPointF point_2(image->dataset->geom.GetBeamX_pxl(), image->dataset->geom.GetBeamY_pxl() + r - 3);
QPointF point_3(image->dataset->geom.GetBeamX_pxl() - r + 3, image->dataset->geom.GetBeamY_pxl());
QPointF point_4(image->dataset->geom.GetBeamX_pxl() + r - 3, image->dataset->geom.GetBeamY_pxl());
std::optional<QPointF> point;
if (visibleRect.contains(point_1))
point = point_1;
else if (visibleRect.contains(point_2))
point = point_2;
else if (visibleRect.contains(point_3))
point = point_3;
else if (visibleRect.contains(point_4))
point = point_4;
if (point) {
QFont font("Arial", 8);
QGraphicsTextItem *textItem = scene()->addText(
QString("%1 A").arg(QString::number(d, 'g', 2)), font);
textItem->setDefaultTextColor(feature_color);
textItem->setPos(point.value());
overlay.push_back(textItem);
}
}
}
void JFJochViewerImage::DrawBeamCenter() {
DrawCross(image->dataset->geom.GetBeamX_pxl(), image->dataset->geom.GetBeamY_pxl(), 25, 5, 2);
}
void JFJochViewerImage::DrawTopPixels() {
int i = 0;
for (auto iter = image->valid_pixel.crbegin();
iter != image->valid_pixel.rend() && i < show_highest_pixels;
iter++, i++)
DrawCross(iter->second % image->dataset->image_size_x + 0.5, iter->second / image->dataset->image_size_x + 0.5, 15, 3);
}
void JFJochViewerImage::updateOverlay() {
if (!scene() || !image) return;
for (auto &i: overlay)
delete i;
overlay.clear();
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
int startX = std::max(0, static_cast<int>(std::floor(visibleRect.left())));
int endX = std::min(static_cast<int>(image->dataset->image_size_x), static_cast<int>(std::ceil(visibleRect.right())));
int startY = std::max(0, static_cast<int>(std::floor(visibleRect.top())));
int endY = std::min(static_cast<int>(image->dataset->image_size_y), static_cast<int>(std::ceil(visibleRect.bottom())));
if (scale_factor > 80.0 && (endX - startX + 1) * (endY - startY + 1) < 2000) {
// Iterate through the pixels within the visible range
for (int y = startY; y < endY; ++y) {
for (int x = startX; x < endX; ++x) {
QString pixelText;
int32_t val = image->image[x + y * image->dataset->image_size_x];
if (val == SATURATED_PXL_VALUE)
pixelText = "Sat";
else if (val == GAP_PXL_VALUE)
pixelText = "Gap";
else if (val == ERROR_PXL_VALUE)
pixelText = "Err";
else
pixelText = QString("%1").arg(val);
// Add or update text in the scene
QGraphicsTextItem *textItem = scene()->addText(pixelText, font);
if (luminance(image_rgb[x + y * image->dataset->image_size_x]) > 128.0)
textItem->setDefaultTextColor(Qt::black); // Text color
else
textItem->setDefaultTextColor(Qt::white); // Text color
textItem->setPos(x-0.7, y-0.8); // Position the text over the pixel
textItem->setScale(0.2); // Scale down to 10% of the original size
overlay.push_back(textItem);
}
}
}
DrawBeamCenter();
if (show_spots)
DrawSpots();
DrawResolutionRings();
DrawTopPixels();
if (show_saturation)
DrawSaturation();
}
void JFJochViewerImage::resizeEvent(QResizeEvent *event) {
// Call the base class implementation first
QGraphicsView::resizeEvent(event);
updateOverlay();
}
void JFJochViewerImage::onScroll(int value) {
updateOverlay();
}
void JFJochViewerImage::Redraw() {
if (!image) return;
// Save the current transformation (zoom state)
QTransform currentTransform = this->transform();
// Regenerate the image
LoadImageInternal();
// Restore the zoom level
this->setTransform(currentTransform, false); // "false" prevents resetting the view
updateOverlay();
}
void JFJochViewerImage::loadImage() {
image = reader.CopyImage();
Redraw();
}
void JFJochViewerImage::changeBackground(float val) {
background = val;
Redraw();
}
void JFJochViewerImage::changeForeground(float val) {
foreground = val;
Redraw();
}
void JFJochViewerImage::setColorMap(int color_map) {
try {
color_scale.Select(static_cast<ColorScaleEnum>(color_map));
Redraw();
} catch (...) {}
}
void JFJochViewerImage::noImage() {
image.reset();
if (scene())
scene()->clear();
}
void JFJochViewerImage::setResolutionRing(QVector<float> v) {
res_ring = v;
res_ring_auto = false;
Redraw();
}
void JFJochViewerImage::setResolutionRingAuto() {
res_ring_auto = true;
Redraw();
}
void JFJochViewerImage::showSpots(bool input) {
show_spots = input;
Redraw();
}
void JFJochViewerImage::setFeatureColor(QColor input) {
feature_color = input;
Redraw();
}
void JFJochViewerImage::setSpotColor(QColor input) {
spot_color = input;
Redraw();
}
void JFJochViewerImage::showHighestPixels(int32_t v) {
show_highest_pixels = v;
Redraw();
}
void JFJochViewerImage::DrawSaturation() {
for (const auto &iter: image->saturated_pixel)
DrawCross(iter % image->dataset->image_size_x + 0.5, iter / image->dataset->image_size_y + 0.5, 20, 4);
}
void JFJochViewerImage::DrawCross(float x, float y, float size, float width, float z) {
float sc_size = size / sqrt(scale_factor);
QPen pen(feature_color, width);
pen.setCosmetic(true);
QGraphicsLineItem *horizontalLine = scene()->addLine(x - sc_size,y, x + sc_size, y, pen);
QGraphicsLineItem *verticalLine = scene()->addLine(x, y - sc_size, x, y + sc_size, pen);
overlay.push_back(horizontalLine);
overlay.push_back(verticalLine);
horizontalLine->setZValue(z); // Ensure it appears above other items
verticalLine->setZValue(z); // Ensure it appears above other items
}
void JFJochViewerImage::showSaturation(bool input) {
show_saturation = input;
Redraw();
}