Android 端相機實時掃描之視訊流採集
本文是ofollow,noindex">SmartCamera 原理分析的第一篇,SmartCamera 是我開源的一個 Android 相機拓展模組,能夠實時採集並且識別相機內物體邊框是否吻合指定區域。
SmartCamera 是繼SmartCropper 之後開源的另外一個基於 OpenCV 實現的開源庫,他們的不同點主要包括以下幾個方面:
- SmartCropper 是處理一張圖片,輸出一張裁剪的圖片,而 SmartCamera 需要實時處理 Android 相機輸出的視訊流,對效能要求會更高;
- SmartCamera 是識別相機內物體是否吻合指定的四邊形,實現方式上也會有所差異;
- 另外 SmartCropper 的使用者經常會反饋某些場景識別率不高,故 SmartCamera 提供了實時預覽模式,並且提供了更細化的演算法引數調優,讓開發者可以自己修改掃描演算法以獲得更好的適配性。
閱讀本系列文章之前,讀者可以先閱讀之前寫的《Android 端基於 OpenCV 的邊框識別功能》 ,瞭解如何從頭開始搭建一個整合 OpenCV 的 NDK 專案,瞭解 OpenCV 庫的作用及其用法;然後可以將 SmartCamera clone 到本地方便程式碼查閱與除錯;
最重要的是別忘記給 SmartCamera 和 SmartCropper 點個 star!
本文主要講述 Android 相機視訊流採集,視訊流幀資料格式分析,以及如何提高採集的效能。
相機視訊流獲取
android.hardware.Camera 提供瞭如下 API 獲取相機視訊流:
mCamera.setPreviewCallback(new Camera.PreviewCallback() { @Override public void onPreviewFrame(byte[] data, Camera camera) { } });
每一幀的資料均會通過該回調返回,回撥內的 byte[] data 即是相機內幀影象資料。該回調會將每一幀資料一個不漏的給你,大多數情況下我們根本來不及處理,會將幀資料直接丟棄。另外每一幀的資料都是一塊新的記憶體區域會造成頻繁的 GC。
所以 android.hardware.Camera 提供了另外一個有更好的效能, 更容易控制的方式:
mCamera.setPreviewCallbackWithBuffer(new Camera.PreviewCallback() { @Override public void onPreviewFrame(byte[] data, Camera camera) { } });
該方法需要與以下方法配合使用:
mCamera.addCallbackBuffer(new byte[size])
這樣回撥內每一幀的 data 就會複用同一塊緩衝區域,data 物件沒有改變,但是 data 資料的內容改變了,並且該回調不會返回每一幀的資料,而是在重新呼叫 addCallbackBuffer 之後才會繼續回撥,這樣我們可以更容易控制回撥的數量。
程式碼如下:
mCamera.addCallbackBuffer(new byte[size]) mCamera.setPreviewCallbackWithBuffer(new Camera.PreviewCallback() { @Override public void onPreviewFrame(byte[] data, Camera camera) { processFrame(data); mCamera.addCallbackBuffer(data) } });
雖然我們會在 processFrame 函式中進行大量效能優化,但是為了不影響處理幀資料時阻塞 UI 執行緒造成掉幀,我們可以將處理邏輯放置到後臺執行緒中,這裡使用了 HandlerThread, 配合 Handler 將處理資料的邏輯放置到了後臺執行緒中。
最終程式碼如下所示:
HandlerThread processThread = new HandlerThread("processThread"); processThread.start(); processHandler = new Handler(processThread.getLooper()) { @Override public void handleMessage(Message msg) { super.handleMessage(msg); processFrame(previewBuffer); mCamera.addCallbackBuffer(previewBuffer); } }; mCamera.addCallbackBuffer(previewBuffer); mCamera.setPreviewCallbackWithBuffer(new Camera.PreviewCallback() { @Override public void onPreviewFrame(byte[] data, Camera camera) { processHandler.sendEmptyMessage(1); } }); mCamera.startPreview();
在 onPreviewFrame 回撥函式中只是傳送了訊息通知 HandlerThread 處理資料,處理的資料即為 previewBuffer ,處理完了之後呼叫:
mCamera.addCallbackBuffer(previewBuffer);
這樣 onPreviewFrame 會開始回撥下一幀資料。
那麼緩衝區域的大小 size 是如何確定的呢?這要從幀資料格式說起。
幀資料格式
首先每一幀圖片的預覽大小是我們提前設定好的,可以通過如下方法獲取:
int width = mCameraParameters.getPreviewSize().width; int height = mCameraParameters.getPreviewSize().height;
很多人可能會猜 size 應該等於 width * height,實際上這要看這每一幀圖片的格式,假設是 ARGB 格式,並且每個通道有 256(0x00 – 0xFF) 個值,每個通道需要一個位元組或者說 8 個位(bit)來表示,那麼表示每個畫素點的範圍是:
0x00000000 - 0xFFFFFFFF
一個畫素點總共需要 4 個位元組(byte)表示,也就能得出表示一張 width * height 圖片的 byte 陣列的大小為:
// 4個通道,每個通道 8 個位,總共需要 4 位元組 width * height * ( 8 + 8 + 8 + 8 ) / 8 = width * height * 4 (byte)
舉一反三,假設每一幀的圖片格式為 RGB_565,那麼 byte 陣列的大小是:
// 4個通道,需要 16 個位,總共需要 2 位元組 width * height * ( 5 + 6 + 5 ) / 8 = width * height * 2 (byte)
那麼回到 setPreviewCallbackWithBuffer 回撥返回的 data 資料,這個資料的格式是怎樣的呢?
不用猜,查閱 Android 官方開發者文件:https://developer.android.com/reference/android/hardware/Camera.PreviewCallback
得知 data 的預設格式為 YCbCr_420_SP (NV21) ,也可以通過如下程式碼設定成其他的預覽格式:
Camera.Parameters.setPreviewFormat(ImageFormat)
ImageFormat 枚舉了很多種圖片格式,其中 ImageFormat.NV21 和 ImageFormat.YV12 是官方推薦的格式 ,原因是所有的相機都支援這兩種格式。
官方推薦也不是我瞎猜的,見官方文件:
https://developer.android.com/reference/android/hardware/Camera.Parameters#setPreviewFormat(int)
那麼 NV21, YV12 又是什麼格式,與我們熟知的 ARGB 格式有什麼不同呢?
NV21, YV12 格式均屬於 YUV 格式,也可以表示為 YCbCr,Cb、Cr的含義等同於U、V。
YUV,分為三個分量,“Y”表示明亮度(Luminance、Luma),“U” 和 “V” 則是色度、濃度(Chrominance、Chroma),Y’UV的發明是由於彩色電視與黑白電視的過渡時期[1]。黑白視訊只有Y(Luma,Luminance)視訊,也就是灰階值。到了彩色電視規格的制定,是以YUV/YIQ的格式來處理彩色電視影象,把UV視作表示彩度的C(Chrominance或Chroma),如果忽略C訊號,那麼剩下的Y(Luma)訊號就跟之前的黑白電視訊號相同,這樣一來便解決彩色電視機與黑白電視機的相容問題。Y’UV最大的優點在於只需佔用極少的頻寬。
上面的表述來至於維基百科。大致可以得出以下結論:
YUV 格式的圖片可以方便的提取 Y 分量從而得到灰度圖片。
關於 YUV 格式更詳細的介紹可以參考:https://www.cnblogs.com/azraelly/archive/2013/01/01/2841269.html
下面直接給出結論:
根據取樣格式不同, 或者說排列順序不同,YUV 又細分成了 NV21, YV12 等格式,如下所示:
I420: YYYYYYYY UU VV=> YUV420P
YV12: YYYYYYYY VV UU=> YUV420P
NV12: YYYYYYYY UV UV=> YUV420SP
NV21: YYYYYYYY VU VU=> YUV420SP
其中 YUV 4:2:0 取樣,每四個Y共用一組UV分量。
假設有一張 NV21 格式的圖片,大小為 width * height, 其中 Y 分量表示的灰度圖每個畫素可以使用 1byte 表示,Y 分量佔用了:
width * height * 1 byte
VU 分量佔用了:
width * height / 4 + width * height / 4
所以該圖片佔用總大小為:
width * height * 1.5 byte
終於確定了 size 的大小,實際上 Android API 已經給我們提供了方便的計算方法,我們不用背各個格式所需的大小:
width * height * ImageFormat.getBitsPerPixel(ImageFormat.NV21) / 8]
ImageFormat.getBitsPerPixel(ImageFormat.NV21) 返回了 12 ,表示 NV21 格式的圖片每個畫素需要 12 個 bit 表示,即 1.5 個 byte。
Android API 同樣提供了方法讓我們將 YUV 格式的圖片轉化為我們熟知的 ARGB 格式:
YuvImage = image = new YuvImage(data, ImageFormat.NV21, size.width, size.height, null); ByteArrayOutputStream stream = new ByteArrayOutputStream(); image.compressToJpeg(new Rect(0, 0, size.width, size.height), 100, stream); Bitmap bitmap = BitmapFactory.decodeByteArray(stream.toByteArray(), 0, stream.size());
但是在實時掃描的場景中我們並不需要將 YUV 的格式轉成 ARGB 格式,而是直接將 data 資料傳遞給 jni 函式處理,至於 jni 函式如何處理這段資料會在下篇文章中分析。
>> 轉載請註明來源: