H264编码原理

1. 视频为什么需要进行编码压缩

一张为720x480的图像,用YUV420P的格式来表示,其大小为:7204801.5 约等于0.5MB

  • 如果是25帧,10分钟的数据量 0.5M1060*25 = 7500MB -> 7GB多

视频编码压缩的目的是降低视频数据大小,方便存储和传输
在这里插入图片描述

2. 为什么压缩的原始数据采用YUV格式

  1. 视频编码是对一张张图像来进行的。我们知道彩色图像的格式是 RGB 的,但RGB 三个颜色是有相关性的。
  2. 采用YUV格式,利用人对图像的感觉的生理特性,对于亮度信息比较敏感,而对于色度信息不太敏感,所以视频编码是将Y分量和UV分量分开来编码的,并且可以减少UV分量,
    比如我们之前说的YUV420P
    在这里插入图片描述

3. 视频压缩原理

编码的目的是为了压缩,各种视频编码算法都是为了让视频体
积变得更小,减少对存储空间和传输带宽的占用。编码的核心
是去除冗余信息,通过以下几种冗余来达到压缩视频的目的:

  1. **空间冗余:**图像相邻像素之间有较强的相关性,比如一帧图像划分成多个 16x16 的块之后,相邻的块很多时候都有比较明显的相似性。
  2. **时间冗余:**视频序列的相邻前后帧图像之间内容相似,比
    如帧率为 25fps 的视频中前后两帧图像相差只有 40ms,前
    后两张图像的变化较小,相似性很高。
  3. **视觉冗余:**我们的眼睛对某些细节不敏感,对图像中高频
    信息的敏感度小于低频信息的。可以去除图像中的一些高
    频信息,人眼看起来跟不去除高频信息差别不大(有损压
    缩)。
  4. **编码冗余(信息熵冗余):**一幅图像中不同像素出现的概率是不同的。对出现次数比较多的像素,用少的位数来编码。对出现次数比较少的像素,用多的位数来编码,能够减少编码的大小。比如哈夫曼编码。

4.图像帧的类型( I、P、B帧)

I帧、P帧和B帧是视频压缩领域中的基础概念,用于提升视频压缩效率、视频质量和视频恢复能力。

  • I帧(关键帧或帧内帧)仅由帧内预测的宏块组成。
  • P帧代表预测帧,除帧内空域预测以外,它还可以通过时域预测来进行压缩。P帧通过使用已经编码的帧进行运动估计。
  • B帧可以参考在其前后出现的帧,B帧中的B代表双向(Bi-Directional)。
    -在这里插入图片描述
    例子:
    帧间隔40ms,
    0ms 编码I帧 直接发送出去
    40ms 不能编码B帧,因为要参考后面的P帧,因此没数据发送
    80ms P帧进来,先把第二帧编码为B帧,第三帧编码为P帧, 先发送P帧,再发送B帧

5. GOP图像序列

  • 一个序列的第一个图像叫做 IDR 图像(立即刷新图像),IDR 图像都是 I 帧图像。在视频编码序列中,GOP即Group of picture(图像组),指两个IDR帧之间的距离。
  • GOP长度越大,视频压缩效率越高,但视频质量和视频流恢复能力也越差,反之亦然。
  • 直播,如果是一秒25帧,一般gop设置为25, 50(一般是帧率的倍数).
  • 如果不是直播流,B帧一般设置2帧连续B帧,以降低码率。

5.1 GOP之Closed GOP和Open GOP

Closed GOP和Open GOP常见于视频流中,并影响压缩效率、视频容错能力以及ABR流的切换能力。

  • 顾名思义,Closed GOP对GOP外部的帧是封闭的。一个属于Closed GOP的帧只能参考这个GOP之内的帧。
  • Open GOP与Closed GOP相反,Open GOP内部的帧可以参考其他GOP中的帧。
    -在这里插入图片描述

5.2 GOP间隔

GOP 越大,编码的 I 帧就会越少。相比而言,P 帧、B 帧的压缩率更高,因此整个视频的编码效率就会越高。但是 GOP 太大,也会导致 IDR 帧距离太大,点播场景时进行视频的seek 操作就会不方便。
在这里插入图片描述

