1
0
mirror of https://github.com/opencv/opencv.git synced 2026-07-29 15:23:05 +04:00

Merge pull request #27559 from Kumataro:fix27555

imgcodecs: bmp: support to write 32bpp BMP with BI_BITFIELDS #27559

### 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
- [x] 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:
Kumataro
2025-09-09 15:23:43 +09:00
committed by GitHub
parent ee41f46b46
commit 9196edd299
4 changed files with 145 additions and 4 deletions
@@ -119,6 +119,7 @@ enum ImwriteFlags {
IMWRITE_JPEGXL_EFFORT = 641,//!< For JPEG XL, encoder effort/speed level without affecting decoding speed; it is between 1 (fastest) and 10 (slowest). Default is 7.
IMWRITE_JPEGXL_DISTANCE = 642,//!< For JPEG XL, distance level for lossy compression: target max butteraugli distance, lower = higher quality, 0 = lossless; range: 0 .. 25. Default is 1.
IMWRITE_JPEGXL_DECODING_SPEED = 643,//!< For JPEG XL, decoding speed tier for the provided options; minimum is 0 (slowest to decode, best quality/density), and maximum is 4 (fastest to decode, at the cost of some quality/density). Default is 0.
IMWRITE_BMP_COMPRESSION = 768, //!< For BMP, use to specify compress parameter for 32bpp image. Default is IMWRITE_BMP_COMPRESSION_BITFIELDS. See cv::ImwriteBMPCompressionFlags.
IMWRITE_GIF_LOOP = 1024, //!< Not functional since 4.12.0. Replaced by cv::Animation::loop_count.
IMWRITE_GIF_SPEED = 1025, //!< Not functional since 4.12.0. Replaced by cv::Animation::durations.
IMWRITE_GIF_QUALITY = 1026, //!< For GIF, it can be a quality from 1 to 8. Default is 2. See cv::ImwriteGifCompressionFlags.
@@ -245,6 +246,12 @@ enum ImwriteHDRCompressionFlags {
IMWRITE_HDR_COMPRESSION_RLE = 1
};
//! Imwrite BMP specific values for IMWRITE_BMP_COMPRESSION parameter key.
enum ImwriteBMPCompressionFlags {
IMWRITE_BMP_COMPRESSION_RGB = 0, //!< Use BI_RGB. OpenCV v4.12.0 or before supports to encode with this compression only.
IMWRITE_BMP_COMPRESSION_BITFIELDS = 3, //!< Use BI_BITFIELDS. OpenCV v4.13.0 or later can support to encode with this compression. (only for 32 BPP images)
};
//! Imwrite GIF specific values for IMWRITE_GIF_QUALITY parameter key, if larger than 3, then its related to the size of the color table.
enum ImwriteGIFCompressionFlags {
IMWRITE_GIF_FAST_NO_DITHER = 1,
@@ -499,6 +506,11 @@ 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 BMP encoder, 8-bit unsigned (CV_8U) images can be saved.
- BMP images with an alpha channel can be saved using this function.
To achieve this, create an 8-bit 4-channel (CV_8UC4) BGRA image, ensuring the alpha channel is the last component.
Fully transparent pixels should have an alpha value of 0, while fully opaque pixels should have an alpha value of 255.
OpenCV v4.13.0 or later use BI_BITFIELDS compression as default. See IMWRITE_BMP_COMPRESSION.
- 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.
+57 -3
View File
@@ -42,6 +42,7 @@
#include "precomp.hpp"
#include "grfmt_bmp.hpp"
#include "opencv2/core/utils/logger.hpp"
namespace cv
{
@@ -603,6 +604,7 @@ BmpEncoder::BmpEncoder()
{
m_description = "Windows bitmap (*.bmp;*.dib)";
m_buf_supported = true;
m_supported_encode_key = {IMWRITE_BMP_COMPRESSION};
}
@@ -615,11 +617,12 @@ ImageEncoder BmpEncoder::newEncoder() const
return makePtr<BmpEncoder>();
}
bool BmpEncoder::write( const Mat& img, const std::vector<int>& )
bool BmpEncoder::write( const Mat& img, const std::vector<int>& params )
{
int width = img.cols, height = img.rows, channels = img.channels();
int fileStep = (width*channels + 3) & -4;
uchar zeropad[] = "\0\0\0\0";
WLByteStream strm;
if( m_buf )
@@ -630,7 +633,35 @@ bool BmpEncoder::write( const Mat& img, const std::vector<int>& )
else if( !strm.open( m_filename ))
return false;
int bitmapHeaderSize = 40;
// sRGB colorspace requires BITMAPV5HEADER.
// See https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapv5header
bool useV5BitFields = true;
for(size_t i = 0 ; i < params.size(); i++)
{
const int value = params[i+1];
switch(params[i])
{
case IMWRITE_BMP_COMPRESSION:
{
switch(value) {
case IMWRITE_BMP_COMPRESSION_RGB:
useV5BitFields = false;
break;
case IMWRITE_BMP_COMPRESSION_BITFIELDS:
useV5BitFields = true;
break;
default:
useV5BitFields = true;
CV_LOG_WARNING(nullptr, cv::format("The value(%d) for IMWRITE_BMP_COMPRESSION must be one of ImwriteBMPCompressionFlags. It is fallbacked to true", value));
break;
}
}
break;
}
}
useV5BitFields &= (channels == 4); // BMP_BITFIELDS requires 32 bit per pixel.
int bitmapHeaderSize = useV5BitFields ? 124 : 40;
int paletteSize = channels > 1 ? 0 : 1024;
int headerSize = 14 /* fileheader */ + bitmapHeaderSize + paletteSize;
size_t fileSize = (size_t)fileStep*height + headerSize;
@@ -653,13 +684,36 @@ bool BmpEncoder::write( const Mat& img, const std::vector<int>& )
CHECK_WRITE(strm.putDWord( height ));
CHECK_WRITE(strm.putWord( 1 ));
CHECK_WRITE(strm.putWord( channels << 3 ));
CHECK_WRITE(strm.putDWord( BMP_RGB ));
CHECK_WRITE(strm.putDWord( useV5BitFields ? BMP_BITFIELDS : BMP_RGB ));
CHECK_WRITE(strm.putDWord( 0 ));
CHECK_WRITE(strm.putDWord( 0 ));
CHECK_WRITE(strm.putDWord( 0 ));
CHECK_WRITE(strm.putDWord( 0 ));
CHECK_WRITE(strm.putDWord( 0 ));
if( useV5BitFields )
{
CHECK_WRITE(strm.putDWord( 0x00FF0000 )); // bV5RedMask
CHECK_WRITE(strm.putDWord( 0x0000FF00 )); // bV5GreenMask
CHECK_WRITE(strm.putDWord( 0x000000FF )); // bV5BlueMask
CHECK_WRITE(strm.putDWord( 0xFF000000 )); // bV5AlphaMask
CHECK_WRITE(strm.putBytes( "BGRs", 4)); // bV5CSType (sRGB)
{ // bV5Endpoints
for(int index_rgb = 0; index_rgb < 3; index_rgb ++ ){ // Red/Green/Blue
CHECK_WRITE(strm.putDWord( 0 )); // ciexyzX
CHECK_WRITE(strm.putDWord( 0 )); // ciexyzY
CHECK_WRITE(strm.putDWord( 0 )); // ciexyzZ
}
}
CHECK_WRITE(strm.putDWord( 0 )); // bV5GammaRed
CHECK_WRITE(strm.putDWord( 0 )); // bV5GammaGreen
CHECK_WRITE(strm.putDWord( 0 )); // bV5GammaBlue
CHECK_WRITE(strm.putDWord( 0 )); // bV5Intent
CHECK_WRITE(strm.putDWord( 0 )); // bV5ProfileData
CHECK_WRITE(strm.putDWord( 0 )); // bV5ProfileSize
CHECK_WRITE(strm.putDWord( 0 )); // bV5Reserved
}
if( channels == 1 )
{
FillGrayPalette( palette, 8 );
+59 -1
View File
@@ -360,7 +360,16 @@ TEST(Imgcodecs_Bmp, read_32bit_rgb)
const string root = cvtest::TS::ptr()->get_data_path();
const string filenameInput = root + "readwrite/test_32bit_rgb.bmp";
const Mat img = cv::imread(filenameInput, IMREAD_UNCHANGED);
Mat img;
ASSERT_NO_THROW(img = cv::imread(filenameInput));
ASSERT_FALSE(img.empty());
ASSERT_EQ(CV_8UC3, img.type());
ASSERT_NO_THROW(img = cv::imread(filenameInput, IMREAD_UNCHANGED));
ASSERT_FALSE(img.empty());
ASSERT_EQ(CV_8UC3, img.type());
ASSERT_NO_THROW(img = cv::imread(filenameInput, IMREAD_COLOR | IMREAD_ANYCOLOR | IMREAD_ANYDEPTH));
ASSERT_FALSE(img.empty());
ASSERT_EQ(CV_8UC3, img.type());
}
@@ -427,6 +436,55 @@ TEST(Imgcodecs_Bmp, rgba_scale)
ASSERT_EQ(data[0], 255);
}
typedef testing::TestWithParam<ImwriteBMPCompressionFlags> Imgcodecs_bmp_compress;
TEST_P(Imgcodecs_bmp_compress, rgba32bpp)
{
const ImwriteBMPCompressionFlags comp = GetParam();
RNG rng = theRNG();
Mat src(256, 256, CV_8UC4);
rng.fill(src, RNG::UNIFORM, Scalar(0,0,0,0), Scalar(255,255,255,255));
vector<uint8_t> buf;
bool ret = false;
ASSERT_NO_THROW(ret = cv::imencode(".bmp", src, buf, {IMWRITE_BMP_COMPRESSION, static_cast<int>(comp)}));
ASSERT_TRUE(ret);
ASSERT_EQ(buf[0x0e], comp == IMWRITE_BMP_COMPRESSION_RGB ? 40 : 124 ); // the size of header
ASSERT_EQ(buf[0x0f], 0);
ASSERT_EQ(buf[0x1c], 32); // the number of bits per pixel = 32
ASSERT_EQ(buf[0x1d], 0);
ASSERT_EQ(buf[0x1e], static_cast<int>(comp)); // the compression method
ASSERT_EQ(buf[0x1f], 0);
ASSERT_EQ(buf[0x20], 0);
ASSERT_EQ(buf[0x21], 0);
Mat dst;
ASSERT_NO_THROW(dst = cv::imdecode(buf, IMREAD_UNCHANGED));
ASSERT_FALSE(dst.empty());
if(comp == IMWRITE_BMP_COMPRESSION_RGB)
{
// If BI_RGB is used, output BMP file stores RGB image.
ASSERT_EQ(CV_8UC3, dst.type());
Mat srcBGR;
cv::cvtColor(src, srcBGR, cv::COLOR_BGRA2BGR);
EXPECT_PRED_FORMAT2(cvtest::MatComparator(0, 0), srcBGR, dst);
}
else
{
// If BI_BITFIELDS is used, output BMP file stores RGBA image.
ASSERT_EQ(CV_8UC4, dst.type());
EXPECT_PRED_FORMAT2(cvtest::MatComparator(0, 0), src, dst);
}
}
INSTANTIATE_TEST_CASE_P(All,
Imgcodecs_bmp_compress,
testing::Values(
IMWRITE_BMP_COMPRESSION_RGB,
IMWRITE_BMP_COMPRESSION_BITFIELDS));
#ifdef HAVE_IMGCODEC_HDR
TEST(Imgcodecs_Hdr, regression)
{
+17
View File
@@ -80,6 +80,23 @@ void PrintTo(const ImreadModes& val, std::ostream* os)
*os << "IMREAD_UNKNOWN(" << (int)v << ")";
}
static inline
void PrintTo(const ImwriteBMPCompressionFlags& val, std::ostream* os)
{
switch(val)
{
case IMWRITE_BMP_COMPRESSION_RGB:
*os << "IMWRITE_BMP_COMPRESSION_RGB";
break;
case IMWRITE_BMP_COMPRESSION_BITFIELDS:
*os << "IMWRITE_BMP_COMPRESSION_BITFIELDS";
break;
default:
*os << "IMWRITE_BMP_COMPRESSION_UNKNOWN(" << (int)val << ")";
break;
}
}
} // namespace
#endif