rugnux finds the beam stop and its holder in a projection of 60 images and marks them in the pixel mask as bit 9 (--detect-beam-stop[=N|off], on by default). Reflections behind the stop are attenuated but not flagged, so they integrate low with a plausible sigma and nothing downstream catches them: the signal-box gate requires 100% valid pixels and shadow pixels are valid, the background clip is high-side only, and the |zeta| cut applies only to the space-group search merge. The detection compares each pixel's background against the typical background at the same radius on two channels. An azimuthal one (the ring median) finds the holder arm, which is a minority of its ring; a radial one (the background just outside) finds the disk, which the ring median cannot see because inside a fully blocked ring the median is the shadow itself. Pixels are pooled over a 5x5 box and tested only where the background has actually been counted, so low-background data no longer masks the whole detector. Recorded reflections are carved back out - a beam stop cannot block a reflection that was measured. Bit 9 belongs to the run that found it, not to the dataset: it is cleared when a run starts, so a mask read back from a file that carries one starts clear. The user mask (bit 8) is left alone. Scaling and merging gain a low-resolution limit, default 50 A (--scaling-low-resolution <num>, 0 removes it), applied per observation before scaling so it also protects the per-frame scale fit and the space-group search. 50 A is the value XDS configurations use; rugnux_vs_xds.py now matches both of XDS's resolution limits instead of only the high one, so the lowest shell is the same shell in the two programs. The viewer draws the detected shadow in coral with a "Show beam stop" switch in the side panel, exposes the low-resolution limit in the settings dock, and offers detection in its processing jobs. Adding an image marker meant giving the reader a MIN_REAL_PXL_VALUE, because several places classify a pixel by range rather than by equality and would otherwise read the new marker as a very negative intensity. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
223 lines
9.5 KiB
C++
223 lines
9.5 KiB
C++
// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#include <QHBoxLayout>
|
|
#include <QPushButton>
|
|
#include <QColorDialog>
|
|
#include <QGridLayout>
|
|
|
|
#include "JFJochViewerSidePanel.h"
|
|
|
|
#include "widgets/TitleLabel.h"
|
|
#include "widgets/CollapsibleSection.h"
|
|
#include "widgets/JFJochViewerImageStatistics.h"
|
|
|
|
namespace {
|
|
// A small square filled with the given colour, used as a legend/key swatch.
|
|
QLabel *MakeColorSwatch(QWidget *parent, const QColor &color) {
|
|
auto *swatch = new QLabel(parent);
|
|
swatch->setFixedSize(13, 13);
|
|
swatch->setStyleSheet(QString("background-color: %1; border: 1px solid #666;").arg(color.name()));
|
|
return swatch;
|
|
}
|
|
}
|
|
|
|
JFJochViewerSidePanel::JFJochViewerSidePanel(QWidget *parent) : QWidget(parent) {
|
|
|
|
auto layout = new QVBoxLayout(this);
|
|
|
|
layout->addWidget(new TitleLabel("Image statistics", this));
|
|
|
|
auto stats = new JFJochViewerImageStatistics(this);
|
|
layout->addWidget(stats);
|
|
connect(this, &JFJochViewerSidePanel::imageLoaded, stats, &JFJochViewerImageStatistics::loadImage);
|
|
|
|
auto *featuresSection = new CollapsibleSection("Image features", this);
|
|
|
|
auto spotToggleCheckBox = new QCheckBox("Show spots", this);
|
|
spotToggleCheckBox->setCheckState(Qt::CheckState::Unchecked);
|
|
|
|
connect(spotToggleCheckBox, &QCheckBox::toggled, this, &JFJochViewerSidePanel::spotsToggled);
|
|
|
|
auto highlightIceRingToggleCheckBox = new QCheckBox("Highlight spots on ice rings", this);
|
|
highlightIceRingToggleCheckBox->setCheckState(Qt::CheckState::Checked);
|
|
connect(highlightIceRingToggleCheckBox, &QCheckBox::toggled, this,&JFJochViewerSidePanel::highlightIceRingsToggled);
|
|
|
|
auto predictionsToggleCheckBox = new QCheckBox("Show predictions", this);
|
|
predictionsToggleCheckBox->setCheckState(Qt::CheckState::Unchecked);
|
|
|
|
connect(predictionsToggleCheckBox, &QCheckBox::toggled, this,
|
|
&JFJochViewerSidePanel::predictionsToggled);
|
|
|
|
auto highestPixelsComboBox = new QComboBox(this);
|
|
highestPixelsComboBox->addItem("Show 0 highest pixels", 0);
|
|
highestPixelsComboBox->addItem("Show 1 highest pixel", 1);
|
|
highestPixelsComboBox->addItem("Show 2 highest pixels", 2);
|
|
highestPixelsComboBox->addItem("Show 3 highest pixels", 3);
|
|
highestPixelsComboBox->addItem("Show 5 highest pixels", 5);
|
|
highestPixelsComboBox->addItem("Show 10 highest pixels", 10);
|
|
highestPixelsComboBox->setCurrentIndex(0);
|
|
|
|
connect(highestPixelsComboBox, &QComboBox::currentIndexChanged, this, [this, highestPixelsComboBox](int index) {
|
|
int value = highestPixelsComboBox->itemData(index).toInt();
|
|
emit showHighestPixels(value);
|
|
});
|
|
|
|
auto saturatedPixelsCheckBox = new QCheckBox("Show saturated pixels", this);
|
|
|
|
connect(saturatedPixelsCheckBox, &QCheckBox::toggled, this, &JFJochViewerSidePanel::saturatedPixelsToggled);
|
|
|
|
auto beamStopCheckBox = new QCheckBox("Show beam stop", this);
|
|
beamStopCheckBox->setChecked(true);
|
|
beamStopCheckBox->setToolTip("Draw the detected beam-stop shadow (pixel mask bit 9) in coral. "
|
|
"Unchecked, it is drawn like any other masked pixel.");
|
|
connect(beamStopCheckBox, &QCheckBox::toggled, this, &JFJochViewerSidePanel::showBeamStop);
|
|
|
|
auto colorSelectButton = new QPushButton("Select feature color", this);
|
|
|
|
connect(colorSelectButton, &QPushButton::clicked, this, [this]() {
|
|
QColor color = QColorDialog::getColor(Qt::magenta, this, "Select Feature Color");
|
|
if (color.isValid()) {
|
|
emit setFeatureColor(color);
|
|
if (legend_indexed_swatch_)
|
|
legend_indexed_swatch_->setStyleSheet(
|
|
QString("background-color: %1; border: 1px solid #666;").arg(color.name()));
|
|
}
|
|
});
|
|
|
|
auto spotColorSelectButton = new QPushButton("Select spot color", this);
|
|
|
|
connect(spotColorSelectButton, &QPushButton::clicked, this, [this]() {
|
|
QColor color = QColorDialog::getColor(Qt::green, this, "Select Spot Color");
|
|
if (color.isValid()) {
|
|
emit setSpotColor(color);
|
|
if (legend_spot_swatch_)
|
|
legend_spot_swatch_->setStyleSheet(
|
|
QString("background-color: %1; border: 1px solid #666;").arg(color.name()));
|
|
}
|
|
});
|
|
|
|
auto image_feature_grid = new QGridLayout();
|
|
image_feature_grid->addWidget(spotToggleCheckBox, 0, 0);
|
|
image_feature_grid->addWidget(highlightIceRingToggleCheckBox, 0, 1);
|
|
image_feature_grid->addWidget(predictionsToggleCheckBox, 1, 0);
|
|
image_feature_grid->addWidget(beamStopCheckBox, 1, 1);
|
|
|
|
image_feature_grid->addWidget(saturatedPixelsCheckBox, 2, 0);
|
|
image_feature_grid->addWidget(highestPixelsComboBox, 2, 1);
|
|
image_feature_grid->addWidget(colorSelectButton, 3, 0);
|
|
image_feature_grid->addWidget(spotColorSelectButton, 3, 1);
|
|
|
|
// Legend: what each overlay colour means (matches JFJochDiffractionImage draw colours).
|
|
legend_spot_swatch_ = MakeColorSwatch(this, Qt::green);
|
|
legend_indexed_swatch_ = MakeColorSwatch(this, Qt::magenta);
|
|
auto *legendLayout = new QGridLayout();
|
|
legendLayout->setContentsMargins(0, 4, 0, 0);
|
|
legendLayout->setHorizontalSpacing(6);
|
|
legendLayout->setVerticalSpacing(2);
|
|
struct LegendRow { QLabel *swatch; const char *text; };
|
|
const LegendRow legend_rows[] = {
|
|
{legend_spot_swatch_, "Spots (not indexed)"},
|
|
{legend_indexed_swatch_, "Indexed spots"},
|
|
{MakeColorSwatch(this, Qt::cyan), "Spots on ice ring"},
|
|
{MakeColorSwatch(this, QColor(0xFA, 0x72, 0x68)), "Secondary lattice"},
|
|
{MakeColorSwatch(this, Qt::darkRed), "Predictions"},
|
|
};
|
|
int legend_row = 0;
|
|
for (const auto &r : legend_rows) {
|
|
legendLayout->addWidget(r.swatch, legend_row, 0, Qt::AlignVCenter);
|
|
legendLayout->addWidget(new QLabel(tr(r.text), this), legend_row, 1);
|
|
legend_row++;
|
|
}
|
|
legendLayout->setColumnStretch(1, 1);
|
|
image_feature_grid->addWidget(new QLabel(tr("Legend"), this), 4, 0, 1, 2);
|
|
image_feature_grid->addLayout(legendLayout, 5, 0, 1, 2);
|
|
|
|
featuresSection->setContentLayout(image_feature_grid);
|
|
featuresSection->setExpanded(false);
|
|
layout->addWidget(featuresSection);
|
|
|
|
auto *ringsSection = new CollapsibleSection("Resolution rings", this);
|
|
auto *ringsLayout = new QVBoxLayout();
|
|
ringsLayout->setContentsMargins(0, 0, 0, 0);
|
|
res_rings = new ResolutionRingWidget(this);
|
|
|
|
connect(res_rings, &ResolutionRingWidget::resRingsSet, [this] (const QVector<float> &v) {
|
|
emit resRingsSet(v);
|
|
});
|
|
|
|
connect(res_rings, &ResolutionRingWidget::ringModeSet, [this] (JFJochDiffractionImage::RingMode mode) {
|
|
emit ringModeSet(mode);
|
|
});
|
|
|
|
ringsLayout->addWidget(res_rings);
|
|
ringsSection->setContentLayout(ringsLayout);
|
|
ringsSection->setExpanded(false);
|
|
layout->addWidget(ringsSection);
|
|
|
|
// The per-image statistics plot now lives in the dataset-info dock (its "Per-image" toggle),
|
|
// and powder calibration / full analysis live in the settings dock and the hero buttons,
|
|
// so they are no longer duplicated here.
|
|
|
|
roiSection = new CollapsibleSection("ROI", this);
|
|
auto *roiLayout = new QVBoxLayout();
|
|
roiLayout->setContentsMargins(0, 0, 0, 0);
|
|
|
|
auto roiLabelsCheckBox = new QCheckBox("Show labels", this);
|
|
roiLabelsCheckBox->setCheckState(Qt::CheckState::Unchecked);
|
|
connect(roiLabelsCheckBox, &QCheckBox::toggled, this, &JFJochViewerSidePanel::showROILabels);
|
|
|
|
auto roiFillCheckBox = new QCheckBox("Translucent fill", this);
|
|
roiFillCheckBox->setCheckState(Qt::CheckState::Unchecked);
|
|
connect(roiFillCheckBox, &QCheckBox::toggled, this, &JFJochViewerSidePanel::showROIFill);
|
|
|
|
auto roiToggleRow = new QHBoxLayout();
|
|
roiToggleRow->addWidget(roiLabelsCheckBox);
|
|
roiToggleRow->addWidget(roiFillCheckBox);
|
|
roiLayout->addLayout(roiToggleRow);
|
|
|
|
roi_list = new JFJochViewerROIList(this);
|
|
roiLayout->addWidget(roi_list);
|
|
connect(this, &JFJochViewerSidePanel::imageLoaded, roi_list, &JFJochViewerROIList::loadImage);
|
|
connect(roi_list, &JFJochViewerROIList::roisChanged, this, &JFJochViewerSidePanel::roisChanged);
|
|
connect(roi_list, &JFJochViewerROIList::selectedROIChanged, this, &JFJochViewerSidePanel::selectedROIChanged);
|
|
connect(roi_list, &JFJochViewerROIList::downloadROIs, this, &JFJochViewerSidePanel::downloadROIs);
|
|
connect(roi_list, &JFJochViewerROIList::uploadROIs, this, &JFJochViewerSidePanel::uploadROIs);
|
|
connect(roi_list, &JFJochViewerROIList::maskFromROI, this, &JFJochViewerSidePanel::maskFromROI);
|
|
connect(roi_list, &JFJochViewerROIList::roisChanged, this, [this] { roiSection->setExpanded(true); });
|
|
roiSection->setContentLayout(roiLayout);
|
|
roiSection->setExpanded(false);
|
|
layout->addWidget(roiSection);
|
|
|
|
layout->addStretch();
|
|
setLayout(layout); // Set the layout to the widget
|
|
}
|
|
|
|
void JFJochViewerSidePanel::spotsToggled(bool input) {
|
|
emit showSpots(input);
|
|
}
|
|
|
|
void JFJochViewerSidePanel::predictionsToggled(bool input) {
|
|
emit showPredictions(input);
|
|
}
|
|
|
|
void JFJochViewerSidePanel::loadImage(std::shared_ptr<const JFJochReaderImage> image) {
|
|
emit imageLoaded(image);
|
|
}
|
|
|
|
void JFJochViewerSidePanel::saturatedPixelsToggled(bool input) {
|
|
emit showSaturatedPixels(input);
|
|
}
|
|
|
|
void JFJochViewerSidePanel::highlightIceRingsToggled(bool input) {
|
|
emit highlightIceRings(input);
|
|
}
|
|
|
|
void JFJochViewerSidePanel::selectROIInList(QString name) {
|
|
roi_list->setSelected(name);
|
|
}
|
|
|
|
void JFJochViewerSidePanel::SetRings(QVector<float> v) {
|
|
res_rings->setRings(v);
|
|
}
|