草庐IT

FFmpeg编码(YUV转H264)并改变视频分辨率示例

程序媛zcx 2023-10-10 原文

最简单的基于FFmpeg的编码器-纯净版(不包含libavformat)_雷霄骅的博客-CSDN博客_ffmpeg 编码器

初学音视频、ffmpeg。根据雷神的例子跑起来,调用libavcodec将YUV像素数据(YUV420P)编码为H.264码流,H.265为(HEVC)。

视频编码:

视频编码方式就是指通过特定的压缩技术,将某个视频格式的文件转换成另一种视频格式文件的方式。视频编码格式常见到的有:MPEG-2 TS、Divx、Xvid、H.264、WMV-HD和VC-1。

原始的图像和声音是需要占用很大的存储空间和带宽的,不适合运输和传送(例如例子的yuv数据),所以我们需要对原始图像和声音加工,压缩得更小。就是图像和声音的压缩方法。

 

编码流程:

  • 首先要有未压缩的 YUV 原始数据。
  • 其次要根据想要编码的格式选择特定的编码器。
  • 最后编码器的输出即为编码后的视频帧。

示例里编码完的ds.h264 文件在vlc 里播放可以看到分辨率为 480x272

现在研究把480x272 的输分辨率改为 320x270,需要使用以下函数:

FFmpeg中的 sws_scale() 函数主要是用来做视频像素格式和分辨率的转换。

// 初始化
struct SwsContext *sws_ctx(
            int srcW, /* 输入图像的宽度 */
            int srcH, /* 输入图像的宽度 */
            enum AVPixelFormat srcFormat, /* 输入图像的像素格式 */
            int dstW, /* 输出图像的宽度 */
            int dstH, /* 输出图像的高度 */
            enum AVPixelFormat dstFormat, /* 输出图像的像素格式 */
            int flags,/* 选择缩放算法(只有当输入输出图像大小不同时有效),一般选择SWS_FAST_BILINEAR */
            SwsFilter *srcFilter, /* 输入图像的滤波器信息, 若不需要传NULL */
            SwsFilter *dstFilter, /* 输出图像的滤波器信息, 若不需要传NULL */
            const double *param /* 特定缩放算法需要的参数(?),默认为NULL */
            );

// 转换的函数  
sws_scale(sws_ctx, src_frame->data, src_frame->linesize,  
          0, height, //源图像的高  
          dst_frame->data, dst_frame->linesize); 
    1.参数 SwsContext *c, 转换格式的上下文。也就是 sws_getContext 函数返回的结果。
    2.参数 const uint8_t *const srcSlice[], 输入图像的每个颜色通道的数据指针。其实就是解码后的        AVFrame中的data[]数组。因为不同像素的存储格式不同,所以srcSlice[]维数也有可能不同。
以YUV420P为例,它是planar格式,它的内存中的排布如下:
    YYYYYYYY UUUU VVVV
    使用FFmpeg解码后存储在AVFrame的data[]数组中时:
    data[0]——-Y分量, Y1, Y2, Y3, Y4, Y5, Y6, Y7, Y8……
    data[1]——-U分量, U1, U2, U3, U4……
    data[2]——-V分量, V1, V2, V3, V4……
    linesize[]数组中保存的是对应通道的数据宽度 ,
    linesize[0]——-Y分量的宽度
    linesize[1]——-U分量的宽度
    linesize[2]——-V分量的宽度

    而RGB24,它是packed格式,它在data[]数组中则只有一维,它在存储方式如下:
    data[0]: R1, G1, B1, R2, G2, B2, R3, G3, B3, R4, G4, B4……
    这里要特别注意,linesize[0]的值并不一定等于图片的宽度,有时候为了对齐各解码器的CPU,实际尺寸会大于图片的宽度,这点在我们编程时(比如OpengGL硬件转换/渲染)要特别注意,否则解码出来的图像会异常。

    3.参数const int srcStride[],输入图像的每个颜色通道的跨度。.也就是每个通道的行字节数,对应的是解码后的AVFrame中的linesize[]数组。根据它可以确立下一行的起始位置,不过stride和width不一定相同,这是因为:
