add fast mode for encoder by avoid octree color quantization

This commit is contained in:
redhecker 2024-10-07 20:45:58 +08:00
parent 52c88bc15d
commit 6ea52ddb35
3 changed files with 168 additions and 57 deletions

View File

@ -114,10 +114,10 @@ enum ImwriteFlags {
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 8.
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.
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 {
@ -222,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.

View File

@ -465,9 +465,11 @@ GifEncoder::GifEncoder() {
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
@ -487,27 +489,15 @@ bool GifEncoder::isFormatSupported(int depth) const {
}
bool GifEncoder::write(const Mat &img, const std::vector<int> &params) {
std::vector<Mat> img_vec;
Mat img_;
img.copyTo(img_);
img_vec.push_back(img_);
std::vector<Mat> img_vec(1, img);
return writeFrames(img_vec, params);
}
bool GifEncoder::writemulti(const std::vector<Mat> &img_vec, const std::vector<int> &params) {
std::vector<Mat> img_vec_;
for (const auto &img : img_vec) {
if (img.empty()) {
return false;
}
Mat img_;
img.copyTo(img_);
img_vec_.push_back(img_);
}
return writeFrames(img_vec_, params);
return writeFrames(img_vec, params);
}
bool GifEncoder::writeFrames(std::vector<Mat>& img_vec,
bool GifEncoder::writeFrames(const std::vector<Mat>& img_vec,
const std::vector<int>& params) {
if (img_vec.empty()) {
return false;
@ -530,13 +520,9 @@ bool GifEncoder::writeFrames(std::vector<Mat>& img_vec,
case IMWRITE_GIF_SPEED:
frameDelay = 100 - std::min(std::max(params[i + 1] - 1, 0), 99); // from 10ms to 1000ms
break;
case IMWRITE_GIF_QUALITY:
lzwMinCodeSize = std::min(std::max(params[i + 1], 3), 8);
colorNum = 1 << lzwMinCodeSize;
globalColorTableSize = colorNum;
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);
@ -544,6 +530,24 @@ bool GifEncoder::writeFrames(std::vector<Mat>& img_vec,
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) {
@ -551,32 +555,55 @@ bool GifEncoder::writeFrames(std::vector<Mat>& img_vec,
colorNum = 1 << lzwMinCodeSize;
globalColorTableSize = colorNum;
}
localColorTableSize = localColorTableSize ? colorNum : 0;
if (dithering != 3) {
for (auto &img : img_vec) {
int depth = (int)ceil(log2(colorNum) / 3) + dithering;
ditheringKernel(img, depth, criticalTransparency);
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_ = 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);
bool result = writeHeader(img_vec_);
if (!result) {
strm.close();
return false;
}
for (const auto &img : img_vec) {
for (const auto &img : img_vec_) {
result = writeFrame(img);
if (!result) {
strm.close();
if (!m_buf)
remove(m_filename.c_str());
return false;
}
}
strm.putByte(0x3B); // trailer
strm.close();
return true;
return result;
}
ImageEncoder GifEncoder::newEncoder() const {
@ -610,8 +637,7 @@ bool GifEncoder::writeFrame(const Mat &img) {
strm.putWord(height);
flag = localColorTableSize > 0 ? 0x80 : 0x00;
if (localColorTableSize > 0) {
std::vector<Mat> img_vec;
img_vec.push_back(img);
std::vector<Mat> img_vec(1, img);
getColorTable(img_vec, false);
}
flag |= lzwMinCodeSize - 1;
@ -636,10 +662,10 @@ bool GifEncoder::lzwEncode() {
lzwTable.allocate((1 << 12) * 256);
// clear lzwTable
memset(lzwTable.data(), 0, (1 << 12) * 256 * sizeof(int));
memset(lzwTable.data(), 0, (1 << 20) * sizeof(int16_t)); // 20 = 12 + 8 = 2^12(max lzw table size) * 256
// next code
int idx = (1 << lzwMinCodeSize) + 2;
auto idx = (int16_t)((1 << lzwMinCodeSize) + 2);
int bufferLen = 0;
uchar buffer[256];
@ -676,9 +702,9 @@ bool GifEncoder::lzwEncode() {
if(idx == (1 << lzwMaxCodeSize)){
output |= (((size_t)1 << lzwMinCodeSize) << bitLeft);
bitLeft += lzwCodeSize;
memset(lzwTable.data(), 0, (1 << 12) * 256 * sizeof(int));
memset(lzwTable.data(), 0, (1 << 20) * sizeof(int16_t)); // clear lzwTable
// next code
idx = (1 << lzwMinCodeSize) + 2;
idx = (int16_t)((1 << lzwMinCodeSize) + 2);
lzwCodeSize = lzwMinCodeSize + 1;
}
} else{
@ -757,6 +783,45 @@ 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;
@ -792,6 +857,18 @@ 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.allocate(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);
@ -799,48 +876,61 @@ void GifEncoder::getColorTable(const std::vector<Mat> &img_vec, bool isGlobal) {
quantG.getPalette(globalColorTable.data());
} else {
quantL = OctreeColorQuant(colorNum, bitDepth, criticalTransparency);
quantL.addMat(img_vec[0]);
quantL.addMats(img_vec);
localColorTable.allocate(colorNum * 3);
quantL.getPalette(localColorTable.data());
}
}
void GifEncoder::ditheringKernel(Mat &img, int depth, uchar criticalTransparency) {
int GifEncoder::ditheringKernel(const Mat &img, Mat &img_, int depth, uchar criticalTransparency) {
int transparentRGB = -1;
if (img.empty()) {
return;
return -1;
} else if (img.type() == CV_8UC3){
Mat error = Mat::zeros(img.rows + 2, img.cols + 2, CV_32FC3);
int constant = (1 << (9 - depth)) - 1;
Vec3f bias = Vec3f(0.5, 0.5, 0.5);
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 = (Vec3b)(old_pixel / constant + bias) * constant;
img.at<Vec3b>(i, j) = new_pixel;
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) += 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 = (1 << (9 - depth)) - 1;
Vec4f bias = Vec4f(0.5, 0.5, 0.5, 0.5);
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 = (Vec4b)(old_pixel / constant + bias) * constant;
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;
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) += 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)
}
@ -848,6 +938,7 @@ void GifEncoder::ditheringKernel(Mat &img, int depth, uchar criticalTransparency
} else {
CV_Assert(false);
}
return transparentRGB;
}
void GifEncoder::close() {

View File

@ -130,6 +130,12 @@ private:
void recurseReduce(const std::shared_ptr<OctreeNode>& 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;
@ -139,12 +145,13 @@ private:
uchar opMode;
uchar criticalTransparency;
uchar transparentColor;
Vec3b transparentRGB;
int top, left, width, height;
OctreeColorQuant quantG;
OctreeColorQuant quantL;
AutoBuffer<int> lzwTable;
AutoBuffer<int16_t> lzwTable;
AutoBuffer<uchar> imgCodeStream;
AutoBuffer<uchar> globalColorTable;
@ -157,13 +164,14 @@ private:
int bitDepth;
int dithering;
int lzwMinCodeSize, lzwMaxCodeSize;
bool fast;
bool writeFrames(std::vector<Mat>& img_vec, const std::vector<int>& params);
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 pixel2code(const Mat& img);
void getColorTable(const std::vector<Mat>& img_vec, bool isGlobal);
static void ditheringKernel(Mat &img, int depth, uchar transparency);
static int ditheringKernel(const Mat &img, Mat &img_, int depth, uchar transparency);
bool lzwEncode();
void close();
};