From 6c374bebeece731190b2238a2cc831e0175aa0b0 Mon Sep 17 00:00:00 2001 From: Kumataro Date: Thu, 26 Feb 2026 17:01:36 +0900 Subject: [PATCH] Merge pull request #28519 from Kumataro:fix28503 imgcodecs(webp): improve IMWRITE_WEBP_LOSSLESS_MODE to support exact lossless compression #28519 Close #28503 Add IMWRITE_WEBP_LOSSLESS_MODE enum to control lossless strategy: - IMWRITE_WEBP_LOSSLESS_OFF: Lossy compression. - IMWRITE_WEBP_LOSSLESS_ON: Optimize/drop color in transparent pixels. - IMWRITE_WEBP_LOSSLESS_PRESERVE_COLOR: Preserve color in transparent pixels. Note: Currently limited to still images; animated WebP support is deferred per YAGNI. ### 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. - [x] The feature is well documented and sample code can be built with the project CMake --- .../imgcodecs/include/opencv2/imgcodecs.hpp | 11 +- modules/imgcodecs/src/grfmt_webp.cpp | 157 +++++++++++++++--- modules/imgcodecs/test/test_webp.cpp | 148 +++++++++++++++++ 3 files changed, 289 insertions(+), 27 deletions(-) diff --git a/modules/imgcodecs/include/opencv2/imgcodecs.hpp b/modules/imgcodecs/include/opencv2/imgcodecs.hpp index bad9f006b5..6c59d55ec3 100644 --- a/modules/imgcodecs/include/opencv2/imgcodecs.hpp +++ b/modules/imgcodecs/include/opencv2/imgcodecs.hpp @@ -102,7 +102,8 @@ enum ImwriteFlags { IMWRITE_EXR_TYPE = (3 << 4) + 0 /* 48 */, //!< override EXR storage type (FLOAT (FP32) is default) IMWRITE_EXR_COMPRESSION = (3 << 4) + 1 /* 49 */, //!< override EXR compression type (ZIP_COMPRESSION = 3 is default) IMWRITE_EXR_DWA_COMPRESSION_LEVEL = (3 << 4) + 2 /* 50 */, //!< override EXR DWA compression level (45 is default) - IMWRITE_WEBP_QUALITY = 64, //!< For WEBP, it can be a quality from 1 to 100 (the higher is the better). By default (without any parameter) and for quality above 100 the lossless compression is used. + IMWRITE_WEBP_QUALITY = 64, //!< For WEBP, it can be a lossy quality from 1 to 100 (the higher is the better) for IMWRITE_WEBP_LOSSLESS_OFF. By default (without this parameter) or if quality > 100, IMWRITE_WEBP_LOSSLESS_ON is used instead. + IMWRITE_WEBP_LOSSLESS_MODE = 65, //!< For WEBP, it can be a lossless compression strategy. See cv::ImwriteWEBPLosslessMode. Default is IMWRITE_WEBP_LOSSLESS_OFF. For Animated WEBP, it is not supported. IMWRITE_HDR_COMPRESSION = (5 << 4) + 0 /* 80 */, //!< specify HDR compression IMWRITE_PAM_TUPLETYPE = 128,//!< For PAM, sets the TUPLETYPE field to the corresponding string value that is defined for the format IMWRITE_TIFF_RESUNIT = 256,//!< For TIFF, use to specify which DPI resolution unit to set. See ImwriteTiffResolutionUnitFlags. Default is IMWRITE_TIFF_RESOLUTION_UNIT_INCH. @@ -240,6 +241,14 @@ enum ImwritePAMFlags { IMWRITE_PAM_FORMAT_RGB_ALPHA = 5 }; +//! Imwrite WEBP specific values for IMWRITE_WEBP_LOSSLESS_MODE parameter key. +enum ImwriteWEBPLosslessMode { + IMWRITE_WEBP_LOSSLESS_OFF = 0, //!< Lossy compression mode. Uses IMWRITE_WEBP_QUALITY to control compression. (Default) + //!< @note If IMWRITE_WEBP_QUALITY is not specified, it falls back to IMWRITE_WEBP_LOSSLESS_ON to maintain backward compatibility. + IMWRITE_WEBP_LOSSLESS_ON = 1, //!< Standard lossless compression. May modify or discard RGB values of fully transparent pixels to improve compression ratio. + IMWRITE_WEBP_LOSSLESS_PRESERVE_COLOR = 2, //!< Exact lossless compression. Preserves all RGB data even for pixels with 0 alpha (equivalent to WebP's exact flag). +}; + //! Imwrite HDR specific values for IMWRITE_HDR_COMPRESSION parameter key enum ImwriteHDRCompressionFlags { IMWRITE_HDR_COMPRESSION_NONE = 0, diff --git a/modules/imgcodecs/src/grfmt_webp.cpp b/modules/imgcodecs/src/grfmt_webp.cpp index 068be52b8b..a37bbfca54 100644 --- a/modules/imgcodecs/src/grfmt_webp.cpp +++ b/modules/imgcodecs/src/grfmt_webp.cpp @@ -338,7 +338,7 @@ WebPEncoder::WebPEncoder() m_support_metadata[IMAGE_METADATA_EXIF] = true; m_support_metadata[IMAGE_METADATA_XMP] = true; m_support_metadata[IMAGE_METADATA_ICCP] = true; - m_supported_encode_key = {IMWRITE_WEBP_QUALITY}; + m_supported_encode_key = {IMWRITE_WEBP_QUALITY, IMWRITE_WEBP_LOSSLESS_MODE}; } WebPEncoder::~WebPEncoder() { } @@ -348,32 +348,121 @@ ImageEncoder WebPEncoder::newEncoder() const return makePtr(); } +// Simple API style +static size_t cvEncodeLosslessExactBGRA(const uint8_t* rgba, int width, int height, int stride, uint8_t** output) +{ + WebPConfig config; + WebPPicture pic; + WebPMemoryWriter wrt; + + // 6 is the default value for speed/compression balance in lossless mode. + // It doesn't affect visual quality, only file size and encoding time. + if (!WebPConfigInit(&config) || !WebPConfigLosslessPreset(&config, 6)) + { + return 0; + } + config.exact = 1; + + if (!WebPPictureInit(&pic)) + { + return 0; + } + pic.width = width; + pic.height = height; + pic.use_argb = 1; // BGRA + if (!WebPPictureImportBGRA(&pic, rgba, stride)) + { + return 0; + } + + WebPMemoryWriterInit(&wrt); + pic.writer = WebPMemoryWrite; + pic.custom_ptr = &wrt; + + if (!WebPEncode(&config, &pic)) + { + WebPMemoryWriterClear(&wrt); + WebPPictureFree(&pic); + return 0; + } + + *output = wrt.mem; + size_t size = wrt.size; + WebPPictureFree(&pic); + return size; +} + bool WebPEncoder::write(const Mat& img, const std::vector& params) { CV_CheckDepthEQ(img.depth(), CV_8U, "WebP codec supports 8U images only"); const int width = img.cols, height = img.rows; - bool comp_lossless = true; - float quality = 100.0f; + int lossless_mode = -1; // not specified + float quality = 0.0f; // not specified for(size_t i = 0; i < params.size(); i += 2) { const int value = params[i+1]; - if (params[i] == IMWRITE_WEBP_QUALITY) + if (params[i] == IMWRITE_WEBP_LOSSLESS_MODE) { - comp_lossless = false; - quality = static_cast(value); - if (quality < 1.0f) + switch(value) { - quality = 1.0f; - CV_LOG_WARNING(nullptr, cv::format("The value(%d) for IMWRITE_WEBP_QUALITY must be between 1 to 100(lossy) or more(lossless). It is fallbacked to 1", value)); - } - if (quality > 100.0f) - { - comp_lossless = true; + case IMWRITE_WEBP_LOSSLESS_ON: + case IMWRITE_WEBP_LOSSLESS_OFF: + case IMWRITE_WEBP_LOSSLESS_PRESERVE_COLOR: + lossless_mode = value; + break; + default: + CV_LOG_WARNING(nullptr, cv::format("The value(%d) for IMWRITE_WEBP_LOSSLESS_MODE must be one of ImwriteWEBPLosslessMode. It is ignored", value)); + break; } } + if (params[i] == IMWRITE_WEBP_QUALITY) + { + if (value < 1) + { + CV_LOG_WARNING(nullptr, cv::format("The value(%d) for IMWRITE_WEBP_QUALITY must be between 1 to 100(lossy) or more(lossless). It is fallbacked to 1", value)); + quality = 1.0f; + } + else if (value > 100) + { + quality = 101.0f; + } + else // value is 1 to 100 + { + quality = static_cast(value); + } + } + } + + switch(lossless_mode) + { + case -1: // not specified by user + case IMWRITE_WEBP_LOSSLESS_OFF: + // Fallback to lossless if quality is not specified (-1.0f) or out of lossy range (>100.0f). + // This maintains backward compatibility where WebP defaults to lossless. + if ((quality < 1.0f) || (quality > 100.0f)) + { + lossless_mode = IMWRITE_WEBP_LOSSLESS_ON; + quality = 101.0f; + } + else + { + lossless_mode = IMWRITE_WEBP_LOSSLESS_OFF; + // Use specified quality for lossy compression. + } + break; + + case IMWRITE_WEBP_LOSSLESS_ON: + case IMWRITE_WEBP_LOSSLESS_PRESERVE_COLOR: + // Force quality value to lossless range when explicit lossless mode is selected. + quality = 101.0f; + break; + + default: + CV_Error(Error::StsError, cv::format("Unexpected lossless_mode(%d)", lossless_mode)); + break; } int channels = img.channels(); @@ -391,29 +480,45 @@ bool WebPEncoder::write(const Mat& img, const std::vector& params) uint8_t *encoder_out = NULL; size_t size = 0; - if (comp_lossless) + if (channels == 3) { - if (channels == 3) + switch(lossless_mode) { - size = WebPEncodeLosslessBGR(image->ptr(), width, height, (int)image->step, &encoder_out); - } - else if (channels == 4) - { - size = WebPEncodeLosslessBGRA(image->ptr(), width, height, (int)image->step, &encoder_out); + case IMWRITE_WEBP_LOSSLESS_OFF: + size = WebPEncodeBGR(image->ptr(), width, height, (int)image->step, quality, &encoder_out); + break; + case IMWRITE_WEBP_LOSSLESS_ON: + case IMWRITE_WEBP_LOSSLESS_PRESERVE_COLOR: + size = WebPEncodeLosslessBGR(image->ptr(), width, height, (int)image->step, &encoder_out); + break; + default: + CV_Error(Error::StsError, cv::format("Unexcepted lossless_mode(%d)", lossless_mode)); + break; } } else { - if (channels == 3) + CV_CheckEQ(channels, 4, "Unexpected channels is used"); + switch(lossless_mode) { - size = WebPEncodeBGR(image->ptr(), width, height, (int)image->step, quality, &encoder_out); - } - else if (channels == 4) - { - size = WebPEncodeBGRA(image->ptr(), width, height, (int)image->step, quality, &encoder_out); + case IMWRITE_WEBP_LOSSLESS_OFF: + size = WebPEncodeBGRA(image->ptr(), width, height, (int)image->step, quality, &encoder_out); + break; + case IMWRITE_WEBP_LOSSLESS_ON: + size = WebPEncodeLosslessBGRA(image->ptr(), width, height, (int)image->step, &encoder_out); + break; + case IMWRITE_WEBP_LOSSLESS_PRESERVE_COLOR: + size = cvEncodeLosslessExactBGRA(image->ptr(), width, height, (int)image->step, &encoder_out); + break; } } + if (size == 0) + { + CV_LOG_ERROR(NULL, cv::format("WebP encoding failed with lossless_mode=%d", lossless_mode)); + return false; + } + #if WEBP_DECODER_ABI_VERSION >= 0x0206 Ptr out_cleaner(encoder_out, WebPFree); #else diff --git a/modules/imgcodecs/test/test_webp.cpp b/modules/imgcodecs/test/test_webp.cpp index b3fe9b2caf..b016dbb40d 100644 --- a/modules/imgcodecs/test/test_webp.cpp +++ b/modules/imgcodecs/test/test_webp.cpp @@ -114,6 +114,154 @@ TEST(Imgcodecs_WebP, encode_decode_with_alpha_webp) EXPECT_EQ(512, img_webp_bgr.rows); } + +// See https://github.com/opencv/opencv/issues/28503 +TEST(Imgcodecs_WebP, encode_decode_LOSSLESS_MODE) +{ + cv::Mat img(cv::Size(64,4), CV_8UC4, cv::Scalar(124,64,67,0) ); + for(int ix = 0; ix < img.size().width; ix++) + { + img.at(0, ix)[3] = 0; // Transpacency pixel + img.at(1, ix)[3] = 1; + img.at(2, ix)[3] = 254; + img.at(3, ix)[3] = 255; + } + + std::vector work; + EXPECT_NO_THROW(cv::imencode(".webp", img, work, {IMWRITE_WEBP_LOSSLESS_MODE, IMWRITE_WEBP_LOSSLESS_ON})); + cv::Mat img_ON = cv::imdecode(work, IMREAD_UNCHANGED); + EXPECT_NO_THROW(cv::imencode(".webp", img, work, {IMWRITE_WEBP_LOSSLESS_MODE, IMWRITE_WEBP_LOSSLESS_PRESERVE_COLOR})); + cv::Mat img_PRESERVE_COLOR = cv::imdecode(work, IMREAD_UNCHANGED); + + for(int ix = 0; ix < img.size().width; ix++) + { + EXPECT_EQ(img_ON.at(0, ix), Vec4b(0, 0, 0, 0)); // LOSSLESS_ON -> COLOR will be optimized/dropped. + EXPECT_EQ(img_PRESERVE_COLOR.at(0, ix), Vec4b(124,64,67,0)); // PRESERVE_COLOR + + EXPECT_EQ(img_ON.at(1, ix), img_PRESERVE_COLOR.at(1, ix) ); + EXPECT_EQ(img_ON.at(2, ix), img_PRESERVE_COLOR.at(2, ix) ); + EXPECT_EQ(img_ON.at(3, ix), img_PRESERVE_COLOR.at(3, ix) ); + } +} + +// Expected result categories for WebP encoding tests. +enum ImencodeLosslessResult { + LOSSY, // Expect lossy compression (pixel differences allowed) + LOSSLESS, // Expect standard lossless compression (pixel values match) + EXACT // Expect exact lossless (preserving RGB values of transparent pixels) +}; + +typedef std::tuple WebPModePriorityParams; +class Imgcodecs_WebP_Mode_Priority : public testing::TestWithParam {}; + +TEST_P(Imgcodecs_WebP_Mode_Priority, encode_webp_mode_priority) +{ + const int mode = std::get<0>(GetParam()); + const int quality = std::get<1>(GetParam()); + const ImencodeLosslessResult expected = std::get<2>(GetParam()); + + // Generate a 100x100 RGBA test image. + // Set a transparent pixel with specific color (Blue) to verify EXACT mode. + Mat src(100, 100, CV_8UC4, Scalar(255, 255, 255, 255)); + src.at(0, 0) = Vec4b(255, 0, 0, 0); // Transparent Blue (B:255, G:0, R:0, A:0) + + // Build the imwrite parameter vector dynamically. + std::vector params; + if (mode != -1) { + params.push_back(IMWRITE_WEBP_LOSSLESS_MODE); + params.push_back(mode); + } + if (quality != -1) { + params.push_back(IMWRITE_WEBP_QUALITY); + params.push_back(quality); + } + + // Encode to memory and decode back. + std::vector buf; + ASSERT_TRUE(imencode(".webp", src, buf, params)); + Mat dst = imdecode(buf, IMREAD_UNCHANGED); + ASSERT_FALSE(dst.empty()); + + // Validation logic + if (expected == LOSSY) { + // We expect some differences in lossy mode + double diff = cv::norm(src, dst, NORM_INF); + EXPECT_GT(diff, 0) << "Should be lossy (Quality: " << quality << ")"; + } + else if (expected == LOSSLESS) { + // Standard lossless: we allow the library to modify RGB values + // of fully transparent pixels (A=0) to improve compression ratio. + // Thus, we compare only visible pixels or check with a slightly relaxed condition. + + // Option A: If you want to allow RGB changes on A=0: + // We can't use cv::norm directly if A=0 pixels are modified. + // Let's check if they are identical except for the transparent pixel. + Mat diff; + absdiff(src, dst, diff); + Scalar total_diff = sum(diff); + // If only the (0,0) pixel changed from (255,0,0,0) to (0,0,0,0), + // total_diff will be 255. + EXPECT_LE(total_diff[0] + total_diff[1] + total_diff[2], 255) + << "Standard lossless should not have significant pixel differences"; + EXPECT_EQ(src.at(0,0)[3], dst.at(0,0)[3]) << "Alpha must be preserved"; + } + else if (expected == EXACT) { + // Exact lossless: Every single bit must match, including transparent pixels. + double diff = cv::norm(src, dst, NORM_INF); + EXPECT_EQ(0, diff) << "Exact mode must preserve all pixel values perfectly"; + EXPECT_EQ(src.at(0, 0), dst.at(0, 0)) + << "RGB values of transparent pixels must be preserved in EXACT mode"; + } + else { + FAIL() << "Unknown expectation type"; + } +} + +/** + * Helper to generate human-readable test names in gtest output. + */ +static std::string getModeStr(int m) { + if (m == -1) return "OMIT"; + if (m == IMWRITE_WEBP_LOSSLESS_OFF) return "OFF"; + if (m == IMWRITE_WEBP_LOSSLESS_ON) return "ON"; + if (m == IMWRITE_WEBP_LOSSLESS_PRESERVE_COLOR) return "PRESERVE"; + return "UNKNOWN"; +} + +static std::string getExpectStr(ImencodeLosslessResult r) { + return (r == LOSSY) ? "LOSSY" : (r == EXACT) ? "EXACT" : "LOSSLESS"; +} + +INSTANTIATE_TEST_CASE_P(Imgcodecs, Imgcodecs_WebP_Mode_Priority, + testing::Values( + // Default (OMIT mode) cases + WebPModePriorityParams(-1, -1, LOSSLESS), + WebPModePriorityParams(-1, 80, LOSSY), + WebPModePriorityParams(-1, 101, LOSSLESS), + + // LOSSLESS_OFF (Explicitly off) + WebPModePriorityParams(IMWRITE_WEBP_LOSSLESS_OFF, -1, LOSSLESS), + WebPModePriorityParams(IMWRITE_WEBP_LOSSLESS_OFF, 80, LOSSY), + WebPModePriorityParams(IMWRITE_WEBP_LOSSLESS_OFF, 101, LOSSLESS), + + // LOSSLESS_ON (Force lossless) + WebPModePriorityParams(IMWRITE_WEBP_LOSSLESS_ON, -1, LOSSLESS), + WebPModePriorityParams(IMWRITE_WEBP_LOSSLESS_ON, 80, LOSSLESS), + WebPModePriorityParams(IMWRITE_WEBP_LOSSLESS_ON, 101, LOSSLESS), + + // PRESERVE_COLOR (Exact lossless) + WebPModePriorityParams(IMWRITE_WEBP_LOSSLESS_PRESERVE_COLOR, -1, EXACT), + WebPModePriorityParams(IMWRITE_WEBP_LOSSLESS_PRESERVE_COLOR, 80, EXACT), + WebPModePriorityParams(IMWRITE_WEBP_LOSSLESS_PRESERVE_COLOR, 101, EXACT) + ), + [](const testing::TestParamInfo& info_) { + std::string mode = getModeStr(std::get<0>(info_.param)); + int q = std::get<1>(info_.param); + std::string q_str = (q == -1) ? "omit" : std::to_string(q); + return mode + "_q" + q_str + "_" + getExpectStr(std::get<2>(info_.param)); + } +); + #endif // HAVE_WEBP }} // namespace