23_FFmpeg像素格式转换
简介
前面使用 SDL 显示了一张YUV图片以及YUV视频。接下来使用Qt中的
QImage来实现一个简单的 YUV 播放器,查看QImage支持的像素格式,你会发现QImage仅支持显示RGB像素格式数据,并不支持直接显示YUV像素格式数据,但是YUV和RGB之间是可以相互转换的,我们将YUV像素格式数据转换成RGB像素格式数据就可以使用QImage显示了。
YUV转RGB常见有三种方式:
- 使用 FFmpeg 提供的库 libswscale :
优点:同一个函数实现了像素格式转换和分辨率缩放以及前后图像滤波处理;
缺点:速度慢。 - 使用 Google 提供的 libyuv:
优点:兼容性好功能全面;速度快,仅次于 OpenGL shader;
缺点:暂无。 - 使用 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传给了参数linesizes,linesizes就很好理解了是每一帧平面一行的大小。
// 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);
outData和outStrides是同样的道理。输出像素格式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像素格式转换的更多相关文章
- FFmpeg(10)-利用FFmpeg进行视频像素格式和尺寸的转换(sws_getCachedContext(), sws_scale())
一.包含头文件和库文件 像素格式的相关函数包含在 libswscale.so 库中,CMakeLists需要做下列改动: # swscale add_library(swscale SHARED IM ...
- Unity 利用FFmpeg实现录屏、直播推流、音频视频格式转换、剪裁等功能
目录 一.FFmpeg简介. 二.FFmpeg常用参数及命令. 三.FFmpeg在Unity 3D中的使用. 1.FFmpeg 录屏. 2.FFmpeg 推流. 3.FFmpeg 其他功能简述. 一. ...
- 简述WPF中的图像像素格式(PixelFormats)
原文:简述WPF中的图像像素格式(PixelFormats) --------------------------------------------------------------------- ...
- FFmpeg学习4:音频格式转换
前段时间,在学习试用FFmpeg播放音频的时候总是有杂音,网上的很多教程是基于之前版本的FFmpeg的,而新的FFmepg3中audio增加了平面(planar)格式,而SDL播放音频是不支持平面格式 ...
- 【视频处理】YUV与RGB格式转换
YUV格式具有亮度信息和色彩信息分离的特点,但大多数图像处理操作都是基于RGB格式. 因此当要对图像进行后期处理显示时,需要把YUV格式转换成RGB格式. RGB与YUV的变换公式如下: YUV(25 ...
- 自己积累的一些Emgu CV代码(主要有图片格式转换,图片裁剪,图片翻转,图片旋转和图片平移等功能)
using System; using System.Drawing; using Emgu.CV; using Emgu.CV.CvEnum; using Emgu.CV.Structure; na ...
- 对索引像素格式的图片进行Setpixel(具有索引像素格式的图像不支持SetPixel)解决方案
最近编写了一个验证码识别软件.其中对png.jpg图片进行二值化处理时,出现了错误:具有索引像素格式的图像不支持SetPixel解决方案.从字面上来看,这说明我对一个具有索引色的图片进行了直接RGB颜 ...
- 无法从带有索引像素格式的图像创建graphics对象(转)
大家在用 .NET 做图片水印功能的时候, 很可能会遇到 “无法从带有索引像素格式的图像创建graphics对象”这个错误,对应的英文错误提示是“A Graphics object cannot be ...
- python 将png图片格式转换生成gif动画
先看知乎上面的一个连接 用Python写过哪些[脑洞大开]的小工具? https://www.zhihu.com/question/33646570/answer/157806339 这个哥们通过爬气 ...
- 无法从带有索引像素格式的图像创建graphics对象
大家在用 .NET 做图片水印功能的时候, 很可能会遇到 “无法从带有索引像素格式的图像创建graphics对象”这个错误,对应的英文错误提示是“A Graphics object cannot be ...
随机推荐
- api接口调用
api接口调用 CURL 是一个利用URL语法规定来传输文件和数据的工具,支持很多协议,如HTTP.FTP.TELNET等.最爽的是,PHP也支持 CURL 库.使用PHP的CURL 库可以简单和有效 ...
- 在K8s中,提供的DNS组件是什么?有什么特性?
在Kubernetes (K8s)集群中,用于内部DNS服务的组件已经从早期的kube-dns过渡到了coredns. kube-dns(已弃用): 在Kubernetes 1.10版本之前,kube ...
- Hadoop相关面试题
1.简答说一下hadoop的map-reduce编程模型 首先map task会从本地文件系统读取数据,转换成key-value形式的键值对集合 使用的是hadoop内置的数据类型,比如longwri ...
- 树莓派安装freeswitch
树莓派版本: Raspberry Pi 4B 操作系统 : Ubuntu Server 20.04_x64 freeswitch版本 : 1.10.3 1.下载freeswitch源代码 wget h ...
- C++ 使用 UTF8 BOM 替换 UTF8 文件
void Utf8ToUtf8Bom(const wchar_t* filename) { std::ifstream infile; std::string strline; std::string ...
- QT - Day 5
1 event事件 用途:用于事件的分发 也可以做拦截操作,不建议 bool event( QEvent * e); 返回值 如果是true 代表用户处理这个事件,不向下分发了 e->ty ...
- win32 - ListView_GetItemPosition的使用
ListView_GetItemPosition : Gets the position of a list-view item 理论上获得桌面图标的正确方法是使用shell项,=> IFold ...
- centos7.5 hadoop NAT 静态IP网络环境搭建
1 设置 VMware 网络环境 1. 选择VMNet8 并将子网IP 修改为 192.168.10.0,保证集群ip都在这个网段下 2. 选择NAT 设置,配置NAT的网关为 192.168.10. ...
- sql题目---day39
# 1.查询所有的课程的名称以及对应的任课老师姓名 #where select teacher.tname,course.cname from teacher,course where course. ...
- 推荐10款C#开源好用的Windows软件
DevToys 项目简介:DevToys是一个专门为开发者设计的Windows工具箱,完全支持离线运行,无需使用许多不真实的网站来处理你的数据,常用功能有:格式化(支持 JSON.SQL.XML).J ...