a.由于数据帧存储的对齐,有可能会向每行后面增加一些填充字节这样 stride = width + N;
b.packet色彩空间下,每个像素几个通道数据混合在一起,例如RGB24,每个像素3字节连续存放,因此下一行的位置需要跳过3*width字节。

    4.参数int srcSliceY, int srcSliceH,定义在输入图像上处理区域,srcSliceY是起始位置,srcSliceH是处理多少行。如果srcSliceY=0,srcSliceH=height,表示一次性处理完整个图像。这种设置是为了多线程并行,例如可以创建两个线程,第一个线程处理 [0, h/2-1]行,第二个线程处理 [h/2, h-1]行。并行处理加快速度。
5.参数uint8_t *const dst[], const int dstStride[]定义输出图像信息(输出的每个颜色通道数据指针,每个颜色通道行字节数)


// 释放sws_scale
sws_freeContext(sws_ctx); 

在雷神代码基础上修改主要加了以上函数,加了一些不懂函数的注释。现在把代码贴出来,主要要区分开输入数据和输出数据:

#include <QApplication>


#include <stdio.h>

#define __STDC_CONSTANT_MACROS

#ifdef _WIN32
//Windows
extern "C"
{
#include "libavutil/opt.h"
#include "libavcodec/avcodec.h"
#include "libavutil/imgutils.h"
};
#else
//Linux...
#ifdef __cplusplus
extern "C"
{
#endif
#include <libavutil/opt.h>
#include <libavcodec/avcodec.h>
#include <libavutil/imgutils.h>
#ifdef __cplusplus
};
#endif
#endif

//test different codec
#define TEST_H264  1
#define TEST_HEVC  0// H265


int main(int argc, char* argv[])
{
    AVCodec *pCodec;
    AVCodecContext *pCodecCtx= NULL;
    int i, ret, got_output;
    FILE *fp_in;
    FILE *fp_out;
    //    AVFrame *src_frame;//存储一帧未编码的像素数据。
    AVFrame *src_frame, *dst_frame;// 输入和输出
    AVPacket pkt;//存储一帧压缩编码数据。
    int y_size;
    int framecnt=0;

    char filename_in[]="../ds_480x272.yuv";

    static struct SwsContext *img_convert_ctx;


#if TEST_HEVC
    AVCodecID codec_id=AV_CODEC_ID_HEVC;
    char filename_out[]="ds.hevc";
#else
    AVCodecID codec_id=AV_CODEC_ID_H264;
    char filename_out[]="ds.h264";
#endif

    int src_w = 480, src_h = 272;
    int dst_w = 320, dst_h = 270;
    int framenum = 100;

    avcodec_register_all();//注册所有编解码器相关的组件。av_register_all():注册所有的编解码器,复用/解复用器等等组件

    pCodec = avcodec_find_encoder(codec_id);//查找ID值为AV_CODEC_ID_H264的AAC编码器
    if (!pCodec) {
        printf("Codec not found\n");
        return -1;
    }
    pCodecCtx = avcodec_alloc_context3(pCodec); // 为AVCodecContext分配内存 创建AVCodecContext结构体。AVCodecContext中很多的参数是编码的时候使用
    if (!pCodecCtx) {
        printf("Could not allocate video codec context\n");
        return -1;
    }
    pCodecCtx->bit_rate = 400000;//平均比特率 目标的码率,即采样的码率;显然,采样码率越大,视频大小越大
    pCodecCtx->width = dst_w;//如果是视频的话,代表宽和高 编码目标的视频帧大小,以像素为单位
    pCodecCtx->height = dst_h;
    //帧率的基本单位,我们用分数来表示,
    //用分数来表示的原因是,有很多视频的帧率是带小数的eg:NTSC 使用的帧率是29.97
    pCodecCtx->time_base.num=1; //根据该参数,可以把PTS转化为实际的时间(单位为秒s)
    pCodecCtx->time_base.den=25;
    pCodecCtx->gop_size = 10;//一组图片中的图片数
    //两个非B帧之间允许出现多少个B帧数
    //设置0表示不使用B帧
    //b 帧越多,图片越小
    pCodecCtx->max_b_frames = 1;
    pCodecCtx->pix_fmt = AV_PIX_FMT_YUV420P;//像素格式 也就是说采用什么样的色彩空间来表明一个像素点
    //设置显示的率
    pCodecCtx->framerate = {10, 1};
    //量化参数设置(影响视频清晰度) 越小越清晰
    pCodecCtx->qmax = 51;
    pCodecCtx->qmin = 10;

    if (codec_id == AV_CODEC_ID_H264)
        av_opt_set(pCodecCtx->priv_data, "preset", "slow", 0);

    if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) { //打开编码器。
        printf("Could not open codec\n");
        return -1;
    }

    src_frame = av_frame_alloc();//分配AVFrame AVFrame结构体一般用于存储原始数据(即非压缩数据,例如对视频来说是YUV,RGB,对音频来说是PCM
    if (!src_frame) {
        printf("Could not allocate video frame\n");
        return -1;
    }
    src_frame->format = pCodecCtx->pix_fmt; //帧的格式
    src_frame->width  = src_w;
    src_frame->height = src_h;//视频帧宽和高(1920x1080,1280x720...)



    dst_frame = av_frame_alloc();
    dst_frame->width  = dst_w;
    dst_frame->height = dst_h;  //初始化一个SwsContext
    img_convert_ctx = sws_getContext(src_frame->width, src_frame->height, \
                                     pCodecCtx->pix_fmt, dst_frame->width, dst_frame->height, \
                                     pCodecCtx->pix_fmt, 0, nullptr, nullptr, nullptr);

