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
This commit is contained in:
xaos-cz
2025-07-14 15:04:25 +02:00
committed by GitHub
parent 4c024c35fb
commit 69b2024a3d
5 changed files with 130 additions and 36 deletions

View File

@@ -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).

View File

@@ -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<int>& 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<int>& 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 ));
}

View File

@@ -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

View File

@@ -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<Mat>& 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<ExrEncoder>() );
#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<ExrEncoder>() );
#else
CV_Assert( channels == 1 || channels == 3 || channels == 4 );
#endif
Mat temp;
if( !encoder->isFormatSupported(image.depth()) )

View File

@@ -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<cv::Vec<float, 7>>(0, 0)[0] = 101.125;
img.at<cv::Vec<float, 7>>(2, 1)[3] = 203.500;
img.at<cv::Vec<float, 7>>(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)
{