opencv/modules/imgcodecs/src/grfmt_gif.cpp
2025-01-03 14:29:18 +03:00

1147 lines
41 KiB
C++

// 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_animation.durations.push_back(m_strm.getWord() * 10); // delay time
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<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) {
img.at<Vec4b>(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<Vec4b>(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<Vec4b>(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<lzwNodeD> 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<GifDecoder>();
}
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();
if (len == 3 && m_strm.getByte() == 1)
{
m_animation.loop_count = m_strm.getWord();
}
}
} 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<int> &params) {
std::vector<Mat> img_vec(1, img);
return writemulti(img_vec, params);
}
bool GifEncoder::writeanimation(const Animation& animation, const std::vector<int>& params) {
if (animation.frames.empty()) {
return false;
}
if (m_buf) {
if (!strm.open(*m_buf)) {
return false;
}
} else if (!strm.open(m_filename)) {
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
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<Mat> img_vec_;
if (fast) {
const uchar transparent = 0x92; // 1001_0010: the middle of the color table
if (dithering == GRFMT_GIF_None) {
img_vec_ = animation.frames;
transparentColor = transparent;
} else {
localColorTableSize = 0;
int transRGB;
const int depth = 3 << 8 | 3 << 4 | 2; // r:g:b = 3:3:2
for (auto &img: animation.frames) {
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 : animation.frames) {
Mat img_(img.size(), img.type());
ditheringKernel(img, img_, depth, criticalTransparency);
img_vec_.push_back(img_);
}
} else {
img_vec_ = animation.frames;
}
bool result = writeHeader(img_vec_);
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]);
}
strm.putByte(0x3B); // trailer
strm.close();
return result;
}
ImageEncoder GifEncoder::newEncoder() const {
return makePtr<GifEncoder>();
}
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<Mat> 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<Mat>& 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<Vec3b>(i, j)[2] & 0xe0) |
((img.at<Vec3b>(i, j)[1] >> 3) & 0x1c) |
((img.at<Vec3b>(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<Vec4b>(i, j)[3] < criticalTransparency) {
imgCodeStream[i * width + j] = transparentColor;
continue;
}
uchar colorIdx = (img.at<Vec4b>(i, j)[2] & 0xe0) |
((img.at<Vec4b>(i, j)[1] >> 3) & 0x1c) |
((img.at<Vec4b>(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<Vec3b>(i, j)[2],
img.at<Vec3b>(i, j)[1],
img.at<Vec3b>(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<Vec4b>(i, j)[3] < criticalTransparency) {
imgCodeStream[i * width + j] = transparentColor;
continue;
}
imgCodeStream[i * width + j] = quant.getLeaf(img.at<Vec4b>(i, j)[2],
img.at<Vec4b>(i, j)[1],
img.at<Vec4b>(i, j)[0]);
}
}
} else {
CV_Assert(false);
}
return true;
}
void GifEncoder::getColorTable(const std::vector<Mat> &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<Vec3b>(i, j) + error.at<Vec3f>(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<Vec3b>(i, j) = new_pixel;
Vec3f diff = old_pixel - (Vec3f)new_pixel;
error.at<Vec3f>(i + 1, j + 2) += diff * 7 / 16; // (i, j + 1)
error.at<Vec3f>(i + 2, j) += diff * 3 / 16; // (i + 1, j - 1)
error.at<Vec3f>(i + 2, j + 1) += diff * 5 / 16; // (i + 1, j)
error.at<Vec3f>(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<Vec4b>(i, j)[3] < criticalTransparency) {
transparentRGB = (img.at<Vec4b>(i, j)[2] << 16) |
(img.at<Vec4b>(i, j)[1] << 8) |
(img.at<Vec4b>(i, j)[0]);
img_.at<Vec4b>(i, j) = img.at<Vec4b>(i, j);
continue;
}
Vec4f old_pixel = (Vec4f)img.at<Vec4b>(i, j) + error.at<Vec4f>(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<Vec4b>(i, j)[3];
img_.at<Vec4b>(i, j) = new_pixel;
Vec4f diff = old_pixel - (Vec4f)new_pixel;
error.at<Vec4f>(i + 1, j + 2) += diff * 7 / 16; // (i, j + 1)
error.at<Vec4f>(i + 2, j) += diff * 3 / 16; // (i + 1, j - 1)
error.at<Vec4f>(i + 2, j + 1) += diff * 5 / 16; // (i + 1, j)
error.at<Vec4f>(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<OctreeNode>();
r = g = b = 0;
for (int i = 0; i < bitLength; i++) {
m_nodeList[i] = std::vector<std::shared_ptr<OctreeNode>>();
}
}
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<Vec3b>(i, j)[2],
img.at<Vec3b>(i, j)[1],
img.at<Vec3b>(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<Vec4b>(i, j)[3] < m_criticalTransparency) {
r = img.at<Vec4b>(i, j)[2];
g = img.at<Vec4b>(i, j)[1];
b = img.at<Vec4b>(i, j)[0];
continue;
}
addColor(img.at<Vec4b>(i, j)[2],
img.at<Vec4b>(i, j)[1],
img.at<Vec4b>(i, j)[0]);
}
}
} else {
CV_Assert(false);
}
}
void GifEncoder::OctreeColorQuant::addMats(const std::vector<Mat> &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<OctreeNode> 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<OctreeNode>();
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<OctreeNode> 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<OctreeNode> 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<OctreeNode>& node) {
// reduce all the children of the node
if (node == nullptr || node->isLeaf) {
return;
}
std::vector<std::shared_ptr<OctreeNode>> stack;
stack.push_back(node);
while (!stack.empty()) {
std::shared_ptr<OctreeNode> 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