Merge pull request #23596 from vrabaud:libavif

Add AVIF support through libavif. #23596

This is to fix https://github.com/opencv/opencv/issues/19271
Extra: https://github.com/opencv/opencv_extra/pull/1069

### 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:
Vincent Rabaud 2023-06-09 14:39:10 +02:00 committed by GitHub
parent 0c8e6e0e68
commit 472aad46a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 870 additions and 1 deletions

View File

@ -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 OCV_OPTION(WITH_AVFOUNDATION "Use AVFoundation for Video I/O (iOS/Mac)" ON
VISIBLE_IF APPLE VISIBLE_IF APPLE
VERIFY HAVE_AVFOUNDATION) VERIFY HAVE_AVFOUNDATION)
OCV_OPTION(WITH_AVIF "Enable AVIF support" OFF
VERIFY HAVE_AVIF)
OCV_OPTION(WITH_CAP_IOS "Enable iOS video capture" ON OCV_OPTION(WITH_CAP_IOS "Enable iOS video capture" ON
VISIBLE_IF IOS VISIBLE_IF IOS
VERIFY HAVE_CAP_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})") status(" WEBP:" WEBP_FOUND THEN "${WEBP_LIBRARY} (ver ${WEBP_VERSION})" ELSE "build (ver ${WEBP_VERSION})")
endif() 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_PNG OR HAVE_PNG OR WITH_SPNG)
if(WITH_SPNG) if(WITH_SPNG)
status(" PNG:" "build-${SPNG_LIBRARY} (ver ${SPNG_VERSION})") status(" PNG:" "build-${SPNG_LIBRARY} (ver ${SPNG_VERSION})")

View File

@ -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()

View File

@ -37,6 +37,16 @@ if(NOT ZLIB_FOUND)
ocv_parse_header2(ZLIB "${${ZLIB_LIBRARY}_SOURCE_DIR}/zlib.h" ZLIB_VERSION) ocv_parse_header2(ZLIB "${${ZLIB_LIBRARY}_SOURCE_DIR}/zlib.h" ZLIB_VERSION)
endif() 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) --- # --- libjpeg (optional) ---
if(WITH_JPEG) if(WITH_JPEG)
if(BUILD_JPEG) if(BUILD_JPEG)

View File

@ -72,6 +72,9 @@
#cmakedefine HAVE_OPENJPEG #cmakedefine HAVE_OPENJPEG
#cmakedefine HAVE_JASPER #cmakedefine HAVE_JASPER
/* AVIF codec */
#cmakedefine HAVE_AVIF
/* IJG JPEG codec */ /* IJG JPEG codec */
#cmakedefine HAVE_JPEG #cmakedefine HAVE_JPEG

View File

@ -13,6 +13,11 @@ if(HAVE_WINRT_CX AND NOT WINRT)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /ZW") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /ZW")
endif() endif()
if (HAVE_AVIF)
ocv_include_directories(${AVIF_INCLUDE_DIR})
list(APPEND GRFMT_LIBS ${AVIF_LIBRARY})
endif()
if(HAVE_JPEG) if(HAVE_JPEG)
ocv_include_directories(${JPEG_INCLUDE_DIR} ${${JPEG_LIBRARY}_BINARY_DIR}) ocv_include_directories(${JPEG_INCLUDE_DIR} ${${JPEG_LIBRARY}_BINARY_DIR})
list(APPEND GRFMT_LIBS ${JPEG_LIBRARIES}) list(APPEND GRFMT_LIBS ${JPEG_LIBRARIES})

View File

