mirror of
https://github.com/opencv/opencv.git
synced 2025-06-23 20:21:40 +08:00
Merge pull request #26971 from Kumataro:fix26970
imgcodecs: gif: support animated gif without loop #26971 Close #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
This commit is contained in:
parent
8207549638
commit
3e43d0cfca
@ -118,8 +118,8 @@ enum ImwriteFlags {
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
IMWRITE_GIF_LOOP = 1024,//!< For GIF, it can be a loop flag from 0 to 65535. Default is 0 - loop forever.
|
||||
IMWRITE_GIF_SPEED = 1025,//!< For GIF, it is between 1 (slowest) and 100 (fastest). Default is 96.
|
||||
IMWRITE_GIF_LOOP = 1024, //!< Not functional since 4.12.0. Replaced by cv::Animation::loop_count.
|
||||
IMWRITE_GIF_SPEED = 1025, //!< Not functional since 4.12.0. Replaced by cv::Animation::durations.
|
||||
IMWRITE_GIF_QUALITY = 1026, //!< For GIF, it can be a quality from 1 to 8. Default is 2. See cv::ImwriteGifCompressionFlags.
|
||||
IMWRITE_GIF_DITHER = 1027, //!< For GIF, it can be a quality from -1(most dither) to 3(no dither). Default is 0.
|
||||
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
|
||||
struct CV_EXPORTS_W_SIMPLE Animation
|
||||
{
|
||||
//! Number of times the animation should loop. 0 means infinite looping.
|
||||
/*! @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.
|
||||
* - (GIF) See https://issues.chromium.org/issues/40459899
|
||||
* 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 .
|
||||
* - (WebP) See https://issues.chromium.org/issues/41276895
|
||||
*/
|
||||
CV_PROP_RW int loop_count;
|
||||
//! Background color of the animation in BGRA format.
|
||||
CV_PROP_RW Scalar bgcolor;
|
||||
//! Duration for each frame in milliseconds.
|
||||
/*! @note (GIF) Due to file format limitation
|
||||
* - Durations must be multiples of 10 milliseconds. Any provided value will be rounded down to the nearest 10ms (e.g., 88ms → 80ms).
|
||||
* - 0ms(or smaller than expected in user application) duration may cause undefined behavior, e.g. it is handled with default duration.
|
||||
* - Over 65535 * 10 milliseconds duration is not supported.
|
||||
*/
|
||||
CV_PROP_RW std::vector<int> durations;
|
||||
//! Vector of frames, where each Mat represents a single frame.
|
||||
CV_PROP_RW std::vector<Mat> frames;
|
||||
|
@ -47,8 +47,8 @@ bool GifDecoder::readHeader() {
|
||||
return false;
|
||||
}
|
||||
|
||||
String signature(6, ' ');
|
||||
m_strm.getBytes((uchar*)signature.data(), 6);
|
||||
std::string signature(6, ' ');
|
||||
m_strm.getBytes((uchar*)signature.c_str(), 6);
|
||||
CV_Assert(signature == R"(GIF87a)" || signature == R"(GIF89a)");
|
||||
|
||||
// #1: read logical screen descriptor
|
||||
@ -428,6 +428,7 @@ void GifDecoder::close() {
|
||||
|
||||
bool GifDecoder::getFrameCount_() {
|
||||
m_frame_count = 0;
|
||||
m_animation.loop_count = 1;
|
||||
auto type = (uchar)m_strm.getByte();
|
||||
while (type != 0x3B) {
|
||||
if (!(type ^ 0x21)) {
|
||||
@ -436,11 +437,18 @@ bool GifDecoder::getFrameCount_() {
|
||||
// Application Extension need to be handled for the loop count
|
||||
if (extension == 0xFF) {
|
||||
int len = m_strm.getByte();
|
||||
bool isFoundNetscape = false;
|
||||
while (len) {
|
||||
// TODO: In strictly, Application Identifier and Authentication Code should be checked.
|
||||
if (len == 3) {
|
||||
if (m_strm.getByte() == 0x01) {
|
||||
m_animation.loop_count = m_strm.getWord();
|
||||
if (len == 11) {
|
||||
std::string app_auth_code(len, ' ');
|
||||
m_strm.getBytes(const_cast<void*>(static_cast<const void*>(app_auth_code.c_str())), len);
|
||||
isFoundNetscape = (app_auth_code == R"(NETSCAPE2.0)");
|
||||
} else if (len == 3) {
|
||||
if (isFoundNetscape && (m_strm.getByte() == 0x01)) {
|
||||
int loop_count = m_strm.getWord();
|
||||
// If loop_count == 0, it means loop forever.
|
||||
// Otherwise, the loop is displayed extra one time than it is written in the data.
|
||||
m_animation.loop_count = (loop_count == 0) ? 0 : loop_count + 1;
|
||||
} else {
|
||||
// this branch should not be reached in normal cases
|
||||
m_strm.skip(2);
|
||||
@ -505,8 +513,8 @@ bool GifDecoder::getFrameCount_() {
|
||||
}
|
||||
|
||||
bool GifDecoder::skipHeader() {
|
||||
String signature(6, ' ');
|
||||
m_strm.getBytes((uchar *) signature.data(), 6);
|
||||
std::string signature(6, ' ');
|
||||
m_strm.getBytes((uchar *) signature.c_str(), 6);
|
||||
// skip height and width
|
||||
m_strm.skip(4);
|
||||
char flags = (char) m_strm.getByte();
|
||||
@ -538,9 +546,7 @@ GifEncoder::GifEncoder() {
|
||||
|
||||
// default value of the params
|
||||
fast = true;
|
||||
loopCount = 0; // infinite loops by default
|
||||
criticalTransparency = 1; // critical transparency, default 1, range from 0 to 255, 0 means no transparency
|
||||
frameDelay = 5; // 20fps by default, 10ms per unit
|
||||
bitDepth = 8; // the number of bits per pixel, default 8, currently it is a constant number
|
||||
lzwMinCodeSize = 8; // the minimum code size, default 8, this changes as the color number changes
|
||||
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
|
||||
return false;
|
||||
}
|
||||
|
||||
loopCount = animation.loop_count;
|
||||
|
||||
// confirm the params
|
||||
for (size_t i = 0; i < params.size(); i += 2) {
|
||||
switch (params[i]) {
|
||||
case IMWRITE_GIF_LOOP:
|
||||
loopCount = std::min(std::max(params[i + 1], 0), 65535); // loop count is in 2 bytes
|
||||
CV_LOG_WARNING(NULL, "IMWRITE_GIF_LOOP is not functional since 4.12.0. Replaced by cv::Animation::loop_count.");
|
||||
break;
|
||||
case IMWRITE_GIF_SPEED:
|
||||
frameDelay = 100 - std::min(std::max(params[i + 1] - 1, 0), 99); // from 10ms to 1000ms
|
||||
CV_LOG_WARNING(NULL, "IMWRITE_GIF_SPEED is not functional since 4.12.0. Replaced by cv::Animation::durations.");
|
||||
break;
|
||||
case IMWRITE_GIF_DITHER:
|
||||
dithering = std::min(std::max(params[i + 1], -1), 3);
|
||||
@ -648,15 +652,28 @@ bool GifEncoder::writeanimation(const Animation& animation, const std::vector<in
|
||||
} else {
|
||||
img_vec_ = animation.frames;
|
||||
}
|
||||
bool result = writeHeader(img_vec_);
|
||||
bool result = writeHeader(img_vec_, animation.loop_count);
|
||||
if (!result) {
|
||||
strm.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < img_vec_.size(); i++) {
|
||||
frameDelay = cvRound(animation.durations[i] / 10);
|
||||
result = writeFrame(img_vec_[i]);
|
||||
// Animation duration is in 1ms unit.
|
||||
const int frameDelay = animation.durations[i];
|
||||
CV_CheckGE(frameDelay, 0, "It must be positive value");
|
||||
|
||||
// GIF file stores duration in 10ms unit.
|
||||
const int frameDelay10ms = cvRound(frameDelay / 10);
|
||||
CV_LOG_IF_WARNING(NULL, (frameDelay10ms == 0),
|
||||
cv::format("frameDelay(%d) is rounded to 0ms, its behaviour is user application depended.", frameDelay));
|
||||
CV_CheckLE(frameDelay10ms, 65535, "It requires to be stored in WORD");
|
||||
|
||||
result = writeFrame(img_vec_[i], frameDelay10ms);
|
||||
if (!result) {
|
||||
strm.close();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
strm.putByte(0x3B); // trailer
|
||||
@ -668,10 +685,11 @@ ImageEncoder GifEncoder::newEncoder() const {
|
||||
return makePtr<GifEncoder>();
|
||||
}
|
||||
|
||||
bool GifEncoder::writeFrame(const Mat &img) {
|
||||
bool GifEncoder::writeFrame(const Mat &img, const int frameDelay10ms) {
|
||||
if (img.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
height = m_height, width = m_width;
|
||||
|
||||
// graphic control extension
|
||||
@ -681,7 +699,7 @@ bool GifEncoder::writeFrame(const Mat &img) {
|
||||
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.putWord(frameDelay10ms);
|
||||
strm.putByte(transparentColor);
|
||||
strm.putByte(0x00); // end of the extension
|
||||
|
||||
@ -796,7 +814,7 @@ bool GifEncoder::lzwEncode() {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GifEncoder::writeHeader(const std::vector<Mat>& img_vec) {
|
||||
bool GifEncoder::writeHeader(const std::vector<Mat>& img_vec, const int loopCount) {
|
||||
strm.putBytes(fmtGifHeader, (int)strlen(fmtGifHeader));
|
||||
|
||||
if (img_vec[0].empty()) {
|
||||
@ -821,16 +839,23 @@ bool GifEncoder::writeHeader(const std::vector<Mat>& img_vec) {
|
||||
strm.putBytes(globalColorTable.data(), globalColorTableSize * 3);
|
||||
}
|
||||
|
||||
if ( loopCount != 1 ) // If no-loop, Netscape Application Block is unnecessary.
|
||||
{
|
||||
// loopCount 0 means loop forever.
|
||||
// Otherwise, most browsers(Edge, Chrome, Firefox...) will loop with extra 1 time.
|
||||
// GIF data should be written with loop count decreased by 1.
|
||||
const int _loopCount = ( loopCount == 0 ) ? loopCount : loopCount - 1;
|
||||
|
||||
// add application extension to set the loop count
|
||||
strm.putByte(0x21); // GIF extension code
|
||||
strm.putByte(0xFF); // application extension table
|
||||
strm.putByte(0x0B); // length of application block, in decimal is 11
|
||||
strm.putBytes(R"(NETSCAPE2.0)", 11); // application authentication code
|
||||
strm.putByte(0x03); // length of application block, in decimal is 3
|
||||
strm.putByte(0x01); // identifier
|
||||
strm.putWord(loopCount);
|
||||
strm.putByte(0x00); // end of the extension
|
||||
// add Netscape Application Block to set the loop count in application extension.
|
||||
strm.putByte(0x21); // GIF extension code
|
||||
strm.putByte(0xFF); // application extension table
|
||||
strm.putByte(0x0B); // length of application block, in decimal is 11
|
||||
strm.putBytes(R"(NETSCAPE2.0)", 11); // application authentication code
|
||||
strm.putByte(0x03); // length of application block, in decimal is 3
|
||||
strm.putByte(0x01); // identifier
|
||||
strm.putWord(_loopCount);
|
||||
strm.putByte(0x00); // end of the extension
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -157,7 +157,6 @@ private:
|
||||
int globalColorTableSize;
|
||||
int localColorTableSize;
|
||||
|
||||
uchar opMode;
|
||||
uchar criticalTransparency;
|
||||
uchar transparentColor;
|
||||
Vec3b transparentRGB;
|
||||
@ -173,8 +172,6 @@ private:
|
||||
std::vector<uchar> localColorTable;
|
||||
|
||||
// params
|
||||
int loopCount;
|
||||
int frameDelay;
|
||||
int colorNum;
|
||||
int bitDepth;
|
||||
int dithering;
|
||||
@ -182,8 +179,8 @@ private:
|
||||
bool fast;
|
||||
|
||||
bool writeFrames(const std::vector<Mat>& img_vec, const std::vector<int>& params);
|
||||
bool writeHeader(const std::vector<Mat>& img_vec);
|
||||
bool writeFrame(const Mat& img);
|
||||
bool writeHeader(const std::vector<Mat>& img_vec, const int loopCount);
|
||||
bool writeFrame(const Mat& img, const int frameDelay);
|
||||
bool pixel2code(const Mat& img);
|
||||
void getColorTable(const std::vector<Mat>& img_vec, bool isGlobal);
|
||||
static int ditheringKernel(const Mat &img, Mat &img_, int depth, uchar transparency);
|
||||
|
@ -414,6 +414,110 @@ TEST(Imgcodecs_Gif, decode_disposal_method)
|
||||
}
|
||||
}
|
||||
|
||||
// See https://github.com/opencv/opencv/issues/26970
|
||||
typedef testing::TestWithParam<int> Imgcodecs_Gif_loop_count;
|
||||
TEST_P(Imgcodecs_Gif_loop_count, imwriteanimation)
|
||||
{
|
||||
const string gif_filename = cv::tempfile(".gif");
|
||||
|
||||
int loopCount = GetParam();
|
||||
cv::Animation anim(loopCount);
|
||||
|
||||
vector<cv::Mat> src;
|
||||
for(int n = 1; n <= 5 ; n ++ )
|
||||
{
|
||||
cv::Mat frame(64, 64, CV_8UC3, cv::Scalar::all(0));
|
||||
cv::putText(frame, cv::format("%d", n), cv::Point(0,64), cv::FONT_HERSHEY_PLAIN, 4.0, cv::Scalar::all(255));
|
||||
anim.frames.push_back(frame);
|
||||
anim.durations.push_back(1000 /* ms */);
|
||||
}
|
||||
|
||||
bool ret = false;
|
||||
#if 0
|
||||
// To output gif image for test.
|
||||
EXPECT_NO_THROW(ret = imwriteanimation(cv::format("gif_loop-%d.gif", loopCount), anim));
|
||||
EXPECT_TRUE(ret);
|
||||
#endif
|
||||
EXPECT_NO_THROW(ret = imwriteanimation(gif_filename, anim));
|
||||
EXPECT_TRUE(ret);
|
||||
|
||||
// Read raw GIF data.
|
||||
std::ifstream ifs(gif_filename);
|
||||
std::stringstream ss;
|
||||
ss << ifs.rdbuf();
|
||||
string tmp = ss.str();
|
||||
std::vector<uint8_t> buf(tmp.begin(), tmp.end());
|
||||
|
||||
std::vector<uint8_t> netscape = {0x21, 0xFF, 0x0B, 'N','E','T','S','C','A','P','E','2','.','0'};
|
||||
auto pos = std::search(buf.begin(), buf.end(), netscape.begin(), netscape.end());
|
||||
if(loopCount == 1) {
|
||||
EXPECT_EQ(pos, buf.end()) << "Netscape Application Block should not be included if Animation.loop_count == 1";
|
||||
} else {
|
||||
EXPECT_NE(pos, buf.end()) << "Netscape Application Block should be included if Animation.loop_count != 1";
|
||||
}
|
||||
|
||||
remove(gif_filename.c_str());
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(/*nothing*/,
|
||||
Imgcodecs_Gif_loop_count,
|
||||
testing::Values(
|
||||
-1,
|
||||
0, // Default, loop-forever
|
||||
1,
|
||||
2,
|
||||
65534,
|
||||
65535, // Maximum Limit
|
||||
65536
|
||||
)
|
||||
);
|
||||
|
||||
typedef testing::TestWithParam<int> Imgcodecs_Gif_duration;
|
||||
TEST_P(Imgcodecs_Gif_duration, imwriteanimation)
|
||||
{
|
||||
const string gif_filename = cv::tempfile(".gif");
|
||||
|
||||
cv::Animation anim;
|
||||
|
||||
int duration = GetParam();
|
||||
vector<cv::Mat> src;
|
||||
for(int n = 1; n <= 5 ; n ++ )
|
||||
{
|
||||
cv::Mat frame(64, 64, CV_8UC3, cv::Scalar::all(0));
|
||||
cv::putText(frame, cv::format("%d", n), cv::Point(0,64), cv::FONT_HERSHEY_PLAIN, 4.0, cv::Scalar::all(255));
|
||||
anim.frames.push_back(frame);
|
||||
anim.durations.push_back(duration /* ms */);
|
||||
}
|
||||
|
||||
bool ret = false;
|
||||
#if 0
|
||||
// To output gif image for test.
|
||||
EXPECT_NO_THROW(ret = imwriteanimation(cv::format("gif_duration-%d.gif", duration), anim));
|
||||
EXPECT_EQ(ret, ( (0 <= duration) && (duration <= 655350) ) );
|
||||
#endif
|
||||
EXPECT_NO_THROW(ret = imwriteanimation(gif_filename, anim));
|
||||
EXPECT_EQ(ret, ( (0 <= duration) && (duration <= 655350) ) );
|
||||
|
||||
remove(gif_filename.c_str());
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(/*nothing*/,
|
||||
Imgcodecs_Gif_duration,
|
||||
testing::Values(
|
||||
-1, // Unsupported
|
||||
0, // Undefined Behaviour
|
||||
1,
|
||||
9,
|
||||
10,
|
||||
50,
|
||||
100, // 10 FPS
|
||||
1000, // 1 FPS
|
||||
655340,
|
||||
655350, // Maximum Limit
|
||||
655360 // Unsupported
|
||||
)
|
||||
);
|
||||
|
||||
}//opencv_test
|
||||
}//namespace
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user