mirror of https://github.com/opencv/opencv.git
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 CMakepull/23774/head
parent
0c8e6e0e68
commit
472aad46a6
11 changed files with 870 additions and 1 deletions
@ -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() |
@ -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> ¶ms) { |
||||||
|
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> ¶ms) { |
||||||
|
return writeToOutput(img_vec, params); |
||||||
|
} |
||||||
|
|
||||||
|
bool AvifEncoder::writeToOutput(const std::vector<Mat> &img_vec, |
||||||
|
const std::vector<int> ¶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<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 |
@ -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_*/ |
@ -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
|
Loading…
Reference in new issue