前言
最近一段时间,接触到移动端音视频通话相关的内容,主要是结合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度的问题!