Android 爛裝置想跑人臉辨識11 – ToF RAW12 轉 3D 點雲

在門禁機的真實專案中,除了默默在後台刷臉開門,很多客戶(特別是高端科技園區、展覽館或金融專案)會提出一個極具科技感的要求:在螢幕上即時呈現賽博龐克風格的 3D 點雲人臉(Point Cloud View),並支援手指滑動旋轉。

普通 RGB 鏡頭拍到的是 2D 平面像素,而 iToF 深度鏡頭吐出來的是整片 3D 空間物理座標。

但問題來了:一張 640 × 480 的影像有整整 30.7 萬個像素點。要把 Sony 鏡頭吐出的原始生肉雜訊資料(RAW12),在 1.3GHz 的弱晶片上解算出精準的 3D 點雲、IR 灰階圖,並以 30fps 即時渲染出來,如果用傳統 CPU 慢慢算,每幀至少花費 50ms 以上,門禁機當場卡死。

今天就來深度拆解 iToF 光學物理與數學映射3D 點雲(Pcl)的完整製作工藝,以及核心演算法庫 libRawToDepthSDK.so 中的 RawToIrDepthNew 與 ARM NEON 極限加速技術


一、 iToF 物理本質:物理世界與數學的極簡映射

光速每秒 30 萬公里,光線從鏡頭出發打到臉上再彈回,只花了約 2 奈秒(0.000000002 秒)。普通晶片根本按不出這種極速碼錶。

iToF 演算法的最高境界,就是把看不見的光波物理特徵,直接映射成中學幾何的向量數學

二、 Pcl 3D 點雲的完整製作(從 RAW12 到空間粒子)

把 Sony 640 × 1920 的 RAW12 原始生肉數據變成螢幕上的 3D 旋轉點雲,在專案中經過了 5 道極為精密的工序:

