MediaPipeの顔骨格追跡の仕様を調べました

This image is generated with ChatGPT-4, and edited by the author.
作成日:2023年07月05日(水) 00:00
最終更新日:2024年10月06日(日) 20:37
カテゴリ:コンピュータビジョン
タグ:  MediaPipe 動作解析 骨格追跡

毎回同じようなことを調べては忘れているので,MediaPipeの顔骨格追跡について調べたことをまとめました.

こんにちは.高山です.
MediaPipeというライブラリを聞いたことがありますか?
Googleが出しているライブラリで,機械学習ベースの画像処理や自然言語処理が簡単に利用できます.

MediaPipeに含まれている機能の一つに,顔の骨格追跡があります (他の部位の追跡と異なり,追跡点は人体上の骨格ではないのですが,便宜上こう呼びます).
追跡点ベースの手話認識をしていると顔の追跡点を用いて認識を行う場合があります.
MediaPipeの公式ドキュメントにはあまり細かい仕様が記載されておらず,毎回同じようなことを調べては忘れてしまっていたので,ここに記載しておくことにしました.

更新履歴 (大きな変更のみ記載しています)

  • 2024/09/17: タイトルとタグを更新しました

1. MediaPipeの顔骨格追跡概要

図1に示すように,顔の骨格追跡機能は入力画像から478点の顔の追跡点を抽出します (うち8点は瞳).

MediaPipeの顔追跡処理を説明する図です.入力画像に対して処理を施すと顔の追跡点座標が得られる様子が描かれています.
MediaPipeの顔骨格追跡

各追跡点は \((x, y, z)\) の3次元座標値を保持しています (追跡点クラスにはvisibilityとpresenceというパラメータもありますが,顔骨格追跡の場合は0固定されています).

xとyは,それぞれ水平方向と鉛直方向の座標です.
値は画像幅と画像高さで正規化されており,\((x, y) = (0, 0), (x, y) = (1, 1)\) は,それぞれ画像の左上と右下を示します.

zは奥行きを示しています.
z値に関する原点や正規化基準については,2023/07/05時点の公式ドキュメントでは確認できませんでした(過去のドキュメントには記載されていた気がするのですが...).

2. 追跡点の配置

478点の追跡点は配列形式で得られます.
各追跡点が顔のどこに対応するかは2023/07/05時点の公式ドキュメントには記載がないですが,Githubのレポジトリを探すと描画処理用の 画像コードが見つかります.
これらの情報を基に配置を調べたところ,下記の図2のようになっているようです (画像をクリックすると拡大画像が別タブで開けます).

顔追跡点の配置を番号付きで描いた図です.
(a): 全体
顔追跡点のうち,眼,眉,鼻,口,および輪郭を番号付きで描いた図です.
(b): 主要部
顔追跡点の配置

3. 追跡点の定義一覧 (Python向け)

最後に,プログラムで利用するための定義を下記に記します.
ここではPython向けに書いてますが,他のプログラミング言語でも同じように定義できます.

3.1 主要部の追跡点配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 唇
FACEPT_LIPS = [0, 13, 14, 17, 37, 39, 40, 61, 78, 80,
               81, 82, 84, 87, 88, 91, 95, 146, 178, 181,
               185, 191, 267, 269, 270, 291, 308, 310, 311, 312,
               314, 317, 318, 321, 324, 375, 402, 405, 409, 415]

# 左眼
FACEPT_LEYE = [249, 263, 362, 373, 374, 380, 381, 382, 384, 385,
               386, 387, 388, 390, 398, 466]

# 左眉
FACEPT_LEYEBROW = [276, 282, 283, 285, 293, 295, 296, 300, 334, 336]

# 左の瞳
FACEPT_LIRIS = [474, 475, 476, 477]

# 右眼
FACEPT_REYE = [7, 33, 133, 144, 145, 153, 154, 155, 157, 158,
               159, 160, 161, 163, 173, 246]

# 右眉
FACEPT_REYEBROW = [46, 52, 53, 55, 63, 65, 66, 70, 105, 107]

# 右の瞳
FACEPT_RIRIS = [469, 470, 471, 472]

# 鼻
FACEPT_NOSE = [1, 2, 4, 5, 6, 19, 45, 48, 64, 94,
               97, 98, 115, 168, 195, 197, 220, 275, 278, 294,
               326, 327, 344, 440]

