mirror of
https://github.com/opencv/opencv.git
synced 2026-07-31 00:03:03 +04:00
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
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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<WebPEncoder>();
|
||||
}
|
||||
|
||||
// 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<int>& 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<float>(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<float>(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<int>& 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<uint8_t> out_cleaner(encoder_out, WebPFree);
|
||||
#else
|
||||
|
||||
@@ -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<Vec4b>(0, ix)[3] = 0; // Transpacency pixel
|
||||
img.at<Vec4b>(1, ix)[3] = 1;
|
||||
img.at<Vec4b>(2, ix)[3] = 254;
|
||||
img.at<Vec4b>(3, ix)[3] = 255;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> 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<Vec4b>(0, ix), Vec4b(0, 0, 0, 0)); // LOSSLESS_ON -> COLOR will be optimized/dropped.
|
||||
EXPECT_EQ(img_PRESERVE_COLOR.at<Vec4b>(0, ix), Vec4b(124,64,67,0)); // PRESERVE_COLOR
|
||||
|
||||
EXPECT_EQ(img_ON.at<Vec4b>(1, ix), img_PRESERVE_COLOR.at<Vec4b>(1, ix) );
|
||||
EXPECT_EQ(img_ON.at<Vec4b>(2, ix), img_PRESERVE_COLOR.at<Vec4b>(2, ix) );
|
||||
EXPECT_EQ(img_ON.at<Vec4b>(3, ix), img_PRESERVE_COLOR.at<Vec4b>(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<int, int, ImencodeLosslessResult> WebPModePriorityParams;
|
||||
class Imgcodecs_WebP_Mode_Priority : public testing::TestWithParam<WebPModePriorityParams> {};
|
||||
|
||||
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<Vec4b>(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<int> 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<uchar> 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<Vec4b>(0,0)[3], dst.at<Vec4b>(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<Vec4b>(0, 0), dst.at<Vec4b>(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<WebPModePriorityParams>& 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
|
||||
|
||||
Reference in New Issue
Block a user