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

338 lines
12 KiB
C++

// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include "JFJochViewerImageStatistics.h"
#include <QFormLayout>
#include <QRegularExpression>
static QString mkUnitCell(const UnitCell &uc) {
return QString("<b>%1</b> Å <b>%2</b> Å <b>%3</b> Å <b>%4</b>° <b>%5</b>° <b>%6</b>°")
.arg(QString::number(uc.a, 'f', 1))
.arg(QString::number(uc.b, 'f', 1))
.arg(QString::number(uc.c, 'f', 1))
.arg(QString::number(uc.alpha, 'f', 1))
.arg(QString::number(uc.beta, 'f', 1))
.arg(QString::number(uc.gamma, 'f', 1));
}
static QString mkSourceInstrumentText(const DiffractionExperiment& exp) {
QString src, inst;
const auto &m = exp.GetInstrumentMetadata();
if (!m.GetSourceName().empty()) src = QString::fromStdString(m.GetSourceName());
if (!m.GetInstrumentName().empty()) inst = QString::fromStdString(m.GetInstrumentName());
if (!src.isEmpty() && !inst.isEmpty()) return src + " / " + inst;
if (!src.isEmpty()) return src;
if (!inst.isEmpty()) return inst;
return QString("-");
}
static QString mkSourceInstrumentTooltip(const DiffractionExperiment &exp) {
QStringList tooltips;
if (exp.GetRingCurrent_mA().has_value()) {
tooltips << QString("Ring current: <b>%1</b> mA").arg(exp.GetRingCurrent_mA().value(), 0, 'f', 2);
}
if (exp.GetTotalFlux().has_value()) {
tooltips << QString("Total flux: <b>%1</b> ph/s").arg(exp.GetTotalFlux().value(), 0, 'e', 2);
}
if (exp.GetAttenuatorTransmission().has_value()) {
tooltips << QString("Attenuation: <b>%1</b>").arg(exp.GetAttenuatorTransmission().value(), 0, 'f', 3);
}
return tooltips.join("<br/>");
}
static QString mkSampleText(const DiffractionExperiment& exp) {
const auto& name = exp.GetSampleName();
return name.empty() ? QString("-") : QString::fromStdString(name);
}
static QString mkSampleTooltip(const DiffractionExperiment& exp) {
if (exp.GetSampleTemperature_K().has_value()) {
return QString("Sample temperature: <b>%1</b> K").arg(exp.GetSampleTemperature_K().value(), 0, 'f', 2);
}
return QString();
}
static QString mkSymmetry(const LatticeMessage& sym) {
QString text("Bravais lattice: <b>");
switch (sym.crystal_system) {
case gemmi::CrystalSystem::Triclinic:
text += "a";
break;
case gemmi::CrystalSystem::Monoclinic:
text += "m";
break;
case gemmi::CrystalSystem::Orthorhombic:
text += "o";
break;
case gemmi::CrystalSystem::Tetragonal:
text += "t";
break;
case gemmi::CrystalSystem::Hexagonal:
text += "h";
break;
case gemmi::CrystalSystem::Cubic:
text += "c";
break;
default: ;
}
text += QString(sym.centering) + "</b><br/>";
text += QString("Niggli class <b>%1</b><br/>").arg(sym.niggli_class);
text += QString("Primitive cell: %1<br/>").arg(mkUnitCell(sym.primitive.GetUnitCell()));
return text;
}
JFJochViewerImageStatistics::JFJochViewerImageStatistics(QWidget *parent) : QWidget(parent) {
QFormLayout* layout = new QFormLayout(this);
dataset_name = new QLabel(this);
layout->addRow(new QLabel("Dataset:"), dataset_name);
detector_name = new QLabel(this);
layout->addRow(new QLabel("Detector:"), detector_name);
source_name = new QLabel(this);
layout->addRow("Source / Instrument:", source_name);
sample_name = new QLabel(this);
layout->addRow("Sample:", sample_name);
exposure_time = new QLabel(this);
layout->addRow(new QLabel("Exposure Time:"), exposure_time);
rotation_angle = new QLabel(this);
layout->addRow(new QLabel("Image angle:"), rotation_angle);
valid_values = new QLabel(this);
layout->addRow(new QLabel("Valid values:"), valid_values);
spots = new QLabel(this);
layout->addRow(new QLabel("Spots:"), spots);
bkg_estimate = new QLabel(this);
layout->addRow(new QLabel("Background:"), bkg_estimate);
indexed = new QLabel(this);
layout->addRow(new QLabel("Indexing:"), indexed);
res_estimate = new QLabel(this);
layout->addRow(new QLabel("Resolution estimate:"), res_estimate);
profile_radius = new QLabel(this);
layout->addRow(new QLabel("Profile radius:"), profile_radius);
b_factor = new QLabel(this);
layout->addRow(new QLabel("B-factor:"), b_factor);
roi_sum = new QLabel(this);
layout->addRow(new QLabel("ROI sum:"), roi_sum);
roi_mean = new QLabel(this);
layout->addRow(new QLabel("ROI mean:"), roi_mean);
roi_stddev = new QLabel(this);
layout->addRow(new QLabel("ROI st. dev.:"), roi_stddev);
roi_max = new QLabel(this);
layout->addRow(new QLabel("ROI max:"), roi_max);
}
QString TrimZeros(double number, int precision) {
auto s = QString::number(number, 'f', precision);
s.remove(QRegularExpression("\\.?0+$"));
if (s.isEmpty()) s = "0";
return s;
}
QString FormatTime(std::chrono::microseconds time) {
return TrimZeros(time.count()/1e6, 6);
}
void JFJochViewerImageStatistics::loadImage(std::shared_ptr<const JFJochReaderImage> image) {
if (!image) {
source_name->setText("");
sample_name->setText("");
dataset_name->setText("");
dataset_name->setToolTip("");
detector_name->setText("");
detector_name->setToolTip("");
exposure_time->setText("");
exposure_time->setToolTip("");
rotation_angle->setText("");
rotation_angle->setToolTip("");
valid_values->setText("");
valid_values->setToolTip("");
spots->setText("");
bkg_estimate->setText("");
indexed->setText("");
indexed->setToolTip("");
roi_sum->setText("");
roi_mean->setText("");
roi_stddev->setText("");
roi_max->setText("");
b_factor->setText("");
profile_radius->setText("");
res_estimate->setText("");
return;
}
QString text;
auto &exp = image->Dataset().experiment;
text = QString("<b>%1</b>").arg(QString::fromStdString(exp.GetFilePrefix()));
dataset_name->setText(text);
dataset_name->setToolTip(QString("Beam center: <b>%1</b> <b>%2</b> pxl<br/>Detector distance: <b>%3</b> mm<br/>Energy: <b>%4</b> eV<br/>Wavelength: <b>%5</b> Å")
.arg(QString::number(exp.GetBeamX_pxl(), 'f', 2))
.arg(QString::number(exp.GetBeamY_pxl(), 'f', 2))
.arg(QString::number(exp.GetDetectorDistance_mm(), 'f', 3))
.arg(QString::number(exp.GetIncidentEnergy_keV() * 1000.0, 'f', 0))
.arg(QString::number(exp.GetWavelength_A(), 'f', 3)));
text = QString("<b>%1</b>").arg(QString::fromStdString(exp.GetDetectorDescription()));
detector_name->setToolTip(QString("Width: <b>%1</b> pxl (<b>%2</b> mm)<br/>Height: <b>%3</b> pxl (<b>%4</b> mm)<br/>Pixel size: <b>%5</b> μm")
.arg(QString::number(exp.GetXPixelsNum()))
.arg(QString::number(exp.GetXPixelsNum() * exp.GetPixelSize_mm(), 'f', 3))
.arg(QString::number(exp.GetYPixelsNum()))
.arg(QString::number(exp.GetYPixelsNum() * exp.GetPixelSize_mm(), 'f', 3))
.arg(QString::number(exp.GetPixelSize_mm() * 1000.0, 'f', 0)));
detector_name->setText(text);
source_name->setText(mkSourceInstrumentText(exp));
source_name->setToolTip(mkSourceInstrumentTooltip(exp));
sample_name->setText(mkSampleText(exp));
sample_name->setToolTip(mkSampleTooltip(exp));
if (exp.GetGoniometer()) {
rotation_angle->setText(QString("<b>%1°</b>")
.arg(TrimZeros(exp.GetGoniometer()->GetIncrement_deg(), 3)));
rotation_angle->setToolTip(QString("Start angle: <b>%1°</b><br/>This image: <b>%2°</b>")
.arg(TrimZeros(exp.GetGoniometer()->GetStart_deg(), 3))
.arg(TrimZeros(exp.GetGoniometer()->GetAngle_deg(image->ImageData().number), 3))
);
} else {
rotation_angle->setText(QString("-"));
rotation_angle->setToolTip("");
}
exposure_time->setText(QString("<b>%1</b> s").arg(FormatTime(exp.GetImageTime())));
exposure_time->setToolTip(QString("Count time: <b>%1</b> s<br/>").arg(FormatTime(exp.GetImageCountTime())));
text = QString("<b>%1</b>").arg(image->ImageData().spots.size());
spots->setText(text);
if (image->ImageData().spot_count.has_value())
spots->setToolTip(QString("Unfiltered (total): <b>%1</b> <br/>Low resolution: <b>%2</b><br/>Ice ring: <b>%3</b><br/>Indexed <b>%4</b><br/>")
.arg(image->ImageData().spot_count.value())
.arg(image->ImageData().spot_count_low_res.value_or(0))
.arg(image->ImageData().spot_count_ice_rings.value_or(0))
.arg(image->ImageData().spot_count_indexed.value_or(0)));
text = QString("<b>%1</b> - <b>%2</b>")
.arg(image->ValidPixels().begin()->first)
.arg(image->ValidPixels().rbegin()->first);
valid_values->setText(text);
valid_values->setToolTip(QString("Error pixels: <b>%1</b><br/>Saturated pixels: <b>%2</b>")
.arg(image->ErrorPixels().size()).arg(image->SaturatedPixels().size()));
if (!image->Dataset().bkg_estimate.empty()) {
text = QString("<b>%1</b>").arg(image->ImageData().bkg_estimate.value_or(0));
bkg_estimate->setText(text);
} else {
bkg_estimate->setText("N/A");
}
auto mos = image->ImageData().profile_radius;
if (mos) {
text = QString("<b>%1</b> Å<sup>-1</sup>").arg(QString::number(mos.value(), 'f', 6));
profile_radius->setText(text);
} else {
profile_radius->setText("N/A");
}
auto b = image->ImageData().b_factor;
if (b) {
text = QString("<b>%1</b> Å<sup>2</sup>").arg(QString::number(b.value(), 'f', 2));
b_factor->setText(text);
} else {
b_factor->setText("N/A");
}
auto res = image->ImageData().resolution_estimate;
if (res) {
text = QString("<b>%1</b> Å").arg(QString::number(res.value(), 'f', 2));
res_estimate->setText(text);
} else {
res_estimate->setText("N/A");
}
if (!image->Dataset().indexing_result.empty()) {
QString tooltip;
auto latt = image->ImageData().indexing_lattice;
if (latt) {
text = QString("<span style=\"color: purple;\">%1</span>")
.arg(mkUnitCell(latt->GetUnitCell()));
auto vec0 = latt->Vec0();
auto vec1 = latt->Vec1();
auto vec2 = latt->Vec2();
tooltip = QString("<b>Lattice vectors (Å):</b><br/>"
"a = (%1, %2, %3)<br/>"
"b = (%4, %5, %6)<br/>"
"c = (%7, %8, %9)")
.arg(vec0.x, 7, 'f', 1)
.arg(vec0.y, 7, 'f', 1)
.arg(vec0.z, 7, 'f', 1)
.arg(vec1.x, 7, 'f', 1)
.arg(vec1.y, 7, 'f', 1)
.arg(vec1.z, 7, 'f', 1)
.arg(vec2.x, 7, 'f', 1)
.arg(vec2.y, 7, 'f', 1)
.arg(vec2.z, 7, 'f', 1);
if (image->ImageData().lattice_type) {
tooltip += QString("<br/><br/>");
tooltip += mkSymmetry(*image->ImageData().lattice_type);
} else if (image->Dataset().experiment.GetSpaceGroupNumber()) {
tooltip += QString("<br/><br/>Space group (user-provided): <b>%1</b>")
.arg(QString::fromStdString(image->Dataset().experiment.GetSpaceGroupName()));
}
} else
text = QString("<span style=\"color: red;\"><b>No lattice</b></span>");
indexed->setToolTip(tooltip);
indexed->setText(text);
} else {
indexed->setText("N/A");
}
auto roi = image->GetROI();
if (roi && roi->pixels > 0) {
text = QString("<b>%1</b>").arg(roi->sum);
roi_sum->setText(text);
auto roi_npixel = static_cast<double>(roi->pixels);
double roi_mean_val = static_cast<double>(roi->sum) / roi_npixel;
text = QString("<b>%1</b>").arg(QString::number(roi_mean_val, 'f', 3));
roi_mean->setText(text);
double variance = static_cast<double>(roi->sum_square) / roi_npixel - roi_mean_val * roi_mean_val;
text = QString("<b>%1</b>").arg(QString::number(sqrt(variance), 'f', 3));
roi_stddev->setText(text);
text = QString("<b>%1</b>").arg(roi->max_count);
roi_max->setText(text);
} else {
roi_sum->setText("N/A");
roi_mean->setText("N/A");
roi_stddev->setText("N/A");
roi_max->setText("N/A");
}
}