From 73f52133a1fa20ced946195ae0122d8b486c59a3 Mon Sep 17 00:00:00 2001 From: Urvang Joshi Date: Sun, 17 Nov 2013 18:04:07 -0800 Subject: [PATCH] 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 --- examples/gif2webp.c | 8 +- examples/gif2webp_util.c | 190 +++++++++++++++++++++++++++++++-------- examples/gif2webp_util.h | 4 +- man/gif2webp.1 | 6 +- 4 files changed, 167 insertions(+), 41 deletions(-) diff --git a/examples/gif2webp.c b/examples/gif2webp.c index c23abcbe..714fa76f 100644 --- a/examples/gif2webp.c +++ b/examples/gif2webp.c @@ -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 ............. quality factor (0:small..100:big)\n"); printf(" -m ............... compression method (0=fast, 6=slowest)\n"); printf(" -kmin ............ 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(); diff --git a/examples/gif2webp_util.c b/examples/gif2webp_util.c index e3f519be..c9d96c62 100644 --- a/examples/gif2webp_util.c +++ b/examples/gif2webp_util.c @@ -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; } diff --git a/examples/gif2webp_util.h b/examples/gif2webp_util.h index a5ef67d7..fd540459 100644 --- a/examples/gif2webp_util.h +++ b/examples/gif2webp_util.h @@ -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); diff --git a/man/gif2webp.1 b/man/gif2webp.1 index ed5feb83..755de738 100644 --- a/man/gif2webp.1 +++ b/man/gif2webp.1 @@ -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.