← [ TECH / 技術部 ] に戻る
OBSERVATION · 其の7215 · 2026.07.24

GNM Head 導入記(2) MediaPipe 68点対応表を、自分でレンダリングして自分で検証する

GNM Head 導入記(2) MediaPipe 68点対応表を、自分でレンダリングして自分で検証する — GNM Head, MediaPipe, 68点対応表

この記事のポイント

  • MediaPipe の 468 点顔メッシュと GNM Head の既知 68 点ランドマークを繋ぐ対応表は、公式にもコミュニティにも存在しない。外部の未検証データに頼らず、GNM 自身をレンダリングした画像に MediaPipe を実際にかけて対応を導出した。
  • 導出した対応は、既知の意味を持つ点との誤差で自己検証できる。顔の中央部(目・鼻・口)は 1〜3px、輪郭は誤差が大きいという精度分布まで数字で確認した。
  • フィッティングの正則化(regularization)は「汎化のための過学習防止」ではない。今回は 1 件への最接近が目的で、正則化は数値安定化としてのみ使う、という設計判断を整理する。

前回は、Google が 2026 年 7 月に公開した実在人体 3D 統計モデル「GNM Head」を手元の Mac に導入しました。地形から検出した顔のメッシュを、実在しうる人間の顔として最も近い形に投影するための土台です。今回はその続きで、検出側の MediaPipe と GNM Head を繋ぐ「対応表」を自分で作る話をします。

結論を先に言うと、この対応表は公式にもコミュニティにも存在しませんでした。ないなら誰かの未検証データを拾ってくるのではなく、自分でレンダリングして、自分で検証して作る——それが今回の記事のいちばん伝えたいところです。

本記事はローカル LLM による自動執筆パイプラインで生成されました。現段階ではクラウド AI(Claude 等)の補助や人間の編集が介在していますが、pareido.jp では最終的に AI が自律的にコンテンツを制作できる仕組みの構築を目指しています。

対応表が存在しない、という出発点

MediaPipe Face Mesh は、顔に 468 点(虹彩を含めると 478 点)のメッシュを貼る顔検出です。一方、GNM Head が持っている既知の 68 点ランドマークは、輪郭・眉・目・鼻・口という、いわゆる dlib 系の標準的な顔 68 点に相当します。この 2 つのインデックスを繋ぐ対応表がないと、MediaPipe で検出した顔を GNM の座標系に持ち込めません

ところが、この対応表は公式ドキュメントにもコミュニティにも見当たりませんでした。google-ai-edge/mediapipe の issue #4490 でも同じ需要が挙がったまま未解決です。468 点を目視で 68 点に拾う手作業もできますが、それでは再現も検証もできない——他人がやり直したときに同じ結果になる保証がありません。

そこで発想を変えます。GNM Head 自身は、自分の 68 点ランドマークが 3D メッシュのどの頂点かを知っています(自分のモデルの一部だから当然です)。ならば GNM の平均顔を一度画像としてレンダリングし、その同じ画像に MediaPipe をかければ、両者を同じ画像平面の上で突き合わせられます。参照するのは GNM 本体と MediaPipe 本体だけ。第三者の対応表は一切要りません。

GNM をレンダリングして、MediaPipe にかける

手順そのものは単純です。順に並べると次のようになります。

  1. GNM の平均顔(neutral)を 512×512px の正面画像としてレンダリングする
  2. GNM の既知 68 点の 3D 座標を、同じカメラで画像平面に投影して 2D 化する
  3. その同じ画像に MediaPipe をかけ、468 点の検出結果を得る
  4. 投影した 68 点それぞれについて、最も近い MediaPipe 検出点を最近傍で対応づける

結果、68 点すべてに MediaPipe 側のインデックスが対応づきました(n_matched = 68)。GNM のランドマーク番号 → MediaPipe の点番号、という辞書ができあがります。

大事なのは、この工程で使ったのは GNM 本体と MediaPipe 本体だけという点です。同じ画像を自分で用意できるので、誰でも同じ手順で再現でき、次に述べるように誤差で検証もできます。外部の未検証情報に一度も頼っていないことが、この手法の価値です。

誤差で検証する

対応が正しいかは、意味が確定している点と照合すれば検証できます。MediaPipe の点番号のうち、目尻は 33(右)/263(左)、口角は 61(右)/291(左)というように、位置の意味が決まっているものがあります。投影した GNM 点が、その意味どおりの MediaPipe 点にどれだけ近づいたか——最近傍距離(512px 画像上のピクセル)がそのまま精度になります。

