Panel layout stage 3: per-panel spot finding (CPU+GPU, CCL) and raster algorithms on the plane raster
Spot finding works panel by panel, as the FPGA path does per JUNGFRAU module. PanelBoxes (common/) gives one box per DetectorModule; an empty table gives one box, the whole image, so every clamp reduces to the old image bounds in the same evaluation order (rotation sets, EIGER, CCDs, our JUNGFRAU unchanged). - ImageSpotFinder::SetPanels: the CPU local-box pass (dense, count-threshold and the DetectAt/candidate path of the adaptive finder) slides its windows inside each panel box; host CCL labels panel by panel; pixels on no panel are folded into the resolution mask. - GPU: analyze_pixel tiles each panel (warps numbered panel after panel), analyze_candidates and union_neighbours clip to the pixel's panel, the device spot carries its panel; the GPU list is put in panel order so it equals the host's. - DiffractionSpot carries its panel; SpotAnalyze maps by ImageToPlane(panel, ...) (d_A on the plane). - ImagePreprocessorBuffer::GatherTiles (CPU+GPU) clips each square to the panel of its centre. - MXAnalysisWithoutFPGA and the pre-scan workers hand the module table to finders and buffers. Raster algorithms: new common/AssembleToPlane.h (PlaneRasterOf, AssembleToPlane nearest-pixel gather with gaps, GatherFromPlane back to stored pixels; empty table = copy). With panels, ShadowFinder::GetMask runs on the projection assembled to the plane raster and gathers the mask back per stored pixel (CPU route); FindBeamCenterFFT scores the assembled projection and reports plane coordinates; the detector diagnostic JPEG shows the assembled plane. Without panels all three take their old path. Harness: PanelLayout_SpotsNeverCrossPanels enabled (count threshold and SNR on the edge scene), new PanelLayout_SpotFinderParity (CPU vs GPU, local box and adaptive, on the stacked layout), PanelLayout_AssembleToPlane, PanelLayout_ShadowFinder, PanelLayout_BeamCenterFFT, PanelLayout_DetectorDiagnostic (stacked vs assembled image cut to the raster). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015CeEGgpKdfkCTJb3YNMJcu
This commit is contained in:
1 parent
c8fb472f4d
commit
fd798cd665
26 files changed
+792
-184
No files matched your search
+243
-9
@@ -13,9 +13,10 @@
|
||||
// consumer retags its case [PanelLayout]:
|
||||
// stage 1 - dense maps: PanelLayout_AzintMapping, PanelLayout_ResolutionMask
|
||||
// stage 2 - sparse/Bragg: PanelLayout_BraggIntegration
|
||||
// stage 3 - per-panel spot finding: PanelLayout_SpotsNeverCrossPanels; the raster algorithms
|
||||
// (ShadowFinder, BeamCenterFFT, BeamCenterFromBackground, DiagnosticOutput) add their case here,
|
||||
// comparing the result on the stacked layout with the result on the assembled image.
|
||||
// stage 3 - per-panel spot finding: PanelLayout_SpotsNeverCrossPanels (+ PanelLayout_SpotFinderParity on a
|
||||
// GPU); the raster algorithms on the plane raster: PanelLayout_AssembleToPlane,
|
||||
// PanelLayout_ShadowFinder, PanelLayout_BeamCenterFFT, PanelLayout_DetectorDiagnostic - each
|
||||
// comparing the stacked layout with the assembled image cut to the raster.
|
||||
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
@@ -23,6 +24,7 @@
|
||||
#include <map>
|
||||
#include <vector>
|
||||
|
||||
#include "../common/AssembleToPlane.h"
|
||||
#include "../common/AzimuthalIntegrationMapping.h"
|
||||
#include "../common/BraggIntegrationSettings.h"
|
||||
#include "../common/DetectorModules.h"
|
||||
@@ -30,10 +32,20 @@
|
||||
#include "../common/DiffractionExperiment.h"
|
||||
#include "../common/PixelMask.h"
|
||||
#include "../common/Reflection.h"
|
||||
#include "../common/CUDAWrapper.h"
|
||||
#include "../image_analysis/beam_stop/ShadowFinder.h"
|
||||
#include "../image_analysis/bragg_integration/BraggIntegrationEngineCPU.h"
|
||||
#include "../image_analysis/geom_refinement/BeamCenterFFT.h"
|
||||
#include "../image_analysis/image_preprocessing/ImagePreprocessorBuffer.h"
|
||||
#include "../image_analysis/spot_finding/AdaptiveSpotFinderCPU.h"
|
||||
#include "../image_analysis/spot_finding/ImageSpotFinderCPU.h"
|
||||
#include "../image_analysis/spot_finding/SpotUtils.h"
|
||||
#include "../rugnux/DiagnosticOutput.h"
|
||||
#ifdef JFJOCH_USE_CUDA
|
||||
#include "../image_analysis/image_preprocessing/ImagePreprocessorBufferGPU.h"
|
||||
#include "../image_analysis/spot_finding/AdaptiveSpotFinderGPU.h"
|
||||
#include "../image_analysis/spot_finding/ImageSpotFinderGPU.h"
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -123,6 +135,7 @@ std::vector<SpotToSave> FindSpots(const DiffractionExperiment &x, const ImagePre
|
||||
settings.min_pix_per_spot = 1;
|
||||
settings.high_res_gap_Q_recipA = std::nullopt;
|
||||
ImageSpotFinderCPU finder(static_cast<int32_t>(x.GetXPixelsNum()), static_cast<int32_t>(x.GetYPixelsNum()));
|
||||
finder.SetPanels(x.GetModules());
|
||||
finder.SetResolutionMask(std::vector<bool>(x.GetPixelsNum(), false));
|
||||
DataMessage msg{};
|
||||
msg.number = 0;
|
||||
@@ -307,17 +320,238 @@ TEST_CASE("PanelLayout_BraggIntegration", "[.][PanelLayoutTodo]") {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO stage 3: spot finding per panel. Two half spots on the facing edges of panels 0 and 1 as stored -
|
||||
// adjacent rows of the stacked array, 139 px apart on the plane - stay two spots, and the local-background
|
||||
// (SNR) finder agrees with the assembled image away from panel edges.
|
||||
TEST_CASE("PanelLayout_SpotsNeverCrossPanels", "[.][PanelLayoutTodo]") {
|
||||
// Spot finding per panel. Two half spots on the facing edges of panels 0 and 1 as stored - adjacent rows of
|
||||
// the stacked array, 139 px apart on the plane - stay two spots, and the local-background (SNR) finder
|
||||
// agrees with the assembled image: its windows stop at the panel edge, as they stop at the masked gap on
|
||||
// the assembled image (16 px there, wider than the window's 15), instead of reaching into the other spot.
|
||||
TEST_CASE("PanelLayout_SpotsNeverCrossPanels", "[PanelLayout]") {
|
||||
// Panel 0 stored row 63 and panel 1 stored row 0, both at stored column 30.
|
||||
const std::vector<Spot> edge = {{40.0, 73.0}, {179.0, 73.0}};
|
||||
const Scene scene(edge);
|
||||
CheckSameSpots(FindSpots(scene.assembled, scene.assembled_image),
|
||||
FindSpots(scene.stacked, scene.stacked_image));
|
||||
const auto a = FindSpots(scene.assembled, scene.assembled_image);
|
||||
CHECK(a.size() == 2);
|
||||
CheckSameSpots(a, FindSpots(scene.stacked, scene.stacked_image));
|
||||
const auto a_snr = FindSpots(scene.assembled, scene.assembled_image, 3.0f);
|
||||
CHECK(a_snr.size() == 2);
|
||||
CheckSameSpots(a_snr, FindSpots(scene.stacked, scene.stacked_image, 3.0f));
|
||||
|
||||
const Scene interior(INTERIOR_SPOTS);
|
||||
CheckSameSpots(FindSpots(interior.assembled, interior.assembled_image, 3.0f),
|
||||
FindSpots(interior.stacked, interior.stacked_image, 3.0f));
|
||||
}
|
||||
|
||||
#ifdef JFJOCH_USE_CUDA
|
||||
// The GPU finders (local box, adaptive, and the device connected components) work panel by panel as the CPU
|
||||
// ones do: on the stacked edge scene the same components, in the same order, on the same panels.
|
||||
TEST_CASE("PanelLayout_SpotFinderParity", "[PanelLayout]") {
|
||||
if (get_gpu_count() == 0) {
|
||||
WARN("No CUDA GPU present. Skipping PanelLayout_SpotFinderParity");
|
||||
return;
|
||||
}
|
||||
const Scene scene({{40.0, 73.0}, {179.0, 73.0}, {50.3, 40.7}, {40.4, 140.8}});
|
||||
const DiffractionExperiment &x = scene.stacked;
|
||||
const auto w = static_cast<int32_t>(x.GetXPixelsNum()), h = static_cast<int32_t>(x.GetYPixelsNum());
|
||||
ImagePreprocessorBufferGPU buffer(x.GetPixelsNum());
|
||||
for (size_t i = 0; i < buffer.size(); i++)
|
||||
buffer[i] = scene.stacked_image[i];
|
||||
REQUIRE(cudaMemcpy(buffer.getGPUBuffer(), buffer.getBuffer().data(), x.GetPixelsNum() * sizeof(int32_t),
|
||||
cudaMemcpyHostToDevice) == cudaSuccess);
|
||||
REQUIRE(cudaDeviceSynchronize() == cudaSuccess);
|
||||
|
||||
const auto same = [&](const std::vector<DiffractionSpot> &cpu, const std::vector<DiffractionSpot> &gpu) {
|
||||
REQUIRE(cpu.size() == gpu.size());
|
||||
for (size_t i = 0; i < cpu.size(); i++) {
|
||||
CHECK(cpu[i].RawCoord().x == gpu[i].RawCoord().x);
|
||||
CHECK(cpu[i].RawCoord().y == gpu[i].RawCoord().y);
|
||||
CHECK(cpu[i].Count() == gpu[i].Count());
|
||||
CHECK(cpu[i].Panel() == gpu[i].Panel());
|
||||
const auto c = cpu[i].RawCoord();
|
||||
CHECK(cpu[i].Panel() == x.GetModules().ModuleOfImagePixel(c.x, c.y));
|
||||
}
|
||||
};
|
||||
const std::vector<bool> no_mask(x.GetPixelsNum(), false);
|
||||
auto stream = std::make_shared<CudaStream>();
|
||||
|
||||
SpotFindingSettings settings;
|
||||
settings.signal_to_noise_threshold = 3.0f;
|
||||
settings.photon_count_threshold = 20;
|
||||
settings.min_pix_per_spot = 1;
|
||||
ImageSpotFinderCPU cpu(w, h);
|
||||
ImageSpotFinderGPU gpu(w, h, stream);
|
||||
for (ImageSpotFinder *f: {static_cast<ImageSpotFinder *>(&cpu), static_cast<ImageSpotFinder *>(&gpu)}) {
|
||||
f->SetPanels(x.GetModules());
|
||||
f->SetResolutionMask(no_mask);
|
||||
}
|
||||
const auto cpu_spots = cpu.Run(buffer, settings);
|
||||
CHECK(cpu_spots.size() == 4);
|
||||
same(cpu_spots, gpu.Run(buffer, settings));
|
||||
|
||||
const AzimuthalIntegrationMapping mapping(x, scene.stacked_mask);
|
||||
SpotFindingSettings adaptive = settings;
|
||||
adaptive.adaptive_threshold = true;
|
||||
adaptive.false_pixels_per_frame = 10.0f;
|
||||
AdaptiveSpotFinderCPU adaptive_cpu(mapping);
|
||||
AdaptiveSpotFinderGPU adaptive_gpu(mapping, stream);
|
||||
for (ImageSpotFinder *f: {static_cast<ImageSpotFinder *>(&adaptive_cpu), static_cast<ImageSpotFinder *>(&adaptive_gpu)}) {
|
||||
f->SetPanels(x.GetModules());
|
||||
f->SetResolutionMask(no_mask);
|
||||
}
|
||||
const auto adaptive_spots = adaptive_cpu.Run(buffer, adaptive);
|
||||
CHECK(adaptive_spots.size() == 4);
|
||||
same(adaptive_spots, adaptive_gpu.Run(buffer, adaptive));
|
||||
}
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
// The raster the stacked panels are assembled to starts at the plane origin and ends at the last panel
|
||||
// pixel, so it is the assembled image cut to its size: the same picture, in which the raster algorithms
|
||||
// must find the same answer for both layouts.
|
||||
template<class T>
|
||||
std::vector<T> CutToRaster(const std::vector<T> &assembled, const PlaneRaster &r) {
|
||||
std::vector<T> out;
|
||||
for (int64_t j = 0; j < r.height; j++)
|
||||
for (int64_t i = 0; i < r.width; i++)
|
||||
out.push_back(assembled[(r.y0 + j) * PLANE_W + r.x0 + i]);
|
||||
return out;
|
||||
}
|
||||
|
||||
PlaneRaster StackedRaster() {
|
||||
return PlaneRasterOf(StackedPanels(), PW, NPANELS * PH);
|
||||
}
|
||||
|
||||
// A plane picture: background b, nothing inside the disk of radius r about the beam and along an arm running
|
||||
// right of it, and a ring of radius 60 about (cx, cy).
|
||||
std::vector<int32_t> Frame(const DetectorModules &modules, int64_t width, int64_t height, bool stacked,
|
||||
double r, double cx, double cy) {
|
||||
const auto value = [&](double px, double py) {
|
||||
const double dx = px - 110.0, dy = py - 82.0;
|
||||
if (dx * dx + dy * dy <= r * r || (dx >= 0 && std::fabs(dy) <= 4.0))
|
||||
return int32_t{0};
|
||||
const double ring = std::hypot(px - cx, py - cy) - 60.0;
|
||||
return static_cast<int32_t>(4 + std::lround(20.0 * std::exp(-ring * ring / 8.0)));
|
||||
};
|
||||
std::vector<int32_t> out(static_cast<size_t>(width * height), INT32_MIN);
|
||||
if (stacked)
|
||||
modules.ForEachPixel(width, height, [&](int64_t, size_t i, double px, double py) { out[i] = value(px, py); });
|
||||
else
|
||||
StackedPanels().ForEachPixel(PW, NPANELS * PH, [&](int64_t, size_t, double px, double py) {
|
||||
out[std::lround(py) * width + std::lround(px)] = value(px, py);
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
// The assembled detector at the raster's size, gaps masked.
|
||||
struct RasterScene {
|
||||
PlaneRaster raster = StackedRaster();
|
||||
DiffractionExperiment assembled = Experiment(raster.width, raster.height, {});
|
||||
PixelMask assembled_mask;
|
||||
DiffractionExperiment stacked = Experiment(PW, NPANELS * PH, StackedPanels());
|
||||
PixelMask stacked_mask{PW, NPANELS * PH};
|
||||
|
||||
RasterScene() {
|
||||
std::vector<uint32_t> gaps(raster.width * raster.height, 1u << PixelMask::ModuleGapPixelBit);
|
||||
StackedPanels().ForEachPixel(PW, NPANELS * PH, [&](int64_t, size_t, double px, double py) {
|
||||
gaps[std::lround(py) * raster.width + std::lround(px)] = 0;
|
||||
});
|
||||
assembled_mask = PixelMask(gaps);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// The panels put together on the plane raster are the assembled image, gaps and all; an empty table is a copy.
|
||||
TEST_CASE("PanelLayout_AssembleToPlane", "[PanelLayout]") {
|
||||
const Scene scene(INTERIOR_SPOTS);
|
||||
const PlaneRaster r = StackedRaster();
|
||||
CHECK(r.x0 == 0);
|
||||
CHECK(r.y0 == 0);
|
||||
CHECK(r.width == 210);
|
||||
CHECK(r.height == 186);
|
||||
const std::vector<int32_t> stacked(scene.stacked_image.data(), scene.stacked_image.data() + scene.stacked_image.size());
|
||||
const std::vector<int32_t> assembled(scene.assembled_image.data(),
|
||||
scene.assembled_image.data() + scene.assembled_image.size());
|
||||
const auto plane = AssembleToPlane(scene.modules, stacked, PW, NPANELS * PH, r, INT32_MIN);
|
||||
CHECK(plane == CutToRaster(assembled, r));
|
||||
CHECK(GatherFromPlane(scene.modules, plane, r, PW, NPANELS * PH, 0) == stacked);
|
||||
|
||||
const DetectorModules none;
|
||||
const PlaneRaster image = PlaneRasterOf(none, PLANE_W, PLANE_H);
|
||||
CHECK(image.width == PLANE_W);
|
||||
CHECK(image.height == PLANE_H);
|
||||
CHECK(AssembleToPlane(none, assembled, PLANE_W, PLANE_H, image, 0) == assembled);
|
||||
CHECK(GatherFromPlane(none, assembled, image, PLANE_W, PLANE_H, 0) == assembled);
|
||||
}
|
||||
|
||||
// The beam stop - a disk on the beam in the gap cross, reaching onto all four panels, and an arm across the
|
||||
// gap between panels 0 and 1 - is found on the plane: each stored pixel gets the mask of the assembled pixel
|
||||
// it is.
|
||||
TEST_CASE("PanelLayout_ShadowFinder", "[PanelLayout]") {
|
||||
const RasterScene rs;
|
||||
ShadowFinder assembled(rs.assembled, rs.assembled_mask);
|
||||
ShadowFinder stacked(rs.stacked, rs.stacked_mask);
|
||||
std::vector<std::vector<int32_t>> frames;
|
||||
std::vector<uint8_t> buffer;
|
||||
for (int f = 0; f < 12; f++) {
|
||||
frames.push_back(Frame({}, rs.raster.width, rs.raster.height, false, 25.0, 110.0, 82.0));
|
||||
DataMessage a{};
|
||||
a.image = CompressedImage(frames.back(), rs.raster.width, rs.raster.height);
|
||||
assembled.AddImage(a, buffer);
|
||||
frames.push_back(Frame(StackedPanels(), PW, NPANELS * PH, true, 25.0, 110.0, 82.0));
|
||||
DataMessage s{};
|
||||
s.image = CompressedImage(frames.back(), PW, NPANELS * PH);
|
||||
stacked.AddImage(s, buffer);
|
||||
}
|
||||
const auto a = assembled.GetMask();
|
||||
const auto s = stacked.GetMask();
|
||||
REQUIRE(s.size() == static_cast<size_t>(PW * NPANELS * PH));
|
||||
size_t shadowed = 0, differ = 0;
|
||||
StackedPanels().ForEachPixel(PW, NPANELS * PH, [&](int64_t, size_t i, double px, double py) {
|
||||
shadowed += s[i] != 0;
|
||||
differ += s[i] != a[std::lround(py) * rs.raster.width + std::lround(px)];
|
||||
});
|
||||
CHECK(shadowed > 500);
|
||||
CHECK(differ == 0);
|
||||
}
|
||||
|
||||
// The point-symmetry capture scores the plane raster and reports plane coordinates: the same shortlist from
|
||||
// both layouts, led by the centre of the ring.
|
||||
TEST_CASE("PanelLayout_BeamCenterFFT", "[PanelLayout]") {
|
||||
const RasterScene rs;
|
||||
const auto to_float = [](const std::vector<int32_t> &v) {
|
||||
std::vector<float> out;
|
||||
for (int32_t i: v)
|
||||
out.push_back(i == INT32_MIN ? NAN : static_cast<float>(i));
|
||||
return out;
|
||||
};
|
||||
const auto a = FindBeamCenterFFT(rs.assembled, to_float(Frame({}, rs.raster.width, rs.raster.height, false,
|
||||
0.0, 104.0, 95.0)));
|
||||
const auto s = FindBeamCenterFFT(rs.stacked, to_float(Frame(StackedPanels(), PW, NPANELS * PH, true,
|
||||
0.0, 104.0, 95.0)));
|
||||
REQUIRE(!a.point.empty());
|
||||
REQUIRE(s.point.size() == a.point.size());
|
||||
for (size_t i = 0; i < a.point.size(); i++) {
|
||||
CHECK(s.point[i].beam_x_pxl == a.point[i].beam_x_pxl);
|
||||
CHECK(s.point[i].beam_y_pxl == a.point[i].beam_y_pxl);
|
||||
CHECK(s.point[i].score == a.point[i].score);
|
||||
}
|
||||
CHECK(s.margin_point == a.margin_point);
|
||||
}
|
||||
|
||||
// The diagnostic picture shows the detector as it is, gaps in the gap colour: the same JPEG from both layouts.
|
||||
TEST_CASE("PanelLayout_DetectorDiagnostic", "[PanelLayout]") {
|
||||
const RasterScene rs;
|
||||
const auto to_float = [](const std::vector<int32_t> &v) {
|
||||
std::vector<float> out;
|
||||
for (int32_t i: v)
|
||||
out.push_back(i == INT32_MIN ? NAN : static_cast<float>(i));
|
||||
return out;
|
||||
};
|
||||
const auto a = RenderDetectorDiagnostic(rs.assembled, rs.assembled_mask,
|
||||
to_float(Frame({}, rs.raster.width, rs.raster.height, false,
|
||||
25.0, 104.0, 95.0)));
|
||||
const auto s = RenderDetectorDiagnostic(rs.stacked, rs.stacked_mask,
|
||||
to_float(Frame(StackedPanels(), PW, NPANELS * PH, true,
|
||||
25.0, 104.0, 95.0)));
|
||||
CHECK(s == a);
|
||||
}
|
||||
Reference in new issue
Block a user