Android 爛裝置想跑人臉辨識6 – 三路高頻影像流零拷貝:單一可重用 Bitmap 鐵碗與 Zero-Allocation

在我們這台掛在牆上的壁掛門禁機上,同時插了兩顆硬體鏡頭:

  • RGB 可見光鏡頭:640×480 YUV420 @ 30fps(每幀 460.8 KB,吞吐量約 13.8 MB/s)。
  • Sony IMX516 iToF 深度鏡頭:640×1920 RAW12 @ 30fps(4 相位 Raw 拼接,每幀 1.84 MB,吞吐量高達 55.3 MB/s)。

算盤一撥,現場數據令人頭皮發麻:

13.8 MB/s (RGB) + 55.3 MB/s (ToF RAW12) = 69.1 MB/s

兩顆鏡頭同時運作,底層每秒要吞吐超過 60 幀影像,原始數據灌入速度接近 每秒 70MB

如果照著網路上常見的教科書範例寫:在相機回調裡把 YUV 轉成 Bitmap、每一幀 new 一個 Data 容器塞進隊列、再呼叫模型推論——

69.1 MB/s (相機原始幀) + 36.8 MB/s (RGB ARGB_8888 Bitmap) = 105.9 MB/s

每秒鐘往記憶體裡傾倒超過 100MB 的物件垃圾!在我們這台只有 4 核心 1.3GHz 弱 CPU、2GB RAM 的破板子上,Java Heap 在 1.5 秒內就會被直接塞滿,觸發頻繁的 Stop-The-World 阻塞式 GC,畫面瞬間卡死成幻燈片。

這是一場在懸崖邊緣的極限拉鋸戰。要讓弱雞硬體穩跑 30fps,我們動了五道關鍵的外科手術。


手術一:發放鐵碗,告別免洗餐具(0-new 鐵律)

在 30fps 影像流下,留給每一幀的處理時間預算只有極其嚴苛的 33.3ms

若每幀 new 一個 1.84MB 的 RAW 陣列加上 1.22MB 的 Bitmap,Java 堆積水位每秒暴漲。我們的原則極度冷血:全系統嚴禁使用免洗餐具,所有人開機自備鐵碗!

// CameraRgbHelper.java
private static final int YUV_QUEUE_MAX_SIZE = 3;

// 開機只預先分配 3 個固定的 byte[] 鐵碗,終生不銷毀
private ArrayBlockingQueue<byte[]> mFreeYuvQueue = new ArrayBlockingQueue<>(YUV_QUEUE_MAX_SIZE);
private ArrayBlockingQueue<byte[]> mYuvQueue = new ArrayBlockingQueue<>(YUV_QUEUE_MAX_SIZE);

private final ImageReader.OnImageAvailableListener mOnImageAvailableListener = new ImageReader.OnImageAvailableListener() {
    @Override
    public void onImageAvailable(ImageReader reader) {
        Image image = reader.acquireLatestImage();
        if (image == null) return;

        try {
            // 從池子借出洗乾淨的空閒鐵碗
            byte[] ironBowl = mFreeYuvQueue.poll();
            if (ironBowl == null) {
                // 隊列滿載時無情拋棄最舊的幀(Drop-Oldest),保證 AI 永遠只算最新畫面
                ironBowl = mYuvQueue.poll();
            }

            if (ironBowl != null) {
                // 透過 QFastCV 進行 ARM NEON 向量記憶體直拷(耗時 < 1ms)
                QFastCV.yuv420SPToYuv420P(..., ironBowl, ...);
                mYuvQueue.offer(ironBowl);
            }
        } finally {
            // 靈魂關鍵:必須在 0.3ms 內歸還硬體 Buffer,防止 Camera HAL 飢餓死鎖!
            image.close();
        }
    }
};

相機底層的硬體 Buffer 只有 2~3 個,只要你的 Java 執行緒稍微卡頓沒關閉,整個 Camera HAL 驅動就會當場死鎖。我們在 0.3ms 內把數據倒進鐵碗並立刻 image.close(),硬體資源秒借秒還。


手術二:破除 Bitmap 詛咒(單一可重用 Bitmap 鐵碗)

相機吐出來的是 YUV byte[],但 AI SDK 介面偏偏要求傳入 Bitmap。因為 copyPixelsFromBuffer 只做純粹的記憶體拷貝,不包含色彩空間轉換,所以我們必須先將 YUV 轉換為 ARGB。如果每幀呼叫 Bitmap.createBitmap()bitmap.copy(),每秒會額外產生 36MB 的 Bitmap 垃圾。

