diff --git a/modules/imgcodecs/src/grfmt_jpegxl.cpp b/modules/imgcodecs/src/grfmt_jpegxl.cpp index 7e0a191090..d94a722178 100644 --- a/modules/imgcodecs/src/grfmt_jpegxl.cpp +++ b/modules/imgcodecs/src/grfmt_jpegxl.cpp @@ -12,6 +12,19 @@ namespace cv { +// Callback functions for JpegXLDecoder +static void cbRGBtoBGR_8U(void *opaque, size_t x, size_t y, size_t num_pixels, const void *pixels); +static void cbRGBAtoBGRA_8U(void *opaque, size_t x, size_t y, size_t num_pixels, const void *pixels); +static void cbRGBtoBGR_16U(void *opaque, size_t x, size_t y, size_t num_pixels, const void *pixels); +static void cbRGBAtoBGRA_16U(void *opaque, size_t x, size_t y, size_t num_pixels, const void *pixels); +static void cbRGBtoBGR_32F(void *opaque, size_t x, size_t y, size_t num_pixels, const void *pixels); +static void cbRGBAtoBGRA_32F(void *opaque, size_t x, size_t y, size_t num_pixels, const void *pixels); +static void cbRGBtoGRAY_8U(void *opaque, size_t x, size_t y, size_t num_pixels, const void *pixels); +static void cbRGBAtoGRAY_8U(void *opaque, size_t x, size_t y, size_t num_pixels, const void *pixels); +static void cbRGBtoGRAY_16U(void *opaque, size_t x, size_t y, size_t num_pixels, const void *pixels); +static void cbRGBAtoGRAY_16U(void *opaque, size_t x, size_t y, size_t num_pixels, const void *pixels); +static void cbRGBtoGRAY_32F(void *opaque, size_t x, size_t y, size_t num_pixels, const void *pixels); +static void cbRGBAtoGRAY_32F(void *opaque, size_t x, size_t y, size_t num_pixels, const void *pixels); /////////////////////// JpegXLDecoder /////////////////// @@ -20,7 +33,7 @@ JpegXLDecoder::JpegXLDecoder() : m_f(nullptr, &fclose) m_signature = "\xFF\x0A"; m_decoder = nullptr; m_buf_supported = false; - m_type = m_convert = -1; + m_type = -1; m_status = JXL_DEC_NEED_MORE_INPUT; } @@ -37,7 +50,7 @@ void JpegXLDecoder::close() m_f.reset(); m_read_buffer = {}; m_width = m_height = 0; - m_type = m_convert = -1; + m_type = -1; m_status = JXL_DEC_NEED_MORE_INPUT; } @@ -76,7 +89,7 @@ ImageDecoder JpegXLDecoder::newDecoder() const return makePtr(); } -bool JpegXLDecoder::read(Mat* pimg) +bool JpegXLDecoder::readHeader() { // Open file if (!m_f) { @@ -106,24 +119,87 @@ bool JpegXLDecoder::read(Mat* pimg) } } + return read(); +} + +bool JpegXLDecoder::readData(Mat& img) +{ + if (!m_decoder || m_width == 0 || m_height == 0 || m_type == -1) + return false; + + // Prepare to decode image + const uint32_t scn = CV_MAT_CN(m_type); // from image + const uint32_t dcn = (uint32_t)img.channels(); // to OpenCV + const int depth = CV_MAT_DEPTH(img.type()); + JxlImageOutCallback cbFunc = nullptr; + + CV_CheckChannels(scn, (scn == 1 || scn == 3 || scn == 4), "Unsupported src channels"); + CV_CheckChannels(dcn, (dcn == 1 || dcn == 3 || dcn == 4), "Unsupported dst channels"); + CV_CheckDepth(depth, (depth == CV_8U || depth == CV_16U || depth == CV_32F), "Unsupported depth"); + + m_format = { + dcn, + JXL_TYPE_UINT8, // (temporary) + JXL_NATIVE_ENDIAN, // endianness + 0 // align stride to bytes + }; + switch (depth) { + case CV_8U: m_format.data_type = JXL_TYPE_UINT8; break; + case CV_16U: m_format.data_type = JXL_TYPE_UINT16; break; + case CV_32F: m_format.data_type = JXL_TYPE_FLOAT; break; + default: break; + } + // libjxl cannot read to BGR pixel order directly. + // So we have to use callback function to convert from RGB(A) to BGR(A). + if (!m_use_rgb) { + switch (dcn) { + case 1: break; + case 3: cbFunc = (depth == CV_32F)? cbRGBtoBGR_32F: (depth == CV_16U)? cbRGBtoBGR_16U: cbRGBtoBGR_8U; break; + case 4: cbFunc = (depth == CV_32F)? cbRGBAtoBGRA_32F: (depth == CV_16U)? cbRGBAtoBGRA_16U: cbRGBAtoBGRA_8U; break; + default: break; + } + } + // libjxl cannot convert from color image to gray image directly. + // So we have to use callback function to convert from RGB(A) to GRAY. + if( (scn >= 3) && (dcn == 1) ) + { + m_format.num_channels = scn; + switch (scn) { + case 3: cbFunc = (depth == CV_32F)? cbRGBtoGRAY_32F: (depth == CV_16U)? cbRGBtoGRAY_16U: cbRGBtoGRAY_8U; break; + case 4: cbFunc = (depth == CV_32F)? cbRGBAtoGRAY_32F: (depth == CV_16U)? cbRGBAtoGRAY_16U: cbRGBAtoGRAY_8U; break; + default: break; + } + } + if(cbFunc != nullptr) + { + if (JXL_DEC_SUCCESS != JxlDecoderSetImageOutCallback(m_decoder.get(), + &m_format, + cbFunc, + static_cast(&img))) + { + return false; + } + }else{ + if (JXL_DEC_SUCCESS != JxlDecoderSetImageOutBuffer(m_decoder.get(), + &m_format, + img.ptr(), + img.total() * img.elemSize())) + { + return false; + } + } + + return read(); +} + +// Common reading routine for readHeader() and readBody() +bool JpegXLDecoder::read() +{ // Create buffer for reading const size_t read_buffer_size = 16384; // 16KB chunks if (m_read_buffer.capacity() < read_buffer_size) m_read_buffer.resize(read_buffer_size); - // Create image if needed - if (m_type != -1 && pimg) { - pimg->create(m_height, m_width, m_type); - if (!pimg->isContinuous()) - return false; - if (JXL_DEC_SUCCESS != JxlDecoderSetImageOutBuffer(m_decoder.get(), - &m_format, - pimg->ptr(), - pimg->total() * pimg->elemSize())) { - return false; - } - } - // Start decoding loop do { // Check if we need more input @@ -163,6 +239,7 @@ bool JpegXLDecoder::read(Mat* pimg) case JXL_DEC_BASIC_INFO: { if (m_type != -1) return false; + JxlBasicInfo info; if (JXL_DEC_SUCCESS != JxlDecoderGetBasicInfo(m_decoder.get(), &info)) return false; @@ -172,49 +249,18 @@ bool JpegXLDecoder::read(Mat* pimg) m_width = info.xsize; m_height = info.ysize; - m_format = { - ncn, - JXL_TYPE_UINT8, // (temporary) - JXL_LITTLE_ENDIAN, // endianness - 0 // align stride to bytes - }; - if (!m_use_rgb) { - switch (ncn) { - case 3: - m_convert = cv::COLOR_RGB2BGR; - break; - case 4: - m_convert = cv::COLOR_RGBA2BGRA; - break; - default: - m_convert = -1; - } + int depth = (info.exponent_bits_per_sample > 0)?CV_32F: + (info.bits_per_sample == 16)?CV_16U: + (info.bits_per_sample == 8)?CV_8U: -1; + if(depth == -1) + { + return false; // Return to readHeader() } - if (info.exponent_bits_per_sample > 0) { - m_format.data_type = JXL_TYPE_FLOAT; - m_type = CV_MAKETYPE( CV_32F, ncn ); - } else { - switch (info.bits_per_sample) { - case 8: - m_format.data_type = JXL_TYPE_UINT8; - m_type = CV_MAKETYPE( CV_8U, ncn ); - break; - case 16: - m_format.data_type = JXL_TYPE_UINT16; - m_type = CV_MAKETYPE( CV_16U, ncn ); - break; - default: - return false; - } - } - if (!pimg) - return true; - break; + m_type = CV_MAKETYPE( depth, ncn ); + return true; } case JXL_DEC_FULL_IMAGE: { // Image is ready - if (m_convert != -1) - cv::cvtColor(*pimg, *pimg, m_convert); break; } case JXL_DEC_ERROR: { @@ -229,17 +275,172 @@ bool JpegXLDecoder::read(Mat* pimg) return true; } -bool JpegXLDecoder::readHeader() +// Callback functopms +static void cbRGBtoBGR_8U(void *opaque, size_t x, size_t y, size_t num_pixels, const void *pixels) { - close(); - return read(nullptr); + const uint8_t* src = static_cast(pixels); + + constexpr int dstStep = 3; + const cv::Mat *pDst = static_cast(opaque); + uint8_t* dstBase = const_cast(pDst->ptr(y)); + uint8_t* dst = dstBase + x * dstStep; + + icvCvt_RGB2BGR_8u_C3R( src, 0, dst, 0, Size(num_pixels , 1) ); +} +static void cbRGBAtoBGRA_8U(void *opaque, size_t x, size_t y, size_t num_pixels, const void *pixels) +{ + const uint8_t* src = static_cast(pixels); + + constexpr int dstStep = 4; + const cv::Mat *pDst = static_cast(opaque); + uint8_t* dstBase = const_cast(reinterpret_cast(pDst->ptr(y))); + uint8_t* dst = dstBase + x * dstStep; + + icvCvt_RGBA2BGRA_8u_C4R( src, 0, dst, 0, Size(num_pixels, 1) ); +} +static void cbRGBtoBGR_16U(void *opaque, size_t x, size_t y, size_t num_pixels, const void *pixels) +{ + const uint16_t* src = static_cast(pixels); + + constexpr int dstStep = 3; + const cv::Mat *pDst = static_cast(opaque); + uint16_t* dstBase = const_cast(reinterpret_cast(pDst->ptr(y))); + uint16_t* dst = dstBase + x * dstStep; + + icvCvt_BGR2RGB_16u_C3R( src, 0, dst, 0, Size(num_pixels, 1)); +} +static void cbRGBAtoBGRA_16U(void *opaque, size_t x, size_t y, size_t num_pixels, const void *pixels) +{ + const uint16_t* src = static_cast(pixels); + + constexpr int dstStep = 4; + const cv::Mat *pDst = static_cast(opaque); + uint16_t* dstBase = const_cast(reinterpret_cast(pDst->ptr(y))); + uint16_t* dst = dstBase + x * dstStep; + + icvCvt_BGRA2RGBA_16u_C4R( src, 0, dst, 0, Size(num_pixels, 1)); +} +static void cbRGBtoBGR_32F(void *opaque, size_t x, size_t y, size_t num_pixels, const void *pixels) +{ + constexpr int srcStep = 3; + const uint32_t* src = static_cast(pixels); + + constexpr int dstStep = 3; + const cv::Mat *pDst = static_cast(opaque); + uint32_t* dstBase = const_cast(reinterpret_cast(pDst->ptr(y))); + uint32_t* dst = dstBase + x * dstStep; + + for(size_t i = 0 ; i < num_pixels; i++) + { + dst[ i * dstStep + 0 ] = src[ i * srcStep + 2]; + dst[ i * dstStep + 1 ] = src[ i * srcStep + 1]; + dst[ i * dstStep + 2 ] = src[ i * srcStep + 0]; + } +} +static void cbRGBAtoBGRA_32F(void *opaque, size_t x, size_t y, size_t num_pixels, const void *pixels) +{ + constexpr int srcStep = 4; + const uint32_t* src = static_cast(pixels); + + constexpr int dstStep = 4; + const cv::Mat *pDst = static_cast(opaque); + uint32_t* dstBase = const_cast(reinterpret_cast(pDst->ptr(y))); + uint32_t* dst = dstBase + x * dstStep; + + for(size_t i = 0 ; i < num_pixels; i++) + { + dst[ i * dstStep + 0 ] = src[ i * srcStep + 2]; + dst[ i * dstStep + 1 ] = src[ i * srcStep + 1]; + dst[ i * dstStep + 2 ] = src[ i * srcStep + 0]; + dst[ i * dstStep + 3 ] = src[ i * srcStep + 3]; + } } -bool JpegXLDecoder::readData(Mat& img) +static void cbRGBtoGRAY_8U(void *opaque, size_t x, size_t y, size_t num_pixels, const void *pixels) { - if (!m_decoder || m_width == 0 || m_height == 0) - return false; - return read(&img); + const uint8_t* src = static_cast(pixels); + + constexpr int dstStep = 1; + const cv::Mat *pDst = static_cast(opaque); + uint8_t* dstBase = const_cast(reinterpret_cast(pDst->ptr(y))); + uint8_t* dst = dstBase + x * dstStep; + + icvCvt_BGR2Gray_8u_C3C1R(src, 0, dst, 0, Size(num_pixels, 1) ); +} +static void cbRGBAtoGRAY_8U(void *opaque, size_t x, size_t y, size_t num_pixels, const void *pixels) +{ + const uint8_t* src = static_cast(pixels); + + constexpr int dstStep = 1; + const cv::Mat *pDst = static_cast(opaque); + uint8_t* dstBase = const_cast(reinterpret_cast(pDst->ptr(y))); + uint8_t* dst = dstBase + x * dstStep; + + icvCvt_BGRA2Gray_8u_C4C1R(src, 0, dst, 0, Size(num_pixels, 1) ); +} +static void cbRGBtoGRAY_16U(void *opaque, size_t x, size_t y, size_t num_pixels, const void *pixels) +{ + const uint16_t* src = static_cast(pixels); + + constexpr int dstStep = 1; + const cv::Mat *pDst = static_cast(opaque); + uint16_t* dstBase = const_cast(reinterpret_cast(pDst->ptr(y))); + uint16_t* dst = dstBase + x * dstStep; + + icvCvt_BGRA2Gray_16u_CnC1R(src, 0, dst, 0, Size(num_pixels, 1), /* ncn= */ 3 ); +} +static void cbRGBAtoGRAY_16U(void *opaque, size_t x, size_t y, size_t num_pixels, const void *pixels) +{ + const uint16_t* src = static_cast(pixels); + + constexpr int dstStep = 1; + const cv::Mat *pDst = static_cast(opaque); + uint16_t* dstBase = const_cast(reinterpret_cast(pDst->ptr(y))); + uint16_t* dst = dstBase + x * dstStep; + + icvCvt_BGRA2Gray_16u_CnC1R(src, 0, dst, 0, Size(num_pixels, 1), /* ncn= */ 4 ); +} +static void cbRGBtoGRAY_32F(void *opaque, size_t x, size_t y, size_t num_pixels, const void *pixels) +{ + constexpr float cR = 0.299f; + constexpr float cG = 0.587f; + constexpr float cB = 1.000f - cR - cG; + + constexpr int srcStep = 3; + const float* src = static_cast(pixels); + + constexpr int dstStep = 1; + const cv::Mat *pDst = static_cast(opaque); + float* dstBase = const_cast(reinterpret_cast(pDst->ptr(y))); + float* dst = dstBase + x * dstStep; + + for(size_t i = 0 ; i < num_pixels; i++) + { + dst[ i * dstStep ] = src[ i * srcStep + 0] * cR + + src[ i * srcStep + 1] * cG + + src[ i * srcStep + 2] * cB; + } +} +static void cbRGBAtoGRAY_32F(void *opaque, size_t x, size_t y, size_t num_pixels, const void *pixels) +{ + constexpr float cR = 0.299f; + constexpr float cG = 0.587f; + constexpr float cB = 1.000f - cR - cG; + + constexpr int srcStep = 4; + const float* src = static_cast(pixels); + + constexpr int dstStep = 1; + const cv::Mat *pDst = static_cast(opaque); + float* dstBase = const_cast(reinterpret_cast(pDst->ptr(y))); + float* dst = dstBase + x * dstStep; + + for(size_t i = 0 ; i < num_pixels; i++) + { + dst[ i * dstStep ] = src[ i * srcStep + 0] * cR + + src[ i * srcStep + 1] * cG + + src[ i * srcStep + 2] * cB; + } } /////////////////////// JpegXLEncoder /////////////////// diff --git a/modules/imgcodecs/src/grfmt_jpegxl.hpp b/modules/imgcodecs/src/grfmt_jpegxl.hpp index 87e4bfcba5..e1b971a157 100644 --- a/modules/imgcodecs/src/grfmt_jpegxl.hpp +++ b/modules/imgcodecs/src/grfmt_jpegxl.hpp @@ -41,12 +41,11 @@ protected: JxlDecoderPtr m_decoder; JxlThreadParallelRunnerPtr m_parallel_runner; JxlPixelFormat m_format; - int m_convert; std::vector m_read_buffer; JxlDecoderStatus m_status; private: - bool read(Mat* pimg); + bool read(); }; diff --git a/modules/imgcodecs/test/test_jpegxl.cpp b/modules/imgcodecs/test/test_jpegxl.cpp index 6c5aad181f..8a94faa42c 100644 --- a/modules/imgcodecs/test/test_jpegxl.cpp +++ b/modules/imgcodecs/test/test_jpegxl.cpp @@ -180,6 +180,75 @@ TEST(Imgcodecs_JpegXL, encode_from_uncontinued_image) EXPECT_TRUE(ret); } +// See https://github.com/opencv/opencv/issues/26767 + +typedef tuple MatType_and_ImreadFlag; +typedef testing::TestWithParam Imgcodecs_JpegXL_MatType_ImreadFlag; + +TEST_P(Imgcodecs_JpegXL_MatType_ImreadFlag, all_imreadFlags) +{ + string tmp_fname = cv::tempfile(".jxl"); + const int matType = get<0>(GetParam()); + const int imreadFlag = get<1>(GetParam()); + + Mat img(240, 320, matType); + randu(img, Scalar(0, 0, 0, 255), Scalar(255, 255, 255, 255)); + + vector param; + param.push_back(IMWRITE_JPEGXL_DISTANCE); + param.push_back(0 /* Lossless */); + EXPECT_NO_THROW(imwrite(tmp_fname, img, param)); + + Mat img_decoded; + EXPECT_NO_THROW(img_decoded = imread(tmp_fname, imreadFlag)); + EXPECT_FALSE(img_decoded.empty()); + + switch( imreadFlag ) + { + case IMREAD_UNCHANGED: + EXPECT_EQ( img.type(), img_decoded.type() ); + break; + case IMREAD_GRAYSCALE: + EXPECT_EQ( img_decoded.depth(), CV_8U ); + EXPECT_EQ( img_decoded.channels(), 1 ); + break; + case IMREAD_COLOR: + case IMREAD_COLOR_RGB: + EXPECT_EQ( img_decoded.depth(), CV_8U ); + EXPECT_EQ( img_decoded.channels(), 3 ); + break; + case IMREAD_ANYDEPTH: + EXPECT_EQ( img_decoded.depth(), img.depth() ); + EXPECT_EQ( img_decoded.channels(), 1 ); + break; + case IMREAD_ANYCOLOR: + EXPECT_EQ( img_decoded.depth(), CV_8U ) ; + EXPECT_EQ( img_decoded.channels(), img.channels() == 1 ? 1 : 3 ); // Alpha channel will be dropped. + break; + } + remove(tmp_fname.c_str()); +} + +INSTANTIATE_TEST_CASE_P( + /**/, + Imgcodecs_JpegXL_MatType_ImreadFlag, + testing::Combine( + testing::Values( + CV_8UC1, CV_8UC3, CV_8UC4, + CV_16UC1, CV_16UC3, CV_16UC4, + CV_32FC1, CV_32FC3, CV_32FC4 + ), + testing::Values( + IMREAD_UNCHANGED, + IMREAD_GRAYSCALE, + IMREAD_COLOR, + IMREAD_COLOR_RGB, + IMREAD_ANYDEPTH, + IMREAD_ANYCOLOR + ) +) ); + + #endif // HAVE_JPEGXL } // namespace diff --git a/modules/imgcodecs/test/test_precomp.hpp b/modules/imgcodecs/test/test_precomp.hpp index f5fb5c82e4..d2095932e8 100644 --- a/modules/imgcodecs/test/test_precomp.hpp +++ b/modules/imgcodecs/test/test_precomp.hpp @@ -25,6 +25,16 @@ void PrintTo(const ImreadModes& val, std::ostream* os) v &= ~IMREAD_COLOR; *os << "IMREAD_COLOR" << (v == 0 ? "" : " | "); } + else if ((v & IMREAD_COLOR_RGB) != 0) + { + CV_Assert(IMREAD_COLOR_RGB == 256); + v &= ~IMREAD_COLOR_RGB; + *os << "IMREAD_COLOR_RGB" << (v == 0 ? "" : " | "); + } + else if ((v & IMREAD_ANYCOLOR) != 0) + { + // Do nothing + } else { CV_Assert(IMREAD_GRAYSCALE == 0); @@ -50,11 +60,6 @@ void PrintTo(const ImreadModes& val, std::ostream* os) v &= ~IMREAD_IGNORE_ORIENTATION; *os << "IMREAD_IGNORE_ORIENTATION" << (v == 0 ? "" : " | "); } - if ((v & IMREAD_COLOR_RGB) != 0) - { - v &= ~IMREAD_COLOR_RGB; - *os << "IMREAD_COLOR_RGB" << (v == 0 ? "" : " | "); - } switch (v) { case IMREAD_UNCHANGED: return;