Merge branch 'worktree-agent-ae76aa0c66b33a4db' into grid-scan-integration

This commit is contained in:
2026-09-08 07:55:31 +02:00
8 changed files with 464 additions and 17 deletions
+1
View File
@@ -113,6 +113,7 @@ ADD_EXECUTABLE(jfjoch_test
LatticeReductionTest.cpp
BeamCenterFromBackgroundTest.cpp
BeamCenterFromSpotsTest.cpp
GridScanCompositeTest.cpp
)
target_link_libraries(jfjoch_test Catch2WithMain JFJochBroker JFJochReceiver JFJochReader JFJochStreamWriter
+122
View File
@@ -0,0 +1,122 @@
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include "../viewer/image_viewer/GridScanComposite.h"
using namespace grid_scan;
TEST_CASE("GridScanComposite_Background", "[grid_scan]") {
// Air only: a flat level with counting noise on it. Without a floor on the range this would
// be stretched to fill the whole grey scale and look like a loop.
std::vector<float> air;
for (int i = 0; i < 100; i++)
air.push_back(5.0f + 0.02f * ((i % 7) - 3));
const auto flat = NormaliseBackground(air);
for (float v : flat)
CHECK(v < 0.1f);
// A loop in the middle of the same air.
std::vector<float> loop = air;
for (int i = 40; i < 60; i++)
loop[i] = 13.0f;
const auto mapped = NormaliseBackground(loop);
CHECK(mapped[0] < 0.1f);
CHECK(mapped[50] > 0.9f);
}
TEST_CASE("GridScanComposite_BackgroundKeepsGaps", "[grid_scan]") {
const std::vector<float> bkg{5.0f, std::numeric_limits<float>::quiet_NaN(), 13.0f};
const auto out = NormaliseBackground(bkg);
REQUIRE(out.size() == 3);
CHECK(std::isnan(out[1]));
CHECK(std::isfinite(out[0]));
CHECK(std::isfinite(out[2]));
}
TEST_CASE("GridScanComposite_Color", "[grid_scan]") {
// Nothing anywhere: the pale plate.
const rgb air = CompositeColor(0.0f, 0.0f, 0.0f);
CHECK(air.r == kPlate.r);
CHECK(air.g == kPlate.g);
CHECK(air.b == kPlate.b);
// Material but no diffraction: the loop grey.
const rgb loop = CompositeColor(1.0f, 0.0f, 0.0f);
CHECK(loop.r == kLoop.r);
CHECK(loop.b == kLoop.b);
// A saturated score paints its own colour whatever the background says.
const rgb protein = CompositeColor(1.0f, 1.0f, 0.0f);
CHECK(protein.r == kProtein.r);
CHECK(protein.g == kProtein.g);
CHECK(protein.b == kProtein.b);
const rgb ice = CompositeColor(0.0f, 0.0f, 1.0f);
CHECK(ice.r == kIce.r);
CHECK(ice.b == kIce.b);
// Equal parts of both sit half way between the two, not on either.
const rgb both = CompositeColor(1.0f, 0.5f, 0.5f);
CHECK(both.r == static_cast<uint8_t>(std::lround(0.5f * (kProtein.r + kIce.r))));
CHECK(both.b == static_cast<uint8_t>(std::lround(0.5f * (kProtein.b + kIce.b))));
// Protein three times ice: still mostly orange, but on the way to cyan.
const rgb mostly_protein = CompositeColor(1.0f, 0.75f, 0.25f);
CHECK(mostly_protein.b > protein.b);
CHECK(mostly_protein.b < both.b);
// A weak score is a weak tint over the base, not a full-strength colour.
const rgb faint = CompositeColor(1.0f, 0.1f, 0.0f);
CHECK(faint.r > loop.r);
CHECK(faint.b < loop.b);
CHECK(faint.b > kProtein.b);
}
TEST_CASE("GridScanComposite_FrameGeometry", "[grid_scan]") {
// Steps differ, as they do in a real raster: 20 um across, 16 um down.
constexpr float step_x = 20.0f;
constexpr float step_y = 16.0f;
// Along the grid x axis: 100 um is 5 cells across, 10 um is 0.625 cells down.
const auto along_x = CrystalFrameCorners(4.0f, 3.0f, 100.0f, 10.0f, 0.0f, step_x, step_y);
CHECK_THAT(along_x[0].x, Catch::Matchers::WithinAbs(4.5 + 2.5, 1e-4));
CHECK_THAT(along_x[0].y, Catch::Matchers::WithinAbs(3.5 + 0.3125, 1e-4));
CHECK_THAT(along_x[2].x, Catch::Matchers::WithinAbs(4.5 - 2.5, 1e-4));
// The same needle at 90 degrees: the long axis is now along y, and because the y step is the
// finer one it covers more cells than it did across - 100/16 rather than 100/20.
const auto along_y = CrystalFrameCorners(4.0f, 3.0f, 100.0f, 10.0f, 90.0f, step_x, step_y);
float min_x = along_y[0].x, max_x = along_y[0].x, min_y = along_y[0].y, max_y = along_y[0].y;
for (const auto &c : along_y) {
min_x = std::min(min_x, c.x);
max_x = std::max(max_x, c.x);
min_y = std::min(min_y, c.y);
max_y = std::max(max_y, c.y);
}
CHECK_THAT(max_x - min_x, Catch::Matchers::WithinAbs(10.0 / step_x, 1e-3));
CHECK_THAT(max_y - min_y, Catch::Matchers::WithinAbs(100.0 / step_y, 1e-3));
// At 45 degrees the frame is still a 100 x 10 needle - its axis-aligned bounding box is the
// ~78 x 78 blob the oriented frame exists to avoid drawing.
const auto tilted = CrystalFrameCorners(4.0f, 3.0f, 100.0f, 10.0f, 45.0f, step_x, step_x);
const float long_side = std::hypot(tilted[0].x - tilted[1].x, tilted[0].y - tilted[1].y);
const float short_side = std::hypot(tilted[1].x - tilted[2].x, tilted[1].y - tilted[2].y);
CHECK_THAT(long_side, Catch::Matchers::WithinAbs(100.0 / step_x, 1e-3));
CHECK_THAT(short_side, Catch::Matchers::WithinAbs(10.0 / step_x, 1e-3));
float bbox = 0;
for (const auto &c : tilted)
bbox = std::max(bbox, std::abs(c.x - 4.5f));
CHECK(bbox > 1.9f); // the bounding box is far wider than the needle itself
}
TEST_CASE("GridScanComposite_RoundBlob", "[grid_scan]") {
CHECK(IsRoundBlob(30.0f, 30.0f));
CHECK(IsRoundBlob(33.0f, 30.0f));
CHECK(!IsRoundBlob(100.0f, 10.0f));
CHECK(!IsRoundBlob(40.0f, 30.0f));
}
+2
View File
@@ -103,6 +103,8 @@ ADD_EXECUTABLE(jfjoch_viewer jfjoch_viewer.cpp JFJochViewerWindow.cpp JFJochView
widgets/ResolutionRingWidget.h
image_viewer/JFJochGridScanImage.cpp
image_viewer/JFJochGridScanImage.h
image_viewer/GridScanComposite.h
GridScanDataStub.h
widgets/PowderCalibrationWidget.cpp
widgets/PowderCalibrationWidget.h
${APP_RESOURCES}
+56
View File
@@ -0,0 +1,56 @@
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <vector>
#include "../reader/JFJochReaderDataset.h"
// ===========================================================================================
// STUB - DELETE THIS FILE AT INTEGRATION.
//
// The composite grid-scan map is drawn from the per-image protein and ice scores and from the
// crystal list of the whole raster. None of the three is on JFJochReaderDataset yet; they arrive
// with a separate change, through reader/. Until then this header is the one place in the viewer
// that names them, so the widget and the dataset-info panel are already written against the real
// shape of the data.
//
// At integration: drop this file and its two includes (image_viewer/JFJochGridScanImage.h and
// JFJochViewerDatasetInfo.cpp), take GridScanCrystal from the reader instead, and replace the three
// accessors below with `ds.protein_score`, `ds.ice_score` and `ds.grid_scan_result` at their call
// sites in JFJochViewerDatasetInfo.cpp. Nothing else has to change: both data paths (HTTP and HDF5)
// fill the same JFJochReaderDataset, so both light up at once.
// ===========================================================================================
// One crystal found in a grid scan. Position and extent are in the grid's own frame; the extent is
// along the crystal's own principal axes, which is why angle_deg is needed to draw it.
struct GridScanCrystal {
float nx = 0; // centre, fractional grid coordinates, 0-based
float ny = 0;
float x_um = 0; // centre, signed offset along the grid axes
float y_um = 0;
int64_t image_number = -1;
float major_um = 0; // extent along the crystal's own principal axes
float minor_um = 0;
float angle_deg = 0; // major axis from +x grid axis, counter-clockwise, in [0, 180)
float score = 0;
float ice_score = 0;
float res_A = 0;
int64_t n_images = 0;
};
inline const std::vector<float> &GridScanProteinScore(const JFJochReaderDataset &) {
static const std::vector<float> none;
return none;
}
inline const std::vector<float> &GridScanIceScore(const JFJochReaderDataset &) {
static const std::vector<float> none;
return none;
}
inline const std::vector<GridScanCrystal> &GridScanCrystals(const JFJochReaderDataset &) {
static const std::vector<GridScanCrystal> none;
return none;
}
+16 -1
View File
@@ -6,6 +6,7 @@
#include <QColor>
#include "JFJochViewerDatasetInfo.h"
#include "GridScanDataStub.h" // STUB - see the note in that file
namespace {
// Stable colour per run by its position in the run list (Original is index 0 -> blue), so a
@@ -20,6 +21,10 @@ namespace {
return palette[((index % n) + n) % n];
}
// Combo value of the composite grid-scan map. Well clear of the running metric numbers and
// below the ROI block (100 and up), so a metric added later cannot collide with it.
constexpr int kCompositeMetric = 50;
// A run's image->original-number map as floats for the chart x-axis (empty => identity).
std::vector<float> XForRun(const JFJochReaderDataset &ds) {
return {ds.source_image_number.begin(), ds.source_image_number.end()};
@@ -136,6 +141,10 @@ void JFJochViewerDatasetInfo::UpdateLabels() {
}
if (this->dataset) {
// First for a grid scan: the composite is what a raster is looked at for, and the whole
// point is that it comes up without anyone having to know to ask for it.
if (dataset->experiment.GetGridScan())
combo_box->addItem("Protein / ice map", kCompositeMetric);
combo_box->addItem("Background estimate", 0);
combo_box->addItem("Ice ring score", 14);
combo_box->addItem("Spindle blind fraction", 15);
@@ -239,6 +248,7 @@ std::vector<float> JFJochViewerDatasetInfo::ExtractMetric(const JFJochReaderData
else if (val == 13) data = ds.indexing_lattice_count;
else if (val == 14) data = ds.ice_ring_ratio;
else if (val == 15) data = ds.spindle_blind_fraction;
else if (val == kCompositeMetric) data = GridScanProteinScore(ds);
else if (val >= 100) {
const int roi_index = (val - 100) / 4;
if (val % 4 == 0) {
@@ -310,7 +320,12 @@ void JFJochViewerDatasetInfo::UpdatePlot() {
std::move(overlays), primary_color, XForRun(*dataset), full_range);
if (dataset->experiment.GetGridScan()) {
grid_scan_image->loadData(data, dataset->experiment.GetGridScan().value(), one_over_d2);
if (val == kCompositeMetric)
grid_scan_image->loadComposite(dataset->bkg_estimate, GridScanProteinScore(*dataset),
GridScanIceScore(*dataset), GridScanCrystals(*dataset),
dataset->experiment.GetGridScan().value());
else
grid_scan_image->loadData(data, dataset->experiment.GetGridScan().value(), one_over_d2);
grid_button->setEnabled(true);
if (!image_button->isChecked())
grid_button->setChecked(true);
+128
View File
@@ -0,0 +1,128 @@
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <algorithm>
#include <array>
#include <cmath>
#include <limits>
#include <vector>
#include "../../common/ColorScale.h"
#include "../../common/JFJochMath.h"
// Colouring and frame geometry of the composite grid-scan map. Kept free of Qt so it can be
// tested on its own; the widget (JFJochGridScanImage) only feeds it and draws the result.
namespace grid_scan {
// Cyan for ice and orange for protein are the viewer's own annotation colours - indices 0 and 2 of
// ROIAnnotationColor() in widgets/ROIColorPalette.h - repeated here as plain rgb rather than QColor.
// The two greys are the map's own: the pale one is where the beam saw nothing, the mid one is where
// the background estimate says there is material.
constexpr rgb kPlate{0xf2, 0xf2, 0xf2};
constexpr rgb kLoop{0x8c, 0x8c, 0x8c};
constexpr rgb kProtein{0xff, 0x57, 0x22};
constexpr rgb kIce{0x00, 0xff, 0xff};
// A blob this close to round has no orientation worth drawing: angle_deg is then whatever the fit
// happened to return, and a tilted box would claim a direction the data do not carry.
constexpr float kRoundAspect = 1.2f;
inline float Clamp01(float v) {
return v < 0.0f ? 0.0f : (v > 1.0f ? 1.0f : v);
}
// Colour of one cell. `obj` is the normalised background - it finds the loop, not the crystal -
// while `protein` and `ice` are the saturating detection scores and are used exactly as they come:
// they already live in [0,1], and stretching a saturating score would only turn "no protein
// anywhere" into "protein everywhere".
inline rgb CompositeColor(float obj, float protein, float ice) {
obj = Clamp01(obj);
protein = Clamp01(protein);
ice = Clamp01(ice);
const float sum = protein + ice;
const float tot = std::min(sum, 1.0f);
// Hue of the mix: the two in proportion, so one alone is its own colour and equal parts sit
// half way between. Where there is neither, tot is 0 and the mix is not used.
const float w = (sum > 0.0f) ? 1.0f / sum : 0.0f;
auto channel = [&](uint8_t plate_c, uint8_t loop_c, uint8_t protein_c, uint8_t ice_c) {
const float base = plate_c * (1.0f - obj) + loop_c * obj;
const float mix = (protein_c * protein + ice_c * ice) * w;
return static_cast<uint8_t>(std::lround(base * (1.0f - tot) + mix * tot));
};
return rgb{channel(kPlate.r, kLoop.r, kProtein.r, kIce.r),
channel(kPlate.g, kLoop.g, kProtein.g, kIce.g),
channel(kPlate.b, kLoop.b, kProtein.b, kIce.b)};
}
// The background estimate is not a saturating score, so unlike protein and ice it has to be
// normalised before it can be a grey level. Cells that carry no value stay NaN.
inline std::vector<float> NormaliseBackground(const std::vector<float> &bkg) {
std::vector<float> finite;
finite.reserve(bkg.size());
for (float v : bkg)
if (std::isfinite(v))
finite.push_back(v);
std::vector<float> out(bkg.size(), std::numeric_limits<float>::quiet_NaN());
if (finite.empty())
return out;
// Percentiles, not min/max: one dead or one hot cell would otherwise set the whole scale.
std::sort(finite.begin(), finite.end());
const float lo = finite[static_cast<size_t>(0.02 * (finite.size() - 1))];
const float hi = finite[static_cast<size_t>(0.98 * (finite.size() - 1))];
// The range is floored at half the low end. Over pure air the spread is only counting noise on
// a flat level, and without a floor that noise would be stretched into a convincing fake loop.
const float range = std::max({hi - lo, 0.5f * lo, 1e-6f});
for (size_t i = 0; i < bkg.size(); i++)
if (std::isfinite(bkg[i]))
out[i] = Clamp01((bkg[i] - lo) / range);
return out;
}
// One corner of a crystal frame, in grid-cell coordinates: cell (i, j) covers [i, i+1] x [j, j+1].
struct FrameCorner {
float x;
float y;
};
// Corners of the frame around a crystal, in the order they are drawn. The extent comes in
// micrometres along the crystal's own principal axes, so it has to go through both step sizes -
// they genuinely differ (20 x 16 um is a normal raster), and a frame laid out in cell units would
// come out the wrong shape and the wrong angle.
//
// angle_deg is taken counter-clockwise in the grid index frame, i.e. from +nx towards +ny. That is
// the frame the map itself is drawn in (nx to the right, ny downwards), so if the producer measures
// its angle with ny pointing up instead, the two sine terms below change sign.
inline std::array<FrameCorner, 4> CrystalFrameCorners(float nx, float ny,
float major_um, float minor_um,
float angle_deg,
float step_x_um, float step_y_um) {
const float a = angle_deg * static_cast<float>(PI) / 180.0f;
const float c = std::cos(a);
const float s = std::sin(a);
const float hu = 0.5f * major_um;
const float hv = 0.5f * minor_um;
const std::array<FrameCorner, 4> local{{{hu, hv}, {-hu, hv}, {-hu, -hv}, {hu, -hv}}};
std::array<FrameCorner, 4> out{};
for (int i = 0; i < 4; i++) {
const float dx_um = c * local[i].x - s * local[i].y;
const float dy_um = s * local[i].x + c * local[i].y;
out[i] = FrameCorner{nx + 0.5f + dx_um / step_x_um, ny + 0.5f + dy_um / step_y_um};
}
return out;
}
inline bool IsRoundBlob(float major_um, float minor_um) {
return !(major_um > kRoundAspect * minor_um);
}
} // namespace grid_scan
+120 -14
View File
@@ -11,30 +11,39 @@ void JFJochGridScanImage::clear() {
W = 0;
H = 0;
this->settings = std::nullopt;
composite = false;
obj_map.clear();
protein_map.clear();
ice_map.clear();
crystals.clear();
if (scene())
scene()->clear();
resetScenePointers();
}
void JFJochGridScanImage::SetupGrid(const GridScanSettings &in_settings) {
this->settings = in_settings;
W = in_settings.GetGridSizeX_step();
H = in_settings.GetGridSizeY_step();
// Draw a cell in the proportion of the area it covers, so a scan with a coarse x step and a
// fine y step looks like the sample region it was taken over, not like a square grid.
pixel_aspect_ = std::fabs(in_settings.GetGridStepY_um() / in_settings.GetGridStepX_um());
std::vector<int64_t> indices(static_cast<size_t>(W) * H);
std::iota(indices.begin(), indices.end(), 0);
image_index = in_settings.Rearrange(indices, -1);
}
void JFJochGridScanImage::loadData(const std::vector<float> &data, const GridScanSettings &settings, bool in_one_over_d2) {
if (data.empty()) {
clear();
return;
}
this->settings = settings;
W = settings.GetGridSizeX_step();
H = settings.GetGridSizeY_step();
// Draw a cell in the proportion of the area it covers, so a scan with a coarse x step and a
// fine y step looks like the sample region it was taken over, not like a square grid.
pixel_aspect_ = std::fabs(settings.GetGridStepY_um() / settings.GetGridStepX_um());
composite = false;
SetupGrid(settings);
image_fp = settings.Rearrange(data, NAN);
std::vector<int64_t> indices(image_fp.size());
std::iota(indices.begin(), indices.end(), 0);
image_index = settings.Rearrange(indices, -1);
one_over_d2 = in_one_over_d2;
if (one_over_d2) {
for (int i = 0; i < image_fp.size(); i++)
@@ -64,6 +73,61 @@ void JFJochGridScanImage::loadData(const std::vector<float> &data, const GridSca
Redraw();
}
void JFJochGridScanImage::loadComposite(const std::vector<float> &bkg, const std::vector<float> &protein,
const std::vector<float> &ice,
const std::vector<GridScanCrystal> &in_crystals,
const GridScanSettings &settings) {
if (bkg.empty() && protein.empty() && ice.empty()) {
clear();
return;
}
composite = true;
one_over_d2 = false;
SetupGrid(settings);
obj_map = settings.Rearrange(grid_scan::NormaliseBackground(bkg), NAN);
// A score that has not been measured is not a score of zero, but for the colouring the two are
// the same thing: nothing painted over the grey.
protein_map = settings.Rearrange(protein, 0.0f);
ice_map = settings.Rearrange(ice, 0.0f);
crystals = in_crystals;
image_fp = obj_map;
background = 0.0f;
foreground = 1.0f;
RenderImage();
Redraw();
}
void JFJochGridScanImage::ColorRow(size_t y, const PixelColorMap &map, QRgb *out) const {
if (!composite) {
JFJochImage::ColorRow(y, map, out);
return;
}
for (size_t x = 0; x < W; ++x) {
const size_t idx = y * W + x;
if (std::isnan(obj_map[idx])) {
out[x] = qRgb(map.gap.r, map.gap.g, map.gap.b);
continue;
}
const rgb c = grid_scan::CompositeColor(obj_map[idx], protein_map[idx], ice_map[idx]);
out[x] = qRgb(c.r, c.g, c.b);
}
}
QString JFJochGridScanImage::PixelLabel(int x, int y) const {
if (!composite)
return JFJochImage::PixelLabel(x, y);
const size_t idx = static_cast<size_t>(y) * W + x;
if (std::isnan(obj_map[idx]))
return QStringLiteral("-");
return QString("%1/%2").arg(QString::number(protein_map[idx], 'f', 2),
QString::number(ice_map[idx], 'f', 2));
}
void JFJochGridScanImage::mouseHover(const QPointF &pt, Qt::KeyboardModifiers modifiers) {
// Map mouse position to image pixel if inside bounds
if (W == 0 || H == 0 || image_index.empty())
@@ -84,7 +148,15 @@ void JFJochGridScanImage::mouseHover(const QPointF &pt, Qt::KeyboardModifiers mo
if (modifiers & Qt::ShiftModifier)
emit imageSelected(image_id);
if (one_over_d2) {
if (composite) {
emit writeStatusBar(QString("Image %1 x %2 y %3 protein %4 ice %5")
.arg(image_id)
.arg(x)
.arg(y)
.arg(QString::number(protein_map[idx], 'f', 2))
.arg(QString::number(ice_map[idx], 'f', 2)),
6000);
} else if (one_over_d2) {
if (std::isnan(image_fp[idx]))
emit writeStatusBar(QString("Image %1 x %2 y %3 no resolution estimation")
.arg(image_id)
@@ -165,7 +237,41 @@ void JFJochGridScanImage::setImage(int64_t val) {
Redraw();
}
void JFJochGridScanImage::drawCrystalFrames() {
// White over black rather than a colour from the palette: the frame has to stay legible over
// the pale plate, the grey loop, cyan, orange and everything in between, and no single hue
// does that.
QPen halo(Qt::black, 5);
halo.setCosmetic(true);
QPen line(Qt::white, 2);
line.setCosmetic(true);
const float step_x = settings->GetGridStepX_um();
const float step_y = settings->GetGridStepY_um();
for (const auto &c : crystals) {
if (grid_scan::IsRoundBlob(c.major_um, c.minor_um)) {
// No orientation to show, so no oriented box - a circle of the mean extent.
const float r_um = 0.25f * (c.major_um + c.minor_um);
const QRectF box(c.nx + 0.5 - r_um / step_x, c.ny + 0.5 - r_um / step_y,
2 * r_um / step_x, 2 * r_um / step_y);
addOverlayItem(scene()->addEllipse(box, halo));
addOverlayItem(scene()->addEllipse(box, line));
} else {
QPolygonF poly;
for (const auto &corner : grid_scan::CrystalFrameCorners(c.nx, c.ny, c.major_um, c.minor_um,
c.angle_deg, step_x, step_y))
poly << QPointF(corner.x, corner.y);
addOverlayItem(scene()->addPolygon(poly, halo));
addOverlayItem(scene()->addPolygon(poly, line));
}
}
}
void JFJochGridScanImage::addCustomOverlay() {
if (composite && settings)
drawCrystalFrames();
if (current_image_W < 0 || current_image_H < 0 || current_image_W >= W || current_image_H >= H)
return;
@@ -174,4 +280,4 @@ void JFJochGridScanImage::addCustomOverlay() {
auto *rect = scene()->addRect(current_image_W, current_image_H, 1, 1, pen);
addOverlayItem(rect);
}
}
+19 -2
View File
@@ -4,6 +4,8 @@
#pragma once
#include "JFJochImage.h"
#include "GridScanComposite.h"
#include "../GridScanDataStub.h" // STUB: GridScanCrystal - see the note in that file
#include "../../reader/JFJochReaderDataset.h"
Q_DECLARE_METATYPE(GridScanSettings)
@@ -17,10 +19,23 @@ class JFJochGridScanImage : public JFJochImage {
int64_t current_image_H = -1;
bool one_over_d2 = false;
// Composite mode: instead of one colourmapped scalar the map shows the loop in grey with ice
// and protein painted over it. The three arrays are rearranged to the grid like image_fp.
bool composite = false;
std::vector<float> obj_map; // normalised background - where there is material
std::vector<float> protein_map; // saturating score, used as it comes
std::vector<float> ice_map;
std::vector<GridScanCrystal> crystals;
void mouseHover(const QPointF &scenePos, Qt::KeyboardModifiers modifiers) override;
void mouseDoubleClickEvent(QMouseEvent *event) override;
void loadImage(QMouseEvent *event);
void addCustomOverlay() override;
void drawCrystalFrames();
void ColorRow(size_t y, const PixelColorMap &map, QRgb *out) const override;
[[nodiscard]] QString PixelLabel(int x, int y) const override;
// Grid geometry and the cell -> image map, shared by both load paths.
void SetupGrid(const GridScanSettings &settings);
signals:
void imageSelected(int64_t number);
void writeStatusBar(QString string, int timeout_ms = 0);
@@ -28,8 +43,10 @@ public:
JFJochGridScanImage(QWidget *parent = nullptr);
public slots:
void loadData(const std::vector<float> &data, const GridScanSettings &settings, bool one_over_d2);
// Protein and ice over the shape of the loop, with a frame around each crystal found.
void loadComposite(const std::vector<float> &bkg, const std::vector<float> &protein,
const std::vector<float> &ice, const std::vector<GridScanCrystal> &crystals,
const GridScanSettings &settings);
void setImage(int64_t val);
void clear();
};