简介

前面使用 SDL 显示了一张YUV图片以及YUV视频。接下来使用Qt中的QImage来实现一个简单的 YUV 播放器,查看QImage支持的像素格式,你会发现QImage仅支持显示RGB像素格式数据,并不支持直接显示YUV像素格式数据,但是YUV和RGB之间是可以相互转换的,我们将YUV像素格式数据转换成RGB像素格式数据就可以使用QImage显示了。

YUV转RGB常见有三种方式:

  1. 使用 FFmpeg 提供的库 libswscale :

    优点:同一个函数实现了像素格式转换和分辨率缩放以及前后图像滤波处理;

    缺点:速度慢。
  2. 使用 Google 提供的 libyuv:

    优点:兼容性好功能全面;速度快,仅次于 OpenGL shader;

    缺点:暂无。
  3. 使用 OpenGL shader:

    优点:速度快,不增加包体积;

    缺点:兼容性一般。

下面主要介绍如何使用FFmpeg提供的库libswscale进行转换,其他转换方式将会在后面介绍。

1、像素格式转换核心函数sws_scale

sws_scale函数主要是用来做像素格式和分辨率的转换,每次转换一帧数据:

int sws_scale(struct SwsContext *c, const uint8_t *const srcSlice[],
const int srcStride[], int srcSliceY, int srcSliceH,
uint8_t *const dst[], const int dstStride[]);

参数说明:

  • c:转换上下文,可以通过函数sws_getContext创建;
  • srcSlice[]:输入缓冲区,元素指向一帧中每个平面的数据,以yuv420p为例,{指向每帧中Y平面数据的指针,指向每帧中U平面数据的指针,指向每帧中V平面数据的指针,null}
  • srcStride[]:每个平面一行的大小,以yuv420p为例,{每帧中Y平面一行的长度,每帧中U平面一行的长度,每帧中U平面一行的长度,0}
  • srcSliceY:输入图像上开始处理区域的起始位置。
  • srcSliceH:处理多少行。如果srcSliceY = 0,srcSliceH = height,表示一次性处理完整个图像。这种设置是为了多线程并行,例如可以创建两个线程,第一个线程处理[0, h/2-1]行,第二个线程处理[h/2, h-1]行,并行处理加快速度。
  • dst[]:输出的图像数据,和输入参数srcSlice[]类似。
  • dstStride[]:和输入参数srcStride[]类似。

注意:sws_scale 函数不会为传入的输入数据和输出数据创建堆空间。

2、获取转换上下文函数

struct SwsContext *sws_getContext(int srcW, int srcH, enum AVPixelFormat srcFormat,
int dstW, int dstH, enum AVPixelFormat dstFormat,
int flags, SwsFilter *srcFilter,
SwsFilter *dstFilter, const double *param);

参数说明:

  • srcW, srcH, srcFormat:输入图像宽高和输入图像像素格式(我们这里输入图像像素格式是yuv420p);
  • dstW, dstH, dstFormat:输出图像宽高和输出图像像素格式(我们这里输出图像像素格式是rgb24),不仅可以转换像素格式,也可以分辨率缩放;
  • flag:指定使用何种算法,例如快速线性、差值和矩阵等等,不同的算法性能也不同,快速线性算法性能相对较高。只针对尺寸的变换。
    /* values for the flags, the stuff on the command line is different */
    #define SWS_FAST_BILINEAR 1
    #define SWS_BILINEAR 2
    #define SWS_BICUBIC 4
    #define SWS_X 8
    #define SWS_POINT 0x10
    #define SWS_AREA 0x20
    #define SWS_BICUBLIN 0x40
    #define SWS_GAUSS 0x80
    #define SWS_SINC 0x100
    #define SWS_LANCZOS 0x200
    #define SWS_SPLINE 0x400
  • srcFilter, stFilter:这两个参数是做过滤器用的,目前暂时没有用到,传nullptr即可;
  • param:和flag算法相关,也可以传nullptr
  • 返回值:成功返回转换格式上下文指针,失败返回 NULL;

注意:sws_getContext函数注释中有提示我们最后使用完上下文不要忘记调用函数sws_freeContext释放,一般函数名中有create或者alloc等单词的函数需要我们释放,为什么调用sws_getContext后也需要释放呢?此时我们可以参考一下源码:

ffmpeg-4.3.2/libswscale/utils.c

发现源码当中调用了sws_alloc_set_opts,所以最后是需要释放上下文的。当然我们也可以使用如下方式创建转换上下文,最后同样需要调用sws_freeContext释放上下文:

ctx = sws_alloc_context();
av_opt_set_int(ctx, "srcw", in.width, 0);
av_opt_set_int(ctx, "srch", in.height, 0);
av_opt_set_pixel_fmt(ctx, "src_format", in.format, 0);
av_opt_set_int(ctx, "dstw", out.width, 0);
av_opt_set_int(ctx, "dsth", out.height, 0);
av_opt_set_pixel_fmt(ctx, "dst_format", out.format, 0);
av_opt_set_int(ctx, "sws_flags", SWS_BILINEAR, 0); if (sws_init_context(ctx, nullptr, nullptr) < 0) {
// sws_freeContext(ctx);
goto end;
}

3、创建输入输出缓冲区

首先我们创建需要的局部变量:

// 输入/输出缓冲区,元素指向每帧中每一个平面的数据
uint8_t *inData[4], *outData[4];
// 每个平面一行的大小
int inStrides[4], outStrides[4];
// 每一帧图像的大小
int inFrameSize, outFrameSize; // 此处需要注意的是下面写法是错误的,*是跟着最右边的变量名的:
// uint8_t *inData[4], outData[4];
// 其等价于:
// uint8_t *inData[4];
// uint8_t outData[4];

我们创建好了输入输出缓冲区变量,然后需要为输入输出缓冲区各开辟一块堆空间(sws_scale函数不会为我们开辟输入输出缓冲区堆空间,可查看源码),FFmpeg为我们提供了现成的函数av_image_alloc

ret = av_image_alloc(inData, inStrides, in.width, in.height, in.format, 1);
ret = av_image_alloc(outData, outStrides, out.width, out.height, out.format, 1); // 最后不要忘记释放输入输出缓冲区
av_freep(&inData[0]);
av_freep(&outData[0]);

建议inData数组和inStrides数组的大小是4,虽然我们目前的输入像素格式yuv420p有 Y 、U 和 V 共 3 个平面,但是有可能会有 4 个平面的情况,比如可能会多1个透明度平面。有多少个平面取决于像素格式。

以 yuv420p 像素格式数据举例:

如何让inData[0]、inData[1]、inData[2]指向Y、U、V平面数据呢?

1、分别指向各自堆空间



每一帧图片的YUV是紧挨在一起的,如果YUV分别创建各自的堆空间,到时候还需要将它们分别拷贝到各自的堆空间中,比较麻烦。

2、指向同一个堆空间

YUV在同一个堆空间里面,而这个堆空间的大小正好是一帧的大小

// 每一帧的 Y 平面数据、U 平面数据和 V 平面数据是紧挨在一起的
// inData[0] -> Y 平面数据
// inData[1] -> U 平面数据
// inData[2] -> V 平面数据
inData[0] = (uint8_t *)malloc(inFrameSize);
inData[1] = inData[0] + 每帧中Y平面数据长度;
inData[2] = inData[0] + 每帧中Y平面数据长度 + 每帧中U平面数据长度;

关于inStrides的理解,inStrides中存放的是每个平面每一行的大小也相当于是linesizes,以当前输入数据举例(视频宽高:640x480 像素格式:yuv420p):

Y 平面:
------ 640列 ------
YY...............YY |
YY...............YY |
YY...............YY |
................... 480行
YY...............YY |
YY...............YY |
YY...............YY | U 平面:
--- 320列 ---
UU........UU |
UU........UU |
............ 240行
UU........UU |
UU........UU | V 平面:
--- 320列 ---
VV........VV |
VV........VV |
............ 240行
VV........VV |
VV........VV | inStrides[0] = Y 平面每一行的大小 = 640
inStrides[1] = U 平面每一行的大小 = 320
inStrides[2] = V 平面每一行的大小 = 320
640x480,rgb24

-------  640个RGB ------
RGB RGB .... RGB RGB |
RGB RGB .... RGB RGB |
RGB RGB .... RGB RGB
RGB RGB .... RGB RGB 480行
RGB RGB .... RGB RGB
RGB RGB .... RGB RGB |
RGB RGB .... RGB RGB |
RGB RGB .... RGB RGB | RGR只有一个平面
一个平面的行大小640 * 3 = 1920

在QT中我们通过debug运行后可以看到inStrides和outStrides数据内容:

我们也可以参考前面用到的开辟输入输出缓冲区函数av_image_alloc,调用函数时我们把inStrides传给了参数linesizeslinesizes就很好理解了是每一帧平面一行的大小。