代表的な点を、GNM ランドマーク番号(dlib 準拠の並び)ごとに抜き出すと次のとおりです。

GNM ランドマーク 部位 最近傍距離(px) 判定
54 / 60 / 61 口(口角付近) 0.09 / 0.17 / 0.36 ○ ほぼ完全一致
40 / 45 0.51 / 0.40 ○ 実用精度
27 / 30 鼻筋 1.97 / 2.31 ○ 実用精度
12 / 6 輪郭(頬〜顎) 6.16 / 6.65 △ 参考精度
3 / 0 輪郭(耳の近く) 7.54 / 9.76 × 誤差大

顔の中央部——目・鼻・口は 1px を切る点も多く、おおむね 1〜3px に収まります。一方で、輪郭(フェイスライン)や耳の近くは 6〜10px と大きめです。これは MediaPipe の輪郭点が中央部より疎で、GNM の輪郭頂点と一対一に噛み合いにくいためです。数字が悪い箇所を隠さずに言えば、この対応表は顔の内側は実用精度、輪郭は参考精度という性格を持ちます。フィッティングで重みを付けるなら、中央部を信頼するのが妥当です。

フィッティングと正則化の設計判断

対応表ができたら、MediaPipe で検出した顔を GNM の identity 空間(253 次元、線形) にフィットさせます。工程は 2 段で、まず Umeyama(Kabsch)アラインメントで座標系(回転・並進・スケール)を最小二乗で合わせ、次に線形最小二乗で identity 係数を解きます。計算自体は軽く、CPU で数百 ms、GPU は要りません。

ここで 正則化(regularization)をどう入れるかが、誤解しやすい設計判断です。一般的な機械学習では、正則化は「訓練データに過剰適合せず、未知データに汎化させる」ために入れます。ですが今回の目的はその逆で、未知データへの汎化は要りません。「目の前のこの MediaPipe 検出 1 件に、GNM を最も近づけること」そのものが目的だからです。ではなぜ正則化が要るのか——無正則化(reg=0)だと線形系が数値的に破綻するのを防ぐためです。実測では係数ノルムが 2949 まで暴走しました。つまり正則化は汎化のためではなく、純粋な数値安定化としてのみ使います。

強さを変えたときの実測を並べます(対象は秋吉台のサンプル、係数ノルムと目標説明率)。

正則化 reg 係数ノルム 目標説明率 所見
0 2949(暴走) 数値破綻
1e-4 2.9 16.7% ほぼ平均顔、実質ノイズしか説明できない
1e-6 22 48.6% 個性が明確、破綻なし
1e-7 52 57.4% 表面にやや歪みが出るが個性が強い

この記事の時点では、個性を最大限拾う方向で 1e-7 を既定値にしました。ただし表面の歪みと個性のトレードオフは、実際に生成まで通してみないと最終判断ができません。ここは次回に持ち越す論点として残しておきます。

そのまま使える対応表

導出した対応表を、このまま保存すれば使える形で以下に置きます。WordPress の記事にファイルを添付する仕組みがないため、本文にコードブロックとして丸ごと埋め込むのが、再利用可能にする唯一の手段です。gnm_landmark_to_mediapipe_index が GNM の 68 点番号 → MediaPipe 点番号の辞書、distances_px が上で使った検証時の最近傍距離です。

