YUV数据采集存储及部分转换原理
最近利用虹软的算法做了人脸识别项目自然就接触到比较多的关于摄像头数据的问题。
今天主要理解一下YUV数据的原理以及nv21数据和yv12的数据互转的原理。
首先感谢以下作者
https://www.cnblogs.com/cumtchw/p/10224329.html
https://blog.csdn.net/u010842019/article/details/52086103
YUV简介
YUV是一种普遍的编码方式,Y表示亮度(Luminance、Luma),U代表色度(Chrominance)、V代表饱和度(Chroma);YUV格式的编码的诞生有效地兼容了黑白电视和彩色电视。相对于较为平常的RGB三通道图像,YUV格式编码的图像视频文件在传输中占据较小的频宽
YUV的采样方式
YUV采样方式
主要描述像素Y、U、V分量采样比例,即表达每个像素时,Y、U、V分量的数目,通常有三种方式:YUV4:4:4,YUV4:2:2,YUV4:2:0。
YUV4:4:4采样,每一个Y对应一组UV分量。
YUV4:2:2采样,每两个Y共用一组UV分量。
YUV4:2:0采样,每四个Y共用一组UV分量。
用图直观地表示采集的方式,以黑点表示采样该像点的Y分量,以空心圆圈表示采用该像素点的UV分量。
image.png
这里重点讲一下yuv420的格式的码流存储方式
首先举一个例子:将摄像头传过来的原始数据 yv12转nv21数据。
先看下网上的例子
class CameraUtil {
companion object {
val intance by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
CameraUtil()
}
}
// YV12 To NV21
fun YV12toNV21(yv12: ByteArray, nv21: ByteArray, width: Int, height: Int) {
val frameSize = width * height
val qFrameSize = frameSize / 4
val tempFrameSize = frameSize * 5 / 4
System.arraycopy(yv12, 0, nv21, 0, frameSize) // Y
for (i in 0 until qFrameSize) {
nv21[frameSize + i * 2] = yv12[frameSize + i] // Cb (U)
nv21[frameSize + i * 2 + 1] = yv12[tempFrameSize + i] // Cr (V)
}
}
}
看完一脸懵逼,这什么 /4,什么又*5/4。底下这公式又是怎么来的。
好吧不管三七二十一能用就好,于是发挥了你强大的CV大法完成功能需求。
然而作为程序员的我们千万不能对算法知其然而不知其所以然
我们好好总结下原理,首先通过yuv420的采集方式我们知道一个uv数据对应了四个y
然后我们看一下存储的几个概念
打包格式(packed)和平面格式(planar)
打包格式是指将YUV保存在一个数组里面,然后YUV交叉存放.
平面格式是指将YUV分量分别保存在三个不同的数组中.
对于yuv420 它是平面模式和半平面模式(可以归为平面模式)所以我们可以看到2个名词
yuv420p和yuv420sp就是分别对应了2种不同的存储方式
其中半平面模式是先保存Y分量,然后UV交叉保存。
yuv420p
yuv420sp
图片转至:https://www.cnblogs.com/cumtchw/p/10224329.html
yv12属于yuv420p,nv21属于yuv420sp
有存储规则可以得出 y分量就是width*height,重点是u和v的转换,其实本质就是将原来的数组某些位置转换成其他值而已
// YV12 To NV21
fun YV12toNV21(yv12: ByteArray, nv21: ByteArray, width: Int, height: Int) {
val frameSize = width * height
val qFrameSize = frameSize / 4
val tempFrameSize = frameSize * 5 / 4
System.arraycopy(yv12, 0, nv21, 0, frameSize) // Y
for (i in 0 until qFrameSize) {
nv21[frameSize + i * 2] = yv12[frameSize + i] // Cb (U)
nv21[frameSize + i * 2 + 1] = yv12[tempFrameSize + i] // Cr (V)
}
}
u分量
首先y保存完后指针的位置已经是指向了width * height 即 frameSize的位置,而U和V的分量的总量是y的四分之一(YUV420采样方式是四个Y对应一个UV)即qFrameSize = frameSize / 4 ,其次要从420p中提取u分量存储到420sp中的相应位置。由于420p的u分量是连续存储的 所以就是[frameSize+i],而420sp是UV交叉存储的,u的位置是0,2,4.....所以就是[frameSize+2*i]
即 nv21[frameSize + i * 2] = yv12[frameSize + i]
v分量
理解了u分量的存储,v分量其实也是差不多的道理。由于存储好了u分量,指针的位置起始位置就是width * height + width * height/4即tempFrameSize=frameSize * 5 / 4。所以420p提取的位置是[tempFrameSize + i],420sp的v分量是1,3,5,7....即[frameSize + i * 2 + 1]
即 nv21[frameSize + i * 2 + 1] = yv12[tempFrameSize + i]
弄懂了概念,我们也就很容易的将nv21数据转换成yv12了
fun NV21toYV12(input: ByteArray, output: ByteArray, width: Int, height: Int) {
val frameSize = width * height
val qFrameSize = frameSize / 4
val tempFrameSize = frameSize * 5 / 4
System.arraycopy(input, 0, output, 0, frameSize) // Y
for (i in 0 until qFrameSize) {
output[frameSize + i] = input[frameSize + i * 2]// Cb (U)
output[tempFrameSize + i] = input[frameSize + i * 2 + 1]// Cr (V)
}
}
验证一下
创建一个页面左半边是正常的surfaceview,右半边是nv21数据转成yv12然后通过纹理渲染(OpenGL)的方式将yv12数据渲染到jfGLSurfaceView上的view.
override fun onPreview(data: ByteArray?, cameraSize: Point?) {
val outData = ByteArray(data!!.size)
CameraUtil.intance.NV21toYV12(data, outData, previewWidth, previewHeight)
val frameSize = previewWidth * previewHeight
val qFrameSize = frameSize / 4
val tempFrameSize = frameSize * 5 / 4
val y = ByteArray(frameSize)
val u = ByteArray(qFrameSize)
val v = ByteArray(qFrameSize)
System.arraycopy(outData, 0, y, 0, frameSize) // Y
System.arraycopy(outData, frameSize, u, 0, qFrameSize) // u
System.arraycopy(outData, frameSize+qFrameSize, v, 0, qFrameSize) // v
jfGLSurfaceView.feedYuvData(previewWidth,previewHeight,y,v,u)
}
device-2020-05-14-182403.png
可以看到两个view上都有相同的预览界面(因为同一个通道无法同时打开两个摄像头),说明转换成功。
如果你修改了u、v分量的数据可能还有意想不到的效果
device-2020-05-14-184753.png
不仅如此,通过弄懂原理我们可以弄懂很多问题,比如 一帧分辨率是1280 * 720的nv21数据他的存储大小是多少呢?
大小 = 三个分量的大小,及y = width * height, u = v =y/4 。y+u+v = width*height *1.5 = 1382400.