UnityでOVRFaceExpressionsのデータをまるっとCSVで取得する

# はじめに
[前回の記事](https://qiita.com/empelt/items/b584d380e990f1eb1c95)でMetaQuestProのフェイストラッキングで得られるデータの説明と取得する方法を書きました。
この記事では，OVRFaceExpressionsの値をまるっと取得して，CSVに出力する方法をまとめます。今後ウィンクによる認証を実装するため，今回はウィンクをした時のOVRFaceExpressionsの値を取得します。

# ソースコードの修正
前回同様，[Unity-Movement](https://github.com/oculus-samples/Unity-Movement)のソースコードを修正していきます。

## Log.csの作成
Logという名前で空のオブジェクトを作成して，Log.csをアタッチします。

```C#:Log.cs
using System.Collections.Generic;
using UnityEngine;
using System.IO;

public class Log : MonoBehaviour {
  private static string filePath = string.Empty;

  void Start() {
    CreateDirectory();
  }

  private void CreateDirectory() {
    filePath = Application.persistentDataPath + "/Log/";

    if (!Directory.Exists(filePath)) {
      Directory.CreateDirectory(filePath);
    }
  }
    
  public static void Output(string fileName, List<string> logs) {
    var FullPath = Path.Combine(filePath, fileName);
    File.WriteAllLines(FullPath, logs);
  }
}
```

私の環境(Windows&MetaQuestPro)ではApplication.persistentDataPathは，
`PC\Quest Pro\内部共有ストレージ\Android\data\com.DefaultCompany.プロジェクト名\files\`
だったので，そこからCSVファイルを取ってくることができます。




## OVRFaceExpressionsの修正

```C#:OVRFaceExpressions.cs
// OVRFaceExpressionsの値を時系列順に配列で管理するためのMyFaceExpressionクラスを定義
public class MyFaceExpression {
  public float BrowLowererL;
  public float BrowLowererR;
  ... (以下略)

  public MyFaceExpression(
    float BrowLowererL,
    float BrowLowererR,
    ... (以下略)
  ) {
    this.BrowLowererL = BrowLowererL;
    this.BrowLowererR = BrowLowererR;
    ... (以下略)
  }
}

public class OVRFaceExpressions : MonoBehaviour, IReadOnlyCollection<float>, OVRFaceExpressions.WeightProvider {
  ...
  private GameObject LogObj;
  private Log Log;
  private List<MyFaceExpression> myFaceExpressions = new List<MyFaceExpression>();
  ...

  private void Start() {
    Log = LogObj.GetComponent<Log>();
  }

  private void Update() {
    ...
    if (ValidExpressions) {
      // Aボタンを押している間，データを取得する
      if (OVRInput.Get(OVRInput.Button.One)) {
        myFaceExpressions.Add(new MyFaceExpression(
          GetWeight(FaceExpression.BrowLowererL),
          GetWeight(FaceExpression.BrowLowererR),
          ... (以下略)
        ));
      }

    　// Bボタンを押すと，取得したデータをCSVファイルに保存する
      if (OVRInput.GetDown(OVRInput.Button.Two)) {
        var fileName = "face_" + DateTime.Now.ToString("yyyyMMddHHmmss") + ".csv";
        Save(fileName, myFaceExpressions);
      }
    }
    ...
  }

  private void Save(string fileName, List<MyFaceExpression> history) {
    Log.Output(fileName, MyFaceExpressionListToCSV(history));
  }

  private List<string> MyFaceExpressionListToCSV(List<MyFaceExpression> history) {
    List<string> str = new List<string>() {"BrowLowererL,BrowLowererR,...(以下略)"};
      for (int i = 0; i < history.Count; i++) {
        str.Add(string.Join(",", new List<string>(){
          history[i].BrowLowererL.ToString(),
          history[i].BrowLowererR.ToString(),
          ... (以下略)
        }));
      }
    return str;
  }
}  
```


## メトロノームの実装(おまけ)
後々データを利用しやすくするために，指定したタイミングでウィンクをするためのメトロノームを実装します。

```C#:OVRFaceExpressions.cs
public class OVRFaceExpressions : MonoBehaviour, IReadOnlyCollection<float>, OVRFaceExpressions.WeightProvider {
  ...
  [SerializeField] private AudioSource audioSource;
  [SerializeField] private AudioClip main;
  [SerializeField] private AudioClip sub;

  private int cnt = 0;
  private float _repeatSpan = 1.0f;
  private float _timeElapsed = 0.0f;
  ...
  void Play() {
  　// 4拍のうち最初の1拍目(main)を別の音にする
    switch (cnt) {
      case 0:
        audioSource.PlayOneShot(main);
        break;
      default:
        audioSource.PlayOneShot(sub);
        break;
    }
    cnt = (cnt + 1) % 4;
  }
  ...
  private void Update() {
    _timeElapsed += Time.deltaTime;

    if (_timeElapsed >= _repeatSpan) {
      // _repeatSpan(1秒)ごとに音を鳴らす
      Play();
      _timeElapsed = 0.0f;
    }
  }
  ...
}
```
あとはUnity側のInspectorウィンドウでAudioSourceとAudioClipをアタッチすればOKです。




# 取得したデータ

取得したデータは，OVRFaceExpressionsで定義された63個のキーに対応する各値です(意味をなしていないA列は見なかったことに，，)。

4秒間隔で**左目**でウィンクをしており，とりあえず20秒くらい取得しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/359441/d212e3e7-985c-4ca0-8736-2201ff9d9132.png)

# データ分析
ウィンクで認証をする際にどのデータが重要になるかを分析する必要があります。

とりあえず63個すべてにおいての折れ線グラフを出すと下図の様になりました。

![face.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/359441/0affd0e8-bf77-d5bc-7898-2c0c4a9fe16a.png)


わかりづらいので部位ごとに項目を分別してみます。

### 眉

![brow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/359441/17c3dc97-6370-368a-a4a5-0f2c0eae4c67.png)


### 目

![eyes1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/359441/28d8af49-3ff3-7736-7ef5-b3bb947dbaa4.png)

![eyes2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/359441/bd7d9893-fe6a-77ee-f52a-1ad0969dbbd9.png)


### 頬

![cheek.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/359441/5989fdab-56e3-e358-29f8-9f5601158629.png)

### 鼻

![nose.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/359441/c2cc4dd8-a876-0d86-35bc-4181a3d02276.png)


### 口

![mouth.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/359441/0b6706ce-2169-4263-01a4-2410594fbff7.png)

### 唇

![lip1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/359441/6656885e-4516-b980-2505-e551eeaf7327.png)

![lip2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/359441/c9d666fd-6221-1226-df8d-57804fe692f9.png)

![lip3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/359441/56494d3f-ae48-bfcb-3009-2b765a367ef2.png)


### 顎

![jaw.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/359441/81a5ab15-d805-afa3-e9f7-56176c9ec921.png)


# おわりに
これでCSVでデータを取得することができたので，次回は実際に分類器モデルを作ってみたいと思います。(データ集め大変，，)


# 参考にした記事
https://cnaan.hatenablog.com/entry/2021/12/10/070000