Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 3e43d0c

Browse filesBrowse files
authored
Merge pull request opencv#26971 from Kumataro:fix26970
imgcodecs: gif: support animated gif without loop opencv#26971 Close opencv#26970 ### 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
1 parent 8207549 commit 3e43d0c
Copy full SHA for 3e43d0c

File tree

Expand file treeCollapse file tree

4 files changed

+173
-37
lines changed
Filter options
Expand file treeCollapse file tree

4 files changed

+173
-37
lines changed

‎modules/imgcodecs/include/opencv2/imgcodecs.hpp

Copy file name to clipboardExpand all lines: modules/imgcodecs/include/opencv2/imgcodecs.hpp
+12-2Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,8 @@ enum ImwriteFlags {
118118
IMWRITE_JPEGXL_EFFORT = 641,//!< For JPEG XL, encoder effort/speed level without affecting decoding speed; it is between 1 (fastest) and 10 (slowest). Default is 7.
119119
IMWRITE_JPEGXL_DISTANCE = 642,//!< For JPEG XL, distance level for lossy compression: target max butteraugli distance, lower = higher quality, 0 = lossless; range: 0 .. 25. Default is 1.
120120
IMWRITE_JPEGXL_DECODING_SPEED = 643,//!< For JPEG XL, decoding speed tier for the provided options; minimum is 0 (slowest to decode, best quality/density), and maximum is 4 (fastest to decode, at the cost of some quality/density). Default is 0.
121-
IMWRITE_GIF_LOOP = 1024,//!< For GIF, it can be a loop flag from 0 to 65535. Default is 0 - loop forever.
122-
IMWRITE_GIF_SPEED = 1025,//!< For GIF, it is between 1 (slowest) and 100 (fastest). Default is 96.
121+
IMWRITE_GIF_LOOP = 1024, //!< Not functional since 4.12.0. Replaced by cv::Animation::loop_count.
122+
IMWRITE_GIF_SPEED = 1025, //!< Not functional since 4.12.0. Replaced by cv::Animation::durations.
123123
IMWRITE_GIF_QUALITY = 1026, //!< For GIF, it can be a quality from 1 to 8. Default is 2. See cv::ImwriteGifCompressionFlags.
124124
IMWRITE_GIF_DITHER = 1027, //!< For GIF, it can be a quality from -1(most dither) to 3(no dither). Default is 0.
125125
IMWRITE_GIF_TRANSPARENCY = 1028, //!< For GIF, the alpha channel lower than this will be set to transparent. Default is 1.
@@ -260,10 +260,20 @@ It provides support for looping, background color settings, frame timing, and fr
260260
struct CV_EXPORTS_W_SIMPLE Animation
261261
{
262262
//! Number of times the animation should loop. 0 means infinite looping.
263+
/*! @note At some file format, when N is set, whether it is displayed N or N+1 times depends on the implementation of the user application. This loop times behaviour has not been documented clearly.
264+
* - (GIF) See https://issues.chromium.org/issues/40459899
265+
* And animated GIF with loop is extended with the Netscape Application Block(NAB), which it not a part of GIF89a specification. See https://en.wikipedia.org/wiki/GIF#Animated_GIF .
266+
* - (WebP) See https://issues.chromium.org/issues/41276895
267+
*/
263268
CV_PROP_RW int loop_count;
264269
//! Background color of the animation in BGRA format.
265270
CV_PROP_RW Scalar bgcolor;
266271
//! Duration for each frame in milliseconds.
272+
/*! @note (GIF) Due to file format limitation
273+
* - Durations must be multiples of 10 milliseconds. Any provided value will be rounded down to the nearest 10ms (e.g., 88ms → 80ms).
274+
* - 0ms(or smaller than expected in user application) duration may cause undefined behavior, e.g. it is handled with default duration.
275+
* - Over 65535 * 10 milliseconds duration is not supported.
276+
*/
267277
CV_PROP_RW std::vector<int> durations;
268278
//! Vector of frames, where each Mat represents a single frame.
269279
CV_PROP_RW std::vector<Mat> frames;

‎modules/imgcodecs/src/grfmt_gif.cpp

Copy file name to clipboardExpand all lines: modules/imgcodecs/src/grfmt_gif.cpp
+55-30Lines changed: 55 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ bool GifDecoder::readHeader() {
4747
return false;
4848
}
4949

50-
String signature(6, ' ');
51-
m_strm.getBytes((uchar*)signature.data(), 6);
50+
std::string signature(6, ' ');
51+
m_strm.getBytes((uchar*)signature.c_str(), 6);
5252
CV_Assert(signature == R"(GIF87a)" || signature == R"(GIF89a)");
5353

5454
// #1: read logical screen descriptor
@@ -428,6 +428,7 @@ void GifDecoder::close() {
428428

429429
bool GifDecoder::getFrameCount_() {
430430
m_frame_count = 0;
431+
m_animation.loop_count = 1;
431432
auto type = (uchar)m_strm.getByte();
432433
while (type != 0x3B) {
433434
if (!(type ^ 0x21)) {
@@ -436,11 +437,18 @@ bool GifDecoder::getFrameCount_() {
436437
// Application Extension need to be handled for the loop count
437438
if (extension == 0xFF) {
438439
int len = m_strm.getByte();
440+
bool isFoundNetscape = false;
439441
while (len) {
440-
// TODO: In strictly, Application Identifier and Authentication Code should be checked.
441-
if (len == 3) {
442-
if (m_strm.getByte() == 0x01) {
443-
m_animation.loop_count = m_strm.getWord();
442+
if (len == 11) {
443+
std::string app_auth_code(len, ' ');
444+
m_strm.getBytes(const_cast<void*>(static_cast<const void*>(app_auth_code.c_str())), len);
445+
isFoundNetscape = (app_auth_code == R"(NETSCAPE2.0)");
446+
} else if (len == 3) {
447+
if (isFoundNetscape && (m_strm.getByte() == 0x01)) {
448+
int loop_count = m_strm.getWord();
449+
// If loop_count == 0, it means loop forever.
450+
// Otherwise, the loop is displayed extra one time than it is written in the data.
451+
m_animation.loop_count = (loop_count == 0) ? 0 : loop_count + 1;
444452
} else {
445453
// this branch should not be reached in normal cases
446454
m_strm.skip(2);
@@ -505,8 +513,8 @@ bool GifDecoder::getFrameCount_() {
505513
}
506514

507515
bool GifDecoder::skipHeader() {
508-
String signature(6, ' ');
509-
m_strm.getBytes((uchar *) signature.data(), 6);
516+
std::string signature(6, ' ');
517+
m_strm.getBytes((uchar *) signature.c_str(), 6);
510518
// skip height and width
511519
m_strm.skip(4);
512520
char flags = (char) m_strm.getByte();
@@ -538,9 +546,7 @@ GifEncoder::GifEncoder() {
538546

539547
// default value of the params
540548
fast = true;
541-
loopCount = 0; // infinite loops by default
542549
criticalTransparency = 1; // critical transparency, default 1, range from 0 to 255, 0 means no transparency
543-
frameDelay = 5; // 20fps by default, 10ms per unit
544550
bitDepth = 8; // the number of bits per pixel, default 8, currently it is a constant number
545551
lzwMinCodeSize = 8; // the minimum code size, default 8, this changes as the color number changes
546552
colorNum = 256; // the number of colors in the color table, default 256
@@ -566,16 +572,14 @@ bool GifEncoder::writeanimation(const Animation& animation, const std::vector<in
566572
return false;
567573
}
568574

569-
loopCount = animation.loop_count;
570-
571575
// confirm the params
572576
for (size_t i = 0; i < params.size(); i += 2) {
573577
switch (params[i]) {
574578
case IMWRITE_GIF_LOOP:
575-
loopCount = std::min(std::max(params[i + 1], 0), 65535); // loop count is in 2 bytes
579+
CV_LOG_WARNING(NULL, "IMWRITE_GIF_LOOP is not functional since 4.12.0. Replaced by cv::Animation::loop_count.");
576580
break;
577581
case IMWRITE_GIF_SPEED:
578-
frameDelay = 100 - std::min(std::max(params[i + 1] - 1, 0), 99); // from 10ms to 1000ms
582+
CV_LOG_WARNING(NULL, "IMWRITE_GIF_SPEED is not functional since 4.12.0. Replaced by cv::Animation::durations.");
579583
break;
580584
case IMWRITE_GIF_DITHER:
581585
dithering = std::min(std::max(params[i + 1], -1), 3);
@@ -648,15 +652,28 @@ bool GifEncoder::writeanimation(const Animation& animation, const std::vector<in
648652
} else {
649653
img_vec_ = animation.frames;
650654
}
651-
bool result = writeHeader(img_vec_);
655+
bool result = writeHeader(img_vec_, animation.loop_count);
652656
if (!result) {
653657
strm.close();
654658
return false;
655659
}
656660

657661
for (size_t i = 0; i < img_vec_.size(); i++) {
658-
frameDelay = cvRound(animation.durations[i] / 10);
659-
result = writeFrame(img_vec_[i]);
662+
// Animation duration is in 1ms unit.
663+
const int frameDelay = animation.durations[i];
664+
CV_CheckGE(frameDelay, 0, "It must be positive value");
665+
666+
// GIF file stores duration in 10ms unit.
667+
const int frameDelay10ms = cvRound(frameDelay / 10);
668+
CV_LOG_IF_WARNING(NULL, (frameDelay10ms == 0),
669+
cv::format("frameDelay(%d) is rounded to 0ms, its behaviour is user application depended.", frameDelay));
670+
CV_CheckLE(frameDelay10ms, 65535, "It requires to be stored in WORD");
671+
672+
result = writeFrame(img_vec_[i], frameDelay10ms);
673+
if (!result) {
674+
strm.close();
675+
return false;
676+
}
660677
}
661678

662679
strm.putByte(0x3B); // trailer
@@ -668,10 +685,11 @@ ImageEncoder GifEncoder::newEncoder() const {
668685
return makePtr<GifEncoder>();
669686
}
670687

671-
bool GifEncoder::writeFrame(const Mat &img) {
688+
bool GifEncoder::writeFrame(const Mat &img, const int frameDelay10ms) {
672689
if (img.empty()) {
673690
return false;
674691
}
692+
675693
height = m_height, width = m_width;
676694

677695
// graphic control extension
@@ -681,7 +699,7 @@ bool GifEncoder::writeFrame(const Mat &img) {
681699
const int gcePackedFields = static_cast<int>(GIF_DISPOSE_RESTORE_PREVIOUS << GIF_DISPOSE_METHOD_SHIFT) |
682700
static_cast<int>(criticalTransparency ? GIF_TRANSPARENT_INDEX_GIVEN : GIF_TRANSPARENT_INDEX_NOT_GIVEN);
683701
strm.putByte(gcePackedFields);
684-
strm.putWord(frameDelay);
702+
strm.putWord(frameDelay10ms);
685703
strm.putByte(transparentColor);
686704
strm.putByte(0x00); // end of the extension
687705

@@ -796,7 +814,7 @@ bool GifEncoder::lzwEncode() {
796814
return true;
797815
}
798816

799-
bool GifEncoder::writeHeader(const std::vector<Mat>& img_vec) {
817+
bool GifEncoder::writeHeader(const std::vector<Mat>& img_vec, const int loopCount) {
800818
strm.putBytes(fmtGifHeader, (int)strlen(fmtGifHeader));
801819

802820
if (img_vec[0].empty()) {
@@ -821,16 +839,23 @@ bool GifEncoder::writeHeader(const std::vector<Mat>& img_vec) {
821839
strm.putBytes(globalColorTable.data(), globalColorTableSize * 3);
822840
}
823841

824-
825-
// add application extension to set the loop count
826-
strm.putByte(0x21); // GIF extension code
827-
strm.putByte(0xFF); // application extension table
828-
strm.putByte(0x0B); // length of application block, in decimal is 11
829-
strm.putBytes(R"(NETSCAPE2.0)", 11); // application authentication code
830-
strm.putByte(0x03); // length of application block, in decimal is 3
831-
strm.putByte(0x01); // identifier
832-
strm.putWord(loopCount);
833-
strm.putByte(0x00); // end of the extension
842+
if ( loopCount != 1 ) // If no-loop, Netscape Application Block is unnecessary.
843+
{
844+
// loopCount 0 means loop forever.
845+
// Otherwise, most browsers(Edge, Chrome, Firefox...) will loop with extra 1 time.
846+
// GIF data should be written with loop count decreased by 1.
847+
const int _loopCount = ( loopCount == 0 ) ? loopCount : loopCount - 1;
848+
849+
// add Netscape Application Block to set the loop count in application extension.
850+
strm.putByte(0x21); // GIF extension code
851+
strm.putByte(0xFF); // application extension table
852+
strm.putByte(0x0B); // length of application block, in decimal is 11
853+
strm.putBytes(R"(NETSCAPE2.0)", 11); // application authentication code
854+
strm.putByte(0x03); // length of application block, in decimal is 3
855+
strm.putByte(0x01); // identifier
856+
strm.putWord(_loopCount);
857+
strm.putByte(0x00); // end of the extension
858+
}
834859

835860
return true;
836861
}

‎modules/imgcodecs/src/grfmt_gif.hpp

Copy file name to clipboardExpand all lines: modules/imgcodecs/src/grfmt_gif.hpp
+2-5Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,6 @@ class GifEncoder CV_FINAL : public BaseImageEncoder {
157157
int globalColorTableSize;
158158
int localColorTableSize;
159159

160-
uchar opMode;
161160
uchar criticalTransparency;
162161
uchar transparentColor;
163162
Vec3b transparentRGB;
@@ -173,17 +172,15 @@ class GifEncoder CV_FINAL : public BaseImageEncoder {
173172
std::vector<uchar> localColorTable;
174173

175174
// params
176-
int loopCount;
177-
int frameDelay;
178175
int colorNum;
179176
int bitDepth;
180177
int dithering;
181178
int lzwMinCodeSize, lzwMaxCodeSize;
182179
bool fast;
183180

184181
bool writeFrames(const std::vector<Mat>& img_vec, const std::vector<int>& params);
185-
bool writeHeader(const std::vector<Mat>& img_vec);
186-
bool writeFrame(const Mat& img);
182+
bool writeHeader(const std::vector<Mat>& img_vec, const int loopCount);
183+
bool writeFrame(const Mat& img, const int frameDelay);
187184
bool pixel2code(const Mat& img);
188185
void getColorTable(const std::vector<Mat>& img_vec, bool isGlobal);
189186
static int ditheringKernel(const Mat &img, Mat &img_, int depth, uchar transparency);

‎modules/imgcodecs/test/test_gif.cpp

Copy file name to clipboardExpand all lines: modules/imgcodecs/test/test_gif.cpp
+104Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,110 @@ TEST(Imgcodecs_Gif, decode_disposal_method)
414414
}
415415
}
416416

417+
// See https://github.com/opencv/opencv/issues/26970
418+
typedef testing::TestWithParam<int> Imgcodecs_Gif_loop_count;
419+
TEST_P(Imgcodecs_Gif_loop_count, imwriteanimation)
420+
{
421+
const string gif_filename = cv::tempfile(".gif");
422+
423+
int loopCount = GetParam();
424+
cv::Animation anim(loopCount);
425+
426+
vector<cv::Mat> src;
427+
for(int n = 1; n <= 5 ; n ++ )
428+
{
429+
cv::Mat frame(64, 64, CV_8UC3, cv::Scalar::all(0));
430+
cv::putText(frame, cv::format("%d", n), cv::Point(0,64), cv::FONT_HERSHEY_PLAIN, 4.0, cv::Scalar::all(255));
431+
anim.frames.push_back(frame);
432+
anim.durations.push_back(1000 /* ms */);
433+
}
434+
435+
bool ret = false;
436+
#if 0
437+
// To output gif image for test.
438+
EXPECT_NO_THROW(ret = imwriteanimation(cv::format("gif_loop-%d.gif", loopCount), anim));
439+
EXPECT_TRUE(ret);
440+
#endif
441+
EXPECT_NO_THROW(ret = imwriteanimation(gif_filename, anim));
442+
EXPECT_TRUE(ret);
443+
444+
// Read raw GIF data.
445+
std::ifstream ifs(gif_filename);
446+
std::stringstream ss;
447+
ss << ifs.rdbuf();
448+
string tmp = ss.str();
449+
std::vector<uint8_t> buf(tmp.begin(), tmp.end());
450+
451+
std::vector<uint8_t> netscape = {0x21, 0xFF, 0x0B, 'N','E','T','S','C','A','P','E','2','.','0'};
452+
auto pos = std::search(buf.begin(), buf.end(), netscape.begin(), netscape.end());
453+
if(loopCount == 1) {
454+
EXPECT_EQ(pos, buf.end()) << "Netscape Application Block should not be included if Animation.loop_count == 1";
455+
} else {
456+
EXPECT_NE(pos, buf.end()) << "Netscape Application Block should be included if Animation.loop_count != 1";
457+
}
458+
459+
remove(gif_filename.c_str());
460+
}
461+
462+
INSTANTIATE_TEST_CASE_P(/*nothing*/,
463+
Imgcodecs_Gif_loop_count,
464+
testing::Values(
465+
-1,
466+
0, // Default, loop-forever
467+
1,
468+
2,
469+
65534,
470+
65535, // Maximum Limit
471+
65536
472+
)
473+
);
474+
475+
typedef testing::TestWithParam<int> Imgcodecs_Gif_duration;
476+
TEST_P(Imgcodecs_Gif_duration, imwriteanimation)
477+
{
478+
const string gif_filename = cv::tempfile(".gif");
479+
480+
cv::Animation anim;
481+
482+
int duration = GetParam();
483+
vector<cv::Mat> src;
484+
for(int n = 1; n <= 5 ; n ++ )
485+
{
486+
cv::Mat frame(64, 64, CV_8UC3, cv::Scalar::all(0));
487+
cv::putText(frame, cv::format("%d", n), cv::Point(0,64), cv::FONT_HERSHEY_PLAIN, 4.0, cv::Scalar::all(255));
488+
anim.frames.push_back(frame);
489+
anim.durations.push_back(duration /* ms */);
490+
}
491+
492+
bool ret = false;
493+
#if 0
494+
// To output gif image for test.
495+
EXPECT_NO_THROW(ret = imwriteanimation(cv::format("gif_duration-%d.gif", duration), anim));
496+
EXPECT_EQ(ret, ( (0 <= duration) && (duration <= 655350) ) );
497+
#endif
498+
EXPECT_NO_THROW(ret = imwriteanimation(gif_filename, anim));
499+
EXPECT_EQ(ret, ( (0 <= duration) && (duration <= 655350) ) );
500+
501+
remove(gif_filename.c_str());
502+
}
503+
504+
INSTANTIATE_TEST_CASE_P(/*nothing*/,
505+
Imgcodecs_Gif_duration,
506+
testing::Values(
507+
-1, // Unsupported
508+
0, // Undefined Behaviour
509+
1,
510+
9,
511+
10,
512+
50,
513+
100, // 10 FPS
514+
1000, // 1 FPS
515+
655340,
516+
655350, // Maximum Limit
517+
655360 // Unsupported
518+
)
519+
);
520+
417521
}//opencv_test
418522
}//namespace
419523

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.