From f0d6a14b640b91e8040871d6a091bdd96d5e5252 Mon Sep 17 00:00:00 2001 From: Urvang Joshi Date: Fri, 9 Aug 2013 14:42:36 -0700 Subject: [PATCH] gif2webp: Insert independent frames at regular intervals. We use the 'do not blend' option for creating independent frames. We also mark the already independent frames as 'do not blend'. This bounds the maximum number of frames that need to be decoded to decode a given frame, thus leading to a much better decoding performance. Change-Id: I7cef98af2b53751ec36993fd2bd54f7f4c4aad2b --- examples/gif2webp.c | 265 +++++++++++++++++++++++++++++++++++++------- man/gif2webp.1 | 13 +++ 2 files changed, 235 insertions(+), 43 deletions(-) diff --git a/examples/gif2webp.c b/examples/gif2webp.c index 9b224df4..6f1b5de3 100644 --- a/examples/gif2webp.c +++ b/examples/gif2webp.c @@ -36,11 +36,60 @@ static int transparent_index = -1; // No transparency by default. -static void ClearPicture(WebPPicture* const picture) { - int x, y; - for (y = 0; y < picture->height; ++y) { - uint32_t* const dst = picture->argb + y * picture->argb_stride; - for (x = 0; x < picture->width; ++x) dst[x] = TRANSPARENT_COLOR; +static void ClearRectangle(WebPPicture* const picture, + int left, int top, int width, int height) { + int j; + for (j = top; j < top + height; ++j) { + uint32_t* const dst = picture->argb + j * picture->argb_stride; + int i; + for (i = left; i < left + width; ++i) { + dst[i] = TRANSPARENT_COLOR; + } + } +} + +typedef struct WebPFrameRect { + int x_offset, y_offset, width, height; +} WebPFrameRect; + +// Clear pixels in 'picture' within given 'rect' to transparent color. +static void ClearPicture(WebPPicture* const picture, + const WebPFrameRect* const rect) { + if (rect != NULL) { + ClearRectangle(picture, rect->x_offset, rect->y_offset, + rect->width, rect->height); + } else { + ClearRectangle(picture, 0, 0, picture->width, picture->height); + } +} + +// TODO: Also used in picture.c. Move to a common location. +static void CopyPlane(const uint8_t* src, int src_stride, + uint8_t* dst, int dst_stride, int width, int height) { + while (height-- > 0) { + memcpy(dst, src, width); + src += src_stride; + dst += dst_stride; + } +} + +// Given 'curr_frame' and its 'rect', blend it in the 'canvas'. +static void BlendCurrentFrameInCanvas(const WebPPicture* const curr_frame, + const WebPFrameRect* const rect, + WebPPicture* const canvas) { + int j; + assert(canvas->width == curr_frame->width && + canvas->height == curr_frame->height); + for (j = rect->y_offset; j < rect->y_offset + rect->height; ++j) { + int i; + for (i = rect->x_offset; i < rect->x_offset + rect->width; ++i) { + const uint32_t src = + curr_frame->argb[j * curr_frame->argb_stride + i]; + const int src_alpha = src >> 24; + if (src_alpha != 0) { + canvas->argb[j * canvas->argb_stride + i] = src; + } + } } } @@ -60,25 +109,104 @@ static void Remap(const uint8_t* const src, const GifFileType* const gif, } } -static int ReadSubImage(GifFileType* gif, WebPPicture* pic, WebPPicture* view) { +// Returns true if 'curr' frame is a key frame, that is, it can be decoded +// independently of 'prev' canvas. +static int IsKeyFrame(const WebPPicture* const curr, + const WebPFrameRect* const curr_rect, + const WebPPicture* const prev) { + int i, j; + int is_key_frame = 1; + + // If previous canvas (with previous frame disposed) is all transparent, + // current frame is a key frame. + for (i = 0; i < prev->width; ++i) { + for (j = 0; j < prev->height; ++j) { + const uint32_t prev_alpha = (prev->argb[j * prev->argb_stride + i]) >> 24; + if (prev_alpha != 0) { + is_key_frame = 0; + break; + } + } + if (!is_key_frame) break; + } + if (is_key_frame) return 1; + + // If current frame covers the whole canvas and does not contain any + // transparent pixels that depend on previous canvas, then current frame is + // a key frame. + if (curr_rect->width == curr->width && curr_rect->height == curr->height) { + assert(curr_rect->x_offset == 0 && curr_rect->y_offset == 0); + is_key_frame = 1; + for (j = 0; j < prev->height; ++j) { + for (i = 0; i < prev->width; ++i) { + const uint32_t prev_alpha = + (prev->argb[j * prev->argb_stride + i]) >> 24; + const uint32_t curr_alpha = + (curr->argb[j * curr->argb_stride + i]) >> 24; + if (curr_alpha != 0xff && prev_alpha != 0) { + is_key_frame = 0; + break; + } + } + if (!is_key_frame) break; + } + if (is_key_frame) return 1; + } + + return 0; +} + +// Convert 'curr' frame to a key frame. +static void ConvertToKeyFrame(WebPPicture* const curr, + const WebPPicture* const prev, + WebPFrameRect* const rect) { + int j; + assert(curr->width == prev->width && curr->height == prev->height); + + // Replace transparent pixels of current canvas with those from previous + // canvas (with previous frame disposed). + for (j = 0; j < curr->height; ++j) { + int i; + for (i = 0; i < curr->width; ++i) { + uint32_t* const curr_pixel = curr->argb + j * curr->argb_stride + i; + const int curr_alpha = *curr_pixel >> 24; + if (curr_alpha == 0) { + *curr_pixel = prev->argb[j * prev->argb_stride + i]; + } + } + } + + // Frame rectangle now covers the whole canvas. + rect->x_offset = 0; + rect->y_offset = 0; + rect->width = curr->width; + rect->height = curr->height; +} + +static int ReadSubImage(GifFileType* gif, WebPPicture* const prev, + int is_first_frame, size_t key_frame_interval, + size_t* const count_since_key_frame, + WebPFrameRect* const gif_rect, WebPPicture* sub_image, + WebPMuxFrameInfo* const info, WebPPicture* const curr) { const GifImageDesc image_desc = gif->Image; - const int offset_x = image_desc.Left; - const int offset_y = image_desc.Top; - const int sub_w = image_desc.Width; - const int sub_h = image_desc.Height; uint32_t* dst = NULL; uint8_t* tmp = NULL; int ok = 0; + WebPFrameRect rect = { + image_desc.Left, image_desc.Top, image_desc.Width, image_desc.Height + }; + *gif_rect = rect; // Use a view for the sub-picture: - if (!WebPPictureView(pic, offset_x, offset_y, sub_w, sub_h, view)) { + if (!WebPPictureView(curr, rect.x_offset, rect.y_offset, + rect.width, rect.height, sub_image)) { fprintf(stderr, "Sub-image %dx%d at position %d,%d is invalid!\n", - sub_w, sub_h, offset_x, offset_y); + rect.width, rect.height, rect.x_offset, rect.y_offset); goto End; } - dst = view->argb; + dst = sub_image->argb; - tmp = (uint8_t*)malloc(sub_w * sizeof(*tmp)); + tmp = (uint8_t*)malloc(rect.width * sizeof(*tmp)); if (tmp == NULL) goto End; if (image_desc.Interlace) { // Interlaced image. @@ -88,21 +216,56 @@ static int ReadSubImage(GifFileType* gif, WebPPicture* pic, WebPPicture* view) { int pass; for (pass = 0; pass < 4; ++pass) { int y; - for (y = interlace_offsets[pass]; y < sub_h; y += interlace_jumps[pass]) { - if (DGifGetLine(gif, tmp, sub_w) == GIF_ERROR) goto End; - Remap(tmp, gif, dst + y * view->argb_stride, sub_w); + for (y = interlace_offsets[pass]; y < rect.height; + y += interlace_jumps[pass]) { + if (DGifGetLine(gif, tmp, rect.width) == GIF_ERROR) goto End; + Remap(tmp, gif, dst + y * sub_image->argb_stride, rect.width); } } } else { // Non-interlaced image. int y; - for (y = 0; y < sub_h; ++y) { - if (DGifGetLine(gif, tmp, sub_w) == GIF_ERROR) goto End; - Remap(tmp, gif, dst + y * view->argb_stride, sub_w); + for (y = 0; y < rect.height; ++y) { + if (DGifGetLine(gif, tmp, rect.width) == GIF_ERROR) goto End; + Remap(tmp, gif, dst + y * sub_image->argb_stride, rect.width); } } - // re-align the view with even offset (and adjust dimensions if needed). - WebPPictureView(pic, offset_x & ~1, offset_y & ~1, - sub_w + (offset_x & 1), sub_h + (offset_y & 1), view); + + // Snap to even offsets (and adjust dimensions if needed). + rect.width += (rect.x_offset & 1); + rect.height += (rect.y_offset & 1); + rect.x_offset &= ~1; + rect.y_offset &= ~1; + + // Make this a key frame if needed. + if (is_first_frame || IsKeyFrame(curr, &rect, prev)) { + *count_since_key_frame = 0; + } else { + ++*count_since_key_frame; + if (*count_since_key_frame > key_frame_interval) { + ConvertToKeyFrame(curr, prev, &rect); + *count_since_key_frame = 0; + } + } + + if (*count_since_key_frame == 0) { + info->blend_method = WEBP_MUX_NO_BLEND; // Key frame, so no need to blend. + } else { + info->blend_method = WEBP_MUX_BLEND; // The blending method in GIF. + } + + // Update the canvas so that it can be used to check for and/or create an + // key frame in the next iterations. + if (info->blend_method == WEBP_MUX_NO_BLEND) { + CopyPlane((uint8_t*)curr->argb, 4 * curr->argb_stride, (uint8_t*)prev->argb, + 4 * prev->argb_stride, 4 * curr->width, curr->height); + } else { + BlendCurrentFrameInCanvas(curr, gif_rect, prev); + } + + WebPPictureView(curr, rect.x_offset, rect.y_offset, rect.width, rect.height, + sub_image); + info->x_offset = rect.x_offset; + info->y_offset = rect.y_offset; ok = 1; End: @@ -171,6 +334,7 @@ static void Help(void) { printf(" -lossy ................. Encode image using lossy compression.\n"); printf(" -q ............. quality factor (0:small..100:big)\n"); printf(" -m ............... compression method (0=fast, 6=slowest)\n"); + printf(" -kmax ............ Max distance between key frames\n"); printf(" -f ............... filter strength (0=off..100)\n"); printf("\n"); printf(" -version ............... print version number and exit.\n"); @@ -189,11 +353,14 @@ int main(int argc, const char *argv[]) { const char *in_file = NULL, *out_file = NULL; FILE* out = NULL; GifFileType* gif = NULL; - WebPPicture picture; + WebPPicture current; + WebPPicture previous; WebPMuxFrameInfo frame; WebPMuxAnimParams anim = { WHITE_COLOR, 0 }; int is_first_frame = 1; + size_t count_since_key_frame = 0; // Frames seen since the last key frame. + size_t key_frame_interval = 9; // Max distance between key frames. int done; int c; int quiet = 0; @@ -206,8 +373,10 @@ int main(int argc, const char *argv[]) { memset(&frame, 0, sizeof(frame)); frame.id = WEBP_CHUNK_ANMF; frame.dispose_method = WEBP_MUX_DISPOSE_BACKGROUND; + frame.blend_method = WEBP_MUX_BLEND; - if (!WebPConfigInit(&config) || !WebPPictureInit(&picture)) { + if (!WebPConfigInit(&config) || + !WebPPictureInit(¤t) || !WebPPictureInit(&previous)) { fprintf(stderr, "Error! Version mismatch!\n"); return -1; } @@ -230,6 +399,9 @@ int main(int argc, const char *argv[]) { config.quality = (float)strtod(argv[++c], NULL); } else if (!strcmp(argv[c], "-m") && c < argc - 1) { config.method = strtol(argv[++c], NULL, 0); + } else if (!strcmp(argv[c], "-kmax") && c < argc - 1) { + key_frame_interval = strtoul(argv[++c], NULL, 0); + if (key_frame_interval == 0) key_frame_interval = ~0; } else if (!strcmp(argv[c], "-f") && c < argc - 1) { config.filter_strength = strtol(argv[++c], NULL, 0); } else if (!strcmp(argv[c], "-version")) { @@ -272,11 +444,12 @@ int main(int argc, const char *argv[]) { #endif if (gif == NULL) goto End; - // Allocate picture buffer - picture.width = gif->SWidth; - picture.height = gif->SHeight; - picture.use_argb = 1; - if (!WebPPictureAlloc(&picture)) goto End; + // Allocate current buffer + current.width = gif->SWidth; + current.height = gif->SHeight; + current.use_argb = 1; + if (!WebPPictureAlloc(¤t)) goto End; + if (!WebPPictureCopy(¤t, &previous)) goto End; mux = WebPMuxNew(); if (mux == NULL) { @@ -293,14 +466,15 @@ int main(int argc, const char *argv[]) { switch (type) { case IMAGE_DESC_RECORD_TYPE: { WebPPicture sub_image; + WebPFrameRect gif_rect; WebPMemoryWriter memory; - if (frame.dispose_method == WEBP_MUX_DISPOSE_BACKGROUND) { - ClearPicture(&picture); - } - if (!DGifGetImageDesc(gif)) goto End; - if (!ReadSubImage(gif, &picture, &sub_image)) goto End; + if (!ReadSubImage(gif, &previous, is_first_frame, key_frame_interval, + &count_since_key_frame, &gif_rect, &sub_image, &frame, + ¤t)) { + goto End; + } if (!config.lossless) { // We need to call BGRA variant because of the way we do Remap(). Note @@ -317,7 +491,7 @@ int main(int argc, const char *argv[]) { sub_image.custom_ptr = &memory; WebPMemoryWriterInit(&memory); if (!WebPEncode(&config, &sub_image)) { - fprintf(stderr, "Error! Cannot encode picture as WebP\n"); + fprintf(stderr, "Error! Cannot encode current as WebP\n"); fprintf(stderr, "Error code: %d\n", sub_image.error_code); goto End; } @@ -325,8 +499,6 @@ int main(int argc, const char *argv[]) { // Now we have all the info about the frame, as a Graphic Control // Extension Block always appears before the Image Descriptor Block. // So add the frame to mux. - frame.x_offset = gif->Image.Left & ~1; - frame.y_offset = gif->Image.Top & ~1; frame.bitstream.bytes = memory.mem; frame.bitstream.size = memory.size; err = WebPMuxPushFrame(mux, &frame, 1); @@ -340,11 +512,17 @@ int main(int argc, const char *argv[]) { sub_image.width, sub_image.height, frame.x_offset, frame.y_offset, frame.duration); - printf("dispose:%d transparent index:%d\n", - frame.dispose_method, transparent_index); + printf("dispose:%d blend:%d transparent index:%d\n", + frame.dispose_method, frame.blend_method, transparent_index); } WebPDataClear(&frame.bitstream); WebPPictureFree(&sub_image); + is_first_frame = 0; + + if (frame.dispose_method == WEBP_MUX_DISPOSE_BACKGROUND) { + ClearPicture(¤t, NULL); + ClearPicture(&previous, &gif_rect); + } break; } case EXTENSION_RECORD_TYPE: { @@ -380,8 +558,8 @@ int main(int argc, const char *argv[]) { fprintf(stderr, "GIF decode warning: invalid background color " "index. Assuming white background.\n"); } - ClearPicture(&picture); - is_first_frame = 0; + ClearPicture(¤t, NULL); + ClearPicture(&previous, NULL); } break; } @@ -520,7 +698,8 @@ int main(int argc, const char *argv[]) { End: WebPDataClear(&webp_data); WebPMuxDelete(mux); - WebPPictureFree(&picture); + WebPPictureFree(¤t); + WebPPictureFree(&previous); if (out != NULL && out_file != NULL) fclose(out); if (gif_error != GIF_OK) { diff --git a/man/gif2webp.1 b/man/gif2webp.1 index 2b88bb9a..f2e0fd1a 100644 --- a/man/gif2webp.1 +++ b/man/gif2webp.1 @@ -49,6 +49,19 @@ additional encoding possibilities and decide on the quality gain. Lower value can result is faster processing time at the expense of larger file size and lower compression quality. .TP +.BI \-kmax " int +Relevant only for animated images with large number of frames (>50). +Specify the maximum distance between consecutive key frames (independently +decodable frames) in the output image. The tool will insert some key frames into +the output image as needed so that this criteria is satisfied. A value of 0 will +turn off insertion of key frames. +Typical values are in the range 5 to 30. Default value is 9. +When lower values are used, more frames will be converted to key frames. This +may lead to smaller number of frames required to decode a frame on average, +thereby improving the decoding performance. But this may lead to slightly bigger +file sizes. +Higher values may lead to worse decoding performance, but smaller file sizes. +.TP .BI \-f " int For lossy encoding only (specified by the \-lossy option). Specify the strength of the deblocking filter, between 0 (no filtering) and 100 (maximum filtering).