diff --git a/CMakeLists.txt b/CMakeLists.txt index fd47d83a2c..abff989606 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -240,6 +240,8 @@ OCV_OPTION(WITH_1394 "Include IEEE1394 support" ON OCV_OPTION(WITH_AVFOUNDATION "Use AVFoundation for Video I/O (iOS/Mac)" ON VISIBLE_IF APPLE VERIFY HAVE_AVFOUNDATION) +OCV_OPTION(WITH_AVIF "Enable AVIF support" OFF + VERIFY HAVE_AVIF) OCV_OPTION(WITH_CAP_IOS "Enable iOS video capture" ON VISIBLE_IF IOS VERIFY HAVE_CAP_IOS) @@ -1379,6 +1381,14 @@ if(WITH_WEBP OR HAVE_WEBP) status(" WEBP:" WEBP_FOUND THEN "${WEBP_LIBRARY} (ver ${WEBP_VERSION})" ELSE "build (ver ${WEBP_VERSION})") endif() +if(WITH_AVIF OR HAVE_AVIF) + if(AVIF_VERSION) + status(" AVIF:" AVIF_FOUND THEN "${AVIF_LIBRARY} (ver ${AVIF_VERSION})" ELSE "NO") + else() + status(" AVIF:" AVIF_FOUND THEN "${AVIF_LIBRARY}" ELSE "NO") + endif() +endif() + if(WITH_PNG OR HAVE_PNG OR WITH_SPNG) if(WITH_SPNG) status(" PNG:" "build-${SPNG_LIBRARY} (ver ${SPNG_VERSION})") diff --git a/cmake/OpenCVFindAVIF.cmake b/cmake/OpenCVFindAVIF.cmake new file mode 100644 index 0000000000..26195a7769 --- /dev/null +++ b/cmake/OpenCVFindAVIF.cmake @@ -0,0 +1,46 @@ +#============================================================================= +# Find AVIF library +#============================================================================= +# Find the native AVIF headers and libraries. +# +# 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. +#============================================================================= + +# Look for the header file. + +unset(AVIF_FOUND) + +find_package(libavif QUIET) + +if(TARGET avif) + MARK_AS_ADVANCED(AVIF_INCLUDE_DIR) + MARK_AS_ADVANCED(AVIF_LIBRARY) + + SET(AVIF_FOUND TRUE) + GET_TARGET_PROPERTY(AVIF_LIBRARY avif LOCATION) + GET_TARGET_PROPERTY(AVIF_INCLUDE_DIR1 avif INCLUDE_DIRECTORIES) + GET_TARGET_PROPERTY(AVIF_INCLUDE_DIR2 avif INTERFACE_INCLUDE_DIRECTORIES) + set(AVIF_INCLUDE_DIR) + if(AVIF_INCLUDE_DIR1) + LIST(APPEND AVIF_INCLUDE_DIR ${AVIF_INCLUDE_DIR1}) + endif() + if(AVIF_INCLUDE_DIR2) + LIST(APPEND AVIF_INCLUDE_DIR ${AVIF_INCLUDE_DIR2}) + endif() +else() + FIND_PATH(AVIF_INCLUDE_DIR NAMES avif/avif.h) + + # Look for the library. + FIND_LIBRARY(AVIF_LIBRARY NAMES avif) + MARK_AS_ADVANCED(AVIF_LIBRARY) + + # handle the QUIETLY and REQUIRED arguments and set AVIF_FOUND to TRUE if + # all listed variables are TRUE + INCLUDE(${CMAKE_ROOT}/Modules/FindPackageHandleStandardArgs.cmake) + FIND_PACKAGE_HANDLE_STANDARD_ARGS(AVIF DEFAULT_MSG AVIF_LIBRARY AVIF_INCLUDE_DIR) + + SET(AVIF_LIBRARIES ${AVIF_LIBRARY}) + SET(AVIF_INCLUDE_DIRS ${AVIF_INCLUDE_DIR}) +endif() diff --git a/cmake/OpenCVFindLibsGrfmt.cmake b/cmake/OpenCVFindLibsGrfmt.cmake index 4e8a1de17a..e544f78eaa 100644 --- a/cmake/OpenCVFindLibsGrfmt.cmake +++ b/cmake/OpenCVFindLibsGrfmt.cmake @@ -37,6 +37,16 @@ if(NOT ZLIB_FOUND) ocv_parse_header2(ZLIB "${${ZLIB_LIBRARY}_SOURCE_DIR}/zlib.h" ZLIB_VERSION) endif() +# --- libavif (optional) --- + +if(WITH_AVIF) + ocv_clear_internal_cache_vars(AVIF_LIBRARY AVIF_INCLUDE_DIR) + include(cmake/OpenCVFindAVIF.cmake) + if(AVIF_FOUND) + set(HAVE_AVIF 1) + endif() +endif() + # --- libjpeg (optional) --- if(WITH_JPEG) if(BUILD_JPEG) diff --git a/cmake/templates/cvconfig.h.in b/cmake/templates/cvconfig.h.in index 633164c75c..d6c7875411 100644 --- a/cmake/templates/cvconfig.h.in +++ b/cmake/templates/cvconfig.h.in @@ -72,6 +72,9 @@ #cmakedefine HAVE_OPENJPEG #cmakedefine HAVE_JASPER +/* AVIF codec */ +#cmakedefine HAVE_AVIF + /* IJG JPEG codec */ #cmakedefine HAVE_JPEG diff --git a/modules/imgcodecs/CMakeLists.txt b/modules/imgcodecs/CMakeLists.txt index 1572543aff..8183837c43 100644 --- a/modules/imgcodecs/CMakeLists.txt +++ b/modules/imgcodecs/CMakeLists.txt @@ -13,6 +13,11 @@ if(HAVE_WINRT_CX AND NOT WINRT) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /ZW") endif() +if (HAVE_AVIF) + ocv_include_directories(${AVIF_INCLUDE_DIR}) + list(APPEND GRFMT_LIBS ${AVIF_LIBRARY}) +endif() + if(HAVE_JPEG) ocv_include_directories(${JPEG_INCLUDE_DIR} ${${JPEG_LIBRARY}_BINARY_DIR}) list(APPEND GRFMT_LIBS ${JPEG_LIBRARIES}) diff --git a/modules/imgcodecs/include/opencv2/imgcodecs.hpp b/modules/imgcodecs/include/opencv2/imgcodecs.hpp index ca79b90d19..c1bdf72291 100644 --- a/modules/imgcodecs/include/opencv2/imgcodecs.hpp +++ b/modules/imgcodecs/include/opencv2/imgcodecs.hpp @@ -105,7 +105,10 @@ enum ImwriteFlags { IMWRITE_TIFF_XDPI = 257,//!< For TIFF, use to specify the X direction DPI IMWRITE_TIFF_YDPI = 258,//!< For TIFF, use to specify the Y direction DPI IMWRITE_TIFF_COMPRESSION = 259,//!< For TIFF, use to specify the image compression scheme. See libtiff for integer constants corresponding to compression formats. Note, for images whose depth is CV_32F, only libtiff's SGILOG compression scheme is used. For other supported depths, the compression scheme can be specified by this flag; LZW compression is the default. - 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_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_32F. Default is 8. + IMWRITE_AVIF_SPEED = 514 //!< For AVIF, it is between 0 (slowest) and (fastest). Default is 9. }; enum ImwriteJPEGSamplingFactorParams { @@ -185,6 +188,7 @@ Currently, the following file formats are supported: - JPEG 2000 files - \*.jp2 (see the *Note* section) - Portable Network Graphics - \*.png (see the *Note* section) - WebP - \*.webp (see the *Note* section) +- AVIF - \*.avif (see the *Note* section) - Portable image format - \*.pbm, \*.pgm, \*.ppm \*.pxm, \*.pnm (always supported) - PFM files - \*.pfm (see the *Note* section) - Sun rasters - \*.sr, \*.ras (always supported) diff --git a/modules/imgcodecs/src/grfmt_avif.cpp b/modules/imgcodecs/src/grfmt_avif.cpp new file mode 100644 index 0000000000..e8d1446cbe --- /dev/null +++ b/modules/imgcodecs/src/grfmt_avif.cpp @@ -0,0 +1,369 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level +// directory of this distribution and at http://opencv.org/license.html + +#include "precomp.hpp" + +#ifdef HAVE_AVIF + +#include +#include + +#include +#include "opencv2/imgproc.hpp" +#include "grfmt_avif.hpp" + +#define CV_AVIF_USE_QUALITY \ + (AVIF_VERSION > ((0 * 1000000) + (11 * 10000) + (1 * 100))) + +#if !CV_AVIF_USE_QUALITY +#define AVIF_QUALITY_LOSSLESS 100 +#define AVIF_QUALITY_WORST 0 +#define AVIF_QUALITY_BEST 100 + +#endif + +namespace cv { +namespace { + +struct AvifImageDeleter { + void operator()(avifImage *image) { avifImageDestroy(image); } +}; + +using AvifImageUniquePtr = std::unique_ptr; + +avifResult CopyToMat(const avifImage *image, int channels, Mat *mat) { + CV_Assert((int)image->height == mat->rows); + CV_Assert((int)image->width == mat->cols); + if (channels == 1) { + const cv::Mat image_wrap = + cv::Mat(image->height, image->width, + CV_MAKE_TYPE((image->depth == 8) ? CV_8U : CV_16U, 1), + image->yuvPlanes[0], image->yuvRowBytes[0]); + if ((image->depth == 8 && mat->depth() == CV_8U) || + (image->depth > 8 && mat->depth() == CV_16U)) { + image_wrap.copyTo(*mat); + } else { + CV_Assert(image->depth > 8 && mat->depth() == CV_8U); + image_wrap.convertTo(*mat, CV_8U, 1. / (1 << (image->depth - 8))); + } + return AVIF_RESULT_OK; + } + avifRGBImage rgba; + avifRGBImageSetDefaults(&rgba, image); + if (channels == 3) { + rgba.format = AVIF_RGB_FORMAT_BGR; + } else { + CV_Assert(channels == 4); + rgba.format = AVIF_RGB_FORMAT_BGRA; + } + rgba.rowBytes = mat->step[0]; + rgba.depth = (mat->depth() == CV_16U) ? image->depth : 8; + rgba.pixels = reinterpret_cast(mat->data); + return avifImageYUVToRGB(image, &rgba); +} + +AvifImageUniquePtr ConvertToAvif(const cv::Mat &img, bool lossless, + int bit_depth) { + CV_Assert(img.depth() == CV_8U || img.depth() == CV_16U); + + const int width = img.cols; + const int height = img.rows; + + avifImage *result; + + if (img.channels() == 1) { + result = avifImageCreateEmpty(); + if (result == nullptr) return nullptr; + result->width = width; + result->height = height; + result->depth = bit_depth; + result->yuvFormat = AVIF_PIXEL_FORMAT_YUV400; + result->colorPrimaries = AVIF_COLOR_PRIMARIES_UNSPECIFIED; + result->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED; + result->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_IDENTITY; + result->yuvRange = AVIF_RANGE_FULL; + result->yuvPlanes[0] = img.data; + result->yuvRowBytes[0] = img.step[0]; + result->imageOwnsYUVPlanes = AVIF_FALSE; + return AvifImageUniquePtr(result); + } + + if (lossless) { + result = + avifImageCreate(width, height, bit_depth, AVIF_PIXEL_FORMAT_YUV444); + if (result == nullptr) return nullptr; + result->colorPrimaries = AVIF_COLOR_PRIMARIES_UNSPECIFIED; + result->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED; + result->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_IDENTITY; + result->yuvRange = AVIF_RANGE_FULL; + } else { + result = + avifImageCreate(width, height, bit_depth, AVIF_PIXEL_FORMAT_YUV420); + if (result == nullptr) return nullptr; + result->colorPrimaries = AVIF_COLOR_PRIMARIES_BT709; + result->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_SRGB; + result->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_BT601; + result->yuvRange = AVIF_RANGE_FULL; + } + + avifRGBImage rgba; + avifRGBImageSetDefaults(&rgba, result); + if (img.channels() == 3) { + rgba.format = AVIF_RGB_FORMAT_BGR; + } else { + CV_Assert(img.channels() == 4); + rgba.format = AVIF_RGB_FORMAT_BGRA; + } + rgba.rowBytes = img.step[0]; + rgba.depth = bit_depth; + rgba.pixels = + const_cast(reinterpret_cast(img.data)); + + if (avifImageRGBToYUV(result, &rgba) != AVIF_RESULT_OK) { + avifImageDestroy(result); + return nullptr; + } + return AvifImageUniquePtr(result); +} + +} // namespace + +// 64Mb limit to avoid memory saturation. +static const size_t kParamMaxFileSize = utils::getConfigurationParameterSizeT( + "OPENCV_IMGCODECS_AVIF_MAX_FILE_SIZE", 64 * 1024 * 1024); + +static constexpr size_t kAvifSignatureSize = 500; + +AvifDecoder::AvifDecoder() { + m_buf_supported = true; + channels_ = 0; + decoder_ = avifDecoderCreate(); +} + +AvifDecoder::~AvifDecoder() { + if (decoder_ != nullptr) avifDecoderDestroy(decoder_); +} + +size_t AvifDecoder::signatureLength() const { return kAvifSignatureSize; } + +bool AvifDecoder::checkSignature(const String &signature) const { + avifDecoderSetIOMemory(decoder_, + reinterpret_cast(signature.c_str()), + signature.size()); + decoder_->io->sizeHint = 1e9; + const avifResult status = avifDecoderParse(decoder_); + return (status == AVIF_RESULT_OK || status == AVIF_RESULT_TRUNCATED_DATA); +} + +#define OPENCV_AVIF_CHECK_STATUS(X, ENCDEC) \ + { \ + const avifResult status = (X); \ + if (status != AVIF_RESULT_OK) { \ + const std::string error(ENCDEC->diag.error); \ + CV_Error(Error::StsParseError, \ + error + " " + avifResultToString(status)); \ + return false; \ + } \ + } + +ImageDecoder AvifDecoder::newDecoder() const { return makePtr(); } + +bool AvifDecoder::readHeader() { + if (!m_buf.empty()) { + CV_Assert(m_buf.type() == CV_8UC1); + CV_Assert(m_buf.rows == 1); + } + + OPENCV_AVIF_CHECK_STATUS( + m_buf.empty() + ? avifDecoderSetIOFile(decoder_, m_filename.c_str()) + : avifDecoderSetIOMemory( + decoder_, reinterpret_cast(m_buf.data), + m_buf.total()), + decoder_); + OPENCV_AVIF_CHECK_STATUS(avifDecoderParse(decoder_), decoder_); + + m_width = decoder_->image->width; + m_height = decoder_->image->height; + 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); + m_type = CV_MAKETYPE(bit_depth_ == 8 ? CV_8U : CV_16U, channels_); + is_first_image_ = true; + return true; +} + +bool AvifDecoder::readData(Mat &img) { + CV_CheckGE(m_width, 0, ""); + CV_CheckGE(m_height, 0, ""); + + CV_CheckEQ(img.cols, m_width, ""); + CV_CheckEQ(img.rows, m_height, ""); + CV_CheckType( + img.type(), + (img.channels() == 1 || img.channels() == 3 || img.channels() == 4) && + (img.depth() == CV_8U || img.depth() == CV_16U), + "AVIF only supports 1, 3, 4 channels and CV_8U and CV_16U"); + + Mat read_img; + if (img.channels() == channels_) { + read_img = img; + } else { + // Use the asked depth but keep the number of channels. OpenCV and not + // libavif will do the color conversion. + read_img.create(m_height, m_width, CV_MAKE_TYPE(img.depth(), channels_)); + } + + if (is_first_image_) { + if (!nextPage()) return false; + is_first_image_ = false; + } + + if (CopyToMat(decoder_->image, channels_, &read_img) != AVIF_RESULT_OK) { + CV_Error(Error::StsInternal, "Cannot convert from AVIF to Mat"); + return false; + } + + if (decoder_->image->exif.size > 0) { + m_exif.parseExif(decoder_->image->exif.data, decoder_->image->exif.size); + } + + if (img.channels() == channels_) { + // We already wrote to the right buffer. + } else { + if (channels_ == 1 && img.channels() == 3) { + cvtColor(read_img, img, COLOR_GRAY2BGR); + } else if (channels_ == 1 && img.channels() == 4) { + cvtColor(read_img, img, COLOR_GRAY2BGRA); + } else if (channels_ == 3 && img.channels() == 1) { + cvtColor(read_img, img, COLOR_BGR2GRAY); + } else if (channels_ == 3 && img.channels() == 4) { + cvtColor(read_img, img, COLOR_BGR2BGRA); + } else if (channels_ == 4 && img.channels() == 1) { + cvtColor(read_img, img, COLOR_BGRA2GRAY); + } else if (channels_ == 4 && img.channels() == 3) { + cvtColor(read_img, img, COLOR_BGRA2BGR); + } else { + CV_Error(Error::StsInternal, ""); + } + } + return true; +} + +bool AvifDecoder::nextPage() { + const avifResult status = avifDecoderNextImage(decoder_); + if (status == AVIF_RESULT_NO_IMAGES_REMAINING) return false; + if (status != AVIF_RESULT_OK) { + const std::string error(decoder_->diag.error); + CV_Error(Error::StsParseError, error + " " + avifResultToString(status)); + return false; + } + return true; +} + +//////////////////////////////////////////////////////////////////////////////// + +AvifEncoder::AvifEncoder() { + m_description = "AVIF files (*.avif)"; + m_buf_supported = true; + encoder_ = avifEncoderCreate(); +} + +AvifEncoder::~AvifEncoder() { + if (encoder_) avifEncoderDestroy(encoder_); +} + +bool AvifEncoder::isFormatSupported(int depth) const { + return (depth == CV_8U || depth == CV_16U); +} + +bool AvifEncoder::write(const Mat &img, const std::vector ¶ms) { + std::vector img_vec(1, img); + return writeToOutput(img_vec, params); +} + +bool AvifEncoder::writemulti(const std::vector &img_vec, + const std::vector ¶ms) { + return writeToOutput(img_vec, params); +} + +bool AvifEncoder::writeToOutput(const std::vector &img_vec, + const std::vector ¶ms) { + int bit_depth = 8; + int speed = AVIF_SPEED_FASTEST; + for (size_t i = 0; i < params.size(); i += 2) { + if (params[i] == IMWRITE_AVIF_QUALITY) { + const int quality = std::min(std::max(params[i + 1], AVIF_QUALITY_WORST), + AVIF_QUALITY_BEST); +#if CV_AVIF_USE_QUALITY + encoder_->quality = quality; +#else + encoder_->minQuantizer = encoder_->maxQuantizer = + (AVIF_QUANTIZER_BEST_QUALITY - AVIF_QUANTIZER_WORST_QUALITY) * + quality / (AVIF_QUALITY_BEST - AVIF_QUALITY_WORST) + + AVIF_QUANTIZER_WORST_QUALITY; +#endif + } else if (params[i] == IMWRITE_AVIF_DEPTH) { + bit_depth = params[i + 1]; + } else if (params[i] == IMWRITE_AVIF_SPEED) { + speed = params[i + 1]; + } + } + + avifRWData output_ori = AVIF_DATA_EMPTY; + std::unique_ptr output(&output_ori, + avifRWDataFree); +#if CV_AVIF_USE_QUALITY + const bool do_lossless = (encoder_->quality == AVIF_QUALITY_LOSSLESS); +#else + const bool do_lossless = + (encoder_->minQuantizer == AVIF_QUANTIZER_BEST_QUALITY && + encoder_->maxQuantizer == AVIF_QUANTIZER_BEST_QUALITY); +#endif + encoder_->speed = speed; + + const avifAddImageFlags flag = (img_vec.size() == 1) + ? AVIF_ADD_IMAGE_FLAG_SINGLE + : AVIF_ADD_IMAGE_FLAG_NONE; + std::vector images; + std::vector imgs_scaled; + for (const cv::Mat &img : img_vec) { + CV_CheckType( + img.type(), + (bit_depth == 8 && img.depth() == CV_8U) || + ((bit_depth == 10 || bit_depth == 12) && 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"); + CV_Check(img.channels(), + img.channels() == 1 || img.channels() == 3 || img.channels() == 4, + "AVIF only supports 1, 3, 4 channels"); + + images.emplace_back(ConvertToAvif(img, do_lossless, bit_depth)); + } + for (const AvifImageUniquePtr &image : images) { + OPENCV_AVIF_CHECK_STATUS( + avifEncoderAddImage(encoder_, image.get(), /*durationInTimescale=*/1, + flag), + encoder_); + } + + OPENCV_AVIF_CHECK_STATUS(avifEncoderFinish(encoder_, output.get()), encoder_); + + if (m_buf) { + m_buf->resize(output->size); + std::memcpy(m_buf->data(), output->data, output->size); + } else { + std::ofstream(m_filename, std::ofstream::binary) + .write(reinterpret_cast(output->data), output->size); + } + + return (output->size > 0); +} + +ImageEncoder AvifEncoder::newEncoder() const { return makePtr(); } + +} // namespace cv + +#endif diff --git a/modules/imgcodecs/src/grfmt_avif.hpp b/modules/imgcodecs/src/grfmt_avif.hpp new file mode 100644 index 0000000000..e64357366a --- /dev/null +++ b/modules/imgcodecs/src/grfmt_avif.hpp @@ -0,0 +1,62 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level +// directory of this distribution and at http://opencv.org/license.html + +#ifndef _GRFMT_AVIF_H_ +#define _GRFMT_AVIF_H_ + +#include "grfmt_base.hpp" + +#ifdef HAVE_AVIF + +struct avifDecoder; +struct avifEncoder; +struct avifRWData; + +namespace cv { + +class AvifDecoder CV_FINAL : public BaseImageDecoder { + public: + AvifDecoder(); + ~AvifDecoder(); + + bool readHeader() CV_OVERRIDE; + bool readData(Mat& img) CV_OVERRIDE; + bool nextPage() CV_OVERRIDE; + + size_t signatureLength() const CV_OVERRIDE; + bool checkSignature(const String& signature) const CV_OVERRIDE; + ImageDecoder newDecoder() const CV_OVERRIDE; + + protected: + int channels_; + int bit_depth_; + avifDecoder* decoder_; + bool is_first_image_; +}; + +class AvifEncoder CV_FINAL : public BaseImageEncoder { + public: + AvifEncoder(); + ~AvifEncoder() CV_OVERRIDE; + + bool isFormatSupported(int depth) const CV_OVERRIDE; + + bool write(const Mat& img, const std::vector& params) CV_OVERRIDE; + + bool writemulti(const std::vector& img_vec, + const std::vector& params) CV_OVERRIDE; + + ImageEncoder newEncoder() const CV_OVERRIDE; + + private: + bool writeToOutput(const std::vector& img_vec, + const std::vector& params); + avifEncoder* encoder_; +}; + +} // namespace cv + +#endif + +#endif /*_GRFMT_AVIF_H_*/ diff --git a/modules/imgcodecs/src/grfmts.hpp b/modules/imgcodecs/src/grfmts.hpp index 637538d223..46b79ff96c 100644 --- a/modules/imgcodecs/src/grfmts.hpp +++ b/modules/imgcodecs/src/grfmts.hpp @@ -43,6 +43,7 @@ #define _GRFMTS_H_ #include "grfmt_base.hpp" +#include "grfmt_avif.hpp" #include "grfmt_bmp.hpp" #include "grfmt_sunras.hpp" #include "grfmt_jpeg.hpp" diff --git a/modules/imgcodecs/src/loadsave.cpp b/modules/imgcodecs/src/loadsave.cpp index 7d980b9343..d0413c1ade 100644 --- a/modules/imgcodecs/src/loadsave.cpp +++ b/modules/imgcodecs/src/loadsave.cpp @@ -132,6 +132,10 @@ struct ImageCodecInitializer */ ImageCodecInitializer() { +#ifdef HAVE_AVIF + decoders.push_back(makePtr()); + encoders.push_back(makePtr()); +#endif /// BMP Support decoders.push_back( makePtr() ); encoders.push_back( makePtr() ); diff --git a/modules/imgcodecs/test/test_avif.cpp b/modules/imgcodecs/test/test_avif.cpp new file mode 100644 index 0000000000..99b8f7769c --- /dev/null +++ b/modules/imgcodecs/test/test_avif.cpp @@ -0,0 +1,355 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level +// directory of this distribution and at http://opencv.org/license.html + +#include +#include + +#include "test_precomp.hpp" + +#ifdef HAVE_AVIF + +namespace opencv_test { +namespace { + +class Imgcodecs_Avif_RoundTripSuite + : public testing::TestWithParam> { + protected: + static cv::Mat modifyImage(const cv::Mat& img_original, int channels, + int bit_depth) { + cv::Mat img; + if (channels == 1) { + cv::cvtColor(img_original, img, cv::COLOR_BGR2GRAY); + } else if (channels == 4) { + std::vector imgs; + cv::split(img_original, imgs); + imgs.push_back(cv::Mat(imgs[0])); + imgs[imgs.size() - 1] = cv::Scalar::all(128); + cv::merge(imgs, img); + } else { + img = img_original.clone(); + } + + cv::Mat img_final = img; + // Convert image to CV_16U for some bit depths. + if (bit_depth > 8) img.convertTo(img_final, CV_16U, 1 << (bit_depth - 8)); + + return img_final; + } + + void SetUp() { + bit_depth_ = std::get<0>(GetParam()); + channels_ = std::get<1>(GetParam()); + quality_ = std::get<2>(GetParam()); + imread_mode_ = std::get<3>(GetParam()); + encoding_params_ = {cv::IMWRITE_AVIF_QUALITY, quality_, + cv::IMWRITE_AVIF_DEPTH, bit_depth_}; + } + + bool IsBitDepthValid() const { + return (bit_depth_ == 8 || bit_depth_ == 10 || bit_depth_ == 12); + } + + // Makes sure images are close enough after encode/decode roundtrip. + void ValidateRead(const cv::Mat& img_original, const cv::Mat& img) const { + EXPECT_EQ(img_original.size(), img.size()); + if (imread_mode_ == IMREAD_UNCHANGED) { + ASSERT_EQ(img_original.type(), img.type()); + // Lossless. + 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)); + if (quality_ == 50) { + EXPECT_LE(norm, 10); + } else if (quality_ == 0) { + EXPECT_LE(norm, 13); + } else { + EXPECT_FALSE(true); + } + } + } + } + + public: + int bit_depth_; + int channels_; + int quality_; + int imread_mode_; + std::vector encoding_params_; +}; + +//////////////////////////////////////////////////////////////////////////////// + +class Imgcodecs_Avif_Image_RoundTripSuite + : public Imgcodecs_Avif_RoundTripSuite { + public: + const cv::Mat& get_img_original() { + const Key key = {channels_, (bit_depth_ < 8) ? 8 : bit_depth_}; + return imgs_[key]; + } + + // Prepare the original image modified for different number of channels and + // bit depth. + static void SetUpTestCase() { + const string root = cvtest::TS::ptr()->get_data_path(); + const string filename = root + "../cv/shared/lena.png"; + const cv::Mat img_original = cv::imread(filename); + 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}) { + const Key key{channels, bit_depth}; + imgs_[key] = modifyImage(img_resized, channels, bit_depth); + } + } + } + + static const int kWidth; + static const int kHeight; + + private: + typedef std::tuple Key; + static std::map imgs_; +}; +std::map, cv::Mat> + Imgcodecs_Avif_Image_RoundTripSuite::imgs_; +const int Imgcodecs_Avif_Image_RoundTripSuite::kWidth = 51; +const int Imgcodecs_Avif_Image_RoundTripSuite::kHeight = 31; + +class Imgcodecs_Avif_Image_WriteReadSuite + : public Imgcodecs_Avif_Image_RoundTripSuite {}; + +TEST_P(Imgcodecs_Avif_Image_WriteReadSuite, imwrite_imread) { + const cv::Mat& img_original = get_img_original(); + ASSERT_FALSE(img_original.empty()); + + // Encode. + const string output = cv::tempfile(".avif"); + if (!IsBitDepthValid()) { + EXPECT_NO_FATAL_FAILURE( + cv::imwrite(output, img_original, encoding_params_)); + EXPECT_NE(0, remove(output.c_str())); + return; + } + EXPECT_NO_THROW(cv::imwrite(output, img_original, encoding_params_)); + + // Read from file. + const cv::Mat img = cv::imread(output, imread_mode_); + + ValidateRead(img_original, img); + + EXPECT_EQ(0, remove(output.c_str())); +} + +INSTANTIATE_TEST_CASE_P( + Imgcodecs_AVIF, Imgcodecs_Avif_Image_WriteReadSuite, + ::testing::Combine(::testing::ValuesIn({6, 8, 10, 12}), + ::testing::ValuesIn({1, 3, 4}), + ::testing::ValuesIn({0, 50, 100}), + ::testing::ValuesIn({IMREAD_UNCHANGED, IMREAD_GRAYSCALE, + IMREAD_COLOR}))); + +class Imgcodecs_Avif_Image_EncodeDecodeSuite + : public Imgcodecs_Avif_Image_RoundTripSuite {}; + +TEST_P(Imgcodecs_Avif_Image_EncodeDecodeSuite, imencode_imdecode) { + const cv::Mat& img_original = get_img_original(); + ASSERT_FALSE(img_original.empty()); + + // Encode. + std::vector buf; + if (!IsBitDepthValid()) { + EXPECT_THROW(cv::imencode(".avif", img_original, buf, encoding_params_), + cv::Exception); + return; + } + bool result; + EXPECT_NO_THROW( + result = cv::imencode(".avif", img_original, buf, encoding_params_);); + EXPECT_TRUE(result); + + // Read back. + const cv::Mat img = cv::imdecode(buf, imread_mode_); + + ValidateRead(img_original, img); +} + +INSTANTIATE_TEST_CASE_P( + Imgcodecs_AVIF, Imgcodecs_Avif_Image_EncodeDecodeSuite, + ::testing::Combine(::testing::ValuesIn({6, 8, 10, 12}), + ::testing::ValuesIn({1, 3, 4}), + ::testing::ValuesIn({0, 50, 100}), + ::testing::ValuesIn({IMREAD_UNCHANGED, IMREAD_GRAYSCALE, + IMREAD_COLOR}))); + +//////////////////////////////////////////////////////////////////////////////// + +typedef testing::TestWithParam Imgcodecs_AVIF_Exif; + +TEST_P(Imgcodecs_AVIF_Exif, exif_orientation) { + const string root = cvtest::TS::ptr()->get_data_path(); + const string filename = root + GetParam(); + const int colorThresholdHigh = 250; + const int colorThresholdLow = 5; + + Mat m_img = imread(filename); + ASSERT_FALSE(m_img.empty()); + Vec3b vec; + + // Checking the first quadrant (with supposed red) + vec = m_img.at(2, 2); // some point inside the square + EXPECT_LE(vec.val[0], colorThresholdLow); + EXPECT_LE(vec.val[1], colorThresholdLow); + EXPECT_GE(vec.val[2], colorThresholdHigh); + + // Checking the second quadrant (with supposed green) + vec = m_img.at(2, 7); // some point inside the square + EXPECT_LE(vec.val[0], colorThresholdLow); + EXPECT_GE(vec.val[1], colorThresholdHigh); + EXPECT_LE(vec.val[2], colorThresholdLow); + + // Checking the third quadrant (with supposed blue) + vec = m_img.at(7, 2); // some point inside the square + EXPECT_GE(vec.val[0], colorThresholdHigh); + EXPECT_LE(vec.val[1], colorThresholdLow); + EXPECT_LE(vec.val[2], colorThresholdLow); +} + +const string exif_files[] = {"readwrite/testExifOrientation_1.avif", + "readwrite/testExifOrientation_2.avif", + "readwrite/testExifOrientation_3.avif", + "readwrite/testExifOrientation_4.avif", + "readwrite/testExifOrientation_5.avif", + "readwrite/testExifOrientation_6.avif", + "readwrite/testExifOrientation_7.avif", + "readwrite/testExifOrientation_8.avif"}; + +INSTANTIATE_TEST_CASE_P(ExifFiles, Imgcodecs_AVIF_Exif, + testing::ValuesIn(exif_files)); + +//////////////////////////////////////////////////////////////////////////////// + +class Imgcodecs_Avif_Animation_RoundTripSuite + : public Imgcodecs_Avif_RoundTripSuite { + public: + const std::vector& get_anim_original() { + const Key key = {channels_, bit_depth_}; + return anims_[key]; + } + + // Prepare the original image modified for different number of channels and + // bit depth. + static void SetUpTestCase() { + const string root = cvtest::TS::ptr()->get_data_path(); + const string filename = root + "../cv/shared/lena.png"; + const cv::Mat img_original = cv::imread(filename); + 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}) { + const Key key{channels, bit_depth}; + const cv::Mat img = modifyImage(img_resized, channels, bit_depth); + cv::Mat img2, img3; + cv::flip(img, img2, 0); + cv::flip(img, img3, -1); + anims_[key] = {img, img2, img3}; + } + } + } + + void ValidateRead(const std::vector& anim_original, + const std::vector& anim) const { + ASSERT_EQ(anim_original.size(), anim.size()); + for (size_t i = 0; i < anim.size(); ++i) { + Imgcodecs_Avif_RoundTripSuite::ValidateRead(anim_original[i], anim[i]); + } + } + + static const int kWidth; + static const int kHeight; + + private: + typedef std::tuple Key; + static std::map> anims_; +}; +std::map, std::vector> + Imgcodecs_Avif_Animation_RoundTripSuite::anims_; +const int Imgcodecs_Avif_Animation_RoundTripSuite::kWidth = 5; +const int Imgcodecs_Avif_Animation_RoundTripSuite::kHeight = 5; + +class Imgcodecs_Avif_Animation_WriteReadSuite + : public Imgcodecs_Avif_Animation_RoundTripSuite {}; + +TEST_P(Imgcodecs_Avif_Animation_WriteReadSuite, encode_decode) { + const std::vector& anim_original = get_anim_original(); + ASSERT_FALSE(anim_original.empty()); + + // Encode. + const string output = cv::tempfile(".avif"); + if (!IsBitDepthValid()) { + EXPECT_THROW(cv::imwritemulti(output, anim_original, encoding_params_), + cv::Exception); + EXPECT_NE(0, remove(output.c_str())); + return; + } + EXPECT_NO_THROW(cv::imwritemulti(output, anim_original, encoding_params_)); + + // Read from file. + std::vector anim; + ASSERT_TRUE(cv::imreadmulti(output, anim, imread_mode_)); + + ValidateRead(anim_original, anim); + + EXPECT_EQ(0, remove(output.c_str())); +} + +INSTANTIATE_TEST_CASE_P( + Imgcodecs_AVIF, Imgcodecs_Avif_Animation_WriteReadSuite, + ::testing::Combine(::testing::ValuesIn({8, 10, 12}), + ::testing::ValuesIn({1, 3}), ::testing::ValuesIn({50}), + ::testing::ValuesIn({IMREAD_UNCHANGED, IMREAD_GRAYSCALE, + IMREAD_COLOR}))); +class Imgcodecs_Avif_Animation_WriteDecodeSuite + : public Imgcodecs_Avif_Animation_RoundTripSuite {}; + +TEST_P(Imgcodecs_Avif_Animation_WriteDecodeSuite, encode_decode) { + const std::vector& anim_original = get_anim_original(); + ASSERT_FALSE(anim_original.empty()); + + // Encode. + const string output = cv::tempfile(".avif"); + if (!IsBitDepthValid()) { + EXPECT_THROW(cv::imwritemulti(output, anim_original, encoding_params_), + cv::Exception); + EXPECT_NE(0, remove(output.c_str())); + return; + } + EXPECT_NO_THROW(cv::imwritemulti(output, anim_original, encoding_params_)); + + // Put file into buffer and read from buffer. + std::ifstream file(output, std::ios::binary | std::ios::ate); + std::streamsize size = file.tellg(); + file.seekg(0, std::ios::beg); + std::vector buf(size); + EXPECT_TRUE(file.read(reinterpret_cast(buf.data()), size)); + EXPECT_EQ(0, remove(output.c_str())); + std::vector anim; + ASSERT_TRUE(cv::imdecodemulti(buf, imread_mode_, anim)); + + ValidateRead(anim_original, anim); +} + +INSTANTIATE_TEST_CASE_P( + Imgcodecs_AVIF, Imgcodecs_Avif_Animation_WriteDecodeSuite, + ::testing::Combine(::testing::ValuesIn({8, 10, 12}), + ::testing::ValuesIn({1, 3}), ::testing::ValuesIn({50}), + ::testing::ValuesIn({IMREAD_UNCHANGED, IMREAD_GRAYSCALE, + IMREAD_COLOR}))); + +} // namespace +} // namespace opencv_test + +#endif // HAVE_AVIF