Merge pull request #30497 from Kumataro:fix_norm_calculation_and_extend_16bit_tests_4.x

imgcodecs(avif): fix norm calculation and extend 16-bit tests(4.x)
This commit is contained in:
Alexander Smorkalov
2026-10-25 10:56:25 +03:00
committed by GitHub
3 changed files with 56 additions and 52 deletions

View File

@@ -115,7 +115,7 @@ enum ImwriteFlags {
IMWRITE_TIFF_PREDICTOR = 317,//!< For TIFF, use to specify predictor. See cv::ImwriteTiffPredictorFlags. Default is IMWRITE_TIFF_PREDICTOR_HORIZONTAL .
IMWRITE_JPEG2000_COMPRESSION_X1000 = 272,//!< For JPEG2000, use to specify the target compression rate (multiplied by 1000). The value can be from 0 to 1000. Default is 1000.
IMWRITE_AVIF_QUALITY = 512,//!< For AVIF, it can be a quality between 0 and 100 (the higher the better). Default is 95.
IMWRITE_AVIF_DEPTH = 513,//!< For AVIF, it can be 8, 10, 12 or 16. 16 requires libavif v1.4.0 or later. If >8, it is stored/read as CV_16U. Default is 8.
IMWRITE_AVIF_DEPTH = 513,//!< For AVIF, it can be 8, 10, 12 or 16. 16 requires libavif v1.4.0 or later (for a single still image only, not animation). If >8, it is stored/read as CV_16U. Default is 8.
IMWRITE_AVIF_SPEED = 514,//!< For AVIF, it is between 0 (slowest) and 10(fastest). Default is 9.
IMWRITE_JPEGXL_QUALITY = 640,//!< For JPEG XL, it can be a quality from 0 to 100 (the higher is the better). Default value is 95. If set, distance parameter is re-calicurated from quality level automatically. This parameter request libjxl v0.10 or later.
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.

View File

@@ -374,11 +374,16 @@ bool AvifEncoder::writeanimation(const Animation& animation,
}
#if CV_AVIF_USE_QUALITY
encoder_->quality = quality;
encoder_->qualityAlpha = quality; // Keep alpha channel quality in sync with color channels
#else
encoder_->minQuantizer = encoder_->maxQuantizer =
(AVIF_QUANTIZER_BEST_QUALITY - AVIF_QUANTIZER_WORST_QUALITY) *
quality / (AVIF_QUALITY_BEST - AVIF_QUALITY_WORST) +
AVIF_QUANTIZER_WORST_QUALITY;
encoder_->minQuantizerAlpha = encoder_->maxQuantizerAlpha =
(AVIF_QUANTIZER_BEST_QUALITY - AVIF_QUANTIZER_WORST_QUALITY) *
quality / (AVIF_QUALITY_BEST - AVIF_QUALITY_WORST) +
AVIF_QUANTIZER_WORST_QUALITY; // Keep alpha channel quality in sync with color channels
#endif
} else if (params[i] == IMWRITE_AVIF_DEPTH) {
bit_depth = value;
@@ -409,6 +414,9 @@ bool AvifEncoder::writeanimation(const Animation& animation,
#ifdef OPENCV_IMGCODECS_AVIF_SUPPORT_16BIT
if(bit_depth == 16) {
// Encoding 16-bit AVIF animations should fail due to a libavif limitation.
CV_CheckEQ(animation.frames.size(), 1L, "16-bit encoding only supports a single still image, not animation.");
encoder_->sampleTransformRecipe = (do_lossless)?
AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_12B_4B: // lossless
AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_12B_8B_OVERLAP_4B; // lossy

View File

@@ -23,16 +23,24 @@ class Imgcodecs_Avif_RoundTripSuite
} else if (channels == 4) {
std::vector<cv::Mat> imgs;
cv::split(img_original, imgs);
imgs.push_back(cv::Mat(imgs[0]));
imgs[imgs.size() - 1] = cv::Scalar::all(128);
cv::Mat alpha(cv::Size(imgs[0].cols, imgs[0].rows), CV_8UC1, cv::Scalar(128)); // Do not use opaque alpha plane. it will be dropped by libavif to optimize.
imgs.push_back(alpha);
cv::merge(imgs, img);
} else {
img = img_original.clone();
}
cv::Mat img_final = img;
cv::Mat img_final;
// Convert image to CV_16U for some bit depths.
if (bit_depth > 8) img.convertTo(img_final, CV_16U, 1 << (bit_depth - 8));
if (bit_depth == 10) {
img.convertTo(img_final, CV_16U, 1023.0 / 255.0);
} else if (bit_depth == 12) {
img.convertTo(img_final, CV_16U, 4095.0 / 255.0);
} else if (bit_depth == 16) {
img.convertTo(img_final, CV_16U, 65535.0 / 255.0);
} else {
img_final = img;
}
return img_final;
}
@@ -46,6 +54,7 @@ class Imgcodecs_Avif_RoundTripSuite
cv::IMWRITE_AVIF_DEPTH, bit_depth_};
}
// Memo: this is used only for animation test. AVIF animation does not support 16 bit_depth.
bool IsBitDepthValid() const {
return (bit_depth_ == 8 || bit_depth_ == 10 || bit_depth_ == 12);
}
@@ -59,13 +68,17 @@ class Imgcodecs_Avif_RoundTripSuite
if (quality_ == 100) {
EXPECT_EQ(0, cvtest::norm(img, img_original, NORM_INF));
} else {
const float norm = cvtest::norm(img, img_original, NORM_L2) /
img.channels() / img.cols / img.rows /
(1 << (bit_depth_ - 8));
const float num_samples = static_cast<float>(img.channels() * img.cols * img.rows);
const float norm_ = cvtest::norm(img, img_original, NORM_L2) / std::sqrt(num_samples);
const float norm = (bit_depth_ == 16)? norm_ / 65535.0f * 255.0f :
(bit_depth_ == 12)? norm_ / 4095.0f * 255.0f :
(bit_depth_ == 10)? norm_ / 1023.0f * 255.0f :
norm_; // scale to 8 bit
if (quality_ == 50) {
EXPECT_LE(norm, 10);
EXPECT_LE(norm, 20); // Maximum = 18.15 -> ceil(18.15 * 1.1) = 20
} else if (quality_ == 0) {
EXPECT_LE(norm, 13);
EXPECT_LE(norm, 60); // Maximum = 54.0416 -> ceil(54.0416 * 1.1) = 60
} else {
EXPECT_FALSE(true);
}
@@ -100,7 +113,7 @@ class Imgcodecs_Avif_Image_RoundTripSuite
cv::Mat img_resized;
cv::resize(img_original, img_resized, cv::Size(kWidth, kHeight), 0, 0);
for (int channels : {1, 3, 4}) {
for (int bit_depth : {8, 10, 12}) {
for (int bit_depth : {8, 10, 12, 16}) {
const Key key{channels, bit_depth};
imgs_[key] = modifyImage(img_resized, channels, bit_depth);
}
@@ -123,6 +136,12 @@ class Imgcodecs_Avif_Image_WriteReadSuite
: public Imgcodecs_Avif_Image_RoundTripSuite {};
TEST_P(Imgcodecs_Avif_Image_WriteReadSuite, imwrite_imread) {
#ifndef OPENCV_IMGCODECS_AVIF_SUPPORT_16BIT
if(bit_depth_ == 16) {
throw SkipTestException("Test is skipped(libavif 1.4.0+ is required to test 16 bit supporting)");
}
#endif
const cv::Mat& img_original = get_img_original();
ASSERT_FALSE(img_original.empty());
@@ -140,7 +159,7 @@ TEST_P(Imgcodecs_Avif_Image_WriteReadSuite, imwrite_imread) {
INSTANTIATE_TEST_CASE_P(
Imgcodecs_AVIF, Imgcodecs_Avif_Image_WriteReadSuite,
::testing::Combine(::testing::ValuesIn({6, 8, 10, 12}),
::testing::Combine(::testing::ValuesIn({6, 8, 10, 12, 16}),
::testing::ValuesIn({1, 3, 4}),
::testing::ValuesIn({0, 50, 100}),
::testing::ValuesIn({IMREAD_UNCHANGED, IMREAD_GRAYSCALE,
@@ -150,6 +169,12 @@ class Imgcodecs_Avif_Image_EncodeDecodeSuite
: public Imgcodecs_Avif_Image_RoundTripSuite {};
TEST_P(Imgcodecs_Avif_Image_EncodeDecodeSuite, imencode_imdecode) {
#ifndef OPENCV_IMGCODECS_AVIF_SUPPORT_16BIT
if(bit_depth_ == 16) {
throw SkipTestException("Test is skipped(libavif 1.4.0+ is required to test 16 bit supporting)");
}
#endif
const cv::Mat& img_original = get_img_original();
ASSERT_FALSE(img_original.empty());
@@ -168,7 +193,7 @@ TEST_P(Imgcodecs_Avif_Image_EncodeDecodeSuite, imencode_imdecode) {
INSTANTIATE_TEST_CASE_P(
Imgcodecs_AVIF, Imgcodecs_Avif_Image_EncodeDecodeSuite,
::testing::Combine(::testing::ValuesIn({6, 8, 10, 12}),
::testing::Combine(::testing::ValuesIn({6, 8, 10, 12, 16}),
::testing::ValuesIn({1, 3, 4}),
::testing::ValuesIn({0, 50, 100}),
::testing::ValuesIn({IMREAD_UNCHANGED, IMREAD_GRAYSCALE,
@@ -304,50 +329,21 @@ INSTANTIATE_TEST_CASE_P(
::testing::ValuesIn({IMREAD_UNCHANGED, IMREAD_GRAYSCALE,
IMREAD_COLOR, IMREAD_COLOR_RGB})));
////////////////////////////////////////////////////////////////////////////////
#ifdef OPENCV_IMGCODECS_AVIF_SUPPORT_16BIT
class Imgcodecs_Avif_16bit : public testing::TestWithParam<int> {};
TEST_P(Imgcodecs_Avif_16bit, encode_decode)
TEST(Imgcodecs_Avif, unsupported_animation_with_16bit)
#else
TEST(Imgcodecs_Avif, DISABLED_unsupported_animation_with_16bit)
#endif
{
const int quality = GetParam();
const int bit_depth = 16;
const string root = cvtest::TS::ptr()->get_data_path();
const string filename = root + "../cv/features2d/tsukuba.png";
cv::Mat src;
EXPECT_NO_THROW(src = cv::imread(filename));
ASSERT_FALSE(src.empty());
EXPECT_NO_THROW(src.convertTo(src, CV_16UC3, 65536.0 / 255.0));
std::vector<uint8_t> buf;
EXPECT_NO_THROW(imencode(".avif", src, buf, {IMWRITE_AVIF_DEPTH, bit_depth, IMWRITE_AVIF_QUALITY, quality}));
cv::Mat dst;
EXPECT_NO_THROW(dst = imdecode(buf, IMREAD_UNCHANGED));
ASSERT_FALSE(dst.empty());
const float norm = cvtest::norm(dst, src, NORM_L2) /
src.channels() / src.cols / src.rows /
(1 << (bit_depth - 8));
switch ( quality ) {
case 100: EXPECT_EQ(norm, 0); break; // lossless
case 50: EXPECT_LE(norm, 10); break;
case 0: EXPECT_LE(norm, 13); break;
default: ADD_FAILURE() << "Unknown quality setting: " << quality; break;
}
cv::Mat frame(cv::Size(160, 120), CV_16UC3, cv::Scalar::all(32));
std::vector<cv::Mat> frames = {frame, frame};
std::vector<uchar> buf;
// Encoding 16-bit AVIF animations should fail due to a libavif limitation.
EXPECT_NO_THROW(EXPECT_FALSE(cv::imencodemulti(".avif", frames, buf, {IMWRITE_AVIF_DEPTH, 16})));
}
INSTANTIATE_TEST_CASE_P(
Imgcodecs_AVIF, Imgcodecs_Avif_16bit,
testing::Values(0, 50, 100) // Quality
);
#endif // OPENCV_IMGCODECS_AVIF_SUPPORT_16BIT
} // namespace
} // namespace opencv_test