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:
Urvang Joshi 2013-11-17 18:04:07 -08:00
parent 87cffcc3c9
commit 73f52133a1
4 changed files with 167 additions and 41 deletions

View File

@ -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();

View File

@ -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;
}

View File

@ -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);

View File

@ -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.