基于 FFMPEG 的像素格式变换(swscale,致敬雷霄骅)
Posted liyuanbhu
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于 FFMPEG 的像素格式变换(swscale,致敬雷霄骅)相关的知识,希望对你有一定的参考价值。
基于 FFMPEG 的像素格式变换(swscale,致敬雷霄骅)
前几天写了几篇关于ffmpeg 编程转封装的入门文章,下一步本来是要写转码或者编码的。但是发现无论是转码还是编码,都会遇到图像像素格式的变换。我们通常能在软件界面上显示的图像,都是 RGB 格式的(RGB24 或者 RGB32)。但是视频文件中的图像基本都是 YUV格式的(YUV420p 或者 YUV422p)。为了能继续我后面的软件开发,就需要先补充些 YUV 格式的知识。还有 YUV和 RGB 直接相互转换的方法。
关于 YUV 和 RGB 的转换我一起写过一篇博客:
https://blog.csdn.net/liyuanbhu/article/details/68951683
读完这篇博客应该就能转换了。不过 ffmpeg 里面既然有相应的功能,我们还是尽量用 ffmpeg 提供的功能,毕竟 ffmpeg 里面的代码应该比我自己写的要更优化。
这篇博客参考了雷博士的博客:
https://blog.csdn.net/leixiaohua1020/article/details/14215391
雷博士这篇博客写于好几年之前了,现在里面用到的有些函数已经不适用了。我的博客里会给出替代方案。另外,我这篇博客的体系也与雷博的有些差别。我尽量把代码做到了最精简。
ffmpeg 里面 libswscale 是用来做像素格式转换的,同时也能做图像的大小的放缩。为了方便,我的代码中还用到了 Qt。因为 Qt 中的 QImage 非常的方便。但是 QImage 也有不足的地方,就是不支持 YUV 格式。
图像放缩
libswscale 使用比较简单,基本流程就是三个函数。
sws_getContext()
sws_scale()
sws_freeContext()
sws_getContext() 函数和 sws_freeContext() 很简单。其中 sws_getContext 最后三个参数一般用不到,都可以输入 nullptr。flags 是插值类型,通常用 SWS_BICUBIC 会有比较好的效果。如果对速度要求比较高,可以用 SWS_FAST_BILINEAR。这两个函数的声明如下:
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);
void sws_freeContext(struct SwsContext *swsContext);
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[]);
其中 srcSlice 是输入图像的数据指针,dst 是输出图像的数据指针。srcStride 和 dstStride 是每行有多少个字节。相当于QImage 的 bytesPerLine()。srcSliceY 可以直接填 0。srcSliceH 是输入图像的行数。
我们仔细看看两个图像数据指针:
const uint8_t *const srcSlice[];
uint8_t *const dst[];
和我们想的有些不同,通常大家可能觉得这两个指针会是这样的:
const uint8_t * srcSlice;
uint8_t dst;
结果这两个指针都是指针数组。为什么会这样呢?因为我们的图像数据有可能是多个内存块。或者说图像数据存在多个数组中。这时候用一个指针就不够用了。我们知道,图像中的数据的排布方式有两大类:planar 和 packed。
packed 比较常见,比如 RGB24 的图像,像素数据的排布就是 R、G、B、R、G、B、R、G、B… 。
planar 通常用在视频数据中,比如 YUV422p。像素数据的排布是 Y Y Y Y Y Y… U U U… V V V…
一幅图像中的 Y U V 数据分别在三个数组中。
先写一个QImage 图像缩放的代码,QImage 的数据都是 packed。不用考虑图像分块的问题。下面是代码:
bool scale(const QImage &inImage, QImage &outImage, double scaleX, double scaleY)
int srcW = inImage.width();
int srcH = inImage.height();
int desW = srcW * scaleX;
int desH = srcH * scaleY;
if(outImage.size() != QSize(desW, desH) || outImage.format() != inImage.format())
outImage = QImage(QSize(desW, desH), inImage.format());
AVPixelFormat srcFormat = toAVPixelFormat(inImage.format());
uint8_t *in_data[1];
int in_linesize[1];
in_data[0] = (uint8_t *) inImage.bits();
in_linesize[0] = inImage.bytesPerLine();
//av_image_fill_arrays(in_data, in_linesize, inImage.bits(), AV_PIX_FMT_YUYV422, srcW, srcH, 1);
uint8_t *out_data[1];
int out_linesize[1];
out_data[0] = outImage.bits();
out_linesize[0] = outImage.bytesPerLine();
SwsContext * pContext = sws_getContext(srcW, srcH, srcFormat,
desW, desH, srcFormat, SWS_FAST_BILINEAR, nullptr, nullptr, nullptr);
if(!pContext) return false;
sws_scale(pContext, in_data, in_linesize, 0, srcH,
out_data, out_linesize);
sws_freeContext(pContext);
return true;
分几部分讲讲:
uint8_t *in_data[1];
int in_linesize[1];
in_data[0] = (uint8_t *) inImage.bits();
in_linesize[0] = inImage.bytesPerLine();
由于 QImage 数据只有 1块,用一个指针就够了。所以我们的指针数组长度是 1。in_linesize[] 数组的长度也是 1 就够了。代码里还涉及到 QImage::Format 到 AVPixelFormat 格式的映射。我写了如下这样的一个函数。
enum AVPixelFormat toAVPixelFormat(QImage::Format format)
switch (format)
case QImage::Format_Invalid:
case QImage::Format_MonoLSB:
return AV_PIX_FMT_NONE;
case QImage::Format_Mono:
return AV_PIX_FMT_MONOBLACK;
case QImage::Format_Indexed8:
return AV_PIX_FMT_PAL8;
case QImage::Format_Alpha8:
case QImage::Format_Grayscale8:
return AV_PIX_FMT_GRAY8;
case QImage::Format_Grayscale16:
return AV_PIX_FMT_GRAY16LE;
case QImage::Format_RGB32:
case QImage::Format_ARGB32:
case QImage::Format_ARGB32_Premultiplied:
return AV_PIX_FMT_BGRA;
case QImage::Format_RGB16:
case QImage::Format_ARGB8565_Premultiplied:
return AV_PIX_FMT_RGB565LE;
case QImage::Format_RGB666:
case QImage::Format_ARGB6666_Premultiplied:
return AV_PIX_FMT_NONE;
case QImage::Format_RGB555:
case QImage::Format_ARGB8555_Premultiplied:
return AV_PIX_FMT_BGR555LE;
case QImage::Format_RGB888:
return AV_PIX_FMT_RGB24;
case QImage::Format_RGB444:
case QImage::Format_ARGB4444_Premultiplied:
return AV_PIX_FMT_RGB444LE;
case QImage::Format_RGBX8888:
case QImage::Format_RGBA8888:
case QImage::Format_RGBA8888_Premultiplied:
return AV_PIX_FMT_RGBA;
case QImage::Format_BGR30:
case QImage::Format_A2BGR30_Premultiplied:
case QImage::Format_RGB30:
case QImage::Format_A2RGB30_Premultiplied:
return AV_PIX_FMT_NONE;
case QImage::Format_RGBX64:
case QImage::Format_RGBA64:
case QImage::Format_RGBA64_Premultiplied:
return AV_PIX_FMT_RGBA64LE;
case QImage::Format_BGR888:
return AV_PIX_FMT_BGR24;
default:
return AV_PIX_FMT_NONE;
return AV_PIX_FMT_NONE;
这个 toAVPixelFormat() 函数没有充分测试,有可能有些格式的映射是错的。不过常见的 QImage::Format_Grayscale8、QImage::Format_RGB32、QImage::Format_RGB888 应该是没什么问题的。
下面的例子是 YUV422p 到 RGB32 的转换代码。
QImage YUV422pToQImageRGB32(const uchar *y, const uchar *u, const uchar *v, int width, int height)
QImage image(QSize(width, height), QImage::Format_RGB32);
SwsContext * pContext = sws_getContext(width, height, AV_PIX_FMT_YUV422P,
width, height, AV_PIX_FMT_RGBA, SWS_FAST_BILINEAR, nullptr, nullptr, nullptr);
const uint8_t *in_data[4];
int in_linesize[4];
in_data[0] = y;
in_data[1] = u;
in_data[2] = v;
in_linesize[0] = width;
in_linesize[2] = width / 2;
in_linesize[3] = width / 2;
uint8_t *out_data[1];
int out_linesize[1];
out_data[0] = image.bits();
out_linesize[0] = image.bytesPerLine();
sws_scale(pContext, in_data, in_linesize, 0, height, out_data, out_linesize);
sws_freeContext(pContext);
return image;
有的时候YUV422P 的数据是连在一起的,只有一个数据的头指针。
QImage YUV422pToQImageRGB32(const uchar *yuv, int width, int height)
QImage image(QSize(width, height), QImage::Format_RGB32);
SwsContext * pContext = sws_getContext(width, height, AV_PIX_FMT_YUV422P,
width, height, AV_PIX_FMT_RGBA, SWS_FAST_BILINEAR, nullptr, nullptr, nullptr);
const uint8_t *in_data[4];
int in_linesize[4];
in_data[0] = yuv;
in_data[1] = in_data[0] + width * height;
in_data[2] = in_data[1] + width * height / 2;
in_linesize[0] = width;
in_linesize[2] = width / 2;
in_linesize[3] = width / 2;
uint8_t *out_data[1];
int out_linesize[1];
out_data[0] = image.bits();
out_linesize[0] = image.bytesPerLine();
sws_scale(pContext, in_data, in_linesize, 0, height, out_data, out_linesize);
sws_freeContext(pContext);
return image;
如果是 YUV420P 数据。那么代码是这样的:
QImage YUV420pToQImageRGB32(const uchar *yuv, int width, int height)
QImage image(QSize(width, height), QImage::Format_RGB32);
SwsContext * pContext = sws_getContext(width, height, AV_PIX_FMT_YUV420P,
width, height, AV_PIX_FMT_RGBA, SWS_FAST_BILINEAR, nullptr, nullptr, nullptr);
const uint8_t *in_data[4];
int in_linesize[4];
in_data[0] = yuv;
in_data[1] = in_data[0] + width * height;
in_data[2] = in_data[1] + width * height / 4;
in_linesize[0] = width;
in_linesize[2] = width / 2;
in_linesize[3] = width / 2;
uint8_t *out_data[1];
int out_linesize[1];
out_data[0] = image.bits();
out_linesize[0] = image.bytesPerLine();
sws_scale(pContext, in_data, in_linesize, 0, height, out_data, out_linesize);
sws_freeContext(pContext);
return image;
有些同学可能会问,你是如何确定这么写代码就是对的呢?其实我在写这些代码之前,写了另一个代码,自己生成了一个 YUV420P 数据。
int width = 1280;
int height = 960;
int yuvBufferSize = av_image_get_buffer_size(AV_PIX_FMT_YUV420P, width, height, 1);
uchar * yuvBuffer = (uint8_t*)av_malloc(yuvBufferSize);
qDebug() << "width = " << width << ", height = " << height;
qDebug() << "yuvBufferSize = " << yuvBufferSize;
uint8_t *out_data[4];
int out_linesize[4];
av_image_fill_arrays(out_data, out_linesize, yuvBuffer, AV_PIX_FMT_YUV420P, width, height, 1);
qDebug() << "out_data[0] = " << out_data[0] << ", size = " << out_data[1] - out_data[0] << ", out_linesize[0] = " << out_linesize[0];
qDebug() << "out_data[1] = " << out_data[1] << ", size = " << out_data[2] - out_data[1] << ", out_linesize[1] = " << out_linesize[1];
qDebug() << "out_data[2] = " << out_data[2] << ", out_linesize[2] = " << out_linesize[2];
av_free(yuvBuffer);
这个代码的输出结果如下:
width = 1280 , height = 960
yuvBufferSize = 1843200
out_data[0] = 0x1f7dda88080 , size = 1228800 , out_linesize[0] = 1280
out_data[1] = 0x1f7ddbb4080 , size = 307200 , out_linesize[1] = 640
out_data[2] = 0x1f7ddbff080 , out_linesize[2] = 640
我们知道 1280 * 960 = 1228800,1843200 = 1228800 * 1.5
所以 YUV420P 格式下, Y 通道占 1228800个字节, UV 加起来占 614400 字节,U、V 每个通道是 Y 通道的 1/4。U、V 每个通道每行的字节数是 Y 每行字节数的一半。
如果程序改成 YUV422P ,结果则是:
width = 1280 , height = 960
yuvBufferSize = 2457600
out_data[0] = 0x23c9bd5c080 , size = 1228800 , out_linesize[0] = 1280
out_data[1] = 0x23c9be88080 , size = 614400 , out_linesize[1] = 640
out_data[2] = 0x23c9bf1e080 , out_linesize[2] = 640
U、V 每个通道字节数是 Y 通道的 1/2。U、V 每个通道每行的字节数还是 Y 每行字节数的一半。
YUV422P 和 YUV420P 的区别在于 YUV420P 的 UV 通道的行数和列数都只有图片行数和列数的一边。YUV422P 则 UV 通道的行数和图片的行数是相同的。
从上面的代码中我们也能看到。当我们不确定如何填 srcStride 和 dstStride 时 ,可以用 av_image_fill_arrays() 来帮我们。
在我们处理视频图像时,通常不会像我这样直接定义 out_data 和out_linesize。我们通常是用 AVFrame。这时可以参考我下面的代码片段:
AVFrame *pFrameYUV420P = av_frame_alloc();
pFrameYUV420P->width = 1280;
pFrameYUV420P->height = 960;
pFrameYUV420P->format = AV_PIX_FMT_YUV422P;
int yuvBufferSize = av_image_get_buffer_size((AVPixelFormat) pFrameYUV420P->format,
pFrameYUV420P->width,
pFrameYUV420P->height, 1);
uchar * yuvBuffer = (uint8_t*)av_malloc(yuvBufferSize);
av_image_fill_arrays(pFrameYUV420P->data,
pFrameYUV420P->linesize,
yuvBuffer,
(AVPixelFormat) pFrameYUV420P->format,
pFrameYUV420P->width,
pFrameYUV420P->height,
1);
// 这里来填充具体的数据
av_frame_free(&pFrameYUV420P);//这里会自动释放 yuvBuffer
这个代码还有另一种写法:
AVFrame *pFrameYUV420P = av_frame_alloc();
pFrameYUV420P->width = 1280;
pFrameYUV420P->height = 960;
pFrameYUV420P->format = AV_PIX_FMT_YUV422P;
av_image_alloc(pFrameYUV420P->data,
pFrameYUV420P->linesize,
pFrameYUV420P->width,
pFrameYUV420P->height,
(AVPixelFormat) pFrameYUV420P->format,
1);
// 这里来填充具体的数据
av_frame_free(&pFrameYUV420P);
下面给个 QImage 转 YUV420P 的例子:
void QImageToYUV420P(QImage &image, uchar * yuv)
int srcW = image.width();
int srcH = image.height();
int desW = srcW;
int desH = srcH;
AVPixelFormat srcFormat = toAVPixelFormat(image.format());
struct SwsContext *pContext = sws_getContext(srcW, srcH, srcFormat ,
desW, desH, AV_PIX_FMT_YUV420P,
SWS_BICUBIC, nullptr, nullptr, nullptr);
uint8_t *in_data[1];
int in_linesize[1];
av_image_fill_arrays(in_data, in_linesize, image.bits(), srcFormat, srcW, srcH, 1);
uint8_t *out_data[4];
int out_linesize[4];
av_image_fill_arrays(out_data, out_linesize, yuv, AV_PIX_FMT_YUV420P, srcW, srcH, 1);
sws_scale(pContext, in_data, in_linesize, 0, srcH,
out_data, out_linesize);
sws_freeContext(pContext);
void testYUV420p()
QImage image;
image.load("D:/test.jpg");
int width = image.width();
int height = image.height();
int yuvBufferSize = av_image_get_buffer_size(AV_PIX_FMT_YUV420P, width, height, 1);
uchar * yuvBuffer = (uint8_t*)av_malloc(yuvBufferSize);
QImageToYUV420P(image, yuvBuffer);
QFile file("D:/image.yuv");
file.open(QFile::WriteOnly);
file.write((const char *)yuvBuffer, yuvBufferSize);
file.close();
av_free(yuvBuffer);
原始图像如下:
转码后的图像:
这里用到了一个 YUV 播放器,下载地址如下:
https://sourceforge.net/projects/raw-yuvplayer/
好了,就写这么多吧。看过这篇博客的同学应该掌握了用sws_scale() 做像素格式转换的方法了。
以上是关于基于 FFMPEG 的像素格式变换(swscale,致敬雷霄骅)的主要内容,如果未能解决你的问题,请参考以下文章
FFmpeg(10)-基于FFmpeg进行像素格式转换(sws_getCachedContext(), sws_scale())
基于FFmpeg的视频播放器之五:使用SDL2渲染yuv420p
FFmpegffmpeg 命令查询二 ( 比特流过滤器 | 可用协议 | 过滤器 | 像素格式 | 标准声道布局 | 音频采样格式 | 颜色名称 )