From 905fa09e04a43722329be791ec29ff2afc30a04a Mon Sep 17 00:00:00 2001 From: Kumataro Date: Wed, 21 Oct 2026 09:21:33 +0900 Subject: [PATCH] imgcodecs(avif): support 16-bit reading/writing with libavif v1.4.0+ (5.x) --- CMakeLists.txt | 6 +-- cmake/OpenCVFindAVIF.cmake | 39 +++++++++++++++- cmake/OpenCVFindLibsGrfmt.cmake | 2 +- modules/imgcodecs/CMakeLists.txt | 7 ++- .../imgcodecs/include/opencv2/imgcodecs.hpp | 2 +- modules/imgcodecs/src/grfmt_avif.cpp | 41 ++++++++++++++--- modules/imgcodecs/test/test_avif.cpp | 44 +++++++++++++++++++ 7 files changed, 125 insertions(+), 16 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index eea07f85ac..834915dcbc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1502,11 +1502,7 @@ if(WITH_WEBP OR HAVE_WEBP) endif() if(WITH_AVIF OR HAVE_AVIF) - if(libavif_VERSION) - status(" AVIF:" AVIF_FOUND THEN "${AVIF_LIBRARY} (ver ${libavif_VERSION})" ELSE "NO") - else() - status(" AVIF:" AVIF_FOUND THEN "${AVIF_LIBRARY}" ELSE "NO") - endif() + status(" AVIF:" AVIF_FOUND THEN "${AVIF_LIBRARY} (ver ${AVIF_VERSION})" ELSE "NO") endif() if(WITH_SPNG) diff --git a/cmake/OpenCVFindAVIF.cmake b/cmake/OpenCVFindAVIF.cmake index 8f9358d70d..04d05c42e9 100644 --- a/cmake/OpenCVFindAVIF.cmake +++ b/cmake/OpenCVFindAVIF.cmake @@ -6,6 +6,7 @@ # AVIF_INCLUDE_DIRS - where to find avif/avif.h, etc. # AVIF_LIBRARIES - List of libraries when using AVIF. # AVIF_FOUND - True if AVIF is found. +# AVIF_VERSION - libavif version string (e.g. "1.4.0") #============================================================================= # Look for the header file. @@ -40,7 +41,41 @@ else() # all listed variables are TRUE INCLUDE(${CMAKE_ROOT}/Modules/FindPackageHandleStandardArgs.cmake) FIND_PACKAGE_HANDLE_STANDARD_ARGS(AVIF DEFAULT_MSG AVIF_LIBRARY AVIF_INCLUDE_DIR) +endif() - SET(AVIF_LIBRARIES ${AVIF_LIBRARY}) - SET(AVIF_INCLUDE_DIRS ${AVIF_INCLUDE_DIR}) +if(NOT AVIF_FOUND) + UNSET(AVIF_LIBRARIES) + UNSET(AVIF_INCLUDE_DIRS) + UNSET(AVIF_VERSION) + return() +endif() + +SET(AVIF_LIBRARIES ${AVIF_LIBRARY}) +SET(AVIF_INCLUDE_DIRS ${AVIF_INCLUDE_DIR}) + +# unset(libavif_VERSION) # For debugging: uncomment to test fallback version extraction when find_package(libavif) cannot find libavif + +if(NOT libavif_VERSION) + find_file(AVIF_H NAMES avif/avif.h PATHS ${AVIF_INCLUDE_DIRS} NO_DEFAULT_PATH) + + if(AVIF_H) + file(STRINGS "${AVIF_H}" AVIF_MAJOR_LINE REGEX "^#define[ \t]+AVIF_VERSION_MAJOR[ \t]+[0-9]+") + file(STRINGS "${AVIF_H}" AVIF_MINOR_LINE REGEX "^#define[ \t]+AVIF_VERSION_MINOR[ \t]+[0-9]+") + file(STRINGS "${AVIF_H}" AVIF_PATCH_LINE REGEX "^#define[ \t]+AVIF_VERSION_PATCH[ \t]+[0-9]+") + + string(REGEX REPLACE ".*AVIF_VERSION_MAJOR[ \t]+([0-9]+).*" "\\1" AVIF_MAJOR "${AVIF_MAJOR_LINE}") + string(REGEX REPLACE ".*AVIF_VERSION_MINOR[ \t]+([0-9]+).*" "\\1" AVIF_MINOR "${AVIF_MINOR_LINE}") + string(REGEX REPLACE ".*AVIF_VERSION_PATCH[ \t]+([0-9]+).*" "\\1" AVIF_PATCH "${AVIF_PATCH_LINE}") + + if(AVIF_MAJOR MATCHES "^[0-9]+$" AND AVIF_MINOR MATCHES "^[0-9]+$" AND AVIF_PATCH MATCHES "^[0-9]+$") + SET(libavif_VERSION "${AVIF_MAJOR}.${AVIF_MINOR}.${AVIF_PATCH}") + endif() + endif() + + UNSET(AVIF_H CACHE) + UNSET(AVIF_H) +endif() + +if(libavif_VERSION) + SET(AVIF_VERSION "${libavif_VERSION}") endif() diff --git a/cmake/OpenCVFindLibsGrfmt.cmake b/cmake/OpenCVFindLibsGrfmt.cmake index 53ea0ca9b2..592f7c1cc5 100644 --- a/cmake/OpenCVFindLibsGrfmt.cmake +++ b/cmake/OpenCVFindLibsGrfmt.cmake @@ -60,7 +60,7 @@ endif() # --- libavif (optional) --- if(WITH_AVIF) - ocv_clear_internal_cache_vars(AVIF_LIBRARY AVIF_INCLUDE_DIR) + ocv_clear_internal_cache_vars(AVIF_LIBRARIES AVIF_INCLUDE_DIRS) include(cmake/OpenCVFindAVIF.cmake) if(AVIF_FOUND) set(HAVE_AVIF 1) diff --git a/modules/imgcodecs/CMakeLists.txt b/modules/imgcodecs/CMakeLists.txt index 56ddc96f6d..776c1f0752 100644 --- a/modules/imgcodecs/CMakeLists.txt +++ b/modules/imgcodecs/CMakeLists.txt @@ -14,8 +14,11 @@ if(HAVE_WINRT_CX AND NOT WINRT) endif() if (HAVE_AVIF) - ocv_include_directories(${AVIF_INCLUDE_DIR}) - list(APPEND GRFMT_LIBS ${AVIF_LIBRARY}) + ocv_include_directories(${AVIF_INCLUDE_DIRS}) + list(APPEND GRFMT_LIBS ${AVIF_LIBRARIES}) + if(NOT (AVIF_VERSION VERSION_LESS "1.4.0")) # libavif v1.4.0+ supports 16 bit images. + add_definitions(-DOPENCV_IMGCODECS_AVIF_SUPPORT_16BIT=1) + endif() endif() if(HAVE_JPEG) diff --git a/modules/imgcodecs/include/opencv2/imgcodecs.hpp b/modules/imgcodecs/include/opencv2/imgcodecs.hpp index 77279ecbd9..ab4d7d52a3 100644 --- a/modules/imgcodecs/include/opencv2/imgcodecs.hpp +++ b/modules/imgcodecs/include/opencv2/imgcodecs.hpp @@ -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 or 12. 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. 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. diff --git a/modules/imgcodecs/src/grfmt_avif.cpp b/modules/imgcodecs/src/grfmt_avif.cpp index a33150e894..3061f36217 100644 --- a/modules/imgcodecs/src/grfmt_avif.cpp +++ b/modules/imgcodecs/src/grfmt_avif.cpp @@ -28,6 +28,23 @@ namespace cv { namespace { +static bool isSupportedBitDepth16U(int requestDepth) +{ + if (requestDepth == 10 || requestDepth == 12) + { + return true; + } + +#ifdef OPENCV_IMGCODECS_AVIF_SUPPORT_16BIT + if (requestDepth == 16) + { + return true; + } +#endif + + return false; +} + struct AvifImageDeleter { void operator()(avifImage *image) { avifImageDestroy(image); } }; @@ -216,6 +233,11 @@ bool AvifDecoder::readHeader() { return false; } decoder_->strictFlags = AVIF_STRICT_DISABLED; + +#ifdef OPENCV_IMGCODECS_AVIF_SUPPORT_16BIT + decoder_->imageContentToDecode |= AVIF_IMAGE_CONTENT_SAMPLE_TRANSFORMS; +#endif + if (!m_buf.empty()) { CV_Assert(m_buf.type() == CV_8UC1); CV_Assert(m_buf.rows == 1); @@ -241,7 +263,7 @@ bool AvifDecoder::readHeader() { channels_ = (decoder_->image->yuvFormat == AVIF_PIXEL_FORMAT_YUV400) ? 1 : 3; if (decoder_->alphaPresent) ++channels_; bit_depth_ = decoder_->image->depth; - CV_Assert(bit_depth_ == 8 || bit_depth_ == 10 || bit_depth_ == 12); + CV_Assert(bit_depth_ == 8 || isSupportedBitDepth16U(bit_depth_)); m_type = CV_MAKETYPE(bit_depth_ == 8 ? CV_8U : CV_16U, channels_); is_first_image_ = true; return true; @@ -360,10 +382,10 @@ bool AvifEncoder::writeanimation(const Animation& animation, #endif } else if (params[i] == IMWRITE_AVIF_DEPTH) { bit_depth = value; - if ((bit_depth != 8) && (bit_depth !=10) && (bit_depth !=12)) + if ((bit_depth != 8) && (!isSupportedBitDepth16U(bit_depth))) { bit_depth = 8; - CV_LOG_WARNING(nullptr, cv::format("The value(%d) for IMWRITE_AVIF_DEPTH must be 8, 10 or 12. It is fallbacked to %d", value, bit_depth)); + CV_LOG_WARNING(nullptr, cv::format("The value(%d) for IMWRITE_AVIF_DEPTH must be 8, 10, 12 (or 16 with libavif v1.4.0+). It is fallbacked to %d", value, bit_depth)); } } else if (params[i] == IMWRITE_AVIF_SPEED) { speed = std::min(std::max(value,0),10); @@ -385,17 +407,26 @@ bool AvifEncoder::writeanimation(const Animation& animation, #endif encoder_->speed = speed; +#ifdef OPENCV_IMGCODECS_AVIF_SUPPORT_16BIT + if(bit_depth == 16) { + encoder_->sampleTransformRecipe = (do_lossless)? + AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_12B_4B: // lossless + AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_12B_8B_OVERLAP_4B; // lossy + } +#endif + const avifAddImageFlags flag = (animation.frames.size() == 1) ? AVIF_ADD_IMAGE_FLAG_SINGLE : AVIF_ADD_IMAGE_FLAG_NONE; std::vector images; + for (const cv::Mat &img : animation.frames) { CV_CheckType( img.type(), (bit_depth == 8 && img.depth() == CV_8U) || - ((bit_depth == 10 || bit_depth == 12) && img.depth() == CV_16U), + (isSupportedBitDepth16U(bit_depth) && img.depth() == CV_16U), "AVIF only supports bit depth of 8 with CV_8U input or " - "bit depth of 10 or 12 with CV_16U input"); + "bit depth of 10, 12 (or 16 with libavif v1.4.0+) with CV_16U input"); CV_Check(img.channels(), img.channels() == 1 || img.channels() == 3 || img.channels() == 4, diff --git a/modules/imgcodecs/test/test_avif.cpp b/modules/imgcodecs/test/test_avif.cpp index 81cbfe9c66..8c00e0dd04 100644 --- a/modules/imgcodecs/test/test_avif.cpp +++ b/modules/imgcodecs/test/test_avif.cpp @@ -304,6 +304,50 @@ 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 {}; + +TEST_P(Imgcodecs_Avif_16bit, encode_decode) +{ + 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 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; + } +} + +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