简介

前面使用 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. ListView 判断有没有选中的行方法

    ListView1.SelCount 返回选中行的行数 应该是 没有测试 但是测试了 如果没有选中行 返回0 如果选中一行了 返回1

  2. .NET Core开发实战(第25课:路由与终结点:如何规划好你的Web API)--学习笔记(下)

    25 | 路由与终结点:如何规划好你的Web API 自定义约束实现了路由约束接口,它只有一个 Match 方法,这个方法传入了 Http 当前的 httpContext,route,routeKey ...

  3. .NET Core开发实战(第5课:依赖注入:良好架构的起点)--学习笔记(下)

    05 | 依赖注入:良好架构的起点 注册完毕之后,想替换某些组件的某些部分时,可以使用 Replace 和 RemoveAll services.AddSingleton<IOrderServi ...

  4. MYSQL 1 DAY

    目录 MySQL 1.sql.DB.DBMS分别是什么,他们之间的关系? 2.什么是表? 3.学习MySQL主要还是学习通用的SQL语句,那么SQL语句包括增删改查,SQL语句怎么分类呢? 4.导入数 ...

  5. CF1010C Border 题解

    题目传送门 前置知识 最大公约数 | 裴蜀定理 简化题意 给定一个长度为 \(n\) 的序列 \(a\),求 \((\sum\limits_{i=1}^{n}d_ia_i) \bmod k\) 一共会 ...

  6. C语言,函数形参与实参个数不一致问题

    ​ 最近阅读工程代码的时候,同一个函数,不同场景调用时,输入的实参个数不一样,但是编译却没有问题.查看函数的定义,相关的C文件里并没有给形参指定默认值,这就很奇怪了. ​ 最终,发现在函数相关的头文件 ...

  7. 【Unity3D】UI Toolkit自定义元素

    1 前言 ​ UI Toolkit 支持通过继承 VisualElement 实现自定义元素,便于通过脚本控制元素.另外,UI Toolkit 也支持将一个容器及其所有子元素作为一个模板,便于通过脚本 ...

  8. aspell命令

    aspell命令 aspell命令是一个交互式拼写检查器,其会扫描指定的文件或任何标准输入的文件,检查拼写错误,并允许交互式地纠正单词. 语法 aspell [options] command 参数 ...

  9. 使用RegSetValueEx创建键值

    #include <iostream> #include <string> #include <sstream> #include <fstream> ...

  10. timeit测试函数执行时间

    def list_append(): l = [] for i in range(5000): l.append(i) def list_insert(): l = [] for i in range ...