// ffmpeg-4.3.2/libavutil/imgutils.h
int av_image_alloc(uint8_t *pointers[4], int linesizes[4],
int w, int h, enum AVPixelFormat pix_fmt, int align);

outDataoutStrides是同样的道理。输出像素格式rgb24只有1个平面(yuv444 packed像素格式也只有一个平面)。

示例代码:

在 .pro 中引入库:

win32{
FFMPEG_HOME = D:/SoftwareInstall/ffmpeg-4.3.2
} macx{
FFMPEG_HOME = /usr/local/ffmpeg
} INCLUDEPATH += $${FFMPEG_HOME}/include LIBS += -L$${FFMPEG_HOME}/lib \
-lavutil \
-lswscale

ffmpegutils.h:

#ifndef FFMPEGUTILS_H
#define FFMPEGUTILS_H
#define __STDC_CONSTANT_MACROS extern "C"{
#include <libavutil/avutil.h>
#include <libavutil/imgutils.h>
#include <libswscale/swscale.h>
} typedef struct {
const char *filename;
int width;
int height;
AVPixelFormat format;
} RawVideoFile; class FFmpegUtils
{
public:
FFmpegUtils();
static void convertRawVideo(RawVideoFile &in, RawVideoFile &out);
}; #endif // FFMPEGUTILS_H

ffmpegutils.cpp

#include "ffmpegutils.h"
#include <QFile>
#include <QDebug> FFmpegUtils::FFmpegUtils(){ } void FFmpegUtils::convertRawVideo(RawVideoFile &in, RawVideoFile &out){
int ret = 0;
// 转换上下文
SwsContext *ctx = nullptr;
// 输入/输出缓冲区,元素指向每帧中每一个平面的数据
uint8_t *inData[4], *outData[4];
// 每个平面一行的大小
int inStrides[4], outStrides[4];
// 每一帧图片的大小
int inFrameSize, outFrameSize; // 输入文件
QFile inFile(in.filename);
// 输出文件
QFile outFile(out.filename); // 创建输入缓冲区
ret = av_image_alloc(inData, inStrides, in.width, in.height, in.format, 1);
if(ret < 0){
char errbuf[1024];
av_strerror(ret,errbuf,sizeof (errbuf));
qDebug() << "av_image_alloc inData error:" << errbuf;
goto end;
} // 创建输出缓冲区
ret = av_image_alloc(outData, outStrides, out.width, out.height, out.format, 1);
if (ret < 0) {
char errbuf[1024];
av_strerror(ret, errbuf, sizeof (errbuf));
qDebug() << "av_image_alloc outData error:" << errbuf;
goto end;
} // 创建转换上下文
// 方式一:
ctx = sws_getContext(in.width, in.height, in.format,
out.width, out.height, out.format,
SWS_BILINEAR, nullptr, nullptr, nullptr);
if (!ctx) {
qDebug() << "sws_getContext error";
goto end;
} // 方式二:
// ctx = sws_alloc_context();
// av_opt_set_int(ctx, "srcw", in.width, 0);
// av_opt_set_int(ctx, "srch", in.height, 0);
// av_opt_set_pixel_fmt(ctx, "src_format", in.format, 0);
// av_opt_set_int(ctx, "dstw", out.width, 0);
// av_opt_set_int(ctx, "dsth", out.height, 0);
// av_opt_set_pixel_fmt(ctx, "dst_format", out.format, 0);
// av_opt_set_int(ctx, "sws_flags", SWS_BILINEAR, 0); // if (sws_init_context(ctx, nullptr, nullptr) < 0) {
// qDebug() << "sws_init_context error";
// goto end;
// } if (!inFile.open(QFile::ReadOnly)) {
qDebug() << "open in file failure";
goto end;
} if (!outFile.open(QFile::WriteOnly)) {
qDebug() << "open out file failure";
goto end;
} // 计算一帧图像大小
inFrameSize = av_image_get_buffer_size(in.format, in.width, in.height, 1);
outFrameSize = av_image_get_buffer_size(out.format, out.width, out.height, 1); while (inFile.read((char *)inData[0], inFrameSize) == inFrameSize) {
// 每一帧的转换
sws_scale(ctx, inData, inStrides, 0, in.height, outData, outStrides);
// 每一帧写入文件
outFile.write((char *)outData[0], outFrameSize);
}
end:
inFile.close();
outFile.close();
av_freep(&inData[0]);
av_freep(&outData[0]);
sws_freeContext(ctx);
}

main.cpp