6. H264编码原理

对于每一帧图像,是划分为一个个块进行编码,就是我们说的宏块。

宏块大小一般是 16x16(H264、VP8),32x32(H265、VP9),64x64(H265、VP9、AV1),128x128(AV1)

6.1 宏块扫描

  • 对于一个 YUV 图像,可以把划分成一个个 16x16 的宏块(以 H264 为例),Y、U、V 分量的大小分别是 16x16、8x8、8x8。这里我们只对 Y 分量进行分析(U、V 分量同理)。假设 Y 分量这16x16 个像素就是一个个数字,采用“之”字方式扫描每一个像素值,则可以得到一个“像素串”。

压缩的目的是使得编码器当前的字符出现连续相同的字符,
比如1,1,1,1,1,1,1,1,我们可以描述为8个

数字越小越容易用更少的bit做压缩,比如一连串数字很小
(比如 0,1,2,1,0)的“像素串” ,因为 0 在二进制中只
占 1 个位,2只占2个位即可。
在这里插入图片描述
如何做到将这串像素值变成有很多 0 的“像素串”呢?
在这里插入图片描述

6.2 帧内预测

  1. 帧内预测就是在当前编码图像内部已经编码完成的块中找到与将要编码的块相邻的块。一般就是即将编码块的左边块、上边块、左上角块和右上角块,通过将这些块与编码块相邻的像素经过多种不同的算法得到多个不同的预测块。
  2. 然后我们再用编码块减去每一个预测块得到一个个残差块。最后,我们取这
    些算法得到的残差块中像素的绝对值加起来最小的块为预测块。而得到这个预测块的算法为帧内预测模式。
    在这里插入图片描述

6.3 残缺块

在这里插入图片描述
此时我们把原始编码块的像素值减去预测块的像素值得到残缺块,可以看到对应的数值更为接近0.
原始值-预测值=残缺块(预测算法 :水平预测 垂直预测)
在这里插入图片描述
H.264整数DCT公式推导及蝶形算法分析

6.4 帧间预测

  • 同理,帧间预测也是一样的。我们在前面已经编码完成的图像中,循环遍历每一个块,将它作为预测块,用当前的编码块与这个块做差值,得到残差块,取残差块中像素值的绝对值加起来最小的块为预测块,预测块所在的已经编码的图像称为参考帧。预测块在参考帧中的坐标值 (x0, y0) 与编码块在编码帧中的坐标值 (x1, y1) 的差值 (x0 - x1, y0 - y1) 称之为运动矢量
  • 而在参考帧中去寻找预测块的过程称之为运动搜索。事实上编码过程中真正的运动搜索不是一个个块去遍历寻找的,而是有快速的运动搜索算法的
  • 通过预测得到的残差块的像素值相比编码块的像素值,去除了大部分空间冗余信息
    和时间冗余信息,这样得到的像素值更小。如果把这个残差块做扫描得到的像素串
    送去做行程编码,是不是相比直接拿编码块的像素串去做编码更有可能得到更大的压缩率?

6.5 DCT 变换和量化(傅里叶变换)

  • 我们的目标不只是将像素值变小,而是希望能出现连续的 0 像素
  • 这就需要利用我们人眼的视觉敏感性的特点了。我们刚才说了人眼对高频信息不太敏感。因为人眼看到的效果可能差别不大,所以我们可以去除一些高频信息。这个就是接下来我们要讨论的 DCT 变换和量化。

6.5.1 DCT变换(余弦变换)

高频(大部分像素值)的值会变大,低频(占小部分)的会变小。我们时为了记录更多小的值

DCT变换取整数时又一定的损失
在这里插入图片描述