{
  "gnm_landmark_to_mediapipe_index": {
    "0": 127, "1": 93, "2": 149, "3": 136, "4": 172, "5": 58, "6": 132,
    "7": 176, "8": 175, "9": 396, "10": 378, "11": 379, "12": 365,
    "13": 367, "14": 435, "15": 323, "16": 454, "17": 46, "18": 53,
    "19": 66, "20": 107, "21": 55, "22": 285, "23": 336, "24": 296,
    "25": 283, "26": 276, "27": 168, "28": 197, "29": 195, "30": 4,
    "31": 98, "32": 60, "33": 141, "34": 328, "35": 327, "36": 246,
    "37": 470, "38": 158, "39": 155, "40": 153, "41": 144, "42": 341,
    "43": 385, "44": 387, "45": 249, "46": 373, "47": 380, "48": 61,
    "49": 40, "50": 37, "51": 0, "52": 267, "53": 269, "54": 308,
    "55": 321, "56": 314, "57": 17, "58": 181, "59": 91, "60": 78,
    "61": 73, "62": 11, "63": 311, "64": 325, "65": 311, "66": 15,
    "67": 179
  },
  "distances_px": {
    "0": 9.758216914097375, "1": 3.3744229079198096, "2": 3.0164053468513297,
    "3": 7.542381279362868, "4": 4.221505106146105, "5": 3.655552889445025,
    "6": 6.647759365732364, "7": 2.4560499909812967, "8": 3.0306153562459306,
    "9": 5.118439006280587, "10": 3.3894816094901112, "11": 3.3660272013323627,
    "12": 6.155540534425908, "13": 4.6121428772462405, "14": 4.795362898522175,
    "15": 4.234979004605431, "16": 5.579294198715337, "17": 2.1787744034197023,
    "18": 4.897138244222928, "19": 3.045586086266193, "20": 5.016322176994156,
    "21": 5.212691559640672, "22": 4.569783920941294, "23": 6.703611475805139,
    "24": 5.27075149011431, "25": 3.937351726988231, "26": 2.3194962313624226,
    "27": 1.9734960202019385, "28": 2.9919669017698185, "29": 2.5888335560071454,
    "30": 2.3108893138796667, "31": 4.00365324314247, "32": 2.9540472156869275,
    "33": 1.2793959886798691, "34": 1.0900790865629106, "35": 0.745988641195553,
    "36": 0.8958041078964639, "37": 3.8144303599253027, "38": 3.2717082825587243,
    "39": 0.829686652883988, "40": 0.5094540994243197, "41": 0.9367538786187886,
    "42": 1.3784791245037142, "43": 1.4681840279173015, "44": 3.516698201140613,
    "45": 0.3954155087197065, "46": 0.9349019170827038, "47": 0.9054848698992617,
    "48": 0.7886289567225818, "49": 2.904181680987455, "50": 5.4701877590301615,
    "51": 4.540060021670443, "52": 2.9115083467275786, "53": 1.1435139043687794,
    "54": 0.08636990049906992, "55": 3.029539772597233, "56": 3.8592813585191426,
    "57": 1.9439659430019383, "58": 2.243609074329981, "59": 0.36538643272601673,
    "60": 0.1678042188169263, "61": 0.3602882319265298, "62": 2.3197954887067596,
    "63": 3.0824575765418136, "64": 0.5723690787338886, "65": 1.8887645941286613,
    "66": 1.621502129694872, "67": 1.508887205974976
  },
  "render_size": 512,
  "n_matched": 68,
  "n_total_gnm_landmarks": 68
}

まとめ / 次回へ

存在しない対応表は、外部に頼らず自分でレンダリングして自分で検証すれば作れる、というのが今回の結論です。誤差は顔の中央部で 1〜3px、輪郭は参考精度。フィッティングの正則化は汎化のためではなく数値安定化として理解するのが正しく、現時点の既定値は 1e-7 に置きました。

次回は、この対応表とフィッティング結果を SDXL の img2img + ControlNet に繋いで、地形由来の顔を実際に生成するところまで進めます。その過程で踏んだ 3 件のバグ(見上げる顔・透けて見える目と歯・上を向く視線)の切り分けと修正も、あわせて記録する予定です。

▶ 関連動画 · YOUTUBE
━━ 観るのを再開 ━━
前の記事を読む
技術部 · 【スーパーマリオ256W】世界57を歩く — 疑似ループから真のループへ、クリアできないワールド
動画を観る
YouTube
次の記事を読む
技術部 · GNM Head 導入記 ── 地形の骨格を、実在しうる顔として結ぶ(3)|GNMフィット顔をSDXLでアニメアバター化する ── img2img+ControlNetと、上を向く顔を巡る3つのバグ
━━ 他の観測領域 ━━
TECH · 技術部
GNM Head 導入記 ── 地形の骨格を、実在しうる顔として結ぶ(3)|GNMフィット顔をSDXLでアニメアバター化する ── img2img+ControlNetと、上を向く顔を巡る3つのバグ
PHIL · 思想部
AI自動化ピラミッドの可視化(9)|稼働率は下がり続けていた――月次レビュー第2回
FRONT · 辺境部
【スーパーマリオ256W】世界57を歩く — 疑似ループから真のループへ、クリアできないワールド