diff --git a/modules/imgcodecs/src/grfmt_avif.cpp b/modules/imgcodecs/src/grfmt_avif.cpp index 58f6ce91c1..d3fb500604 100644 --- a/modules/imgcodecs/src/grfmt_avif.cpp +++ b/modules/imgcodecs/src/grfmt_avif.cpp @@ -303,21 +303,6 @@ bool AvifEncoder::write(const Mat &img, const std::vector ¶ms) { return writemulti(img_vec, params); } -bool AvifEncoder::writemulti(const std::vector &img_vec, - const std::vector ¶ms) { - - 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::writeanimation(const Animation& animation, const std::vector ¶ms) { int bit_depth = 8; diff --git a/modules/imgcodecs/src/grfmt_avif.hpp b/modules/imgcodecs/src/grfmt_avif.hpp index 31f9b00a41..87b765619e 100644 --- a/modules/imgcodecs/src/grfmt_avif.hpp +++ b/modules/imgcodecs/src/grfmt_avif.hpp @@ -41,11 +41,7 @@ class AvifEncoder CV_FINAL : public BaseImageEncoder { ~AvifEncoder() CV_OVERRIDE; bool isFormatSupported(int depth) const CV_OVERRIDE; - bool write(const Mat& img, const std::vector& params) CV_OVERRIDE; - - bool writemulti(const std::vector& img_vec, - const std::vector& params) CV_OVERRIDE; bool writeanimation(const Animation& animation, const std::vector& params) CV_OVERRIDE; ImageEncoder newEncoder() const CV_OVERRIDE; diff --git a/modules/imgcodecs/src/grfmt_base.cpp b/modules/imgcodecs/src/grfmt_base.cpp index d01899fb7e..1e09882780 100644 --- a/modules/imgcodecs/src/grfmt_base.cpp +++ b/modules/imgcodecs/src/grfmt_base.cpp @@ -43,6 +43,7 @@ #include "grfmt_base.hpp" #include "bitstrm.hpp" +#include namespace cv { @@ -139,9 +140,19 @@ bool BaseImageEncoder::setDestination( std::vector& buf ) return true; } -bool BaseImageEncoder::writemulti(const std::vector&, const std::vector& ) +bool BaseImageEncoder::writemulti(const std::vector& img_vec, const std::vector& params) { - return false; + if(img_vec.size() > 1) + 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 BaseImageEncoder::writeanimation(const Animation&, const std::vector& ) diff --git a/modules/imgcodecs/src/grfmt_gif.cpp b/modules/imgcodecs/src/grfmt_gif.cpp index caf8a8fc4f..5a65ae04b1 100644 --- a/modules/imgcodecs/src/grfmt_gif.cpp +++ b/modules/imgcodecs/src/grfmt_gif.cpp @@ -219,7 +219,7 @@ void GifDecoder::readExtensions() { len = (uchar)m_strm.getByte(); CV_Assert(len == 4); auto flags = (uchar)m_strm.getByte(); - m_strm.getWord(); // delay time, not used + m_animation.durations.push_back(m_strm.getWord() * 10); // delay time opMode = (GifOpMode)((flags & 0x1C) >> 2); hasTransparentColor = flags & 0x01; transparentColor = (uchar)m_strm.getByte(); @@ -407,6 +407,10 @@ bool GifDecoder::getFrameCount_() { while (len) { m_strm.skip(len); len = m_strm.getByte(); + if (len == 3 && m_strm.getByte() == 1) + { + m_animation.loop_count = m_strm.getWord(); + } } } else if (!(type ^ 0x2C)) { // skip image data @@ -490,16 +494,11 @@ bool GifEncoder::isFormatSupported(int depth) const { bool GifEncoder::write(const Mat &img, const std::vector ¶ms) { std::vector img_vec(1, img); - return writeFrames(img_vec, params); + return writemulti(img_vec, params); } -bool GifEncoder::writemulti(const std::vector &img_vec, const std::vector ¶ms) { - return writeFrames(img_vec, params); -} - -bool GifEncoder::writeFrames(const std::vector& img_vec, - const std::vector& params) { - if (img_vec.empty()) { +bool GifEncoder::writeanimation(const Animation& animation, const std::vector& params) { + if (animation.frames.empty()) { return false; } @@ -511,6 +510,8 @@ bool GifEncoder::writeFrames(const std::vector& img_vec, return false; } + loopCount = animation.loop_count; + // confirm the params for (size_t i = 0; i < params.size(); i += 2) { switch (params[i]) { @@ -561,13 +562,13 @@ bool GifEncoder::writeFrames(const std::vector& img_vec, if (fast) { const uchar transparent = 0x92; // 1001_0010: the middle of the color table if (dithering == GRFMT_GIF_None) { - img_vec_ = img_vec; + img_vec_ = animation.frames; transparentColor = transparent; } else { localColorTableSize = 0; int transRGB; const int depth = 3 << 8 | 3 << 4 | 2; // r:g:b = 3:3:2 - for (auto &img: img_vec) { + for (auto &img: animation.frames) { Mat img_(img.size(), img.type()); transRGB = ditheringKernel(img, img_, depth, criticalTransparency); if (transRGB >= 0) { @@ -583,13 +584,13 @@ bool GifEncoder::writeFrames(const std::vector& img_vec, } else if (dithering != GRFMT_GIF_None) { int depth = (int)floor(log2(colorNum) / 3) + dithering; depth = depth << 8 | depth << 4 | depth; - for (auto &img : img_vec) { + for (auto &img : animation.frames) { Mat img_(img.size(), img.type()); ditheringKernel(img, img_, depth, criticalTransparency); img_vec_.push_back(img_); } } else { - img_vec_ = img_vec; + img_vec_ = animation.frames; } bool result = writeHeader(img_vec_); if (!result) { @@ -597,8 +598,9 @@ bool GifEncoder::writeFrames(const std::vector& img_vec, return false; } - for (const auto &img : img_vec_) { - result = writeFrame(img); + for (size_t i = 0; i < img_vec_.size(); i++) { + frameDelay = cvRound(animation.durations[i] / 10); + result = writeFrame(img_vec_[i]); } strm.putByte(0x3B); // trailer diff --git a/modules/imgcodecs/src/grfmt_gif.hpp b/modules/imgcodecs/src/grfmt_gif.hpp index 284a9cf945..8f520745ba 100644 --- a/modules/imgcodecs/src/grfmt_gif.hpp +++ b/modules/imgcodecs/src/grfmt_gif.hpp @@ -86,9 +86,7 @@ public: bool isFormatSupported(int depth) const CV_OVERRIDE; bool write(const Mat& img, const std::vector& params) CV_OVERRIDE; - - bool writemulti(const std::vector& img_vec, - const std::vector& params) CV_OVERRIDE; + bool writeanimation(const Animation& animation, const std::vector& params) CV_OVERRIDE; ImageEncoder newEncoder() const CV_OVERRIDE; diff --git a/modules/imgcodecs/src/grfmt_png.cpp b/modules/imgcodecs/src/grfmt_png.cpp index 3236abc637..5604ddcf28 100644 --- a/modules/imgcodecs/src/grfmt_png.cpp +++ b/modules/imgcodecs/src/grfmt_png.cpp @@ -152,8 +152,7 @@ bool APNGFrame::setMat(const cv::Mat& src, unsigned delayNum, unsigned delayDen) if (!src.empty()) { - png_uint_32 rowbytes = src.cols * src.channels(); - + png_uint_32 rowbytes = src.depth() == CV_16U ? src.cols * src.channels() * 2 : src.cols * src.channels(); _width = src.cols; _height = src.rows; _colorType = src.channels() == 1 ? PNG_COLOR_TYPE_GRAY : src.channels() == 3 ? PNG_COLOR_TYPE_RGB : PNG_COLOR_TYPE_RGB_ALPHA; @@ -389,7 +388,7 @@ bool PngDecoder::readData( Mat& img ) { if (m_frame_count > 1) { - Mat mat_cur = Mat(img.rows, img.cols, m_type); + Mat mat_cur = Mat::zeros(img.rows, img.cols, m_type); uint32_t id = 0; uint32_t j = 0; uint32_t imagesize = m_width * m_height * mat_cur.channels(); @@ -437,7 +436,7 @@ bool PngDecoder::readData( Mat& img ) if (dop == 2) memcpy(frameNext.getPixels(), frameCur.getPixels(), imagesize); - compose_frame(frameCur.getRows(), frameRaw.getRows(), bop, x0, y0, w0, h0, mat_cur.channels()); + compose_frame(frameCur.getRows(), frameRaw.getRows(), bop, x0, y0, w0, h0, mat_cur); if (delay_den < 1000) delay_num = cvRound(1000.0 / delay_den); m_animation.durations.push_back(delay_num); @@ -495,7 +494,7 @@ bool PngDecoder::readData( Mat& img ) { if (processing_finish()) { - compose_frame(frameCur.getRows(), frameRaw.getRows(), bop, x0, y0, w0, h0, mat_cur.channels()); + compose_frame(frameCur.getRows(), frameRaw.getRows(), bop, x0, y0, w0, h0, mat_cur); if (delay_den < 1000) delay_num = cvRound(1000.0 / delay_den); m_animation.durations.push_back(delay_num); @@ -606,41 +605,81 @@ bool PngDecoder::nextPage() { return ++m_frame_no < (int)m_frame_count; } -void PngDecoder::compose_frame(std::vector& rows_dst, const std::vector& rows_src, unsigned char _bop, uint32_t x, uint32_t y, uint32_t w, uint32_t h, int channels) +void PngDecoder::compose_frame(std::vector& rows_dst, const std::vector& rows_src, unsigned char _bop, uint32_t x, uint32_t y, uint32_t w, uint32_t h, Mat& img) { - uint32_t i, j; - int u, v, al; + int channels = img.channels(); + if (img.depth() == CV_16U) + cv::parallel_for_(cv::Range(0, h), [&](const cv::Range& range) { + for (int j = range.start; j < range.end; j++) { + uint16_t* sp = reinterpret_cast(rows_src[j]); + uint16_t* dp = reinterpret_cast(rows_dst[j + y]) + x * channels; - for (j = 0; j < h; j++) - { - unsigned char* sp = rows_src[j]; - unsigned char* dp = rows_dst[j + y] + x * channels; - - if (_bop == 0) - memcpy(dp, sp, w * channels); - else - for (i = 0; i < w; i++, sp += 4, dp += 4) - { - if (sp[3] == 255) - memcpy(dp, sp, 4); - else - if (sp[3] != 0) - { - if (dp[3] != 0) - { - u = sp[3] * 255; - v = (255 - sp[3]) * dp[3]; - al = u + v; - dp[0] = (sp[0] * u + dp[0] * v) / al; - dp[1] = (sp[1] * u + dp[1] * v) / al; - dp[2] = (sp[2] * u + dp[2] * v) / al; - dp[3] = al / 255; - } - else - memcpy(dp, sp, 4); - } + if (_bop == 0) { + // Overwrite mode: copy source row directly to destination + memcpy(dp, sp, w * channels * sizeof(uint16_t)); } - } + else { + // Blending mode + for (unsigned int i = 0; i < w; i++, sp += channels, dp += channels) { + if (sp[3] == 65535) { // Fully opaque in 16-bit (max value) + memcpy(dp, sp, channels * sizeof(uint16_t)); + } + else if (sp[3] != 0) { // Partially transparent + if (dp[3] != 0) { // Both source and destination have alpha + uint32_t u = sp[3] * 65535; // 16-bit max + uint32_t v = (65535 - sp[3]) * dp[3]; + uint32_t al = u + v; + dp[0] = static_cast((sp[0] * u + dp[0] * v) / al); // Red + dp[1] = static_cast((sp[1] * u + dp[1] * v) / al); // Green + dp[2] = static_cast((sp[2] * u + dp[2] * v) / al); // Blue + dp[3] = static_cast(al / 65535); // Alpha + } + else { + // If destination alpha is 0, copy source pixel + memcpy(dp, sp, channels * sizeof(uint16_t)); + } + } + } + } + } + }); + else + cv::parallel_for_(cv::Range(0, h), [&](const cv::Range& range) { + for (int j = range.start; j < range.end; j++) { + unsigned char* sp = rows_src[j]; + unsigned char* dp = rows_dst[j + y] + x * channels; + + if (_bop == 0) { + // Overwrite mode: copy source row directly to destination + memcpy(dp, sp, w * channels); + } + else { + // Blending mode + for (unsigned int i = 0; i < w; i++, sp += channels, dp += channels) { + if (sp[3] == 255) { + // Fully opaque: copy source pixel directly + memcpy(dp, sp, channels); + } + else if (sp[3] != 0) { + // Alpha blending + if (dp[3] != 0) { + int u = sp[3] * 255; + int v = (255 - sp[3]) * dp[3]; + int al = u + v; + dp[0] = (sp[0] * u + dp[0] * v) / al; // Red + dp[1] = (sp[1] * u + dp[1] * v) / al; // Green + dp[2] = (sp[2] * u + dp[2] * v) / al; // Blue + dp[3] = al / 255; // Alpha + } + else { + // If destination alpha is 0, copy source pixel + memcpy(dp, sp, channels); + } + } + } + } + } + }); } size_t PngDecoder::read_from_io(void* _Buffer, size_t _ElementSize, size_t _ElementCount) @@ -742,7 +781,6 @@ bool PngDecoder::processing_finish() void PngDecoder::info_fn(png_structp png_ptr, png_infop info_ptr) { png_set_expand(png_ptr); - png_set_strip_16(png_ptr); (void)png_set_interlace_handling(png_ptr); png_read_update_info(png_ptr, info_ptr); } @@ -1352,21 +1390,6 @@ void PngEncoder::deflateRectFin(unsigned char* zbuf, uint32_t* zsize, int bpp, i deflateEnd(&fin_zstream); } -bool PngEncoder::writemulti(const std::vector& img_vec, const std::vector& params) -{ - CV_Assert(img_vec[0].depth() == CV_8U); - 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 PngEncoder::writeanimation(const Animation& animation, const std::vector& params) { int compression_level = 6; @@ -1433,6 +1456,8 @@ bool PngEncoder::writeanimation(const Animation& animation, const std::vector 0 && !getRect(width, height, frames.back().getPixels(), apngFrame.getPixels(), over1.data(), bpp, rowbytes, 0, 0, 0, 3)) diff --git a/modules/imgcodecs/src/grfmt_png.hpp b/modules/imgcodecs/src/grfmt_png.hpp index 4ba22d69d7..fa2642b8f3 100644 --- a/modules/imgcodecs/src/grfmt_png.hpp +++ b/modules/imgcodecs/src/grfmt_png.hpp @@ -136,7 +136,7 @@ protected: static void row_fn(png_structp png_ptr, png_bytep new_row, png_uint_32 row_num, int pass); bool processing_start(void* frame_ptr, const Mat& img); bool processing_finish(); - void compose_frame(std::vector& rows_dst, const std::vector& rows_src, unsigned char bop, uint32_t x, uint32_t y, uint32_t w, uint32_t h, int channels); + void compose_frame(std::vector& rows_dst, const std::vector& rows_src, unsigned char bop, uint32_t x, uint32_t y, uint32_t w, uint32_t h, Mat& img); size_t read_from_io(void* _Buffer, size_t _ElementSize, size_t _ElementCount); uint32_t read_chunk(Chunk& chunk); @@ -176,7 +176,6 @@ public: bool isFormatSupported( int depth ) const CV_OVERRIDE; bool write( const Mat& img, const std::vector& params ) CV_OVERRIDE; - bool writemulti(const std::vector& img_vec, const std::vector& params) CV_OVERRIDE; bool writeanimation(const Animation& animinfo, const std::vector& params) CV_OVERRIDE; ImageEncoder newEncoder() const CV_OVERRIDE; diff --git a/modules/imgcodecs/src/grfmt_webp.cpp b/modules/imgcodecs/src/grfmt_webp.cpp index a0597068ac..2d55995789 100644 --- a/modules/imgcodecs/src/grfmt_webp.cpp +++ b/modules/imgcodecs/src/grfmt_webp.cpp @@ -392,20 +392,6 @@ bool WebPEncoder::write(const Mat& img, const std::vector& params) 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"); diff --git a/modules/imgcodecs/src/grfmt_webp.hpp b/modules/imgcodecs/src/grfmt_webp.hpp index 3aba83d4c9..f9ceb5e524 100644 --- a/modules/imgcodecs/src/grfmt_webp.hpp +++ b/modules/imgcodecs/src/grfmt_webp.hpp @@ -90,7 +90,6 @@ 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/test/test_animation.cpp b/modules/imgcodecs/test/test_animation.cpp index fcfce4b6ab..e8c42cbcc0 100644 --- a/modules/imgcodecs/test/test_animation.cpp +++ b/modules/imgcodecs/test/test_animation.cpp @@ -99,6 +99,61 @@ static bool fillFrames(Animation& animation, bool hasAlpha, int n = 14) return true; } +#ifdef HAVE_IMGCODEC_GIF + +TEST(Imgcodecs_Gif, imwriteanimation_rgba) +{ + Animation s_animation, l_animation; + EXPECT_TRUE(fillFrames(s_animation, true)); + s_animation.bgcolor = Scalar(0, 0, 0, 0); // TO DO not implemented yet. + + // Create a temporary output filename for saving the animation. + string output = cv::tempfile(".gif"); + + // Write the animation to a .webp file and verify success. + EXPECT_TRUE(imwriteanimation(output, s_animation)); + + // Read the animation back and compare with the original. + EXPECT_TRUE(imreadanimation(output, l_animation)); + + size_t expected_frame_count = s_animation.frames.size(); + + // Verify that the number of frames matches the expected count. + EXPECT_EQ(expected_frame_count, imcount(output)); + EXPECT_EQ(expected_frame_count, l_animation.frames.size()); + + // 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(cvRound(s_animation.durations[i] / 10), cvRound(l_animation.durations[i] / 10)); + + EXPECT_TRUE(imreadanimation(output, l_animation, 5, 3)); + EXPECT_EQ(expected_frame_count + 3, l_animation.frames.size()); + EXPECT_EQ(l_animation.frames.size(), l_animation.durations.size()); + EXPECT_EQ(0, cvtest::norm(l_animation.frames[5], l_animation.frames[16], NORM_INF)); + EXPECT_EQ(0, cvtest::norm(l_animation.frames[6], l_animation.frames[17], NORM_INF)); + EXPECT_EQ(0, cvtest::norm(l_animation.frames[7], l_animation.frames[18], 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()); + + // Clean up by removing the temporary file. + EXPECT_EQ(0, remove(output.c_str())); +} + +#endif // HAVE_IMGCODEC_GIF + #ifdef HAVE_WEBP TEST(Imgcodecs_WebP, imwriteanimation_rgba) @@ -305,6 +360,51 @@ TEST(Imgcodecs_APNG, imwriteanimation_rgba) EXPECT_EQ(0, remove(output.c_str())); } +TEST(Imgcodecs_APNG, imwriteanimation_rgba16u) +{ + Animation s_animation, l_animation; + EXPECT_TRUE(fillFrames(s_animation, true)); + + for (size_t i = 0; i < s_animation.frames.size(); i++) + { + s_animation.frames[i].convertTo(s_animation.frames[i], CV_16U, 255); + } + // Create a temporary output filename for saving the animation. + string output = cv::tempfile(".png"); + + // Write the animation to a .png file and verify success. + EXPECT_TRUE(imwriteanimation(output, s_animation)); + + // Read the animation back and compare with the original. + EXPECT_TRUE(imreadanimation(output, l_animation)); + + size_t expected_frame_count = s_animation.frames.size() - 2; + + // Verify that the number of frames matches the expected count. + EXPECT_EQ(expected_frame_count, imcount(output)); + EXPECT_EQ(expected_frame_count, l_animation.frames.size()); + + std::vector buf; + readFileBytes(output, buf); + vector apng_frames; + + EXPECT_TRUE(imdecodemulti(buf, IMREAD_UNCHANGED, apng_frames)); + EXPECT_EQ(expected_frame_count, apng_frames.size()); + + apng_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, apng_frames)); + + // Expect all frames written as multi-page image + EXPECT_EQ(expected_frame_count, apng_frames.size()); + + // Clean up by removing the temporary file. + EXPECT_EQ(0, remove(output.c_str())); +} + TEST(Imgcodecs_APNG, imwriteanimation_rgb) { Animation s_animation, l_animation;