6.5.2 量化步长

  • 由于人眼对高频信息不太敏感,如果我们通过一种手段去除掉大部分高频信息,也就是将大部分高频信息置为 0,但又不太影响人的观感,是不是就可以达到我们最初的目标,即可以得到有一连串 0 的像素串?这就涉及到量化操作了。
  • 我们让变换块的系数都同时除以一个值,这个值我们称之为量化步长,也就是 QStep(QStep 是编码器内部的概念,用户一般使用量化参数 QP 这个值,QP 和 QStep 得到的结果就是量化后的系数。QStep 越大,得到量化后的系数就会越小。同时,相同的 QStep 值,高频系数值相比低频系数值更小,量化后就更容易变成 0。这样一来,将大部分高频系数变成 0。如下图所示:
    在这里插入图片描述

6.5.3 量化步长

在这里插入图片描述

  • 解码的时候,需要将 QStep 乘以量化后的系数得到变换系数,很明显这个变换系数和原始没有量化的变换系数是不一样的,这个就是常说的有损编码。
  • 而到底损失多少呢?其由 QStep 来控制,QStep 越大,损失就越大。QStep 跟 QP 一一对应。从编码器应用角度来看,QP 值越大,损失就越大,从而画面的清晰度就会越低。同时,QP 值越大系数被量化成 0 的概率就越大,这样编码之后码流大小就会越小,压缩就会越高。

6.5.4 量化步长表

在这里插入图片描述

编码原理总结

  • 为了能够在最后熵编码的时候压缩率更高,对于送到熵编码(以行程编码为例)的“像素串”,包含的0越多,越能提高压缩率。为了达到这个目标:
    • 先通过帧内预测或者帧间预测去除空间冗余和时间冗余,从而得到一个像素值相比编码块小很多的残差块。
    • 然后再通过 DCT 变换将低频和高频信息分离开来得到变换块,然后再对变换块的系数做量化
    • 由于高频系数通常比较小,很容易量化为 0,同时人眼对高频信息不太敏感,这样就得到了一串含有很多个 0,大多数情况下是一串含有连续 0 的“像素串”,并且人的观感还不会太明显。这样,最后熵编码就能把图像压缩成比较小的数据,以此达到视频压缩的目的。
  • 这即是视频编码的原理。

代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <libavcodec/avcodec.h>
#include <libavutil/time.h>
#include <libavutil/opt.h>
#include <libavutil/imgutils.h>

int64_t get_time()
{
    return av_gettime_relative() / 1000;  // 换算成毫秒
}
static int encode(AVCodecContext *enc_ctx, AVFrame *frame, AVPacket *pkt,
                  FILE *outfile)
{
    int ret;

    /* send the frame to the encoder */
    if (frame)
        printf("Send frame %3"PRId64"\n", frame->pts);
    /* 通过查阅代码,使用x264进行编码时,具体缓存帧是在x264源码进行,
     * 不会增加avframe对应buffer的reference*/
    ret = avcodec_send_frame(enc_ctx, frame);
    if (ret < 0)
    {
        fprintf(stderr, "Error sending a frame for encoding\n");
        return -1;
    }

    while (ret >= 0)
    {
        ret = avcodec_receive_packet(enc_ctx, pkt);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            return 0;
        } else if (ret < 0) {
            fprintf(stderr, "Error encoding audio frame\n");
            return -1;
        }

        if(pkt->flags & AV_PKT_FLAG_KEY)
            printf("Write packet flags:%d pts:%3"PRId64" dts:%3"PRId64" (size:%5d)\n",
               pkt->flags, pkt->pts, pkt->dts, pkt->size);
        if(!pkt->flags)
            printf("Write packet flags:%d pts:%3"PRId64" dts:%3"PRId64" (size:%5d)\n",
               pkt->flags, pkt->pts, pkt->dts, pkt->size);
        fwrite(pkt->data, 1, pkt->size, outfile);
    }
    return 0;
}
/**
 * @brief 提取测试文件:ffmpeg -i test_1280x720.flv -t 5 -r 25 -pix_fmt yuv420p yuv420p_1280x720.yuv
 *           参数输入: yuv420p_1280x720.yuv yuv420p_1280x720.h264 libx264
 * @param argc
 * @param argv
 * @return
 */
