Build Packages / Create release (push) Successful in 59s
Build Packages / build:viewer-tgz:cpu (push) Successful in 11m44s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 9m55s
Build Packages / build:viewer-tgz:cuda (push) Successful in 11m51s
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 5m3s
Build Packages / build:windows:nocuda (push) Successful in 16m58s
Build Packages / build:windows:cuda (push) Successful in 19m23s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 23m30s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 15m12s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 14m46s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 15m2s
Build Packages / build:rugnux:windows (push) Successful in 10m52s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 16m16s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 14m53s
Build Packages / Generate python client (push) Successful in 33s
Build Packages / Build documentation (push) Successful in 1m18s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 16m27s
Build Packages / build:rpm (rocky8) (push) Successful in 16m46s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 16m47s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 15m58s
Build Packages / build:rpm (rocky9) (push) Successful in 17m35s
Build Packages / Unit tests (push) Successful in 1h14m23s
The cased white label read as an outline artefact, and its scene-space font - scaled by 1/zoom to hold an apparent size - broke down at high magnification, where a sub-point font rasterises into a white block. The label is now drawn in device coordinates (ItemIgnoresTransformations) at 1.6x the application font, bold, with no outline, so it is the same size at every zoom by construction. The colour is picked from what ends up under it: near-black normally - the label usually sits outside the detector image, on the canvas that is white under every colour map - and white only over the image of a dark map. Not the ring's magenta: where the label meets its own dashes the two blend and the glyphs dissolve. The unit-cell rows pad every number to three integer digits with figure spaces, so 95.0 and 102.3 occupy the same width and the columns do not shift as values change magnitude; no axis reaches 1000 A and no angle 1000 degrees. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015vRwzF8PLaaa2novAKYorq
425 lines
18 KiB
C++
425 lines
18 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) {
|
|
// Padded to three integer digits with figure spaces (digit-wide), so the columns hold
|
|
// still as values change magnitude; no axis reaches 1000 Å and no angle 1000°.
|
|
QString s = QString::number(v, 'f', 1);
|
|
while (s.size() < 5)
|
|
s.prepend(QChar(0x2007));
|
|
return QString("<td align=\"right\"%1><b>%2</b> </td>")
|
|
.arg(style).arg(s);
|
|
};
|
|
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("");
|
|
}
|