//    int dst_bytes_num = avpicture_get_size(pCodecCtx->pix_fmt, dst_frame->width, dst_frame->height);//计算这个格式的图片,需要多少字节来存储
//    uint8_t* dst_buff = (uint8_t *)av_malloc(dst_bytes_num * sizeof(uint8_t));//申请空间来存放图片数据。包含源数据和目标数据
    //    前面的av_frame_alloc函数,只是为这个AVFrame结构体分配了内存,
    //    而该类型的指针指向的内存还没分配。这里把av_malloc得到的内存和AVFrame关联起来。
    //    当然,其还会设置AVFrame的其他成员
//    avpicture_fill((AVPicture *)dst_frame, dst_buff, pCodecCtx->pix_fmt, dst_frame->width, dst_frame->height);//ffmpeg4.2.2中就已经被抛弃了, 取而代之的是av_image_fill_arrays()

//以上注释的代码也能实现 av_image_alloc
    av_image_alloc(dst_frame->data, dst_frame->linesize, dst_frame->width, dst_frame->height,
                         pCodecCtx->pix_fmt, 16);

    ret = av_image_alloc(src_frame->data, src_frame->linesize, src_frame->width, src_frame->height,
                         pCodecCtx->pix_fmt, 16);//按照指定的宽、高、像素格式来分析图像内存
    //    pointers[4]:保存图像通道的地址。如果是RGB,则前三个指针分别指向R,G,B的内存地址。第四个指针保留不用
    //    linesizes[4]:保存图像每个通道的内存对齐的步长,即一行的对齐内存的宽度,此值大小等于图像宽度。
    //     w:                 要申请内存的图像宽度。
    //     h:                  要申请内存的图像高度。
    //     pix_fmt:        要申请内存的图像的像素格式。
    //     align:            用于内存对齐的值。
    //     返回值:所申请的内存空间的总大小。如果是负值,表示申请失败。
    if (ret < 0) {
        printf("Could not allocate raw picture buffer\n");
        return -1;
    }
    //Input raw data
    fp_in = fopen(filename_in, "rb");//使用给定的模式 mode 打开 filename 所指向的文件。 rb+读写打开一个二进制文件,允许读数据。
    if (!fp_in) {
        printf("Could not open %s\n", filename_in);
        return -1;
    }
    //Output bitstream
    fp_out = fopen(filename_out, "wb");//wb 只写打开或新建一个二进制文件;只允许写数据。
    if (!fp_out) {
        printf("Could not open %s\n", filename_out);
        return -1;
    }

    y_size = src_frame->width * src_frame->height;
    //Encode
    for (i = 0; i < framenum; i++) {
        av_init_packet(&pkt);//设置默认值
        pkt.data = NULL;    // packet data will be allocated by the encoder
        pkt.size = 0;   //从给定流 fp_in 读取数据到 src_frame->data[0] 所指向的数组中。

        //Read raw YUV data
        if (fread(src_frame->data[0],1,y_size,fp_in)<= 0||		// Y
                fread(src_frame->data[1],1,y_size/4,fp_in)<= 0||	// U
                fread(src_frame->data[2],1,y_size/4,fp_in)<= 0){	// V
            return -1;
        }else if(feof(fp_in)){//测试给定流 fp_in 的文件结束标识符。
            break;
        }

        sws_scale(img_convert_ctx, (uint8_t const * const *)src_frame->data,
                  src_frame->linesize, 0, src_frame->height, dst_frame->data, dst_frame->linesize);


        src_frame->pts = i;
        /* encode the image */
        ret = avcodec_encode_video2(pCodecCtx, &pkt, dst_frame, &got_output);//编码一帧数据。要传转换完的 dst_frame

        if (ret < 0) {
            printf("Error encoding frame\n");
            return -1;
        }
        if (got_output) {
            printf("Succeed to encode frame: %5d\tsize:%5d\n",framecnt,pkt.size);
            framecnt++;
            fwrite(pkt.data, 1, pkt.size, fp_out);//把 pkt.data 所指向的数组中的数据写入到给定流 fp_out 中
            av_free_packet(&pkt);//清空pkt中data以及buf的内容,并没有把pkt的指针清空
        }
    }
    //Flush Encoder
    for (got_output = 1; got_output; i++) {
        ret = avcodec_encode_video2(pCodecCtx, &pkt, NULL, &got_output);
        if (ret < 0) {
            printf("Error encoding frame\n");
            return -1;
        }
        if (got_output) {
            printf("Flush Encoder: Succeed to encode 1 frame!\tsize:%5d\n",pkt.size);
            fwrite(pkt.data, 1, pkt.size, fp_out);
            av_free_packet(&pkt);
        }
    }

    fclose(fp_out);//关闭流 fp_out。刷新所有的缓冲区。
    avcodec_close(pCodecCtx);//关闭编码器
    av_free(pCodecCtx);//释放已分配av_malloc() AVCodecContext
    av_freep(&src_frame->data[0]);//释放并清理指针
    av_frame_free(&src_frame);//释放AVFrame
    av_frame_free(&dst_frame);//释放AVFrame
	sws_freeContext(img_convert_ctx);	
    return 0;
}

