最近利用虹軟的算法做了人臉識(shí)別項(xiàng)目自然就接觸到比較多的關(guān)于攝像頭數(shù)據(jù)的問題。
今天主要理解一下YUV數(shù)據(jù)的原理以及nv21數(shù)據(jù)和yv12的數(shù)據(jù)互轉(zhuǎn)的原理。
首先感謝以下作者
https://www.cnblogs.com/cumtchw/p/10224329.html
https://blog.csdn.net/u010842019/article/details/52086103
YUV簡(jiǎn)介
YUV是一種普遍的編碼方式,Y表示亮度(Luminance、Luma),U代表色度(Chrominance)、V代表飽和度(Chroma);YUV格式的編碼的誕生有效地兼容了黑白電視和彩色電視。相對(duì)于較為平常的RGB三通道圖像,YUV格式編碼的圖像視頻文件在傳輸中占據(jù)較小的頻寬
YUV的采樣方式
YUV采樣方式
主要描述像素Y、U、V分量采樣比例,即表達(dá)每個(gè)像素時(shí),Y、U、V分量的數(shù)目,通常有三種方式:YUV4:4:4,YUV4:2:2,YUV4:2:0。
YUV4:4:4采樣,每一個(gè)Y對(duì)應(yīng)一組UV分量。
YUV4:2:2采樣,每?jī)蓚€(gè)Y共用一組UV分量。
YUV4:2:0采樣,每四個(gè)Y共用一組UV分量。
用圖直觀地表示采集的方式,以黑點(diǎn)表示采樣該像點(diǎn)的Y分量,以空心圓圈表示采用該像素點(diǎn)的UV分量。

這里重點(diǎn)講一下yuv420的格式的碼流存儲(chǔ)方式
首先舉一個(gè)例子:將攝像頭傳過來的原始數(shù)據(jù) yv12轉(zhuǎn)nv21數(shù)據(jù)。
先看下網(wǎng)上的例子
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。底下這公式又是怎么來的。
好吧不管三七二十一能用就好,于是發(fā)揮了你強(qiáng)大的CV大法完成功能需求。
然而作為程序員的我們千萬不能對(duì)算法知其然而不知其所以然
我們好好總結(jié)下原理,首先通過yuv420的采集方式我們知道一個(gè)uv數(shù)據(jù)對(duì)應(yīng)了四個(gè)y
然后我們看一下存儲(chǔ)的幾個(gè)概念
打包格式(packed)和平面格式(planar)
打包格式是指將YUV保存在一個(gè)數(shù)組里面,然后YUV交叉存放.
平面格式是指將YUV分量分別保存在三個(gè)不同的數(shù)組中.
對(duì)于yuv420 它是平面模式和半平面模式(可以歸為平面模式)所以我們可以看到2個(gè)名詞
yuv420p和yuv420sp就是分別對(duì)應(yīng)了2種不同的存儲(chǔ)方式
其中半平面模式是先保存Y分量,然后UV交叉保存。
yuv420p

yuv420sp

圖片轉(zhuǎn)至:https://www.cnblogs.com/cumtchw/p/10224329.html
yv12屬于yuv420p,nv21屬于yuv420sp
有存儲(chǔ)規(guī)則可以得出 y分量就是width*height,重點(diǎn)是u和v的轉(zhuǎn)換,其實(shí)本質(zhì)就是將原來的數(shù)組某些位置轉(zhuǎn)換成其他值而已
// 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保存完后指針的位置已經(jīng)是指向了width * height 即 frameSize的位置,而U和V的分量的總量是y的四分之一(YUV420采樣方式是四個(gè)Y對(duì)應(yīng)一個(gè)UV)即qFrameSize = frameSize / 4 ,其次要從420p中提取u分量存儲(chǔ)到420sp中的相應(yīng)位置。由于420p的u分量是連續(xù)存儲(chǔ)的 所以就是[frameSize+i],而420sp是UV交叉存儲(chǔ)的,u的位置是0,2,4.....所以就是[frameSize+2*i]
即 nv21[frameSize + i * 2] = yv12[frameSize + i]
v分量
理解了u分量的存儲(chǔ),v分量其實(shí)也是差不多的道理。由于存儲(chǔ)好了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數(shù)據(jù)轉(zhuǎn)換成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)
}
}
驗(yàn)證一下
創(chuàng)建一個(gè)頁(yè)面左半邊是正常的surfaceview,右半邊是nv21數(shù)據(jù)轉(zhuǎn)成yv12然后通過紋理渲染(OpenGL)的方式將yv12數(shù)據(jù)渲染到j(luò)fGLSurfaceView上的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)
}

可以看到兩個(gè)view上都有相同的預(yù)覽界面(因?yàn)橥粋€(gè)通道無法同時(shí)打開兩個(gè)攝像頭),說明轉(zhuǎn)換成功。
如果你修改了u、v分量的數(shù)據(jù)可能還有意想不到的效果

不僅如此,通過弄懂原理我們可以弄懂很多問題,比如 一幀分辨率是1280 * 720的nv21數(shù)據(jù)他的存儲(chǔ)大小是多少呢?
大小 = 三個(gè)分量的大小,及y = width * height, u = v =y/4 。y+u+v = width*height *1.5 = 1382400.