diff --git a/cmake/OpenCVFindLibsGrfmt.cmake b/cmake/OpenCVFindLibsGrfmt.cmake index 240df36e4d..a034a37431 100644 --- a/cmake/OpenCVFindLibsGrfmt.cmake +++ b/cmake/OpenCVFindLibsGrfmt.cmake @@ -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) --- diff --git a/cmake/OpenCVFindWebP.cmake b/cmake/OpenCVFindWebP.cmake index 24f4f93efe..3af0908cbb 100644 --- a/cmake/OpenCVFindWebP.cmake +++ b/cmake/OpenCVFindWebP.cmake @@ -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() diff --git a/doc/tutorials/app/animations.markdown b/doc/tutorials/app/animations.markdown new file mode 100644 index 0000000000..2f7c5e1cc8 --- /dev/null +++ b/doc/tutorials/app/animations.markdown @@ -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. diff --git a/doc/tutorials/app/table_of_content_app.markdown b/doc/tutorials/app/table_of_content_app.markdown index 867f570078..714fb66b0c 100644 --- a/doc/tutorials/app/table_of_content_app.markdown +++ b/doc/tutorials/app/table_of_content_app.markdown @@ -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 diff --git a/modules/imgcodecs/include/opencv2/imgcodecs.hpp b/modules/imgcodecs/include/opencv2/imgcodecs.hpp index cd38f22325..25273342d2 100644 --- a/modules/imgcodecs/include/opencv2/imgcodecs.hpp +++ b/modules/imgcodecs/include/opencv2/imgcodecs.hpp @@ -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 durations; + //! Vector of frames, where each Mat represents a single frame. + CV_PROP_RW std::vector 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& 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& params = std::vector()); + /** @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. diff --git a/modules/imgcodecs/src/grfmt_avif.cpp b/modules/imgcodecs/src/grfmt_avif.cpp index 4752c6ece4..23cdbb1baa 100644 --- a/modules/imgcodecs/src/grfmt_avif.cpp +++ b/modules/imgcodecs/src/grfmt_avif.cpp @@ -11,6 +11,7 @@ #include #include +#include #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 ¶ms) { std::vector img_vec(1, img); - return writeToOutput(img_vec, params); + return writemulti(img_vec, params); } bool AvifEncoder::writemulti(const std::vector &img_vec, const std::vector ¶ms) { - 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 &img_vec, - const std::vector ¶ms) { +bool AvifEncoder::writeanimation(const Animation& animation, + const std::vector ¶ms) { 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 &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 images; std::vector 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 &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) { diff --git a/modules/imgcodecs/src/grfmt_avif.hpp b/modules/imgcodecs/src/grfmt_avif.hpp index e64357366a..31f9b00a41 100644 --- a/modules/imgcodecs/src/grfmt_avif.hpp +++ b/modules/imgcodecs/src/grfmt_avif.hpp @@ -46,12 +46,11 @@ class AvifEncoder CV_FINAL : public BaseImageEncoder { bool writemulti(const std::vector& img_vec, const std::vector& params) CV_OVERRIDE; + bool writeanimation(const Animation& animation, const std::vector& params) CV_OVERRIDE; ImageEncoder newEncoder() const CV_OVERRIDE; private: - bool writeToOutput(const std::vector& img_vec, - const std::vector& params); avifEncoder* encoder_; }; diff --git a/modules/imgcodecs/src/grfmt_base.cpp b/modules/imgcodecs/src/grfmt_base.cpp index 6ae9d98f24..d01899fb7e 100644 --- a/modules/imgcodecs/src/grfmt_base.cpp +++ b/modules/imgcodecs/src/grfmt_base.cpp @@ -144,6 +144,11 @@ bool BaseImageEncoder::writemulti(const std::vector&, const std::vector& ) +{ + return false; +} + ImageEncoder BaseImageEncoder::newEncoder() const { return ImageEncoder(); diff --git a/modules/imgcodecs/src/grfmt_base.hpp b/modules/imgcodecs/src/grfmt_base.hpp index f6b5ba1b27..a90bd8a3de 100644 --- a/modules/imgcodecs/src/grfmt_base.hpp +++ b/modules/imgcodecs/src/grfmt_base.hpp @@ -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& img_vec, const std::vector& params); + virtual bool writeanimation(const Animation& animation, const std::vector& params); + /** * @brief Get a description of the image encoder (e.g., the format it supports). * @return A string describing the encoder. diff --git a/modules/imgcodecs/src/grfmt_webp.cpp b/modules/imgcodecs/src/grfmt_webp.cpp index ca54effe86..a0597068ac 100644 --- a/modules/imgcodecs/src/grfmt_webp.cpp +++ b/modules/imgcodecs/src/grfmt_webp.cpp @@ -44,17 +44,15 @@ #include "precomp.hpp" -#include -#include - #include #include - #include "grfmt_webp.hpp" - -#include "opencv2/imgproc.hpp" - +#include #include +#include +#include +#include +#include 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, ×tamp); + 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& 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& img_vec, const std::vector& 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& 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(animation.bgcolor[0]) & 0xFF) << 24 | + (static_cast(animation.bgcolor[1]) & 0xFF) << 16 | + (static_cast(animation.bgcolor[2]) & 0xFF) << 8 | + (static_cast(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(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 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; } } diff --git a/modules/imgcodecs/src/grfmt_webp.hpp b/modules/imgcodecs/src/grfmt_webp.hpp index 6d833e0db6..3aba83d4c9 100644 --- a/modules/imgcodecs/src/grfmt_webp.hpp +++ b/modules/imgcodecs/src/grfmt_webp.hpp @@ -49,6 +49,8 @@ #include +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 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& params) CV_OVERRIDE; + bool writemulti(const std::vector& img_vec, const std::vector& params) CV_OVERRIDE; + bool writeanimation(const Animation& animation, const std::vector& params) CV_OVERRIDE; ImageEncoder newEncoder() const CV_OVERRIDE; }; diff --git a/modules/imgcodecs/src/loadsave.cpp b/modules/imgcodecs/src/loadsave.cpp index 25f32a789c..14c8b2c81a 100644 --- a/modules/imgcodecs/src/loadsave.cpp +++ b/modules/imgcodecs/src/loadsave.cpp @@ -689,6 +689,116 @@ bool imreadmulti(const String& filename, std::vector& 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& 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& 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. */ diff --git a/modules/imgcodecs/test/test_webp.cpp b/modules/imgcodecs/test/test_webp.cpp index 3647714039..0e8280c73b 100644 --- a/modules/imgcodecs/test/test_webp.cpp +++ b/modules/imgcodecs/test/test_webp.cpp @@ -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& 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 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 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(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 buf; + readFileBytes(output, buf); + vector 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(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 buf; + readFileBytes(output, buf); + + vector 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 diff --git a/samples/cpp/tutorial_code/imgcodecs/animations.cpp b/samples/cpp/tutorial_code/imgcodecs/animations.cpp new file mode 100644 index 0000000000..2f56a9f6ea --- /dev/null +++ b/samples/cpp/tutorial_code/imgcodecs/animations.cpp @@ -0,0 +1,51 @@ +#include +#include +#include +#include + +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; +} diff --git a/samples/python/tutorial_code/imgcodecs/animations.py b/samples/python/tutorial_code/imgcodecs/animations.py new file mode 100644 index 0000000000..162450eb12 --- /dev/null +++ b/samples/python/tutorial_code/imgcodecs/animations.py @@ -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")