From 69b2024a3dac6f51291523d6e31a0337bc98837f Mon Sep 17 00:00:00 2001 From: xaos-cz <64235805+xaos-cz@users.noreply.github.com> Date: Mon, 14 Jul 2025 15:04:25 +0200 Subject: [PATCH] imgcodecs: OpenEXR multispectral read:write support (#27485) imgcodecs: OpenEXR multispectral read/write support #27485 OpenCV Extra: https://github.com/opencv/opencv_extra/pull/1262/ Adds capability to read and write multispectral (>4 channels) images in OpenEXR format. ### Pull Request Readiness Checklist See details at https://github.com/opencv/opencv/wiki/How_to_contribute#making-a-good-pull-request - [x] I agree to contribute to the project under Apache 2 License. - [x] To the best of my knowledge, the proposed patch is not based on a code under GPL or another license that is incompatible with OpenCV - [x] The PR is proposed to the proper branch - [ ] There is a reference to the original bug report and related work - [x] There is accuracy test, performance test and test data in opencv_extra repository, if applicable Patch to opencv_extra has the same branch name. - [ ] The feature is well documented and sample code can be built with the project CMake --- .../imgcodecs/include/opencv2/imgcodecs.hpp | 2 +- modules/imgcodecs/src/grfmt_exr.cpp | 120 +++++++++++++----- modules/imgcodecs/src/grfmt_exr.hpp | 2 + modules/imgcodecs/src/loadsave.cpp | 12 ++ modules/imgcodecs/test/test_exr.impl.hpp | 30 +++++ 5 files changed, 130 insertions(+), 36 deletions(-) diff --git a/modules/imgcodecs/include/opencv2/imgcodecs.hpp b/modules/imgcodecs/include/opencv2/imgcodecs.hpp index d0f6ee61d6..14d562c823 100644 --- a/modules/imgcodecs/include/opencv2/imgcodecs.hpp +++ b/modules/imgcodecs/include/opencv2/imgcodecs.hpp @@ -484,7 +484,7 @@ filename extension (see cv::imread for the list of extensions). In general, only single-channel or 3-channel (with 'BGR' channel order) images can be saved using this function, with these exceptions: -- With OpenEXR encoder, only 32-bit float (CV_32F) images can be saved. +- With OpenEXR encoder, only 32-bit float (CV_32F) images can be saved. More than 4 channels can be saved. (imread can load it then.) - 8-bit unsigned (CV_8U) images are not supported. - With Radiance HDR encoder, non 64-bit float (CV_64F) images can be saved. - All images will be converted to 32-bit float (CV_32F). diff --git a/modules/imgcodecs/src/grfmt_exr.cpp b/modules/imgcodecs/src/grfmt_exr.cpp index 0ffd0d2618..44a0934517 100644 --- a/modules/imgcodecs/src/grfmt_exr.cpp +++ b/modules/imgcodecs/src/grfmt_exr.cpp @@ -118,7 +118,8 @@ ExrDecoder::ExrDecoder() m_ischroma = false; m_hasalpha = false; m_native_depth = false; - + m_multispectral = false; + m_channels = 0; } @@ -140,7 +141,7 @@ void ExrDecoder::close() int ExrDecoder::type() const { - return CV_MAKETYPE((m_isfloat ? CV_32F : CV_32S), ((m_iscolor && m_hasalpha) ? 4 : m_iscolor ? 3 : m_hasalpha ? 2 : 1)); + return CV_MAKETYPE((m_isfloat ? CV_32F : CV_32S), (m_multispectral ? m_channels : (m_iscolor && m_hasalpha) ? 4 : m_iscolor ? 3 : m_hasalpha ? 2 : 1)); } @@ -169,6 +170,7 @@ bool ExrDecoder::readHeader() m_green = channels.findChannel( "G" ); m_blue = channels.findChannel( "B" ); m_alpha = channels.findChannel( "A" ); + m_multispectral = channels.findChannel( "0" ) != nullptr; if( m_alpha ) // alpha channel supported in RGB, Y, and YC scenarios m_hasalpha = true; @@ -179,6 +181,23 @@ bool ExrDecoder::readHeader() m_ischroma = false; result = true; } + else if( m_multispectral ) + { + m_channels = 0; + for( auto it = channels.begin(); it != channels.end(); it++ ) + m_channels++; + + m_iscolor = true; // ??? false + m_ischroma = false; + m_hasalpha = false; + result = m_channels <= CV_CN_MAX; + + for ( int i = 1; result && i < m_channels; i++ ) // channel 0 was found previously + { + const Channel *ch = channels.findChannel( std::to_string(i) ); + result = ch && ch->xSampling == 1 && ch->ySampling == 1; // subsampling is not supported + } + } else { m_green = channels.findChannel( "Y" ); @@ -214,8 +233,9 @@ bool ExrDecoder::readHeader() bool ExrDecoder::readData( Mat& img ) { m_native_depth = CV_MAT_DEPTH(type()) == img.depth(); + bool multispectral = img.channels() > 4; bool color = img.channels() > 2; // output mat has 3+ channels; Y or YA are the 1 and 2 channel scenario - bool alphasupported = ( img.channels() % 2 == 0 ); // even number of channels indicates alpha + bool alphasupported = !multispectral && ( img.channels() % 2 == 0 ); // even number of channels indicates alpha int channels = 0; uchar* data = img.ptr(); size_t step = img.step; @@ -231,10 +251,17 @@ bool ExrDecoder::readData( Mat& img ) const size_t floatsize = sizeof(float); size_t xstep = m_native_depth ? floatsize : 1; // 4 bytes if native depth (FLOAT), otherwise converting to 1 byte U8 depth size_t ystep = 0; - const int channelstoread = ( (m_iscolor && alphasupported) ? 4 : + const int channelstoread = ( multispectral ? img.channels() : (m_iscolor && alphasupported) ? 4 : ( (m_iscolor && !m_ischroma) || color) ? 3 : alphasupported ? 2 : 1 ); // number of channels to read may exceed channels in output img size_t xStride = floatsize * channelstoread; + if ( m_multispectral ) // possible gray/RGB conversions + { + CV_CheckChannelsEQ(img.channels(), CV_MAT_CN(type()), "OpenCV EXR decoder needs more number of channels for multispectral images. Use cv::IMREAD_UNCHANGED mode for imread."); // IMREAD_ANYCOLOR needed + CV_CheckDepthEQ(img.depth(), CV_MAT_DEPTH(type()), "OpenCV EXR decoder supports CV_32F depth only for multispectral images. Use cv::IMREAD_UNCHANGED mode for imread."); // IMREAD_ANYDEPTH needed + } + CV_Assert( multispectral == m_multispectral && (!multispectral || justcopy) ); // should be true after previous checks + // See https://github.com/opencv/opencv/issues/26705 // If ALGO_HINT_ACCURATE is set, read BGR and swap to RGB. // If ALGO_HINT_APPROX is set, read RGB directly. @@ -312,6 +339,15 @@ bool ExrDecoder::readData( Mat& img ) xsample[0] = m_green->xSampling; } } + else if( m_multispectral ) + { + for ( int i = 0; i < m_channels; i++ ) + { + frame.insert( std::to_string(i), Slice( m_type, + buffer - m_datawindow.min.x * xStride - m_datawindow.min.y * ystep + (floatsize * i), + xStride, ystep, 1, 1, 0.0 )); + } + } else { if( m_blue ) @@ -382,39 +418,42 @@ bool ExrDecoder::readData( Mat& img ) { m_file->readPixels( m_datawindow.min.y, m_datawindow.max.y ); - if( m_iscolor ) + if( !m_multispectral ) { - if (doReadRGB) + if( m_iscolor ) { - if( m_red && (m_red->xSampling != 1 || m_red->ySampling != 1) ) - UpSample( data, channelstoread, step / xstep, m_red->xSampling, m_red->ySampling ); - if( m_green && (m_green->xSampling != 1 || m_green->ySampling != 1) ) - UpSample( data + xstep, channelstoread, step / xstep, m_green->xSampling, m_green->ySampling ); - if( m_blue && (m_blue->xSampling != 1 || m_blue->ySampling != 1) ) - UpSample( data + 2 * xstep, channelstoread, step / xstep, m_blue->xSampling, m_blue->ySampling ); + if (doReadRGB) + { + if( m_red && (m_red->xSampling != 1 || m_red->ySampling != 1) ) + UpSample( data, channelstoread, step / xstep, m_red->xSampling, m_red->ySampling ); + if( m_green && (m_green->xSampling != 1 || m_green->ySampling != 1) ) + UpSample( data + xstep, channelstoread, step / xstep, m_green->xSampling, m_green->ySampling ); + if( m_blue && (m_blue->xSampling != 1 || m_blue->ySampling != 1) ) + UpSample( data + 2 * xstep, channelstoread, step / xstep, m_blue->xSampling, m_blue->ySampling ); + } + else + { + if( m_blue && (m_blue->xSampling != 1 || m_blue->ySampling != 1) ) + UpSample( data, channelstoread, step / xstep, m_blue->xSampling, m_blue->ySampling ); + if( m_green && (m_green->xSampling != 1 || m_green->ySampling != 1) ) + UpSample( data + xstep, channelstoread, step / xstep, m_green->xSampling, m_green->ySampling ); + if( m_red && (m_red->xSampling != 1 || m_red->ySampling != 1) ) + UpSample( data + 2 * xstep, channelstoread, step / xstep, m_red->xSampling, m_red->ySampling ); + } } - else - { - if( m_blue && (m_blue->xSampling != 1 || m_blue->ySampling != 1) ) - UpSample( data, channelstoread, step / xstep, m_blue->xSampling, m_blue->ySampling ); - if( m_green && (m_green->xSampling != 1 || m_green->ySampling != 1) ) - UpSample( data + xstep, channelstoread, step / xstep, m_green->xSampling, m_green->ySampling ); - if( m_red && (m_red->xSampling != 1 || m_red->ySampling != 1) ) - UpSample( data + 2 * xstep, channelstoread, step / xstep, m_red->xSampling, m_red->ySampling ); - } - } - else if( m_green && (m_green->xSampling != 1 || m_green->ySampling != 1) ) - UpSample( data, channelstoread, step / xstep, m_green->xSampling, m_green->ySampling ); + else if( m_green && (m_green->xSampling != 1 || m_green->ySampling != 1) ) + UpSample( data, channelstoread, step / xstep, m_green->xSampling, m_green->ySampling ); - if( chromatorgb ) - { - if (doReadRGB) - ChromaToRGB( (float *)data, m_height, channelstoread, step / xstep ); - else - ChromaToBGR( (float *)data, m_height, channelstoread, step / xstep ); + if( chromatorgb ) + { + if (doReadRGB) + ChromaToRGB( (float *)data, m_height, channelstoread, step / xstep ); + else + ChromaToBGR( (float *)data, m_height, channelstoread, step / xstep ); + } } } - else + else // m_multispectral should be false { uchar *out = data; int x, y; @@ -804,13 +843,19 @@ bool ExrEncoder::write( const Mat& img, const std::vector& params ) header.channels().insert( "B", Channel( type ) ); //printf("bunt\n"); } - else + else if( channels == 1 || channels == 2 ) { header.channels().insert( "Y", Channel( type ) ); //printf("gray\n"); } + else if( channels > 4 ) + { + for ( int i = 0; i < channels; i++ ) + header.channels().insert( std::to_string(i), Channel( type ) ); + //printf("multi-channel\n"); + } - if( channels % 2 == 0 ) + if( channels % 2 == 0 && channels <= 4) { // even number of channels indicates Alpha header.channels().insert( "A", Channel( type ) ); } @@ -843,10 +888,15 @@ bool ExrEncoder::write( const Mat& img, const std::vector& params ) frame.insert( "G", Slice( type, buffer + size, size * channels, bufferstep )); frame.insert( "R", Slice( type, buffer + size * 2, size * channels, bufferstep )); } - else + else if( channels == 1 || channels == 2 ) frame.insert( "Y", Slice( type, buffer, size * channels, bufferstep )); + else if( channels > 4 ) + { + for ( int i = 0; i < channels; i++ ) + frame.insert( std::to_string(i), Slice( type, buffer + size * i, size * channels, bufferstep )); + } - if( channels % 2 == 0 ) + if( channels % 2 == 0 && channels <= 4 ) { // even channel count indicates Alpha channel frame.insert( "A", Slice( type, buffer + size * (channels - 1), size * channels, bufferstep )); } diff --git a/modules/imgcodecs/src/grfmt_exr.hpp b/modules/imgcodecs/src/grfmt_exr.hpp index 48ca09acd8..ec37649d17 100644 --- a/modules/imgcodecs/src/grfmt_exr.hpp +++ b/modules/imgcodecs/src/grfmt_exr.hpp @@ -100,6 +100,8 @@ protected: bool m_iscolor; bool m_isfloat; bool m_hasalpha; + bool m_multispectral; + int m_channels; private: ExrDecoder(const ExrDecoder &); // copy disabled diff --git a/modules/imgcodecs/src/loadsave.cpp b/modules/imgcodecs/src/loadsave.cpp index 8f811f9085..d1e3bc93cf 100644 --- a/modules/imgcodecs/src/loadsave.cpp +++ b/modules/imgcodecs/src/loadsave.cpp @@ -98,6 +98,9 @@ static inline int calcType(int type, int flags) if( (flags & IMREAD_ANYDEPTH) == 0 ) type = CV_MAKETYPE(CV_8U, CV_MAT_CN(type)); + //if( (flags & IMREAD_ANYCOLOR) != 0 /*&& CV_MAT_CN(type) > 1*/ ) + // type = CV_MAKETYPE(CV_MAT_DEPTH(type), CV_MAT_CN(type)); + //else if( (flags & IMREAD_COLOR) != 0 || (flags & IMREAD_COLOR_RGB) != 0 ) if( (flags & IMREAD_COLOR) != 0 || (flags & IMREAD_COLOR_RGB) != 0 || ((flags & IMREAD_ANYCOLOR) != 0 && CV_MAT_CN(type) > 1) ) type = CV_MAKETYPE(CV_MAT_DEPTH(type), 3); @@ -1055,7 +1058,12 @@ static bool imwrite_( const String& filename, const std::vector& img_vec, Mat image = img_vec[page]; CV_Assert(!image.empty()); +#ifdef HAVE_OPENEXR + CV_Assert( image.channels() == 1 || image.channels() == 3 || image.channels() == 4 || encoder.dynamicCast() ); +#else CV_Assert( image.channels() == 1 || image.channels() == 3 || image.channels() == 4 ); +#endif + Mat temp; if( !encoder->isFormatSupported(image.depth()) ) @@ -1609,7 +1617,11 @@ bool imencodeWithMetadata( const String& ext, InputArray _img, CV_Assert(!image.empty()); const int channels = image.channels(); +#ifdef HAVE_OPENEXR + CV_Assert( channels == 1 || channels == 3 || channels == 4 || encoder.dynamicCast() ); +#else CV_Assert( channels == 1 || channels == 3 || channels == 4 ); +#endif Mat temp; if( !encoder->isFormatSupported(image.depth()) ) diff --git a/modules/imgcodecs/test/test_exr.impl.hpp b/modules/imgcodecs/test/test_exr.impl.hpp index 6b4ac0b8d1..d439b7da44 100644 --- a/modules/imgcodecs/test/test_exr.impl.hpp +++ b/modules/imgcodecs/test/test_exr.impl.hpp @@ -68,6 +68,36 @@ TEST(Imgcodecs_EXR, readWrite_32FC3) EXPECT_EQ(0, remove(filenameOutput.c_str())); } +TEST(Imgcodecs_EXR, readWrite_32FC7) +{ // 0-6 channels (multispectral) + const string root = cvtest::TS::ptr()->get_data_path(); + const string filenameInput = root + "readwrite/test32FC7.exr"; + const string filenameOutput = cv::tempfile(".exr"); +#ifndef GENERATE_DATA + const Mat img = cv::imread(filenameInput, IMREAD_UNCHANGED); +#else + const Size sz(3, 5); + Mat img(sz, CV_32FC7); + img.at>(0, 0)[0] = 101.125; + img.at>(2, 1)[3] = 203.500; + img.at>(4, 2)[6] = 305.875; + ASSERT_TRUE(cv::imwrite(filenameInput, img)); +#endif + ASSERT_FALSE(img.empty()); + ASSERT_EQ(CV_MAKETYPE(CV_32F, 7), img.type()); + + ASSERT_TRUE(cv::imwrite(filenameOutput, img)); + const Mat img2 = cv::imread(filenameOutput, IMREAD_UNCHANGED); + EXPECT_EQ(img2.type(), img.type()); + EXPECT_EQ(img2.size(), img.size()); + EXPECT_LE(cvtest::norm(img, img2, NORM_INF | NORM_RELATIVE), 1e-3); + EXPECT_EQ(0, remove(filenameOutput.c_str())); + const Mat img3 = cv::imread(filenameInput, IMREAD_GRAYSCALE); + ASSERT_TRUE(img3.empty()); + const Mat img4 = cv::imread(filenameInput, IMREAD_COLOR); + ASSERT_TRUE(img4.empty()); +} + TEST(Imgcodecs_EXR, readWrite_32FC1_half) {