Build Packages / Create release (push) Successful in 17s
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 7m35s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 8m55s
Build Packages / build:viewer-tgz:cpu (push) Successful in 10m40s
Build Packages / build:viewer-tgz:cuda (push) Successful in 12m1s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 15m35s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 16m9s
Build Packages / build:windows:nocuda (push) Successful in 17m10s
Build Packages / build:windows:cuda (push) Successful in 19m39s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 14m39s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 25m5s
Build Packages / build:rugnux:windows (push) Successful in 10m49s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 19m3s
Build Packages / Generate python client (push) Successful in 44s
Build Packages / Build documentation (push) Successful in 1m15s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 19m33s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 19m18s
Build Packages / build:rpm (rocky8) (push) Successful in 17m17s
Build Packages / build:rpm (rocky9) (push) Successful in 18m28s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 21m45s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 19m38s
Build Packages / Unit tests (push) Successful in 1h38m36s
The resolution-ring label was placed at one of four fixed azimuths with its corner on the ring line, in a hardcoded 16 pt Arial, in the ring's own magenta: it clipped at the viewport edge, ignored the font size, and vanished into busy patterns. It now walks the ring's cached trace, centres on the first point where the whole text fits in the viewport (failing that, clamps the nearest visible point inside), keeps a constant on-screen size of 1.2x the application font, and is cased like the spot markers so it reads on any colour map. In the same function visibleRect now maps viewport()->rect(), which is what mapToScene expects. The dataset plot's angle axis snaps its ticks to round angles - multiples of 90 degrees when the sweep is wide enough - instead of whatever angle the evenly-spaced image ticks landed on. The inspector prints the indexed cell as two aligned rows (alpha below a, beta below b, gamma below c), mosaicity with two decimals, and "No lattice" in grey, keeping red for actual trouble. The armed "Analyze image" button stays navy and is marked by a coral outline instead of turning into a differently-coloured button. The dataset navigation keys leave the image's keyPressEvent - the window-wide filter of the previous commit sees them first. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015vRwzF8PLaaa2novAKYorq
420 lines
17 KiB
C++
420 lines
17 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 "../../common/time_utc.h"
|
|
#include <QFormLayout>
|
|
#include <QRegularExpression>
|
|
|
|
// Two aligned rows - the lengths over their angles, so that alpha sits below a, beta below b,
|
|
// gamma below c. Qt's rich text does not inherit a surrounding colour into a table, so the
|
|
// colour is applied per cell.
|
|
static QString mkUnitCell(const UnitCell &uc, const char *color = nullptr) {
|
|
const QString style = color ? QString(" style=\"color: %1;\"").arg(color) : QString();
|
|
auto num = [&style](double v) {
|
|
return QString("<td align=\"right\"%1><b>%2</b> </td>")
|
|
.arg(style).arg(QString::number(v, 'f', 1));
|
|
};
|
|
auto unit = [&style](const char *u) {
|
|
return QString("<td%1>%2</td>").arg(style).arg(u);
|
|
};
|
|
return "<table cellspacing=\"0\" cellpadding=\"0\"><tr>"
|
|
+ num(uc.a) + num(uc.b) + num(uc.c) + unit("Å")
|
|
+ "</tr><tr>"
|
|
+ num(uc.alpha) + num(uc.beta) + num(uc.gamma) + unit("°")
|
|
+ "</tr></table>";
|
|
}
|
|
|
|
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);
|
|
return text;
|
|
}
|
|
|
|
JFJochViewerImageStatistics::JFJochViewerImageStatistics(QWidget *parent) : QWidget(parent) {
|
|
QFormLayout* layout = new QFormLayout(this);
|
|
|
|
// Order: what the dataset is, then how it was collected, then what this image contains.
|
|
dataset_name = new QLabel(this);
|
|
dataset_name->setWordWrap(true);
|
|
layout->addRow(new QLabel("Dataset:"), dataset_name);
|
|
|
|
collection_time = new QLabel(this);
|
|
collection_time->setWordWrap(true);
|
|
layout->addRow(new QLabel("Collection time:"), collection_time);
|
|
|
|
source_name = new QLabel(this);
|
|
layout->addRow("Source / Instrument:", source_name);
|
|
|
|
sample_name = new QLabel(this);
|
|
layout->addRow("Sample:", sample_name);
|
|
|
|
detector_name = new QLabel(this);
|
|
layout->addRow(new QLabel("Detector:"), detector_name);
|
|
|
|
detector_distance = new QLabel(this);
|
|
layout->addRow(new QLabel("Detector distance:"), detector_distance);
|
|
|
|
beam_center = new QLabel(this);
|
|
layout->addRow(new QLabel("Beam center:"), beam_center);
|
|
|
|
wavelength = new QLabel(this);
|
|
layout->addRow(new QLabel("Wavelength:"), wavelength);
|
|
|
|
exposure_time = new QLabel(this);
|
|
layout->addRow(new QLabel("Exposure Time:"), exposure_time);
|
|
|
|
rotation_angle_label = new QLabel(this);
|
|
rotation_angle = new QLabel(this);
|
|
layout->addRow(rotation_angle_label, rotation_angle);
|
|
|
|
// Sample-head angles that do not move during a run (an SLS Smargon writes chi and phi). Both
|
|
// labels stay empty for a file that carries none, so the row costs nothing where it means nothing.
|
|
smargon_label = new QLabel(this);
|
|
smargon_angles = new QLabel(this);
|
|
layout->addRow(smargon_label, smargon_angles);
|
|
|
|
valid_values = new QLabel(this);
|
|
layout->addRow(new QLabel("Valid values:"), valid_values);
|
|
|
|
masked_pixels = new QLabel(this);
|
|
layout->addRow(new QLabel("Masked pixels:"), masked_pixels);
|
|
|
|
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);
|
|
|
|
multiple_lattices = new QLabel(this);
|
|
layout->addRow(multiple_lattices);
|
|
|
|
res_estimate = new QLabel(this);
|
|
layout->addRow(new QLabel("Resolution estimate:"), res_estimate);
|
|
|
|
profile_radius_label = new QLabel("");
|
|
profile_radius = new QLabel(this);
|
|
layout->addRow(profile_radius_label, profile_radius);
|
|
}
|
|
|
|
QString TrimZeros(double number, int precision) {
|
|
auto s = QString::number(number, 'f', precision);
|
|
s.remove(QRegularExpression("\\.?0+$"));
|
|
if (s.isEmpty()) s = "0";
|
|
return s;
|
|
}
|
|
|
|
template <typename Duration>
|
|
QString FormatTime(Duration time, int precision = 6) {
|
|
return TrimZeros(std::chrono::duration<float>(time).count(), precision);
|
|
}
|
|
|
|
void JFJochViewerImageStatistics::loadImage(std::shared_ptr<const JFJochReaderImage> image) {
|
|
if (!image) {
|
|
source_name->setText("");
|
|
sample_name->setText("");
|
|
dataset_name->setText("");
|
|
collection_time->setText("");
|
|
detector_name->setText("");
|
|
detector_name->setToolTip("");
|
|
detector_distance->setText("");
|
|
beam_center->setText("");
|
|
wavelength->setText("");
|
|
wavelength->setToolTip("");
|
|
exposure_time->setText("");
|
|
exposure_time->setToolTip("");
|
|
rotation_angle->setText("");
|
|
rotation_angle->setToolTip("");
|
|
rotation_angle_label->setText("");
|
|
smargon_label->setText("");
|
|
smargon_angles->setText("");
|
|
valid_values->setText("");
|
|
valid_values->setToolTip("");
|
|
spots->setText("");
|
|
bkg_estimate->setText("");
|
|
indexed->setText("");
|
|
indexed->setToolTip("");
|
|
multiple_lattices->setText("");
|
|
profile_radius_label->setText("");
|
|
profile_radius->setText("");
|
|
profile_radius->setToolTip("");
|
|
res_estimate->setText("");
|
|
masked_pixels->setText("");
|
|
masked_pixels->setToolTip("");
|
|
return;
|
|
}
|
|
|
|
QString text;
|
|
|
|
auto &exp = image->Dataset().experiment;
|
|
|
|
// Word-wrap alone would break wherever it likes (mid-directory-name); a zero-width space after
|
|
// every "/" gives it a preferred break point there, so a long path wraps like a path and only
|
|
// falls back to breaking a single component if that component alone doesn't fit.
|
|
QString path = QString::fromStdString(exp.GetFilePrefix());
|
|
path.replace('/', QString("/") + QChar(0x200B)); // U+200B ZERO WIDTH SPACE, a break opportunity
|
|
text = QString("<b>%1</b>").arg(path);
|
|
dataset_name->setText(text);
|
|
|
|
const QString collected = QString::fromStdString(utc_to_local_human_readable(image->Dataset().arm_date));
|
|
collection_time->setText(collected.isEmpty() ? QString("-") : QString("<b>%1</b>").arg(collected));
|
|
|
|
detector_distance->setText(QString("<b>%1</b> mm").arg(QString::number(exp.GetDetectorDistance_mm(), 'f', 3)));
|
|
|
|
beam_center->setText(QString("<b>%1</b> <b>%2</b> pxl")
|
|
.arg(QString::number(exp.GetBeamX_pxl(), 'f', 2))
|
|
.arg(QString::number(exp.GetBeamY_pxl(), 'f', 2)));
|
|
|
|
wavelength->setText(QString("<b>%1</b> Å").arg(QString::number(exp.GetWavelength_A(), 'f', 4)));
|
|
wavelength->setToolTip(QString("Energy: <b>%1</b> eV")
|
|
.arg(QString::number(exp.GetIncidentEnergy_keV() * 1000.0, 'f', 0)));
|
|
|
|
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(QString("<b>%1</b>").arg(mkSourceInstrumentText(exp)));
|
|
source_name->setToolTip(mkSourceInstrumentTooltip(exp));
|
|
|
|
sample_name->setText(QString("<b>%1</b>").arg(mkSampleText(exp)));
|
|
sample_name->setToolTip(mkSampleTooltip(exp));
|
|
|
|
if (exp.GetGoniometer()) {
|
|
// An axis that is present but does not turn has no wedge to add: state the angle it stands at.
|
|
const float wedge = exp.GetGoniometer()->GetWedge_deg();
|
|
const QString angle = TrimZeros(exp.GetGoniometer()->GetAngle_deg(image->ImageData().number), 3);
|
|
rotation_angle_label->setText("Image angle:");
|
|
rotation_angle->setText(wedge == 0.0f
|
|
? QString("<b>%1°</b>").arg(angle)
|
|
: QString("<b>%1° + %2°</b>").arg(angle).arg(TrimZeros(wedge, 3)));
|
|
rotation_angle->setToolTip(QString("Start angle: <b>%1°</b><br/>Increment: <b>%2°</b>")
|
|
.arg(TrimZeros(exp.GetGoniometer()->GetStart_deg(), 3))
|
|
.arg(TrimZeros(exp.GetGoniometer()->GetIncrement_deg(), 3))
|
|
);
|
|
} else if (exp.GetGridScan()) {
|
|
rotation_angle_label->setText("Grid scan:");
|
|
rotation_angle->setText(QString("<b>%1</b> x <b>%2</b> μm")
|
|
.arg(QString::number(exp.GetGridScan()->GetGridStepX_um(), 'f', 1))
|
|
.arg(QString::number(exp.GetGridScan()->GetGridStepY_um(), 'f', 1)));
|
|
rotation_angle->setToolTip(QString("Grid size: <b>%1</b> x <b>%2</b> μm<br/>"
|
|
"Grid elements: <b>%3</b> x <b>%4</b>")
|
|
.arg(QString::number(exp.GetGridScan()->GetGridSizeX_um()))
|
|
.arg(QString::number(exp.GetGridScan()->GetGridSizeY_um()))
|
|
.arg(QString::number(exp.GetGridScan()->GetGridSizeX_step()))
|
|
.arg(QString::number(exp.GetGridScan()->GetGridSizeY_step())));
|
|
} else {
|
|
rotation_angle_label->setText("");
|
|
rotation_angle->setText(QString(""));
|
|
rotation_angle->setToolTip("");
|
|
}
|
|
|
|
if (const auto head = exp.GetDatasetSettings().GetSmargonPosition()) {
|
|
smargon_label->setText("Smargon:");
|
|
smargon_angles->setText(QString("chi <b>%1</b>° / phi <b>%2</b>°")
|
|
.arg(TrimZeros(head->chi_deg, 3))
|
|
.arg(TrimZeros(head->phi_deg, 3)));
|
|
} else {
|
|
smargon_label->setText("");
|
|
smargon_angles->setText("");
|
|
}
|
|
|
|
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)));
|
|
|
|
auto mm = image->ValidMinMax();
|
|
if (mm.has_value()) {
|
|
text = QString("<b>%1</b> - <b>%2</b>")
|
|
.arg(mm->first)
|
|
.arg(mm->second);
|
|
valid_values->setText(text);
|
|
} else {
|
|
valid_values->setText("N/A");
|
|
}
|
|
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 pr = image->ImageData().profile_radius;
|
|
auto mos = image->ImageData().mosaicity_deg;
|
|
if (mos && std::isfinite(mos.value())) {
|
|
text = QString("<b>%1</b>°").arg(QString::number(mos.value(), 'f', 2));
|
|
profile_radius_label->setText("Mosaicity:");
|
|
profile_radius->setText(text);
|
|
if (pr && std::isfinite(pr.value())) {
|
|
text = QString("Profile radius <b>%1</b> Å<sup>-1</sup>").arg(QString::number(pr.value(), 'f', 6));
|
|
profile_radius->setToolTip(text);
|
|
} else
|
|
profile_radius->setToolTip("");
|
|
} else if (pr && std::isfinite(pr.value())) {
|
|
profile_radius_label->setText("Profile radius:");
|
|
text = QString("<b>%1</b> Å<sup>-1</sup>").arg(QString::number(pr.value(), 'f', 6));
|
|
profile_radius->setText(text);
|
|
profile_radius->setToolTip("");
|
|
} else {
|
|
profile_radius_label->setText("");
|
|
profile_radius->setText("");
|
|
profile_radius->setToolTip("");
|
|
}
|
|
|
|
auto res = image->ImageData().resolution_estimate;
|
|
if (res && std::isfinite(res.value())) {
|
|
text = QString("<b>%1</b> Å").arg(QString::number(res.value(), 'f', 2));
|
|
res_estimate->setText(text);
|
|
} else {
|
|
res_estimate->setText("N/A");
|
|
}
|
|
|
|
float total_pixels = image->Dataset().experiment.GetPixelsNum();
|
|
if (total_pixels == 0)
|
|
total_pixels = 1;
|
|
|
|
auto mask_stats = image->Dataset().pixel_mask->GetStatistics();
|
|
text = QString("<b>%1</b>").arg(mask_stats.total_masked);
|
|
masked_pixels->setText(text);
|
|
masked_pixels->setToolTip(
|
|
QString(
|
|
"Error pixels: <b>%1</b> (%2 %)<br/> Noisy pixels: <b>%3</b> (%4 %) <br/> User mask: <b>%5</b> (%6 %)<br/>"
|
|
"Chip gaps: <b>%7</b> (%8 %)<br/>"
|
|
"Non active area (module gap and fill): <b>%9</b> (%10 %)")
|
|
.arg(mask_stats.error_pixel).arg(QString::number(mask_stats.error_pixel / total_pixels * 100.0, 'f', 1))
|
|
.arg(mask_stats.noisy_pixel).arg(QString::number(mask_stats.noisy_pixel / total_pixels * 100.0, 'f', 1))
|
|
.arg(mask_stats.user_mask).arg(QString::number(mask_stats.user_mask / total_pixels * 100.0, 'f', 1))
|
|
.arg(mask_stats.chip_gap_pixel).arg(QString::number(mask_stats.chip_gap_pixel / total_pixels * 100.0, 'f', 1))
|
|
.arg(mask_stats.module_gap_pixel).arg(QString::number(mask_stats.module_gap_pixel / total_pixels * 100.0, 'f', 1))
|
|
);
|
|
|
|
if (!image->Dataset().indexing_result.empty()) {
|
|
QString tooltip;
|
|
|
|
auto latt = image->ImageData().indexing_lattice;
|
|
if (latt) {
|
|
text = mkUnitCell(latt->GetUnitCell(), "purple");
|
|
|
|
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
|
|
// Not indexed is a normal state, not an error - grey, so red keeps meaning trouble.
|
|
text = QString("<span style=\"color: gray;\">No lattice</span>");
|
|
|
|
indexed->setToolTip(tooltip);
|
|
indexed->setText(text);
|
|
} else {
|
|
indexed->setText("N/A");
|
|
}
|
|
|
|
if (image->ImageData().indexing_lattice_count.value_or(0) > 1)
|
|
multiple_lattices->setText("<span style=\"color: red;\"><b>Multiple lattices detected</b></span>");
|
|
else
|
|
multiple_lattices->setText("");
|
|
}
|