研究人員在 Meta 智慧眼鏡的官方 App「Stella」中,於一般未註冊的帳號下發現了一整套完整、可用的人臉辨識系統。三個 ExecuTorch 格式的人臉模型、本地資料庫 schema、向量索引、寫入路徑、通知通道,甚至連使用者介面「Connections」小工具都打包好,靜靜躺在裝置裡。
這篇文章清楚區分「能做到」與「實際上線」之間的差距,因為兩者之間的界線才是重點。
作者能展示的部分:這些機制真的存在,而且各元件彼此串接得宜。透過直接呼叫既有 handler,作者成功執行整條辨識 pipeline——對測試照片偵測出人臉、產出 2048 維的生物特徵 embedding、在本地索引中搜尋,匹配成功後觸發 Android 通知,標題顯示「Person Recognized」。
作者沒辦法展示的部分:這套東西是否已對一般使用者啟用。在未註冊的 stock 帳號上,使用者介面根本看不到,通知 deep-link 指向的目標畫面在這個版本也找不到。而且在測試帳號上,也沒觀察到 Meta 從伺服器端推送 identity 資料到對應資料庫。
所以這不是「Meta 正在偷偷辨識你看的人」,而是:完成這件事所需的整套工具就坐在裝置上,組裝好、可用,由 Meta 把關。
以下所有發現都可針對 com.facebook.stella v273.0.0.21 重現。
三個人臉辨識模型內建於裝置(約 100 MB)
三個 ExecuTorch(.pte)模型透過 NMLML——Meta 的素材傳遞系統——從 Meta 伺服器下載到裝置。
| 素材名稱(Meta 命名) | 檔案 | 大小 | 功能 |
|---|---|---|---|
| android_facerec_scrfd | SCRFD.pte | 3.4 MB | 從影像中偵測人臉 |
| android_facerec_kps_aligner | KPSAligner.pte | 117 KB | 裁切並對齊偵測到的人臉 |
| android_facerec_sface | SFace.pte | 96 MB | 將人臉轉成 2048 維的 embedding(生物特徵指紋) |
這些都對應到開源架構,也是其他 App 和學術專案常用的模型家族:SCRFD(InsightFace,ICLR 2022)、SFace(Zhong 等人,2021)、KPSAligner 標準 keypoint-based alignment(自 2015 年起 MTCNN、dlib、InsightFace 都這樣做)。Meta 的 SFace 變體看起來比公開版大不少(96 MB vs 約 40 MB;輸出 2048 維 vs 公開版的 128 到 512 維)。
要坦白說一件事:只靠「出貨偵測與 embedding 模型」本身,並不能當作辨識的證據。很多 App 跑裝置端人臉偵測,只是為了取景或自動對焦。
一個 cosine-similarity 人臉索引,維度正好對應裝置端指紋器
實際上跑起來、把資料寫進去的辨識 pipeline 對應這個資料庫:
/data/user/0/com.facebook.stella/files/rldrive/person_profiles/objects.db
這個檔案位於 RLDrive(Meta 跨裝置同步框架)之下,命名空間叫 person_profiles,設計上就是由遠端來填充。作者在測試帳號上沒有直接觀察到 Meta 推送資料到 person_profiles,作者要強調自己描述的是「這個通道存在」,不是「有觀察到傳輸」。
schema 長這樣:
CREATE TABLE person (
nodeid INTEGER PRIMARY KEY,
name TEXT,
uri TEXT,
blob BLOB,
deleted INTEGER,
version BLOB
);
CREATE TABLE face (
nodeid INTEGER PRIMARY KEY,
mediaPath TEXT,
personUri TEXT,
blob BLOB,
deleted INTEGER,
uri TEXT,
version BLOB
);
CREATE VIRTUAL TABLE face_mediaPath_vec
USING vec0(mediaPath float[2048] distance_metric=cosine);
每筆 face 紀錄透過 personUri 指向一個人。face.mediaPath 是 face_mediaPath_vec 的 primary key,存的就是 2048 維的 embedding。辨識的流程是:對該索引做 cosine-similarity 查詢,再 join 到 person.name 拿來填通知文字。
幾個對得起來的地方:
- vec0 是開源的 sqlite-vec 延伸模組,把 SQLite 變成向量相似度搜尋引擎。
- 維度 float[2048] 正好是 App 內 SFace embedder 的輸出形狀。
- cosine 是人臉 embedding 比較的標準選擇。
- schema 允許同一個 personUri 對應多筆 face(沒有 UNIQUE 限制),但實際生產環境是一對一還是一對多,從未註冊裝置看不出來。
端對端測試確認兩條分支,並且定位了寫入位置。作者先對資料庫做 SHA-256 snapshot 並 row count,然後把整條辨識 pipeline 跑了兩次:一次對空索引(無匹配),一次對預先塞入單一 embedding 的索引(匹配):
- 無匹配(空的 face_mediaPath_vec):寫入一對(uuid.jpg, uuid.emb)到 NameTagsPending/。沒有通知。
- 匹配:透過 production 的 nametags_recognition channel 觸發 Android 通知,標題「Person recognized」,內文「Recognized Michel Foucault」。NameTagsPending/ 沒多出東西。
無法辨識的人臉會被「staging」到磁碟:裁切圖加指紋,存在 NameTagsPending/
當裝置看到一張本地索引匹配不到的人臉時,Stella 會寫到:
/data/user/0/com.facebook.stella/files/NameTagsPending/
每張無法辨識的人臉會產生一對以新 UUID 命名的檔案:
.jpg——裁切、對齊後的人臉(SCRFD + KPSAligner 的產出)。.emb——2048 維的 SFace 指紋。
目錄權限是 0700,重開機後資料還在。只有在「無匹配」這個分支才會寫入;匹配到的人臉會走通知流程,不會在磁碟上留 trace。
作者直接驗證了 embedding 的結構:
File: NameTagsPending/1566ab46-[...].emb
Size: 8,192 bytes (2048 × float32, big-endian)
L2 norm: 0.999999
Min/max: −0.092110 / +0.098950
Mean: +0.000292
整體來看,(uuid.jpg, uuid.emb)就是一筆完整、可索引的單張人臉生物紀錄——形狀與編碼方式正好就是 cosine 索引 person_profiles/objects.db 設計來比對的對象。
NameTagsPending 這個名字最直白的解讀就是「等待名字的人臉」——以生物特徵形式編碼,等著被貼上標籤。作者把這個結構性事實寫下來,讓它自己說話:一張人臉影像和它的指紋,plaintext 存在一起,0700 權限,重開機後還在——這正是你打算「等標籤到位再回頭辨識人臉」時會組裝出來的資料集。
(編按:輸入 pipeline 的原始照片是 1970 年 Michel Foucault 肖像,Jerry Bauer 攝,318×418 px JPEG,28 KB,公有領域,來源 Wikimedia Commons。Stella pipeline 產出並寫入 NameTagsPending/ 的裁切人臉是 SCRFD 偵測 + KPSAligner 對齊的結果,83×118 px RGB JPEG,4,396 bytes。)
通知通道已經接好
Stella 定義了一個專門的 Android 通知 channel:
NotificationChannel{
id = "nametags_recognition"
name = "NameTags recognition"
description = "Notifications for recognized NameTags connections"
importance = IMPORTANCE_HIGH
sound = system notification sound
}
通知樣板在辨識 handler 裡 hardcode。標題永遠是「Person recognized」;內文永遠是「Recognized ” + name」,name 來自 person_profiles/objects.db 裡的 person 資料表:
NotificationCompat.Builder(ctx, "nametags_recognition")
.setContentTitle("Person recognized")
.setContentText("Recognized " + matched_name)
.setAutoCancel(true)
.setContentIntent(
PendingIntent.getActivity(
ctx,
matched_name.hashCode(),
Intent.ACTION_VIEW with
Uri "fb-viewapp://name_tags?face_id=" + face_id,
FLAG_IMMUTABLE | FLAG_UPDATE_CURRENT))
.build()
NotificationManagerCompat.notify(matched_name.hashCode(), notification)
通知可以點:它的 contentIntent 是一個 fb-viewapp://name_tags?face_id=<face_id> 形式的 deep link,這是 Meta 自家寫的 URL scheme,設計上是要在 Stella 裡打開 person-profile 畫面。
誠實補一句:v273 裡找不到那個目的地畫面。點通知會把 Stella 帶到預設分頁,因為對應的 Compose destination 不在 navigation graph 裡。通知會跳,但它指向的畫面在這個版本還沒建好。
APK 裡有個面向使用者的「Connections」入口
Stella v273 包含一個 widget,在標題為「Connections」的區塊下渲染一張卡片,文案是「See your connections」/「Remember the people you met and make new connections.」這兩段字串都是 APK 裡 hardcode 的字面值,不是伺服器端推送。
在未註冊的 stock 帳號上,這張卡片在 Glasses 分頁裡完全看不到。作者是在測試過程中才讓它出現的。正常使用情境下,使用者不會看到這張卡。
結論
- 完整的裝置端人臉辨識 stack:偵測、對齊、embedding、向量索引、儲存、寫入路徑、通知通道,在 Stella v273 裡都齊備。
- 它能用。端到端跑起來,會辨識已知人臉、在通知裡說出名字;對未知人臉則把裁切圖加指紋 staging 到磁碟。
- 索引維度、embedding 形狀、儲存 schema 三者一致,這是個連貫的系統,不是散落的死碼。
- 使用者實際會碰到的部分:「Connections」卡片、通知要打開的 profile 畫面——要嘛不在 build 裡,要嘛藏得更深。
- 活著的 pipeline 在用的那個資料庫,位於一個由 Meta 在伺服器端填充的 sync 命名空間,與其他已填充的命名空間並列,但在作者的帳號上沒觀察到針對人臉命名空間的推送。
- 作者沒主張的事:Meta 今日正在替使用者辨識陌生人、enrollment 資料在流動、或這套東西已在生產環境啟用。
不容易輕輕帶過的是:把這麼多裝置做到 2048 維人臉指紋和 hardcode 的「Person recognized」通知的程度,是一筆工程投資。這種能力不會是意外出貨的。至於何時或是否進入生產,答案在 Meta 那邊。
本研究與 WIRED 的報導同步發表。
網友熱門留言 (8)