diff --git a/broker/JFJochBrokerHttp.cpp b/broker/JFJochBrokerHttp.cpp index 8166fb87..bf46c6a9 100644 --- a/broker/JFJochBrokerHttp.cpp +++ b/broker/JFJochBrokerHttp.cpp @@ -743,9 +743,8 @@ void JFJochBrokerHttp::config_user_mask_tiff_get(httplib::Response &response) { void JFJochBrokerHttp::config_user_mask_tiff_put(const httplib::Request &request, httplib::Response &response) { - uint32_t cols, lines; - auto v = ReadTIFFFromString32(request.body, cols, lines); - state_machine.SetUserPixelMask(v); + std::vector buffer; + state_machine.SetUserPixelMask(ReadTIFF(request.body, buffer)); response.status = 200; } diff --git a/broker/JFJochStateMachine.cpp b/broker/JFJochStateMachine.cpp index 10e4193b..b2daa353 100644 --- a/broker/JFJochStateMachine.cpp +++ b/broker/JFJochStateMachine.cpp @@ -903,6 +903,21 @@ void JFJochStateMachine::SetUserPixelMask(const std::vector &v) { } } +void JFJochStateMachine::SetUserPixelMask(const CompressedImage &image) { + std::unique_lock ul(m); + + if (state != JFJochState::Idle) + throw WrongDAQStateException("User mask can be only modified in Idle state"); + + try { + pixel_mask.LoadUserMask(experiment, image); + UpdatePixelMaskStatistics(pixel_mask.GetStatistics()); + } catch (const JFJochException &e) { + throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, + "Problem handling user mask " + std::string(e.what())); + } +} + InstrumentMetadata JFJochStateMachine::GetInstrumentMetadata() const { std::unique_lock ul(experiment_instrument_metadata_mutex); return experiment.GetInstrumentMetadata(); diff --git a/broker/JFJochStateMachine.h b/broker/JFJochStateMachine.h index 1d5fb56a..aff240c6 100644 --- a/broker/JFJochStateMachine.h +++ b/broker/JFJochStateMachine.h @@ -220,6 +220,7 @@ public: std::vector GetUserPixelMask() const; void SetUserPixelMask(const std::vector &v); + void SetUserPixelMask(const CompressedImage &image); std::vector GetDeviceStatus() const; diff --git a/broker/jfjoch_api.yaml b/broker/jfjoch_api.yaml index 1a44430a..8096f853 100644 --- a/broker/jfjoch_api.yaml +++ b/broker/jfjoch_api.yaml @@ -3244,9 +3244,9 @@ paths: Upload user mask of the detector - this is for example to account for beam stop shadow or misbehaving regions. If detector is conversion mode the mask can be both in raw (1024x512; stacked modules) or converted coordinates. In the latter case - module gaps are ignored and don't need to be assigned value. - Mask is expected as TIFF (4-byte; unsigned). + Mask is expected as a single-channel TIFF (8-, 16- or 32-bit integer, signed or unsigned). 0 - good pixel, other value - masked - User mask is stored in NXmx pixel mask (bit 8), as well as used in spot finding and azimuthal integration. + User mask is stored in NXmx pixel mask (bit 8), as well as used in spot finding and azimuthal integration. User mask is not automatically applied - i.e. pixels with user mask will have a valid pixel value in the images. requestBody: content: @@ -3257,6 +3257,8 @@ paths: responses: "200": description: All good + "400": + description: Not a valid single-channel TIFF or size doesn't match the detector "500": description: Error within Jungfraujoch code - see output message. content: diff --git a/common/PixelMask.cpp b/common/PixelMask.cpp index c25aec66..2b2dfd81 100644 --- a/common/PixelMask.cpp +++ b/common/PixelMask.cpp @@ -218,6 +218,55 @@ void PixelMask::LoadUserMask(const DiffractionExperiment& experiment, const std: "Size of input user mask invalid"); } +void PixelMask::LoadUserMask(const DiffractionExperiment& experiment, const CompressedImage& image) { + const size_t width = image.GetWidth(); + const size_t height = image.GetHeight(); + + // The image has to match one of the two layouts handled by the vector + // overload below: converted geometry, or raw stacked modules. + const bool converted = (width == static_cast(experiment.GetXPixelsNumConv())) + && (height == static_cast(experiment.GetYPixelsNumConv())); + const bool raw = (width == static_cast(RAW_MODULE_COLS)) + && (height == static_cast(RAW_MODULE_LINES * experiment.GetModulesNum())); + if (!converted && !raw) + throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, + "User mask image size doesn't match the detector"); + + std::vector buffer; + const uint8_t *bytes = image.GetUncompressedPtr(buffer); + + // A pixel is masked when its value is non-zero. Read each pixel as an + // unsigned integer of the matching width - the sign is irrelevant when + // comparing against zero. + std::vector mask(width * height); + auto binarize = [&](auto sample) { + using sample_t = decltype(sample); + const auto *typed = reinterpret_cast(bytes); + for (size_t i = 0; i < mask.size(); i++) + mask[i] = (typed[i] != 0) ? 1 : 0; + }; + + switch (image.GetMode()) { + case CompressedImageMode::Uint8: + case CompressedImageMode::Int8: + binarize(uint8_t{}); + break; + case CompressedImageMode::Uint16: + case CompressedImageMode::Int16: + binarize(uint16_t{}); + break; + case CompressedImageMode::Uint32: + case CompressedImageMode::Int32: + binarize(uint32_t{}); + break; + default: + throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, + "User mask must be an 8-, 16- or 32-bit integer image"); + } + + LoadUserMask(experiment, mask); +} + void PixelMask::LoadDECTRISBadPixelMask(const std::vector &input_mask) { if (input_mask.size() != mask.size()) throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, diff --git a/common/PixelMask.h b/common/PixelMask.h index 5f9ae7c7..f151c4d1 100644 --- a/common/PixelMask.h +++ b/common/PixelMask.h @@ -3,6 +3,7 @@ #pragma once +#include "CompressedImage.h" #include "DetectorSetup.h" #include "DiffractionExperiment.h" #include "../jungfrau/JFCalibration.h" @@ -41,6 +42,7 @@ public: void CalcEdgePixels(const DiffractionExperiment& experiment); void LoadUserMask(const DiffractionExperiment& experiment, const std::vector& mask); + void LoadUserMask(const DiffractionExperiment& experiment, const CompressedImage& image); void LoadDECTRISBadPixelMask(const std::vector& mask); void LoadDarkBadPixelMask(const DiffractionExperiment& experiment, const std::vector& mask); void LoadDetectorBadPixelMask(const DiffractionExperiment& experiment, const JFCalibration *calib); diff --git a/preview/JFJochTIFF.cpp b/preview/JFJochTIFF.cpp index 25c4f8ed..25369b3b 100644 --- a/preview/JFJochTIFF.cpp +++ b/preview/JFJochTIFF.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include "JFJochTIFF.h" @@ -144,78 +145,18 @@ CompressedImage ReadTIFF(const std::string &s, std::vector &buffer) { return CompressedImage(buffer, cols, lines, mode); } -std::vector ReadTIFFFromString32(const std::string &s, uint32_t &cols, uint32_t &lines) { - if (s.empty()) - throw JFJochException(JFJochExceptionCategory::TIFFGeneratorError, "No TIFF file provided"); - - uint32_t rows_per_string = 0; - - std::vector ret; - - uint16_t elem_size; - - std::istringstream input_TIFF_stream(s); - TIFF* tiff = TIFFStreamOpen("MemTIFF", &input_TIFF_stream); - if (tiff == nullptr) - throw JFJochException(JFJochExceptionCategory::TIFFGeneratorError,"Not a proper TIFF file"); - - TIFFGetField(tiff, TIFFTAG_IMAGEWIDTH, &cols); // get the width of the image - TIFFGetField(tiff, TIFFTAG_IMAGELENGTH, &lines); // get the height of the image - TIFFGetField(tiff, TIFFTAG_BITSPERSAMPLE, &elem_size); // get the size of the channels - TIFFGetField(tiff, TIFFTAG_ROWSPERSTRIP, &rows_per_string); - - if (elem_size != 32) - throw JFJochException(JFJochExceptionCategory::TIFFGeneratorError, "Only 32-bit format supported"); - - ret.resize(cols * lines); - - if (cols * sizeof(uint32_t) != TIFFScanlineSize(tiff)) - throw JFJochException(JFJochExceptionCategory::TIFFGeneratorError, "TIFFScanlineSize mismatch"); - - for (int i = 0; i < lines; i++) { - if (TIFFReadScanline(tiff, ret.data() + i * cols, i, 0) < 0) - throw JFJochException(JFJochExceptionCategory::TIFFGeneratorError, "TIFFReadScanline error"); - } - - TIFFClose(tiff); - return ret; -} - std::vector ReadTIFFFromString16(const std::string &s, uint32_t &cols, uint32_t &lines) { - if (s.empty()) - throw JFJochException(JFJochExceptionCategory::TIFFGeneratorError, "No TIFF file provided"); + std::vector buffer; + CompressedImage image = ReadTIFF(s, buffer); - uint32_t rows_per_string = 0; - - std::vector ret; - - uint16_t elem_size; - - std::istringstream input_TIFF_stream(s); - TIFF* tiff = TIFFStreamOpen("MemTIFF", &input_TIFF_stream); - if (tiff == nullptr) - throw JFJochException(JFJochExceptionCategory::TIFFGeneratorError, - "TIFF format error"); - - TIFFGetField(tiff, TIFFTAG_IMAGEWIDTH, &cols); // get the width of the image - TIFFGetField(tiff, TIFFTAG_IMAGELENGTH, &lines); // get the height of the image - TIFFGetField(tiff, TIFFTAG_BITSPERSAMPLE, &elem_size); // get the size of the channels - TIFFGetField(tiff, TIFFTAG_ROWSPERSTRIP, &rows_per_string); - - if (elem_size != 16) + if (image.GetByteDepth() != sizeof(uint16_t) || image.GetNumChannels() != 1) throw JFJochException(JFJochExceptionCategory::TIFFGeneratorError, "Only 16-bit format supported"); - ret.resize(cols * lines); + cols = image.GetWidth(); + lines = image.GetHeight(); - if (cols * sizeof(uint16_t) != TIFFScanlineSize(tiff)) - throw JFJochException(JFJochExceptionCategory::TIFFGeneratorError, "TIFFScanlineSize mismatch"); - - for (int i = 0; i < lines; i++) { - if (TIFFReadScanline(tiff, ret.data() + i * cols, i, 0) < 0) - throw JFJochException(JFJochExceptionCategory::TIFFGeneratorError, "TIFFReadScanline error"); - } - - TIFFClose(tiff); + std::vector ret(static_cast(cols) * lines); + memcpy(ret.data(), buffer.data(), buffer.size()); return ret; } diff --git a/preview/JFJochTIFF.h b/preview/JFJochTIFF.h index ec0dbc10..12db8758 100644 --- a/preview/JFJochTIFF.h +++ b/preview/JFJochTIFF.h @@ -12,7 +12,6 @@ void WriteTIFFToFile(const std::string &filename, const CompressedImage& image); CompressedImage ReadTIFF(const std::string &s, std::vector &buffer); -std::vector ReadTIFFFromString32(const std::string& s, uint32_t &cols, uint32_t &lines); std::vector ReadTIFFFromString16(const std::string& s, uint32_t &cols, uint32_t &lines); void SuppressTIFFErrors(); diff --git a/tests/PixelMaskTest.cpp b/tests/PixelMaskTest.cpp index 361c35d0..d8342eca 100644 --- a/tests/PixelMaskTest.cpp +++ b/tests/PixelMaskTest.cpp @@ -306,6 +306,44 @@ TEST_CASE("PixelMask_LoadUserMaskRaw_UpdatesCachedRawMask", "[PixelMask]") { CHECK((raw_mask_out[0] & (1u << PixelMask::ModuleEdgePixelBit)) != 0); } +TEST_CASE("PixelMask_LoadUserMask_CompressedImage","[PixelMask]") { + DiffractionExperiment experiment(DetJF(4, 1, 8, 36, false)); + experiment.MaskModuleEdges(false).MaskChipEdges(false); + + const uint32_t cols = experiment.GetXPixelsNumConv(); + const uint32_t lines = experiment.GetYPixelsNumConv(); + + // 8-bit single-channel mask, as e.g. PyFAI produces; non-zero == masked + std::vector values(static_cast(cols) * lines, 0); + values[1030 * 700 + 300] = 255; + CompressedImage image(values, cols, lines, CompressedImageMode::Uint8); + + PixelMask mask(experiment); + REQUIRE_NOTHROW(mask.LoadUserMask(experiment, image)); + + REQUIRE(mask.GetMask()[1030 * 700 + 300] == (1 << PixelMask::UserMaskedPixelBit)); + REQUIRE(mask.GetUserMask()[1030 * 700 + 300] == 1); + REQUIRE(mask.GetStatistics().user_mask == 1); + + // 32-bit mask goes through the same path + std::vector values32(static_cast(cols) * lines, 0); + values32[1030 * 700 + 300] = 7; + CompressedImage image32(values32, cols, lines); + PixelMask mask32(experiment); + REQUIRE_NOTHROW(mask32.LoadUserMask(experiment, image32)); + REQUIRE(mask32.GetUserMask()[1030 * 700 + 300] == 1); + + // Float images are rejected outright + std::vector valuesf(static_cast(cols) * lines, 0.0f); + CompressedImage imagef(valuesf, cols, lines); + REQUIRE_THROWS(mask.LoadUserMask(experiment, imagef)); + + // Image whose shape doesn't match the detector is rejected + std::vector wrong(static_cast(cols) * (lines + 1), 0); + CompressedImage bad(wrong, cols, lines + 1, CompressedImageMode::Uint8); + REQUIRE_THROWS(mask.LoadUserMask(experiment, bad)); +} + TEST_CASE("PixelMask_GetMaskRaw_ThrowsForDECTRIS", "[PixelMask]") { DiffractionExperiment experiment(DetDECTRIS(2068, 2164, "Test", "")); PixelMask mask(experiment); diff --git a/tests/TIFFTest.cpp b/tests/TIFFTest.cpp index 4c44ea24..df5e13b5 100644 --- a/tests/TIFFTest.cpp +++ b/tests/TIFFTest.cpp @@ -18,22 +18,59 @@ TEST_CASE("TIFFTest","[TIFF]") { } TEST_CASE("TIFFTest_Write_Read","[TIFF]") { - std::vector values(512*1024), values_out; - for (int i = 0; i < values.size(); i++) { + std::vector values(512*1024); + for (int i = 0; i < values.size(); i++) values[i] = (i * 17 + 2); - } CompressedImage image(values, 1024, 512); std::string s; REQUIRE_NOTHROW(s = WriteTIFFToString(image)); - uint32_t lines, cols; - REQUIRE_NOTHROW(values_out = ReadTIFFFromString32(s, cols, lines)); - REQUIRE(lines == 512); - REQUIRE(cols == 1024); - REQUIRE(values.size() == values_out.size()); - REQUIRE(memcmp(values.data(), values_out.data(), cols * lines * sizeof(uint32_t)) == 0); + std::vector buffer; + CompressedImage out = ReadTIFF(s, buffer); + REQUIRE(out.GetMode() == CompressedImageMode::Uint32); + REQUIRE(out.GetWidth() == 1024); + REQUIRE(out.GetHeight() == 512); + REQUIRE(buffer.size() == values.size() * sizeof(uint32_t)); + REQUIRE(memcmp(values.data(), buffer.data(), buffer.size()) == 0); +} + +TEST_CASE("TIFFTest_Write_Read_8bit","[TIFF]") { + std::vector values(512*1024); + for (int i = 0; i < values.size(); i++) + values[i] = static_cast(i * 17 + 2); + + CompressedImage image(values, 1024, 512, CompressedImageMode::Uint8); + + std::string s; + REQUIRE_NOTHROW(s = WriteTIFFToString(image)); + + std::vector buffer; + CompressedImage out = ReadTIFF(s, buffer); + REQUIRE(out.GetMode() == CompressedImageMode::Uint8); + REQUIRE(out.GetWidth() == 1024); + REQUIRE(out.GetHeight() == 512); + REQUIRE(buffer == values); +} + +TEST_CASE("TIFFTest_Write_Read_16bit","[TIFF]") { + std::vector values(512*1024); + for (int i = 0; i < values.size(); i++) + values[i] = static_cast(i * 17 + 2); + + CompressedImage image(values, 1024, 512); + + std::string s; + REQUIRE_NOTHROW(s = WriteTIFFToString(image)); + + std::vector buffer; + CompressedImage out = ReadTIFF(s, buffer); + REQUIRE(out.GetMode() == CompressedImageMode::Uint16); + REQUIRE(out.GetWidth() == 1024); + REQUIRE(out.GetHeight() == 512); + REQUIRE(buffer.size() == values.size() * sizeof(uint16_t)); + REQUIRE(memcmp(values.data(), buffer.data(), buffer.size()) == 0); } TEST_CASE("TIFFTest_File","[TIFF]") {