1
0
mirror of https://github.com/opencv/opencv.git synced 2026-07-30 15:53: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:
Kumataro
2026-02-26 17:01:36 +09:00
committed by GitHub
parent 1d6b8bdef7
commit 6c374bebee
3 changed files with 289 additions and 27 deletions
+148
View File
@@ -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