• 解决gif导出后显示异常的现象


    解决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导出如下所示:

     

    完美,最终解决了这个问题,前后花了三四个小时, 不过还是值得的。

  • 相关阅读:
    Chromium Trace and Perfetto使用详解
    外贸在谷歌搜索客户,为什么搜索出来的都是同行?
    6个视频剪辑必备的素材网站,免费下载。
    【吞噬星空】爽翻,徐欣喜提永恒之体,罗峰秒杀败类,阿特金磕头认错
    vue3对数据进行类型验证
    Kutools for Excel v26.10 Excel插件工具箱中文版
    使用 Python 和 AI 将自己卡通化VToonify(教程含源码)
    一个集成的BurpSuite漏洞探测插件1.1
    @alilclowcode-engine-ext@1.0.5 不支持安装react@^16.3.0
    阿里Java二面:从底层聊下IO多路复用模型?这不有嘴就会
  • 原文地址:https://blog.csdn.net/c553110519/article/details/128210012