#include <QApplication>
#include <QDebug>
#include "ffmpegutils.h" #ifdef Q_OS_WIN
#define INFILENAME "../test/out_640x480.yuv"
#define OUTFILENAME "../test/out.rgb"
#else
#define INFILENAME "/Users/zuojie/QtProjects/audio-video-dev/test/out_640x480.yuv"
#define OUTFILENAME "/Users/zuojie/QtProjects/audio-video-dev/test/out.rgb"
#endif int main(int argc, char *argv[]){ RawVideoFile in = {
INFILENAME,
640, 480, AV_PIX_FMT_YUV420P
};
RawVideoFile out = {
OUTFILENAME,
640, 480, AV_PIX_FMT_RGB24
};
FFmpegUtils::convertRawVideo(in, out); QApplication a(argc, argv);
MainWindow w;
w.show();
int ret = a.exec(); return ret;
}

程序运行后,回在指定文件夹中生成out.rgb文件,我们可以使用ffplay去播放改文件

ffplay -video_size 640x480 -pixel_format rgb24 out.rgb

上面方法是一个YUV文件直接转另外一个RGB文件,现在我们想要一帧YUV转一帧RGB,可以直接在上面的FFmpegUtils类中新增static void convertRawVideo(RawVideoFrame &in, RawVideoFrame &out);方法

现在ffmpegutils.h文件中新增struct和一个方法

typedef struct {
char *pixels;
int width;
int height;
AVPixelFormat format;
} RawVideoFrame; static void convertRawVideo(RawVideoFrame &in, RawVideoFrame &out);

然后在ffmpegutils.cpp文件中实现此方法

void FFmpegUtils::convertRawVideo(RawVideoFrame &in, RawVideoFrame &out){
int ret = 0;
// 转换上下文
SwsContext *ctx = nullptr;
// 输入/输出缓冲区,元素指向每帧中每一个平面的数据
uint8_t *inData[4], *outData[4];
// 每个平面一行的大小
int inStrides[4], outStrides[4];
// 每一帧图片的大小
int inFrameSize, outFrameSize; // 创建输入缓冲区
ret = av_image_alloc(inData, inStrides, in.width, in.height, in.format, 1);
if(ret < 0){
char errbuf[1024];
av_strerror(ret,errbuf,sizeof (errbuf));
qDebug() << "av_image_alloc inData error:" << errbuf;
goto end;
} // 创建输出缓冲区
ret = av_image_alloc(outData, outStrides, out.width, out.height, out.format, 1);
if (ret < 0) {
char errbuf[1024];
av_strerror(ret, errbuf, sizeof (errbuf));
qDebug() << "av_image_alloc outData error:" << errbuf;
goto end;
} // 创建转换上下文
// 方式一:
ctx = sws_getContext(in.width, in.height, in.format,
out.width, out.height, out.format,
SWS_BILINEAR, nullptr, nullptr, nullptr);
if (!ctx) {
qDebug() << "sws_getContext error";
goto end;
} // 计算一帧图像大小
inFrameSize = av_image_get_buffer_size(in.format, in.width, in.height, 1);
outFrameSize = av_image_get_buffer_size(out.format, out.width, out.height, 1); // 输入
// 拷贝输入像素数据到 inData[0]
memcpy(inData[0], in.pixels, inFrameSize); // 每一帧的转换
sws_scale(ctx, inData, inStrides, 0, in.height, outData, outStrides); // 拷贝像素数据到 outData[0]
out.pixels = (char *)malloc(outFrameSize);
memcpy(out.pixels, outData[0], outFrameSize);
end:
av_freep(&inData[0]);
av_freep(&outData[0]);
sws_freeContext(ctx);
}

代码链接