int main(int argc, char **argv)
{
    char *in_yuv_file = NULL;
    char *out_h264_file = NULL;
    FILE *infile = NULL;
    FILE *outfile = NULL;

    const char *codec_name = NULL;
    const AVCodec *codec = NULL;
    AVCodecContext *codec_ctx= NULL;
    AVFrame *frame = NULL;
    AVPacket *pkt = NULL;
    int ret = 0;

    if (argc < 4) {
        fprintf(stderr, "Usage: %s <input_file out_file codec_name >, argc:%d\n",
                argv[0], argc);
        return 0;
    }
    in_yuv_file = argv[1];      // 输入YUV文件
    out_h264_file = argv[2];
    codec_name = argv[3];

    /* 查找指定的编码器 */
    codec = avcodec_find_encoder_by_name(codec_name);
    if (!codec) {
        fprintf(stderr, "Codec '%s' not found\n", codec_name);
        exit(1);
    }

    codec_ctx = avcodec_alloc_context3(codec);
    if (!codec_ctx) {
        fprintf(stderr, "Could not allocate video codec context\n");
        exit(1);
    }


    /* 设置分辨率*/
    codec_ctx->width = 1280;
    codec_ctx->height = 720;
    /* 设置time base */
    codec_ctx->time_base = (AVRational){1, 25};
    codec_ctx->framerate = (AVRational){25, 1};
    /* 设置I帧间隔
     * 如果frame->pict_type设置为AV_PICTURE_TYPE_I, 则忽略gop_size的设置,一直当做I帧进行编码
     */
    codec_ctx->gop_size = 25;   // I帧间隔
    codec_ctx->max_b_frames = 2; // 如果不想包含B帧则设置为0
    codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
    //
    if (codec->id == AV_CODEC_ID_H264) {
        // 相关的参数可以参考libx264.c的 AVOption options
        // ultrafast all encode time:2270ms
        // medium all encode time:5815ms
        // veryslow all encode time:19836ms
        ret = av_opt_set(codec_ctx->priv_data, "preset", "medium", 0);
        if(ret != 0) {
            printf("av_opt_set preset failed\n");
        }
        ret = av_opt_set(codec_ctx->priv_data, "profile", "main", 0); // 默认是high
        if(ret != 0) {
            printf("av_opt_set profile failed\n");
        }
        ret = av_opt_set(codec_ctx->priv_data, "tune","zerolatency",0); // 直播是才使用该设置
//        ret = av_opt_set(codec_ctx->priv_data, "tune","film",0); //  画质film
        if(ret != 0) {
            printf("av_opt_set tune failed\n");
        }
    }

    /*
     * 设置编码器参数
    */
    /* 设置bitrate */
    codec_ctx->bit_rate = 3000000;
//    codec_ctx->rc_max_rate = 3000000;
//    codec_ctx->rc_min_rate = 3000000;
//    codec_ctx->rc_buffer_size = 2000000;
//    codec_ctx->thread_count = 4;  // 开了多线程后也会导致帧输出延迟, 需要缓存thread_count帧后再编程。
//    codec_ctx->thread_type = FF_THREAD_FRAME; // 并 设置为FF_THREAD_FRAME
    /* 对于H264 AV_CODEC_FLAG_GLOBAL_HEADER  设置则只包含I帧,此时sps pps需要从codec_ctx->extradata读取
     *  不设置则每个I帧都带 sps pps sei
     */
//    codec_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; // 存本地文件时不要去设置

    /* 将codec_ctx和codec进行绑定 */
    ret = avcodec_open2(codec_ctx, codec, NULL);
    if (ret < 0) {
        fprintf(stderr, "Could not open codec: %s\n", av_err2str(ret));
        exit(1);
    }
    printf("thread_count: %d, thread_type:%d\n", codec_ctx->thread_count, codec_ctx->thread_type);
    // 打开输入和输出文件
    infile = fopen(in_yuv_file, "rb");
    if (!infile) {
        fprintf(stderr, "Could not open %s\n", in_yuv_file);
        exit(1);
    }
    outfile = fopen(out_h264_file, "wb");
    if (!outfile) {
        fprintf(stderr, "Could not open %s\n", out_h264_file);
        exit(1);
    }

    // 分配pkt和frame
    pkt = av_packet_alloc();
    if (!pkt) {
        fprintf(stderr, "Could not allocate video frame\n");
        exit(1);
    }
    frame = av_frame_alloc();
    if (!frame) {
        fprintf(stderr, "Could not allocate video frame\n");
        exit(1);
    }

    // 为frame分配buffer
    frame->format = codec_ctx->pix_fmt;
    frame->width  = codec_ctx->width;
    frame->height = codec_ctx->height;
    ret = av_frame_get_buffer(frame, 0);
    if (ret < 0) {
        fprintf(stderr, "Could not allocate the video frame data\n");
        exit(1);
    }
    // 计算出每一帧的数据 像素格式 * 宽 * 高
    // 1382400
    int frame_bytes = av_image_get_buffer_size(frame->format, frame->width,
                                               frame->height, 1);
    printf("frame_bytes %d\n", frame_bytes);
    uint8_t *yuv_buf = (uint8_t *)malloc(frame_bytes);
    if(!yuv_buf) {
        printf("yuv_buf malloc failed\n");
        return 1;
    }
    int64_t begin_time = get_time();
    int64_t end_time = begin_time;
    int64_t all_begin_time = get_time();
    int64_t all_end_time = all_begin_time;
    int64_t pts = 0;
    printf("start enode\n");
    for (;;) {
        memset(yuv_buf, 0, frame_bytes);
        size_t read_bytes = fread(yuv_buf, 1, frame_bytes, infile);
        if(read_bytes <= 0) {
            printf("read file finish\n");
            break;
        }
        /* 确保该frame可写, 如果编码器内部保持了内存参考计数,则需要重新拷贝一个备份
            目的是新写入的数据和编码器保存的数据不能产生冲突
        */
        int frame_is_writable = 1;
        if(av_frame_is_writable(frame) == 0) { // 这里只是用来测试
            printf("the frame can't write, buf:%p\n", frame->buf[0]);
            if(frame->buf && frame->buf[0])        // 打印referenc-counted,必须保证传入的是有效指针
                printf("ref_count1(frame) = %d\n", av_buffer_get_ref_count(frame->buf[0]));
            frame_is_writable = 0;
        }
        ret = av_frame_make_writable(frame);
        if(frame_is_writable == 0) {  // 这里只是用来测试
            printf("av_frame_make_writable, buf:%p\n", frame->buf[0]);
            if(frame->buf && frame->buf[0])        // 打印referenc-counted,必须保证传入的是有效指针
                printf("ref_count2(frame) = %d\n", av_buffer_get_ref_count(frame->buf[0]));
        }
        if(ret != 0) {
            printf("av_frame_make_writable failed, ret = %d\n", ret);
            break;
        }
        int need_size = av_image_fill_arrays(frame->data, frame->linesize, yuv_buf,
                                             frame->format,
                                             frame->width, frame->height, 1);
        if(need_size != frame_bytes) {
            printf("av_image_fill_arrays failed, need_size:%d, frame_bytes:%d\n",
                   need_size, frame_bytes);
            break;
        }
         pts += 40;
        // 设置pts
        frame->pts = pts;       // 使用采样率作为pts的单位,具体换算成秒 pts*1/采样率
        begin_time = get_time();
        ret = encode(codec_ctx, frame, pkt, outfile);
        end_time = get_time();
        printf("encode time:%lldms\n", end_time - begin_time);
        if(ret < 0) {
            printf("encode failed\n");
            break;
        }
    }

    /* 冲刷编码器 */
    encode(codec_ctx, NULL, pkt, outfile);
    all_end_time = get_time();
    printf("all encode time:%lldms\n", all_end_time - all_begin_time);
    // 关闭文件
    fclose(infile);
    fclose(outfile);

    // 释放内存
    if(yuv_buf) {
        free(yuv_buf);
    }

    av_frame_free(&frame);
    av_packet_free(&pkt);
    avcodec_free_context(&codec_ctx);

    printf("main finish, please enter Enter and exit\n");
    getchar();
    return 0;
}


版权声明:本文为weixin_41643938原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。