# 顔の外縁
FACEPT_OVAL = [10, 21, 54, 58, 67, 93, 103, 109, 127, 132,
               136, 148, 149, 150, 152, 162, 172, 176, 234, 251,
               284, 288, 297, 323, 332, 338, 356, 361, 365, 377,
               378, 379, 389, 397, 400, 454]

なお,MediaPipeの描画用定義を使うと下記のようにも書けます.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
from mediapipe.python.solutions.face_mesh_connections import (
    FACEMESH_LIPS,
    FACEMESH_LEFT_EYE,
    FACEMESH_LEFT_EYEBROW,
    FACEMESH_LEFT_IRIS,
    FACEMESH_RIGHT_EYE,
    FACEMESH_RIGHT_IRIS,
    FACEMESH_RIGHT_EYEBROW,
    FACEMESH_FACE_OVAL,
    FACEMESH_NOSE)

FACEPT_LIPS = np.unique(np.array(list(FACEMESH_LIPS)).flatten())
FACEPT_LEFT_EYE = np.unique(np.array(list(FACEMESH_LEFT_EYE)).flatten())
FACEPT_LEFT_EYEBROW = np.unique(np.array(list(FACEMESH_LEFT_EYEBROW)).flatten())
FACEPT_LEFT_IRIS = np.unique(np.array(list(FACEMESH_LEFT_IRIS)).flatten())
FACEPT_RIGHT_EYE = np.unique(np.array(list(FACEMESH_RIGHT_EYE)).flatten())
FACEPT_RIGHT_EYEBROW = np.unique(np.array(list(FACEMESH_RIGHT_EYEBROW)).flatten())
FACEPT_RIGHT_IRIS = np.unique(np.array(list(FACEMESH_RIGHT_IRIS)).flatten())
FACEPT_OVAL = np.unique(np.array(list(FACEMESH_FACE_OVAL)).flatten())
FACEPT_NOSE = np.unique(np.array(list(FACEMESH_NOSE)).flatten())

3.2 左右の追跡点ペア

顔の左右で対称関係になっている追跡点の組を下記に記します. 反転処理などを行う場合に利用できます.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
SWAPPAIR_LIPS = np.array([
    # Upper line in outline.
    [37, 267], [39, 269], [40, 270], [185, 409],
    # Both side in outline.
    [61, 291],
    # Lower line in outline.
    [84, 314], [181, 405], [91, 321], [146, 375],
    # Upper line in inline.
    [82, 312], [81, 311], [80, 310], [191, 415],
    # Both side in inline.
    [78, 308],
    # Lower line in inline.
    [87, 317], [178, 402], [88, 318], [95, 324]])

SWAPPAIR_NOSE = np.array([
    [45, 275], [220, 440], [115, 344], [48, 278], [64, 294],
    [98, 327], [97, 326]])

SWAPPAIR_EYEBROW = np.array([
    # Upper line.
    [107, 336], [66, 296], [105, 334], [63, 293], [70, 300],
    # Lower line.
    [55, 285], [65, 295], [52, 282], [53, 283], [46, 276]])

SWAPPAIR_EYE = np.array([
    # Upper line.
    [173, 398], [157, 384], [158, 385], [159, 386], [160, 387],
    [161, 388], [246, 466],
    # Both corner.
    [133, 362], [33, 263],
    # Lower line.
    [155, 382], [154, 381], [153, 380], [145, 374], [144, 373],
    [163, 390], [7, 249]])

SWAPPAIR_OVAL = np.array([
    # From upper to lower.
    [109, 338], [67, 297], [103, 332], [54, 284], [21, 251],
    [162, 389], [127, 356], [234, 454], [93, 323], [132, 361],
    [58, 288], [172, 397], [136, 365], [150, 379], [149, 378],
    [176, 400], [148, 377]])

