一、Camera2簡介
Camera2是Google在Android 5.0后推出的一個全新的相機API,Camera2和Camera沒有繼承關系,是完全重新設計的,且Camera2支持的功能也更加豐富,但是提供了更豐富的功能的同時也增加了使用的難度。Google的官方Demo:https://github.com/googlesamples/android-Camera2Basic
二、Camera2 VS Camera
以下分別是使用Camera2和Camera打開相機進行預覽並獲取預覽數據的流程圖。


可以看到,和Camera相比,Camera2的調用明顯復雜得多,但同時也提供了更強大的功能:
- 支持在非UI線程獲取預覽數據
- 可以獲取更多的預覽幀
- 對相機的控制更加完備
- 支持更多格式的預覽數據
- 支持高速連拍
但是具體能否使用還要看設備的廠商有無實現。
三、如何使用Camera2
-
獲取預覽數據
一般情況下,大多設備其實只支持ImageFormat.YUV_420_888
和ImageFormat.JPEG
格式的預覽數據,而ImageFormat.JPEG
是壓縮格式,一般適用於拍照的場景,而不適合直接用於算法檢測,因此我們一般取ImageFormat.YUV_420_888
作為我們獲取預覽數據的格式,對於YUV不太了解的同學可以戳這里。
1
2
3
4
|
mImageReader = ImageReader.newInstance(mPreviewSize.getWidth(), mPreviewSize.getHeight(),
ImageFormat.YUV_420_888, 2);
mImageReader.setOnImageAvailableListener(
new OnImageAvailableListenerImpl(), mBackgroundHandler);
|
其中OnImageAvailableListenerImpl的實現如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
private class OnImageAvailableListenerImpl implements ImageReader.OnImageAvailableListener {
private byte[] y;
private byte[] u;
private byte[] v;
private ReentrantLock lock = new ReentrantLock();
@Override
public void onImageAvailable(ImageReader reader) {
Image image = reader.acquireNextImage();
// Y:U:V == 4:2:2
if (camera2Listener != null && image.getFormat() == ImageFormat.YUV_420_888) {
Image.Plane[] planes = image.getPlanes();
// 加鎖確保y、u、v來源於同一個Image
lock.lock();
// 重復使用同一批byte數組,減少gc頻率
if (y == null) {
y = new byte[planes[0].getBuffer().limit() - planes[0].getBuffer().position()];
u = new byte[planes[1].getBuffer().limit() - planes[1].getBuffer().position()];
v = new byte[planes[2].getBuffer().limit() - planes[2].getBuffer().position()];
}
if (image.getPlanes()[0].getBuffer().remaining() == y.length) {
planes[0].getBuffer().get(y);
planes[1].getBuffer().get(u);
planes[2].getBuffer().get(v);
camera2Listener.onPreview(y, u, v, mPreviewSize, planes[0].getRowStride());
}
lock.unlock();
}
image.close();
}
}
|
-
注意事項
1. 圖像格式問題
經過在多台設備上測試,明明設置的預覽數據格式是ImageFormat.YUV_420_888
(4個Y對應一組UV,即平均1個像素占1.5個byte,12位),但是拿到的數據卻都是YUV_422
格式(2個Y對應一組UV,即平均1個像素占2個byte,16位),且U
和V
的長度都少了一些(在Oneplus 5和Samsung Tab s3上長度都少了1),也就是:(u.length == v.length) && (y.length / 2 > u.length) && (y.length / 2 ≈ u.length)
;
而YUV_420_888
數據的Y
、U
、V
關系應該是:y.length / 4 == u.length == v.length
;
且系統API中android.graphics.ImageFormat
類的getBitsPerPixel
方法可說明上述Y、U、V數據比例不對的問題,內容如下:
1
2
3
4
5
6
7
8
9
10
11
|
public static int getBitsPerPixel(int format) {
switch (format) {
...
case YUV_420_888:
return 12;
case YUV_422_888:
return 16;
...
}
return -1;
}
|
以及android.media.ImageUtils
類的imageCopy(Image src, Image dst)
函數中有這么一段注釋說明的確可能會有部分像素丟失:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
public static void imageCopy(Image src, Image dst) {
...
for (int row = 0; row <
effectivePlaneSize.getHeight
(); row++) {
if (row == effectivePlaneSize.getHeight() - 1) {
// Special case for NV21 backed YUV420_888: need handle the last row
// carefully to avoid memory corruption. Check if we have enough bytes to
// copy.
int remainingBytes = srcBuffer.remaining() - srcOffset;
if (srcByteCount > remainingBytes) {
srcByteCount = remainingBytes;
}
}
directByteBufferCopy(srcBuffer, srcOffset, dstBuffer, dstOffset, srcByteCount);
srcOffset += srcRowStride;
dstOffset += dstRowStride;
}
...
}
|
2. 圖像寬度不一定為stride(步長)
在有些設備上,回傳的圖像的rowStride
不一定為previewSize.getWidth()
,比如在OPPO K3手機上,選擇的分辨率為1520x760,但是回傳的圖像數據的rowStride
卻是1536,且總數據少了16個像素(Y少了16,U和V分別少了8)。
3. 當心數組越界
上述說到,Camera2設置的預覽數據格式是ImageFormat.YUV_420_888
時,回傳的Y
,U
,V
的關系一般是(u.length == v.length) && (y.length / 2 > u.length) && (y.length / 2 ≈ u.length)
;U
和V
是有部分缺失的,因此我們在進行數組操作時需要注意越界問題,示例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
/**
* 將Y:U:V == 4:2:2的數據轉換為nv21
*
* @param y Y 數據
* @param u U 數據
* @param v V 數據
* @param nv21 生成的nv21,需要預先分配內存
* @param stride 步長
* @param height 圖像高度
*/
public static void yuv422ToYuv420sp(byte[] y, byte[] u, byte[] v, byte[] nv21, int stride, int height) {
System.arraycopy(y, 0, nv21, 0, y.length);
// 注意,若length值為 y.length * 3 / 2 會有數組越界的風險,需使用真實數據長度計算
int length = y.length + u.length / 2 + v.length / 2;
int uIndex = 0, vIndex = 0;
for (int i = stride * height; i < length; i += 2) {
nv21[i] = v[vIndex];
nv21[i + 1] = u[uIndex];
vIndex += 2;
uIndex += 2;
}
}
|
4. 避免頻繁創建對象
若選擇的圖像格式是ImageFormat.YUV_420_888
,那么相機回傳的Image數據包將含3個plane,分別代表Y
,U
,V
,但是一般情況下我們可能需要的是其組合的結果,如NV21
、I420
等。由於Java的gc會影響性能,在從plane中獲取Y、U、V
數據和Y、U、V
轉換為其他數據的過程中,我們需要注意對象的創建頻率,我們可以創建一次對象重復使用。不僅是Y
,U
,V
這三個對象,組合的對象,如NV21
,也可以用同樣的方式處理,但若有將 NV21傳出當前線程,用於異步處理的操作,則需要做深拷貝,避免異步處理時引用數據被修改
。
四、示例代碼
- 示例代碼
https://github.com/wangshengyang1996/Camera2Demo - demo功能
- 演示Camera2的使用
- 獲取預覽幀數據並隔一段時間將原始畫面和處理過的畫面顯示到UI上
- 將預覽的YUV數據轉換為NV21,再轉換為Bitmap並顯示到控件上,同時也將該Bitmap轉換為相機預覽效果的Bitmap顯示到控件上,便於了解原始數據和預覽畫面的關系
- 運行效果
效果圖
最后,推薦給大家一個比較好用的開源安卓人臉識別sdk:
https://ai.arcsoft.com.cn/ucenter/user/reg?utm_source=cnblogs&utm_medium=referral