當你在一台只有 2GB RAM、CPU 1.3GHz 的破板子上,被要求同時跑滿 RGB、紅外線(IR)與 TOF 深度相機「三路高頻影像流」,還要即時做人臉辨識防偽時,世界的運作規則就變了。
以下是我們在極限生存中,榨乾系統效能、把 GC 壓力降到最低的幾個狠招:
發放鐵碗(RecycleableBuffer 池)
相機每秒吐出來的影像資料跟山一樣大。每一張圖都去 new 新空間(免洗餐具),系統光是撿垃圾就會卡死。
解法:徹底禁止免洗餐具,手刻記憶體池 我們固定 new 好幾個「鐵碗」(FrameData)放到架上。裝完影像、算完之後不丟棄,而是重置狀態後放回架上。
// 池子裡固定 5 個鐵碗
public FrameData acquire() {
FrameData data = pool.poll();
if (data == null) data = new FrameData(); // 碗不夠才應急 new 一個免洗的
return data;
}
public void release(FrameData data) {
data.reset(); // 洗碗:只重置屬性,不釋放佔大空間的 byte[]
pool.offer(data); // 放回架上繼續裝下一頓
}
物理隔離(Bitmap Copy 的無奈)
我們雖然提倡不用免洗餐具,但對於相機傳來的 RGB 畫面,我們居然偷偷複製了一份,硬生生吃了一次效能虧。
解法:為了儘速歸還共享緩衝區 相機吐出來的資料是底層的「零拷貝共享記憶體」。慢慢吞吞送進 AI 裡算,不趕快歸還的話,相機底層拿不到空碗就會卡死罷工。
private void processFrame(Image image) {
Bitmap bitmap = imageToBitmap(image);
image.close(); // 🚨 關鍵:第一時間歸還共享 Buffer 讓相機繼續工作
// 複製一份乾淨的私有副本給 AI
Bitmap copy = bitmap.copy(Bitmap.Config.ARGB_8888, false);
bitmap.recycle(); // 回收轉換用的臨時 Bitmap
frameQueue.offer(copy); // 只把乾淨的副本送進去,避免算到一半被相機底層覆蓋
}
無情的 Drop-Oldest(雙層背壓)
相機一秒塞給你 30 張圖,但你那可悲的 CPU 一秒只能消化 6 張。圖塞在記憶體裡排隊,不用 10 秒機器就會撐爆。
解法:硬體與軟體的雙層殘酷淘汰 寧可流失客人,也絕對不讓店被擠爆。AI 絕對不能去算「過期的舊畫面」。
// 第一層:硬體級丟幀。授權相機底層直接扔掉舊幀,Java 端連看都不看
Image image = reader.acquireLatestImage();
// 第二層:軟體佇列。門口只放 3 張椅子
private static final int FD_QUEUE_MAX_SIZE = 3;
private void enqueueFrame(FrameData newFrame) {
synchronized (frameQueue) {
// 如果滿座了,把坐最久的人拖出來踹出去
while (frameQueue.size() >= FD_QUEUE_MAX_SIZE) {
FrameData oldest = frameQueue.removeFirst();
oldest.recycle(); // 洗碗
frameDataPool.release(oldest); // 放回鐵碗架,形成零 GC 閉環
}
frameQueue.addLast(newFrame);
}
}
漏斗式淘汰(第零關與四大惡霸)
連路邊飛過去的蒼蠅都要無腦啟動 CNN 模型去算?這台破機器的 CPU 會 24 小時滿載,然後過熱降頻、電池膨脹炸裂。
解法:極限過濾的漏斗機制 90% 的時間大門口是沒人的,那個最耗電的 200ms 大傢伙幾乎都在睡覺。
private void processFrame(FrameData frameData) {
// 第零關:時間同步容差 50ms。RGB/IR/Depth 不同步算出來也是垃圾
if (!trySync()) return;
// 第一關:臉部偵測 (~5ms)
List<FaceRect> faces = faceDetector.detect(frameData.getRgbBitmap());
if (faces.isEmpty()) {
frameData.recycle();
return; // 沒臉?直接滾
}
for (FaceRect face : faces) {
// 第二關:模糊檢查 (~1ms)
if (calculateBlurScore(face) < BLUR_THRESHOLD) continue;
// 第三關:活體防偽 (~15ms),靠紅外線與深度圖
if (antiSpoofing.predict(face) < LIVENESS_THRESHOLD) continue;
// 第四關:CNN 神經網路特徵擷取 (200ms+)
// 確定是個站定不動的活人,才准進大廳吹冷氣
float[] embedding = faceRecognition.extractFeature(face);
// ...
}
}
避開 Java 轉換的 GC 炸彈
處理相機 YUV 格式時,很多開發者會順手用 Android 內建的 API。在我們的板子上,這根本是瘋狂製造 GC 食物。
解法:Native 零中間物件直轉 一個中間 Java 物件都不產生,徹底掐死 GC 觸發的機會。
// ❌ 慢速路徑:產生 3 個 GC 炸彈 (YuvImage, ByteArrayOutputStream, byte[])
YuvImage yuvImage = new YuvImage(yuvData, ImageFormat.NV21, width, height, null);
ByteArrayOutputStream out = new ByteArrayOutputStream();
yuvImage.compressToJpeg(...);
byte[] jpegBytes = out.toByteArray();
return BitmapFactory.decodeByteArray(jpegBytes, 0, jpegBytes.length);
// ✅ 高速路徑:JNI 直轉,直接把結果塞進預先準備好的 rgba 陣列,零中間物件
public static native void nativeNV21ToRGBA(byte[] nv21, int width, int height, int[] rgba);

發佈留言