SWAPPAIR_OTHERS = np.array([
    # 2nd line.
    [108, 337], [69, 299], [104, 333], [68, 298], [71, 301],
    [139, 368], [34, 264], [227, 447], [137, 366], [177, 401],
    [215, 435], [138, 367], [135, 364], [169, 394], [170, 395],
    [140, 369], [171, 396],
    # 3rd line excepting eyebrow.
    [156, 383], [143, 372], [116, 345], [123, 352], [147, 376],
    [213, 433], [192, 416], [214, 434], [210, 430], [211, 431],
    [32, 262], [208, 428],
    # 4th line excepting eybrow (temple -> cheek -> sulcus mentolabialis).
    [124, 353], [35, 265], [111, 340], [117, 346], [50, 280],
    [187, 411], [207, 427], [216, 436], [212, 432], [202, 422],
    [204, 424], [194, 418], [201, 421],
    # 5th line (radix nasi -> regio orbitalis -> temple -> cheek -> sulcus mentolabialis).
    [193, 417], [189, 413], [221, 441], [222, 442], [223, 444],
    [224, 445], [225, 342], [113, 342], [226, 446], [31, 261],
    [228, 448], [118, 347], [101, 330], [205, 425], [206, 426],
    [92, 322], [186, 410], [57, 287], [43, 273], [106, 335],
    [182, 406], [83, 313],
    # 6th line (radix nasi -> regio orbitalis -> temple -> cheek -> philtrum).
    [190, 414], [56, 286], [28, 258], [27, 257], [29, 259],
    [30, 460], [247, 467], [130, 359], [25, 255], [110, 339],
    [229, 449], [119, 348], [100, 329], [36, 266], [203, 423],
    [165, 391], [167, 393],
    # 7th line (inner lines of cheeks and nose).
    [122, 351], [245, 465], [244, 464], [243, 463], [112, 341],
    [26, 256], [22, 252], [23, 253], [24, 254], [230, 450],
    [120, 349], [47, 277], [126, 355], [142, 371], [129, 358],
    [98, 327], [97, 326],
    # 8th line (inner lines of cheeks).
    [233, 453], [232, 452], [231, 451], [121, 350], [128, 357],
    [114, 343], [188, 412],
    # 9th line (inner lines of nose).
    [196, 419], [174, 399], [217, 437], [198, 420], [209, 429],
    [49, 279], [102, 331], [64, 294], [240, 460], [99, 328],
    # 10th line (inner lines of nose).
    [3, 248], [236, 456], [198, 420], [131, 360], [48, 278],
    [219, 439], [235, 455], [75, 305], [60, 290],
    # 11th line (inner lines of nose).
    [51, 281], [134, 363],
    # 12th line (inner lines of nose).
    [45, 275], [220, 440], [115, 344],
    # 13th line (inner lines of nose).
    [44, 274], [237, 457], [218, 438], [160, 392], [59, 289],
    # 14th line (inner lines of nose).
    [239, 459], [79, 309],
    # 15th line (inner lines of nose).
    [125, 354], [241, 461], [238, 458], [20, 250], [242, 462],
    [141, 370],
    # 16th line (inner lines of lips).
    [72, 302], [73, 303], [74, 304], [184, 408], [76, 306],
    [77, 307], [90, 320], [180, 404], [85, 315],
    # 17 th line (inner lines of lips).
    [38, 268], [41, 271], [42, 272], [183, 407], [62, 292],
    [96, 325], [89, 319], [179, 403], [86, 316]])

SWAPPAIR_ALL = np.concatenate([
    SWAPPAIR_LIPS,
    SWAPPAIR_NOSE,
    SWAPPAIR_EYEBROW,
    SWAPPAIR_EYE,
    SWAPPAIR_OVAL,
    SWAPPAIR_OTHERS])

反転処理の例を下記に記します.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# detectorはMediaPipeの顔骨格追跡器のインスタンスです.
# imageはnumpy配列の画像です.
result = detector.detect(image)

# 最初に検出された人物の追跡点
face_landmarks = result.face_landmarks[0]

# インデックス入れ替え用のマッピング辞書
swap_mapping0 = {pair[0]: pair[1] for pair in SWAPPAIR_ALL}
swap_mapping1 = {pair[1]: pair[0] for pair in SWAPPAIR_ALL}
swap_mapping = {}
swap_mapping.update(swap_mapping0)
swap_mapping.update(swap_mapping1)

# インデックスの入れ替え
import copy
swap_face_landmarks = copy.deepcopy(face_landmarks)
temp = []
for i in range(len(swap_face_landmarks)):
    if i in swap_mapping:
        temp.append(swap_face_landmarks[swap_mapping[i]])
    else:
        temp.append(swap_face_landmarks[i])

# 座標の反転
for landmark in temp:
    landmark.x = 1 - landmark.x
swap_face_landmarks = temp

今回はMediaPipeの顔骨格追跡点の配置について解説しましたが,如何でしたでしょうか?
MediaPipeの顔骨格追跡機能は,他の手法に比べて得られる追跡点の数が多く様々な応用が考えられますが,数が多い部分追跡点の配置を把握するのに苦労します.

今回紹介した話が,MediaPipeの顔骨格追跡機能を利用しようとお考えの方に何か参考になれば幸いです.