From a63ede6b1df6041f2cd1246e536f82e256f51220 Mon Sep 17 00:00:00 2001 From: Kumataro Date: Wed, 26 Feb 2025 23:15:41 +0900 Subject: [PATCH] Merge pull request #26930 from Kumataro:fix26924 Imgcodecs: gif: support Disposal Method #26930 Close https://github.com/opencv/opencv/issues/26924 ### 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. - [x] The feature is well documented and sample code can be built with the project CMake --- modules/imgcodecs/src/grfmt_gif.cpp | 136 +++++++++++++++------------- modules/imgcodecs/src/grfmt_gif.hpp | 36 ++++++-- modules/imgcodecs/test/test_gif.cpp | 48 ++++++++++ 3 files changed, 147 insertions(+), 73 deletions(-) diff --git a/modules/imgcodecs/src/grfmt_gif.cpp b/modules/imgcodecs/src/grfmt_gif.cpp index 5e5695b07b..c229a45201 100644 --- a/modules/imgcodecs/src/grfmt_gif.cpp +++ b/modules/imgcodecs/src/grfmt_gif.cpp @@ -24,7 +24,6 @@ GifDecoder::GifDecoder() { hasRead = false; hasTransparentColor = false; transparentColor = 0; - opMode = GRFMT_GIF_Nothing; top = 0, left = 0, width = 0, height = 0; depth = 8; idx = 0; @@ -66,6 +65,8 @@ bool GifDecoder::readHeader() { for (int i = 0; i < 3 * globalColorTableSize; i++) { globalColorTable[i] = (uchar)m_strm.getByte(); } + CV_CheckGE(bgColor, 0, "bgColor should be >= 0"); + CV_CheckLT(bgColor, globalColorTableSize, "bgColor should be < globalColorTableSize"); } // get the frame count @@ -81,7 +82,8 @@ bool GifDecoder::readData(Mat &img) { return true; } - readExtensions(); + const GifDisposeMethod disposalMethod = readExtensions(); + // Image separator CV_Assert(!(m_strm.getByte()^0x2C)); left = m_strm.getWord(); @@ -93,38 +95,50 @@ bool GifDecoder::readData(Mat &img) { imgCodeStream.resize(width * height); Mat img_; - switch (opMode) { - case GifOpMode::GRFMT_GIF_PreviousImage: - if (lastImage.empty()){ - img_ = Mat::zeros(m_height, m_width, CV_8UC4); - } else { - img_ = lastImage; + if (lastImage.empty()) + { + Scalar background(0.0, 0.0, 0.0, 0.0); + if (bgColor < globalColorTableSize) + { + background = Scalar( globalColorTable[bgColor * 3 + 2], // B + globalColorTable[bgColor * 3 + 1], // G + globalColorTable[bgColor * 3 + 0], // R + 0); // A + } + img_ = Mat(m_height, m_width, CV_8UC4, background); + } else { + img_ = lastImage; + } + lastImage.release(); + + Mat restore; + switch(disposalMethod) + { + case GIF_DISPOSE_NA: + case GIF_DISPOSE_NONE: + // Do nothing + break; + case GIF_DISPOSE_RESTORE_BACKGROUND: + if (bgColor < globalColorTableSize) + { + const Scalar background = Scalar( globalColorTable[bgColor * 3 + 2], // B + globalColorTable[bgColor * 3 + 1], // G + globalColorTable[bgColor * 3 + 0], // R + 0); // A + restore = Mat(width, height, CV_8UC4, background); + } + else + { + CV_LOG_WARNING(NULL, cv::format("bgColor(%d) is out of globalColorTableSize(%d)", bgColor, globalColorTableSize)); } break; - case GifOpMode::GRFMT_GIF_Background: - // background color is valid iff global color table exists - CV_Assert(globalColorTableSize > 0); - if (hasTransparentColor && transparentColor == bgColor) { - img_ = Mat(m_height, m_width, CV_8UC4, - Scalar(globalColorTable[bgColor * 3 + 2], - globalColorTable[bgColor * 3 + 1], - globalColorTable[bgColor * 3], 0)); - } else { - img_ = Mat(m_height, m_width, CV_8UC4, - Scalar(globalColorTable[bgColor * 3 + 2], - globalColorTable[bgColor * 3 + 1], - globalColorTable[bgColor * 3], 255)); - } - break; - case GifOpMode::GRFMT_GIF_Nothing: - case GifOpMode::GRFMT_GIF_Cover: - // default value - img_ = Mat::zeros(m_height, m_width, CV_8UC4); + case GIF_DISPOSE_RESTORE_PREVIOUS: + restore = Mat(img_, cv::Rect(left,top,width,height)).clone(); break; default: CV_Assert(false); + break; } - lastImage.release(); auto flags = (uchar)m_strm.getByte(); if (flags & 0x80) { @@ -189,6 +203,13 @@ bool GifDecoder::readData(Mat &img) { // release the memory img_.release(); + // update lastImage to dispose current frame. + if(!restore.empty()) + { + Mat roi = Mat(lastImage, cv::Rect(left,top,width,height)); + restore.copyTo(roi); + } + return hasRead; } @@ -212,8 +233,9 @@ bool GifDecoder::nextPage() { } } -void GifDecoder::readExtensions() { +GifDisposeMethod GifDecoder::readExtensions() { uchar len; + GifDisposeMethod disposalMethod = GifDisposeMethod::GIF_DISPOSE_NA; while (!(m_strm.getByte() ^ 0x21)) { auto extensionType = (uchar)m_strm.getByte(); @@ -221,13 +243,19 @@ void GifDecoder::readExtensions() { // the scope of this extension is the next image or plain text extension if (!(extensionType ^ 0xF9)) { hasTransparentColor = false; - opMode = GifOpMode::GRFMT_GIF_Nothing;// default value len = (uchar)m_strm.getByte(); CV_Assert(len == 4); - auto flags = (uchar)m_strm.getByte(); + const uint8_t packedFields = (uchar)m_strm.getByte(); + + const uint8_t dm = (packedFields >> GIF_DISPOSE_METHOD_SHIFT) & GIF_DISPOSE_METHOD_MASK; + CV_CheckLE(dm, GIF_DISPOSE_MAX, "Unsupported Dispose Method"); + disposalMethod = static_cast(dm); + + const uint8_t transColorFlag = packedFields & GIF_TRANS_COLOR_FLAG_MASK; + CV_CheckLE(transColorFlag, GIF_TRANSPARENT_INDEX_MAX, "Unsupported Transparent Color Flag"); + hasTransparentColor = (transColorFlag == GIF_TRANSPARENT_INDEX_GIVEN); + m_animation.durations.push_back(m_strm.getWord() * 10); // delay time - opMode = (GifOpMode)((flags & 0x1C) >> 2); - hasTransparentColor = flags & 0x01; transparentColor = (uchar)m_strm.getByte(); } @@ -240,6 +268,8 @@ void GifDecoder::readExtensions() { } // roll back to the block identifier m_strm.setPos(m_strm.getPos() - 1); + + return disposalMethod; } void GifDecoder::code2pixel(Mat& img, int start, int k){ @@ -247,23 +277,6 @@ void GifDecoder::code2pixel(Mat& img, int start, int k){ for (int j = 0; j < width; j++) { uchar colorIdx = imgCodeStream[idx++]; if (hasTransparentColor && colorIdx == transparentColor) { - if (opMode != GifOpMode::GRFMT_GIF_PreviousImage) { - if (colorIdx < localColorTableSize) { - img.at(top + i, left + j) = - Vec4b(localColorTable[colorIdx * 3 + 2], // B - localColorTable[colorIdx * 3 + 1], // G - localColorTable[colorIdx * 3], // R - 0); // A - } else if (colorIdx < globalColorTableSize) { - img.at(top + i, left + j) = - Vec4b(globalColorTable[colorIdx * 3 + 2], // B - globalColorTable[colorIdx * 3 + 1], // G - globalColorTable[colorIdx * 3], // R - 0); // A - } else { - img.at(top + i, left + j) = Vec4b(0, 0, 0, 0); - } - } continue; } if (colorIdx < localColorTableSize) { @@ -437,14 +450,10 @@ bool GifDecoder::getFrameCount_() { int len = m_strm.getByte(); while (len) { if (len == 4) { - int packedFields = m_strm.getByte(); - // 3 bit : Reserved - // 3 bit : Disposal Method - // 1 bit : User Input Flag - // 1 bit : Transparent Color Flag - if ( (packedFields & 0x01)== 0x01) { - m_type = CV_8UC4; // Transparent Index is given. - } + const uint8_t packedFields = static_cast(m_strm.getByte()); // Packed Fields + const uint8_t transColorFlag = packedFields & GIF_TRANS_COLOR_FLAG_MASK; + CV_CheckLE(transColorFlag, GIF_TRANSPARENT_INDEX_MAX, "Unsupported Transparent Color Flag"); + m_type = (transColorFlag == GIF_TRANSPARENT_INDEX_GIVEN) ? CV_8UC4 : CV_8UC3; m_strm.skip(2); // Delay Time m_strm.skip(1); // Transparent Color Index } else { @@ -518,7 +527,6 @@ GifEncoder::GifEncoder() { m_height = 0, m_width = 0; width = 0, height = 0, top = 0, left = 0; m_buf_supported = true; - opMode = GRFMT_GIF_Cover; transparentColor = 0; // index of the transparent color, default 0. currently it is a constant number transparentRGB = Vec3b(0, 0, 0); // the transparent color, default black lzwMaxCodeSize = 12; // the maximum code size, default 12. currently it is a constant number @@ -665,11 +673,9 @@ bool GifEncoder::writeFrame(const Mat &img) { strm.putByte(0x21); // extension introducer strm.putByte(0xF9); // graphic control label strm.putByte(0x04); // block size, fixed number - // flag is a packed field, and the first 3 bits are reserved - uchar flag = opMode << 2; - if (criticalTransparency) - flag |= 1; - strm.putByte(flag); + const int gcePackedFields = static_cast(GIF_DISPOSE_RESTORE_PREVIOUS << GIF_DISPOSE_METHOD_SHIFT) | + static_cast(criticalTransparency ? GIF_TRANSPARENT_INDEX_GIVEN : GIF_TRANSPARENT_INDEX_NOT_GIVEN); + strm.putByte(gcePackedFields); strm.putWord(frameDelay); strm.putByte(transparentColor); strm.putByte(0x00); // end of the extension @@ -680,7 +686,7 @@ bool GifEncoder::writeFrame(const Mat &img) { strm.putWord(top); strm.putWord(width); strm.putWord(height); - flag = localColorTableSize > 0 ? 0x80 : 0x00; + uint8_t flag = localColorTableSize > 0 ? 0x80 : 0x00; if (localColorTableSize > 0) { std::vector img_vec(1, img); getColorTable(img_vec, false); diff --git a/modules/imgcodecs/src/grfmt_gif.hpp b/modules/imgcodecs/src/grfmt_gif.hpp index 8552718d00..106cc52186 100644 --- a/modules/imgcodecs/src/grfmt_gif.hpp +++ b/modules/imgcodecs/src/grfmt_gif.hpp @@ -11,14 +11,35 @@ namespace cv { -enum GifOpMode -{ - GRFMT_GIF_Nothing = 0, - GRFMT_GIF_PreviousImage = 1, - GRFMT_GIF_Background = 2, - GRFMT_GIF_Cover = 3 +// See https://www.w3.org/Graphics/GIF/spec-gif89a.txt +// 23. Graphic Control Extension. +// +// Reserved : 3 bits +// Disposal Method : 3 bits +// User Input Flag : 1 bit +// Transparent Color Flag : 1 bit +constexpr int GIF_DISPOSE_METHOD_SHIFT = 2; +constexpr int GIF_DISPOSE_METHOD_MASK = 7; // 0b111 +constexpr int GIF_TRANS_COLOR_FLAG_MASK = 1; // 0b1 + +enum GifDisposeMethod { + GIF_DISPOSE_NA = 0, + GIF_DISPOSE_NONE = 1, + GIF_DISPOSE_RESTORE_BACKGROUND = 2, + GIF_DISPOSE_RESTORE_PREVIOUS = 3, + // 4-7 are reserved/undefined. + + GIF_DISPOSE_MAX = GIF_DISPOSE_RESTORE_PREVIOUS, }; +enum GifTransparentColorFlag { + GIF_TRANSPARENT_INDEX_NOT_GIVEN = 0, + GIF_TRANSPARENT_INDEX_GIVEN = 1, + + GIF_TRANSPARENT_INDEX_MAX = GIF_TRANSPARENT_INDEX_GIVEN, +}; + + ////////////////////////////////////////////////////////////////////// //// GIF Decoder //// ////////////////////////////////////////////////////////////////////// @@ -43,7 +64,6 @@ protected: int depth; int idx; - GifOpMode opMode; bool hasTransparentColor; uchar transparentColor; int top, left, width, height; @@ -66,7 +86,7 @@ protected: std::vector prefix; }; - void readExtensions(); + GifDisposeMethod readExtensions(); void code2pixel(Mat& img, int start, int k); bool lzwDecode(); bool getFrameCount_(); diff --git a/modules/imgcodecs/test/test_gif.cpp b/modules/imgcodecs/test/test_gif.cpp index 50d0be6c3a..a7c5ce8264 100644 --- a/modules/imgcodecs/test/test_gif.cpp +++ b/modules/imgcodecs/test/test_gif.cpp @@ -366,6 +366,54 @@ TEST(Imgcodecs_Gif, encode_IMREAD_GRAYSCALE) { EXPECT_EQ(decoded.channels(), 1); } +// See https://github.com/opencv/opencv/issues/26924 +TEST(Imgcodecs_Gif, decode_disposal_method) +{ + const string root = cvtest::TS::ptr()->get_data_path(); + const string filename = root + "gifsuite/disposalMethod.gif"; + cv::Animation anim; + bool ret = false; + EXPECT_NO_THROW(ret = imreadanimation(filename, anim, cv::IMREAD_UNCHANGED)); + EXPECT_TRUE(ret); + + /* [Detail of this test] + * disposalMethod.gif has 5 frames to draw 8x8 rectangles with each color index, offsets and disposal method. + * frame 1 draws {1, ... ,1} rectangle at (1,1) with DisposalMethod = 0. + * frame 2 draws {2, ... ,2} rectangle at (2,2) with DisposalMethod = 3. + * frame 3 draws {3, ... ,3} rectangle at (3,3) with DisposalMethod = 1. + * frame 4 draws {4, ... ,4} rectangle at (4,4) with DisposalMethod = 2. + * frame 5 draws {5, ... ,5} rectangle at (5,5) with DisposalMethod = 1. + * + * To convenience to test, color[N] in the color table has RGB(32*N, some, some). + * color[0] = RGB(0,0,0) (background color). + * color[1] = RGB(32,0,0) + * color[2] = RGB(64,0,255) + * color[3] = RGB(96,255,0) + * color[4] = RGB(128,128,128) + * color[5] = RGB(160,255,255) + */ + const int refIds[5][6] = + {// { 0, 0, 0, 0, 0, 0} 0 is background color. + { 0, 1, 1, 1, 1, 1}, // 1 is to be not disposed. + { 0, 1, 2, 2, 2, 2}, // 2 is to be restored to previous. + { 0, 1, 1, 3, 3, 3}, // 3 is to be left in place. + { 0, 1, 1, 3, 4, 4}, // 4 is to be restored to the background color. + { 0, 1, 1, 3, 0, 5}, // 5 is to be left in place. + }; + + for(int i = 0 ; i < 5; i++) + { + cv::Mat frame = anim.frames[i]; + EXPECT_FALSE(frame.empty()); + EXPECT_EQ(frame.type(), CV_8UC4); + for(int j = 0; j < 6; j ++ ) + { + const cv::Scalar p = frame.at(j,j); + EXPECT_EQ( p[2], refIds[i][j] * 32 ) << " i = " << i << " j = " << j << " pixels = " << p; + } + } +} + }//opencv_test }//namespace