Glimtは毎日の1秒をつなげて1本の映像にします。面白いのは、その1秒をわざわざ撮る必要がないところです。Glimtはすでに撮った動画の中からその1秒を見つけ出し、ほかに5人の子どもが写っているにぎやかな誕生日パーティーの動画でも、あなたのお子さんが写っている1秒を選びます。処理はすべてiPhone上で行われ、サーバーもアカウントもアップロードも不要です。この記事では、使っているモデル、数式、実測のタイミングを交えてその仕組みを紹介します。
パイプライン
なぜAppleのVisionだけで済ませないのか
Visionはここで多くの仕事をしています。顔の検出(VNDetectFaceCaptureQualityRequest。各顔がどれだけシャープで正面を向いているかも評価します)、ランドマークの検出(VNDetectFaceLandmarksRequest)、そしてiOS 18ではフレームの見栄えの評価(CalculateImageAestheticsScoresRequest。レシートやスクリーンショットも判別します)です。ただ、その顔が誰なのかは教えてくれません。写真アプリの「ピープル」アルバムは知っていますが、そのインデックスはアプリには公開されていません。
Glimtの最初のバージョンでは、顔を切り抜いた画像にVNGenerateImageFeaturePrintRequestを使っていました。これは汎用の画像フィンガープリントで、顔そのものと同じくらい髪型、光、色、背景も比較してしまいます。実際、きょうだい同士や、同じ部屋・同じ時間帯に撮った子どもならあっさりマッチしてしまいました。そもそも道具の選択が間違っていたのです。
顔認識モデル
現在のGlimtは独自のモデルを同梱しています。SFace(Zhong et al., 2021, "Sigmoid-Constrained Hypersphere Loss for Robust Face Recognition")で、バックボーンはMobileFaceNet、OpenCV ZooでApache 2.0ライセンスのもと公開されています。LFWでの精度は99.40 %と報告されています。112 × 112の顔画像を受け取り、128個の数値、つまり埋め込み(エンベディング)を返します。同じ人物の2枚の写真からは、同じ方向を向いた埋め込みが得られます。
変換はONNX → PyTorch(onnx2torch)→ Core ML(coremltools、ML Program、float16)の順で行い、そのあと8ビットの線形重み量子化をかけました。結果は9.8 MBで、初回のロードは約0.5秒。テスト入力に対する出力は、元のONNXモデルとコサイン類似度0.998で一致します。入力の正規化はグラフ内に組み込んであるので、アプリからは0–255の素のRGBピクセルを渡すだけです。
アライメントは思った以上に重要
SFaceは固定テンプレートに位置合わせされた顔で学習されています。112 × 112の正方形の中で、目、鼻先、口角が決まった位置に来るように揃えたものです。傾いていたり中心からずれていたりする顔では、埋め込みの質が目に見えて落ちます。Visionのランドマークから5点(瞳孔、鼻領域の中心、唇の両端)を取り出し、それらをテンプレートに最もよく重ねる回転・一様スケール・平行移動をGlimtが求めます。2次元では、点を複素数として扱えば、この最小二乗問題は閉形式で解けます。
a = Σ conj(zᵢ − z̄) · (wᵢ − w̄) / Σ |zᵢ − z̄|² // rotation + scale, one complex number
w = a · z + (w̄ − a · z̄) // the transform for any point z
これをCore Imageで適用し(アフィン変換1回、ソフトウェアレンダラー、出力は112ピクセル)、切り抜いた画像をモデルに渡します。参照写真はそのままの向きと左右反転の2通りで埋め込みを計算し、その平均を取ります。コストをかけずに、少しだけ安定した参照が得られます。
同じ子どもか?閾値のキャリブレーション
アプリは各顔の埋め込みを参照写真とコサイン類似度で比較し、最も高い一致を採用します。どこに線を引くかは推測ではなく、パブリックドメインのホワイトハウスのポートレート(数年の間隔をあけて撮られた公式肖像写真と、両親と2人の娘が写った家族写真)を使って測定しました。
4年の間隔をあけて撮られた同じ男性のポートレートは0.83、家族写真から切り出した同じ人物同士は0.70〜0.85でした。他人は0.25未満に収まります。難しいのは家族です。同じ写真に写った姉妹同士は0.43、母親と娘たちは最大0.41(光とポーズが同じだと、親族はより似て見えます)。一方、別の写真の母親と娘たちを比べると0.26〜0.30まで下がります。そこで線を0.45に引きました。姉妹より上で、同一人物よりは十分に下です。5枚の写真では評価というより動作確認にすぎないので、この線は実際の家族の動画で調整していく予定です。
そっくりさんを除外するルールがあと2つあります。動画では、たまたま1フレームだけ一致しても不十分です(0.45以上のフレームが2つ、または明らかに高い0.57以上のフレームが1つ必要です)。また、1フレームに複数の顔があるときは、最もよく一致する顔だけを対象にします。
複数の参照写真
参照写真1枚は、1つの角度、1つの光、1つの年齢でしかありません。子どもはすぐに変わります。Glimtでは写真を最大5枚まで追加でき、フレーム内の顔はそのうちどれか1枚に似ていればよいので、横顔の写真や去年の冬の顔でも認識できます。複数の人が写っている写真では、正しい顔をタップして選びます。Glimtが「一番大きい顔」を勝手に推測することはありません。
1秒を選び、フレーミングする
各フレームにはスコアが付きます。見栄えが35 %、シャープさが20 %(96 × 96のグレースケール画像に対するラプラシアンの分散。ぼやけたフレームはピクセル間の変化が緩やかです)、はっきりした顔があるかどうかが45 %です。参照写真が設定されている場合は、このスコア全体が40 %、「お子さんかどうか」が60 %になります。他人が写った素敵なフレームより、お子さんが写った平凡なフレームが勝つわけです。選ばれる1秒は平均スコアが最も高い区間なので、ぼやけたフレームの隣にある1枚の良いフレームだけでは勝てません。
縦長の映像のフレーミングには、共有する価値のあるバグがありました。各フレームが最もはっきりした顔を指定し、アプリがその1秒間の位置を平均していました。子どもが2人いると、最もはっきりした顔が2人の間で入れ替わり、平均は真ん中の壁に落ちてしまいます。現在は各フレームがお子さん(またはサイズ × 品質で最も目立つ顔。こちらのほうが安定します)を指定し、アプリは平均ではなく、互いに近い位置の最大のグループを採用します。顔が小さいときは、フレームの高さの約5分の1になるまでズームします(動画では最大1.6倍)。
どのくらい速いのか
意外だったのは、顔の認識が安い部分だということです。Neural Engineで約2 msしかかかりません。動画フレームのデコードにはその15倍かかります。実際のところ、Glimtは忙しい1日分の動画を数秒で、1か月分を数分で処理します。
バックグラウンドでの実行
1年分の抜けている日を埋めるにはしばらく時間がかかるので、アプリを離れても処理を続けたいところです。iOS 26のBGContinuedProcessingTaskはまさにそのためのもので、iOSが進捗をダイナミックアイランドとロック画面に表示してくれます。端末上の小さなログから、痛い目を見て学んだことが3つあります。
- バックグラウンドではGPUが使えない。VisionとCore MLは自分でGPUを選び、アプリが前面にないと黙って失敗するので、すべてのフレームが空に見えます。GPUへのアクセスには特別なエンタイトルメントと対応ハードウェアが必要で、iPhone 15 Proは利用不可と報告しました。
- ただし、すべてにCPUフォールバックがあるわけではない。VisionをCPUに強制すると顔検出が壊れました。顔品質のネットワークが、iPhoneではCPU用の重みなしで提供されているのです("Missing weights path … nonane")。現在のGlimtはまずNeural Engine、次にCPUを要求し、それでもだめなら単純な顔の矩形にフォールバックします。
- 進捗は動かし続ける必要がある。iCloudから長い動画を取得した日に、30秒間進捗が止まり、iOSがタスクを終了させてしまいました。現在のGlimtは、フレームを1つ解析するたび、ダウンロードが1段階進むたびに進捗を報告し、解析用には写真アプリに各動画の中画質版を要求します(映像の書き出しには引き続きオリジナルを使います)。
プライバシー
Glimtにはネットワーク関連のコードがありません。顔、埋め込み、選択結果はiPhone上のアプリ専用ストレージに留まります。ネットワークに触れる可能性があるのは写真アプリ自体だけで、それも端末にない動画をあなた自身のiCloud写真から取得するときに限られます。
クレジット:SFaceはYaoyao Zhong氏ら、ONNX変換はChengrui Wang氏、OpenCV Zoo(Apache 2.0)。質問やアイデアはglimt@theill.comまでどうぞ。