diff --git a/image_analysis/MXAnalysisWithoutFPGA.cpp b/image_analysis/MXAnalysisWithoutFPGA.cpp index 3c228fba2..6a6feb4d2 100644 --- a/image_analysis/MXAnalysisWithoutFPGA.cpp +++ b/image_analysis/MXAnalysisWithoutFPGA.cpp @@ -448,6 +448,19 @@ void MXAnalysisWithoutFPGA::RebuildROI() { roi = std::make_unique(experiment); } +void MXAnalysisWithoutFPGA::AnalyzeStreamImage(DataMessage &output, + AzimuthalIntegrationProfile &profile, + const SpotFindingSettings &spot_finding_settings) { + // In an unsigned image from the card saturated and error pixels have the same value: only the card knows which is which + const auto saturated_pixel_count = output.saturated_pixel_count; + const auto error_pixel_count = output.error_pixel_count; + Analyze(output, profile, spot_finding_settings); + if (saturated_pixel_count) + output.saturated_pixel_count = saturated_pixel_count; + if (error_pixel_count) + output.error_pixel_count = error_pixel_count; +} + void MXAnalysisWithoutFPGA::AnalyzeROIOnly(DataMessage &output) { if ((output.image.GetWidth() != xpixels) || (output.image.GetWidth() * output.image.GetHeight() != npixels)) diff --git a/image_analysis/MXAnalysisWithoutFPGA.h b/image_analysis/MXAnalysisWithoutFPGA.h index 8d3e0708a..d333d6831 100644 --- a/image_analysis/MXAnalysisWithoutFPGA.h +++ b/image_analysis/MXAnalysisWithoutFPGA.h @@ -117,6 +117,8 @@ public: MXAnalysisWithoutFPGA(const DiffractionExperiment &experiment, const AzimuthalIntegrationMapping &integration, const PixelMask &mask, IndexAndRefine &indexer, bool enable_fused_adaptive_gpu = false); void Analyze(DataMessage &output, AzimuthalIntegrationProfile &profile, const SpotFindingSettings &spot_finding_settings); + // Analyze for an image from a stream: saturated and error counts the stream states stay, a DECTRIS message has none. + void AnalyzeStreamImage(DataMessage &output, AzimuthalIntegrationProfile &profile, const SpotFindingSettings &spot_finding_settings); // Surgical ROI-only paths used when a full re-analysis is not wanted: rebuild the // ROI engine after the ROI set changes, recompute ROIs after preprocessing a new diff --git a/receiver/JFJochReceiverLite.cpp b/receiver/JFJochReceiverLite.cpp index a58410c5a..55cb9299c 100644 --- a/receiver/JFJochReceiverLite.cpp +++ b/receiver/JFJochReceiverLite.cpp @@ -342,7 +342,7 @@ void JFJochReceiverLite::DataAnalysisThread(uint32_t id) { AzimuthalIntegrationProfile profile(*az_int_mapping); if (analysis) - analysis->Analyze(data_msg, profile, GetSpotFindingSettings()); + analysis->AnalyzeStreamImage(data_msg, profile, GetSpotFindingSettings()); auto image_end_time = std::chrono::high_resolution_clock::now(); std::chrono::duration image_duration = image_end_time - image_start_time; diff --git a/receiver/StillsGPUAnalysis.cpp b/receiver/StillsGPUAnalysis.cpp index 6011e6aee..3115a2d21 100644 --- a/receiver/StillsGPUAnalysis.cpp +++ b/receiver/StillsGPUAnalysis.cpp @@ -330,7 +330,7 @@ void StillsGPUAnalysis::AnalysisThread() { profile.emplace(*setup.mapping); try { if (analysis) - analysis->Analyze(message, *profile, settings()); + analysis->AnalyzeStreamImage(message, *profile, settings()); } catch (const JFJochException &e) { // The frame is still written, without its analysis. logger.ErrorException(e); diff --git a/tests/FPGAImagePullerTest.cpp b/tests/FPGAImagePullerTest.cpp index 4ff3a8a5a..5d5ee48b0 100644 --- a/tests/FPGAImagePullerTest.cpp +++ b/tests/FPGAImagePullerTest.cpp @@ -142,7 +142,7 @@ namespace { JFJochReceiverOutput output; }; - PathResult Run(const DiffractionExperiment &x, bool puller, int64_t distinct_frames) { + PathResult Run(const DiffractionExperiment &x, bool puller, int64_t distinct_frames, bool rugnux = false) { auto devices = Devices(x); Logger logger("FPGAImagePullerTest"); CollectingPusher pusher; @@ -150,6 +150,7 @@ namespace { service.NumThreads(4); service.SetSpotFindingSettings(Spots()); service.UseFPGAImagePuller(puller); + service.RugnuxLive(rugnux, nullptr, 2, 30); const bool jungfrau = x.GetDetectorType() == DetectorType::JUNGFRAU; for (int64_t f = 0; f < distinct_frames; f++) { @@ -675,6 +676,40 @@ TEST_CASE("FPGAImagePuller_RugnuxReceiver", "[FPGAImagePuller]") { CHECK(a == b); // the third frame in the generator's memory repeats } +// In an unsigned image the card gives saturated and error pixels one value: the counts are the card's, on every receiver. +TEST_CASE("FPGAImagePuller_SaturatedAndErrorCountsFromTheCard", "[FPGAImagePuller]") { + auto x = Base(DetJF(2), 4, 2); + x.Conversion(); + x.Compression(CompressionAlgorithm::BSHUF_LZ4).BitDepthImage(16).PixelSigned(false).FilePrefix("fpga_puller_counts"); + const auto card = Run(x, false, 2); + + PathResult puller; + SECTION("JFJochReceiverLite") { + puller = Run(x, true, 2); + } + SECTION("JFJochReceiverRugnux, stills") { + puller = Run(x, true, 2, true); + } + SECTION("JFJochReceiverRugnux, rotation") { + auto rotation = x; + rotation.Goniometer(GoniometerAxis("omega", 0, 0.1, Coord(-1, 0, 0), std::nullopt)); + puller = Run(rotation, true, 2, true); + } + + REQUIRE(card.images.size() == 4); + REQUIRE(puller.images.size() == 4); + int64_t saturated = 0, error = 0; + for (const auto &[n, image]: puller.images) { + INFO("image " << n); + CHECK(image.message.saturated_pixel_count == card.images.at(n).message.saturated_pixel_count); + CHECK(image.message.error_pixel_count == card.images.at(n).message.error_pixel_count); + saturated += image.message.saturated_pixel_count.value_or(0); + error += image.message.error_pixel_count.value_or(0); + } + CHECK(saturated == 8); + CHECK(error == 8); +} + // Rotation forwards the images unanalysed, so nothing would replace what the card's ROI map, not loaded for this run, gave. TEST_CASE("FPGAImagePuller_NoStaleROI", "[FPGAImagePuller]") { auto x = Base(DetJF(1), 4, 1);