理解I420格式的核心知识
YUV的概念
YUV 和我们熟知的 RGB 类似,是一种颜色编码格式。它主要用于电视系统和模拟视频邻域(如 Camera 系统)。YUV 包含三个分量,其中 Y 表示明亮度(Luminance 或 Luma),也就是灰度值。而 U 和 V 则表示色度(Chrominance 或 Chroma),作用是描述图像色彩及饱和度,用于指定像素的颜色。没有 UV 分量信息,一样可以显示完整的图像,只不过是黑白的灰度图像。YUV 格式的好处是很好地解决了彩色电视机与黑白电视机的兼容问题(当只要Y分量时就是黑白图像)。YCbCr,YPbPr等专有名词都可以称为YUV。
YUV的格式
YUV的格式取决采样方式
YUV的采样方式
- 4:4:4表示完全取样(每一个Y对应一组UV分量)
- 4:2:2表示2:1的水平取样,垂直完全采样(每两个Y共用一组UV分量)
- 4:2:0表示2:1的水平取样,垂直2:1采样(每四个Y共用一组UV分量)
- 4:1:1表示4:1的水平取样,垂直完全采样(每四个Y共用一组UV分量)
YUV的存储格式
我们主要关心的是如何处理YUV数据,所以最关心的是YUV数据的存储格式。内存中存储的方式包括两种:
-
planar 格式:先连续存储所有像素点的 Y 分量,紧接着存储所有像素点的 U 分量,再是V分量(当然不同存储格式的UV的先后顺序是不一样的,如I420的V在U后,YV12则是U在V后)。
-
packed 格式:每个像素的Y,U,V分量交替存储。
Y,U,V分量存储时的先后顺序的不同,代表了不同的格式,以下列出了几种常见的YUV格式:
-
YUYV(属于YUV422)
两个Y共用一组UV分量。Y,U,V分量交替存储。 -
UYVY(属于YUV422)
可以看到与YUYV的分量的顺序不一样。
- YUV422P(属于YUV422)
是Planar存储模式,依次是Y,U,V。U分量在V分量之前
YUV420
根据U分量和Y分量的位置不同,也分为几种类型,常见的如下:
- YV12(属于YUV420)
是Planar存储模式,依次是Y,V,U。V分量在U分量之前。
- NV12(属于YUV420)
先是Y,在是U,V分量交替出现
一个简易存储示意图
I420: YYYYYYYY UU VV =>YUV420P
YV12: YYYYYYYY VV UU =>YUV420P
NV12: YYYYYYYY UVUV =>YUV420SP
NV21: YYYYYYYY VUVU =>YUV420SP
YUV420sp与YUV420p的不同之处在,存储UV分量时,YUV420sp中的UV分量是交替存储。
处理I420
I420属于YUV420P(存储格式为Planar),先是Y分量,再依次是U分量,V分量
这种格式很常用,在x264/265的中要求传入的源数据就是这种格式。在libyuv中,进行YUV图像处理(缩放,剪切,旋转)也是要求以这种格式传入。ffmpeg解码h264/265后数据也是这种格式。这种格式也可以直接通过D3D,OpenGL进行渲染。
计算占用的字节大小
一个分量占用一个字节,每一个象素点对应一个Y分量,四分之一个U分量,四分之一个V分量。所以分辨率为w*h的I420格式,Y的字节数为 w*h
, U的字节数为w*h/4
,V的字节数大小为w*h/4
,总字节数即为w*h*3/2
。
定位像素数据
要处理I420的数据,首先要能定位到像素分量数据。一个典型的场景是,通过freetype在YUV图像上加字幕时,这是需要将指定坐标点的像素替换为freetype返回的字体图数据。
那么下面是取像素点YUV(I420格式)分量数据的公式(Planar存储方式):
size.total = size.width * size.height;
y = yuv[position.y * size.width + position.x];
u = yuv[(position.y / 2) * (size.width / 2) + (position.x / 2) + size.total];
v = yuv[(position.y / 2) * (size.width / 2) + (position.x / 2) + size.total + (size.total / 4)];
size.width/2为U分量和Y分量的步长
转一张很直观的I420存储的示意图。Y,U,V相同颜色的表述是同一像素的分量。可以结合这个图套用上面的公式推算一下:
一种常用的YUV数据表示方法
如果直接分配一段内存用以存储图像数据(比如I420格式),是没法知道这段内存的YUV分量特点的(每个分量的数据起始及长度)。那么我们可以下面这种方式定义数据结构来解释存储结构:
enum enImageFmt
{
enImageFmt_YUV420P
...
};
struct ImageSize
{
int iWidth;
int iHeight;
};
struct VideoFrame
{
//Image的格式
enImageFmt fmt;
//图像的分辨率
ImageSize Size;
//分别指向Y,U,V分量的开头
unsigned char *data[3];
//分别指示Y,U,V分量一行(步长)数据大小
unsigned short linesize[3];
}
//如下的一个示例代码
//指针pImage指向一幅YUV420P格式的图像,通过该结构体指示它的内存结构
VideoFrame frame;
frame.fmt = enImageFmt_YUV420P;
frame.Size.w = w;
frame.Size.h = h;
//Y分量的步长
frame.linesize[0] = w;
//U分量的步长(其长度就是Y分量步长的一半,同理V分量)
frame.linesize[1] = w/2;
//V分量的步长
frame.linesize[2] = w/2;
//那么data[0]指向的就是Y数据的起始
frame.data[0] = (unsigned char*)pImage;
//U数据的起始
frame.data[1] = frame.data[0] + frame.linesize[0]*h;
//V数据的起始
frame.data[2] = frame.data[1] + frame.linesize[1]*h/2;
该结构体不止可以指示YUV420P的结构,也可以指示其它YUV格式的结构。在ffmpeg中也有类型的结构定义。
总结:
虽然主要是讲解的如果处理I420格式,但是前面介绍了几种不同YUV的存储格式。可以结合存储示意图,类推出如何处理其它YUV格式数据。
参考
https://zh.wikipedia.org/wiki/YUV
https://www.cnblogs.com/azraelly/archive/2013/01/01/2841269.html
推荐阅读
-
解密Android Bitmap转I420的难题,附图文详解YUV420数据格式
-
【Codecs】YUV主要格式YV12,I420,YUV420P的区别
-
音视频编解码-YUV采样格式中的YUV444,YUV422,YUV420理解(转)-YUV4:4:4
-
比较YU12、I420、YV12、NV12、NV21、YUV420P、YUV420SP、YUV422P和YUV444P:理解并解析YUV420P的不同之处
-
理解I420格式的核心知识
-
音视频编解码: YUV采样格式中的YUV444,YUV422,YUV420理解
-
不同YUV格式的解读:YU12、I420、YV12、NV12、NV21、YUV420P、YUV420SP、YUV422P和YUV444P的差异解析
-
将Android中的YUV_420_888编码Image转换为I420和NV21格式的byte数组
-
深入理解YUV的采样与格式-探索YUV采样格式的精髓
-
14-傅里叶变换的代码实现-一、numpy实现傅里叶变换和逆傅里叶变换 1.numpy实现傅里叶变换numpy.fft.fft2实现傅里叶变换,返回一个复数数组(complex ndarray),也就是频谱图像numpy.fft.fftshift将零频率分量移到频谱中心(将左上角的低频区域,移到中心位置) 20*np.log(np.abs(fshift))设置频谱的范围。可以理解为,之前通过傅里叶变换得到复数的数组,是不能通过图像的方法展示出来的,需要转换为灰度图像(映射到[0,255]区间)需要注意的是1> 傅里叶得到低频、高频信息,针对低频、高频处理能够实现不同的目的2> 傅里叶过程是可逆的,图像经过傅里叶变换、逆傅里叶变换后,能够恢复到原始图像3> 在频域对图像进行处理,在频域的处理会反映在逆变换图像上 # 将绘制的图显示在窗口 %matplotlib qt5 import cv2 import numpy as np import matplotlib.pyplot as plt img = cv2.imread(r"image\lena.bmp",cv2.IMREAD_GRAYSCALE) # 傅里叶变换 f = np.fft.fft2(img) # 移动中心位置 fshift = np.fft.fftshift(f) # 调整值范围 result = 20*np.log(np.abs(fshift)) plt.subplot(1,2,1) plt.imshow(img,cmap=plt.cm.gray) plt.title("original") plt.axis("off") plt.subplot(1,2,2) plt.imshow(result,cmap=plt.cm.gray) plt.title("result") plt.axis("off") plt.show 傅里叶变换的频谱图像: 2.numpy实现逆傅里叶变换numpy.fft.ifft2实现逆傅里叶变换,返回一个复数数组(complex ndarray)numpy.fft.ifftshiftfftshift函数的逆函数,将中心位置的低频,重新移到左上角iimg = np.abs(逆傅里叶变化结果)设置值的范围,映射到[0,255]区间 # 将绘制的图显示在窗口 %matplotlib qt5 import cv2 import numpy as np import matplotlib.pyplot as plt img = cv2.imread(r"image\boat.bmp",cv2.IMREAD_GRAYSCALE) # 傅里叶变换 f = np.fft.fft2(img) fshift = np.fft.fftshift(f) # 逆傅里叶变换 ishift = np.fft.ifftshift(fshift) iimg = np.fft.ifft2(ishift) iimg = np.abs(iimg) plt.subplot(1,2,1) plt.imshow(img,cmap=plt.cm.gray) plt.title("original") plt.axis("off") plt.subplot(1,2,2) plt.imshow(iimg,cmap=plt.cm.gray) plt.title("iimg") plt.axis("off") plt.show 将一副图像,进行傅里叶变换和逆傅里叶变换后,进行对比(一样的) 实例:通过numpy实现高通滤波,保留图像的边缘信息 获取图像的形状rows,cols = img.shape获取图像的中心点crow,ccol = int(rows/2),int(cols/2)将频谱图像的中心区域(低频区域)设置为0(黑色)fshift[crow-30:crow+30,ccol-30:ccol+30] = 0 # 将绘制的图显示在窗口 %matplotlib qt5 import cv2 import numpy as np import matplotlib.pyplot as plt img = cv2.imread(r"image\boat.bmp",cv2.IMREAD_GRAYSCALE) # 傅里叶变换 f = np.fft.fft2(img) fshift = np.fft.fftshift(f) # 高通滤波 rows,cols = img.shape crow,ccol = int(rows/2),int(cols/2) fshift[crow-30:crow+30,ccol-30:ccol+30] = 0 # 逆傅里叶变换 ishift = np.fft.ifftshift(fshift) iimg = np.fft.ifft2(ishift) iimg = np.abs(iimg) plt.subplot(1,2,1) plt.imshow(img,cmap=plt.cm.gray) plt.title("original") plt.axis("off") plt.subplot(1,2,2) plt.imshow(iimg,cmap=plt.cm.gray) plt.title("iimg") plt.axis("off") plt.show 使用numpy实现高通滤波的实验结果: 二、opencv实现傅里叶变换和逆傅里叶变换 1.opencv实现傅里叶变换 返回结果 = cv2.dft(原始图像,转换标识)1> 返回结果:是双通道的,第一个通道是结果的实数部分,第二个通道是结果的虚数部分2> 原始图像:输入图像要首先转换成np.float32(img)格式3> 转换标识:flags = cv2.DFT_COMPLEX_OUTPUT,输出一个复数阵列numpy.fft.fftshift将零频率分量移到频谱中心(将左上角的低频区域,移到中心位置)调整频谱的范围,将上面频谱图像的复数数组,转换为可以显示的灰度图像(映射到[0,255]区间)返回值 = 20*np.log(cv2.magnitude(参数1,参数2))1> 参数1:浮点型X坐标值,也就是实部2> 参数2:浮点型Y坐标值,也就是虚部 # 将绘制的图显示在窗口 %matplotlib qt5 import cv2 import numpy as np import matplotlib.pyplot as plt img = cv2.imread(r"image\lena.bmp",cv2.IMREAD_GRAYSCALE) # 傅里叶变换 dft = cv2.dft(np.float32(img),flags = cv2.DFT_COMPLEX_OUTPUT) # 移动中心位置 dftShift = np.fft.fftshift(dft) # 调整频谱的范围 result = 20*np.log(cv2.magnitude(dftShift[:,:,0],dftShift[:,:,1])) plt.subplot(1,2,1) plt.imshow(img,cmap=plt.cm.gray) plt.title("original") plt.axis("off") plt.subplot(1,2,2) plt.imshow(result,cmap=plt.cm.gray) plt.title("result") plt.axis("off") plt.show 傅里叶变换的频谱图像: 2.opencv实现逆傅里叶变换返回结果 = cv2.idft(原始数据)1> 返回结果:取决于原始数据的类型和大小2> 原始数据:实数或者复数均可numpy.fft.ifftshiftfftshift函数的逆函数,将中心位置的低频,重新移到左上角调整频谱的范围,映射到[0,255]区间返回值 = cv2.magnitude(参数1,参数2)1> 参数1:浮点型X坐标值,也就是实部2> 参数2:浮点型Y坐标值,也就是虚部 # 将绘制的图显示在窗口 %matplotlib qt5 import cv2 import numpy as np import matplotlib.pyplot as plt img = cv2.imread(r"image\lena.bmp",cv2.IMREAD_GRAYSCALE) # 傅里叶变换 dft = cv2.dft(np.float32(img),flags = cv2.DFT_COMPLEX_OUTPUT) dftShift = np.fft.fftshift(dft) # 逆傅里叶变换 ishift = np.fft.ifftshift(dftShift) iimg = cv2.idft(ishift) iimg = cv2.magnitude(iimg[:,:,0],iimg[:,:,1]) plt.subplot(1,2,1) plt.imshow(img,cmap=plt.cm.gray) plt.title("original") plt.axis("off") plt.subplot(1,2,2) plt.imshow(iimg,cmap=plt.cm.gray) plt.title("inverse") plt.axis("off") plt.show 将一副图像,进行傅里叶变换和逆傅里叶变换后,进行对比(一样的) 实例:通过opencv实现低通滤波,模糊一副图像