前言
最近一段時間,接觸到移動端音視頻通話相關的內容,主要是結合OpenCV,TensorFlow等做一些視頻數據的分析,檢測工作。中間碰到大量的問題,入坑了算是,這裏總結一下!
攝像頭數據回調
關於移動端調用攝像頭的相關內容,這裏就不多說了,我們直接來看回調得到的數據!
iOS
我們設置了AVCaptureVideoDataOutputSampleBufferDelegate
代理後,就在下邊方法中拿到的攝像頭中的一幀數據CMSampleBufferRef
:
- (void)captureOutput:(AVCaptureOutput *)captureOutput
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
fromConnection:(AVCaptureConnection *)connection
那麼這一幀數據是什麼格式呢?取決於我們在採集時候的設置:
NSDictionary *rgbOutputSettings = @{
(__bridge NSString*)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA)
};
[self.videoDataOutput setVideoSettings:rgbOutputSettings];
這裏設置的kCVPixelFormatType_32BGRA
就是得到的數據的格式(32位BGRA)!
還有其他常用的格式,比如:kCVPixelFormatType_420YpCbCr8BiPlanarFullRange
、kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange
,這兩種都是NV12格式!
根據源碼中的註釋,可以知道kCVPixelFormatType_420YpCbCr8BiPlanarFullRange
是8位雙平面組件Y'CbCr
比例爲4:2:0,全範圍(亮度=[0,255] 色度=[1,255])。
kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange
是8位雙平面組件Y'CbCr
比例爲4:2:0,視頻範圍(亮度=[16,235] 色度=[16,240])。
Android
Camera
在5.0之前,使用Camera
API,需要實現Camera.PreviewCallback
接口,在回調方法onPreviewFrame
中拿到數據:
@Override
public void onPreviewFrame(final byte[] bytes, final Camera camera) {
....
}
這裏的bytes
數組就是得到yuv420sp
格式的數據,也稱爲NV21
格式。這是Camera
API的默認預覽格式。當然我們也可以指定預覽的輸出格式:
android.hardware.Camera.Parameters#setPreviewFormat(ImageFormat.NV21)}
具體格式都在ImageFormat
類裏邊!
我們也可以通過調用android.hardware.Camera.Parameters#getSupportedPreviewFormats()
來查看Camera
API支持的預覽格式!
Camera2
在5.0之後,Camera
被廢棄,使用Camera2
API,需要實現OnImageAvailableListener
接口,在回調方法onImageAvailable
中拿到數據
@Override
public void onImageAvailable(final ImageReader reader) {
final Image image = reader.acquireLatestImage();
....
}
調用reader.acquireLatestImage()
方法,就得到一個Image
對象。
這裏就要注意了,這個Image
中存放的數據格式默認是谷歌自家的YUV_420_888
,根據文檔描述這是一種新的YUV格式YUV420Flexible
。而且不再支持Camera
API中預覽回調默認的NV21
格式!
這塊也是折騰好久啊,我們這裏簡單來了解一下:
final Plane[] planes = image.getPlanes();
yRowStride = planes[0].getRowStride();
final int uvRowStride = planes[1].getRowStride();
final int uvPixelStride = planes[1].getPixelStride();
YUV_420_888
類型,其表示YUV420
格式的集合,888
表示Y、U、V分量中每個顏色佔8bit。Image
將三個分量存儲在三個Plane
類中,通過getPlanes()
方法得到一個Plane
數組,plane[0]
存放Y分量,plane[1]
存放U分量,plane[2]
存放V分量。還有那麼既然是YUV格式,Y分量就一定是連續存儲的,那麼重點就在U、V分量在數組中是如何的?!
下面瞭解一下Plane
類型的兩個屬性rowStride
和pixelStride
。
rowStride
:則是行跨度,就是每行存放的數據量。
pixelStride
:指像素跨度,即在一個平面中,U/V分量的取值間隔,這裏我們可以知道即使UV分量分別存儲在不同平面中,他們也不一定是連續存儲的(即PixelStride不總是爲1)
舉個例子,比如一個4x4的圖片:
我們知道一個NV21
格式的數據,如果在Camera的回調中,byte[]數組中分量以
YYYYYYYYYYYYYYYYVUVUVUVU
的形式存儲,而在Camera2得到的Image
中,由於YUV_420_888
格式強制分離了UV分量,這時Plane的屬性:
分量 | rowStride | pixelStride | data |
---|---|---|---|
Y分量 | 4 | 1 | YYYYYYYYYYYYYYYY |
U分量 | 4 | 2 | U_U_U_U_ |
V分量 | 4 | 2 | V_V_V_V_ |
關於Camera2得到的YUV_420_888
格式更多內容,可以參考這篇文章:
Android: Image類淺析(結合YUV_420_888)
轉換
在使用TensorFlow時,一般要求輸入的圖片格式爲RGB
,下邊說一下如何將上述獲得的數據轉換爲RGB
格式!
此轉換來自於TensorFlow Lite的object_detection例子:
首先對於YUV格式的數據,在得到每一個分量上的byte數組後,轉換有一個公式:
private static int YUV2RGB(int y, int u, int v) {
// This value is 2 ^ 18 - 1, and is used to clamp the RGB values before their ranges
// are normalized to eight bits.
static final int kMaxChannelValue = 262143;
// Adjust and check YUV values
y = (y - 16) < 0 ? 0 : (y - 16);
u -= 128;
v -= 128;
// This is the floating point equivalent. We do the conversion in integer
// because some Android devices do not have floating point in hardware.
// nR = (int)(1.164 * nY + 2.018 * nU);
// nG = (int)(1.164 * nY - 0.813 * nV - 0.391 * nU);
// nB = (int)(1.164 * nY + 1.596 * nV);
int y1192 = 1192 * y;
int r = (y1192 + 1634 * v);
int g = (y1192 - 833 * v - 400 * u);
int b = (y1192 + 2066 * u);
// Clipping RGB values to be inside boundaries [ 0 , kMaxChannelValue ]
r = r > kMaxChannelValue ? kMaxChannelValue : (r < 0 ? 0 : r);
g = g > kMaxChannelValue ? kMaxChannelValue : (g < 0 ? 0 : g);
b = b > kMaxChannelValue ? kMaxChannelValue : (b < 0 ? 0 : b);
return 0xff000000 | ((r << 6) & 0xff0000) | ((g >> 2) & 0xff00) | ((b >> 10) & 0xff);
}
然後,對於NV21
格式的Camera回調:
//input即爲攝像頭回調byte[]數組,output爲int[] 轉換後的rgb輸出
//width 爲圖片寬度 height爲圖片高度
final int frameSize = width * height;
for (int j = 0, yp = 0; j < height; j++) {
int uvp = frameSize + (j >> 1) * width;
int u = 0;
int v = 0;
for (int i = 0; i < width; i++, yp++) {
int y = 0xff & input[yp];
if ((i & 1) == 0) {
v = 0xff & input[uvp++];
u = 0xff & input[uvp++];
}
output[yp] = YUV2RGB(y, u, v);
}
}
對於Camera2
的Image
:
首先得到yuv各個分量的byte[]:
protected void fillBytes(final Plane[] planes, final byte[][] yuvBytes) {
// Because of the variable row stride it's not possible to know in
// advance the actual necessary dimensions of the yuv planes.
for (int i = 0; i < planes.length; ++i) {
final ByteBuffer buffer = planes[i].getBuffer();
if (yuvBytes[i] == null) {
LOGGER.d("Initializing buffer %d at size %d", i, buffer.capacity());
yuvBytes[i] = new byte[buffer.capacity()];
}
buffer.get(yuvBytes[i]);
}
}
private byte[][] yuvBytes = new byte[3][];
final Plane[] planes = image.getPlanes();
fillBytes(planes, yuvBytes);
然後得到stride:
yRowStride = planes[0].getRowStride();
final int uvRowStride = planes[1].getRowStride();
final int uvPixelStride = planes[1].getPixelStride();
最後轉換:
/**
* width 爲圖片寬度
* height 爲圖片高度
* yRowStride 爲y分量的行跨度
* uvRowStride 爲uv分量的行跨度
* uvPixelStride 爲uv分量的像素跨度
* yData,uData,vData 分別對應yuvBytes[0],yuvBytes[1],yuvBytes[2]
* out[] 爲輸出的rgb int[]數組
*/
int yp = 0;
for (int j = 0; j < height; j++) {
int pY = yRowStride * j;
int pUV = uvRowStride * (j >> 1);
for (int i = 0; i < width; i++) {
int uv_offset = pUV + (i >> 1) * uvPixelStride;
out[yp++] = YUV2RGB(0xff & yData[pY + i], 0xff & uData[uv_offset], 0xff & vData[uv_offset]);
}
}
結語
好了,以上就是關於移動端獲取攝像頭回調數據的相關內容,在下一篇我們再去了解關於和OpenCV中mat的轉換和相關操作,以及豎屏時,圖片逆時針旋轉90度的問題!