mirror of
https://github.com/webmproject/libwebp.git
synced 2025-02-13 15:32:53 +01:00
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("options:\n");
|
||||||
printf(" -h / -help ............ this help\n");
|
printf(" -h / -help ............ this help\n");
|
||||||
printf(" -lossy ................. Encode image using lossy compression.\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(" -q <float> ............. quality factor (0:small..100:big)\n");
|
||||||
printf(" -m <int> ............... compression method (0=fast, 6=slowest)\n");
|
printf(" -m <int> ............... compression method (0=fast, 6=slowest)\n");
|
||||||
printf(" -kmin <int> ............ Min distance between key frames\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;
|
int default_kmax = 1;
|
||||||
size_t kmin = 0;
|
size_t kmin = 0;
|
||||||
size_t kmax = 0;
|
size_t kmax = 0;
|
||||||
|
int allow_mixed = 0; // If true, each frame can be lossy or lossless.
|
||||||
|
|
||||||
memset(&info, 0, sizeof(info));
|
memset(&info, 0, sizeof(info));
|
||||||
info.id = WEBP_CHUNK_ANMF;
|
info.id = WEBP_CHUNK_ANMF;
|
||||||
@ -279,6 +282,9 @@ int main(int argc, const char *argv[]) {
|
|||||||
out_file = argv[++c];
|
out_file = argv[++c];
|
||||||
} else if (!strcmp(argv[c], "-lossy")) {
|
} else if (!strcmp(argv[c], "-lossy")) {
|
||||||
config.lossless = 0;
|
config.lossless = 0;
|
||||||
|
} else if (!strcmp(argv[c], "-mixed")) {
|
||||||
|
allow_mixed = 1;
|
||||||
|
config.lossless = 0;
|
||||||
} else if (!strcmp(argv[c], "-q") && c < argc - 1) {
|
} else if (!strcmp(argv[c], "-q") && c < argc - 1) {
|
||||||
config.quality = (float)strtod(argv[++c], NULL);
|
config.quality = (float)strtod(argv[++c], NULL);
|
||||||
} else if (!strcmp(argv[c], "-m") && c < argc - 1) {
|
} 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;
|
if (!WebPPictureAlloc(&frame)) goto End;
|
||||||
|
|
||||||
// Initialize cache
|
// 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;
|
if (cache == NULL) goto End;
|
||||||
|
|
||||||
mux = WebPMuxNew();
|
mux = WebPMuxNew();
|
||||||
|
@ -267,6 +267,7 @@ struct WebPFrameCache {
|
|||||||
size_t kmin; // Min distance between key frames.
|
size_t kmin; // Min distance between key frames.
|
||||||
size_t kmax; // Max 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.
|
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 prev_canvas; // Previous canvas (properly disposed).
|
||||||
WebPPicture curr_canvas; // Current canvas (temporary buffer).
|
WebPPicture curr_canvas; // Current canvas (temporary buffer).
|
||||||
int is_first_frame; // True if no frames have been added to the cache
|
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,
|
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));
|
WebPFrameCache* cache = (WebPFrameCache*)malloc(sizeof(*cache));
|
||||||
if (cache == NULL) return NULL;
|
if (cache == NULL) return NULL;
|
||||||
CacheReset(cache);
|
CacheReset(cache);
|
||||||
@ -305,6 +306,7 @@ WebPFrameCache* WebPFrameCacheNew(int width, int height,
|
|||||||
WebPUtilClearPic(&cache->prev_canvas, NULL);
|
WebPUtilClearPic(&cache->prev_canvas, NULL);
|
||||||
|
|
||||||
// Cache data.
|
// Cache data.
|
||||||
|
cache->allow_mixed = allow_mixed;
|
||||||
cache->kmin = kmin;
|
cache->kmin = kmin;
|
||||||
cache->kmax = kmax;
|
cache->kmax = kmax;
|
||||||
cache->count_since_key_frame = 0;
|
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,
|
static int EncodeFrame(const WebPConfig* const config, WebPPicture* const pic,
|
||||||
WebPData* const encoded_data) {
|
WebPMemoryWriter* const memory) {
|
||||||
WebPMemoryWriter memory;
|
|
||||||
pic->use_argb = 1;
|
pic->use_argb = 1;
|
||||||
pic->writer = WebPMemoryWrite;
|
pic->writer = WebPMemoryWrite;
|
||||||
pic->custom_ptr = &memory;
|
pic->custom_ptr = memory;
|
||||||
WebPMemoryWriterInit(&memory);
|
|
||||||
if (!WebPEncode(config, pic)) {
|
if (!WebPEncode(config, pic)) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
encoded_data->bytes = memory.mem;
|
|
||||||
encoded_data->size = memory.size;
|
|
||||||
return 1;
|
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.
|
// Returns cached frame at given 'position' index.
|
||||||
static EncodedFrame* CacheGetFrame(const WebPFrameCache* const cache,
|
static EncodedFrame* CacheGetFrame(const WebPFrameCache* const cache,
|
||||||
size_t position) {
|
size_t position) {
|
||||||
@ -363,29 +496,6 @@ static int64_t KeyFramePenalty(const EncodedFrame* const encoded_frame) {
|
|||||||
encoded_frame->sub_frame.bitstream.size);
|
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,
|
static void DisposeFrame(WebPMuxAnimDispose dispose_method,
|
||||||
const WebPFrameRect* const gif_rect,
|
const WebPFrameRect* const gif_rect,
|
||||||
WebPPicture* const frame, WebPPicture* const canvas) {
|
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 sub_image; // View extracted from 'frame' with rectangle 'rect'.
|
||||||
WebPPicture* const prev_canvas = &cache->prev_canvas;
|
WebPPicture* const prev_canvas = &cache->prev_canvas;
|
||||||
const size_t position = cache->count;
|
const size_t position = cache->count;
|
||||||
|
const int allow_mixed = cache->allow_mixed;
|
||||||
EncodedFrame* const encoded_frame = CacheGetFrame(cache, position);
|
EncodedFrame* const encoded_frame = CacheGetFrame(cache, position);
|
||||||
assert(position < cache->size);
|
assert(position < cache->size);
|
||||||
|
|
||||||
@ -425,7 +536,7 @@ int WebPFrameCacheAddFrame(WebPFrameCache* const cache,
|
|||||||
|
|
||||||
if (cache->is_first_frame || IsKeyFrame(frame, &rect, prev_canvas)) {
|
if (cache->is_first_frame || IsKeyFrame(frame, &rect, prev_canvas)) {
|
||||||
// Add this as a key frame.
|
// 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)) {
|
encoded_frame)) {
|
||||||
goto End;
|
goto End;
|
||||||
}
|
}
|
||||||
@ -438,8 +549,8 @@ int WebPFrameCacheAddFrame(WebPFrameCache* const cache,
|
|||||||
++cache->count_since_key_frame;
|
++cache->count_since_key_frame;
|
||||||
if (cache->count_since_key_frame <= cache->kmin) {
|
if (cache->count_since_key_frame <= cache->kmin) {
|
||||||
// Add this as a frame rectangle.
|
// Add this as a frame rectangle.
|
||||||
if (!SetFrame(config, 0, prev_canvas, frame, &rect, info, &sub_image,
|
if (!SetFrame(config, allow_mixed, 0, prev_canvas, frame, &rect, info,
|
||||||
encoded_frame)) {
|
&sub_image, encoded_frame)) {
|
||||||
goto End;
|
goto End;
|
||||||
}
|
}
|
||||||
cache->flush_count = cache->count;
|
cache->flush_count = cache->count;
|
||||||
@ -452,8 +563,8 @@ int WebPFrameCacheAddFrame(WebPFrameCache* const cache,
|
|||||||
int64_t curr_delta;
|
int64_t curr_delta;
|
||||||
|
|
||||||
// Add frame rectangle to cache.
|
// Add frame rectangle to cache.
|
||||||
if (!SetFrame(config, 0, prev_canvas, frame, &rect, info, &sub_image,
|
if (!SetFrame(config, allow_mixed, 0, prev_canvas, frame, &rect, info,
|
||||||
encoded_frame)) {
|
&sub_image, encoded_frame)) {
|
||||||
goto End;
|
goto End;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -469,8 +580,8 @@ int WebPFrameCacheAddFrame(WebPFrameCache* const cache,
|
|||||||
full_image_info.y_offset = rect.y_offset;
|
full_image_info.y_offset = rect.y_offset;
|
||||||
|
|
||||||
// Add key frame to cache, too.
|
// Add key frame to cache, too.
|
||||||
frame_added = SetFrame(config, 1, NULL, NULL, NULL, &full_image_info,
|
frame_added = SetFrame(config, allow_mixed, 1, NULL, NULL, NULL,
|
||||||
&full_image, encoded_frame);
|
&full_image_info, &full_image, encoded_frame);
|
||||||
WebPPictureFree(&full_image);
|
WebPPictureFree(&full_image);
|
||||||
if (!frame_added) goto End;
|
if (!frame_added) goto End;
|
||||||
|
|
||||||
@ -498,7 +609,10 @@ int WebPFrameCacheAddFrame(WebPFrameCache* const cache,
|
|||||||
|
|
||||||
End:
|
End:
|
||||||
WebPPictureFree(&sub_image);
|
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;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,9 +44,11 @@ typedef struct WebPFrameCache WebPFrameCache;
|
|||||||
|
|
||||||
// Given the minimum distance between key frames 'kmin' and maximum distance
|
// Given the minimum distance between key frames 'kmin' and maximum distance
|
||||||
// between key frames 'kmax', returns an appropriately allocated cache object.
|
// 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'.
|
// Use WebPFrameCacheDelete() to deallocate the 'cache'.
|
||||||
WebPFrameCache* WebPFrameCacheNew(int width, int height,
|
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'.
|
// Release all the frame data from 'cache' and free 'cache'.
|
||||||
void WebPFrameCacheDelete(WebPFrameCache* const cache);
|
void WebPFrameCacheDelete(WebPFrameCache* const cache);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
.\" Hey, EMACS: -*- nroff -*-
|
.\" Hey, EMACS: -*- nroff -*-
|
||||||
.TH GIF2WEBP 1 "September 30, 2013"
|
.TH GIF2WEBP 1 "November 13, 2013"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
gif2webp \- Convert a GIF image to WebP
|
gif2webp \- Convert a GIF image to WebP
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
@ -28,6 +28,10 @@ Print the version number (as major.minor.revision) and exit.
|
|||||||
.B \-lossy
|
.B \-lossy
|
||||||
Encode the image using lossy compression.
|
Encode the image using lossy compression.
|
||||||
.TP
|
.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
|
.BI \-q " float
|
||||||
Specify the compression factor for RGB channels between 0 and 100. The default
|
Specify the compression factor for RGB channels between 0 and 100. The default
|
||||||
is 75.
|
is 75.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user