23_FFmpeg像素格式转换的更多相关文章

  1. FFmpeg(10)-利用FFmpeg进行视频像素格式和尺寸的转换(sws_getCachedContext(), sws_scale())

    一.包含头文件和库文件 像素格式的相关函数包含在 libswscale.so 库中,CMakeLists需要做下列改动: # swscale add_library(swscale SHARED IM ...

  2. Unity 利用FFmpeg实现录屏、直播推流、音频视频格式转换、剪裁等功能

    目录 一.FFmpeg简介. 二.FFmpeg常用参数及命令. 三.FFmpeg在Unity 3D中的使用. 1.FFmpeg 录屏. 2.FFmpeg 推流. 3.FFmpeg 其他功能简述. 一. ...

  3. 简述WPF中的图像像素格式(PixelFormats)

    原文:简述WPF中的图像像素格式(PixelFormats) --------------------------------------------------------------------- ...

  4. FFmpeg学习4:音频格式转换

    前段时间,在学习试用FFmpeg播放音频的时候总是有杂音,网上的很多教程是基于之前版本的FFmpeg的,而新的FFmepg3中audio增加了平面(planar)格式,而SDL播放音频是不支持平面格式 ...

  5. 【视频处理】YUV与RGB格式转换

    YUV格式具有亮度信息和色彩信息分离的特点,但大多数图像处理操作都是基于RGB格式. 因此当要对图像进行后期处理显示时,需要把YUV格式转换成RGB格式. RGB与YUV的变换公式如下: YUV(25 ...

  6. 自己积累的一些Emgu CV代码(主要有图片格式转换,图片裁剪,图片翻转,图片旋转和图片平移等功能)

    using System; using System.Drawing; using Emgu.CV; using Emgu.CV.CvEnum; using Emgu.CV.Structure; na ...

  7. 对索引像素格式的图片进行Setpixel(具有索引像素格式的图像不支持SetPixel)解决方案

    最近编写了一个验证码识别软件.其中对png.jpg图片进行二值化处理时,出现了错误:具有索引像素格式的图像不支持SetPixel解决方案.从字面上来看,这说明我对一个具有索引色的图片进行了直接RGB颜 ...

  8. 无法从带有索引像素格式的图像创建graphics对象(转)

    大家在用 .NET 做图片水印功能的时候, 很可能会遇到 “无法从带有索引像素格式的图像创建graphics对象”这个错误,对应的英文错误提示是“A Graphics object cannot be ...

  9. python 将png图片格式转换生成gif动画

    先看知乎上面的一个连接 用Python写过哪些[脑洞大开]的小工具? https://www.zhihu.com/question/33646570/answer/157806339 这个哥们通过爬气 ...

  10. 无法从带有索引像素格式的图像创建graphics对象

    大家在用 .NET 做图片水印功能的时候, 很可能会遇到 “无法从带有索引像素格式的图像创建graphics对象”这个错误,对应的英文错误提示是“A Graphics object cannot be ...

随机推荐

  1. Golang中make和new的区别

    1. 相同点 都是内建函数,都是在堆上分配内存,都需要传递类型参数 2. 不同点 传递的参数不一样,new函数只接收一个参数,make函数可以接收一个以上的参数 package main import ...

  2. delphi 异常测试(我自己捕捉)

    由于最近的短信模块老是报SocketErorr错误,有的时候也不确定是哪里有问题,影响短信的销售,所以这里这样写,把出现的异常捕捉到显示出来.然后跳过这个不发送 ------------------- ...

  3. Python-目录下相同格式的Excel文件合并

    最近在客户现场接到一个任务,需要将全国所有省份的数据进行合并.目录是分层级的,首先是省份目录.然后地级市目录.最里面是区县目录.需要将每个目录中的数据进行合并,然后添加4列数据,并将某一个列的数据进行 ...

  4. CF1829H Don't Blame Me

    题目链接 题解 知识点:线性dp,位运算. 考虑设 \(f_{i,j}\) 表示考虑了前 \(i\) 个数字,与和为 \(j\) 的方案数.转移方程显然. 注意初值为 \(f_{0,63} = 1\) ...

  5. NC15128 老子的全排列呢

    题目链接 题目 题目描述 老李见和尚赢了自己的酒,但是自己还舍不得,所以就耍起了赖皮,对和尚说,光武不行,再来点文的,你给我说出来1-8的全排序,我就让你喝,这次绝不耍你,你能帮帮和尚么? 输入描述 ...

  6. 【framework】WindowContainer简介

    1 前言 ​ WindowContainer 继承自 ConfigurationContainer,是 WMS 家族的重要基类.ConfigurationContainer简介 中,已介绍 Confi ...

  7. Springboot thymeleaf实战总结

    介绍 以下总结了使用Thymeleaf做项目过程中碰到的有价值的知识点.拿出来分享! 1.配置context-path 在公共模板中添加: <script type="text/jav ...

  8. 我的小程序之旅九:微信开放平台unionId机制介绍

    一.机制说明 参考文档:https://developers.weixin.qq.com/minigame/dev/guide/open-ability/union-id.html 如果开发者拥有多个 ...

  9. virtualapp安装应用流程源码分析

    1. HomeActivity 为处理的入口 @Override protected void onActivityResult(int requestCode, int resultCode, In ...

  10. 【Android 逆向】【攻防世界】easy-apk

    apk 安装到手机,随便输入点内容,提示错误 2. apk 拖入到jadx中看看 public class MainActivity extends AppCompatActivity { /* JA ...