Face Recoボックスの中身を解説してみる

# Face Reco.
Face Reco.ボックスを触っていてわかったことをまとめた。

# 環境

* Choregraphe v2.3.1

## Face Reco.ボックスの概要
Face Reco.ボックスは、Learn Faceボックスで学習した顔を認識して、認識した顔にひも付けられた名前を出力するボックス。ボックスの中身は以下のようになっている。

![face_reco.png](https://qiita-image-store.s3.amazonaws.com/0/77079/3fc98204-9838-3f78-c161-3ea45537264f.png)


Face Reco.ボックスの中は一つのReco. Det. Facesボックスから構成されている。このボックスが認識結果を受け取り、名前を出力する。Reco. Det. Facesボックスの入力は、Face Detectedイベントから出力される情報で、出力は認識した顔にひも付けられた名前となっている。以下のPythonスクリプトがReco. Det. Facesボックスの中身である。

```Python

class MyClass(GeneratedClass):
    def __init__(self):
        GeneratedClass.__init__(self, False)

    def onLoad(self):
        self.timeFilteredResult = [];

    def onUnload(self):
        #puts code for box cleanup here
        pass

    def onInput_onStart(self, p):
        if(len(p) > 0):
            if(len(p[1]) > 0): # just in case of the ALValue is in the wrong format
                # get the ALValue returned by the time filtered recognition:
                #    - [] when nothing new.
                #    - [4] when a face has been detected but not recognized during the first 8s.
                #    - [2, [faceName]] when one face has been recognized.
                #    - [3, [faceName1, faceName2, ...]] when several faces have been recognized.
                self.timeFilteredResult = p[1][len(p[1]) -1]
                if( len(self.timeFilteredResult) == 1 ):
                    # If a face has been detected for more than 8s but not recognized
                    if(self.timeFilteredResult[0] == 4):
                        self.onDetectWithoutReco()
                elif( len(self.timeFilteredResult) == 2 ):
                    # If one or several faces have been recognized
                    if(self.timeFilteredResult[0] in [2, 3]):
                        for s in self.timeFilteredResult[1]:
                            self.onRecognizedFace( s )

    def onInput_onStop(self):
        pass
        
```
Face Detectedイベントからの出力を受け取って、その内容に基づいて処理を行っている。
Face Recoボックスが何をしているかを理解するには、どうやらFaceDetectedイベントから受け取る情報を理解しないとダメらしい。ということで、FaceDetectedイベントについて調べてみた。

# FaceDetectedイベント
## FaceDetectedイベントの概要
FaceDetectedイベントはPepperが人の顔を検知すると発生するイベント。Pepperが人の顔を検知している間は、イベントが発生し続ける。また、単に顔を検知するだけでなく、目、鼻、口の場所も検知している。
FaceDetectedイベントが発生すると、検知した顔に関連する情報が出力される。以下で、そのデータ構造と内容を見ていく。

## データ構造
以下のFaceDetectedという名前のついたリストが、Pepperが人の顔を検知した際に出力される情報。FaceDetectedはその中にTimeStamp, FaceInfoなどの様々な情報が入れ子になって格納されている。この中のTime_Filtered_Reco_Infoの中身をもとに、Reco. Det. Facesボックスの出力が決まる。FaceInfoのNは検知した顔の数だと思われる。

```py
FaceDetected =
[
  TimeStamp,
  [ FaceInfo[N], Time_Filtered_Reco_Info ],
  CameraPose_InTorsoFrame,
  CameraPose_InRobotFrame,
  Camera_Id
]
```

### TimeStamp
まずは、TimeStamp。ここには、顔検知が行われた際に使用された画像のタイムスタンプに関する情報が格納されている。このタイムスタンプはUNIX時間で表現されている。

```py
TimeStamp =
[
  TimeStamp_Seconds,
  Timestamp_Microseconds
]
```


### FaceInfo
次に、FaceInfo。これはリスト構造であり、その中には検知した顔それぞれについてShapeInfoとExtraInfo[N]が含まれている。ShapeInfoは顔の形に関する情報、ExtraInfoはShapeInfoに格納されている以外の顔の形に関する情報が格納されている。

```py
FaceInfo =
[
  ShapeInfo,
  ExtraInfo[N]
]
```

**ShapeInfo**
顔の形に関する情報が格納されている。
alphaとbetaは、カメラアングルから見た顔の位置を表している。
sizeXとsizeYは、カメラアングルから見た顔の大きさを表している。

```py
ShapeInfo =
[
  0,
  alpha,
  beta,
  sizeX,
  sizeY
]
```

**ExtraInfo**
ExtraInfoにはShapeInfoに格納されている以外の顔の情報が格納されている。具体的には以下の情報が格納されている。

* faceID（顔ID）
* scoreReco（認識スコア）
* faceLabel（顔ラベル）
* leftEyePointsとrightEyePoints（目の位置）
* nosePoints（鼻の位置）
* mouthPoints（口の位置）

```py
ExtraInfo =
[
  faceID,
  scoreReco,
  faceLabel,
  leftEyePoints,
  rightEyePoints,
  unused, # 後方互換性のため
  unused,
  nosePoints,
  mouthPoints
]
```

認識スコアは認識過程から返されるスコアで、高ければ高いほど良い。
顔ラベルは、認識された顔の名前を表していて、この名前は学習時につけたものと同じ。
目、鼻、口の位置はカメラアングルによって与えられる。
ちなみに、unusedは以下のような要素数6で全要素が0のリストであった。

```py
unused = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
```


### EyePoints
```py
EyePoints =
[
  eyeCenter_x,
  eyeCenter_y,
  noseSideLimit_x,
  noseSideLimit_y,
  earSideLimit_x,
  earSideLimit_y,
  0.0, # 後方互換性のため
  0.0,
  0.0,
  0.0,
  0.0,
  0.0,
  0.0,
  0.0
]
```

### NosePoints
```py
NosePoints =
[
  bottomCenterLimit_x,
  bottomCenterLimit_y,
  bottomLeftLimit_x,
  bottomLeftLimit_y,
  bottomRightLimit_x,
  bottomRightLimit_y
]
```

### MouthPoints
```py
MouthPoints =
[
  leftLimit_x,
  leftLimit_y,
  rightLimit_x,
  rightLimit_y,
  topLimit_x,
  topLimit_y,
  0.0,
  0.0,
  0.0,
  0.0,
  0.0,
  0.0,
  0.0,
  0.0,
  0.0,
  0.0
]
```

### Time_Filtered_Reco_Info
Time_Filtered_Reco_InfoはReco. Det. Facesボックスの出力を決めるのに重要な情報が格納されていて、以下の４つの場合がありえる：
1. []
2. [ 2, [ faceLabel ] ]
3. [ 3, [ faceLabel0, ..., faceLabelP ] ]
4. [ 4 ]

1は新しいものがない場合？これはよくわからなかった。
2は顔が一つだけ認識された場合。
3は複数の顔が認識された場合。
4は顔が認識されずに8秒以上が経過した場合。4の状態になった場合には、Learn Faceボックスを用いて、顔を学習させるとよい。

### CameraPose_InTorsoFrame
Frame_Torsoにおける、画像が撮られた時点での、カメラのPosition6Dを表している。

### CameraPose_InRobotFrame
Frame_Robotにおける、画像が撮られた時点での、カメラのPosition6Dを表している。

### Camera_Id
Camera_Idは検知に使われたカメラのIDを表している。Pepperに2つあるカメラのうち、額にあるカメラで検知した場合は0、口にあるカメラで検知した場合は1が返される。

## コード解説
以上をふまえて、Reco. Det. Facesボックスの中身を見てみる。以下は必要な箇所だけ抜粋している。

```py

class MyClass(GeneratedClass):

    ...

    def onLoad(self):
        self.timeFilteredResult = [];

    def onInput_onStart(self, p):
        if(len(p) > 0):
            if(len(p[1]) > 0): # just in case of the ALValue is in the wrong format
                # get the ALValue returned by the time filtered recognition:
                #    - [] when nothing new.
                #    - [4] when a face has been detected but not recognized during the first 8s.
                #    - [2, [faceName]] when one face has been recognized.
                #    - [3, [faceName1, faceName2, ...]] when several faces have been recognized.
                self.timeFilteredResult = p[1][len(p[1]) -1]
                if( len(self.timeFilteredResult) == 1 ):
                    # If a face has been detected for more than 8s but not recognized
                    if(self.timeFilteredResult[0] == 4):
                        self.onDetectWithoutReco()
                elif( len(self.timeFilteredResult) == 2 ):
                    # If one or several faces have been recognized
                    if(self.timeFilteredResult[0] in [2, 3]):
                        for s in self.timeFilteredResult[1]:
                            self.onRecognizedFace( s )
    ...
        
```

主な処理はonInput_onStartメソッドの中で行われている。人の顔を検知していないときはFaceDetectedは
空リスト[]を返すので、最初の条件式len(p) > 0は、人の顔を検知できているか否かを判断していることになる。

2番目の条件式len(p[1]) > 0はALValueの形式が間違っているときに偽となり、以降の処理を行わないようだ。どうすれば偽にできるかはわかっていない。

3番目の条件式len(self.timeFilteredResult) == 1と4番目の条件式self.timeFilteredResult[0] == 4は人の顔を検知したものの8秒以上認識されなかった場合の判定を行っている。

4番目以降の条件式は、人の顔が検知され場合のことが書かれていて、検知した顔の数ぶん、onRecognizedFaceから顔ラベル（名前）の出力を行う。

# まとめ
Face Recoボックスの中身について解説してみた。
Face Recoボックス内のReco. Det.Facesボックスから名前が出力されることがわかった。ただ、8秒以上顔を検知しているものの認識されなかった場合にonDetectWithoutRecoから出力が行われるが、これはどこにも結線されていない。そのためこのような場合は、検知している顔を知らない顔とみなし、Learn Faceボックスで顔の学習を行うようにすれば、自然な流れで顔認識と学習を行えそうだ。

今回できなかったこととしては、カメラから見た顔の大きさや位置の解釈がよくわからなかったことが挙げられる。

# 参考資料
* NAOドキュメント 2.3.1