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:
2026-06-18 16:05:04 +02:00
co-authored by Claude Opus 4.8
parent 57ae145555
commit 0f5271f14c
10 changed files with 165 additions and 82 deletions
+2 -3
View File
@@ -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;
}
+15
View File
@@ -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();
+1
View File
@@ -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;
+4 -2
View File
@@ -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:
+49
View File
@@ -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,
+2
View File
@@ -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
View File
@@ -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;
}
-1
View File
@@ -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();
+38
View File
@@ -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
View File
@@ -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]") {