オンライン会議の映像から表情（Action Unit）を可視化してみた

## 今回やること

顔の写っている映像データを使って、表情の変化を可視化したい

### 課題

顔動作解析ツールであるOpenFaceで取得できるAction Unitで表情を可視化できるが、部分的な筋肉動作の特徴を一つずつ出力しているので、全体の表情変化がわかりにくいため

そこで、日立のライフ顕微鏡的な形で出力できれば、全体を通して傾向が見れるのではないかという発想に至りました。
> [日立のライフ顕微鏡の記事](https://www.hitachi-systems.com/news/2013/20130806.html)

### Action Unit (AU)とは？

顔の筋肉群の基本的な動作単位を指します。
例えば、眉を下げる、頬を上げる、唇の両端を引き上げるなどの部分的な表情に関係する動作などがあります。

OpenFaceだと17種類取れます。
> [OpenFace - Action Unit](https://github.com/TadasBaltrusaitis/OpenFace/wiki/Action-Units)

## 使用したツール

- [OpenFace](https://github.com/TadasBaltrusaitis/OpenFace)
  - 顔動作解析（視線移動、頭部位置、表情が数値として取得が可能）
  - issueを見る限り、リアルタイム処理はできなそう

### できたもの

![a-20210128_speak_group_non-interval.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/477704/38d065fe-9cbe-9cac-2804-614cce70ffde.png)

縦軸は、AUの番号と3人会話の発話状態も紐づけています。
横軸は、会議時間です。
AUの強度変化が低いとグレー、高くなると徐々に赤みが増し、最大は黒になります。
3人会話の発話状態は、色がついている部分が発話している箇所と思ってください。

## コード

（コードが冗長 & 命名がひどいので、リファクタリング頑張ります。）

```python

import pandas as pd

au_list = [" timestamp", " AU01_r", " AU02_r", " AU04_r", " AU05_r", " AU06_r",
 " AU07_r", " AU09_r", " AU10_r", " AU12_r", " AU14_r", " AU15_r",
 " AU17_r", " AU20_r", " AU23_r", " AU25_r", " AU26_r", " AU45_r"] # 先頭スペースは各自で削除してください

au_id = ["AU01", "AU02", "AU04", "AU05", "AU06",
 "AU07", "AU09", "AU10", "AU12", "AU14", "AU15",
 "AU17", "AU20", "AU23", "AU25", "AU26", "AU45", "A", "B", "C"]

# OpenFaceで出力したCSVファイルの読み込み
df_face = pd.read_csv("face.csv")

# Action Unitのデータのみ抽出
df_face_au = df_face[au_list]

# OpenFaceのtimestampからグラフ横軸の時間間隔を抽出
x_width = df_face_au.at[1, " timestamp"]

# FigureとAxesの生成
fig, ax = plt.subplots(figsize=(15, 8))
plt.rcParams["font.size"] = 24

# 目盛りの生成処理
list_timestamp = df_face[" timestamp"].values

for index_au_id in range(len(au_id)):
    range_0_list = []  # 0の時
    range_0to1_list = []  # 0より大きく1未満
    range_1to2_list = []  # 1より大きく2未満
    range_2to3_list = []  # 2より大きく3未満
    range_3to4_list = []  # 3より大きく4未満
    range_4to5_list = []  # 4より大きく5未満

    # AU特徴を配列に変換
    list_df = df_face_au.values

    for i in range(len(df_face)):
        if list_df[i] > 0.0 and list_df[i] < 1.0:
            range_0to1_list.append((list_timestamp[i], x_width))
        elif list_df[i] > 1.0 and list_df[i] < 2.0:
            range_1to2_list.append((list_timestamp[i], x_width))
        elif list_df[i] > 2.0 and list_df[i] < 3.0:
            range_2to3_list.append((list_timestamp[i], x_width))
        elif list_df[i] > 3.0 and list_df[i] < 4.0:
            range_3to4_list.append((list_timestamp[i], x_width))
        elif list_df[i] > 4.0 and list_df[i] < 5.0:
            range_4to5_list.append((list_timestamp[i], x_width))
        else:
            range_0_list.append((list_timestamp[i], x_width))
 
    # 以下の関数は後述
    make_broken_barh(ax, range_0_list, index_au_id, color[0])
    make_broken_barh(ax, range_0to1_list, index_au_id, color[1])
    make_broken_barh(ax, range_1to2_list, index_au_id, color[2])
    make_broken_barh(ax, range_2to3_list, index_au_id, color[3])
    make_broken_barh(ax, range_3to4_list, index_au_id, color[4])
    make_broken_barh(ax, range_4to5_list, index_au_id, color[5])

    ax.broken_barh(speak_list, (2, 2), facecolors="blue")
    ax.broken_barh(speak_list1, (4, 2), facecolors="purple")
    ax.broken_barh(speak_list2, (6, 2), facecolors="green")


def make_broken_barh(ax, range_list, index, color):
    ax.broken_barh(range_list, (2*index, 2), facecolors=(color))


```

broken_barhを使えば実現できます。

以上です。