mask: accept any-bit-depth TIFF for user mask upload
PUT /config/user_mask.tiff only accepted 32-bit unsigned TIFF, so masks exported by tools like PyFAI (8-bit) failed. Route the upload through the universal ReadTIFF reader and let PixelMask take a CompressedImage directly: it validates the 2D shape against the detector's converted/raw layouts, binarizes any 8/16/32-bit integer image (non-zero == masked), and rejects float/multi-channel images. Also dedupe the TIFF readers: ReadTIFFFromString16 is now a thin wrapper over ReadTIFF, and the now-unused ReadTIFFFromString32 is removed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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<uint8_t> buffer;
|
||||
state_machine.SetUserPixelMask(ReadTIFF(request.body, buffer));
|
||||
response.status = 200;
|
||||
}
|
||||
|
||||
|
||||
@@ -903,6 +903,21 @@ void JFJochStateMachine::SetUserPixelMask(const std::vector<uint32_t> &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();
|
||||
|
||||
@@ -220,6 +220,7 @@ public:
|
||||
std::vector<uint32_t> GetUserPixelMask() const;
|
||||
|
||||
void SetUserPixelMask(const std::vector<uint32_t> &v);
|
||||
void SetUserPixelMask(const CompressedImage &image);
|
||||
|
||||
std::vector<DeviceStatus> GetDeviceStatus() const;
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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<size_t>(experiment.GetXPixelsNumConv()))
|
||||
&& (height == static_cast<size_t>(experiment.GetYPixelsNumConv()));
|
||||
const bool raw = (width == static_cast<size_t>(RAW_MODULE_COLS))
|
||||
&& (height == static_cast<size_t>(RAW_MODULE_LINES * experiment.GetModulesNum()));
|
||||
if (!converted && !raw)
|
||||
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
||||
"User mask image size doesn't match the detector");
|
||||
|
||||
std::vector<uint8_t> 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<uint32_t> mask(width * height);
|
||||
auto binarize = [&](auto sample) {
|
||||
using sample_t = decltype(sample);
|
||||
const auto *typed = reinterpret_cast<const sample_t *>(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<uint32_t> &input_mask) {
|
||||
if (input_mask.size() != mask.size())
|
||||
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
||||
|
||||
@@ -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<uint32_t>& mask);
|
||||
void LoadUserMask(const DiffractionExperiment& experiment, const CompressedImage& image);
|
||||
void LoadDECTRISBadPixelMask(const std::vector<uint32_t>& mask);
|
||||
void LoadDarkBadPixelMask(const DiffractionExperiment& experiment, const std::vector<uint32_t>& mask);
|
||||
void LoadDetectorBadPixelMask(const DiffractionExperiment& experiment, const JFCalibration *calib);
|
||||
|
||||
+8
-67
@@ -3,6 +3,7 @@
|
||||
|
||||
#include <tiffio.h>
|
||||
#include <tiffio.hxx>
|
||||
#include <cstring>
|
||||
#include <sstream>
|
||||
|
||||
#include "JFJochTIFF.h"
|
||||
@@ -144,78 +145,18 @@ CompressedImage ReadTIFF(const std::string &s, std::vector<uint8_t> &buffer) {
|
||||
return CompressedImage(buffer, cols, lines, mode);
|
||||
}
|
||||
|
||||
std::vector<uint32_t> 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<uint32_t> 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<uint16_t> ReadTIFFFromString16(const std::string &s, uint32_t &cols, uint32_t &lines) {
|
||||
if (s.empty())
|
||||
throw JFJochException(JFJochExceptionCategory::TIFFGeneratorError, "No TIFF file provided");
|
||||
std::vector<uint8_t> buffer;
|
||||
CompressedImage image = ReadTIFF(s, buffer);
|
||||
|
||||
uint32_t rows_per_string = 0;
|
||||
|
||||
std::vector<uint16_t> 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<uint16_t> ret(static_cast<size_t>(cols) * lines);
|
||||
memcpy(ret.data(), buffer.data(), buffer.size());
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ void WriteTIFFToFile(const std::string &filename, const CompressedImage& image);
|
||||
|
||||
CompressedImage ReadTIFF(const std::string &s, std::vector<uint8_t> &buffer);
|
||||
|
||||
std::vector<uint32_t> ReadTIFFFromString32(const std::string& s, uint32_t &cols, uint32_t &lines);
|
||||
std::vector<uint16_t> ReadTIFFFromString16(const std::string& s, uint32_t &cols, uint32_t &lines);
|
||||
|
||||
void SuppressTIFFErrors();
|
||||
|
||||
@@ -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<uint8_t> values(static_cast<size_t>(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<uint32_t> values32(static_cast<size_t>(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<float> valuesf(static_cast<size_t>(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<uint8_t> wrong(static_cast<size_t>(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);
|
||||
|
||||
+46
-9
@@ -18,22 +18,59 @@ TEST_CASE("TIFFTest","[TIFF]") {
|
||||
}
|
||||
|
||||
TEST_CASE("TIFFTest_Write_Read","[TIFF]") {
|
||||
std::vector<uint32_t> values(512*1024), values_out;
|
||||
for (int i = 0; i < values.size(); i++) {
|
||||
std::vector<uint32_t> 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<uint8_t> 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<uint8_t> values(512*1024);
|
||||
for (int i = 0; i < values.size(); i++)
|
||||
values[i] = static_cast<uint8_t>(i * 17 + 2);
|
||||
|
||||
CompressedImage image(values, 1024, 512, CompressedImageMode::Uint8);
|
||||
|
||||
std::string s;
|
||||
REQUIRE_NOTHROW(s = WriteTIFFToString(image));
|
||||
|
||||
std::vector<uint8_t> 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<uint16_t> values(512*1024);
|
||||
for (int i = 0; i < values.size(); i++)
|
||||
values[i] = static_cast<uint16_t>(i * 17 + 2);
|
||||
|
||||
CompressedImage image(values, 1024, 512);
|
||||
|
||||
std::string s;
|
||||
REQUIRE_NOTHROW(s = WriteTIFFToString(image));
|
||||
|
||||
std::vector<uint8_t> 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]") {
|
||||
|
||||
Reference in New Issue
Block a user