Merge pull request #25608 from sturkmen72:animated_webp_support

Animated WebP Support #25608

related issues #24855 #22569 

### 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
- [ ] There is accuracy test, performance test and test data in opencv_extra repository, if applicable
      Patch to opencv_extra has the same branch name.
- [ ] The feature is well documented and sample code can be built with the project CMake
This commit is contained in:
Suleyman TURKMEN
2024-12-20 13:06:28 +03:00
committed by GitHub
parent 0903061589
commit d9a139f9e8
15 changed files with 1006 additions and 123 deletions

View File

@@ -196,19 +196,34 @@ if(WITH_WEBP AND NOT WEBP_FOUND
endif()
if(NOT WEBP_VERSION AND WEBP_INCLUDE_DIR)
ocv_clear_vars(ENC_MAJ_VERSION ENC_MIN_VERSION ENC_REV_VERSION)
if(EXISTS "${WEBP_INCLUDE_DIR}/enc/vp8enci.h")
ocv_parse_header("${WEBP_INCLUDE_DIR}/enc/vp8enci.h" WEBP_VERSION_LINES ENC_MAJ_VERSION ENC_MIN_VERSION ENC_REV_VERSION)
set(WEBP_VERSION "${ENC_MAJ_VERSION}.${ENC_MIN_VERSION}.${ENC_REV_VERSION}")
elseif(EXISTS "${WEBP_INCLUDE_DIR}/webp/encode.h")
if(EXISTS "${WEBP_INCLUDE_DIR}/webp/encode.h")
file(STRINGS "${WEBP_INCLUDE_DIR}/webp/encode.h" WEBP_ENCODER_ABI_VERSION REGEX "#define[ \t]+WEBP_ENCODER_ABI_VERSION[ \t]+([x0-9a-f]+)" )
if(WEBP_ENCODER_ABI_VERSION MATCHES "#define[ \t]+WEBP_ENCODER_ABI_VERSION[ \t]+([x0-9a-f]+)")
set(WEBP_ENCODER_ABI_VERSION "${CMAKE_MATCH_1}")
set(WEBP_VERSION "encoder: ${WEBP_ENCODER_ABI_VERSION}")
else()
unset(WEBP_ENCODER_ABI_VERSION)
endif()
endif()
if(EXISTS "${WEBP_INCLUDE_DIR}/webp/decode.h")
file(STRINGS "${WEBP_INCLUDE_DIR}/webp/decode.h" WEBP_DECODER_ABI_VERSION REGEX "#define[ \t]+WEBP_DECODER_ABI_VERSION[ \t]+([x0-9a-f]+)" )
if(WEBP_DECODER_ABI_VERSION MATCHES "#define[ \t]+WEBP_DECODER_ABI_VERSION[ \t]+([x0-9a-f]+)")
set(WEBP_DECODER_ABI_VERSION "${CMAKE_MATCH_1}")
else()
unset(WEBP_DECODER_ABI_VERSION)
endif()
endif()
if(EXISTS "${WEBP_INCLUDE_DIR}/webp/demux.h")
file(STRINGS "${WEBP_INCLUDE_DIR}/webp/demux.h" WEBP_DEMUX_ABI_VERSION REGEX "#define[ \t]+WEBP_DEMUX_ABI_VERSION[ \t]+([x0-9a-f]+)" )
if(WEBP_DEMUX_ABI_VERSION MATCHES "#define[ \t]+WEBP_DEMUX_ABI_VERSION[ \t]+([x0-9a-f]+)")
set(WEBP_DEMUX_ABI_VERSION "${CMAKE_MATCH_1}")
else()
unset(WEBP_DEMUX_ABI_VERSION)
endif()
endif()
set(WEBP_VERSION "decoder: ${WEBP_DECODER_ABI_VERSION}, encoder: ${WEBP_ENCODER_ABI_VERSION}, demux: ${WEBP_DEMUX_ABI_VERSION}")
endif()
# --- libopenjp2 (optional, check before libjasper) ---

View File

@@ -10,8 +10,6 @@
# Look for the header file.
unset(WEBP_FOUND)
FIND_PATH(WEBP_INCLUDE_DIR NAMES webp/decode.h)
if(NOT WEBP_INCLUDE_DIR)
@@ -21,13 +19,14 @@ else()
# Look for the library.
FIND_LIBRARY(WEBP_LIBRARY NAMES webp)
MARK_AS_ADVANCED(WEBP_LIBRARY)
FIND_LIBRARY(WEBP_MUX_LIBRARY NAMES webpmux)
FIND_LIBRARY(WEBP_DEMUX_LIBRARY NAMES webpdemux)
# handle the QUIETLY and REQUIRED arguments and set WEBP_FOUND to TRUE if
# all listed variables are TRUE
INCLUDE(${CMAKE_ROOT}/Modules/FindPackageHandleStandardArgs.cmake)
FIND_PACKAGE_HANDLE_STANDARD_ARGS(WebP DEFAULT_MSG WEBP_LIBRARY WEBP_INCLUDE_DIR)
SET(WEBP_LIBRARIES ${WEBP_LIBRARY})
SET(WEBP_LIBRARIES ${WEBP_LIBRARY} ${WEBP_MUX_LIBRARY} ${WEBP_DEMUX_LIBRARY})
SET(WEBP_INCLUDE_DIRS ${WEBP_INCLUDE_DIR})
endif()

View File

@@ -0,0 +1,91 @@
Handling Animated Image Files {#tutorial_animations}
===========================
@tableofcontents
| | |
| -: | :- |
| Original author | Suleyman Turkmen (with help of ChatGPT) |
| Compatibility | OpenCV >= 4.11 |
Goal
----
In this tutorial, you will learn how to:
- Use `cv::imreadanimation` to load frames from animated image files.
- Understand the structure and parameters of the `cv::Animation` structure.
- Display individual frames from an animation.
- Use `cv::imwriteanimation` to write `cv::Animation` to a file.
Source Code
-----------
@add_toggle_cpp
- **Downloadable code**: Click
[here](https://github.com/opencv/opencv/tree/4.x/samples/cpp/tutorial_code/imgcodecs/animations.cpp)
- **Code at a glance:**
@include samples/cpp/tutorial_code/imgcodecs/animations.cpp
@end_toggle
@add_toggle_python
- **Downloadable code**: Click
[here](https://github.com/opencv/opencv/tree/4.x/samples/python/tutorial_code/imgcodecs/animations.py)
- **Code at a glance:**
@include samples/python/tutorial_code/imgcodecs/animations.py
@end_toggle
Explanation
-----------
## Initializing the Animation Structure
Initialize a `cv::Animation` structure to hold the frames from the animated image file.
@add_toggle_cpp
@snippet cpp/tutorial_code/imgcodecs/animations.cpp init_animation
@end_toggle
@add_toggle_python
@snippet python/tutorial_code/imgcodecs/animations.py init_animation
@end_toggle
## Loading Frames
Use `cv::imreadanimation` to load frames from the specified file. Here, we load all frames from an animated WebP image.
@add_toggle_cpp
@snippet cpp/tutorial_code/imgcodecs/animations.cpp read_animation
@end_toggle
@add_toggle_python
@snippet python/tutorial_code/imgcodecs/animations.py read_animation
@end_toggle
## Displaying Frames
Each frame in the `animation.frames` vector can be displayed as a standalone image. This loop iterates through each frame, displaying it in a window with a short delay to simulate the animation.
@add_toggle_cpp
@snippet cpp/tutorial_code/imgcodecs/animations.cpp show_animation
@end_toggle
@add_toggle_python
@snippet python/tutorial_code/imgcodecs/animations.py show_animation
@end_toggle
## Saving Animation
@add_toggle_cpp
@snippet cpp/tutorial_code/imgcodecs/animations.cpp write_animation
@end_toggle
@add_toggle_python
@snippet python/tutorial_code/imgcodecs/animations.py write_animation
@end_toggle
## Summary
The `cv::imreadanimation` and `cv::imwriteanimation` functions make it easy to work with animated image files by loading frames into a `cv::Animation` structure, allowing frame-by-frame processing.
With these functions, you can load, process, and save frames from animated image files like GIF, AVIF, APNG, and WebP.

View File

@@ -10,3 +10,4 @@ Application utils (highgui, imgcodecs, videoio modules) {#tutorial_table_of_cont
- @subpage tutorial_orbbec_uvc
- @subpage tutorial_intelperc
- @subpage tutorial_wayland_ubuntu
- @subpage tutorial_animations

View File

@@ -236,6 +236,36 @@ enum ImwriteGIFCompressionFlags {
//! @} imgcodecs_flags
/** @brief Represents an animation with multiple frames.
The `Animation` struct is designed to store and manage data for animated sequences such as those from animated formats (e.g., GIF, AVIF, APNG, WebP).
It provides support for looping, background color settings, frame timing, and frame storage.
*/
struct CV_EXPORTS_W_SIMPLE Animation
{
//! Number of times the animation should loop. 0 means infinite looping.
CV_PROP_RW int loop_count;
//! Background color of the animation in BGRA format.
CV_PROP_RW Scalar bgcolor;
//! Duration for each frame in milliseconds.
CV_PROP_RW std::vector<int> durations;
//! Vector of frames, where each Mat represents a single frame.
CV_PROP_RW std::vector<Mat> frames;
/** @brief Constructs an Animation object with optional loop count and background color.
@param loopCount An integer representing the number of times the animation should loop:
- `0` (default) indicates infinite looping, meaning the animation will replay continuously.
- Positive values denote finite repeat counts, allowing the animation to play a limited number of times.
- If a negative value or a value beyond the maximum of `0xffff` (65535) is provided, it is reset to `0`
(infinite looping) to maintain valid bounds.
@param bgColor A `Scalar` object representing the background color in BGRA format:
- Defaults to `Scalar()`, indicating an empty color (usually transparent if supported).
- This background color provides a solid fill behind frames that have transparency, ensuring a consistent display appearance.
*/
Animation(int loopCount = 0, Scalar bgColor = Scalar());
};
/** @brief Loads an image from a file.
@anchor imread
@@ -323,6 +353,38 @@ The function imreadmulti loads a specified range from a multi-page image from th
*/
CV_EXPORTS_W bool imreadmulti(const String& filename, CV_OUT std::vector<Mat>& mats, int start, int count, int flags = IMREAD_ANYCOLOR);
/** @example samples/cpp/tutorial_code/imgcodecs/animations.cpp
An example to show usage of cv::imreadanimation and cv::imwriteanimation functions.
Check @ref tutorial_animations "the corresponding tutorial" for more details
*/
/** @brief Loads frames from an animated image file into an Animation structure.
The function imreadanimation loads frames from an animated image file (e.g., GIF, AVIF, APNG, WEBP) into the provided Animation struct.
@param filename A string containing the path to the file.
@param animation A reference to an Animation structure where the loaded frames will be stored. It should be initialized before the function is called.
@param start The index of the first frame to load. This is optional and defaults to 0.
@param count The number of frames to load. This is optional and defaults to 32767.
@return Returns true if the file was successfully loaded and frames were extracted; returns false otherwise.
*/
CV_EXPORTS_W bool imreadanimation(const String& filename, CV_OUT Animation& animation, int start = 0, int count = INT16_MAX);
/** @brief Saves an Animation to a specified file.
The function imwriteanimation saves the provided Animation data to the specified file in an animated format.
Supported formats depend on the implementation and may include formats like GIF, AVIF, APNG, or WEBP.
@param filename The name of the file where the animation will be saved. The file extension determines the format.
@param animation A constant reference to an Animation struct containing the frames and metadata to be saved.
@param params Optional format-specific parameters encoded as pairs (paramId_1, paramValue_1, paramId_2, paramValue_2, ...).
These parameters are used to specify additional options for the encoding process. Refer to `cv::ImwriteFlags` for details on possible parameters.
@return Returns true if the animation was successfully saved; returns false otherwise.
*/
CV_EXPORTS_W bool imwriteanimation(const String& filename, const Animation& animation, const std::vector<int>& params = std::vector<int>());
/** @brief Returns the number of images inside the given file
The function imcount returns the number of pages in a multi-page image (e.g. TIFF), the number of frames in an animation (e.g. AVIF), and 1 otherwise.

View File

@@ -11,6 +11,7 @@
#include <memory>
#include <opencv2/core/utils/configuration.private.hpp>
#include <opencv2/core/utils/logger.hpp>
#include "opencv2/imgproc.hpp"
#include "grfmt_avif.hpp"
@@ -242,6 +243,8 @@ bool AvifDecoder::readData(Mat &img) {
return false;
}
m_animation.durations.push_back(decoder_->imageTiming.durationInTimescales);
if (decoder_->image->exif.size > 0) {
m_exif.parseExif(decoder_->image->exif.data, decoder_->image->exif.size);
}
@@ -297,16 +300,26 @@ bool AvifEncoder::isFormatSupported(int depth) const {
bool AvifEncoder::write(const Mat &img, const std::vector<int> &params) {
std::vector<Mat> img_vec(1, img);
return writeToOutput(img_vec, params);
return writemulti(img_vec, params);
}
bool AvifEncoder::writemulti(const std::vector<Mat> &img_vec,
const std::vector<int> &params) {
return writeToOutput(img_vec, params);
CV_LOG_INFO(NULL, "Multi page image will be written as animation with 1 second frame duration.");
Animation animation;
animation.frames = img_vec;
for (size_t i = 0; i < animation.frames.size(); i++)
{
animation.durations.push_back(1000);
}
return writeanimation(animation, params);
}
bool AvifEncoder::writeToOutput(const std::vector<Mat> &img_vec,
const std::vector<int> &params) {
bool AvifEncoder::writeanimation(const Animation& animation,
const std::vector<int> &params) {
int bit_depth = 8;
int speed = AVIF_SPEED_FASTEST;
for (size_t i = 0; i < params.size(); i += 2) {
@@ -340,12 +353,12 @@ bool AvifEncoder::writeToOutput(const std::vector<Mat> &img_vec,
#endif
encoder_->speed = speed;
const avifAddImageFlags flag = (img_vec.size() == 1)
const avifAddImageFlags flag = (animation.frames.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) {
for (const cv::Mat &img : animation.frames) {
CV_CheckType(
img.type(),
(bit_depth == 8 && img.depth() == CV_8U) ||
@@ -358,13 +371,15 @@ bool AvifEncoder::writeToOutput(const std::vector<Mat> &img_vec,
images.emplace_back(ConvertToAvif(img, do_lossless, bit_depth));
}
for (const AvifImageUniquePtr &image : images) {
for (size_t i = 0; i < images.size(); i++)
{
OPENCV_AVIF_CHECK_STATUS(
avifEncoderAddImage(encoder_, image.get(), /*durationInTimescale=*/1,
flag),
avifEncoderAddImage(encoder_, images[i].get(), animation.durations[i], flag),
encoder_);
}
encoder_->timescale = 1000;
OPENCV_AVIF_CHECK_STATUS(avifEncoderFinish(encoder_, output.get()), encoder_);
if (m_buf) {

View File

@@ -46,12 +46,11 @@ class AvifEncoder CV_FINAL : public BaseImageEncoder {
bool writemulti(const std::vector<Mat>& img_vec,
const std::vector<int>& params) CV_OVERRIDE;
bool writeanimation(const Animation& animation, 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_;
};

View File

@@ -144,6 +144,11 @@ bool BaseImageEncoder::writemulti(const std::vector<Mat>&, const std::vector<int
return false;
}
bool BaseImageEncoder::writeanimation(const Animation&, const std::vector<int>& )
{
return false;
}
ImageEncoder BaseImageEncoder::newEncoder() const
{
return ImageEncoder();

View File

@@ -133,6 +133,8 @@ public:
*/
virtual bool checkSignature(const String& signature) const;
const Animation& animation() const { return m_animation; };
/**
* @brief Create and return a new instance of the derived image decoder.
* @return A new ImageDecoder object.
@@ -151,6 +153,7 @@ protected:
bool m_use_rgb; ///< Flag indicating whether to decode the image in RGB order.
ExifReader m_exif; ///< Object for reading EXIF metadata from the image.
size_t m_frame_count; ///< Number of frames in the image (for animations and multi-page images).
Animation m_animation;
};
@@ -215,6 +218,8 @@ public:
*/
virtual bool writemulti(const std::vector<Mat>& img_vec, const std::vector<int>& params);
virtual bool writeanimation(const Animation& animation, const std::vector<int>& params);
/**
* @brief Get a description of the image encoder (e.g., the format it supports).
* @return A string describing the encoder.

View File

@@ -44,17 +44,15 @@
#include "precomp.hpp"
#include <webp/decode.h>
#include <webp/encode.h>
#include <stdio.h>
#include <limits.h>
#include "grfmt_webp.hpp"
#include "opencv2/imgproc.hpp"
#include <opencv2/core/utils/logger.hpp>
#include <opencv2/core/utils/configuration.private.hpp>
#include <webp/decode.h>
#include <webp/encode.h>
#include <webp/demux.h>
#include <webp/mux.h>
namespace cv
{
@@ -67,12 +65,18 @@ static const size_t WEBP_HEADER_SIZE = 32;
WebPDecoder::WebPDecoder()
{
m_buf_supported = true;
channels = 0;
fs_size = 0;
m_has_animation = false;
m_previous_timestamp = 0;
}
WebPDecoder::~WebPDecoder() {}
void WebPDecoder::UniquePtrDeleter::operator()(WebPAnimDecoder* decoder) const
{
WebPAnimDecoderDelete(decoder);
}
size_t WebPDecoder::signatureLength() const
{
return WEBP_HEADER_SIZE;
@@ -102,6 +106,11 @@ ImageDecoder WebPDecoder::newDecoder() const
bool WebPDecoder::readHeader()
{
if (m_has_animation)
{
return true;
}
uint8_t header[WEBP_HEADER_SIZE] = { 0 };
if (m_buf.empty())
{
@@ -124,28 +133,49 @@ bool WebPDecoder::readHeader()
}
WebPBitstreamFeatures features;
if (VP8_STATUS_OK == WebPGetFeatures(header, sizeof(header), &features))
if (VP8_STATUS_OK < WebPGetFeatures(header, sizeof(header), &features)) return false;
m_has_animation = features.has_animation == 1;
if (m_has_animation)
{
CV_CheckEQ(features.has_animation, 0, "WebP backend does not support animated webp images");
m_width = features.width;
m_height = features.height;
if (features.has_alpha)
if (m_buf.empty())
{
m_type = CV_8UC4;
channels = 4;
}
else
{
m_type = CV_8UC3;
channels = 3;
fs.seekg(0, std::ios::beg); CV_Assert(fs && "File stream error");
data.create(1, validateToInt(fs_size), CV_8UC1);
fs.read((char*)data.ptr(), fs_size);
CV_Assert(fs && "Can't read file data");
fs.close();
}
return true;
CV_Assert(data.type() == CV_8UC1); CV_Assert(data.rows == 1);
WebPData webp_data;
webp_data.bytes = (const uint8_t*)data.ptr();
webp_data.size = data.total();
WebPAnimDecoderOptions dec_options;
WebPAnimDecoderOptionsInit(&dec_options);
dec_options.color_mode = m_use_rgb ? MODE_RGBA : MODE_BGRA;
anim_decoder.reset(WebPAnimDecoderNew(&webp_data, &dec_options));
CV_Assert(anim_decoder.get() && "Error parsing image");
WebPAnimInfo anim_info;
WebPAnimDecoderGetInfo(anim_decoder.get(), &anim_info);
m_animation.loop_count = anim_info.loop_count;
m_animation.bgcolor[0] = (anim_info.bgcolor >> 24) & 0xFF;
m_animation.bgcolor[1] = (anim_info.bgcolor >> 16) & 0xFF;
m_animation.bgcolor[2] = (anim_info.bgcolor >> 8) & 0xFF;
m_animation.bgcolor[3] = anim_info.bgcolor & 0xFF;
m_frame_count = anim_info.frame_count;
}
m_width = features.width;
m_height = features.height;
m_type = features.has_alpha ? CV_8UC4 : CV_8UC3;
return false;
return true;
}
bool WebPDecoder::readData(Mat &img)
@@ -155,7 +185,7 @@ bool WebPDecoder::readData(Mat &img)
CV_CheckEQ(img.cols, m_width, "");
CV_CheckEQ(img.rows, m_height, "");
if (m_buf.empty())
if (data.empty())
{
fs.seekg(0, std::ios::beg); CV_Assert(fs && "File stream error");
data.create(1, validateToInt(fs_size), CV_8UC1);
@@ -165,70 +195,96 @@ bool WebPDecoder::readData(Mat &img)
}
CV_Assert(data.type() == CV_8UC1); CV_Assert(data.rows == 1);
Mat read_img;
CV_CheckType(img.type(), img.type() == CV_8UC1 || img.type() == CV_8UC3 || img.type() == CV_8UC4, "");
if (img.type() != m_type || img.cols != m_width || img.rows != m_height)
{
Mat read_img;
CV_CheckType(img.type(), img.type() == CV_8UC1 || img.type() == CV_8UC3 || img.type() == CV_8UC4, "");
if (img.type() != m_type)
read_img.create(m_height, m_width, m_type);
}
else
{
read_img = img; // copy header
}
uchar* out_data = read_img.ptr();
size_t out_data_size = read_img.dataend - out_data;
uchar* res_ptr = NULL;
if (m_has_animation)
{
uint8_t* buf;
int timestamp;
WebPAnimDecoderGetNext(anim_decoder.get(), &buf, &timestamp);
Mat tmp(Size(m_width, m_height), CV_8UC4, buf);
if (img.type() == CV_8UC1)
{
read_img.create(m_height, m_width, m_type);
cvtColor(tmp, img, COLOR_BGR2GRAY);
}
else
if (img.type() == CV_8UC3)
{
read_img = img; // copy header
}
uchar* out_data = read_img.ptr();
size_t out_data_size = read_img.dataend - out_data;
uchar *res_ptr = NULL;
if (channels == 3)
{
CV_CheckTypeEQ(read_img.type(), CV_8UC3, "");
if (m_use_rgb)
res_ptr = WebPDecodeRGBInto(data.ptr(), data.total(), out_data,
(int)out_data_size, (int)read_img.step);
else
res_ptr = WebPDecodeBGRInto(data.ptr(), data.total(), out_data,
(int)out_data_size, (int)read_img.step);
}
else if (channels == 4)
{
CV_CheckTypeEQ(read_img.type(), CV_8UC4, "");
if (m_use_rgb)
res_ptr = WebPDecodeRGBAInto(data.ptr(), data.total(), out_data,
(int)out_data_size, (int)read_img.step);
else
res_ptr = WebPDecodeBGRAInto(data.ptr(), data.total(), out_data,
(int)out_data_size, (int)read_img.step);
}
if (res_ptr != out_data)
return false;
if (read_img.data == img.data && img.type() == m_type)
{
// nothing
}
else if (img.type() == CV_8UC1)
{
cvtColor(read_img, img, COLOR_BGR2GRAY);
}
else if (img.type() == CV_8UC3 && m_type == CV_8UC4)
{
cvtColor(read_img, img, COLOR_BGRA2BGR);
}
else if (img.type() == CV_8UC4 && m_type == CV_8UC3)
{
cvtColor(read_img, img, COLOR_BGR2BGRA);
cvtColor(tmp, img, COLOR_BGRA2BGR);
}
else
{
CV_Error(Error::StsInternal, "");
}
tmp.copyTo(img);
m_animation.durations.push_back(timestamp - m_previous_timestamp);
m_previous_timestamp = timestamp;
return true;
}
if (m_type == CV_8UC3)
{
CV_CheckTypeEQ(read_img.type(), CV_8UC3, "");
if (m_use_rgb)
res_ptr = WebPDecodeRGBInto(data.ptr(), data.total(), out_data,
(int)out_data_size, (int)read_img.step);
else
res_ptr = WebPDecodeBGRInto(data.ptr(), data.total(), out_data,
(int)out_data_size, (int)read_img.step);
}
else if (m_type == CV_8UC4)
{
CV_CheckTypeEQ(read_img.type(), CV_8UC4, "");
if (m_use_rgb)
res_ptr = WebPDecodeRGBAInto(data.ptr(), data.total(), out_data,
(int)out_data_size, (int)read_img.step);
else
res_ptr = WebPDecodeBGRAInto(data.ptr(), data.total(), out_data,
(int)out_data_size, (int)read_img.step);
}
if (res_ptr != out_data)
return false;
if (read_img.data == img.data && img.type() == m_type)
{
// nothing
}
else if (img.type() == CV_8UC1)
{
cvtColor(read_img, img, COLOR_BGR2GRAY);
}
else if (img.type() == CV_8UC3 && m_type == CV_8UC4)
{
cvtColor(read_img, img, COLOR_BGRA2BGR);
}
else
{
CV_Error(Error::StsInternal, "");
}
return true;
}
bool WebPDecoder::nextPage()
{
// Prepare the next page, if any.
return WebPAnimDecoderHasMoreFrames(anim_decoder.get()) > 0;
}
WebPEncoder::WebPEncoder()
{
m_description = "WebP files (*.webp)";
@@ -312,23 +368,152 @@ bool WebPEncoder::write(const Mat& img, const std::vector<int>& params)
#endif
CV_Assert(size > 0);
size_t bytes_written = 0;
if (m_buf)
{
m_buf->resize(size);
memcpy(&(*m_buf)[0], out, size);
bytes_written = size;
}
else
{
FILE *fd = fopen(m_filename.c_str(), "wb");
if (fd != NULL)
{
fwrite(out, size, sizeof(uint8_t), fd);
bytes_written = fwrite(out, sizeof(uint8_t), size, fd);
if (size != bytes_written)
{
CV_LOG_ERROR(NULL, cv::format("Only %zu or %zu bytes are written\n",bytes_written, size));
}
fclose(fd); fd = NULL;
}
}
return size > 0;
return (size > 0) && (bytes_written == size);
}
bool WebPEncoder::writemulti(const std::vector<Mat>& img_vec, const std::vector<int>& params)
{
CV_LOG_INFO(NULL, "Multi page image will be written as animation with 1 second frame duration.");
Animation animation;
animation.frames = img_vec;
for (size_t i = 0; i < animation.frames.size(); i++)
{
animation.durations.push_back(1000);
}
return writeanimation(animation, params);
}
bool WebPEncoder::writeanimation(const Animation& animation, const std::vector<int>& params)
{
CV_CheckDepthEQ(animation.frames[0].depth(), CV_8U, "WebP codec supports only 8-bit unsigned images");
int ok = 0;
int timestamp = 0;
const int width = animation.frames[0].cols, height = animation.frames[0].rows;
WebPAnimEncoderOptions anim_config;
WebPConfig config;
WebPPicture pic;
WebPData webp_data;
WebPDataInit(&webp_data);
if (!WebPAnimEncoderOptionsInit(&anim_config) ||
!WebPConfigInit(&config) ||
!WebPPictureInit(&pic)) {
CV_LOG_ERROR(NULL, "Library version mismatch!\n");
WebPDataClear(&webp_data);
return false;
}
int bgvalue = (static_cast<int>(animation.bgcolor[0]) & 0xFF) << 24 |
(static_cast<int>(animation.bgcolor[1]) & 0xFF) << 16 |
(static_cast<int>(animation.bgcolor[2]) & 0xFF) << 8 |
(static_cast<int>(animation.bgcolor[3]) & 0xFF);
anim_config.anim_params.bgcolor = bgvalue;
anim_config.anim_params.loop_count = animation.loop_count;
if (params.size() > 1)
{
if (params[0] == IMWRITE_WEBP_QUALITY)
{
config.lossless = 0;
config.quality = static_cast<float>(params[1]);
if (config.quality < 1.0f)
{
config.quality = 1.0f;
}
if (config.quality >= 100.0f)
{
config.lossless = 1;
}
}
anim_config.minimize_size = 0;
}
std::unique_ptr<WebPAnimEncoder, void (*)(WebPAnimEncoder*)> anim_encoder(
WebPAnimEncoderNew(width, height, &anim_config), WebPAnimEncoderDelete);
pic.width = width;
pic.height = height;
pic.use_argb = 1;
pic.argb_stride = width;
WebPEncode(&config, &pic);
bool is_input_rgba = animation.frames[0].channels() == 4;
Size canvas_size = Size(animation.frames[0].cols,animation.frames[0].rows);
for (size_t i = 0; i < animation.frames.size(); i++)
{
Mat argb;
CV_Assert(canvas_size == Size(animation.frames[i].cols,animation.frames[i].rows));
if (is_input_rgba)
pic.argb = (uint32_t*)animation.frames[i].data;
else
{
cvtColor(animation.frames[i], argb, COLOR_BGR2BGRA);
pic.argb = (uint32_t*)argb.data;
}
ok = WebPAnimEncoderAdd(anim_encoder.get(), &pic, timestamp, &config);
timestamp += animation.durations[i];
}
// add a last fake frame to signal the last duration
ok = ok & WebPAnimEncoderAdd(anim_encoder.get(), NULL, timestamp, NULL);
ok = ok & WebPAnimEncoderAssemble(anim_encoder.get(), &webp_data);
size_t bytes_written = 0;
if (ok)
{
if (m_buf)
{
m_buf->resize(webp_data.size);
memcpy(&(*m_buf)[0], webp_data.bytes, webp_data.size);
bytes_written = webp_data.size;
}
else
{
FILE* fd = fopen(m_filename.c_str(), "wb");
if (fd != NULL)
{
bytes_written = fwrite(webp_data.bytes, sizeof(uint8_t), webp_data.size, fd);
if (webp_data.size != bytes_written)
{
CV_LOG_ERROR(NULL, cv::format("Only %zu or %zu bytes are written\n",bytes_written, webp_data.size));
}
fclose(fd); fd = NULL;
}
}
}
bool status = (ok > 0) && (webp_data.size == bytes_written);
// free resources
WebPDataClear(&webp_data);
return status;
}
}

View File

@@ -49,6 +49,8 @@
#include <fstream>
struct WebPAnimDecoder;
namespace cv
{
@@ -61,6 +63,7 @@ public:
bool readData( Mat& img ) CV_OVERRIDE;
bool readHeader() CV_OVERRIDE;
bool nextPage() CV_OVERRIDE;
size_t signatureLength() const CV_OVERRIDE;
bool checkSignature( const String& signature) const CV_OVERRIDE;
@@ -68,10 +71,16 @@ public:
ImageDecoder newDecoder() const CV_OVERRIDE;
protected:
struct UniquePtrDeleter {
void operator()(WebPAnimDecoder* decoder) const;
};
std::ifstream fs;
size_t fs_size;
Mat data;
int channels;
std::unique_ptr<WebPAnimDecoder, UniquePtrDeleter> anim_decoder;
bool m_has_animation;
int m_previous_timestamp;
};
class WebPEncoder CV_FINAL : public BaseImageEncoder
@@ -81,6 +90,8 @@ public:
~WebPEncoder() 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;
bool writeanimation(const Animation& animation, const std::vector<int>& params) CV_OVERRIDE;
ImageEncoder newEncoder() const CV_OVERRIDE;
};

View File

@@ -689,6 +689,116 @@ bool imreadmulti(const String& filename, std::vector<Mat>& mats, int start, int
return imreadmulti_(filename, flags, mats, start, count);
}
static bool
imreadanimation_(const String& filename, int flags, int start, int count, Animation& animation)
{
bool success = false;
if (start < 0) {
start = 0;
}
if (count < 0) {
count = INT16_MAX;
}
/// Search for the relevant decoder to handle the imagery
ImageDecoder decoder;
decoder = findDecoder(filename);
/// if no decoder was found, return false.
if (!decoder) {
CV_LOG_WARNING(NULL, "Decoder for " << filename << " not found!\n");
return false;
}
/// set the filename in the driver
decoder->setSource(filename);
// read the header to make sure it succeeds
try
{
// read the header to make sure it succeeds
if (!decoder->readHeader())
return false;
}
catch (const cv::Exception& e)
{
CV_LOG_ERROR(NULL, "imreadanimation_('" << filename << "'): can't read header: " << e.what());
return false;
}
catch (...)
{
CV_LOG_ERROR(NULL, "imreadanimation_('" << filename << "'): can't read header: unknown exception");
return false;
}
int current = 0;
int frame_count = (int)decoder->getFrameCount();
count = count + start > frame_count ? frame_count - start : count;
uint64 pixels = (uint64)decoder->width() * (uint64)decoder->height() * (uint64)(count + 4);
if (pixels > CV_IO_MAX_IMAGE_PIXELS) {
CV_LOG_WARNING(NULL, "\nyou are trying to read " << pixels <<
" bytes that exceed CV_IO_MAX_IMAGE_PIXELS.\n");
return false;
}
while (current < start + count)
{
// grab the decoded type
const int type = calcType(decoder->type(), flags);
// established the required input image size
Size size = validateInputImageSize(Size(decoder->width(), decoder->height()));
// read the image data
Mat mat(size.height, size.width, type);
success = false;
try
{
if (decoder->readData(mat))
success = true;
}
catch (const cv::Exception& e)
{
CV_LOG_ERROR(NULL, "imreadanimation_('" << filename << "'): can't read data: " << e.what());
}
catch (...)
{
CV_LOG_ERROR(NULL, "imreadanimation_('" << filename << "'): can't read data: unknown exception");
}
if (!success)
break;
// optionally rotate the data if EXIF' orientation flag says so
if ((flags & IMREAD_IGNORE_ORIENTATION) == 0 && flags != IMREAD_UNCHANGED)
{
ApplyExifOrientation(decoder->getExifTag(ORIENTATION), mat);
}
if (current >= start)
{
animation.durations.push_back(decoder->animation().durations[decoder->animation().durations.size() - 1]);
animation.frames.push_back(mat);
}
if (!decoder->nextPage())
{
break;
}
++current;
}
animation.bgcolor = decoder->animation().bgcolor;
animation.loop_count = decoder->animation().loop_count;
return success;
}
bool imreadanimation(const String& filename, CV_OUT Animation& animation, int start, int count)
{
CV_TRACE_FUNCTION();
return imreadanimation_(filename, IMREAD_UNCHANGED, start, count, animation);
}
static
size_t imcount_(const String& filename, int flags)
{
@@ -823,6 +933,55 @@ bool imwrite( const String& filename, InputArray _img,
return imwrite_(filename, img_vec, params, false);
}
static bool imwriteanimation_(const String& filename, const Animation& animation, const std::vector<int>& params)
{
ImageEncoder encoder = findEncoder(filename);
if (!encoder)
CV_Error(Error::StsError, "could not find a writer for the specified extension");
encoder->setDestination(filename);
bool code = false;
try
{
code = encoder->writeanimation(animation, params);
if (!code)
{
FILE* f = fopen(filename.c_str(), "wb");
if (!f)
{
if (errno == EACCES)
{
CV_LOG_ERROR(NULL, "imwriteanimation_('" << filename << "'): can't open file for writing: permission denied");
}
}
else
{
fclose(f);
remove(filename.c_str());
}
}
}
catch (const cv::Exception& e)
{
CV_LOG_ERROR(NULL, "imwriteanimation_('" << filename << "'): can't write data: " << e.what());
}
catch (...)
{
CV_LOG_ERROR(NULL, "imwriteanimation_('" << filename << "'): can't write data: unknown exception");
}
return code;
}
bool imwriteanimation(const String& filename, const Animation& animation, const std::vector<int>& params)
{
CV_Assert(!animation.frames.empty());
CV_Assert(animation.frames.size() == animation.durations.size());
return imwriteanimation_(filename, animation, params);
}
static bool
imdecode_( const Mat& buf, int flags, Mat& mat )
{
@@ -1468,6 +1627,13 @@ ImageCollection::iterator ImageCollection::iterator::operator++(int) {
return tmp;
}
Animation::Animation(int loopCount, Scalar bgColor)
: loop_count(loopCount), bgcolor(bgColor)
{
if (loopCount < 0 || loopCount > 0xffff)
this->loop_count = 0; // loop_count should be non-negative
}
}
/* End of file. */

View File

@@ -7,23 +7,9 @@ namespace opencv_test { namespace {
#ifdef HAVE_WEBP
TEST(Imgcodecs_WebP, encode_decode_lossless_webp)
static void readFileBytes(const std::string& fname, std::vector<unsigned char>& buf)
{
const string root = cvtest::TS::ptr()->get_data_path();
string filename = root + "../cv/shared/lena.png";
cv::Mat img = cv::imread(filename);
ASSERT_FALSE(img.empty());
string output = cv::tempfile(".webp");
EXPECT_NO_THROW(cv::imwrite(output, img)); // lossless
cv::Mat img_webp = cv::imread(output);
std::vector<unsigned char> buf;
FILE * wfile = NULL;
wfile = fopen(output.c_str(), "rb");
FILE * wfile = fopen(fname.c_str(), "rb");
if (wfile != NULL)
{
fseek(wfile, 0, SEEK_END);
@@ -39,12 +25,24 @@ TEST(Imgcodecs_WebP, encode_decode_lossless_webp)
fclose(wfile);
}
if (data_size != wfile_size)
{
EXPECT_TRUE(false);
}
EXPECT_EQ(data_size, wfile_size);
}
}
TEST(Imgcodecs_WebP, encode_decode_lossless_webp)
{
const string root = cvtest::TS::ptr()->get_data_path();
string filename = root + "../cv/shared/lena.png";
cv::Mat img = cv::imread(filename);
ASSERT_FALSE(img.empty());
string output = cv::tempfile(".webp");
EXPECT_NO_THROW(cv::imwrite(output, img)); // lossless
cv::Mat img_webp = cv::imread(output);
std::vector<unsigned char> buf;
readFileBytes(output, buf);
EXPECT_EQ(0, remove(output.c_str()));
cv::Mat decode = cv::imdecode(buf, IMREAD_COLOR);
@@ -115,6 +113,234 @@ TEST(Imgcodecs_WebP, encode_decode_with_alpha_webp)
EXPECT_EQ(512, img_webp_bgr.rows);
}
TEST(Imgcodecs_WebP, load_save_animation_rgba)
{
RNG rng = theRNG();
// Set the path to the test image directory and filename for loading.
const string root = cvtest::TS::ptr()->get_data_path();
const string filename = root + "pngsuite/tp1n3p08.png";
// Create an Animation object using the default constructor.
// This initializes the loop count to 0 (infinite looping), background color to 0 (transparent)
Animation l_animation;
// Create an Animation object with custom parameters.
int loop_count = 0xffff; // 0xffff is the maximum value to set.
Scalar bgcolor(125, 126, 127, 128); // different values for test purpose.
Animation s_animation(loop_count, bgcolor);
// Load the image file with alpha channel (IMREAD_UNCHANGED).
Mat image = imread(filename, IMREAD_UNCHANGED);
ASSERT_FALSE(image.empty()) << "Failed to load image: " << filename;
// Add the first frame with a duration value of 500 milliseconds.
int duration = 100;
s_animation.durations.push_back(duration * 5);
s_animation.frames.push_back(image.clone()); // Store the first frame.
putText(s_animation.frames[0], "0", Point(5, 28), FONT_HERSHEY_SIMPLEX, .5, Scalar(100, 255, 0, 255), 2);
// Define a region of interest (ROI) in the loaded image for manipulation.
Mat roi = image(Rect(0, 16, 32, 16)); // Select a subregion of the image.
// Modify the ROI in 13 iterations to simulate slight changes in animation frames.
for (int i = 1; i < 14; i++)
{
for (int x = 0; x < roi.rows; x++)
for (int y = 0; y < roi.cols; y++)
{
// Apply random changes to pixel values to create animation variations.
Vec4b& pixel = roi.at<Vec4b>(x, y);
if (pixel[3] > 0)
{
if (pixel[0] > 10) pixel[0] -= (uchar)rng.uniform(3, 10); // Reduce blue channel.
if (pixel[1] > 10) pixel[1] -= (uchar)rng.uniform(3, 10); // Reduce green channel.
if (pixel[2] > 10) pixel[2] -= (uchar)rng.uniform(3, 10); // Reduce red channel.
pixel[3] -= (uchar)rng.uniform(2, 5); // Reduce alpha channel.
}
}
// Update the duration and add the modified frame to the animation.
duration += rng.uniform(2, 10); // Increase duration with random value (to be sure different duration values saved correctly).
s_animation.frames.push_back(image.clone());
putText(s_animation.frames[i], format("%d", i), Point(5, 28), FONT_HERSHEY_SIMPLEX, .5, Scalar(100, 255, 0, 255), 2);
s_animation.durations.push_back(duration);
}
// Add two identical frames with the same duration.
s_animation.durations.push_back(duration);
s_animation.frames.push_back(s_animation.frames[13].clone());
s_animation.durations.push_back(duration);
s_animation.frames.push_back(s_animation.frames[13].clone());
// Create a temporary output filename for saving the animation.
string output = cv::tempfile(".webp");
// Write the animation to a .webp file and verify success.
EXPECT_TRUE(imwriteanimation(output, s_animation));
imwriteanimation("output.webp", s_animation);
// Read the animation back and compare with the original.
EXPECT_TRUE(imreadanimation(output, l_animation));
// Since the last frames are identical, WebP optimizes by storing only one of them,
// and the duration value for the last frame is handled by libwebp.
size_t expected_frame_count = s_animation.frames.size() - 2;
// Verify that the number of frames matches the expected count.
EXPECT_EQ(imcount(output), expected_frame_count);
EXPECT_EQ(l_animation.frames.size(), expected_frame_count);
// Check that the background color and loop count match between saved and loaded animations.
EXPECT_EQ(l_animation.bgcolor, s_animation.bgcolor); // written as BGRA order
EXPECT_EQ(l_animation.loop_count, s_animation.loop_count);
// Verify that the durations of frames match.
for (size_t i = 0; i < l_animation.frames.size() - 1; i++)
EXPECT_EQ(s_animation.durations[i], l_animation.durations[i]);
EXPECT_TRUE(imreadanimation(output, l_animation, 5, 3));
EXPECT_EQ(l_animation.frames.size(), expected_frame_count + 3);
EXPECT_EQ(l_animation.frames.size(), l_animation.durations.size());
EXPECT_EQ(0, cvtest::norm(l_animation.frames[5], l_animation.frames[14], NORM_INF));
EXPECT_EQ(0, cvtest::norm(l_animation.frames[6], l_animation.frames[15], NORM_INF));
EXPECT_EQ(0, cvtest::norm(l_animation.frames[7], l_animation.frames[16], NORM_INF));
// Verify whether the imread function successfully loads the first frame
Mat frame = imread(output, IMREAD_UNCHANGED);
EXPECT_EQ(0, cvtest::norm(l_animation.frames[0], frame, NORM_INF));
std::vector<uchar> buf;
readFileBytes(output, buf);
vector<Mat> webp_frames;
EXPECT_TRUE(imdecodemulti(buf, IMREAD_UNCHANGED, webp_frames));
EXPECT_EQ(expected_frame_count, webp_frames.size());
webp_frames.clear();
// Test saving the animation frames as individual still images.
EXPECT_TRUE(imwrite(output, s_animation.frames));
// Read back the still images into a vector of Mats.
EXPECT_TRUE(imreadmulti(output, webp_frames));
// Expect all frames written as multi-page image
expected_frame_count = 14;
EXPECT_EQ(expected_frame_count, webp_frames.size());
// Test encoding and decoding the images in memory (without saving to disk).
webp_frames.clear();
EXPECT_TRUE(imencode(".webp", s_animation.frames, buf));
EXPECT_TRUE(imdecodemulti(buf, IMREAD_UNCHANGED, webp_frames));
EXPECT_EQ(expected_frame_count, webp_frames.size());
// Clean up by removing the temporary file.
EXPECT_EQ(0, remove(output.c_str()));
}
TEST(Imgcodecs_WebP, load_save_animation_rgb)
{
RNG rng = theRNG();
// Set the path to the test image directory and filename for loading.
const string root = cvtest::TS::ptr()->get_data_path();
const string filename = root + "pngsuite/tp1n3p08.png";
// Create an Animation object using the default constructor.
// This initializes the loop count to 0 (infinite looping), background color to 0 (transparent)
Animation l_animation;
// Create an Animation object with custom parameters.
int loop_count = 0xffff; // 0xffff is the maximum value to set.
Scalar bgcolor(125, 126, 127, 128); // different values for test purpose.
Animation s_animation(loop_count, bgcolor);
// Load the image file without alpha channel
Mat image = imread(filename);
ASSERT_FALSE(image.empty()) << "Failed to load image: " << filename;
// Add the first frame with a duration value of 500 milliseconds.
int duration = 100;
s_animation.durations.push_back(duration * 5);
s_animation.frames.push_back(image.clone()); // Store the first frame.
putText(s_animation.frames[0], "0", Point(5, 28), FONT_HERSHEY_SIMPLEX, .5, Scalar(100, 255, 0, 255), 2);
// Define a region of interest (ROI) in the loaded image for manipulation.
Mat roi = image(Rect(0, 16, 32, 16)); // Select a subregion of the image.
// Modify the ROI in 13 iterations to simulate slight changes in animation frames.
for (int i = 1; i < 14; i++)
{
for (int x = 0; x < roi.rows; x++)
for (int y = 0; y < roi.cols; y++)
{
// Apply random changes to pixel values to create animation variations.
Vec3b& pixel = roi.at<Vec3b>(x, y);
if (pixel[0] > 50) pixel[0] -= (uchar)rng.uniform(3, 10); // Reduce blue channel.
if (pixel[1] > 50) pixel[1] -= (uchar)rng.uniform(3, 10); // Reduce green channel.
if (pixel[2] > 50) pixel[2] -= (uchar)rng.uniform(3, 10); // Reduce red channel.
}
// Update the duration and add the modified frame to the animation.
duration += rng.uniform(2, 10); // Increase duration with random value (to be sure different duration values saved correctly).
s_animation.frames.push_back(image.clone());
putText(s_animation.frames[i], format("%d", i), Point(5, 28), FONT_HERSHEY_SIMPLEX, .5, Scalar(100, 255, 0, 255), 2);
s_animation.durations.push_back(duration);
}
// Add two identical frames with the same duration.
s_animation.durations.push_back(duration);
s_animation.frames.push_back(s_animation.frames[13].clone());
s_animation.durations.push_back(duration);
s_animation.frames.push_back(s_animation.frames[13].clone());
// Create a temporary output filename for saving the animation.
string output = cv::tempfile(".webp");
// Write the animation to a .webp file and verify success.
EXPECT_EQ(true, imwriteanimation(output, s_animation));
// Read the animation back and compare with the original.
EXPECT_EQ(true, imreadanimation(output, l_animation));
// Since the last frames are identical, WebP optimizes by storing only one of them,
// and the duration value for the last frame is handled by libwebp.
size_t expected_frame_count = s_animation.frames.size() - 2;
// Verify that the number of frames matches the expected count.
EXPECT_EQ(imcount(output), expected_frame_count);
EXPECT_EQ(l_animation.frames.size(), expected_frame_count);
// Check that the background color and loop count match between saved and loaded animations.
EXPECT_EQ(l_animation.bgcolor, s_animation.bgcolor); // written as BGRA order
EXPECT_EQ(l_animation.loop_count, s_animation.loop_count);
// Verify that the durations of frames match.
for (size_t i = 0; i < l_animation.frames.size() - 1; i++)
EXPECT_EQ(s_animation.durations[i], l_animation.durations[i]);
EXPECT_EQ(true, imreadanimation(output, l_animation, 5, 3));
EXPECT_EQ(l_animation.frames.size(), expected_frame_count + 3);
EXPECT_EQ(l_animation.frames.size(), l_animation.durations.size());
EXPECT_TRUE(cvtest::norm(l_animation.frames[5], l_animation.frames[14], NORM_INF) == 0);
EXPECT_TRUE(cvtest::norm(l_animation.frames[6], l_animation.frames[15], NORM_INF) == 0);
EXPECT_TRUE(cvtest::norm(l_animation.frames[7], l_animation.frames[16], NORM_INF) == 0);
// Verify whether the imread function successfully loads the first frame
Mat frame = imread(output, IMREAD_COLOR);
EXPECT_TRUE(cvtest::norm(l_animation.frames[0], frame, NORM_INF) == 0);
std::vector<uchar> buf;
readFileBytes(output, buf);
vector<Mat> webp_frames;
EXPECT_TRUE(imdecodemulti(buf, IMREAD_UNCHANGED, webp_frames));
EXPECT_EQ(webp_frames.size(), expected_frame_count);
// Clean up by removing the temporary file.
EXPECT_EQ(0, remove(output.c_str()));
}
#endif // HAVE_WEBP
}} // namespace

View File

@@ -0,0 +1,51 @@
#include <opencv2/highgui.hpp>
#include <opencv2/imgcodecs.hpp>
#include <opencv2/imgproc.hpp>
#include <iostream>
using namespace cv;
int main( int argc, const char** argv )
{
std::string filename = argc > 1 ? argv[1] : "animated_image.webp";
//! [write_animation]
if (argc == 1)
{
Animation animation_to_save;
Mat image(128, 256, CV_8UC4, Scalar(150, 150, 150, 255));
int duration = 200;
for (int i = 0; i < 10; ++i) {
animation_to_save.frames.push_back(image.clone());
putText(animation_to_save.frames[i], format("Frame %d", i), Point(30, 80), FONT_HERSHEY_SIMPLEX, 1.5, Scalar(255, 100, 0, 255), 2);
animation_to_save.durations.push_back(duration);
}
imwriteanimation("animated_image.webp", animation_to_save, { IMWRITE_WEBP_QUALITY, 100 });
}
//! [write_animation]
//! [init_animation]
Animation animation;
//! [init_animation]
//! [read_animation]
bool success = imreadanimation(filename, animation);
if (!success) {
std::cerr << "Failed to load animation frames\n";
return -1;
}
//! [read_animation]
//! [show_animation]
while (true)
for (size_t i = 0; i < animation.frames.size(); ++i) {
imshow("Animation", animation.frames[i]);
int key_code = waitKey(animation.durations[i]); // Delay between frames
if (key_code == 27)
exit(0);
}
//! [show_animation]
return 0;
}

View File

@@ -0,0 +1,52 @@
import cv2 as cv
import numpy as np
def main(filename):
## [write_animation]
if filename == "animated_image.webp":
# Create an Animation instance to save
animation_to_save = cv.Animation()
# Generate a base image with a specific color
image = np.full((128, 256, 4), (150, 150, 150, 255), dtype=np.uint8)
duration = 200
frames = []
durations = []
# Populate frames and durations in the Animation object
for i in range(10):
frame = image.copy()
cv.putText(frame, f"Frame {i}", (30, 80), cv.FONT_HERSHEY_SIMPLEX, 1.5, (255, 100, 0, 255), 2)
frames.append(frame)
durations.append(duration)
animation_to_save.frames = frames
animation_to_save.durations = durations
# Write the animation to file
cv.imwriteanimation(filename, animation_to_save, [cv.IMWRITE_WEBP_QUALITY, 100])
## [write_animation]
## [init_animation]
animation = cv.Animation()
## [init_animation]
## [read_animation]
success, animation = cv.imreadanimation(filename)
if not success:
print("Failed to load animation frames")
return
## [read_animation]
## [show_animation]
while True:
for i, frame in enumerate(animation.frames):
cv.imshow("Animation", frame)
key_code = cv.waitKey(animation.durations[i])
if key_code == 27: # Exit if 'Esc' key is pressed
return
## [show_animation]
if __name__ == "__main__":
import sys
main(sys.argv[1] if len(sys.argv) > 1 else "animated_image.webp")