MATLAB AppDesignerで学ぶ顔認知と顔認識

#1.本記事の内容

Webカメラで撮影した画像から　顔・目・口の位置を特定することで
**リアルタイムにサッチャー錯視を作って遊べるアプリ**を作りました。
サッチャー錯視についてかんたんな解説とアプリの作り方についてご紹介します！

本記事のコンテンツをまとめると・・・

- **認知心理学における顔認知について**

 - 人間の脳は顔に対して特別な処理”顔への全体認知処理”を使っていることが推測される
 - そのため倒立した人間の顔と正立した人間の顔で認知の仕方が異なる
 - そのようなメカニズムを示唆する錯視の一つがサッチャー錯視である
 
- **ソフトウェアによる顔認識について**
 - カメラ画像取り込みは[Matlab Support Package for USB Webcams](https://jp.mathworks.com/help/supportpkg/usbwebcams/index.html?s_tid=CRUX_lftnav)を使えばできる
 - ComputerVisionToolboxの[vision.CascadeObjectDetector](https://jp.mathworks.com/help/vision/ref/vision.cascadeobjectdetector-system-object.html)を使えば学習済みの検出器が利用でき、顔・目・鼻・口などの位置を画像から認識できる
 - [vision.PointTracker](https://jp.mathworks.com/help/vision/ref/vision.pointtracker-system-object.html)を使うと画像中のオブジェクトの位置を追跡できるため、一度画像認識でオブジェクトの位置を特定したら毎フレーム位置検出をする必要がない

AppDesignerでGUIアプリを作る方法についてはこちらをご参照ください
[MATLABで簡単にGUIを作ろう！](https://qiita.com/griffin921/items/d761abc656542f62a611)


##1.1今回つくったもの
デモ動画はこちら。
[![IMAGE ALT TEXT HERE](http://img.youtube.com/vi/vpv9xACPGLs/0.jpg)](http://www.youtube.com/watch?v=vpv9xACPGLs)

githubリポジトリはこちら。

https://github.com/griffin921/ThatcherIllusionDemo



まぁ、キモいっていう感想と、なにこれ？って感想と、もしくはその両方を持たれたかなと思います。
これ見ておーーー！と思った人は過去に認知心理学をかじったことがある人ですね！

#2.サッチャー錯視って？
視覚における認知心理学の顔認知という分野で有名な錯視です。

##2.1まずは見てみよう
####サッチャー錯視の作り方
・人間の顔が写った写真を用意します。（実物でも画像データでもOK）
・人間の顔の目の部分、口の部分だけを四角く切り取り、上下反転させて元の位置に貼り付けます。

####錯視の効果
・作成したサッチャー錯視画像を上下反転させて眺めると、あまり違和感がありません。
・しかし、正しい向きに戻すと、反転している口と目に対して強い違和感を感じるようになります。
####Wikipediaの例
Wikipediaにサンプル画像がのっていました。
上側２つは正常な写真です。
下側２つはサッチャー錯視です。目と口だけ上下反転しています。
下側２つは同じ画像のはずなのに右側のほうが画像から感じる違和感が強くありませんか？
<img width="400" alt="hatcher.PNG" src="https://upload.wikimedia.org/wikipedia/commons/2/2c/Thatcher.PNG">

##2.1なんでこんなふうに見えるんだろう
ゼミで輪講した教科書を引っ張り出しまして調べました。

顔の認知は他の物体に比べて全体認知処理（目・鼻・口などのパーツが全体にどのように配置されているか）が重要だと考えられているそうです。

例えばAさん、Bさん２人の顔写真があったとして、　その写真の一部（鼻だけとか）を提示してAさんかBさんかを当てる場合と、顔全体を提示したときにAさんかBさんかを当てる場合では後者のほうが正答率が高かったそうです。
まぁこれは情報量が多いからだろうと思うのですが、不思議なことに統制実験として　
たとえばホイールだけが異なり、ほかは全く同じ２枚の自動車の画像を用いて、
一部（ホイール）と全体での正答率を求めると一部と全体で差はなかったそうです([Tanaka & Farah,1993](https://www.researchgate.net/publication/14881937_Parts_and_Wholes_in_Face_Recognition))

これは、例えば家や車といったありとあらゆるオブジェクトに対して人間の脳は顔を特別に扱っていることが示唆されます。

サッチャー錯視が起こる理由として、顔を倒立させると、定形の顔形状と異なることで"顔の全体認知処理が使えなくなること"が原因ではないかと考えられているそうです。倒立のときは全体で顔と認識するのではなく、目・口などが個別オブジェクトとして認知されるので、それら個別のパーツが正立（正しい向き）で見えているとあまり違和感を感じません。

ところが、サッチャー錯視を正立させると顔の全体認知処理が有効になり、より強く顔画像のおかしさを認知できるようになるのではないかと考えられているそうです（[横澤一彦,視覚科学](https://www.amazon.co.jp/%E8%A6%96%E8%A6%9A%E7%A7%91%E5%AD%A6-%E6%A8%AA%E6%BE%A4-%E4%B8%80%E5%BD%A6/dp/4326250615))

#3.アプリはどうやって作ったの？
Webカメラで画像を取り込んで、顔と目と口の位置を特定するプログラムを　ConputerVisonToolboxを用いて作成しました。
特定だけでなく、トラッキングもやってくれるので動いてもちゃんとついてきます。
（画面の外にはみ出るとエラーがでますが、ほったらかしにしてます。まぁ勘弁してください笑）

特定した目と口の領域を反転させる機能と、画像全体を反転させる機能をもっているので、
だれでもかんたんにリアルタイムサッチャー錯視を体験できるアプリになっています。
毎度おなじみAppDesignerでつくっております。

![スクリーンショット 2020-11-22 22.53.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331637/8ef691ad-a950-9c7a-6f8c-0328fbd6d118.png)

AppDesignerでGUIアプリを作る方法についてはこちらをご参照ください
[MATLABで簡単にGUIを作ろう！](https://qiita.com/griffin921/items/d761abc656542f62a611)

ではどのように作ったか、全部はすごく長くなるので解説は難しいですが、要点だけしぼって解説します！


##3.1 Webカメラ画像の読み込み
Matlab Support Package for USB Webcamsを使いました。
https://jp.mathworks.com/help/supportpkg/usbwebcams/index.html?s_tid=CRUX_lftnav

webcam関数でカメラオブジェクトを生成したら　snapshot関数を用いてフレームを取得できます。
AppDesignerのプロパティでcam　を作ったらapp.camにオブジェクトを作ります。

```MATLAB
   %カメラオブジェクト起動
   app.cam = webcam();
   %カメラからフレーム取得
   VideoFrame = snapshot(app.cam);
```

ビデオフレームの取得と、GUI上のイメージオブジェクトへビデオフレームを保存という処理を
前回私が書いた[一個前の記事](https://qiita.com/griffin921/items/72bca3678ff387445ac0)で紹介したタイマ割り込み関数で呼び出すと、動画がイメージオブジェクトに表示できます！
前回はタイマ割り込み周期が１秒毎でしたが、1/FPS秒周期で動作させればちゃんと動画にみえます。

```MATLAB

    %FPS設定
    FPS = 20;

    %タイマオブジェクト生成
    app.UpdateTimer = timer('Period', 1/FPS,...
                'ExecutionMode', 'fixedRate', ...
                'TasksToExecute', Inf, ... 
                'TimerFcn', @app.mytimer_func);
```

割り込み関数は下記の通り。

```MATLAB
        %------------------------------------------------------------------
        %タイマ割り込み関数　初期化関数で指定したFPSに応じて一定間隔で呼び出されます
        function mytimer_func(app, obj, event)
            
            %カメラからフレーム取得
            VideoFrame = snapshot(app.cam);
            
            %検出対象のオブジェクトポイント数がいずれか10以下になったら検出処理を行う
            %オブジェクトポイント数が10より多い場合、フレーム毎検出領域のトラッキングを行う
            if or(app.Eyes.NumPts < 10 , app.Mouth.NumPts < 10)
                app.DetectingObjectsLamp.Enable = true;
                app.TrackingObjectsLamp.Enable = false;
                VideoFrame = app.DetectProcess(VideoFrame);
            else
                app.DetectingObjectsLamp.Enable = false;
                app.TrackingObjectsLamp.Enable = true;
                VideoFrame =app.TrackingProcess(VideoFrame);
            end
            
            %FlipSwitchがOnなら画像を上下反転させる
            if strcmp(app.VerticalCntSwitch.Value,'On')
                VideoFrame = flipud(VideoFrame);
            end
            
            % ビデオプレイヤーフレーム更新
            app.Image.ImageSource = VideoFrame;
            
        end
        %End mytimer_func--------------------------------------------------
```

##3.2 物体位置の特定

実は画像処理部分はあんまり詳しくないのでふわっとした理解のままつくっています。
間違い等指摘あれば教えて下さい。

Computer Vision Toolboxは学習済みの判別器が用意されているようです。
色々あるのですが顔と顔のパーツであればたいてい認識できるみたいです。
こちらのサンプルを参考にしました。
https://jp.mathworks.com/help/vision/examples/face-detection-and-tracking-using-live-video-acquisition.html

[vision.CascadeObjectDetector](https://jp.mathworks.com/help/vision/ref/vision.cascadeobjectdetector-system-object.html)
カスケード型オブジェクト検出器は、Viola-Jones アルゴリズムを使用して人の顔、鼻、目、口、上半身などを検出します。

下記のように物体認識オブジェクトを生成、画像を物体認識オブジェクトに読み込ませる
画像の中で物体が存在する領域をbboxk形式で取得します。

bboxは4要素ベクトルで、 [x y width height]　で定義されています。

```MATLAB
   % 学習済み物体認識オブジェクトを生成
   app.FaceDetector = vision.CascadeObjectDetector();
   app.Eyes.Detector = vision.CascadeObjectDetector('EyePairSmall', 'UseROI', true);
   app.Mouth.Detector = vision.CascadeObjectDetector('Mouth', 'UseROI', true);

   %グレースケール画像を生成
   GrayFrameImage = rgb2gray(VideoFrame);
            
   %顔領域をグレースケール画像で特定
   FaceBBox = app.FaceDetector.step(GrayFrameImage);
   %、顔領域の中から目と口の位置を特定
   app.Eyes.BBox = app.Eyes.Detector.step(GrayFrameImage,FaceBBox(1,:));
   app.Mouth.BBox = app.Mouth.Detector.step(GrayFrameImage,FaceBBox(1,:));
```

全体の記述はこんな感じ。

```MATLAB
        %%------------------------------------------------------------------
        %%物体位置特定に関係する関数
        %%------------------------------------------------------------------
        
        %------------------------------------------------------------------
        %物体検出関数　フレームから　顔・目・口の位置を検出し、トラッカーに割り当てます
        function [Return_VideoFrame] = DetectProcess(app,VideoFrame)

            %グレースケール画像を生成
            GrayFrameImage = rgb2gray(VideoFrame);
            
            %顔領域をグレースケール画像で特定
            FaceBBox = app.FaceDetector.step(GrayFrameImage);
        
            %顔位置特定が成功したら
            if ~isempty(FaceBBox)
                
                %、顔領域の中から目の位置を特定
                app.Eyes.BBox = app.Eyes.Detector.step(GrayFrameImage,FaceBBox(1,:));
                %目の領域が特定できていたらVideoFrameに位置情報を重畳する。
                if ~isempty(app.Eyes.BBox)
                    [VideoFrame,app.Eyes] = DetectObject(app,app.Eyes,VideoFrame);

                end
                
                %、顔領域の中から口の位置を特定
                app.Mouth.BBox = app.Mouth.Detector.step(GrayFrameImage,FaceBBox(1,:));
                %口の領域が特定できていたらVideoFrameに位置情報を重畳する。
                if ~isempty(app.Mouth.BBox)
                    [VideoFrame,app.Mouth] = DetectObject(app,app.Mouth,VideoFrame);

                end

            end
            Return_VideoFrame = VideoFrame;

        end
        %End DetectProcess-------------------------------------------------
        
        
        function [ReturnVideoFrame,ReturnTarget] = DetectObject(app,Target,VideoFrame)
            
            %グレースケール画像を生成
            GrayFrameImage = rgb2gray(VideoFrame);
           
            % 特定領域のコーナ点を特定する.
            Points = detectMinEigenFeatures(GrayFrameImage, 'ROI', Target.BBox(1, :));
          
            % ポイントトラッカーの初期化
            Target = InitializeTrackObj(app,Points,GrayFrameImage,Target);
    
            % Convert the box corners into the [x1 y1 x2 y2 x3 y3 x4 y4]
            % ポリゴン描画のためのにポリゴン関数にあわせた座標に変換
            bboxPolygon = reshape(Target.BBoxPoints', 1, []);
        
            % 特定領域に長方形のポリゴンをフレームに重畳させる.
            VideoFrame = insertShape(VideoFrame, 'Polygon', bboxPolygon, 'LineWidth', 3);
        
            % 特定コーナをフレームに重畳させる.
            VideoFrame = insertMarker(VideoFrame, Target.xyPoints, '+', 'Color', 'white');
            
           ReturnVideoFrame = VideoFrame;
           ReturnTarget = Target;
        end

```



##3.3 物体のトラッキング
物体位置特定はすごい便利ですが処理が重いです。
毎フレームでで物体位置特定しているとものすごいことになるので、物体のトラッキングを用いています。
これは１フレーム前で定義したとある領域が次のフレームでどれだけ動いたかを推定します。

このトラッキング動作によって毎フレームで物体位置特定せずとも、特定領域が移動しても追跡可能になります。
そのような追跡を行うには[vision.PointTracker](https://jp.mathworks.com/help/vision/ref/vision.pointtracker-system-object.html)を使います。

Trackerはいくつか種類があるようなので必ずしもPointTrackerを使う必要はないと思いますが、追従精度がけっこうよかったきがしたのでつかってみました。

```MATLAB
        %%------------------------------------------------------------------
        %%物体トラッキングに関係する関数
        %%------------------------------------------------------------------
        
        %------------------------------------------------------------------
        %物体トラッキング関数　一つ前のフレームトラッキング結果から　
        %トラッキング対象領域座標を更新する
        function Return_VideoFrame = TrackingProcess(app,VideoFrame)

           %目のトラッカーを更新
           [VideoFrame,app.Eyes] = UpdateTrackObject(app,app.Eyes,VideoFrame);
           
           %口のトラッカーを更新
           [VideoFrame,app.Mouth] = UpdateTrackObject(app,app.Mouth,VideoFrame);
           
           Return_VideoFrame = VideoFrame;
        end
        %End TrackingProcess--------------------------------------------------
        
        %------------------------------------------------------------------
        %オブジェクトトラッカーを更新する
        function [ReturnVideoFrame,ReturnTarget] = UpdateTrackObject(app,Target,VideoFrame)
            
           %グレースケール画像を生成
           GrayFrameImage = rgb2gray(VideoFrame);

           %グレーフレームから現在のトラックポイントを取得する
           [xyPoints, isFound] = step(Target.Tracker, GrayFrameImage);
           visiblePoints = xyPoints(isFound, :);
           oldInliers = Target.OldPoints(isFound, :);
           Target.NumPts = size(visiblePoints, 1);

           %ターゲットのトラックポイントが10以上なトラックを継続する。
           if Target.NumPts >= 10
                % 一つ前のフレームで作成したポイント位置と
                %現在のフレームのポイント位置のズレから2次元の幾何学変換を推定
                [xform, inlierIdx] = estimateGeometricTransform2D(...
                    oldInliers, visiblePoints, 'similarity', 'MaxDistance', 4);
                visiblePoints = visiblePoints(inlierIdx, :);
    
                % 順方向アフィン変換でトラッキング領域を示すポリゴン座標を変換
                Target.BBoxPoints = transformPointsForward(xform, Target.BBoxPoints);
    
                % 特定した目領域を示す四角形の座標
                % 座標形式を[x1 y1 x2 y2 x3 y3 x4 y4]に変換
                
                bboxPolygon = reshape(Target.BBoxPoints', 1, []);
    
                %平行四辺形にならないように調整
                [Xmin,Xmax,Ymin,Ymax,bboxPolygon] = fixReqPoint(app,bboxPolygon);
                
                % ポリゴンを画面に重畳.
                %VideoFrame = insertShape(VideoFrame, 'Polygon', bboxPolygon, 'LineWidth', 3);
    
                % トラックポイントを描画.
                %videoFrame = insertMarker(videoFrame, visiblePoints, '+', 'Color', 'white');
    
                % ポイント位置を更新
                %EyeesPointの位置をメモリに記憶させる.
                Target.OldPoints =  visiblePoints;
                setPoints(Target.Tracker,Target.OldPoints);
                
                %錯視が有効ならばオブジェクトの位置のフレームを反転させる
                if strcmp(app.IllusionCntSwitch.Value,'On')
                    VideoFrame(Ymin:Ymax,Xmin:Xmax,:) = flipud(VideoFrame(Ymin:Ymax,Xmin:Xmax,:));
                end
           end
           ReturnVideoFrame = VideoFrame;
           ReturnTarget = Target;
        end
        %------------------------------------------------------------------
        
        %------------------------------------------------------------------
        %トラッカーの初期化関数
        function Target = InitializeTrackObj(app,Points,GrayFrameImage,Target)
            % ポイントトラッカーの初期化
            Target.xyPoints = Points.Location;
            Target.NumPts = size(Target.xyPoints,1);
            release(Target.Tracker);
            initialize(Target.Tracker, Target.xyPoints, GrayFrameImage);
            %１フレーム前のPointの位置を記憶させる.
            Target.OldPoints = Target.xyPoints;
            Target.BBoxPoints = bbox2points(Target.BBox(1, :));
        end
        %end InitializeTrackObjI-------------------------------------------
        
        %------------------------------------------------------------------
        %トラッカーのポリゴンフレームの座標を調整する。
        function [Xmin,Xmax,Ymin,Ymax,bboxPolygon] = fixReqPoint(app,bboxPolygon)
            
            bboxPolygon(1) = max(bboxPolygon(1),bboxPolygon(7));%X1
            bboxPolygon(2) = max(bboxPolygon(2),bboxPolygon(4));%Y1
            bboxPolygon(3) = max(bboxPolygon(3),bboxPolygon(5));%X2
            bboxPolygon(4) = max(bboxPolygon(2),bboxPolygon(4));%Y2
            bboxPolygon(5) = max(bboxPolygon(3),bboxPolygon(5));%X3
            bboxPolygon(6) = max(bboxPolygon(6),bboxPolygon(8));%Y3
            bboxPolygon(7) = max(bboxPolygon(1),bboxPolygon(7));%X4
            bboxPolygon(8) = max(bboxPolygon(6),bboxPolygon(8));%Y4
            
            Xmin  = round(max(bboxPolygon(1),bboxPolygon(7))) - app.XExpand ;
            Xmax = round(max(bboxPolygon(3),bboxPolygon(5))) + app.XExpand ;
            Ymin  = round(max(bboxPolygon(2),bboxPolygon(4))) - app.YExpand ;
            Ymax = round(max(bboxPolygon(6),bboxPolygon(8))) + app.YExpand ;
        end
        %------------------------------------------------------------------

```

ここでミソは現在の一つ前のトラックポイントをどこかの変数に保存しておくということです。
なんでこんな事をするかというとこの関数を使いたいからです。

```MATLAB
% 一つ前のフレームで作成したポイント位置と
%現在のフレームのポイント位置のズレから2次元の幾何学変換を推定
[xform, inlierIdx] = estimateGeometricTransform2D(...
            oldInliers, visiblePoints, 'similarity', 'MaxDistance', 4);
```
この関数は一つ前のフレームで作成したトラックポイントと現在のフレームののトラックポイントを比較することで２次元の幾何学変換ベクトルを推定してくれます。
つまり、トラックポイントが水平移動しているか、垂直移動しているのか、拡大・縮小しているのか、いわゆるアフィン変換を行うための変換用のベクトルを取得します。
http://zellij.hatenablog.com/entry/20120523/p1

取得したベクトルをもとにトラッキング領域をアフィン変換すれば、特定領域をトラッキングできるようです。

しかしお恥ずかしい話細かいところの動きは理解できていません・・・
今はなんとなく動いてるのでヨシ！ってしています。
特に検出したオブジェクト領域がたしかに目や口を特定しているのですが、サッチャー錯視として切り取る領域としては少し狭いことが多いので、領域を広げて切り取りたいがために
fixReqPoint(app,bboxPolygon)
という謎関数を実装してなんだかよくわからない処理をしています。

この辺アフィン変換による指定領域座標の拡大とかできたら
こんな処理書かなくていいと思うのですが。。。。　勉強不足で力業になってしまいました。



#4. トラッキング対象領域だけ垂直方向に反転

GUI上のスイッチがONになっていたら
トラッキング領域の部分だけ画像の行列データを上下反転してサッチャー錯視を作ります。
flipudという便利な関数があるので垂直方向の反転は簡単です！

```MATLAB
%錯視が有効ならばオブジェクトの位置のフレームを反転させる
if strcmp(app.IllusionCntSwitch.Value,'On')
   VideoFrame(Ymin:Ymax,Xmin:Xmax,:) = flipud(VideoFrame(Ymin:Ymax,Xmin:Xmax,:));
end
```
コード例では目のところだけの記述ですが、口も同様のコードをかけば実施できます！

#5.まとめ

- **認知心理学における顔認知について**

 - 人間の脳は顔に対して特別な処理”顔への全体認知処理”を使っていることが推測される
 - そのため倒立した人間の顔と正立した人間の顔で認知の仕方が異なる
 - そのようなメカニズムを示唆する錯視の一つがサッチャー錯視である
 
- **ソフトウェアによる顔認識について**
 - カメラ画像取り込みは[Matlab Support Package for USB Webcams](https://jp.mathworks.com/help/supportpkg/usbwebcams/index.html?s_tid=CRUX_lftnav)を使えばできる
 - ComputerVisionToolboxの[vision.CascadeObjectDetector](https://jp.mathworks.com/help/vision/ref/vision.cascadeobjectdetector-system-object.html)を使えば学習済みの検出器が利用でき、顔・目・鼻・口などの位置を画像から認識できる
 - [vision.PointTracker](https://jp.mathworks.com/help/vision/ref/vision.pointtracker-system-object.html)を使うと画像中のオブジェクトの位置を追跡できるため、一度画像認識でオブジェクトの位置を特定したら毎フレーム位置検出をする必要がない

以上です！
最後までお読みくださってありがとうございます😊

もしかして、ここまで読んだ方、錯視すきなのでは？

ついでに僕のおすすめ錯視を貼っておきますね。

重力に逆らうボール
http://illusionoftheyear.com/2010/05/impossible-motion-magnet-like-slopes/

回転軸がコロコロ変わる
https://youtu.be/HC0GGkNZPgs



エンジョイ！