简介

前面使用 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. electron 开发 ,如何使用 第三方 库 进行typescript 开发,举例:jquery 其它的 应该也是一致。

    首先要弄明白一点,electron 开发 与 nodejs开发 基本一致. 要引入 jquery 实际上就是 nodejs 引入 jquery 第一步是 去 nmp中央仓库,查看,里面有详细的说明使用 ...

  2. Oracle-报错信息显示问号或者乱码(Oracle 19c)

    问题描述: 通过sqlplus登录Oracle19c时,执行SQL出现报错时,显示错误码出现问号: 解决方法: su   -  oracle vi .bash_profile 添加如下一行内容: ex ...

  3. Hadoop-Operation category READ is not supported in state standby 故障解决

    在查询hdfs时或者执行程序向hdfs写入数据时遇到报错:Operation category READ is not supported in state standby 意思是:该主机状态为待机, ...

  4. NC19885 [AHOI2009]CHESS 中国象棋

    题目链接 题目 题目描述 在N行M列的棋盘上,放若干个炮可以是0个,使得没有任何一个炮可以攻击另一个炮. 请问有多少种放置方法,中国像棋中炮的行走方式大家应该很清楚吧.一个炮要能攻击另一个炮他们必须要 ...

  5. 使用BP神经网络实现函数逼近

    1 一元函数逼近 1.1 待逼近函数 1.2 代码 clear,clc p=[-4:0.1:4]; %神经网络输入值 t=sin(0.5*pi*p)+sin(pi*p); %神经网络目标值 n=15; ...

  6. fold命令

    fold命令 fold命令用于限制文件列宽,其会从指定的文件里读取内容,将超过限定列宽的列加入增列字符后,输出到标准输出设备.若不指定任何文件名称,或是所给予的文件名为-,则fold命令会从标准输入设 ...

  7. Python之读取Excel

    介绍 现在交给你一份2010年美国各州县人口普查表:censuspopdata.xlsx.共72864条记录. 每一行代表一个县某统计区的人口数. 需要你统计出:各县统计区数量和人口数. 表格内容长这 ...

  8. c2工具sliver 执行木马命令后会弹窗一闪而过

    向木马发出指令后, 如果是cmd命令会有一个黑窗口一闪而灭, 不利于木马隐藏, 需要将executeHandler从handlers.go拷贝到handlers_windows.go里,核心代码为 c ...

  9. 糟糕,CPU100%了!!!

    前言 cpu使用率100%问题,是一个让人非常头疼的问题.因为出现这类问题的原因千奇百怪,最关键的是它不是必现的,有可能是系统运行了一段时间之后,在突然的某个时间点出现问题. 今天特地把我和同事,之前 ...

  10. 串口通信RXTXcomm使用

    一.串口通信原理 串口通信(Serial Communications)的概念非常简单,串口按位(bit)发送和接收字节. 尽管比按字节(byte)的并行通信慢,但是串口可以在使用一根线发送数据的同时 ...