解决 gif 导出后显示异常的现象
背景:
上次gif支持透明度后,https://blog.csdn.net/c553110519/article/details/127757148?spm=1001.2014.3001.5501, 发现当输入是动态的时候,会出现异常现象
如下所示:
现象原因分析
刚开始怀疑是是输入的调色盘有问题
后来经过写测试代码把输入调色盘,转化成rgba数据后,发现输入调色盘数据并没有异常问题,从而可以断定是gif编码或者解码过程出现的问题
由于我们用的ffmpeg版本是3,仔细看了关于解码的问题,发现了一些代码比较可疑,如下所示:
C++ if (avpkt->size >= 6) { s->keyframe = memcmp(avpkt->data, gif87a_sig, 6) == 0 || memcmp(avpkt->data, gif89a_sig, 6) == 0; } else { s->keyframe = 0; } if (s->keyframe) { .......//这里代码先删除,方便理解 } else { if ((ret = ff_reget_buffer(avctx, s->frame)) < 0) return ret; ........ }
从上边 截取的源码可以看到,当第一帧gif会带有标志的,所以s->keyframe=1,而接下来的帧不带上边标志位,所以s->keyframe= 0,紧接着下边的s->keyframe的判断,当s->keyframe=0的时候
可以看到ff_reget_buffer(avctx, s->frame), 这个获取s->frame是上次解码gif的frame,所以没有清屏,接下来看下具体解码的过程
C++ for (y = 0; y < height; y++) { int count = ff_lzw_decode(s->lzw, s->idx_line, width); if (count != width) { if (count) av_log(s->avctx, AV_LOG_ERROR, "LZW decode failed\n"); goto decode_tail; } pr = ptr + pw; for (px = ptr, idx = s->idx_line; px < pr; px++, idx++) { if (*idx != s->transparent_color_index) *px = pal[*idx]; } }
重点看下红色加深的地方,这里会判断索引是不是透明度,如果当前不是透明度的索引,就赋值,否则就用frame里边的,刚刚上边frame是上一帧的数据,并没有清掉,因此可以得出结论,当前叠加上一帧最终导致开始那种现象。
知道了原因,就找下下边解决过程
解决过程:
首先还是阅读源码试下是否有可以规避的地方,找到了如下代码
C++ static int gif_read_image(GifState *s, AVFrame *frame) { ..................... /* process disposal method */ if (s->gce_prev_disposal == GCE_DISPOSAL_BACKGROUND) { gif_fill_rect(frame, s->stored_bg_color, s->gce_l, s->gce_t, s->gce_w, s->gce_h); } else if (s->gce_prev_disposal == GCE_DISPOSAL_RESTORE) { gif_copy_img_rect(s->stored_img, (uint32_t *)frame->data[0], frame->linesize[0] / sizeof(uint32_t), s->gce_l, s->gce_t, s->gce_w, s->gce_h); } ..................... }
在gif 解码中看到了s->gce_prev_disposal == GCE_DISPOSAL_BACKGROUND 的时候,会把frame中数据全部设置成s->stored_bg_color(0x00fffffff),这个透明度是0,这不就是我想要的吗,接着就看下gce_prev_disposal 值来源,在下边代码中
C++ static int gif_read_extension(GifState *s) { ....... switch(ext_code) { case GIF_GCE_EXT_LABEL: ....... gce_flags = bytestream2_get_byteu(&s->gb); bytestream2_skipu(&s->gb, 2); // delay during which the frame is shown ....... s->gce_disposal = (gce_flags >> 2) & 0x7; break; } ....... return 0; }
可以看到gce_prev_disposal 来自于s->gce_disposal,是从gif文件中读取的,而刚刚那个gif的值是1,但是我们需要它等于GCE_DISPOSAL_BACKGROUND(2),也就是说要想办法让s->gce_disposal = 2,那就要去找gif 写文件那块代码了,看下这个值是怎么填进去的
C++ static int flush_packet(AVFormatContext *s, AVPacket *new) { ........ /* graphic control extension block */ avio_w8(pb, 0x21); avio_w8(pb, 0xf9); avio_w8(pb, 0x04); /* block size */ avio_w8(pb, 1<<2 | (bcid >= 0)); avio_wl16(pb, gif->duration); avio_w8(pb, bcid < 0 ? DEFAULT_TRANSPARENCY_INDEX : bcid); avio_w8(pb, 0x00); ....... return 0; }
黄色的阴影的代码,写死了是1,也就是gif写文件的时候写死是,那是不是把它改成2就可以了,抱着试试的心态,改成2,导出gif是下边的样子
这就比较气人了,图像指令中间变成了透明了,但是残影没有了,说明思路是对的,接下来就找下为什么出现这个现象。
经过一番折腾后,找到了gif编码里边的可疑代码
C++ static int gif_image_write_image(AVCodecContext *avctx, uint8_t **bytestream, uint8_t *end, const uint32_t *palette, const uint8_t *buf, const int linesize, AVPacket *pkt) { GIFContext *s = avctx->priv_data; int honor_transparency = (s->flags & GF_TRANSDIFF) && s->last_frame; /* Crop image */ if ((s->flags & GF_OFFSETTING) && s->last_frame && !palette) { const uint8_t *ref = s->last_frame->data[0]; const int ref_linesize = s->last_frame->linesize[0]; int x_end = avctx->width - 1, y_end = avctx->height - 1; /* skip common lines */ while (y_start < y_end) { if (memcmp(ref + y_start*ref_linesize, buf + y_start*linesize, width)) break; y_start++; } while (y_end > y_start) { if (memcmp(ref + y_end*ref_linesize, buf + y_end*linesize, width)) break; y_end--; } height = y_end + 1 - y_start; /* skip common columns */ while (x_start < x_end) { int same_column = 1; for (y = y_start; y <= y_end; y++) { if (ref[y*ref_linesize + x_start] != buf[y*linesize + x_start]) { same_column = 0; break; } } if (!same_column) break; x_start++; } while (x_end > x_start) { int same_column = 1; for (y = y_start; y <= y_end; y++) { if (ref[y*ref_linesize + x_end] != buf[y*linesize + x_end]) { same_column = 0; break; } } if (!same_column) break; x_end--; } width = x_end + 1 - x_start; }
可疑看到s->flag 如果是GF_TRANSDIFF 或者GF_OFFSETTING,会进入下边的代码,会比较与上一帧的差异,那就看下s->flag是在哪里赋值的,找了整个文件都没看到哪里有初始化它,那说明调用它的方式在其他地方
C++ int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options) { if (codec->priv_data_size > 0) { if (!avctx->priv_data) { avctx->priv_data = av_mallocz(codec->priv_data_size); if (!avctx->priv_data) { ret = AVERROR(ENOMEM); goto end; } if (codec->priv_class) { *(const AVClass **)avctx->priv_data = codec->priv_class; av_opt_set_defaults(avctx->priv_data); } } if (codec->priv_class && (ret = av_opt_set_dict(avctx->priv_data, &tmp)) < 0) goto free_and_end; } else { avctx->priv_data = NULL; } if ((ret = av_opt_set_dict(avctx, &tmp)) < 0) goto free_and_end; }
C++ #define OFFSET(x) offsetof(GIFContext, x) #define FLAGS AV_OPT_FLAG_VIDEO_PARAM | AV_OPT_FLAG_ENCODING_PARAM static const AVOption gif_options[] = { { "gifflags", "set GIF flags", OFFSET(flags), AV_OPT_TYPE_FLAGS, {.i64 = GF_OFFSETTING|GF_TRANSDIFF}, 0, INT_MAX, FLAGS, "flags" }, { "offsetting", "enable picture offsetting", 0, AV_OPT_TYPE_CONST, {.i64=GF_OFFSETTING}, INT_MIN, INT_MAX, FLAGS, "flags" }, { "transdiff", "enable transparency detection between frames", 0, AV_OPT_TYPE_CONST, {.i64=GF_TRANSDIFF}, INT_MIN, INT_MAX, FLAGS, "flags" }, { NULL } };
原来在codec open的时候会调用av_opt_set_dict 把gif编码器的gif_options 循环一遍,去设置对应的默认值,看下flag默认值{.i64 = GF_OFFSETTING|GF_TRANSDIFF},正好与上边分析一致,那我们知道这个字典外部是可以通过调用ffmpeg接口改掉的,我就把他改成0试下,
gif导出如下所示:
完美,最终解决了这个问题,前后花了三四个小时, 不过还是值得的。