我們在開機時只 new 一張固定的 Bitmap 鐵碗,並預先分配一塊 DirectBuffer。推論時先透過 Native 庫完成必要的 YUV 到 ARGB 轉換,再將底層記憶體直接覆寫進 Bitmap 像素,達成 0 分配:

// 開機只 new 一次 Bitmap 鐵碗與 Native 緩衝區
Bitmap reusableBitmap = Bitmap.createBitmap(640, 480, Bitmap.Config.ARGB_8888);
ByteBuffer argbBuffer = ByteBuffer.allocateDirect(640 * 480 * 4);

public void processFrame(byte[] ironBowlBytes) {
    argbBuffer.rewind();

    // 必須的色彩轉換步驟:透過 LibYUV 在 Native 端將 YUV 轉為 ARGB 並寫入 argbBuffer(極速、0 GC)
    NativeYuvUtils.yuv420ToArgb(ironBowlBytes, 640, 480, argbBuffer);
    
    argbBuffer.rewind();

    // 將已轉換的 ARGB 數據直接 memcpy 刷入既有 Bitmap 的 Native 記憶體位址(0 記憶體分配)
    reusableBitmap.copyPixelsFromBuffer(argbBuffer);

    // 餵給人臉偵測模型
    Face3DAIModelKit.start().runRgbFD(reusableBitmap, 140, 140);
}

整台設備在推論循環中只有一張 Bitmap 實體,透過 Native 轉換與記憶體直灌,徹底消滅 GC 壓力。

鐵碗帶來的硬體級回報:QM215 實測基準數據

這不是理論自嗨。在我們這塊 2GB RAM / Android 9 的高通 QM215 門禁板上,Systrace 與 dumpsys 記錄了最真實的硬體震撼:

系統監控指標 (dumpsys / top) 優化前 (每幀 new Bitmap 免洗餐具) 優化後 (單一 Bitmap 鐵碗機制) 實戰架構收益
Java 堆分配速率 45 ~ 55 MB/s 0 bytes/s (穩態零新增) 徹底消滅短命大物件(LOS)
GC 頻率與類型 每秒 2 ~ 3 次 GC_FOR_ALLOC 0 次 / 小時(穩態零 GC) 消除 JVM 內部資源爭奪
STW 停頓時間 每次 15 ~ 35 ms(畫面凍結) 0 ms 徹底杜絕 UI 與相機管線卡死
Linux 內核 kswapd0 佔用 45% ~ 60%(瘋狂壓縮分頁至 ZRAM) < 2%(內核極度平穩) 奪回 50%+ 實體 CPU 算力
相機預覽掉幀率 35% ~ 42%(30fps 暴跌至 18fps) < 0.1% (穩跑 30.0 fps) 達成 720h 不停機通行

嚴謹正名:Zero-Allocation vs. 真正晶片級 Zero-Copy

嚴格來說,reusableBitmap.copyPixelsFromBuffer(argbBuffer) 底層在 Skia 仍會執行一次 CPU memcpy。在底層多媒體工程師眼裡,這不叫真正的晶片級零拷貝(真正零拷貝走的是相機 ISP 直接輸出 DMA-BUF / ION 共享記憶體,GPU 直接貼圖渲染,CPU 碰都不碰)。

但這套方案在工程實務上解決了最致命的痛點:它實現了應用層的「Zero-Allocation(零記憶體分配)」。受制於第三方閉源 AI SDK 接口寫死只吃 Bitmap 的無奈現實下,鐵碗是唯一能斬斷 GC 海嘯、將 50% CPU 算力奪回給 AI 的最優解!


手術三:無情的 Drop-Oldest(雙層背壓機制)

相機每秒硬生生塞進 30 幀,但在 1.3GHz 的弱 CPU 上,跑完完整的 512 維特徵萃取一秒頂多算 5~6 幀。

算式告訴我們,兩者之間存在著巨大的速率差:

30 fps (相機供給) vs 6 fps (AI 消化極限) = 每秒積壓 24 幀!

