Files
Jungfraujoch/viewer/image_viewer/JFJochDiffractionImage.h
T
leonarski_fandClaude Opus 5.5 d397717ff7
Build Packages / Unit tests (push) Successful in 1h11m40s
Build Packages / build:windows:nocuda (push) Successful in 17m39s
Build Packages / build:windows:cuda (push) Successful in 20m10s
Build Packages / build:viewer-tgz:cpu (push) Successful in 10m2s
Build Packages / build:viewer-tgz:cuda (push) Successful in 11m28s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 9m4s
Build Packages / build:rugnux:windows (push) Successful in 11m20s
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 7m46s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 16m15s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 16m21s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 18m1s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 19m19s
Build Packages / build:rpm (rocky8) (push) Successful in 18m17s
Build Packages / build:rpm (rocky9) (push) Successful in 18m48s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 15m59s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 16m19s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 25m2s
Build Packages / Generate python client (push) Successful in 1m3s
Build Packages / Build documentation (push) Successful in 1m28s
Build Packages / Create release (push) Successful in 18s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 10m50s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 11m15s
Viewer: dock the magnifier, fixed zoom levels, Shift-driven with a frame
The magnifier moves from a helper window into its own dock below the
inspector. It has three fixed zoom levels (x64 and x32 with the pixel
values written on the pixels, x10 without) instead of wheel zoom, and
follows the cursor only while Shift is held over the diffraction image.
Meanwhile the diffraction view draws a frame around the area it covers,
just outside that area so it stays visible at low zoom without hiding the
pixels; releasing Shift (seen by the application-wide key filter, so
wherever the focus is), moving without it or leaving the image hides it.

A "Pop out" button moves the magnifier into a separate window and back.
Qt's own dock floating stays disabled: a floated dock is placed
off-screen on WSLg. Whether it was popped out, and the window geometry,
persist across sessions. The inspector's image statistics become a
collapsible section so a small screen can give the space to the
magnifier. kLayoutVersion is bumped for the new dock.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 13:47:28 +02:00

178 lines
7.6 KiB
C++

// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <QPainterPath>
#include "JFJochImage.h"
#include "../../reader/JFJochReaderImage.h"
#include "../../common/ROIDefinition.h"
class ROIAzimuthal;
class DiffractionGeometry;
class JFJochDiffractionImage : public JFJochImage {
Q_OBJECT
QColor spot_color = Qt::green;
QColor prediction_color = Qt::darkRed;
QColor ice_ring_color = Qt::cyan;
QColor second_lattice_color = QColor(0xFA, 0x72, 0x68); // coral, the viewer "finishing" accent
// With outlining on, spot markers are drawn twice: a black pen this much wider underneath,
// then the marker on top. The hues mean something - cyan is the colour of ice, magenta is
// the one colour no colour map contains - so none of them moves to gain contrast. The casing
// carries the marker where the colour map's low end is light, the bright core carries it
// where it is dark, so one mechanism covers both without branching on the colour map. It is
// opt-in (default off): the doubled stroke makes the markers harder to read for some users.
// Predictions are left alone: darkRed is already legible on a light map and a black casing
// does nothing for it on a dark one.
static constexpr int MARKER_CASING_EXTRA_WIDTH = 4;
float hover_resolution = NAN;
// The "d = ... A" readout is painted in drawForeground() in viewport pixels rather than kept
// as a scene item, so updating it dirties only its own rect. hover_text_rect_ is where it
// currently sits, in viewport coordinates.
QRect hover_text_rect_;
[[nodiscard]] QString HoverResolutionLabel() const;
// Counts are exact integers: label them from the int32 image rather than from a float copy,
// which both avoids materialising that copy and cannot round large summed values.
[[nodiscard]] QString PixelLabel(int x, int y) const override;
// This is the view that has detector counts, so it is the one that offers an ROI
[[nodiscard]] bool AllowROI() const override { return true; }
void drawForeground(QPainter *painter, const QRectF &rect) override;
// The readout sits at a fixed viewport position, but QGraphicsView scrolls by blitting the
// viewport, which carries the painted text along with it - so its old and new places both have
// to be repainted when the view pans.
void scrollContentsBy(int dx, int dy) override;
public:
enum class RingMode {Auto, Estimation, Manual, None, IceRings};
Q_ENUM(RingMode)
JFJochDiffractionImage(QWidget *parent = nullptr);
private:
void addCustomOverlay() override;
void LoadImageInternal();
// Colour straight from the int32 detector image; no float copy of it is ever built
void ColorRow(size_t y, const PixelColorMap &map, QRgb *out) const override;
void DrawResolutionRings();
void DrawROIs();
void DrawAzimuthalROI(const ROIAzimuthal &az, const QColor &color, const DiffractionGeometry &geom);
void AddROILabel(const std::string &name, const QColor &color, float px, float py);
// Interactive editing of the selected (named) ROI: move box/circle for now.
bool roiEditPress(const QPointF &scenePos) override;
void roiEditMove(const QPointF &scenePos) override;
void roiEditRelease() override;
void roiScratchDrawn() override;
void keyPressEvent(QKeyEvent *event) override; // Delete removes the selected ROI
[[nodiscard]] ROIDefinition BuildEditedROIDefinition() const; // current ROIs with the edit applied
[[nodiscard]] ResizeHandle hitTestBoxHandle(const QRectF &r, const QPointF &p, qreal tol) const;
// Grab points for an azimuthal ROI: inner/outer arc (resize Q/d) and the two phi
// edges (rotate). The phi handles are only meaningful for a sector (HasPhi()).
void azimuthalHandles(const ROIAzimuthal &az, const DiffractionGeometry &geom,
QPointF &inner, QPointF &outer, QPointF &phimin, QPointF &phimax) const;
QString selected_roi_;
enum class RoiEdit { None, MoveBox, ResizeBox, MoveCircle, ResizeCircle,
AzimInner, AzimOuter, RotatePhiMin, RotatePhiMax };
RoiEdit roi_edit_ = RoiEdit::None;
ResizeHandle box_handle_ = ResizeHandle::None;
QString edit_name_;
QRectF edit_box_;
QPointF edit_center_;
double edit_radius_ = 0;
float edit_d_min_ = 0, edit_d_max_ = 0, edit_phi_min_ = 0, edit_phi_max_ = 0; // azimuthal edit state
bool edit_has_phi_ = false;
QPointF move_last_;
bool live_pending_ = false; // a live edit is being recomputed; throttles emissions
void DrawSpots();
void DrawPredictions();
void DrawBeamCenter();
void DrawTopPixels();
void DrawSaturation();
void DrawResolutionText();
void DrawCross(float x, float y, float size, float width, float z = 1);
void UpdateForeground();
// The foreground the current image suggests: the top of the valid range in HDR mode, the
// auto-contrast value otherwise. Empty when there is no image to take it from.
[[nodiscard]] std::optional<float> AutoForegroundValue() const;
bool one_shot_auto_ = false; // `A` applied the auto value; pressing it again makes Auto permanent
void leaveEvent(QEvent *event) override;
std::shared_ptr<const JFJochReaderImage> image;
int32_t show_highest_pixels = 0;
QVector<float> res_ring = {};
// Constant-d contours traced for res_ring, kept across overlay rebuilds; see DrawResolutionRings
QVector<float> ring_cache_key_ = {};
QVector<std::pair<float, QPainterPath>> ring_cache_ = {};
RingMode ring_mode = RingMode::Estimation;
bool show_spots = false;
bool show_predictions = false;
bool show_roi_labels = false;
bool show_roi_fill = false;
bool highlight_ice_rings = true;
bool hide_unindexed_spots = false;
bool hide_ice_ring_spots = false;
bool outline_spots = false; // black casing under spot markers, see MARKER_CASING_EXTRA_WIDTH
void mouseHover(const QPointF &scenePos, Qt::KeyboardModifiers modifiers) override;
// The magnifier follows the cursor only while Shift is held, and meanwhile a frame here
// shows the area it covers. magnifier_frame_ is empty while the frame is hidden.
QSizeF magnifier_field_;
QRectF magnifier_frame_;
void SetMagnifierFrame(const QRectF &r);
signals:
void magnifierMoved(QPointF scenePos);
void roiGeometryEdited(ROIDefinition rois);
void roiSelected(QString name); // user picked an ROI by clicking it on the image
public slots:
void setSelectedROI(QString name);
void loadImage(std::shared_ptr<const JFJochReaderImage> image);
void setAutoForeground(bool input);
// Apply the auto-contrast value once, leaving Auto as it was; pressing it a second time on the
// same image switches Auto on for good (which keeps recomputing it on every subsequent image).
void oneShotAutoForeground();
void setResolutionRing(QVector<float> v);
void setResolutionRingMode(RingMode mode);
void showSpots(bool input);
void showPredictions(bool input);
void showROILabels(bool input);
void showROIFill(bool input);
void setSpotColor(QColor input);
void setPredictionColor(QColor input);
void showHighestPixels(int32_t v);
void showSaturation(bool input);
void showBeamStop(bool input);
void highlightIceRings(bool input);
void hideUnindexedSpots(bool input);
void hideIceRingSpots(bool input);
void outlineSpots(bool input);
void setHDRMode(bool input);
void setMagnifierField(QSizeF size);
void hideMagnifierFrame();
};