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:
leonarski_fandClaude Opus 5.5 committed 2026-10-21 10:29:07 +02:00
1 parent c8fb472f4d
commit fd798cd665
26 files changed
+792 -184

No files matched your search

@@ -23,17 +23,25 @@ const int32_t *ImagePreprocessorBuffer::getGPUBuffer() const {
return nullptr;
}
PanelBox ImagePreprocessorBuffer::TileBox(int32_t x, int32_t y, int width) const {
for (const auto &p: panels)
if (x >= p.x0 && x < p.x1 && y >= p.y0 && y < p.y1)
return p;
return PanelBox{0, 0, width, static_cast<int32_t>(npixels / width), -1};
}
const int32_t *ImagePreprocessorBuffer::GatherTiles(const std::vector<int32_t> &x, const std::vector<int32_t> &y,
int width, int half) {
const int side = 2 * half + 1;
const int height = static_cast<int>(npixels / width);
tiles.resize(x.size() * side * side);
int32_t *out = tiles.data();
for (size_t i = 0; i < x.size(); i++)
for (size_t i = 0; i < x.size(); i++) {
const PanelBox b = TileBox(x[i], y[i], width);
for (int py = y[i] - half; py <= y[i] + half; py++)
for (int px = x[i] - half; px <= x[i] + half; px++)
*out++ = (px < 0 || py < 0 || px >= width || py >= height)
*out++ = (px < b.x0 || py < b.y0 || px >= b.x1 || py >= b.y1)
? INT32_MIN : buffer[static_cast<size_t>(py) * width + px];
}
return tiles.data();
}
@@ -8,8 +8,15 @@
#include <cstddef>
#include <climits>
#include "../../common/PanelBoxes.h"
class ImagePreprocessorBuffer {
protected:
// The panels the image is made of (PanelBoxes); empty means one, the whole image.
std::vector<PanelBox> panels;
// The box the square about pixel (x, y) is clipped to: its panel's, or the image's.
PanelBox TileBox(int32_t x, int32_t y, int width) const;
// The image size, kept apart from `buffer` so size() still answers when the host copy was not
// allocated at all (see the host_mirror constructor).
const size_t npixels;
@@ -46,9 +53,13 @@ public:
virtual void Gather(const std::vector<uint32_t> &npixel, std::vector<int32_t> &values) const;
// The (2*half+1)^2 pixels about each (x[i], y[i]) of the image, `width` pixels wide: square after
// square, each row by row, with a pixel off the image read as INT32_MIN. Valid until the next call.
// square, each row by row, with a pixel off the image - or off the panel (x[i], y[i]) is on - read as
// INT32_MIN. Valid until the next call.
virtual const int32_t *GatherTiles(const std::vector<int32_t> &x, const std::vector<int32_t> &y,
int width, int half);
// The panels of the image, for GatherTiles. Default: none, the whole image is one.
void SetPanels(const std::vector<PanelBox> &in_panels) { panels = in_panels; }
// The whole image on the host - on the GPU copied there first, where it was not already.
virtual const int32_t *HostImage();
private:
@@ -19,7 +19,8 @@ __global__ void gather_kernel(const int32_t *__restrict__ image,
values[i] = image[npixel[i]];
}
__global__ void gather_tiles_kernel(const int32_t *__restrict__ image, int width, int height,
// centre holds six values per tile: its centre x, y and the box x0, y0, x1, y1 it is clipped to.
__global__ void gather_tiles_kernel(const int32_t *__restrict__ image, int width,
const int32_t *__restrict__ centre, int half,
int32_t *__restrict__ tiles, size_t count) {
const int side = 2 * half + 1;
@@ -27,9 +28,10 @@ __global__ void gather_tiles_kernel(const int32_t *__restrict__ image, int width
for (size_t i = blockIdx.x * blockDim.x + threadIdx.x; i < count; i += static_cast<size_t>(blockDim.x) * gridDim.x) {
const size_t tile = i / area;
const int p = static_cast<int>(i - tile * area);
const int px = centre[2 * tile] - half + p % side;
const int py = centre[2 * tile + 1] - half + p / side;
tiles[i] = (px < 0 || py < 0 || px >= width || py >= height)
const int32_t *c = centre + 6 * tile;
const int px = c[0] - half + p % side;
const int py = c[1] - half + p / side;
tiles[i] = (px < c[2] || py < c[3] || px >= c[4] || py >= c[5])
? INT32_MIN : image[static_cast<size_t>(py) * width + px];
}
}
@@ -79,18 +81,24 @@ const int32_t *ImagePreprocessorBufferGPU::GatherTiles(const std::vector<int32_t
gpu_tiles = CudaDevicePtr<int32_t>(tile_capacity);
host_tiles.resize(tile_capacity);
}
if (2 * x.size() > tile_centre.size()) {
tile_centre.resize(std::max(2 * x.size(), 2 * tile_centre.size()));
if (6 * x.size() > tile_centre.size()) {
tile_centre.resize(std::max(6 * x.size(), 2 * tile_centre.size()));
gpu_tile_centre = CudaDevicePtr<int32_t>(tile_centre.size());
}
for (size_t i = 0; i < x.size(); i++) {
tile_centre[2 * i] = x[i];
tile_centre[2 * i + 1] = y[i];
const PanelBox b = TileBox(x[i], y[i], width);
int32_t *c = tile_centre.data() + 6 * i;
c[0] = x[i];
c[1] = y[i];
c[2] = b.x0;
c[3] = b.y0;
c[4] = b.x1;
c[5] = b.y1;
}
cuda_err(cudaMemcpyAsync(gpu_tile_centre.get(), tile_centre.data(), 2 * x.size() * sizeof(int32_t),
cuda_err(cudaMemcpyAsync(gpu_tile_centre.get(), tile_centre.data(), 6 * x.size() * sizeof(int32_t),
cudaMemcpyHostToDevice, gather_stream));
const int blocks = static_cast<int>(std::min<size_t>((count + 255) / 256, 4096));
gather_tiles_kernel<<<blocks, 256, 0, gather_stream>>>(gpu_image.get(), width, static_cast<int>(npixels / width),
gather_tiles_kernel<<<blocks, 256, 0, gather_stream>>>(gpu_image.get(), width,
gpu_tile_centre.get(), half, gpu_tiles.get(), count);
cuda_err(cudaGetLastError());
cuda_err(cudaMemcpyAsync(host_tiles.data(), gpu_tiles.get(), count * sizeof(int32_t),