如果不做背壓控制,隊列會在幾秒內被灌爆。我們設計了雙層背壓丟包策略

  1. 硬體層:一律呼叫 acquireLatestImage()。相機底層若積壓多幀,只取最新一幀,過期的殘影在驅動層直接丟棄。
  2. 軟體層:隊列上限鎖死為 3(YUV_QUEUE_MAX_SIZE = 3)。隊列一滿,直接踢掉最舊的幀,把鐵碗歸還空閒池。

門禁機永遠只推論「當下最新的一張臉」,絕不浪費半點算力去算過期了幾百毫秒的歷史殘影。


手術四:階梯式漏斗淘汰(四大惡霸短路退場)

CNN 512 維特徵萃取(MobileFaceNet)在 1.3GHz CPU 上跑一次要耗費 180ms ~ 220ms。如果不管畫面有沒有人、清不清楚都跑完整模型,4 顆 CPU 核心保證 100% 滿載過熱,進而觸發晶片降頻。

我們建立了階梯式漏斗過濾,依計算成本由低到高逐級斬殺:

[相機輸入 (30fps)]
       │
       ▼ 第 0 關:雙鏡頭時間戳同步檢查 (< 0.1ms) ───► 未同步?丟棄!
       │
       ▼ 第 1 關:RGB 人臉偵測 FD (~5ms) ──────────► 沒臉?立即短路退出!
       │
       ▼ 第 2 關:清晰度與姿態過濾 FQ (~1ms) ──────► 模糊 / 側臉?丟棄!
       │
       ▼ 第 3 關:IR + 3D 深度雙模態活體 (~15ms) ──► 假人 / 照片?直接報警!
       │
       ▼ 第 4 關:512 維 CNN 特徵萃取 FR (~200ms)
[比對本地 5000 人特徵庫 (ACL 矩陣搜尋 ~3ms)]

為什麼這能拯救破硬體?

  • 大門口 90% 的時間是沒人的:第 1 關(FD)在 5ms 內確認沒臉,直接 return false 短路退場,省下後續 99% 的無謂運算。
  • 只有當一個大活人端端正正站在鏡頭前、且通過了清晰度與雙模態活體防偽,系統才會調用耗時 200ms 的 CNN 核心模型。
  • 待機時平均 CPU 負載直接從 100% 壓制到 15%~20%,徹底解決密閉機殼內的過熱降頻問題。

手術五:避開 Java 轉換的 GC 炸彈(QFastCV 原生直轉)

使用 Android 官方的 YuvImage.compressToJpeg() 會製造 3 個大型暫存 byte 陣列與昂貴的 JPEG 編解碼開銷,一次轉換耗時 40ms。

我們在 CameraRgbHelper 中直接調用高通 QFastCV 原生庫:

// 透過 ARM NEON 向量指令在 Native C++ 完成 YUV 格式轉換
QFastCV.yuv420SPToYuv420P(
    bufferY, bufferU, 
    CAM_RGB_WIDTH, CAM_RGB_HEIGHT,
    stride0, stride1,
    dstY, dstU, dstV,
    0, 0, 0
);

轉換在 C++ 層以 SIMD 指令並行運算,耗時不到 1ms,Java 堆積 0 垃圾。


結語與防坑心法

在低配嵌入式設備上搞邊緣 AI,硬體不會同情你:

  1. 鐵碗循環是基石:全系統 0-new,用固定的緩衝池斬斷 GC 的觸手。
  2. 背壓丟包要心狠手辣:永遠只推論當下最新的幀,過期幀沒有任何留戀價值。
  3. 階梯漏斗省算力:低成本算法在前面把關,高成本模型在最後收尾。
  4. 記憶體抽脂戰果:歷經鐵碗循環、Native 覆寫與 DirectBuffer 零拷貝這五大手術,Java 層物件池重用機制大幅壓低了 Dalvik Heap 的峰值佔用與 GC 頻率,搭配相機解析度降級與 Stream 數量精簡,在系統層面為 AI 模型釋放了可觀的實體記憶體空間。

下一篇,我們把視角拉到真實物理世界:當門禁機遭遇網卡死鎖、相機斷流與半夜當機時,如何用一套軟硬體 Watchdog 實現物理級不死之身!


參考資料 (References)

  1. Android Camera2 API Best Practices: Avoiding HAL Starvation.
    https://developer.android.com/training/camera2

  2. Android Graphics Bitmap Native Allocation & In-place Buffers.
    https://source.android.com/docs/core/graphics

  3. Qualcomm FastCV Computer Vision SDK Overview.
    https://developer.qualcomm.com/software/fastcv-sdk

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *