Merge pull request #27127 from sturkmen72:apng_has_hidden_frame

Changes about when APNG has a hidden frame #27127

closes : #27074

### Pull Request Readiness Checklist

See details at https://github.com/opencv/opencv/wiki/How_to_contribute#making-a-good-pull-request

- [x] I agree to contribute to the project under Apache 2 License.
- [x] To the best of my knowledge, the proposed patch is not based on a code under GPL or another license that is incompatible with OpenCV
- [x] The PR is proposed to the proper branch
- [x] There is a reference to the original bug report and related work
- [x] There is accuracy test, performance test and test data in opencv_extra repository, if applicable
      Patch to opencv_extra has the same branch name.
- [ ] The feature is well documented and sample code can be built with the project CMake
This commit is contained in:
Suleyman TURKMEN 2025-06-21 10:22:10 +03:00 committed by GitHub
parent 3259863924
commit 850b686f8a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 180 additions and 106 deletions

View File

@ -277,6 +277,8 @@ struct CV_EXPORTS_W_SIMPLE Animation
CV_PROP_RW std::vector<int> durations;
//! Vector of frames, where each Mat represents a single frame.
CV_PROP_RW std::vector<Mat> 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.

View File

@ -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.

View File

@ -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<png_bytep>& 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<png_bytep>& 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<uint16_t>((sp[0] * u + dp[0] * v) / al); // Red
dp[1] = static_cast<uint16_t>((sp[1] * u + dp[1] * v) / al); // Green
dp[2] = static_cast<uint16_t>((sp[2] * u + dp[2] * v) / al); // Blue
dp[3] = static_cast<uint16_t>(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<uint64_t>(alpha) * 65535;
uint64_t v = static_cast<uint64_t>(65535 - alpha) * dp[3];
uint64_t al = u + v;
dp[0] = static_cast<uint16_t>((sp[0] * u + dp[0] * v) / al); // Red
dp[1] = static_cast<uint16_t>((sp[1] * u + dp[1] * v) / al); // Green
dp[2] = static_cast<uint16_t>((sp[2] * u + dp[2] * v) / al); // Blue
dp[3] = static_cast<uint16_t>(al / 65535); // Alpha
}
}
}
@ -694,25 +704,24 @@ void PngDecoder::compose_frame(std::vector<png_bytep>& 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<uint8_t>((sp[0] * u + dp[0] * v) / al); // Red
dp[1] = static_cast<uint8_t>((sp[1] * u + dp[1] * v) / al); // Green
dp[2] = static_cast<uint8_t>((sp[2] * u + dp[2] * v) / al); // Blue
dp[3] = static_cast<uint8_t>(al / 255); // Alpha
}
}
}
@ -1483,7 +1492,7 @@ bool PngEncoder::writeanimation(const Animation& animation, const std::vector<in
if (m_isBilevel)
CV_LOG_WARNING(NULL, "IMWRITE_PNG_BILEVEL parameter is not supported yet.");
uint32_t first =0;
uint32_t loops= animation.loop_count;
uint32_t coltype= animation.frames[0].channels() == 1 ? PNG_COLOR_TYPE_GRAY : animation.frames[0].channels() == 3 ? PNG_COLOR_TYPE_RGB : PNG_COLOR_TYPE_RGB_ALPHA;
@ -1568,7 +1577,7 @@ bool PngEncoder::writeanimation(const Animation& animation, const std::vector<in
buf_IHDR[11] = 0;
buf_IHDR[12] = 0;
png_save_uint_32(buf_acTL, num_frames - first);
png_save_uint_32(buf_acTL, num_frames);
png_save_uint_32(buf_acTL + 4, loops);
writeToStreamOrBuffer(header, 8, m_f);
@ -1577,8 +1586,6 @@ bool PngEncoder::writeanimation(const Animation& animation, const std::vector<in
if (num_frames > 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<in
for (j = 0; j < 6; j++)
op[j].valid = 0;
if (!animation.still_image.empty() && num_frames > 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<in
}
/* dispose = previous */
if (i > 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<in
png_save_uint_16(buf_fcTL + 22, frames[i].getDelayDen());
buf_fcTL[24] = dop;
buf_fcTL[25] = bop;
writeChunk(m_f, "fcTL", buf_fcTL, 26);
writeIDATs(m_f, i, zbuf.data(), zsize, idat_size);
writeChunk(m_f, "fcTL", buf_fcTL, 26);
writeIDATs(m_f, animation.still_image.empty() ? i : 1, zbuf.data(), zsize, idat_size);
/* process apng dispose - begin */
if (dop != 2)
@ -1728,7 +1747,7 @@ bool PngEncoder::writeanimation(const Animation& animation, const std::vector<in
deflateRectFin(zbuf.data(), &zsize, bpp, rowbytes, rows.data(), zbuf_size, op_best);
}
if (num_frames > 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);

View File

@ -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;
}

View File

@ -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<unsigned char> 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<ushort>(0, 0));
EXPECT_EQ(19517, img.at<ushort>(0, 0));
img = imread(filename, IMREAD_COLOR | IMREAD_ANYDEPTH);
ASSERT_FALSE(img.empty());