viewer: soften heights, stochastic strip selection, stable on live sync
Address the review on the image strip / hit feed: - Height constraints: JFJochSimpleChartView used a hard setFixedHeight(300) and, being a page in the dataset-info stack, forced the whole plot dock (and thus the window) taller than the screen once the strip was stacked below. Make it a soft minimum (120). Wrap the settings dock in a QScrollArea so its content can scroll instead of forcing window height. Smaller strip thumbnails (96) and lower default bottom-dock heights. The window no longer grows past its requested size. - Stochastic selection: representatives are now picked at random within N equal bins (over image index, or a metric's sorted order), and a Refresh button re-rolls a fresh set — avoiding deterministic-spacing artefacts. "Most spots" stays deterministic; "Indexed" becomes spaced-random. - Live stability: the strip stores dataset updates but only rebuilds on file open (worker fileOpened) / mode / Spots / Refresh, so HTTP sync image updates no longer trigger constant thumbnail refetching. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -496,10 +496,15 @@ JFJochViewerWindow::JFJochViewerWindow(QWidget *parent, bool dbus, const QString
|
||||
// into the running analysis so edits re-run on the current image / dataset.
|
||||
auto *settingsPanel = new JFJochViewerSettingsDock(spot_finding_settings, indexing_settings,
|
||||
experiment.GetAzimuthalIntegrationSettings(), this);
|
||||
auto *settingsScroll = new QScrollArea(this);
|
||||
settingsScroll->setWidget(settingsPanel);
|
||||
settingsScroll->setWidgetResizable(true);
|
||||
settingsScroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
settingsScroll->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
|
||||
settingsDock = new QDockWidget("Settings", this);
|
||||
settingsDock->setObjectName("settingsDock");
|
||||
settingsDock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);
|
||||
settingsDock->setWidget(settingsPanel);
|
||||
settingsDock->setWidget(settingsScroll); // scrollable: don't force the window taller than the screen
|
||||
addDockWidget(Qt::LeftDockWidgetArea, settingsDock);
|
||||
menuBar->AddDockEntry(settingsDock, "Settings");
|
||||
|
||||
@@ -547,11 +552,13 @@ JFJochViewerWindow::JFJochViewerWindow(QWidget *parent, bool dbus, const QString
|
||||
addDockWidget(Qt::BottomDockWidgetArea, imageStripDock);
|
||||
if (lastDatasetInfoDock) {
|
||||
splitDockWidget(lastDatasetInfoDock, imageStripDock, Qt::Vertical);
|
||||
resizeDocks({lastDatasetInfoDock, imageStripDock}, {340, 180}, Qt::Vertical);
|
||||
resizeDocks({lastDatasetInfoDock, imageStripDock}, {260, 140}, Qt::Vertical);
|
||||
}
|
||||
menuBar->AddDockEntry(imageStripDock, "Image strip");
|
||||
connect(reading_worker, &JFJochImageReadingWorker::datasetLoaded,
|
||||
imageStrip, &JFJochViewerImageStrip::datasetLoaded);
|
||||
connect(reading_worker, &JFJochImageReadingWorker::fileOpened,
|
||||
imageStrip, &JFJochViewerImageStrip::resetForNewFile);
|
||||
connect(imageStrip, &JFJochViewerImageStrip::requestThumbnails,
|
||||
reading_worker, &JFJochImageReadingWorker::RenderThumbnails);
|
||||
connect(reading_worker, &JFJochImageReadingWorker::thumbnailReady,
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
JFJochSimpleChartView::JFJochSimpleChartView(QWidget *parent)
|
||||
: QChartView(new QChart(), parent) {
|
||||
chart()->legend()->hide();
|
||||
setFixedHeight(300);
|
||||
setMinimumHeight(120); // soft floor: let the host dock/window decide the height
|
||||
setRenderHint(QPainter::Antialiasing);
|
||||
setMouseTracking(true);
|
||||
//setRubberBand(QChartView::RubberBand::HorizontalRubberBand);
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
#include <QToolButton>
|
||||
#include <QLabel>
|
||||
#include <QPixmap>
|
||||
#include <QStyle>
|
||||
#include <QRandomGenerator>
|
||||
|
||||
#include <algorithm>
|
||||
#include <numeric>
|
||||
@@ -33,8 +35,15 @@ JFJochViewerImageStrip::JFJochViewerImageStrip(QWidget *parent) : QWidget(parent
|
||||
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(); });
|
||||
|
||||
auto *scroll = new QScrollArea(this);
|
||||
scroll->setWidgetResizable(true);
|
||||
@@ -52,8 +61,14 @@ JFJochViewerImageStrip::JFJochViewerImageStrip(QWidget *parent) : QWidget(parent
|
||||
}
|
||||
|
||||
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);
|
||||
Rebuild();
|
||||
}
|
||||
|
||||
void JFJochViewerImageStrip::resetForNewFile() {
|
||||
Rebuild(); // a new file/stream opened; dataset_ was just set by datasetLoaded
|
||||
}
|
||||
|
||||
void JFJochViewerImageStrip::thumbnailReady(qint64 image_number, QImage thumb) {
|
||||
@@ -71,29 +86,43 @@ QVector<qint64> JFJochViewerImageStrip::ComputeRepresentatives() const {
|
||||
return result;
|
||||
const int N = static_cast<int>(std::min<int64_t>(8, total));
|
||||
const int mode = mode_->currentIndex();
|
||||
const auto &sc = dataset_->spot_count;
|
||||
const auto &sci = dataset_->spot_count_indexed;
|
||||
auto *rng = QRandomGenerator::global();
|
||||
|
||||
auto evenly = [&] {
|
||||
for (int i = 0; i < N; ++i)
|
||||
result.push_back(N == 1 ? 0 : static_cast<int64_t>(i) * (total - 1) / (N - 1));
|
||||
// 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))]);
|
||||
}
|
||||
};
|
||||
|
||||
// Pick N images that span a metric's distribution (sort by value, sample evenly across it) —
|
||||
// a quick "histogram representative" selection.
|
||||
auto spread = [&](const std::vector<float> &metric) {
|
||||
// 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)});
|
||||
if (vals.empty()) { evenly(); return; }
|
||||
std::sort(vals.begin(), vals.end());
|
||||
const int n = static_cast<int>(std::min<size_t>(N, vals.size()));
|
||||
for (int i = 0; i < n; ++i)
|
||||
result.push_back(vals[n == 1 ? 0 : static_cast<size_t>(i) * (vals.size() - 1) / (n - 1)].second);
|
||||
std::vector<int64_t> idx;
|
||||
idx.reserve(vals.size());
|
||||
for (const auto &v : vals) idx.push_back(v.second);
|
||||
return idx;
|
||||
};
|
||||
|
||||
if (mode == 1 && !sc.empty()) { // most spots
|
||||
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());
|
||||
@@ -102,23 +131,17 @@ QVector<qint64> JFJochViewerImageStrip::ComputeRepresentatives() const {
|
||||
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, evenly sampled
|
||||
} 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));
|
||||
if (indexed.empty()) {
|
||||
evenly();
|
||||
} else {
|
||||
const int n = static_cast<int>(std::min<size_t>(N, indexed.size()));
|
||||
for (int i = 0; i < n; ++i)
|
||||
result.push_back(indexed[n == 1 ? 0 : static_cast<size_t>(i) * (indexed.size() - 1) / (n - 1)]);
|
||||
}
|
||||
} else if (mode == 3 && !dataset_->resolution_estimate.empty()) { // resolution spread
|
||||
spread(dataset_->resolution_estimate);
|
||||
} else if (mode == 4 && !dataset_->bkg_estimate.empty()) { // background spread
|
||||
spread(dataset_->bkg_estimate);
|
||||
} else {
|
||||
evenly();
|
||||
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;
|
||||
}
|
||||
@@ -140,7 +163,7 @@ void JFJochViewerImageStrip::Rebuild() {
|
||||
for (qint64 n : reps) {
|
||||
auto *btn = new QToolButton(this);
|
||||
btn->setToolButtonStyle(Qt::ToolButtonTextUnderIcon);
|
||||
btn->setIconSize(QSize(120, 120));
|
||||
btn->setIconSize(QSize(96, 96));
|
||||
btn->setText(QString::number(n + 1));
|
||||
btn->setAutoRaise(true);
|
||||
btn->setCursor(Qt::PointingHandCursor);
|
||||
|
||||
@@ -25,6 +25,7 @@ public:
|
||||
public slots:
|
||||
void datasetLoaded(std::shared_ptr<const JFJochReaderDataset> dataset);
|
||||
void thumbnailReady(qint64 image_number, QImage thumb);
|
||||
void resetForNewFile(); // a new file/stream opened: rebuild once, then stay put on live updates
|
||||
|
||||
signals:
|
||||
void requestThumbnails(QVector<qint64> image_numbers, bool show_spots);
|
||||
|
||||
Reference in New Issue
Block a user