gif2webp: Add a mixed compression mode
When '-mixed' option is given, each frame would be heuristically chosen to be encoded using lossy or lossless compression. The heuristic is based on the number of colors in the image: - If num_colors <= 31, pick lossless compression - If num_colors >= 194, pick lossy compression - Otherwise, try both and pick the one that compresses better. Change-Id: I908c73493ddc38e8db35b7b1959300569e6d3a97
This commit is contained in:
parent
87cffcc3c9
commit
73f52133a1
@ -212,6 +212,8 @@ static void Help(void) {
|
||||
printf("options:\n");
|
||||
printf(" -h / -help ............ this help\n");
|
||||
printf(" -lossy ................. Encode image using lossy compression.\n");
|
||||
printf(" -mixed ................. For each frame in the image, pick lossy\n"
|
||||
" or lossless compression heuristically.\n");
|
||||
printf(" -q <float> ............. quality factor (0:small..100:big)\n");
|
||||
printf(" -m <int> ............... compression method (0=fast, 6=slowest)\n");
|
||||
printf(" -kmin <int> ............ Min distance between key frames\n");
|
||||
@ -253,6 +255,7 @@ int main(int argc, const char *argv[]) {
|
||||
int default_kmax = 1;
|
||||
size_t kmin = 0;
|
||||
size_t kmax = 0;
|
||||
int allow_mixed = 0; // If true, each frame can be lossy or lossless.
|
||||
|
||||
memset(&info, 0, sizeof(info));
|
||||
info.id = WEBP_CHUNK_ANMF;
|
||||
@ -279,6 +282,9 @@ int main(int argc, const char *argv[]) {
|
||||
out_file = argv[++c];
|
||||
} else if (!strcmp(argv[c], "-lossy")) {
|
||||
config.lossless = 0;
|
||||
} else if (!strcmp(argv[c], "-mixed")) {
|
||||
allow_mixed = 1;
|
||||
config.lossless = 0;
|
||||
} else if (!strcmp(argv[c], "-q") && c < argc - 1) {
|
||||
config.quality = (float)strtod(argv[++c], NULL);
|
||||
} else if (!strcmp(argv[c], "-m") && c < argc - 1) {
|
||||
@ -348,7 +354,7 @@ int main(int argc, const char *argv[]) {
|
||||
if (!WebPPictureAlloc(&frame)) goto End;
|
||||
|
||||
// Initialize cache
|
||||
cache = WebPFrameCacheNew(frame.width, frame.height, kmin, kmax);
|
||||
cache = WebPFrameCacheNew(frame.width, frame.height, kmin, kmax, allow_mixed);
|
||||
if (cache == NULL) goto End;
|
||||
|
||||
mux = WebPMuxNew();
|
||||
|
@ -267,6 +267,7 @@ struct WebPFrameCache {
|
||||
size_t kmin; // Min distance between key frames.
|
||||
size_t kmax; // Max distance between key frames.
|
||||
size_t count_since_key_frame; // Frames seen since the last key frame.
|
||||
int allow_mixed; // If true, each frame can be lossy or lossless.
|
||||
WebPPicture prev_canvas; // Previous canvas (properly disposed).
|
||||
WebPPicture curr_canvas; // Current canvas (temporary buffer).
|
||||
int is_first_frame; // True if no frames have been added to the cache
|
||||
@ -284,7 +285,7 @@ static void CacheReset(WebPFrameCache* const cache) {
|
||||
}
|
||||
|
||||
WebPFrameCache* WebPFrameCacheNew(int width, int height,
|
||||
size_t kmin, size_t kmax) {
|
||||
size_t kmin, size_t kmax, int allow_mixed) {
|
||||
WebPFrameCache* cache = (WebPFrameCache*)malloc(sizeof(*cache));
|
||||
if (cache == NULL) return NULL;
|
||||
CacheReset(cache);
|
||||
@ -305,6 +306,7 @@ WebPFrameCache* WebPFrameCacheNew(int width, int height,
|
||||
WebPUtilClearPic(&cache->prev_canvas, NULL);
|
||||
|
||||
// Cache data.
|
||||
cache->allow_mixed = allow_mixed;
|
||||
cache->kmin = kmin;
|
||||
cache->kmax = kmax;
|
||||
cache->count_since_key_frame = 0;
|
||||
@ -335,20 +337,151 @@ void WebPFrameCacheDelete(WebPFrameCache* const cache) {
|
||||
}
|
||||
|
||||
static int EncodeFrame(const WebPConfig* const config, WebPPicture* const pic,
|
||||
WebPData* const encoded_data) {
|
||||
WebPMemoryWriter memory;
|
||||
WebPMemoryWriter* const memory) {
|
||||
pic->use_argb = 1;
|
||||
pic->writer = WebPMemoryWrite;
|
||||
pic->custom_ptr = &memory;
|
||||
WebPMemoryWriterInit(&memory);
|
||||
pic->custom_ptr = memory;
|
||||
if (!WebPEncode(config, pic)) {
|
||||
return 0;
|
||||
}
|
||||
encoded_data->bytes = memory.mem;
|
||||
encoded_data->size = memory.size;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void GetEncodedData(const WebPMemoryWriter* const memory,
|
||||
WebPData* const encoded_data) {
|
||||
encoded_data->bytes = memory->mem;
|
||||
encoded_data->size = memory->size;
|
||||
}
|
||||
|
||||
#define MIN_COLORS_LOSSY 31 // Don't try lossy below this threshold.
|
||||
#define MAX_COLORS_LOSSLESS 194 // Don't try lossless above this threshold.
|
||||
#define MAX_COLOR_COUNT 256 // Power of 2 greater than MAX_COLORS_LOSSLESS.
|
||||
#define HASH_SIZE (MAX_COLOR_COUNT * 4)
|
||||
#define HASH_RIGHT_SHIFT 22 // 32 - log2(HASH_SIZE).
|
||||
|
||||
// TODO(urvang): Also used in enc/vp8l.c. Move to utils.
|
||||
// If the number of colors in the 'pic' is at least MAX_COLOR_COUNT, return
|
||||
// MAX_COLOR_COUNT. Otherwise, return the exact number of colors in the 'pic'.
|
||||
static int GetColorCount(const WebPPicture* const pic) {
|
||||
int x, y;
|
||||
int num_colors = 0;
|
||||
uint8_t in_use[HASH_SIZE] = { 0 };
|
||||
uint32_t colors[HASH_SIZE];
|
||||
static const uint32_t kHashMul = 0x1e35a7bd;
|
||||
const uint32_t* argb = pic->argb;
|
||||
const int width = pic->width;
|
||||
const int height = pic->height;
|
||||
uint32_t last_pix = ~argb[0]; // so we're sure that last_pix != argb[0]
|
||||
|
||||
for (y = 0; y < height; ++y) {
|
||||
for (x = 0; x < width; ++x) {
|
||||
int key;
|
||||
if (argb[x] == last_pix) {
|
||||
continue;
|
||||
}
|
||||
last_pix = argb[x];
|
||||
key = (kHashMul * last_pix) >> HASH_RIGHT_SHIFT;
|
||||
while (1) {
|
||||
if (!in_use[key]) {
|
||||
colors[key] = last_pix;
|
||||
in_use[key] = 1;
|
||||
++num_colors;
|
||||
if (num_colors >= MAX_COLOR_COUNT) {
|
||||
return MAX_COLOR_COUNT; // Exact count not needed.
|
||||
}
|
||||
break;
|
||||
} else if (colors[key] == last_pix) {
|
||||
break; // The color is already there.
|
||||
} else {
|
||||
// Some other color sits here, so do linear conflict resolution.
|
||||
++key;
|
||||
key &= (HASH_SIZE - 1); // Key mask.
|
||||
}
|
||||
}
|
||||
}
|
||||
argb += pic->argb_stride;
|
||||
}
|
||||
return num_colors;
|
||||
}
|
||||
|
||||
#undef MAX_COLOR_COUNT
|
||||
#undef HASH_SIZE
|
||||
#undef HASH_RIGHT_SHIFT
|
||||
|
||||
static int SetFrame(const WebPConfig* const config, int allow_mixed,
|
||||
int is_key_frame, const WebPPicture* const prev_canvas,
|
||||
WebPPicture* const frame, const WebPFrameRect* const rect,
|
||||
const WebPMuxFrameInfo* const info,
|
||||
WebPPicture* const sub_frame, EncodedFrame* encoded_frame) {
|
||||
int try_lossless;
|
||||
int try_lossy;
|
||||
int try_both;
|
||||
WebPMemoryWriter mem1, mem2;
|
||||
WebPData* encoded_data;
|
||||
WebPMuxFrameInfo* const dst =
|
||||
is_key_frame ? &encoded_frame->key_frame : &encoded_frame->sub_frame;
|
||||
*dst = *info;
|
||||
encoded_data = &dst->bitstream;
|
||||
WebPMemoryWriterInit(&mem1);
|
||||
WebPMemoryWriterInit(&mem2);
|
||||
|
||||
if (!allow_mixed) {
|
||||
try_lossless = config->lossless;
|
||||
try_lossy = !try_lossless;
|
||||
} else { // Use a heuristic for trying lossless and/or lossy compression.
|
||||
const int num_colors = GetColorCount(sub_frame);
|
||||
try_lossless = (num_colors < MAX_COLORS_LOSSLESS);
|
||||
try_lossy = (num_colors >= MIN_COLORS_LOSSY);
|
||||
}
|
||||
try_both = try_lossless && try_lossy;
|
||||
|
||||
if (try_lossless) {
|
||||
WebPConfig config_ll = *config;
|
||||
config_ll.lossless = 1;
|
||||
if (!EncodeFrame(&config_ll, sub_frame, &mem1)) {
|
||||
goto Err;
|
||||
}
|
||||
}
|
||||
|
||||
if (try_lossy) {
|
||||
WebPConfig config_lossy = *config;
|
||||
config_lossy.lossless = 0;
|
||||
if (!is_key_frame) {
|
||||
// For lossy compression of a frame, it's better to replace transparent
|
||||
// pixels of 'curr' with actual RGB values, whenever possible.
|
||||
ReduceTransparency(prev_canvas, rect, frame);
|
||||
// TODO(later): Investigate if this helps lossless compression as well.
|
||||
FlattenSimilarBlocks(prev_canvas, rect, frame);
|
||||
}
|
||||
if (!EncodeFrame(&config_lossy, sub_frame, &mem2)) {
|
||||
goto Err;
|
||||
}
|
||||
}
|
||||
|
||||
if (try_both) { // Pick the encoding with smallest size.
|
||||
// TODO(later): Perhaps a rough SSIM/PSNR produced by the encoder should
|
||||
// also be a criteria, in addition to sizes.
|
||||
if (mem1.size <= mem2.size) {
|
||||
free(mem2.mem);
|
||||
GetEncodedData(&mem1, encoded_data);
|
||||
} else {
|
||||
free(mem1.mem);
|
||||
GetEncodedData(&mem2, encoded_data);
|
||||
}
|
||||
} else {
|
||||
GetEncodedData(try_lossless ? &mem1 : &mem2, encoded_data);
|
||||
}
|
||||
return 1;
|
||||
|
||||
Err:
|
||||
free(mem1.mem);
|
||||
free(mem2.mem);
|
||||
return 0;
|
||||
}
|
||||
|
||||
#undef MIN_COLORS_LOSSY
|
||||
#undef MAX_COLORS_LOSSLESS
|
||||
|
||||
// Returns cached frame at given 'position' index.
|
||||
static EncodedFrame* CacheGetFrame(const WebPFrameCache* const cache,
|
||||
size_t position) {
|
||||
@ -363,29 +496,6 @@ static int64_t KeyFramePenalty(const EncodedFrame* const encoded_frame) {
|
||||
encoded_frame->sub_frame.bitstream.size);
|
||||
}
|
||||
|
||||
static int SetFrame(const WebPConfig* const config, int is_key_frame,
|
||||
const WebPPicture* const prev_canvas,
|
||||
WebPPicture* const frame, const WebPFrameRect* const rect,
|
||||
const WebPMuxFrameInfo* const info,
|
||||
WebPPicture* const sub_frame,
|
||||
EncodedFrame* encoded_frame) {
|
||||
WebPMuxFrameInfo* const dst =
|
||||
is_key_frame ? &encoded_frame->key_frame : &encoded_frame->sub_frame;
|
||||
*dst = *info;
|
||||
|
||||
if (!config->lossless && !is_key_frame) {
|
||||
// For lossy compression of a frame, it's better to replace transparent
|
||||
// pixels of 'curr' with actual RGB values, whenever possible.
|
||||
ReduceTransparency(prev_canvas, rect, frame);
|
||||
FlattenSimilarBlocks(prev_canvas, rect, frame);
|
||||
}
|
||||
|
||||
if (!EncodeFrame(config, sub_frame, &dst->bitstream)) {
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void DisposeFrame(WebPMuxAnimDispose dispose_method,
|
||||
const WebPFrameRect* const gif_rect,
|
||||
WebPPicture* const frame, WebPPicture* const canvas) {
|
||||
@ -405,6 +515,7 @@ int WebPFrameCacheAddFrame(WebPFrameCache* const cache,
|
||||
WebPPicture sub_image; // View extracted from 'frame' with rectangle 'rect'.
|
||||
WebPPicture* const prev_canvas = &cache->prev_canvas;
|
||||
const size_t position = cache->count;
|
||||
const int allow_mixed = cache->allow_mixed;
|
||||
EncodedFrame* const encoded_frame = CacheGetFrame(cache, position);
|
||||
assert(position < cache->size);
|
||||
|
||||
@ -425,7 +536,7 @@ int WebPFrameCacheAddFrame(WebPFrameCache* const cache,
|
||||
|
||||
if (cache->is_first_frame || IsKeyFrame(frame, &rect, prev_canvas)) {
|
||||
// Add this as a key frame.
|
||||
if (!SetFrame(config, 1, NULL, NULL, NULL, info, &sub_image,
|
||||
if (!SetFrame(config, allow_mixed, 1, NULL, NULL, NULL, info, &sub_image,
|
||||
encoded_frame)) {
|
||||
goto End;
|
||||
}
|
||||
@ -438,8 +549,8 @@ int WebPFrameCacheAddFrame(WebPFrameCache* const cache,
|
||||
++cache->count_since_key_frame;
|
||||
if (cache->count_since_key_frame <= cache->kmin) {
|
||||
// Add this as a frame rectangle.
|
||||
if (!SetFrame(config, 0, prev_canvas, frame, &rect, info, &sub_image,
|
||||
encoded_frame)) {
|
||||
if (!SetFrame(config, allow_mixed, 0, prev_canvas, frame, &rect, info,
|
||||
&sub_image, encoded_frame)) {
|
||||
goto End;
|
||||
}
|
||||
cache->flush_count = cache->count;
|
||||
@ -452,8 +563,8 @@ int WebPFrameCacheAddFrame(WebPFrameCache* const cache,
|
||||
int64_t curr_delta;
|
||||
|
||||
// Add frame rectangle to cache.
|
||||
if (!SetFrame(config, 0, prev_canvas, frame, &rect, info, &sub_image,
|
||||
encoded_frame)) {
|
||||
if (!SetFrame(config, allow_mixed, 0, prev_canvas, frame, &rect, info,
|
||||
&sub_image, encoded_frame)) {
|
||||
goto End;
|
||||
}
|
||||
|
||||
@ -469,8 +580,8 @@ int WebPFrameCacheAddFrame(WebPFrameCache* const cache,
|
||||
full_image_info.y_offset = rect.y_offset;
|
||||
|
||||
// Add key frame to cache, too.
|
||||
frame_added = SetFrame(config, 1, NULL, NULL, NULL, &full_image_info,
|
||||
&full_image, encoded_frame);
|
||||
frame_added = SetFrame(config, allow_mixed, 1, NULL, NULL, NULL,
|
||||
&full_image_info, &full_image, encoded_frame);
|
||||
WebPPictureFree(&full_image);
|
||||
if (!frame_added) goto End;
|
||||
|
||||
@ -498,7 +609,10 @@ int WebPFrameCacheAddFrame(WebPFrameCache* const cache,
|
||||
|
||||
End:
|
||||
WebPPictureFree(&sub_image);
|
||||
if (!ok) --cache->count; // We reset the count, as the frame addition failed.
|
||||
if (!ok) {
|
||||
FrameRelease(encoded_frame);
|
||||
--cache->count; // We reset the count, as the frame addition failed.
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
|
@ -44,9 +44,11 @@ typedef struct WebPFrameCache WebPFrameCache;
|
||||
|
||||
// Given the minimum distance between key frames 'kmin' and maximum distance
|
||||
// between key frames 'kmax', returns an appropriately allocated cache object.
|
||||
// If 'allow_mixed' is true, the subsequent calls to WebPFrameCacheAddFrame()
|
||||
// will heuristically pick lossy or lossless compression for each frame.
|
||||
// Use WebPFrameCacheDelete() to deallocate the 'cache'.
|
||||
WebPFrameCache* WebPFrameCacheNew(int width, int height,
|
||||
size_t kmin, size_t kmax);
|
||||
size_t kmin, size_t kmax, int allow_mixed);
|
||||
|
||||
// Release all the frame data from 'cache' and free 'cache'.
|
||||
void WebPFrameCacheDelete(WebPFrameCache* const cache);
|
||||
|
@ -1,5 +1,5 @@
|
||||
.\" Hey, EMACS: -*- nroff -*-
|
||||
.TH GIF2WEBP 1 "September 30, 2013"
|
||||
.TH GIF2WEBP 1 "November 13, 2013"
|
||||
.SH NAME
|
||||
gif2webp \- Convert a GIF image to WebP
|
||||
.SH SYNOPSIS
|
||||
@ -28,6 +28,10 @@ Print the version number (as major.minor.revision) and exit.
|
||||
.B \-lossy
|
||||
Encode the image using lossy compression.
|
||||
.TP
|
||||
.B \-mixed
|
||||
Mixed compression mode: optimize compression of the image by picking either
|
||||
lossy or lossless compression for each frame heuristically.
|
||||
.TP
|
||||
.BI \-q " float
|
||||
Specify the compression factor for RGB channels between 0 and 100. The default
|
||||
is 75.
|
||||
|
Loading…
x
Reference in New Issue
Block a user