diff --git a/modules/imgcodecs/include/opencv2/imgcodecs.hpp b/modules/imgcodecs/include/opencv2/imgcodecs.hpp index b78b641121..45a776f4d5 100644 --- a/modules/imgcodecs/include/opencv2/imgcodecs.hpp +++ b/modules/imgcodecs/include/opencv2/imgcodecs.hpp @@ -277,6 +277,8 @@ struct CV_EXPORTS_W_SIMPLE Animation CV_PROP_RW std::vector durations; //! Vector of frames, where each Mat represents a single frame. CV_PROP_RW std::vector frames; + //! image that can be used for the format in addition to the animation or if animation is not supported in the reader (like in PNG). + CV_PROP_RW Mat still_image; /** @brief Constructs an Animation object with optional loop count and background color. diff --git a/modules/imgcodecs/src/grfmt_base.hpp b/modules/imgcodecs/src/grfmt_base.hpp index ae5622528c..6d98bd3735 100644 --- a/modules/imgcodecs/src/grfmt_base.hpp +++ b/modules/imgcodecs/src/grfmt_base.hpp @@ -58,6 +58,11 @@ public: */ size_t getFrameCount() const { return m_frame_count; } + /** + * @brief Set the internal m_frame_count variable to 1. + */ + void resetFrameCount() { m_frame_count = 1; } + /** * @brief Get the type of the image (e.g., color format, depth). * @return The type of the image. diff --git a/modules/imgcodecs/src/grfmt_png.cpp b/modules/imgcodecs/src/grfmt_png.cpp index c4b5a2c3a6..3941961b56 100644 --- a/modules/imgcodecs/src/grfmt_png.cpp +++ b/modules/imgcodecs/src/grfmt_png.cpp @@ -156,7 +156,7 @@ bool APNGFrame::setMat(const cv::Mat& src, unsigned delayNum, unsigned delayDen) if (!src.empty()) { - png_uint_32 rowbytes = src.depth() == CV_16U ? src.cols * src.channels() * 2 : src.cols * src.channels(); + png_uint_32 rowbytes = src.cols * (uint32_t)src.elemSize(); _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; @@ -416,14 +416,17 @@ bool PngDecoder::readData( Mat& img ) if (m_frame_no == 0) { + if (m_mat_raw.empty()) + { + if (m_f) + fseek(m_f, -8, SEEK_CUR); + else + m_buf_pos -= 8; + } m_mat_raw = Mat(img.rows, img.cols, m_type); m_mat_next = Mat(img.rows, img.cols, m_type); frameRaw.setMat(m_mat_raw); frameNext.setMat(m_mat_next); - if (m_f) - fseek(m_f, -8, SEEK_CUR); - else - m_buf_pos -= 8; } else m_mat_next.copyTo(mat_cur); @@ -433,9 +436,6 @@ bool PngDecoder::readData( Mat& img ) if (!processing_start((void*)&frameRaw, mat_cur)) return false; - if(setjmp(png_jmpbuf(m_png_ptr))) - return false; - while (true) { id = read_chunk(chunk); @@ -446,53 +446,53 @@ bool PngDecoder::readData( Mat& img ) { if (!m_is_fcTL_loaded) { - m_is_fcTL_loaded = true; - w0 = m_width; - h0 = m_height; - } - - if (processing_finish()) - { - if (dop == 2) - memcpy(frameNext.getPixels(), frameCur.getPixels(), imagesize); - - compose_frame(frameCur.getRows(), frameRaw.getRows(), bop, x0, y0, w0, h0, mat_cur); - if (!delay_den) - delay_den = 100; - m_animation.durations.push_back(cvRound(1000.*delay_num/delay_den)); - - if (mat_cur.channels() == img.channels()) - { - if (mat_cur.depth() == CV_16U && img.depth() == CV_8U) - mat_cur.convertTo(img, CV_8U, 1. / 255); - else - mat_cur.copyTo(img); - } - else - { - Mat mat_cur_scaled; - if (mat_cur.depth() == CV_16U && img.depth() == CV_8U) - mat_cur.convertTo(mat_cur_scaled, CV_8U, 1. / 255); - else - mat_cur_scaled = mat_cur; - - if (img.channels() == 1) - cvtColor(mat_cur_scaled, img, COLOR_BGRA2GRAY); - else if (img.channels() == 3) - cvtColor(mat_cur_scaled, img, COLOR_BGRA2BGR); - } - - if (dop != 2) - { - memcpy(frameNext.getPixels(), frameCur.getPixels(), imagesize); - if (dop == 1) - for (j = 0; j < h0; j++) - memset(frameNext.getRows()[y0 + j] + x0 * img.channels(), 0, w0 * img.channels()); - } + m_mat_raw.copyTo(m_animation.still_image); } else { - return false; + if (processing_finish()) + { + if (dop == 2) + memcpy(frameNext.getPixels(), frameCur.getPixels(), imagesize); + + compose_frame(frameCur.getRows(), frameRaw.getRows(), bop, x0, y0, w0, h0, mat_cur); + if (!delay_den) + delay_den = 100; + m_animation.durations.push_back(cvRound(1000. * delay_num / delay_den)); + + if (mat_cur.channels() == img.channels()) + { + if (mat_cur.depth() == CV_16U && img.depth() == CV_8U) + mat_cur.convertTo(img, CV_8U, 1. / 255); + else + mat_cur.copyTo(img); + } + else + { + Mat mat_cur_scaled; + if (mat_cur.depth() == CV_16U && img.depth() == CV_8U) + mat_cur.convertTo(mat_cur_scaled, CV_8U, 1. / 255); + else + mat_cur_scaled = mat_cur; + + if (img.channels() == 1) + cvtColor(mat_cur_scaled, img, COLOR_BGRA2GRAY); + else if (img.channels() == 3) + cvtColor(mat_cur_scaled, img, COLOR_BGRA2BGR); + } + + if (dop != 2) + { + memcpy(frameNext.getPixels(), frameCur.getPixels(), imagesize); + if (dop == 1) + for (j = 0; j < h0; j++) + memset(frameNext.getRows()[y0 + j] + x0 * img.channels(), 0, w0 * img.channels()); + } + } + else + { + return false; + } } w0 = png_get_uint_32(&chunk.p[12]); @@ -515,7 +515,16 @@ bool PngDecoder::readData( Mat& img ) } memcpy(&m_chunkIHDR.p[8], &chunk.p[12], 8); - return true; + + if (m_is_fcTL_loaded) + return true; + else + { + m_is_fcTL_loaded = true; + ClearPngPtr(); + if (!processing_start((void*)&frameRaw, mat_cur)) + return false; + } } else if (id == id_IDAT) { @@ -650,8 +659,8 @@ void PngDecoder::compose_frame(std::vector& rows_dst, const std::vect const size_t elem_size = img.elemSize(); if (_bop == 0) { // Overwrite mode: copy source row directly to destination - for(uint32_t j = 0; j < h; ++j) { - std::memcpy(rows_dst[j + y] + x * elem_size,rows_src[j], w * elem_size); + for (uint32_t j = 0; j < h; ++j) { + std::memcpy(rows_dst[j + y] + x * elem_size, rows_src[j], w * elem_size); } return; } @@ -665,23 +674,24 @@ void PngDecoder::compose_frame(std::vector& rows_dst, const std::vect // Blending mode for (unsigned int i = 0; i < w; i++, sp += channels, dp += channels) { - if (channels < 4 || sp[3] == 65535) { // Fully opaque in 16-bit (max value) + uint16_t alpha = sp[3]; + + if (channels < 4 || alpha == 65535 || dp[3] == 0) { + // Fully opaque OR destination fully transparent: direct copy memcpy(dp, sp, elem_size); + continue; } - 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, elem_size); - } + + if (alpha != 0) { + // Alpha blending + uint64_t u = static_cast(alpha) * 65535; + uint64_t v = static_cast(65535 - alpha) * dp[3]; + uint64_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 } } } @@ -694,25 +704,24 @@ void PngDecoder::compose_frame(std::vector& rows_dst, const std::vect // Blending mode for (unsigned int i = 0; i < w; i++, sp += channels, dp += channels) { - if (channels < 4 || sp[3] == 255) { - // Fully opaque: copy source pixel directly + uint8_t alpha = sp[3]; + + if (channels < 4 || alpha == 255 || dp[3] == 0) { + // Fully opaque OR destination fully transparent: direct copy memcpy(dp, sp, elem_size); + continue; } - else if (sp[3] != 0) { + + if (alpha != 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, elem_size); - } + uint32_t u = alpha * 255; + uint32_t v = (255 - alpha) * 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 / 255); // Alpha } } } @@ -1483,7 +1492,7 @@ bool PngEncoder::writeanimation(const Animation& animation, const std::vector 1) writeChunk(m_f, "acTL", buf_acTL, 8); - else - first = 0; if (palsize > 0) writeChunk(m_f, "PLTE", (unsigned char*)(&palette), palsize * 3); @@ -1634,19 +1641,31 @@ bool PngEncoder::writeanimation(const Animation& animation, const std::vector 1) + { + CV_Assert(animation.still_image.type() == animation.frames[0].type() && animation.still_image.size() == animation.frames[0].size()); + APNGFrame apngFrame; + Mat tmp; + if (animation.still_image.depth() == CV_16U) + { + animation.still_image.convertTo(tmp, CV_8U, 1.0 / 255); + } + else + tmp = animation.still_image; + + cvtColor(tmp, tmp, COLOR_BGRA2RGBA); + apngFrame.setMat(tmp); + + deflateRectOp(apngFrame.getPixels(), x0, y0, w0, h0, bpp, rowbytes, zbuf_size, 0); + deflateRectFin(zbuf.data(), &zsize, bpp, rowbytes, rows.data(), zbuf_size, 0); + writeIDATs(m_f, 0, zbuf.data(), zsize, idat_size); + } + deflateRectOp(frames[0].getPixels(), x0, y0, w0, h0, bpp, rowbytes, zbuf_size, 0); deflateRectFin(zbuf.data(), &zsize, bpp, rowbytes, rows.data(), zbuf_size, 0); - if (first) - { - writeIDATs(m_f, 0, zbuf.data(), zsize, idat_size); - for (j = 0; j < 6; j++) - op[j].valid = 0; - deflateRectOp(frames[1].getPixels(), x0, y0, w0, h0, bpp, rowbytes, zbuf_size, 0); - deflateRectFin(zbuf.data(), &zsize, bpp, rowbytes, rows.data(), zbuf_size, 0); - } - - for (i = first; i < num_frames - 1; i++) + for (i = 0; i < num_frames - 1; i++) { uint32_t op_min; int op_best; @@ -1673,7 +1692,7 @@ bool PngEncoder::writeanimation(const Animation& animation, const std::vector first) + if (i > 0) getRect(width, height, rest.data(), frames[i + 1].getPixels(), over3.data(), bpp, rowbytes, zbuf_size, has_tcolor, tcolor, 2); op_min = op[0].size; @@ -1699,9 +1718,9 @@ bool PngEncoder::writeanimation(const Animation& animation, const std::vector 1) + if (num_frames > 1 /* don't write fcTL chunk if animation has only one frame */) { png_save_uint_32(buf_fcTL, next_seq_num++); png_save_uint_32(buf_fcTL + 4, w0); diff --git a/modules/imgcodecs/src/loadsave.cpp b/modules/imgcodecs/src/loadsave.cpp index fd547a378a..dfbf118fb9 100644 --- a/modules/imgcodecs/src/loadsave.cpp +++ b/modules/imgcodecs/src/loadsave.cpp @@ -501,11 +501,12 @@ imread_( const String& filename, int flags, OutputArray mat ) Mat real_mat = mat.getMat(); const void * original_ptr = real_mat.data; bool success = false; + decoder->resetFrameCount(); // this is needed for PngDecoder. it should be called before decoder->readData() try { if (decoder->readData(real_mat)) { - CV_CheckTrue((decoder->getFrameCount() > 1) || original_ptr == real_mat.data, "Internal imread issue"); + CV_CheckTrue(original_ptr == real_mat.data, "Internal imread issue"); success = true; } } @@ -800,6 +801,7 @@ imreadanimation_(const String& filename, int flags, int start, int count, Animat } animation.bgcolor = decoder->animation().bgcolor; animation.loop_count = decoder->animation().loop_count; + animation.still_image = decoder->animation().still_image; return success; } @@ -910,6 +912,7 @@ static bool imdecodeanimation_(InputArray buf, int flags, int start, int count, } animation.bgcolor = decoder->animation().bgcolor; animation.loop_count = decoder->animation().loop_count; + animation.still_image = decoder->animation().still_image; return success; } diff --git a/modules/imgcodecs/test/test_animation.cpp b/modules/imgcodecs/test/test_animation.cpp index ece0d19d29..5fead70135 100644 --- a/modules/imgcodecs/test/test_animation.cpp +++ b/modules/imgcodecs/test/test_animation.cpp @@ -636,6 +636,51 @@ TEST(Imgcodecs_APNG, imencode_animation) } } +TEST(Imgcodecs_APNG, animation_has_hidden_frame) +{ + // 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 + "readwrite/033.png"; + Animation animation1, animation2, animation3; + + imreadanimation(filename, animation1); + + EXPECT_FALSE(animation1.still_image.empty()); + EXPECT_EQ((size_t)2, animation1.frames.size()); + + std::vector buf; + EXPECT_TRUE(imencodeanimation(".png", animation1, buf)); + EXPECT_TRUE(imdecodeanimation(buf, animation2)); + + EXPECT_FALSE(animation2.still_image.empty()); + EXPECT_EQ(animation1.frames.size(), animation2.frames.size()); + + animation1.frames.erase(animation1.frames.begin()); + animation1.durations.erase(animation1.durations.begin()); + EXPECT_TRUE(imencodeanimation(".png", animation1, buf)); + EXPECT_TRUE(imdecodeanimation(buf, animation3)); + + EXPECT_FALSE(animation1.still_image.empty()); + EXPECT_TRUE(animation3.still_image.empty()); + EXPECT_EQ((size_t)1, animation3.frames.size()); +} + +TEST(Imgcodecs_APNG, animation_imread_preview) +{ + // 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 + "readwrite/033.png"; + cv::Mat imread_result; + cv::imread(filename, imread_result, cv::IMREAD_UNCHANGED); + EXPECT_FALSE(imread_result.empty()); + + Animation animation; + imreadanimation(filename, animation); + EXPECT_FALSE(animation.still_image.empty()); + + EXPECT_EQ(0, cv::norm(animation.still_image, imread_result, cv::NORM_INF)); +} + #endif // HAVE_PNG #if defined(HAVE_PNG) || defined(HAVE_SPNG) @@ -676,7 +721,7 @@ TEST(Imgcodecs_APNG, imread_animation_16u) img = imread(filename, IMREAD_ANYDEPTH); ASSERT_FALSE(img.empty()); EXPECT_TRUE(img.type() == CV_16UC1); - EXPECT_EQ(19519, img.at(0, 0)); + EXPECT_EQ(19517, img.at(0, 0)); img = imread(filename, IMREAD_COLOR | IMREAD_ANYDEPTH); ASSERT_FALSE(img.empty());