Files
Jungfraujoch/viewer/JFJochViewerImage.cpp
2025-10-20 20:43:44 +02:00

650 lines
22 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(QWidget *parent) : 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) {
if (!scene() || !image) return;
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();
}
}
QPointF RoundPoint(const QPointF &input) {
return QPointF(qRound(input.x()), qRound(input.y()));
}
void JFJochViewerImage::mousePressEvent(QMouseEvent *event) {
if (!scene() || !image) return;
mouse_event_type = MouseEventType::None;
bool mouse_inside_roi = false;
if (roiStartPos != roiEndPos) {
if (roi_type == RoiType::RoiBox) {
QRectF roiRect = QRectF(roiStartPos, roiEndPos).normalized();
mouse_inside_roi = roiRect.contains(mapToScene(event->pos()));
} else {
auto curr_position = mapToScene(event->pos());
auto delta = roiStartPos - curr_position;
auto radius_vec = roiStartPos - roiEndPos;
auto radius_2 = radius_vec.x() * radius_vec.x() + radius_vec.y() * radius_vec.y();
mouse_inside_roi = (delta.x() * delta.x() + delta.y() * delta.y() < radius_2);
}
}
if (event->button() == Qt::LeftButton) {
setCursor(Qt::ClosedHandCursor); // Change cursor to indicate panning
lastMousePos = event->pos();
if (mouse_inside_roi)
mouse_event_type = MouseEventType::MovingROI;
else
mouse_event_type = MouseEventType::Panning;
} else if (event->buttons() & Qt::RightButton) {
setCursor(Qt::CursorShape::CrossCursor);
if (mouse_inside_roi)
roiEndPos = RoundPoint(mapToScene(event->pos()));
else {
roiStartPos = RoundPoint(mapToScene(event->pos()));
roi_type = (event->modifiers() == Qt::Modifier::SHIFT) ? RoiType::RoiCircle : RoiType::RoiBox;
}
mouse_event_type = MouseEventType::DrawingROI;
}
QGraphicsView::mousePressEvent(event); // Call base implementation
}
// Handle panning while moving the mouse
void JFJochViewerImage::mouseMoveEvent(QMouseEvent *event) {
if (!scene() || !image) return;
auto coord = mapToScene(event->pos());
if (image && (coord.x() >= 0)
&& (coord.x() < image->Dataset().experiment.GetXPixelsNum())
&& (coord.y() >= 0)
&& (coord.y() < image->Dataset().experiment.GetYPixelsNum())) {
float res = image->Dataset().experiment.GetDiffractionGeometry().PxlToRes(coord.x(), coord.y());
float q = 2 * M_PI / res;
if (res <= 0)
q = 0;
int32_t intensity = image->Image()[std::floor(coord.x()) +
std::floor(coord.y()) * image->Dataset().experiment.GetXPixelsNum()];
QString intensity_str = QString("I=%1").arg(intensity, 9);
if (intensity == SATURATED_PXL_VALUE)
intensity_str = "I=Saturated";
else if (intensity == GAP_PXL_VALUE)
intensity_str = " Gap ";
else if (intensity == ERROR_PXL_VALUE)
intensity_str = " Bad pxl ";
emit writeStatusBar(QString("x=%1 y=%2 %3 d=%4 Å")
.arg(coord.x(), 0, 'f', 1)
.arg(coord.y(), 0, 'f', 1)
.arg(intensity_str)
.arg(res, 0, 'f', 2));
} else
emit writeStatusBar("");
QPointF delta;
switch (mouse_event_type) {
case MouseEventType::Panning:
delta = coord - mapToScene(lastMousePos);
lastMousePos = event->pos();
translate(delta.x(), delta.y()); // Adjust the view's pan
updateOverlay();
break;
case MouseEventType::DrawingROI:
roiEndPos = RoundPoint(coord);
updateROI();
break;
case MouseEventType::MovingROI:
delta = coord - mapToScene(lastMousePos);
roiStartPos += delta;
roiEndPos += delta;
lastMousePos = event->pos();
updateROI();
break;
default:
break;
}
QGraphicsView::mouseMoveEvent(event);
}
void JFJochViewerImage::mouseReleaseEvent(QMouseEvent *event) {
if (!scene() || !image) return;
setCursor(Qt::CursorShape::ArrowCursor);
if (mouse_event_type == MouseEventType::MovingROI) {
if (roi_type == RoiType::RoiBox) {
roiStartPos = RoundPoint(roiStartPos);
roiEndPos = RoundPoint(roiEndPos);
}
updateROI();
}
mouse_event_type = MouseEventType::None;
QGraphicsView::mouseReleaseEvent(event);
}
void JFJochViewerImage::LoadImageInternal() {
if (!image || (image->Dataset().experiment.GetXPixelsNum() <= 0))
return;
// Create a QImage with RGB format
QImage qimage(image->Dataset().experiment.GetXPixelsNum(), image->Dataset().experiment.GetYPixelsNum(), QImage::Format_RGB888);
image_rgb.resize(image->Dataset().experiment.GetXPixelsNum() * image->Dataset().experiment.GetYPixelsNum());
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().experiment.GetYPixelsNum(); ++y) {
uchar *scanLine = qimage.scanLine(y); // Get writable pointer to the row
for (int x = 0; x < image->Dataset().experiment.GetXPixelsNum(); ++x) {
auto pxl = x + y * image->Dataset().experiment.GetXPixelsNum();
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
}
}
pixmap = QPixmap::fromImage(qimage);
QGraphicsScene* currentScene = scene();
if (!currentScene) {
// First time - create a new scene
currentScene = new QGraphicsScene(this);
setScene(currentScene);
}
}
void JFJochViewerImage::DrawSpots() {// Compute current visible area in scene coordinates
const QRectF visibleRect = mapToScene(viewport()->geometry()).boundingRect();
for (const auto &s: image->ImageData().spots) {
// Skip reflections outside the viewport
if (!visibleRect.contains(QPointF{s.x, s.y}))
continue;
const qreal desired_half_px = 8.0;
const qreal spot_size = desired_half_px / std::sqrt(std::max(0.0001, scale_factor));
QColor pen_color = spot_color;
if (s.indexed)
pen_color = feature_color;
else if (highlight_ice_rings && s.ice_ring)
pen_color = ice_ring_color;
QPen pen(pen_color, 3);
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);
}
}
void JFJochViewerImage::DrawPredictions() {
QFont font("Arial", 2); // Font for pixel value text
font.setPixelSize(2); // This will render very small text (1-pixel high).
const qreal desired_half_px = 8.0;
const qreal spot_size = desired_half_px / std::sqrt(std::max(0.0001, scale_factor));
QColor pen_color = prediction_color;
QPen pen(pen_color, 3);
pen.setCosmetic(true);
// Compute current visible area in scene coordinates
const QRectF visibleRect = mapToScene(viewport()->geometry()).boundingRect();
for (const auto &s: image->ImageData().reflections) {
// Skip reflections outside the viewport
if (!visibleRect.contains(QPointF{s.predicted_x, s.predicted_y}))
continue;
auto rect = scene()->addEllipse(s.predicted_x - spot_size + 0.5f,
s.predicted_y - spot_size + 0.5f,
2.0f * spot_size,
2.0f * spot_size,
pen);
// When zoomed in enough, draw "h k l" above the box
if (scale_factor >= 10.0) {
// Format label
QString label = QString("%1, %2, %3").arg(s.h).arg(s.k).arg(s.l);
// Position slightly above the top side of the box
const qreal text_x = s.predicted_x - 5.5f;
const qreal text_y = s.predicted_y - 10.0f;
// Add or update text in the scene
QGraphicsTextItem *textItem = scene()->addText(label, font);
textItem->setDefaultTextColor(pen_color);
textItem->setPos(text_x, text_y); // Position the text over the pixel
// textItem->setScale(1.0); // Scale down to 10% of the original size
}
}
}
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().experiment.GetXPixelsNum()), 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().experiment.GetYPixelsNum()), static_cast<int>(std::ceil(visibleRect.bottom())));
auto geom = image->Dataset().experiment.GetDiffractionGeometry();
geom.PoniRot3_rad(0.0);
if (res_ring_auto) {
float radius_x_0 = geom.GetBeamX_pxl() - startX;
float radius_x_1 = endX - geom.GetBeamX_pxl();
float radius_x = std::max(radius_x_0, radius_x_1);
float radius_y_0 = geom.GetBeamY_pxl() - startY;
float radius_y_1 = endY - 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 = {geom.PxlToRes(radius / 2.0f),
geom.PxlToRes(radius / 1.02f)};
else
res_ring = {};
}
QPen pen(feature_color, 5);
pen.setCosmetic(true);
QVector<qreal> dashPattern = {10, 15};
pen.setDashPattern(dashPattern);
float phi_offset = 0;
for (const auto &d: res_ring) {
auto [x1,y1] = geom.ResPhiToPxl(d, 0);
auto [x2,y2] = geom.ResPhiToPxl(d, M_PI_2);
auto [x3,y3] = geom.ResPhiToPxl(d, M_PI);
auto [x4,y4] = geom.ResPhiToPxl(d, 3.0 * M_PI_2);
auto x_min = std::min({x1,x2,x3,x4});
auto x_max = std::max({x1,x2,x3,x4});
auto y_min = std::min({y1,y2,y3,y4});
auto y_max = std::max({y1,y2,y3,y4});
QRectF boundingRect(x_min, y_min, x_max - x_min, y_max-y_min);
scene()->addEllipse(boundingRect, pen);
auto [x5,y5] = geom.ResPhiToPxl(d, phi_offset + 0);
auto [x6,y6] = geom.ResPhiToPxl(d, phi_offset + M_PI_2);
auto [x7,y7] = geom.ResPhiToPxl(d, phi_offset + M_PI);
auto [x8,y8] = geom.ResPhiToPxl(d, phi_offset + 3.0 * M_PI_2);
QPointF point_1(x5, y5);
QPointF point_2(x6,y6);
QPointF point_3(x7, y7);
QPointF point_4(x8, y8);
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", 16);
const qreal f = std::clamp(scale_factor, 0.5, 50.0);
font.setPointSizeF(16.0 / sqrt(f)); // base 12pt around scale_factor ~10
QGraphicsTextItem *textItem = scene()->addText(
QString("%1 Å").arg(QString::number(d, 'f', 2)), font);
textItem->setDefaultTextColor(feature_color);
textItem->setPos(point.value());
}
phi_offset += 4.0 / 180.0 * M_PI;
}
}
void JFJochViewerImage::DrawBeamCenter() {
auto geom = image->Dataset().experiment.GetDiffractionGeometry();
auto [beam_x, beam_y] = geom.GetDirectBeam_pxl();
DrawCross(beam_x, beam_y, 25, 5, 2);
}
void JFJochViewerImage::DrawTopPixels() {
int i = 0;
for (auto iter = image->ValidPixels().crbegin();
iter != image->ValidPixels().rend() && i < show_highest_pixels;
iter++, i++)
DrawCross(iter->second % image->Dataset().experiment.GetXPixelsNum() + 0.5, iter->second / image->Dataset().experiment.GetXPixelsNum() + 0.5, 15, 3);
}
void JFJochViewerImage::updateROI() {
if (roi_type == RoiType::RoiBox) {
QRect roi_box_int = QRectF(RoundPoint(roiStartPos), RoundPoint(roiEndPos)).normalized().toRect();
roi_box = roi_box_int;
emit roiBoxUpdated(roi_box_int);
} else {
QPointF delta = roiStartPos - roiEndPos;
double radius = std::sqrt(delta.x() * delta.x() + delta.y() * delta.y());
roi_box = QRectF(roiStartPos.x() - radius, roiStartPos.y() - radius,
2 * radius, 2 * radius).normalized();
emit roiCircleUpdated(roiStartPos.x(), roiStartPos.y(), radius);
}
updateOverlay();
}
void JFJochViewerImage::updateOverlay() {
if (!scene() || !image) return;
scene()->clear();
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
int startX = std::max(0, static_cast<int>(std::floor(visibleRect.left())));
int endX = std::min(static_cast<int>(image->Dataset().experiment.GetXPixelsNum()), 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().experiment.GetYPixelsNum()), static_cast<int>(std::ceil(visibleRect.bottom())));
if (scale_factor > 20.0 && (endX - startX + 1) * (endY - startY + 1) < 500) {
// 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().experiment.GetXPixelsNum()];
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().experiment.GetXPixelsNum()]) > 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
}
}
}
DrawBeamCenter();
if (show_spots)
DrawSpots();
if (show_predictions)
DrawPredictions();
DrawResolutionRings();
DrawTopPixels();
if (show_saturation)
DrawSaturation();
if (roi_box.width() * roi_box.height() > 0) {
QPen pen(feature_color, 3);
pen.setStyle(Qt::DashLine);
pen.setCosmetic(true);
if (roi_type == RoiType::RoiBox) {
scene()->addRect(roi_box, pen);
} else {
scene()->addEllipse(roi_box, pen);
double pointRadius = 0.5;
QRectF pointBoundingBox = QRectF(roiStartPos.x() - pointRadius,roiStartPos.y() - pointRadius,
2 * pointRadius, 2 * pointRadius);
scene()->addEllipse(pointBoundingBox, pen);
}
}
}
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(std::shared_ptr<const JFJochReaderImage> in_image) {
if (in_image) {
image = in_image;
Redraw();
} else {
image.reset();
if (scene())
scene()->clear();
}
}
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::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::showPredictions(bool input) {
show_predictions = input;
Redraw();
}
void JFJochViewerImage::setFeatureColor(QColor input) {
feature_color = input;
Redraw();
}
void JFJochViewerImage::setSpotColor(QColor input) {
spot_color = input;
Redraw();
}
void JFJochViewerImage::setPredictionColor(QColor input) {
prediction_color = input;
Redraw();
}
void JFJochViewerImage::showHighestPixels(int32_t v) {
show_highest_pixels = v;
Redraw();
}
void JFJochViewerImage::DrawSaturation() {
for (const auto &iter: image->SaturatedPixels())
DrawCross(iter % image->Dataset().experiment.GetXPixelsNum() + 0.5, iter / image->Dataset().experiment.GetXPixelsNum() + 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);
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();
}
void JFJochViewerImage::centerOnSpot(double x, double y) {
if (!image)
return;
int imgW = image->Dataset().experiment.GetXPixelsNum();
int imgH = image->Dataset().experiment.GetYPixelsNum();
if (x >= 0 && x < imgW && y >= 0 && y < imgH)
centerOn(x, y);
}
void JFJochViewerImage::highlightIceRings(bool input) {
highlight_ice_rings = input;
Redraw();
}