pixel_sum existed only on the FPGA path, so a DECTRIS master file carried an all-zero
/entry/image/pixel_sum. The CPU and GPU image preprocessors now compute it in the pass that takes the
minimum and maximum, as a signed 64-bit sum, and MXAnalysisWithoutFPGA reports it: Lite, live stills,
live rotation and rugnux. On the simulated card it equals the card's own sum, for 16-bit and for
summed 32-bit frames.
The sum excludes error and saturated pixels and the device mask (detector and calibration). The hot
pixels and the beam-stop shadow a run finds on its own frames do not change it, though they stay masked
in the image and in the other statistics, so the number does not depend on what a run decided. Those
two have bits of their own (PixelMask::RunMaskBits); the byte-per-pixel mask keeps the difference, 1 for
the device mask and 2 for the run's alone, and the CPU preprocessor gets a packed mask of the second.
Live stills reports this sum of the assembled image in place of the card's, and with the run's mask out
of it the two agree on the test frames.
Frames a live rotation run does not reach stay 0 in the master file, as before, and have no dataset in
the data files. The reader now reads pixel_sum into the dataset and the per-image message; a file
without it opens as before.
(ported from rc184-2 15af14357)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
63 lines
3.4 KiB
C++
63 lines
3.4 KiB
C++
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#pragma once
|
|
|
|
#include <cstdint>
|
|
#include <cstddef>
|
|
#include <vector>
|
|
#include "../common/CompressedImage.h"
|
|
#include "../common/DiffractionExperiment.h"
|
|
#include "../common/PixelMask.h"
|
|
#include "ImagePreprocessorBuffer.h"
|
|
|
|
struct alignas(8) ImageStatistics {
|
|
unsigned long long error_pixel_count = 0;
|
|
unsigned long long saturated_pixel_count = 0;
|
|
unsigned long long masked_pixel_count = 0;
|
|
long long max_value = INT64_MIN;
|
|
long long min_value = INT64_MAX;
|
|
// Of the pixels that are not error, saturated or under the device mask, as on the FPGA. A pixel masked
|
|
// only by what a run determined (PixelMask::RunMaskBits) counts, so the sum does not depend on it.
|
|
long long pixel_sum = 0;
|
|
};
|
|
|
|
class ImagePreprocessor {
|
|
protected:
|
|
const size_t npixels;
|
|
const DiffractionExperiment &experiment;
|
|
const int64_t saturation_limit;
|
|
public:
|
|
ImagePreprocessor(const DiffractionExperiment &experiment);
|
|
virtual ~ImagePreprocessor() = default;
|
|
virtual ImageStatistics Analyze(ImagePreprocessorBuffer &processed_image, const uint8_t *decompressed_image, CompressedImageMode image_mode) = 0;
|
|
|
|
// Analyze straight from the COMPRESSED image, decompressing wherever the implementation prefers.
|
|
// The GPU implementation uploads the compressed chunk and decodes it on the device, so only a few
|
|
// MB cross PCIe instead of the whole frame and the host never decompresses at all.
|
|
// Returns false when this implementation cannot handle the image - the CPU preprocessor always,
|
|
// and the GPU one for any algorithm without a device decoder - and the caller then decompresses
|
|
// on the host and calls Analyze() as before. Keeping the fallback explicit means a format we
|
|
// cannot decode on the device is a slower path, never a wrong answer.
|
|
virtual bool AnalyzeCompressed(ImagePreprocessorBuffer &processed_image, const CompressedImage &image,
|
|
ImageStatistics &stats) { return false; }
|
|
|
|
// Device time the last AnalyzeCompressed() spent getting the chunk across and decompressing it,
|
|
// so the caller can still report a decompression cost once the host no longer does the work.
|
|
// Meaningless unless the previous call returned true.
|
|
[[nodiscard]] virtual float GetLastDecompressionTime_s() const { return 0.0f; }
|
|
|
|
// Resize the buffer an image will be decompressed into and page-lock it, so that the host->device
|
|
// copy of Analyze() is a real DMA. Without page-locking the driver stages the copy through its own
|
|
// pinned pool, which is a host-side copy on the calling thread: it does not overlap and it degrades
|
|
// badly with the number of workers. Nothing to do on the CPU.
|
|
virtual void PinInputBuffer(std::vector<uint8_t> &buffer, size_t size) {}
|
|
|
|
// The same for a region the caller already owns. An uncompressed image is never decompressed
|
|
// into a buffer of ours - the upload reads the reader's own bytes - so that is what has to be
|
|
// page-locked. The region is remembered, so a worker handing over the same buffer on every frame
|
|
// registers it once. The registration is dropped when this object is destroyed, which is why an
|
|
// engine must not outlive the buffer it was given.
|
|
virtual void PinInputRegion(const void *ptr, size_t bytes) {}
|
|
};
|