From 082cd7a74eab9a435c414e4fe7a5c0803d4d3b4d Mon Sep 17 00:00:00 2001 From: Super <12210305@mail.sustech.edu.cn> Date: Sat, 7 Dec 2024 15:17:41 +0800 Subject: [PATCH] Merge pull request #25691 from redhecker:gifSupport [GSoC] Add GIF decode and encode for imgcodecs #25691 this is related to #24855 we add gif support for `imread`, `imreadmulti`, `imwrite` and `imwritemulti` opencv_extra: https://github.com/opencv/opencv_extra/pull/1203 ### 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 --- CMakeLists.txt | 7 + cmake/OpenCVFindLibsGrfmt.cmake | 5 + modules/imgcodecs/CMakeLists.txt | 4 + .../imgcodecs/include/opencv2/imgcodecs.hpp | 21 +- modules/imgcodecs/src/grfmt_gif.cpp | 1144 +++++++++++++++++ modules/imgcodecs/src/grfmt_gif.hpp | 183 +++ modules/imgcodecs/src/grfmts.hpp | 1 + modules/imgcodecs/src/loadsave.cpp | 12 +- modules/imgcodecs/test/test_gif.cpp | 357 +++++ 9 files changed, 1729 insertions(+), 5 deletions(-) create mode 100644 modules/imgcodecs/src/grfmt_gif.cpp create mode 100644 modules/imgcodecs/src/grfmt_gif.hpp create mode 100644 modules/imgcodecs/test/test_gif.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 2be9a87a9e..7af059f8dd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -432,6 +432,9 @@ OCV_OPTION(WITH_ITT "Include Intel ITT support" ON OCV_OPTION(WITH_PROTOBUF "Enable libprotobuf" ON VISIBLE_IF TRUE VERIFY HAVE_PROTOBUF) +OCV_OPTION(WITH_IMGCODEC_GIF "Include GIF support" OFF + VISIBLE_IF TRUE + VERIFY HAVE_IMGCODEC_GIF) OCV_OPTION(WITH_IMGCODEC_HDR "Include HDR support" ON VISIBLE_IF TRUE VERIFY HAVE_IMGCODEC_HDR) @@ -1557,6 +1560,10 @@ if(WITH_GDCM OR HAVE_GDCM) status(" GDCM:" HAVE_GDCM THEN "YES (${GDCM_VERSION})" ELSE "NO") endif() +if(WITH_IMGCODEC_GIF OR DEFINED HAVE_IMGCODEC_GIF) + status(" GIF:" HAVE_IMGCODEC_GIF THEN "YES" ELSE "NO") +endif() + if(WITH_IMGCODEC_HDR OR DEFINED HAVE_IMGCODEC_HDR) status(" HDR:" HAVE_IMGCODEC_HDR THEN "YES" ELSE "NO") endif() diff --git a/cmake/OpenCVFindLibsGrfmt.cmake b/cmake/OpenCVFindLibsGrfmt.cmake index e059305938..240df36e4d 100644 --- a/cmake/OpenCVFindLibsGrfmt.cmake +++ b/cmake/OpenCVFindLibsGrfmt.cmake @@ -371,6 +371,11 @@ if(WITH_GDCM) endif() endif() +if(WITH_IMGCODEC_GIF) + set(HAVE_IMGCODEC_GIF ON) +elseif(DEFINED WITH_IMGCODEC_GIF) + set(HAVE_IMGCODEC_GIF OFF) +endif() if(WITH_IMGCODEC_HDR) set(HAVE_IMGCODEC_HDR ON) elseif(DEFINED WITH_IMGCODEC_HDR) diff --git a/modules/imgcodecs/CMakeLists.txt b/modules/imgcodecs/CMakeLists.txt index 45b19da643..0c52ce7cfb 100644 --- a/modules/imgcodecs/CMakeLists.txt +++ b/modules/imgcodecs/CMakeLists.txt @@ -88,6 +88,10 @@ if(HAVE_GDAL) list(APPEND GRFMT_LIBS ${GDAL_LIBRARY}) endif() +if(HAVE_IMGCODEC_GIF) + add_definitions(-DHAVE_IMGCODEC_GIF) +endif() + if(HAVE_IMGCODEC_HDR) add_definitions(-DHAVE_IMGCODEC_HDR) endif() diff --git a/modules/imgcodecs/include/opencv2/imgcodecs.hpp b/modules/imgcodecs/include/opencv2/imgcodecs.hpp index 1b045c5079..cd38f22325 100644 --- a/modules/imgcodecs/include/opencv2/imgcodecs.hpp +++ b/modules/imgcodecs/include/opencv2/imgcodecs.hpp @@ -111,7 +111,13 @@ enum ImwriteFlags { IMWRITE_JPEG2000_COMPRESSION_X1000 = 272,//!< For JPEG2000, use to specify the target compression rate (multiplied by 1000). The value can be from 0 to 1000. Default is 1000. IMWRITE_AVIF_QUALITY = 512,//!< For AVIF, it can be a quality between 0 and 100 (the higher the better). Default is 95. IMWRITE_AVIF_DEPTH = 513,//!< For AVIF, it can be 8, 10 or 12. If >8, it is stored/read as CV_32F. Default is 8. - IMWRITE_AVIF_SPEED = 514 //!< For AVIF, it is between 0 (slowest) and (fastest). Default is 9. + IMWRITE_AVIF_SPEED = 514,//!< For AVIF, it is between 0 (slowest) and (fastest). Default is 9. + 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_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. + IMWRITE_GIF_COLORTABLE = 1029 //!< For GIF, 0 means global color table is used, 1 means local color table is used. Default is 0. }; enum ImwriteJPEGSamplingFactorParams { @@ -216,6 +222,18 @@ enum ImwriteHDRCompressionFlags { IMWRITE_HDR_COMPRESSION_RLE = 1 }; +//! Imwrite GIF specific values for IMWRITE_GIF_QUALITY parameter key, if larger than 3, then its related to the size of the color table. +enum ImwriteGIFCompressionFlags { + IMWRITE_GIF_FAST_NO_DITHER = 1, + IMWRITE_GIF_FAST_FLOYD_DITHER = 2, + IMWRITE_GIF_COLORTABLE_SIZE_8 = 3, + IMWRITE_GIF_COLORTABLE_SIZE_16 = 4, + IMWRITE_GIF_COLORTABLE_SIZE_32 = 5, + IMWRITE_GIF_COLORTABLE_SIZE_64 = 6, + IMWRITE_GIF_COLORTABLE_SIZE_128 = 7, + IMWRITE_GIF_COLORTABLE_SIZE_256 = 8 +}; + //! @} imgcodecs_flags /** @brief Loads an image from a file. @@ -229,6 +247,7 @@ returns an empty matrix. Currently, the following file formats are supported: - Windows bitmaps - \*.bmp, \*.dib (always supported) +- GIF files - \*.gif (always supported) - JPEG files - \*.jpeg, \*.jpg, \*.jpe (see the *Note* section) - JPEG 2000 files - \*.jp2 (see the *Note* section) - Portable Network Graphics - \*.png (see the *Note* section) diff --git a/modules/imgcodecs/src/grfmt_gif.cpp b/modules/imgcodecs/src/grfmt_gif.cpp new file mode 100644 index 0000000000..caf8a8fc4f --- /dev/null +++ b/modules/imgcodecs/src/grfmt_gif.cpp @@ -0,0 +1,1144 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level +// directory of this distribution and at http://opencv.org/license.html + +#include "precomp.hpp" +#include "grfmt_gif.hpp" + +#ifdef HAVE_IMGCODEC_GIF +namespace cv +{ +////////////////////////////////////////////////////////////////////// +//// GIF Decoder //// +////////////////////////////////////////////////////////////////////// +GifDecoder::GifDecoder() { + m_signature = R"(GIF)"; + m_type = CV_8UC4; + bgColor = -1; + m_buf_supported = true; + globalColorTableSize = 0; + localColorTableSize = 0; + localColorTable.resize(3 * 256); // maximum size of a color table + lzwMinCodeSize = 0; + hasRead = false; + hasTransparentColor = false; + transparentColor = 0; + opMode = GRFMT_GIF_Nothing; + top = 0, left = 0, width = 0, height = 0; + depth = 8; + idx = 0; +} + +GifDecoder::~GifDecoder() { + close(); +} + +bool GifDecoder::readHeader() { + if (!m_buf.empty()) { + if (!m_strm.open(m_buf)) { + return false; + } + } else if (!m_strm.open(m_filename)) { + return false; + } + + String signature(6, ' '); + m_strm.getBytes((uchar*)signature.data(), 6); + CV_Assert(signature == R"(GIF87a)" || signature == R"(GIF89a)"); + + // #1: read logical screen descriptor + m_width = m_strm.getWord(); + m_height = m_strm.getWord(); + CV_Assert(m_width > 0 && m_height > 0); + + char flags = (char)m_strm.getByte(); + + // the background color -> index in the global color table, valid only if the global color table is present + bgColor = m_strm.getByte(); + m_strm.skip(1); // Skip the aspect ratio + + // #2: read global color table + depth = ((flags & 0x70) >> 4) + 1; + if (flags & 0x80) { + globalColorTableSize = 1 << ((flags & 0x07) + 1); + globalColorTable.resize(3 * globalColorTableSize); + for (int i = 0; i < 3 * globalColorTableSize; i++) { + globalColorTable[i] = (uchar)m_strm.getByte(); + } + } + + // get the frame count + bool success = getFrameCount_(); + + hasRead = false; + return success; +} + +bool GifDecoder::readData(Mat &img) { + if (hasRead) { + lastImage.copyTo(img); + return true; + } + + readExtensions(); + // Image separator + CV_Assert(!(m_strm.getByte()^0x2C)); + left = m_strm.getWord(); + top = m_strm.getWord(); + width = m_strm.getWord(); + height = m_strm.getWord(); + CV_Assert(width > 0 && height > 0 && left + width <= m_width && top + height <= m_height); + + 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; + } + 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); + break; + default: + CV_Assert(false); + } + lastImage.release(); + + auto flags = (uchar)m_strm.getByte(); + if (flags & 0x80) { + // local color table + localColorTableSize = 1 << ((flags & 0x07) + 1); + for (int i = 0; i < 3 * localColorTableSize; i++) { + localColorTable[i] = (uchar)m_strm.getByte(); + } + } else if (globalColorTableSize) { + /* + * According to the GIF Specification at https://www.w3.org/Graphics/GIF/spec-gif89a.txt: + * "Both types of color tables are optional, making it possible for a Data Stream to contain + * numerous graphics without a color table at all." + * The specification recommended that the decoder save the last Global Color Table used + * until another Global Color Table is encountered, here we also save the last Local Color Table used + * in case of there is no such thing as "last Global Color Table used". Thus, we only refresh the + * Local Color Table when a Global Color Table or last Global Color Table used is present. + */ + localColorTableSize = 0; + } + + // lzw decompression to get the code stream + hasRead = lzwDecode(); + + // convert code stream into pixels on the image + if (hasRead) { + idx = 0; + if (!(flags & 0x40)) { + // no interlace, simply convert the code stream into pixels from top to down + code2pixel(img_, 0, 1); + } else { + // consider the interlace mode, the image will be rendered in four separate passes + code2pixel(img_, 0, 8); + code2pixel(img_, 4, 8); + code2pixel(img_, 2, 4); + code2pixel(img_, 1, 2); + } + } + + lastImage = img_; + if (!img.empty()) { + if (img.channels() == 3){ + if (m_use_rgb) { + cvtColor(img_, img, COLOR_BGRA2RGB); + } else { + cvtColor(img_, img, COLOR_BGRA2BGR); + } + } else { + if (m_use_rgb) { + cvtColor(img_, img, COLOR_BGRA2RGBA); + } else { + img_.copyTo(img); + } + } + } + + // release the memory + img_.release(); + + return hasRead; +} + +bool GifDecoder::nextPage() { + if (hasRead) { + hasRead = false; + // end of a gif file + if(!(m_strm.getByte() ^ 0x3B)) return false; + m_strm.setPos(m_strm.getPos() - 1); + return true; + } else { + bool success; + try { + Mat emptyImg; + success = readData(emptyImg); + emptyImg.release(); + } catch(...) { + return false; + } + return success; + } +} + +void GifDecoder::readExtensions() { + uchar len; + while (!(m_strm.getByte() ^ 0x21)) { + auto extensionType = (uchar)m_strm.getByte(); + + // read graphic control extension + // 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(); + m_strm.getWord(); // delay time, not used + opMode = (GifOpMode)((flags & 0x1C) >> 2); + hasTransparentColor = flags & 0x01; + transparentColor = (uchar)m_strm.getByte(); + } + + // skip other kinds of extensions + len = (uchar)m_strm.getByte(); + while (len) { + m_strm.skip(len); + len = (uchar)m_strm.getByte(); + } + } + // roll back to the block identifier + m_strm.setPos(m_strm.getPos() - 1); +} + +void GifDecoder::code2pixel(Mat& img, int start, int k){ + for (int i = start; i < height; i += 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) { + img.at(top + i, left + j) = + Vec4b(localColorTable[colorIdx * 3 + 2], // B + localColorTable[colorIdx * 3 + 1], // G + localColorTable[colorIdx * 3], // R + 255); // 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 + 255); // A + } else if (!(localColorTableSize || globalColorTableSize)) { + /* + * According to the GIF Specification at https://www.w3.org/Graphics/GIF/spec-gif89a.txt: + * "If no color table is available at all, the decoder is free to use a system color table + * or a table of its own. In that case, the decoder may use a color table with as many colors + * as its hardware is able to support; it is recommended that such a table have black and + * white as its first two entries, so that monochrome images can be rendered adequately." + */ + uchar intensity = colorIdx ^ 1 ? colorIdx : 255; + img.at(top + i, left + j) = + Vec4b(intensity, intensity, intensity, 255); + } else { + CV_Assert(false); + } + } + } +} + +bool GifDecoder::lzwDecode() { + // initialization + lzwMinCodeSize = m_strm.getByte(); + int lzwCodeSize = lzwMinCodeSize + 1; + int clearCode = 1 << lzwMinCodeSize; + int exitCode = clearCode + 1; + CV_Assert(lzwCodeSize > 2 && lzwCodeSize <= 12); + std::vector lzwExtraTable((1 << 12) + 1); + int colorTableSize = clearCode; + int lzwTableSize = exitCode; + + idx = 0; + int leftBits = 0; + uint32_t src = 0; + auto blockLen = (uchar)m_strm.getByte(); + while (blockLen) { + if (leftBits < lzwCodeSize) { + src |= m_strm.getByte() << leftBits; + blockLen --; + leftBits += 8; + } + + while (leftBits >= lzwCodeSize) { + // get the code + uint16_t code = src & ((1 << lzwCodeSize) - 1); + src >>= lzwCodeSize; + leftBits -= lzwCodeSize; + + // clear code + if (!(code ^ clearCode)) { + lzwExtraTable.clear(); + // reset the code size, the same as that in the initialization part + lzwCodeSize = lzwMinCodeSize + 1; + lzwTableSize = exitCode; + continue; + } + // end of information + if (!(code ^ exitCode)) { + lzwExtraTable.clear(); + lzwCodeSize = lzwMinCodeSize + 1; + lzwTableSize = exitCode; + break; + } + + // check if the code stream is full + if (idx >= width * height) { + return idx == width * height && blockLen == 0 && !m_strm.getByte(); + } + + // output code + // 1. renew the lzw extra table + if (code < colorTableSize) { + lzwExtraTable[lzwTableSize].suffix = (uchar)code; + lzwTableSize ++; + lzwExtraTable[lzwTableSize].prefix.clear(); + lzwExtraTable[lzwTableSize].prefix.push_back((uchar)code); + lzwExtraTable[lzwTableSize].length = 2; + } else if (code <= lzwTableSize) { + lzwExtraTable[lzwTableSize].suffix = lzwExtraTable[code].prefix[0]; + lzwTableSize ++; + lzwExtraTable[lzwTableSize].prefix = lzwExtraTable[code].prefix; + lzwExtraTable[lzwTableSize].prefix.push_back(lzwExtraTable[code].suffix); + lzwExtraTable[lzwTableSize].length = lzwExtraTable[code].length + 1; + } else { + return false; + } + + // 2. output to the code stream + if (code < colorTableSize) { + imgCodeStream[idx++] = (uchar)code; + } else { + for (int i = 0; i < lzwExtraTable[code].length - 1; i++) { + imgCodeStream[idx++] = lzwExtraTable[code].prefix[i]; + } + imgCodeStream[idx++] = lzwExtraTable[code].suffix; + } + + // check if the code size is full + if (lzwTableSize > (1 << 12)) { + return false; + } + + // check if the bit length is full + if (lzwTableSize == (1 << lzwCodeSize)) { + lzwCodeSize < 12 ? lzwCodeSize++ : lzwCodeSize; + } + } + + // go to the next block if this block has been read out + if (!blockLen) { + blockLen = (uchar)m_strm.getByte(); + } + } + + return idx == width * height; +} + +ImageDecoder GifDecoder::newDecoder() const { + return makePtr(); +} + +void GifDecoder::close() { + while (!lastImage.empty()) lastImage.release(); + m_strm.close(); +} + +bool GifDecoder::getFrameCount_() { + m_frame_count = 0; + auto type = (uchar)m_strm.getByte(); + while (type != 0x3B) { + if (!(type ^ 0x21)) { + // skip all kinds of the extensions + m_strm.skip(1); + int len = m_strm.getByte(); + while (len) { + m_strm.skip(len); + len = m_strm.getByte(); + } + } else if (!(type ^ 0x2C)) { + // skip image data + m_frame_count ++; + // skip left, top, width, height + m_strm.skip(8); + int flags = m_strm.getByte(); + // skip local color table + if (flags & 0x80) { + m_strm.skip(3 * (1 << ((flags & 0x07) + 1))); + } + // skip lzw min code size + m_strm.skip(1); + int len = m_strm.getByte(); + while (len) { + m_strm.skip(len); + len = m_strm.getByte(); + } + } else { + CV_Assert(false); + } + type = (uchar)m_strm.getByte(); + } + // roll back to the block identifier + m_strm.setPos(0); + return skipHeader(); +} + +bool GifDecoder::skipHeader() { + String signature(6, ' '); + m_strm.getBytes((uchar *) signature.data(), 6); + // skip height and width + m_strm.skip(4); + char flags = (char) m_strm.getByte(); + // skip the background color and the aspect ratio + m_strm.skip(2); + // skip the global color table + if (flags & 0x80) { + m_strm.skip(3 * (1 << ((flags & 0x07) + 1))); + } + return signature == R"(GIF87a)" || signature == R"(GIF89a)"; +} + +} // namespace cv + +namespace cv +{ +////////////////////////////////////////////////////////////////////// +//// GIF Encoder //// +////////////////////////////////////////////////////////////////////// +static const char* fmtGifHeader = "GIF89a"; +GifEncoder::GifEncoder() { + m_description = "Graphics Interchange Format 89a(*.gif)"; + 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 + + // 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 + dithering = 0; // the level dithering, default 0 + globalColorTableSize = 256, localColorTableSize = 0; +} + +GifEncoder::~GifEncoder() { + close(); +} + +bool GifEncoder::isFormatSupported(int depth) const { + return depth == CV_8U; +} + +bool GifEncoder::write(const Mat &img, const std::vector ¶ms) { + std::vector img_vec(1, img); + return writeFrames(img_vec, params); +} + +bool GifEncoder::writemulti(const std::vector &img_vec, const std::vector ¶ms) { + return writeFrames(img_vec, params); +} + +bool GifEncoder::writeFrames(const std::vector& img_vec, + const std::vector& params) { + if (img_vec.empty()) { + return false; + } + + if (m_buf) { + if (!strm.open(*m_buf)) { + return false; + } + } else if (!strm.open(m_filename)) { + return false; + } + + // 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 + break; + case IMWRITE_GIF_SPEED: + frameDelay = 100 - std::min(std::max(params[i + 1] - 1, 0), 99); // from 10ms to 1000ms + break; + case IMWRITE_GIF_DITHER: + dithering = std::min(std::max(params[i + 1], -1), 3); + fast = false; + break; + case IMWRITE_GIF_TRANSPARENCY: + criticalTransparency = (uchar)std::min(std::max(params[i + 1], 0), 255); + break; + case IMWRITE_GIF_COLORTABLE: + localColorTableSize = std::min(std::max(params[i + 1], 0), 1); + break; + case IMWRITE_GIF_QUALITY: + switch (params[i + 1]) { + case IMWRITE_GIF_FAST_FLOYD_DITHER: + fast = true; + dithering = GRFMT_GIF_FloydSteinberg; + break; + case IMWRITE_GIF_FAST_NO_DITHER: + fast = true; + dithering = GRFMT_GIF_None; + break; + default: + lzwMinCodeSize = std::min(std::max(params[i + 1], 3), 8); + colorNum = 1 << lzwMinCodeSize; + globalColorTableSize = colorNum; + fast = false; + break; + } + break; // case IMWRITE_GIF_QUALITY + } + } + if (criticalTransparency) { + lzwMinCodeSize = std::min(8, lzwMinCodeSize + 1); + colorNum = 1 << lzwMinCodeSize; + globalColorTableSize = colorNum; + } + localColorTableSize = localColorTableSize ? colorNum : 0; + + std::vector img_vec_; + if (fast) { + const uchar transparent = 0x92; // 1001_0010: the middle of the color table + if (dithering == GRFMT_GIF_None) { + img_vec_ = img_vec; + transparentColor = transparent; + } else { + localColorTableSize = 0; + int transRGB; + const int depth = 3 << 8 | 3 << 4 | 2; // r:g:b = 3:3:2 + for (auto &img: img_vec) { + Mat img_(img.size(), img.type()); + transRGB = ditheringKernel(img, img_, depth, criticalTransparency); + if (transRGB >= 0) { + transparentRGB = Vec3b((transRGB >> 16) & 0xFF, (transRGB >> 8) & 0xFF, transRGB & 0xFF); + transparentColor = transparent; + } + img_vec_.push_back(img_); + } + if (transparentColor == 0) { + criticalTransparency = 0; + } + } + } else if (dithering != GRFMT_GIF_None) { + int depth = (int)floor(log2(colorNum) / 3) + dithering; + depth = depth << 8 | depth << 4 | depth; + for (auto &img : img_vec) { + Mat img_(img.size(), img.type()); + ditheringKernel(img, img_, depth, criticalTransparency); + img_vec_.push_back(img_); + } + } else { + img_vec_ = img_vec; + } + bool result = writeHeader(img_vec_); + if (!result) { + strm.close(); + return false; + } + + for (const auto &img : img_vec_) { + result = writeFrame(img); + } + + strm.putByte(0x3B); // trailer + strm.close(); + return result; +} + +ImageEncoder GifEncoder::newEncoder() const { + return makePtr(); +} + +bool GifEncoder::writeFrame(const Mat &img) { + if (img.empty()) { + return false; + } + height = m_height, width = m_width; + + // graphic control extension + 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); + strm.putWord(frameDelay); + strm.putByte(transparentColor); + strm.putByte(0x00); // end of the extension + + // image descriptor + strm.putByte(0x2C); // image separator + strm.putWord(left); + strm.putWord(top); + strm.putWord(width); + strm.putWord(height); + flag = localColorTableSize > 0 ? 0x80 : 0x00; + if (localColorTableSize > 0) { + std::vector img_vec(1, img); + getColorTable(img_vec, false); + } + flag |= lzwMinCodeSize - 1; + strm.putByte(flag); + if (localColorTableSize > 0) { + strm.putBytes(localColorTable.data(), localColorTableSize * 3); + } + + imgCodeStream.resize(width * height); + bool result = pixel2code(img); + if (result) result = lzwEncode(); + + return result; +} + +bool GifEncoder::lzwEncode() { + strm.putByte(lzwMinCodeSize); + int lzwCodeSize = lzwMinCodeSize + 1; + // add clear code to the head of the output stream + int bitLeft = lzwCodeSize; + size_t output = (size_t)1 << lzwMinCodeSize; + + lzwTable.resize((1 << 12) * 256); + // clear lzwTable + memset(lzwTable.data(), 0, (1 << 20) * sizeof(int16_t)); // 20 = 12 + 8 = 2^12(max lzw table size) * 256 + + // next code + auto idx = (int16_t)((1 << lzwMinCodeSize) + 2); + + int bufferLen = 0; + uchar buffer[256]; + + //initialize + int32_t prev = imgCodeStream[0]; + + for (int64_t i = 1; i < height * width; i++) { + // add the output code to the output buffer + while (bitLeft >= 8) { + buffer[bufferLen++] = (uchar)output; + output >>= 8; + bitLeft -= 8; + if(bufferLen == 255) { + strm.putByte(255); + strm.putBytes(buffer, 255); + bufferLen = 0; + } + } + + uchar c = imgCodeStream[i]; + // prev + currentCode(c) is not in the table + if(lzwTable[prev * 256 + c] == 0){ + output |= ((size_t)prev << bitLeft); + bitLeft += lzwCodeSize; + lzwTable[prev * 256 + c] = idx; + prev = c; + // check if the bit length is full + if(idx == (1 << lzwCodeSize)){ + lzwCodeSize ++; + } + idx ++; + // if the lzwTable is full, add clear code to the output + if(idx == (1 << lzwMaxCodeSize)){ + output |= (((size_t)1 << lzwMinCodeSize) << bitLeft); + bitLeft += lzwCodeSize; + memset(lzwTable.data(), 0, (1 << 20) * sizeof(int16_t)); // clear lzwTable + // next code + idx = (int16_t)((1 << lzwMinCodeSize) + 2); + lzwCodeSize = lzwMinCodeSize + 1; + } + } else{ + prev = lzwTable[prev * 256 + c]; + } + } + + // end of the code + output |= ((size_t)prev << bitLeft); + bitLeft += lzwCodeSize; + output |= ((((size_t)1 << lzwMinCodeSize) | 1) << bitLeft); + bitLeft += lzwCodeSize; + while (bitLeft >= 8) { + buffer[bufferLen++] = (uchar)output; + output >>= 8; + bitLeft -= 8; + if(bufferLen == 255) { + strm.putByte(255); + strm.putBytes(buffer, 255); + bufferLen = 0; + } + } + if (bitLeft > 0) { + buffer[bufferLen++] = (uchar)output; + } + if (bufferLen > 0){ + strm.putByte(bufferLen); + strm.putBytes(buffer, bufferLen); + } + // end of the block + strm.putByte(0); + + return true; +} + +bool GifEncoder::writeHeader(const std::vector& img_vec) { + strm.putBytes(fmtGifHeader, (int)strlen(fmtGifHeader)); + + if (img_vec[0].empty()) { + return false; + } + m_width = img_vec[0].cols, m_height = img_vec[0].rows; + if (m_width <= 0 || m_height <= 0 || m_width > 65535 || m_height > 65535) { + return false; + } + strm.putWord(m_width); + strm.putWord(m_height); + + // by default, set the global color table + uchar flags = (globalColorTableSize > 0) << 7; // global color table flag + getColorTable(img_vec, true); + flags |= (bitDepth - 1) << 4; // bit depth + flags |= (lzwMinCodeSize - 1); // global color table size + strm.putByte(flags); + strm.putByte(0); // background color, default value + strm.putByte(0); // aspect ratio, default value + if (globalColorTableSize > 0) { + strm.putBytes(globalColorTable.data(), globalColorTableSize * 3); + } + + + // 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 + + return true; +} + +bool GifEncoder::pixel2code(const Mat &img) { + if(img.empty()) return false; + CV_Assert(img.rows == (top + height) && img.cols == (left + width)); + + if (fast) { + if (img.type() == CV_8UC3) { + for (int i = 0; i < height; i++) { + for (int j = 0; j < width; j++) { + uchar colorIdx = (img.at(i, j)[2] & 0xe0) | + ((img.at(i, j)[1] >> 3) & 0x1c) | + ((img.at(i, j)[0] >> 6) & 0x03); + if (criticalTransparency && colorIdx == transparentColor) { + imgCodeStream[i * width + j] = + transparentColor - 4; // 4 means the minimum color change of green channel + } else { + imgCodeStream[i * width + j] = colorIdx; + } + } + } + } else if (img.type() == CV_8UC4) { + for (int i = 0; i < height; i++) { + for (int j = 0; j < width; j++) { + if (img.at(i, j)[3] < criticalTransparency) { + imgCodeStream[i * width + j] = transparentColor; + continue; + } + uchar colorIdx = (img.at(i, j)[2] & 0xe0) | + ((img.at(i, j)[1] >> 3) & 0x1c) | + ((img.at(i, j)[0] >> 6) & 0x03); + if (criticalTransparency && colorIdx == transparentColor) { + imgCodeStream[i * width + j] = + transparentColor - 4; // 4 means the minimum color change of green channel + } else { + imgCodeStream[i * width + j] = colorIdx; + } + } + } + } else { + CV_Assert(false); + } + return true; + } + + // turn the image into the code stream and set the colorNum + CV_Assert(colorNum <= 256 && (colorNum <= localColorTableSize || colorNum <= globalColorTableSize)); + OctreeColorQuant quant = localColorTableSize > 0 ? quantL : quantG; + + if (img.type() == CV_8UC3) { + for(int i = 0; i < height; i++){ + for(int j = 0; j < width; j++){ + // set codeStream + imgCodeStream[i * width + j] = quant.getLeaf(img.at(i, j)[2], + img.at(i, j)[1], + img.at(i, j)[0]); + } + } + } else if (img.type() == CV_8UC4) { + for(int i = 0; i < height; i++){ + for(int j = 0; j < width; j++){ + if (img.at(i, j)[3] < criticalTransparency) { + imgCodeStream[i * width + j] = transparentColor; + continue; + } + imgCodeStream[i * width + j] = quant.getLeaf(img.at(i, j)[2], + img.at(i, j)[1], + img.at(i, j)[0]); + } + } + } else { + CV_Assert(false); + } + return true; +} + +void GifEncoder::getColorTable(const std::vector &img_vec, bool isGlobal) { + // generate the global/local color table (color quantification) + if (img_vec.empty()) return; + CV_Assert(isGlobal || img_vec.size() == 1); + if (fast) { + globalColorTable.resize(colorNum * 3); + for (int i = 0; i < 256; i++) { + globalColorTable[i * 3] = ((i >> 5) & 7) * 36; + globalColorTable[i * 3 + 1] = ((i >> 2) & 7) * 36; + globalColorTable[i * 3 + 2] = (i & 3) * 85; + } + globalColorTable[transparentColor * 3] = transparentRGB[0]; + globalColorTable[transparentColor * 3 + 1] = transparentRGB[1]; + globalColorTable[transparentColor * 3 + 2] = transparentRGB[2]; + return; + } + if (isGlobal) { + quantG = OctreeColorQuant(colorNum, bitDepth, criticalTransparency); + quantG.addMats(img_vec); + globalColorTable.resize(colorNum * 3); + quantG.getPalette(globalColorTable.data()); + } else { + quantL = OctreeColorQuant(colorNum, bitDepth, criticalTransparency); + quantL.addMats(img_vec); + localColorTable.resize(colorNum * 3); + quantL.getPalette(localColorTable.data()); + } +} + +int GifEncoder::ditheringKernel(const Mat &img, Mat &img_, int depth, uchar criticalTransparency) { + int transparentRGB = -1; + if (img.empty()) { + return -1; + } else if (img.type() == CV_8UC3){ + Mat error = Mat::zeros(img.rows + 2, img.cols + 2, CV_32FC3); + int constant_r = 255 / ((1 << ((depth >> 8) & 0xf)) - 1); + int constant_g = 255 / ((1 << ((depth >> 4) & 0xf)) - 1); + int constant_b = 255 / ((1 << ((depth) & 0xf)) - 1); + for (int i = 0; i < img.rows; i++) { + for (int j = 0; j < img.cols; j++) { + Vec3f old_pixel = (Vec3f)img.at(i, j) + error.at(i + 1, j + 1); + Vec3b new_pixel; + new_pixel[0] = (uchar)(std::lround(std::min(std::max(old_pixel[0], 0.0f), 255.0f) / (float)constant_b) * constant_b); + new_pixel[1] = (uchar)(std::lround(std::min(std::max(old_pixel[1], 0.0f), 255.0f) / (float)constant_g) * constant_g); + new_pixel[2] = (uchar)(std::lround(std::min(std::max(old_pixel[2], 0.0f), 255.0f) / (float)constant_r) * constant_r); + img_.at(i, j) = new_pixel; + Vec3f diff = old_pixel - (Vec3f)new_pixel; + error.at(i + 1, j + 2) += diff * 7 / 16; // (i, j + 1) + error.at(i + 2, j) += diff * 3 / 16; // (i + 1, j - 1) + error.at(i + 2, j + 1) += diff * 5 / 16; // (i + 1, j) + error.at(i + 2, j + 2) += diff / 16; // (i + 1, j + 1) + } + } + } else if (img.type() == CV_8UC4) { + Mat error = Mat::zeros(img.rows + 2, img.cols + 2, CV_32FC4); + int constant_r = 255 / ((1 << ((depth >> 8) & 0xf)) - 1); + int constant_g = 255 / ((1 << ((depth >> 4) & 0xf)) - 1); + int constant_b = 255 / ((1 << ((depth) & 0xf)) - 1); + for (int i = 0; i < img.rows; i++) { + for (int j = 0; j < img.cols; j++) { + // transparent color should not be dithered + if (img.at(i, j)[3] < criticalTransparency) { + transparentRGB = (img.at(i, j)[2] << 16) | + (img.at(i, j)[1] << 8) | + (img.at(i, j)[0]); + img_.at(i, j) = img.at(i, j); + continue; + } + Vec4f old_pixel = (Vec4f)img.at(i, j) + error.at(i + 1, j + 1); + Vec4b new_pixel; + new_pixel[0] = (uchar)(std::lround(std::min(std::max(old_pixel[0], 0.0f), 255.0f) / (float)constant_b) * constant_b); + new_pixel[1] = (uchar)(std::lround(std::min(std::max(old_pixel[1], 0.0f), 255.0f) / (float)constant_g) * constant_g); + new_pixel[2] = (uchar)(std::lround(std::min(std::max(old_pixel[2], 0.0f), 255.0f) / (float)constant_r) * constant_r); + new_pixel[3] = img.at(i, j)[3]; + img_.at(i, j) = new_pixel; + Vec4f diff = old_pixel - (Vec4f)new_pixel; + error.at(i + 1, j + 2) += diff * 7 / 16; // (i, j + 1) + error.at(i + 2, j) += diff * 3 / 16; // (i + 1, j - 1) + error.at(i + 2, j + 1) += diff * 5 / 16; // (i + 1, j) + error.at(i + 2, j + 2) += diff / 16; // (i + 1, j + 1) + } + } + } else { + CV_Assert(false); + } + return transparentRGB; +} + +void GifEncoder::close() { + if (strm.isOpened()) { + strm.close(); + } +} + + +////////////////////////////////////////////////////////////////////// +//// Color Quantization //// +////////////////////////////////////////////////////////////////////// +GifEncoder::OctreeColorQuant::OctreeNode::OctreeNode() { + this->isLeaf = false; + level = 0; + index = 0; + for (auto &i: children) { + i = nullptr; + } + leaf = 0, pixelCount = 0; + redSum = greenSum = blueSum = 0; +} + +GifEncoder::OctreeColorQuant::OctreeColorQuant(int maxColors, int bitLength, uchar criticalTransparency) { + m_maxColors = maxColors; + m_bitLength = bitLength; + m_leafCount = criticalTransparency ? 1 : 0; + m_criticalTransparency = criticalTransparency; + root = std::make_shared(); + r = g = b = 0; + for (int i = 0; i < bitLength; i++) { + m_nodeList[i] = std::vector>(); + } +} + +void GifEncoder::OctreeColorQuant::addMat(const Mat &img) { + if (img.empty()) { + return; + } else if (img.type() == CV_8UC3) { + for (int i = 0; i < img.rows; i++) { + for (int j = 0; j < img.cols; j++) { + addColor(img.at(i, j)[2], + img.at(i, j)[1], + img.at(i, j)[0]); + } + } + } else if (img.type() == CV_8UC4) { + for (int i = 0; i < img.rows; i++) { + for (int j = 0; j < img.cols; j++) { + if (img.at(i, j)[3] < m_criticalTransparency) { + r = img.at(i, j)[2]; + g = img.at(i, j)[1]; + b = img.at(i, j)[0]; + continue; + } + addColor(img.at(i, j)[2], + img.at(i, j)[1], + img.at(i, j)[0]); + } + } + } else { + CV_Assert(false); + } +} + +void GifEncoder::OctreeColorQuant::addMats(const std::vector &img_vec) { + for (const auto& img: img_vec) { + addMat(img); + } + if (m_maxColors < m_leafCount) { + reduceTree(); + } +} + +void GifEncoder::OctreeColorQuant::addColor(int red, int green, int blue) { + std::shared_ptr node = root; + for (int level = 0; level < m_bitLength; level++) { + node -> pixelCount++; + node -> redSum += red; + node -> greenSum += green; + node -> blueSum += blue; + if(node -> isLeaf){ + break; + } + int shift = m_bitLength - level; + int index = ((red >> shift) & 1) << 2 | ((green >> shift) & 1) << 1 | ((blue >> shift) & 1); + if (node->children[index] == nullptr) { + node->children[index] = std::make_shared(); + m_nodeList[level].push_back(node->children[index]); + } + node = node->children[index]; + if (level == m_bitLength - 1){ + node -> pixelCount++; + node -> redSum += red; + node -> greenSum += green; + node -> blueSum += blue; + } + } + if (!(node -> isLeaf)) { + m_leafCount++; + node -> isLeaf = true; + } +} + +// return the relative index of the leaf node +uchar GifEncoder::OctreeColorQuant::getLeaf(uchar red, uchar green, uchar blue) { + std::shared_ptr node = root; + for (int level = 0; level <= m_bitLength; level++) { + if (node->isLeaf) { + break; + } + int shift = m_bitLength - level; + int index = ((red >> shift) & 1) << 2 | ((green >> shift) & 1) << 1 | ((blue >> shift) & 1); + if (node->children[index] == nullptr) { + CV_Assert(false); + } + node = node->children[index]; + } + return node->index; +} + +// get the palette +int GifEncoder::OctreeColorQuant::getPalette(uchar* colorTable) { + CV_Assert(colorTable != nullptr); + uchar index = 0; + if (m_criticalTransparency) { + colorTable[index * 3] = r; + colorTable[index * 3 + 1] = g; + colorTable[index * 3 + 2] = b; + index++; + } + for (int i = 0; i < m_bitLength; i++) { + for (const auto& node : m_nodeList[i]) { + if (node -> isLeaf) { + colorTable[index * 3] = (uchar)(node -> redSum / node -> pixelCount); + colorTable[index * 3 + 1] = (uchar)(node -> greenSum / node -> pixelCount); + colorTable[index * 3 + 2] = (uchar)(node -> blueSum / node -> pixelCount); + node -> index = index++; + } + if (index == m_leafCount) { + break; + } + } + } + return m_leafCount; +} + +void GifEncoder::OctreeColorQuant::reduceTree() { + // reduce to max color + int level = 0; + for (int i = 0; i < m_bitLength; i++) { + auto size = (int32_t)m_nodeList[i].size() + 1; + if (m_maxColors < size) { + level = i - 1; + break; + } + } + for (const auto& node : m_nodeList[level + 1]) { + recurseReduce(node); + } + + while(m_maxColors < m_leafCount) { + int minPixelCount = INT_MAX; + std::shared_ptr minNode = nullptr; + for (const auto& node : m_nodeList[level]) { + if (node->pixelCount < minPixelCount && !(node->isLeaf)) { + minPixelCount = node->pixelCount; + minNode = node; + } + } + CV_Assert(minNode != nullptr); + recurseReduce(minNode); + } +} + +void GifEncoder::OctreeColorQuant::recurseReduce(const std::shared_ptr& node) { + // reduce all the children of the node + if (node == nullptr || node->isLeaf) { + return; + } + std::vector> stack; + stack.push_back(node); + + while (!stack.empty()) { + std::shared_ptr child = stack.back(); + stack.pop_back(); + if (child->isLeaf) { + m_leafCount--; + child->isLeaf = false; + } else { + for (int i = 0; i < m_bitLength; i++) { + if (child->children[i] != nullptr) { + stack.push_back(child->children[i]); + } + } + } + } + m_leafCount++; + node -> isLeaf = true; +} + +} // namespace cv2 +#endif diff --git a/modules/imgcodecs/src/grfmt_gif.hpp b/modules/imgcodecs/src/grfmt_gif.hpp new file mode 100644 index 0000000000..284a9cf945 --- /dev/null +++ b/modules/imgcodecs/src/grfmt_gif.hpp @@ -0,0 +1,183 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level +// directory of this distribution and at http://opencv.org/license.html + +#ifndef OPENCV_GRFMT_GIF_HPP +#define OPENCV_GRFMT_GIF_HPP +#ifdef HAVE_IMGCODEC_GIF + +#include "grfmt_base.hpp" + +namespace cv +{ + +enum GifOpMode +{ + GRFMT_GIF_Nothing = 0, + GRFMT_GIF_PreviousImage = 1, + GRFMT_GIF_Background = 2, + GRFMT_GIF_Cover = 3 +}; + +////////////////////////////////////////////////////////////////////// +//// GIF Decoder //// +////////////////////////////////////////////////////////////////////// + +class GifDecoder CV_FINAL : public BaseImageDecoder +{ +public: + GifDecoder(); + ~GifDecoder() CV_OVERRIDE; + + bool readHeader() CV_OVERRIDE; + bool readData(Mat& img) CV_OVERRIDE; + bool nextPage() CV_OVERRIDE; + void close(); + + ImageDecoder newDecoder() const CV_OVERRIDE; + +protected: + RLByteStream m_strm; + + int bgColor; + int depth; + int idx; + + GifOpMode opMode; + bool hasTransparentColor; + uchar transparentColor; + int top, left, width, height; + + bool hasRead; + std::vector globalColorTable; + std::vector localColorTable; + + int lzwMinCodeSize; + int globalColorTableSize; + int localColorTableSize; + + Mat lastImage; + std::vector imgCodeStream; + + struct lzwNodeD + { + int length; + uchar suffix; + std::vector prefix; + }; + + void readExtensions(); + void code2pixel(Mat& img, int start, int k); + bool lzwDecode(); + bool getFrameCount_(); + bool skipHeader(); +}; + + + +////////////////////////////////////////////////////////////////////// +//// GIF Encoder //// +////////////////////////////////////////////////////////////////////// +class GifEncoder CV_FINAL : public BaseImageEncoder { +public: + GifEncoder(); + ~GifEncoder() CV_OVERRIDE; + + bool isFormatSupported(int depth) const CV_OVERRIDE; + + bool write(const Mat& img, const std::vector& params) CV_OVERRIDE; + + bool writemulti(const std::vector& img_vec, + const std::vector& params) CV_OVERRIDE; + + ImageEncoder newEncoder() const CV_OVERRIDE; + +private: +/** Color Quantization **/ + class OctreeColorQuant + { + struct OctreeNode + { + bool isLeaf; + std::shared_ptr children[8]{}; + int level; + uchar index; + int leaf; + int pixelCount; + size_t redSum, greenSum, blueSum; + + OctreeNode(); + }; + + std::shared_ptr root; + std::vector> m_nodeList[8]; + int32_t m_bitLength; + int32_t m_maxColors; + int32_t m_leafCount; + uchar m_criticalTransparency; + uchar r, g, b; // color under transparent color + + public: + explicit OctreeColorQuant(int maxColors = 256, int bitLength = 8, uchar criticalTransparency = 1); + + int getPalette(uchar* colorTable); + uchar getLeaf(uchar red, uchar green, uchar blue); + + void addMat(const Mat& img); + void addMats(const std::vector& img_vec); + void addColor(int red, int green, int blue); + void reduceTree(); + void recurseReduce(const std::shared_ptr& node); + }; + + enum GifDithering // normal dithering level is -1 to 2 + { + GRFMT_GIF_None = 3, + GRFMT_GIF_FloydSteinberg = 4 + }; + + WLByteStream strm; + int m_width, m_height; + + int globalColorTableSize; + int localColorTableSize; + + uchar opMode; + uchar criticalTransparency; + uchar transparentColor; + Vec3b transparentRGB; + int top, left, width, height; + + OctreeColorQuant quantG; + OctreeColorQuant quantL; + + std::vector lzwTable; + std::vector imgCodeStream; + + std::vector globalColorTable; + std::vector localColorTable; + + // params + int loopCount; + int frameDelay; + int colorNum; + int bitDepth; + int dithering; + int lzwMinCodeSize, lzwMaxCodeSize; + bool fast; + + bool writeFrames(const std::vector& img_vec, const std::vector& params); + bool writeHeader(const std::vector& img_vec); + bool writeFrame(const Mat& img); + bool pixel2code(const Mat& img); + void getColorTable(const std::vector& img_vec, bool isGlobal); + static int ditheringKernel(const Mat &img, Mat &img_, int depth, uchar transparency); + bool lzwEncode(); + void close(); +}; + + +} // namespace cv + +#endif // HAVE_IMGCODEC_GIF +#endif //OPENCV_GRFMT_GIF_HPP diff --git a/modules/imgcodecs/src/grfmts.hpp b/modules/imgcodecs/src/grfmts.hpp index 46b79ff96c..198588630c 100644 --- a/modules/imgcodecs/src/grfmts.hpp +++ b/modules/imgcodecs/src/grfmts.hpp @@ -45,6 +45,7 @@ #include "grfmt_base.hpp" #include "grfmt_avif.hpp" #include "grfmt_bmp.hpp" +#include "grfmt_gif.hpp" #include "grfmt_sunras.hpp" #include "grfmt_jpeg.hpp" #include "grfmt_pxm.hpp" diff --git a/modules/imgcodecs/src/loadsave.cpp b/modules/imgcodecs/src/loadsave.cpp index b057b35a9f..25f32a789c 100644 --- a/modules/imgcodecs/src/loadsave.cpp +++ b/modules/imgcodecs/src/loadsave.cpp @@ -151,14 +151,18 @@ struct ImageCodecInitializer */ ImageCodecInitializer() { -#ifdef HAVE_AVIF - decoders.push_back(makePtr()); - encoders.push_back(makePtr()); -#endif /// BMP Support decoders.push_back( makePtr() ); encoders.push_back( makePtr() ); + #ifdef HAVE_IMGCODEC_GIF + decoders.push_back( makePtr() ); + encoders.push_back( makePtr() ); + #endif + #ifdef HAVE_AVIF + decoders.push_back(makePtr()); + encoders.push_back(makePtr()); + #endif #ifdef HAVE_IMGCODEC_HDR decoders.push_back( makePtr() ); encoders.push_back( makePtr() ); diff --git a/modules/imgcodecs/test/test_gif.cpp b/modules/imgcodecs/test/test_gif.cpp new file mode 100644 index 0000000000..b9c49c0bdb --- /dev/null +++ b/modules/imgcodecs/test/test_gif.cpp @@ -0,0 +1,357 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html +#include "test_precomp.hpp" + +#ifdef HAVE_IMGCODEC_GIF + +namespace opencv_test { namespace { + +const string gifsuite_files_multi[]={ + "basi3p01", + "basi3p02", + "basi3p04", + "basn3p01", + "basn3p02", + "basn3p04", + "ccwn3p08", + "ch1n3p04", + "cs3n3p08", + "cs5n3p08", + "cs8n3p08", + "g03n3p04", + "g04n3p04", + "g05n3p04", + "g07n3p04", + "g10n3p04", + "g25n3p04", + "s32i3p04", + "s32n3p04", + "tp0n3p08", +}; + +const string gifsuite_files_read_single[] = { + "basi3p01", + "basi3p02", + "basi3p04", + "basn3p01", + "basn3p02", + "basn3p04", + "ccwn3p08", + "cdfn2c08", + "cdhn2c08", + "cdsn2c08", + "cdun2c08", + "ch1n3p04", + "cs3n3p08", + "cs5n2c08", + "cs5n3p08", + "cs8n2c08", + "cs8n3p08", + "exif2c08", + "g03n2c08", + "g03n3p04", + "g04n2c08", + "g04n3p04", + "g05n2c08", + "g05n3p04", + "g07n2c08", + "g07n3p04", + "g10n2c08", + "g10n3p04" +}; + +const string gifsuite_files_read_write_suite[]={ + "g25n2c08", + "g25n3p04", + "s01i3p01", + "s01n3p01", + "s02i3p01", + "s02n3p01", + "s03i3p01", + "s03n3p01", + "s04i3p01", + "s04n3p01", + "s05i3p02", + "s05n3p02", + "s06i3p02", + "s06n3p02", + "s07i3p02", + "s07n3p02", + "s08i3p02", + "s08n3p02", + "s09i3p02", + "s09n3p02", + "s32i3p04", + "s32n3p04", + "s33i3p04", + "s33n3p04", + "s34i3p04", + "s34n3p04", + "s35i3p04", + "s35n3p04", + "s36i3p04", + "s36n3p04", + "s37i3p04", + "s37n3p04", + "s38i3p04", + "s38n3p04", + "s39i3p04", + "s39n3p04", + "s40i3p04", + "s40n3p04", + "tp0n3p08", +}; + +const std::pair gifsuite_files_bgra[]={ + make_pair("gif_bgra1",53287), + make_pair("gif_bgra2",52651), + make_pair("gif_bgra3",54809), + make_pair("gif_bgra4",57562), + make_pair("gif_bgra5",56733), + make_pair("gif_bgra6",52110), +}; + +TEST(Imgcodecs_Gif, read_gif_multi) +{ + const string root = cvtest::TS::ptr()->get_data_path(); + const string filename = root + "gifsuite/gif_multi.gif"; + vector img_vec_8UC4; + ASSERT_NO_THROW(cv::imreadmulti(filename, img_vec_8UC4,0,20,IMREAD_UNCHANGED)); + EXPECT_EQ(img_vec_8UC4.size(), imcount(filename)); + vector img_vec_8UC3; + for(const auto & i : img_vec_8UC4){ + cv::Mat img_tmp; + cvtColor(i,img_tmp,COLOR_BGRA2BGR); + img_vec_8UC3.push_back(img_tmp); + } + const long unsigned int expected_size=20; + EXPECT_EQ(img_vec_8UC3.size(),expected_size); + for(long unsigned int i=0;i Imgcodecs_Gif_GifSuite_SingleFrame; + +TEST_P(Imgcodecs_Gif_GifSuite_SingleFrame, read_gif_single) +{ + const string root = cvtest::TS::ptr()->get_data_path(); + const string filename = root + "gifsuite/" + GetParam() + ".gif"; + const string png_filename=root + "pngsuite/" + GetParam() + ".png"; + const long unsigned int expected_size = 1; + + EXPECT_EQ(expected_size, imcount(filename)); + cv::Mat img_8UC4; + ASSERT_NO_THROW(img_8UC4 = cv::imread(filename, IMREAD_UNCHANGED)); + ASSERT_FALSE(img_8UC4.empty()); + cv::Mat img_8UC3; + ASSERT_NO_THROW(cvtColor(img_8UC4, img_8UC3, COLOR_BGRA2BGR)); + cv::Mat img_png; + ASSERT_NO_THROW(img_png = cv::imread(png_filename, IMREAD_UNCHANGED)); + ASSERT_FALSE(img_png.empty()); + EXPECT_PRED_FORMAT2(cvtest::MatComparator(0, 0), img_8UC3, img_png); +} + +INSTANTIATE_TEST_CASE_P(/*nothing*/, Imgcodecs_Gif_GifSuite_SingleFrame, + testing::ValuesIn(gifsuite_files_read_single)); + +TEST(Imgcodecs_Gif, read_gif_big){ + const string root = cvtest::TS::ptr()->get_data_path(); + const string gif_filename = root + "gifsuite/gif_big.gif"; + const string png_filename = root + "gifsuite/gif_big.png"; + cv::Mat img_8UC4; + ASSERT_NO_THROW(img_8UC4 = imread(gif_filename, IMREAD_UNCHANGED)); + ASSERT_FALSE(img_8UC4.empty()); + cv::Mat img_8UC3; + const int expected_col=1303; + const int expected_row=1391; + EXPECT_EQ(expected_col, img_8UC4.cols); + EXPECT_EQ(expected_row, img_8UC4.rows); + ASSERT_NO_THROW(cvtColor(img_8UC4, img_8UC3,COLOR_BGRA2BGR)); + EXPECT_EQ(expected_col, img_8UC3.cols); + EXPECT_EQ(expected_row, img_8UC3.rows); + cv::Mat img_png; + ASSERT_NO_THROW(img_png=imread(png_filename, IMREAD_UNCHANGED)); + ASSERT_FALSE(img_png.empty()); + cv::Mat img_png_8UC3; + ASSERT_NO_THROW(cvtColor(img_png,img_png_8UC3, COLOR_BGRA2BGR)); + EXPECT_EQ(img_8UC3.size, img_png_8UC3.size); + EXPECT_PRED_FORMAT2(cvtest::MatComparator(0, 0), img_8UC3, img_png_8UC3); +} + +typedef testing::TestWithParam> Imgcodecs_Gif_GifSuite_SingleFrame_BGRA; + +TEST_P(Imgcodecs_Gif_GifSuite_SingleFrame_BGRA, read_gif_single_bgra){ + const string root = cvtest::TS::ptr()->get_data_path(); + const string gif_filename = root + "gifsuite/" + GetParam().first + ".gif"; + const string png_filename = root + "gifsuite/" + GetParam().first + ".png"; + cv::Mat gif_img; + cv::Mat png_img; + ASSERT_NO_THROW(gif_img = cv::imread(gif_filename, IMREAD_UNCHANGED)); + ASSERT_FALSE(gif_img.empty()); + ASSERT_NO_THROW(png_img = cv::imread(png_filename, IMREAD_UNCHANGED)); + ASSERT_FALSE(png_img.empty()); + EXPECT_PRED_FORMAT2(cvtest::MatComparator(0, 0), gif_img, png_img); + int transparent_count = 0; + for(int i=0; i(i,j); + if((int)(pixel1[3]) == 0){ + transparent_count++; + } + } + } + EXPECT_EQ(transparent_count,GetParam().second); +} + +INSTANTIATE_TEST_CASE_P(/*nothing*/, Imgcodecs_Gif_GifSuite_SingleFrame_BGRA , + testing::ValuesIn(gifsuite_files_bgra)); + +TEST(Imgcodecs_Gif,read_gif_multi_bgra){ + const string root = cvtest::TS::ptr()->get_data_path(); + const string gif_filename = root + "gifsuite/gif_multi_bgra.gif"; + vector img_vec; + ASSERT_NO_THROW(cv::imreadmulti(gif_filename, img_vec, IMREAD_UNCHANGED)); + EXPECT_EQ(imcount(gif_filename), img_vec.size()); + const int fixed_transparent_count = 53211; + for(auto & frame_count : img_vec){ + int transparent_count=0; + for(int i=0; i(i,j); + if((int)(pixel1[3]) == 0){ + transparent_count++; + } + } + } + EXPECT_EQ(fixed_transparent_count,transparent_count); + } +} + +TEST(Imgcodecs_Gif, read_gif_special){ + const string root = cvtest::TS::ptr()->get_data_path(); + const string gif_filename1 = root + "gifsuite/special1.gif"; + const string png_filename1 = root + "gifsuite/special1.png"; + const string gif_filename2 = root + "gifsuite/special2.gif"; + const string png_filename2 = root + "gifsuite/special2.png"; + cv::Mat gif_img1; + ASSERT_NO_THROW(gif_img1 = cv::imread(gif_filename1,IMREAD_UNCHANGED)); + ASSERT_FALSE(gif_img1.empty()); + cv::Mat png_img1; + ASSERT_NO_THROW(png_img1 = cv::imread(png_filename1,IMREAD_UNCHANGED)); + ASSERT_FALSE(png_img1.empty()); + EXPECT_PRED_FORMAT2(cvtest::MatComparator(0, 0), gif_img1, png_img1); + cv::Mat gif_img2; + ASSERT_NO_THROW(gif_img2 = cv::imread(gif_filename2,IMREAD_UNCHANGED)); + ASSERT_FALSE(gif_img2.empty()); + cv::Mat png_img2; + ASSERT_NO_THROW(png_img2 = cv::imread(png_filename2,IMREAD_UNCHANGED)); + ASSERT_FALSE(png_img2.empty()); + EXPECT_PRED_FORMAT2(cvtest::MatComparator(0, 0), gif_img2, png_img2); +} + +TEST(Imgcodecs_Gif,write_gif_flags){ + const string root = cvtest::TS::ptr()->get_data_path(); + const string png_filename = root + "gifsuite/special1.png"; + vector buff; + const int expected_rows=611; + const int expected_cols=293; + Mat img_gt = Mat::ones(expected_rows, expected_cols, CV_8UC1); + vector param; + param.push_back(IMWRITE_GIF_QUALITY); + param.push_back(7); + param.push_back(IMWRITE_GIF_DITHER); + param.push_back(2); + EXPECT_NO_THROW(imencode(".png", img_gt, buff, param)); + Mat img; + EXPECT_NO_THROW(img = imdecode(buff, IMREAD_ANYDEPTH)); // hang + EXPECT_FALSE(img.empty()); + EXPECT_EQ(img.cols, expected_cols); + EXPECT_EQ(img.rows, expected_rows); + EXPECT_PRED_FORMAT2(cvtest::MatComparator(0, 0), img, img_gt); +} + +TEST(Imgcodecs_Gif, write_gif_big) { + const string root = cvtest::TS::ptr()->get_data_path(); + const string png_filename = root + "gifsuite/gif_big.png"; + const string gif_filename = cv::tempfile(".png"); + cv::Mat img; + ASSERT_NO_THROW(img = cv::imread(png_filename, IMREAD_UNCHANGED)); + ASSERT_FALSE(img.empty()); + EXPECT_EQ(1303, img.cols); + EXPECT_EQ(1391, img.rows); + ASSERT_NO_THROW(imwrite(gif_filename, img)); + cv::Mat img_gif; + ASSERT_NO_THROW(img_gif = cv::imread(gif_filename, IMREAD_UNCHANGED)); + ASSERT_FALSE(img_gif.empty()); + EXPECT_EQ(1303, img_gif.cols); + EXPECT_EQ(1391, img_gif.rows); + EXPECT_EQ(0, remove(gif_filename.c_str())); +} + +typedef testing::TestWithParam Imgcodecs_Gif_GifSuite_Read_Write_Suite; + +TEST_P(Imgcodecs_Gif_GifSuite_Read_Write_Suite ,read_gif_single) +{ + const string root = cvtest::TS::ptr()->get_data_path(); + const string png_filename = root + "pngsuite/"+GetParam()+".png"; + const string gif_filename = cv::tempfile(".gif"); + cv::Mat img; + ASSERT_NO_THROW(img = cv::imread(png_filename, IMREAD_UNCHANGED)); + ASSERT_FALSE(img.empty()); + vector param; + param.push_back(IMWRITE_GIF_QUALITY); + param.push_back(8); + param.push_back(IMWRITE_GIF_DITHER); + param.push_back(3); + ASSERT_NO_THROW(imwrite(gif_filename, img, param)); + cv::Mat img_gif; + ASSERT_NO_THROW(img_gif = cv::imread(gif_filename, IMREAD_UNCHANGED)); + ASSERT_FALSE(img_gif.empty()); + cv::Mat img_8UC3; + ASSERT_NO_THROW(cv::cvtColor(img_gif, img_8UC3, COLOR_BGRA2BGR)); + EXPECT_PRED_FORMAT2(cvtest::MatComparator(29, 0), img, img_8UC3); + EXPECT_EQ(0, remove(gif_filename.c_str())); +} + +INSTANTIATE_TEST_CASE_P(/*nothing*/, Imgcodecs_Gif_GifSuite_Read_Write_Suite , + testing::ValuesIn(gifsuite_files_read_write_suite)); + +TEST(Imgcodecs_Gif, write_gif_multi) { + const string root = cvtest::TS::ptr()->get_data_path(); + const string gif_filename = cv::tempfile(".gif"); + vector img_vec; + for (long unsigned int i = 0; i < 20; i++) { + const string png_filename = root + "pngsuite/" + gifsuite_files_multi[i] + ".png"; + cv::Mat img; + ASSERT_NO_THROW(img = cv::imread(png_filename, IMREAD_UNCHANGED)); + ASSERT_FALSE(img.empty()); + img_vec.push_back(img); + } + vector param; + param.push_back(IMWRITE_GIF_QUALITY); + param.push_back(8); + param.push_back(IMWRITE_GIF_DITHER); + param.push_back(3); + ASSERT_NO_THROW(cv::imwritemulti(gif_filename, img_vec, param)); + vector img_vec_gif; + ASSERT_NO_THROW(cv::imreadmulti(gif_filename, img_vec_gif)); + EXPECT_EQ(img_vec.size(), img_vec_gif.size()); + for (long unsigned int i = 0; i < img_vec.size(); i++) { + cv::Mat img_8UC3; + ASSERT_NO_THROW(cv::cvtColor(img_vec_gif[i], img_8UC3, COLOR_BGRA2BGR)); + EXPECT_PRED_FORMAT2(cvtest::MatComparator(29, 0), img_vec[i], img_8UC3); + } + EXPECT_EQ(0, remove(gif_filename.c_str())); +} + +}//opencv_test +}//namespace + +#endif