下面是转换完的视频: 

 参考文档:

FFmpeg编码基础流程_zhaodb_的博客-CSDN博客_ffmpeg编码流程 FFmpeg: FFmepg中的sws_scale() 函数分析 - 夜行过客 - 博客园

图像视频编码和FFmpeg(3)-----用FFmpeg进行图像格式转换和AVFrame简介 avpicture_fill - bw_0927 - 博客园ffmpeg的API函数用法 :sws_scale函数的用法-具体应用 - 怀想天空2013 - 博客园图像视频编码和FFmpeg(3)-----用FFmpeg进行图像格式转换和AVFrame简介 avpicture_fill - bw_0927 - 博客园

有关FFmpeg编码(YUV转H264)并改变视频分辨率示例的更多相关文章

  1. ruby - 什么是填充的 Base64 编码字符串以及如何在 ruby​​ 中生成它们? - 2

    我正在使用的第三方API的文档状态:"[O]urAPIonlyacceptspaddedBase64encodedstrings."什么是“填充的Base64编码字符串”以及如何在Ruby中生成它们。下面的代码是我第一次尝试创建转换为Base64的JSON格式数据。xa=Base64.encode64(a.to_json) 最佳答案 他们说的padding其实就是Base64本身的一部分。它是末尾的“=”和“==”。Base64将3个字节的数据包编码为4个编码字符。所以如果你的输入数据有长度n和n%3=1=>"=="末尾用于填充n%

  2. ruby - 用逗号、双引号和编码解析 csv - 2

    我正在使用ruby​​1.9解析以下带有MacRoman字符的csv文件#encoding:ISO-8859-1#csv_parse.csvName,main-dialogue"Marceu","Giveittohimóhe,hiswife."我做了以下解析。require'csv'input_string=File.read("../csv_parse.rb").force_encoding("ISO-8859-1").encode("UTF-8")#=>"Name,main-dialogue\r\n\"Marceu\",\"Giveittohim\x97he,hiswife.\"\

  3. C# 到 Ruby sha1 base64 编码 - 2

    我正在尝试在Ruby中复制Convert.ToBase64String()行为。这是我的C#代码:varsha1=newSHA1CryptoServiceProvider();varpasswordBytes=Encoding.UTF8.GetBytes("password");varpasswordHash=sha1.ComputeHash(passwordBytes);returnConvert.ToBase64String(passwordHash);//returns"W6ph5Mm5Pz8GgiULbPgzG37mj9g="当我在Ruby中尝试同样的事情时,我得到了相同sha

  4. postman——集合——执行集合——测试脚本——pm对象简单示例02 - 2

    //1.验证返回状态码是否是200pm.test("Statuscodeis200",function(){pm.response.to.have.status(200);});//2.验证返回body内是否含有某个值pm.test("Bodymatchesstring",function(){pm.expect(pm.response.text()).to.include("string_you_want_to_search");});//3.验证某个返回值是否是100pm.test("Yourtestname",function(){varjsonData=pm.response.json

  5. python ffmpeg 使用 pyav 转换 一组图像 到 视频 - 2

    2022/8/4更新支持加入水印水印必须包含透明图像,并且水印图像大小要等于原图像的大小pythonconvert_image_to_video.py-f30-mwatermark.pngim_dirout.mkv2022/6/21更新让命令行参数更加易用新的命令行使用方法pythonconvert_image_to_video.py-f30im_dirout.mkvFFMPEG命令行转换一组JPG图像到视频时,是将这组图像视为MJPG流。我需要转换一组PNG图像到视频,FFMPEG就不认了。pyav内置了ffmpeg库,不需要系统带有ffmpeg工具因此我使用ffmpeg的python包装p

  6. [工业相机] 分辨率、精度和公差之间的关系 - 2

    📢博客主页:https://blog.csdn.net/weixin_43197380📢欢迎点赞👍收藏⭐留言📝如有错误敬请指正!📢本文由Loewen丶原创,首发于CSDN,转载注明出处🙉📢现在的付出,都会是一种沉淀,只为让你成为更好的人✨文章预览:一.分辨率(Resolution)1、工业相机的分辨率是如何定义的?2、工业相机的分辨率是如何选择的?二.精度(Accuracy)1、像素精度(PixelAccuracy)2、定位精度和重复定位精度(RepeatPrecision)三.公差(Tolerance)四.课后作业(Post-ClassExercises)视觉行业的初学者,甚至是做了1~2年

  7. ruby - 无法在 Ruby 中将 ffmpeg 作为子进程运行 - 2

    我正在尝试使用以下代码通过将ffmpeg实用程序作为子进程运行并获取其输出并解析它来确定视频分辨率:IO.popen'ffmpeg-i'+path_to_filedo|ffmpegIO|#myparsegoeshereend...但是ffmpeg输出仍然连接到标准输出并且ffmepgIO.readlines是空的。ffmpeg实用程序是否需要一些特殊处理?或者还有其他方法可以获得ffmpeg输出吗?我在WinXP和FedoraLinux下测试了这段代码-结果是一样的。 最佳答案 要跟进mouviciel的评论,您需要使用类似pope

  8. ruby-on-rails - 有没有一种工具可以在编码时自动保存对文件的增量更改? - 2

    我最喜欢的Google文档功能之一是它会在我工作时不断自动保存我的文档版本。这意味着即使我在进行关键更改之前忘记在某个点进行保存,也很有可能会自动创建一个保存点。至少,我可以将文档恢复到错误更改之前的状态,并从该点继续工作。对于在MacOS(或UNIX)上运行的Ruby编码器,是否有具有等效功能的工具?例如,一个工具会每隔几分钟自动将Gitcheckin我的本地存储库以获取我正在处理的文件。也许我有点偏执,但这点小保险可以让我在日常工作中安心。 最佳答案 虚拟机有些人可能讨厌我对此的回应,但我在编码时经常使用VIM,它具有自动保存功

  9. ruby - 改变替换的大小写 - 2

    我有以下内容:text.gsub(/(lower)(upper)/,'\1\2')我可以将\2替换为大写吗?类似于:sed-e's/\(abc\)/\U\1/'这在Ruby中可行吗? 最佳答案 查看gsub文档:str.gsub(模式){|匹配|block}→new_str在block形式中,当前匹配字符串作为参数传入,$1、$2、$`、$&、$'等变量将被适当设置。block返回的值将替换为每次调用的匹配项。"alowerupperb".gsub(/(lower)(upper)/){|s|$1+""+$2.upcase}

  10. Ruby-vips 图像处理库。有什么好的使用示例吗? - 2

    我对图像处理完全陌生。我对JPEG内部是什么以及它是如何工作一无所知。我想知道,是否可以在某处找到执行以下简单操作的ruby​​代码:打开jpeg文件。遍历每个像素并将其颜色设置为fx绿色。将结果写入另一个文件。我对如何使用ruby​​-vips库实现这一点特别感兴趣https://github.com/ender672/ruby-vips我的目标-学习如何使用ruby​​-vips执行基本的图像处理操作(Gamma校正、亮度、色调……)任何指向比“helloworld”更复杂的工作示例的链接——比如ruby​​-vips的github页面上的链接,我们将不胜感激!如果有ruby​​-

随机推荐