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
This commit is contained in:
Kumataro 2025-02-26 23:15:41 +09:00 committed by GitHub
parent 43551b72d7
commit a63ede6b1d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 147 additions and 73 deletions

View File

@ -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<GifDisposeMethod>(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<Vec4b>(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<Vec4b>(top + i, left + j) =
Vec4b(globalColorTable[colorIdx * 3 + 2], // B
globalColorTable[colorIdx * 3 + 1], // G
globalColorTable[colorIdx * 3], // R
0); // A
} else {
img.at<Vec4b>(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<uint8_t>(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<int>(GIF_DISPOSE_RESTORE_PREVIOUS << GIF_DISPOSE_METHOD_SHIFT) |
static_cast<int>(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<Mat> img_vec(1, img);
getColorTable(img_vec, false);

View File

@ -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.
// <Packed Fields>
// 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<uchar> prefix;
};
void readExtensions();
GifDisposeMethod readExtensions();
void code2pixel(Mat& img, int start, int k);
bool lzwDecode();
bool getFrameCount_();

View File

@ -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<Vec4b>(j,j);
EXPECT_EQ( p[2], refIds[i][j] * 32 ) << " i = " << i << " j = " << j << " pixels = " << p;
}
}
}
}//opencv_test
}//namespace