@ -105,7 +105,10 @@ enum ImwriteFlags {
IMWRITE_TIFF_XDPI = 257,//!< For TIFF, use to specify the X direction DPI 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_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_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 { enum ImwriteJPEGSamplingFactorParams {
@ -185,6 +188,7 @@ Currently, the following file formats are supported:
- JPEG 2000 files - \*.jp2 (see the *Note* section) - JPEG 2000 files - \*.jp2 (see the *Note* section)
- Portable Network Graphics - \*.png (see the *Note* section) - Portable Network Graphics - \*.png (see the *Note* section)
- WebP - \*.webp (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) - Portable image format - \*.pbm, \*.pgm, \*.ppm \*.pxm, \*.pnm (always supported)
- PFM files - \*.pfm (see the *Note* section) - PFM files - \*.pfm (see the *Note* section)
- Sun rasters - \*.sr, \*.ras (always supported) - Sun rasters - \*.sr, \*.ras (always supported)

View File

@ -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 <avif/avif.h>
#include <fstream>
#include <opencv2/core/utils/configuration.private.hpp>
#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<avifImage, AvifImageDeleter>;
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<uint8_t *>(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<uint8_t *>(reinterpret_cast<const uint8_t *>(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<const uint8_t *>(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<AvifDecoder>(); }
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<const uint8_t *>(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<int> &params) {
std::vector<Mat> img_vec(1, img);
return writeToOutput(img_vec, params);
}
bool AvifEncoder::writemulti(const std::vector<Mat> &img_vec,
const std::vector<int> &params) {
return writeToOutput(img_vec, params);
}
bool AvifEncoder::writeToOutput(const std::vector<Mat> &img_vec,
const std::vector<int> &params) {
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<avifRWData, decltype(&avifRWDataFree)> 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<AvifImageUniquePtr> images;
std::vector<cv::Mat> 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<char *>(output->data), output->size);
}
return (output->size > 0);
}
ImageEncoder AvifEncoder::newEncoder() const { return makePtr<AvifEncoder>(); }
} // namespace cv
#endif

View File

@ -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<int>& params) CV_OVERRIDE;
bool writemulti(const std::vector<Mat>& img_vec,
const std::vector<int>& params) CV_OVERRIDE;
ImageEncoder newEncoder() const CV_OVERRIDE;
private:
bool writeToOutput(const std::vector<Mat>& img_vec,
const std::vector<int>& params);
avifEncoder* encoder_;
};
} // namespace cv
#endif
#endif /*_GRFMT_AVIF_H_*/

View File

@ -43,6 +43,7 @@
#define _GRFMTS_H_ #define _GRFMTS_H_
#include "grfmt_base.hpp" #include "grfmt_base.hpp"
#include "grfmt_avif.hpp"
#include "grfmt_bmp.hpp" #include "grfmt_bmp.hpp"
#include "grfmt_sunras.hpp" #include "grfmt_sunras.hpp"
#include "grfmt_jpeg.hpp" #include "grfmt_jpeg.hpp"

View File

@ -132,6 +132,10 @@ struct ImageCodecInitializer
*/ */
ImageCodecInitializer() ImageCodecInitializer()
{ {
#ifdef HAVE_AVIF
decoders.push_back(makePtr<AvifDecoder>());
encoders.push_back(makePtr<AvifEncoder>());
#endif
/// BMP Support /// BMP Support
decoders.push_back( makePtr<BmpDecoder>() ); decoders.push_back( makePtr<BmpDecoder>() );
encoders.push_back( makePtr<BmpEncoder>() ); encoders.push_back( makePtr<BmpEncoder>() );

View File

@ -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 <cstdint>
#include <fstream>
#include "test_precomp.hpp"
#ifdef HAVE_AVIF
namespace opencv_test {
namespace {
class Imgcodecs_Avif_RoundTripSuite
: public testing::TestWithParam<std::tuple<int, int, int, ImreadModes>> {
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<cv::Mat> 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<int> 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<int, int> Key;
static std::map<Key, cv::Mat> imgs_;
};
std::map<std::tuple<int, int>, 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<unsigned char> 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<string> 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<Vec3b>(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<Vec3b>(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<Vec3b>(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<cv::Mat>& 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<cv::Mat>& anim_original,
const std::vector<cv::Mat>& 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<int, int> Key;
static std::map<Key, std::vector<cv::Mat>> anims_;
};
std::map<std::tuple<int, int>, std::vector<cv::Mat>>
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<cv::Mat>& 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<cv::Mat> 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<cv::Mat>& 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<unsigned char> buf(size);
EXPECT_TRUE(file.read(reinterpret_cast<char*>(buf.data()), size));
EXPECT_EQ(0, remove(output.c_str()));
std::vector<cv::Mat> 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