工序 1:C++ 物理幾何解算(產出 4 平面連續數據 pCloud

RawToDepthSDK.RawToIrDepthNew() 中,C++ 解算出包含 122.8 萬個 float 的一維連續陣列(640 × 480 × 4):

  • [0 ~ 307,199]:X 空間座標(公尺)
  • [307,200 ~ 614,399]:Y 空間座標(公尺)
  • [614,400 ~ 921,599]:Z 物理深度(公尺)
  • [921,600 ~ 1,228,799]:IR 紅外反射強度

工序 2:空間過濾與背景剃除(距離 + 強度雙門檻)

CameraFrameContainerMK1.java 中進行空間裁切:

  • 紅外反射強度門檻(pclIr > pcl_ir_threshold: 25:直接剔除空氣中的微塵雜訊與暗處噪點。
  • 物理空間距離門檻(0.2 < z && z < 1.2:強制只保留離鏡頭 20 公分至 120 公分 的人體點雲,瞬間將背後的天花板與背景牆壁剃除乾淨。

工序 3:sample 步長動態降採樣(砍掉 80% GPU 負擔)

全解析度下有 30.7 萬個點,如果全部送進 GPU 繪圖,高通弱 GPU 會當場卡死。我們透過 for(int xi=0; xi<width; xi+=sample) 進行隔點抽樣:

  • sample = 2:抽取約 7.6 萬點(對應展示圖中約 6.5 萬有效人臉點)。
  • sample = 4:抽取約 1.9 萬點。

精確將每幀點數控制在 2~5 萬點,既維持了人臉五官的立體精細度,又解放了 GPU 80% 的算力!

工序 4:7-Float 頂點交錯排布(Stride = 28 bytes)

為了讓 OpenGL 能夠以最高效率讀取,每個點打包成 7 個連續 Float(7 × 4 = 28 bytes)

// CameraFrameContainerMK1.java
outDepthResult[pointsCount * 7]     = y; // X, Y, Z 空間坐標
outDepthResult[pointsCount * 7 + 1] = x;
outDepthResult[pointsCount * 7 + 2] = z;
outDepthResult[pointsCount * 7 + 3] = colorArray[0]; // R, G, B, A 顏色 (0, 253, 255 霓虹青藍)
outDepthResult[pointsCount * 7 + 4] = colorArray[1];
outDepthResult[pointsCount * 7 + 5] = colorArray[2];
outDepthResult[pointsCount * 7 + 6] = 1.0f;
pointsCount++;

工序 5:Direct Native 記憶體零 GC 管理

GlSurfacePclRenderMK1 初始化時,直接在 Native 堆外記憶體分配 ByteBuffer.allocateDirect()

  • 運行中每一幀直接透過 mTriangle1Vertices.put(outDepthResult) 覆蓋寫入。
  • 全程 0 次 Java 物件分配、0 次 GC 停頓,保障 30fps 極致絲滑。

三、 極限加速如何把 50ms 壓進 3ms?

一張 640 × 480 影像有 307,200 個像素點,每個點都要算三角函數、平方根與浮點除法。

如果讓單核 CPU 跑迴圈一個一個慢慢改 30 萬張考卷,一秒改 30 次直接卡死。我們在 TofRawToDepthNeon.cpp 裡用了 4 大殺手鐧:

1. Atan2 九九乘法表查表法(O(1) 記憶體尋址)

計算 arctan2(Q, I) 需要跑昂貴的多項式泰勒展開(耗費 50~100 個時脈)。我們預先把所有可能出現的 (Q, I) 組合計算成一張 gAtan2Table 查找矩陣。運算時直接翻表查答案,將耗時的三角函數消滅為 O(1) 的記憶體檢索(只需 1~2 個時脈)!

2. ARM NEON SIMD 向量並行(8 像素同時秒殺)

普通 CPU 暫存器(32/64-bit)一次只能算 1 個像素。ARM NEON 擁有 128-bit 向量暫存器(q0~q15),硬體在 1 個時脈週期 內同時載入 8 個像素的 A0, A1, A2, A3 進行並行相減(vsubq_u16),運算吞吐量直接暴增 8 倍!

3. 牛頓-拉弗森逼近開方(消滅除法與開根號硬體延遲)

在 CPU 硬體中,浮點開根號指令(fsqrt)需要 15~30 個時脈,會讓 CPU 流水線嚴重停擺。我們使用 ARM 硬體專屬指令 vrsqrteq_f32 與一輪牛頓疊代乘法,在 3 個時脈 內瞬間完成開方:

// TofRawToDepthNeon.cpp: 牛頓-拉弗森逼近開方
inline static float32x4_t vsqrt(float32x4_t v) {
    float32x4_t r = vrsqrteq_f32(v);        // 1 個時脈:硬體求平方根倒數初始估計
    r = vmulq_f32(vrsqrtsq_f32(v, r), r);   // 1 個時脈:牛頓疊代逼近
    return vmulq_f32(v, r);                // 1 個時脈:轉為開方
}

四、 OpenGL ES 3D 點雲即時渲染(GL_POINTS

解算出 30 萬個 3D 空間座標並打包為 7-Float 頂點後,在 GlSurfacePclRenderMK1.java 進行螢幕繪製:

1. 綁定頂點屬性指針(Stride = 28 bytes)

// GlSurfacePclRenderMK1.java
// 綁定 Position (X, Y, Z) - Offset = 0
aTriangleBuffer.position(0);
GLES20.glVertexAttribPointer(mPositionHandle, 3, GLES20.GL_FLOAT, false, 28, aTriangleBuffer);
GLES20.glEnableVertexAttribArray(mPositionHandle);

// 綁定 Color (R, G, B, A) - Offset = 3
aTriangleBuffer.position(3);
GLES20.glVertexAttribPointer(mColorHandle, 4, GLES20.GL_FLOAT, false, 28, aTriangleBuffer);
GLES20.glEnableVertexAttribArray(mColorHandle);

2. MVP 矩陣與著色器投影

在 Vertex Shader 中,透過手勢旋轉矩陣(M)、虛擬攝影機視角(V)與透視投影(P)三者相乘,一行指令在螢幕上投射出立體旋轉人臉:

uniform mat4 u_MVPMatrix;
uniform float u_PointSize;
attribute vec4 a_Position;
attribute vec4 a_Color;
varying vec4 v_Color;

void main() {
    v_Color = a_Color;
    gl_PointSize = u_PointSize;             // 控制點粒子粗細 (例如 0.55)
    gl_Position = u_MVPMatrix * a_Position; // 3D 空間坐標轉為螢幕 2D 像素!
}

3. 單一 Draw Call 繪製

呼叫 GLES20.glDrawArrays(GLES20.GL_POINTS, 0, pointCount),幾萬顆賽博龐克青藍色立體光暈粒子瞬間在螢幕上旋轉綻放!

視覺參數由 pclview_ui.json 動態配置:

  • color: 0blue0RGBArray 霓虹青藍光暈(RGB: 0, 253, 255)。
  • point_size: 0.55:微粒子大小。
  • scale: 4.0:螢幕居中縮放比例。
  • view_z: -0.7:虛擬攝影機俯視距離。

不需重編譯代碼即可隨意微調點雲風格。



參考資料 (References)

  1. Sony IMX516 Indirect Time-of-Flight (iToF) Sensor Principle & Phase Demodulation.
  2. ARM Cortex-A Series Programmer’s Guide: NEON Vectorization & Newton-Raphson Reciprocal Engine.
    https://developer.arm.com/documentation/den0018/a/
  3. OpenGL ES 2.0 / 3.0 Reference Manual: Points Rendering (GL_POINTS) & Shading Language.
    https://www.khronos.org/registry/OpenGL-Refpages/es3.0/

發佈留言

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