Files
Jungfraujoch/viewer/widgets/JFJochViewerImageStrip.cpp
leonarski_f 75e401f0e5
Build Packages / Unit tests (push) Successful in 1h31m59s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 8m43s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 10m5s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 9m27s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 8m56s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 9m24s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 10m27s
Build Packages / build:rpm (rocky8) (push) Successful in 9m20s
Build Packages / build:rpm (rocky9) (push) Successful in 10m50s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 9m54s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 8m38s
Build Packages / DIALS test (push) Successful in 12m13s
Build Packages / XDS test (durin plugin) (push) Successful in 7m8s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 7m8s
Build Packages / XDS test (neggia plugin) (push) Successful in 7m50s
Build Packages / Generate python client (push) Successful in 16s
Build Packages / Build documentation (push) Successful in 50s
Build Packages / Create release (push) Skipped
v1.0.0-rc.153 (#63)
This is an UNSTABLE release. It includes many experimental features, as well as many AI generated fixes. We recommend using rc.152 for production use.

* jfjoch_broker: Add EXPERIMENTAL pixelrefine mode for image processing
* jfjoch_broker: Allow to load user mask from 8-bit and 16-bit TIFF files
* jfjoch_broker: Add ROI calculation in non-FPGA workflow
* jfjoch_broker: Fixes to TCP image pusher
* jfjoch_broker: Remove NUMA bindings
* jfjoch_broker: Improvements to indexing
* jfjoch_broker: For PSI EIGER, trimming energies are taken from the detector configuration (now compulsory) instead of hardcoded values
* jfjoch_writer: Save ROI definitions and the per-pixel ROI bitmap in the master file; azimuthal ROIs support phi (angular) sectors
* jfjoch_viewer: Major redesign with dockable panels and saved layouts, plus on-canvas creation/move/resize of box, circle and azimuthal ROIs
* jfjoch_viewer: Run jfjoch_process reprocessing jobs from inside the GUI and overlay per-run results

Reviewed-on: #63
2026-06-23 20:29:49 +02:00

193 lines
7.6 KiB
C++

// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include "JFJochViewerImageStrip.h"
#include <QComboBox>
#include <QCheckBox>
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QScrollArea>
#include <QToolButton>
#include <QLabel>
#include <QPixmap>
#include <QStyle>
#include <QRandomGenerator>
#include <QResizeEvent>
#include <algorithm>
#include <numeric>
#include <cmath>
#include <utility>
JFJochViewerImageStrip::JFJochViewerImageStrip(QWidget *parent) : QWidget(parent) {
auto *layout = new QVBoxLayout(this);
layout->setContentsMargins(4, 2, 4, 2);
auto *controls = new QHBoxLayout();
controls->addWidget(new QLabel("Show", this));
mode_ = new QComboBox(this);
mode_->addItem("Evenly spaced", 0);
mode_->addItem("Most spots", 1);
mode_->addItem("Indexed", 2);
mode_->addItem("Resolution", 3); // representatives across the resolution range
mode_->addItem("Background", 4); // representatives across the background range
controls->addWidget(mode_);
spots_ = new QCheckBox("Spots", this);
spots_->setChecked(true);
spots_->setToolTip("Overlay found spots — a real pattern reads like a constellation");
controls->addWidget(spots_);
auto *refresh = new QToolButton(this);
refresh->setIcon(style()->standardIcon(QStyle::SP_BrowserReload));
refresh->setAutoRaise(true);
refresh->setCursor(Qt::PointingHandCursor);
refresh->setToolTip("Re-roll the representative selection");
controls->addWidget(refresh);
controls->addStretch();
layout->addLayout(controls);
connect(refresh, &QToolButton::clicked, this, [this] { Rebuild(); });
scroll_ = new QScrollArea(this);
scroll_->setWidgetResizable(true);
scroll_->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
scroll_->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
scroll_->setMinimumHeight(48); // let the dock shrink; thumbnails scale to fit
auto *stripWidget = new QWidget(scroll_);
strip_ = new QHBoxLayout(stripWidget);
strip_->setContentsMargins(0, 0, 0, 0);
strip_->addStretch();
scroll_->setWidget(stripWidget);
layout->addWidget(scroll_);
connect(mode_, &QComboBox::currentIndexChanged, this, [this] { Rebuild(); });
connect(spots_, &QCheckBox::toggled, this, [this] { Rebuild(); });
}
void JFJochViewerImageStrip::datasetLoaded(std::shared_ptr<const JFJochReaderDataset> dataset) {
// Store the latest dataset (metrics may update during live sync / re-analysis) but do NOT
// rebuild here — that would refetch thumbnails on every HTTP image update. Rebuilding happens
// on file open (resetForNewFile, which fires just after this) and on mode / Spots / Refresh.
dataset_ = std::move(dataset);
}
void JFJochViewerImageStrip::resetForNewFile() {
Rebuild(); // a new file/stream opened; dataset_ was just set by datasetLoaded
}
void JFJochViewerImageStrip::thumbnailReady(qint64 image_number, QImage thumb) {
auto it = slots_.find(image_number);
if (it != slots_.end())
it.value()->setIcon(QPixmap::fromImage(thumb));
}
QVector<qint64> JFJochViewerImageStrip::ComputeRepresentatives() const {
QVector<qint64> result;
if (!dataset_)
return result;
const int64_t total = dataset_->experiment.GetImageNum();
if (total <= 0)
return result;
const int N = static_cast<int>(std::min<int64_t>(8, total));
const int mode = mode_->currentIndex();
auto *rng = QRandomGenerator::global();
// Pick one random image from each of N equal bins over an ordered candidate list. Stochastic,
// so Refresh re-rolls a fresh set and deterministic-spacing artefacts are avoided.
auto binnedRandom = [&](const std::vector<int64_t> &candidates) {
const size_t M = candidates.size();
if (M == 0)
return;
const int n = static_cast<int>(std::min<size_t>(N, M));
for (int i = 0; i < n; ++i) {
const size_t lo = static_cast<size_t>(i) * M / n;
size_t hi = static_cast<size_t>(i + 1) * M / n;
if (hi <= lo) hi = lo + 1;
result.push_back(candidates[lo + rng->bounded(static_cast<quint32>(hi - lo))]);
}
};
// Image indices ordered by a metric (finite values only), low to high.
auto orderedByMetric = [](const std::vector<float> &metric) {
std::vector<std::pair<float, int64_t>> vals;
for (size_t i = 0; i < metric.size(); ++i)
if (std::isfinite(metric[i]))
vals.push_back({metric[i], static_cast<int64_t>(i)});
std::sort(vals.begin(), vals.end());
std::vector<int64_t> idx;
idx.reserve(vals.size());
for (const auto &v : vals) idx.push_back(v.second);
return idx;
};
std::vector<int64_t> allIndices(total);
std::iota(allIndices.begin(), allIndices.end(), 0);
const auto &sc = dataset_->spot_count;
const auto &sci = dataset_->spot_count_indexed;
if (mode == 1 && !sc.empty()) { // most spots (deterministic top-N)
std::vector<int64_t> idx(sc.size());
std::iota(idx.begin(), idx.end(), 0);
const size_t n = std::min<size_t>(N, idx.size());
std::partial_sort(idx.begin(), idx.begin() + n, idx.end(),
[&](int64_t a, int64_t b) { return sc[a] > sc[b]; });
idx.resize(n);
std::sort(idx.begin(), idx.end());
for (int64_t i : idx) result.push_back(i);
} else if (mode == 2 && !sci.empty()) { // indexed images, spaced random
std::vector<int64_t> indexed;
for (size_t i = 0; i < sci.size(); ++i)
if (sci[i] > 0) indexed.push_back(static_cast<int64_t>(i));
binnedRandom(indexed.empty() ? allIndices : indexed);
} else if (mode == 3 && !dataset_->resolution_estimate.empty()) {
binnedRandom(orderedByMetric(dataset_->resolution_estimate));
} else if (mode == 4 && !dataset_->bkg_estimate.empty()) {
binnedRandom(orderedByMetric(dataset_->bkg_estimate));
} else { // evenly spaced (random within each bin)
binnedRandom(allIndices);
}
return result;
}
void JFJochViewerImageStrip::Rebuild() {
// Clear the strip (keep the trailing stretch).
slots_.clear();
while (strip_->count() > 1) {
QLayoutItem *item = strip_->takeAt(0);
if (item->widget())
item->widget()->deleteLater();
delete item;
}
if (!dataset_)
return;
const QVector<qint64> reps = ComputeRepresentatives();
int pos = 0;
for (qint64 n : reps) {
auto *btn = new QToolButton(this);
btn->setToolButtonStyle(Qt::ToolButtonIconOnly); // the image number is drawn in the bitmap
btn->setAutoRaise(true);
btn->setCursor(Qt::PointingHandCursor);
btn->setToolTip(QStringLiteral("Open image %1").arg(n + 1));
connect(btn, &QToolButton::clicked, this, [this, n] { emit imageSelected(n, 1); });
strip_->insertWidget(pos++, btn);
slots_[n] = btn;
}
UpdateThumbnailSize();
emit requestThumbnails(reps, spots_->isChecked());
}
void JFJochViewerImageStrip::UpdateThumbnailSize() {
if (!scroll_)
return;
// Scale the thumbnails to the available height so the strip never needs more room than it has.
const int h = std::clamp(scroll_->viewport()->height() - 4, 40, 240);
for (auto *btn : std::as_const(slots_))
btn->setIconSize(QSize(h, h));
}
void JFJochViewerImageStrip::resizeEvent(QResizeEvent *event) {
QWidget::resizeEvent(event);
UpdateThumbnailSize();
}