作成者はhttps://www.brightfunds.org/organizations/girls-who-code[Girls Who Code]を選択して、https://do.co/w4do-cta [Donationsのための書き込み]プログラムの一環として寄付を受け取りました.

前書き

コンピュータービジョンは、画像やビデオから高次の理解を引き出すことを目的としたコンピューターサイエンスのサブフィールドです。 このフィールドには、オブジェクト検出、画像復元(マトリックス補完)、オプティカルフローなどのタスクが含まれます。 コンピュータービジョンは、自動運転車のプロトタイプ、従業員のいない食料品店、楽しいSnapchatフィルター、モバイルデバイスの顔認証システムなどのテクノロジーを強化します。

このチュートリアルでは、事前学習済みのモデルを使用してSnapchat風のドッグフィルターを構築する際のコンピュータービジョンについて説明します。 Snapchatに慣れていない人のために、このフィルターはあなたの顔を検出し、犬のマスクを重ねます。 次に、顔の感情分類器をトレーニングして、幸せの場合はコーギー、悲しい場合はパグなど、感情に基づいて犬のマスクを選択できるようにします。 途中で、通常の最小二乗法とコンピュータービジョンの両方で関連する概念を探り、機械学習の基礎を学びます。

画像:https://assets.digitalocean.com/articles/python3_dogfilter/Hf5RDn3.gif [動く犬のフィルター]

チュートリアルを進める際には、コンピュータービジョンライブラリである + OpenCV +、線形代数ユーティリティには + numpy +、プロットには `+ matplotlib +`を使用します。 また、コンピュータービジョンアプリケーションを作成するときに、次の概念を適用します。

  • 回帰および分類手法としての通常の最小二乗。

  • 確率的勾配ニューラルネットワークの基礎。

このチュートリアルを完了する必要はありませんが、これらの数学的概念に精通していれば、より詳細な説明のいくつかを理解しやすくなります。

  • 基本的な線形代数の概念:スカラー、ベクトル、および行列。

  • 基礎計算:導関数をとる方法。

このチュートリアルの完全なコードは、https://github.com/do-community/emotion-based-dog-filterにあります。

始めましょう。

前提条件

このチュートリアルを完了するには、次のものが必要です。

手順1-プロジェクトの作成と依存関係のインストール

このプロジェクトのワークスペースを作成し、必要な依存関係をインストールしましょう。 ワークスペースを「+ DogFilter +」と呼びます。

mkdir ~/DogFilter

`+ DogFilter +`ディレクトリに移動します:

cd ~/DogFilter

次に、プロジェクト用の新しいPython仮想環境を作成します。

python3 -m venv

環境をアクティブにします。

source /bin/activate

プロンプトが変わり、環境がアクティブであることを示します。 次に、http://pytorch.org/ [PyTorch]をインストールします。これは、このチュートリアルで使用するPythonの深層学習フレームワークです。 インストールプロセスは、使用しているオペレーティングシステムによって異なります。

macOSで、次のコマンドを使用してPytorchをインストールします。

python -m pip install torch== torchvision==

Linuxでは、次のコマンドを使用します。

pip install http://download.pytorch.org/whl/cpu/torch--cp35-cp35m-.whl
pip install torchvision

Windowsの場合、次のコマンドを使用してPytorchをインストールします。

pip install http://download.pytorch.org/whl/cpu/torch--cp35-cp35m-.whl
pip install torchvision

次に、それぞれコンピュータービジョンと線形代数ライブラリである、「+ OpenCV」と「+ numpy」用に事前にパッケージ化されたバイナリをインストールします。 前者は画像回転などのユーティリティを提供し、後者は行列反転などの線形代数ユーティリティを提供します。

python -m pip install opencv-python== numpy==

最後に、このチュートリアルで使用する画像を保持するアセットのディレクトリを作成します。

mkdir assets

依存関係がインストールされたら、フィルターの最初のバージョンである顔検出器を作成しましょう。

ステップ2-顔検出器の構築

最初の目的は、画像内のすべての顔を検出することです。 単一の画像を受け入れ、ボックスで輪郭が描かれた注釈付き画像を出力するスクリプトを作成します。

幸いなことに、独自の顔検出ロジックを作成する代わりに、_事前学習済みモデル_を使用できます。 モデルをセットアップしてから、事前に訓練されたパラメーターをロードします。 OpenCVは両方を提供することでこれを簡単にします。

OpenCVは、ソースコードでモデルパラメーターを提供します。 ただし、これらのパラメーターを使用するには、ローカルにインストールされたOpenCVへの絶対パスが必要です。 その絶対パスは異なる場合があるため、代わりに独自のコピーをダウンロードして、 `+ assets +`フォルダーに配置します。

wget -O assets/haarcascade_frontalface_default.xml https://github.com/opencv/opencv/raw/master/data/haarcascades/haarcascade_frontalface_default.xml

`+ -O `オプションは、宛先を ` assets / haarcascade_frontalface_default.xml`として指定します。 2番目の引数はソースURLです。

https://pexels.com [Pexels](CC0、https://www.pexels.com/photo/people-girl-design-happy-35188/ [元のリンク]の次の画像ですべての顔を検出します画像])。

image:https://assets.digitalocean.com/articles/python3_dogfilter/CfoBWbF.png [子供の写真]

まず、画像をダウンロードします。 次のコマンドは、ダウンロードした画像を + assets`フォルダーに + children.png + `として保存します。

wget -O assets/children.png https://assets.digitalocean.com/articles/python3_dogfilter/CfoBWbF.png

検出アルゴリズムが機能することを確認するために、個々の画像でそれを実行し、結果の注釈付き画像をディスクに保存します。 これらの注釈付きの結果用に `+ outputs +`フォルダーを作成します。

mkdir outputs

次に、顔検出器用のPythonスクリプトを作成します。 `+ nano `またはお好みのテキストエディターを使用して、ファイル ` step_1_face_detect +`を作成します。

nano step_2_face_detect.py

次のコードをファイルに追加します。 このコードは、画像ユーティリティと顔分類器を含むOpenCVをインポートします。 残りのコードは、典型的なPythonプログラムの定型文です。

step_2_face_detect.py

"""Test for face detection"""

import cv2


def main():
   pass

if __name__ == '__main__':
   main()

次に、「+ main 」関数の「 pass 」を、「 assets +」フォルダーにダウンロードしたOpenCVパラメーターを使用して顔分類器を初期化する次のコードに置き換えます。

step_2_face_detect.py

def main():
   # initialize front face classifier
   cascade = cv2.CascadeClassifier("assets/haarcascade_frontalface_default.xml")

次に、この行を追加して、画像「+ children.png +」をロードします。

step_2_face_detect.py

   frame = cv2.imread('assets/children.png')

次に、このコードを追加して、画像を白黒に変換します。これは、分類器が白黒画像でトレーニングされたためです。 これを実現するために、グレースケールに変換してからヒストグラムを離散化します。

step_2_face_detect.py

   # Convert to black-and-white
   gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
   blackwhite = cv2.equalizeHist(gray)

次に、OpenCVのhttps://docs.opencv.org/2.4/modules/objdetect/doc/cascade_classification.html#cascadeclassifier-detectmultiscale [+ detectMultiScale +]関数を使用して、画像内のすべての顔を検出します。

step_2_face_detect.py

   rects = cascade.detectMultiScale(
       blackwhite, scaleFactor=1.3, minNeighbors=4, minSize=(30, 30),
       flags=cv2.CASCADE_SCALE_IMAGE)
  • `+ scaleFactor +`は、各次元に沿って画像がどれだけ縮小されるかを指定します。

  • 「+ minNeighbors +」は、候補矩形を保持する必要がある隣接矩形の数を示します。

  • `+ minSize +`は、検出されたオブジェクトの最小許容サイズです。 これより小さいオブジェクトは破棄されます。

戻り値のタイプはhttps://www.digitalocean.com/community/tutorials/understanding-tuples-in-python-3[tuples]のリストです。各タプルには、最小x、最小y、幅、その順序での長方形の高さ。

検出されたすべてのオブジェクトを繰り返し処理し、https://docs.opencv.org/2.4/modules/core/doc/drawing_functions.html#rectangle [+ cv2.rectangle +]を使用して画像上に緑色で描画します。

step_2_face_detect.py

   for x, y, w, h in rects:
       cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2)
  • 2番目と3番目の引数は、長方形の反対側の角です。

  • 4番目の引数は、使用する色です。 `(0、255、0)`は、RGBカラースペースの緑に対応します。

  • 最後の引数は、行の幅を示します。

最後に、バウンディングボックス付きの画像を新しいファイルの `+ outputs / children_detected.png +`に書き込みます。

step_2_face_detect.py

   cv2.imwrite('outputs/children_detected.png', frame)

完成したスクリプトは次のようになります。

step_2_face_detect.py

"""Tests face detection for a static image."""

import cv2


def main():

   # initialize front face classifier
   cascade = cv2.CascadeClassifier(
       "assets/haarcascade_frontalface_default.xml")

   frame = cv2.imread('assets/children.png')

   # Convert to black-and-white
   gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
   blackwhite = cv2.equalizeHist(gray)

   rects = cascade.detectMultiScale(
       blackwhite, scaleFactor=1.3, minNeighbors=4, minSize=(30, 30),
   flags=cv2.CASCADE_SCALE_IMAGE)

   for x, y, w, h in rects:
       cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2)

   cv2.imwrite('outputs/children_detected.png', frame)

if __name__ == '__main__':
   main()

ファイルを保存して、エディターを終了します。 次に、スクリプトを実行します。

python step_2_face_detect.py

`+ outputs / children_detected.png +`を開きます。 ボックスで囲まれた顔を示す次の画像が表示されます。

image:https://assets.digitalocean.com/articles/python3_dogfilter/x0fUqyk.png [境界ボックスを持つ子供の写真]

この時点で、作業中の顔検出器があります。 入力として画像を受け入れ、画像内のすべての顔の周りに境界ボックスを描画し、注釈付き画像を出力します。 次に、この同じ検出をライブカメラフィードに適用します。

ステップ3-カメラフィードのリンク

次の目的は、コンピューターのカメラを顔検出器にリンクすることです。 静止画像で顔を検出する代わりに、コンピューターのカメラからすべての顔を検出します。 カメラ入力を収集し、すべての顔を検出して注釈を付け、注釈付きの画像をユーザーに表示します。 手順2のスクリプトから続行するため、そのスクリプトを複製することから始めます。

cp step_2_face_detect.py step_3_camera_face_detect.py

次に、エディターで新しいスクリプトを開きます。

nano step_3_camera_face_detect.py

このhttps://docs.opencv.org/3.0-beta/doc/py_tutorials/py_gui/py_video_display/py_video_display.html#capture-video-from-camera[test]のいくつかの要素を使用して、 `+ main `関数を更新しますスクリプト]公式OpenCVドキュメントから。 コンピューターのカメラからライブフィードをキャプチャするように設定されている ` VideoCapture `オブジェクトを初期化することから始めます。 これを ` main +`関数の先頭、関数内の他のコードの前に配置します:

step_3_camera_face_detect.py

def main():

   ...

`+ frame `を定義する行から開始して、既存のコードをすべてインデントし、すべてのコードを ` while +`ループに配置します。

step_3_camera_face_detect.py

       frame = cv2.imread('assets/children.png')
       ...
       for x, y, w, h in rects:
           cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2)

       cv2.imwrite('outputs/children_detected.png', frame)

+ while`ループの開始時に + frame`を定義する行を置き換えます。 ディスク上の画像から読み取る代わりに、カメラから読み取ります。

step_3_camera_face_detect.py

`+ while `ループの最後にある行 ` cv2.imwrite(…​)+`を置き換えます。 画像をディスクに書き込む代わりに、注釈付きの画像をユーザーの画面に表示します。

step_3_camera_face_detect.py

また、キーボード入力を監視するコードを追加して、プログラムを停止できるようにします。 ユーザーが `+ q `文字を打ったかどうかを確認し、打った場合はアプリケーションを終了します。 ` cv2.imshow(…​)+`の直後に以下を追加します:

step_3_camera_face_detect.py

...
       cv2.imshow('frame', frame)


...

行 `+ cv2.waitkey(1)+`は、キャプチャした画像をユーザーに表示できるように、1ミリ秒間プログラムを停止します。

最後に、キャプチャをリリースし、すべてのウィンドウを閉じます。 これを + while`ループの外側に配置して、 + main`関数を終了します。

step_3_camera_face_detect.py

...

   while True:
   ...

スクリプトは次のようになります。

step_3_camera_face_detect.py

"""Test for face detection on video camera.

Move your face around and a green box will identify your face.
With the test frame in focus, hit `q` to exit.
Note that typing `q` into your terminal will do nothing.
"""

import cv2


def main():
   cap = cv2.VideoCapture(0)

   # initialize front face classifier
   cascade = cv2.CascadeClassifier(
       "assets/haarcascade_frontalface_default.xml")

   while True:
       # Capture frame-by-frame
       ret, frame = cap.read()

       # Convert to black-and-white
       gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
       blackwhite = cv2.equalizeHist(gray)

       # Detect faces
       rects = cascade.detectMultiScale(
           blackwhite, scaleFactor=1.3, minNeighbors=4, minSize=(30, 30),
           flags=cv2.CASCADE_SCALE_IMAGE)

       # Add all bounding boxes to the image
       for x, y, w, h in rects:
           cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2)

       # Display the resulting frame
       cv2.imshow('frame', frame)
       if cv2.waitKey(1) & 0xFF == ord('q'):
           break

   # When everything done, release the capture
   cap.release()
   cv2.destroyAllWindows()


if __name__ == '__main__':
   main()

ファイルを保存して、エディターを終了します。

次に、テストスクリプトを実行します。

python step_3_camera_face_detect.py

これにより、カメラが有効になり、カメラのフィードを表示するウィンドウが開きます。 顔はリアルタイムで緑色の四角で囲まれます:

画像:https://assets.digitalocean.com/articles/python3_dogfilter/a7lyf7q.gif [作業面検出器]

次の目的は、検出された顔を取得し、それぞれに犬のマスクを重ね合わせることです。

ステップ4-犬用フィルターの作成

フィルター自体を作成する前に、画像がどのように数値で表されるかを調べましょう。 これにより、画像を変更し、最終的に犬のフィルターを適用するために必要な背景が得られます。

例を見てみましょう。 数字を使用して白黒画像を作成できます。「+ 0+」は黒に対応し、「+ 1+」は白に対応します。

1と0の間の境界線に注目してください。 どんな形が見えますか?

0 0 0 0 0 0 0 0 0
0 0 0 0 1 0 0 0 0
0 0 0 1 1 1 0 0 0
0 0 1 1 1 1 1 0 0
0 0 0 1 1 1 0 0 0
0 0 0 0 1 0 0 0 0
0 0 0 0 0 0 0 0 0

画像はダイヤモンドです。 このhttps://www.khanacademy.org/math/precalculus/precalc-matrices[matrix]の値を画像として保存する場合。 これにより、次の図が表示されます。

image:https://assets.digitalocean.com/articles/python3_dogfilter/QPontyM.png [画像としてのダイヤモンド]

0.1、0.26、0.74391など、0〜1の任意の値を使用できます。 0に近い数字は暗く、1に近い数字は明るくなります。 これにより、白、黒、およびあらゆるグレーの色合いを表現できます。 これは、0、1、およびその間の任意の値を使用して任意のグレースケール画像を構築できるため、私たちにとって素晴らしいニュースです。 たとえば、次のことを考慮してください。 それが何かわかりますか? 繰り返しますが、各数値はピクセルの色に対応しています。

1  1  1  1  1  1  1  1  1  1  1  1
1  1  1  1  0  0  0  0  1  1  1  1
1  1  0  0 .4 .4 .4 .4  0  0  1  1
1  0 .4 .4 .5 .4 .4 .4 .4 .4  0  1
1  0 .4 .5 .5 .5 .4 .4 .4 .4  0  1
0 .4 .4 .4 .5 .4 .4 .4 .4 .4 .4  0
0 .4 .4 .4 .4  0  0 .4 .4 .4 .4  0
0  0 .4 .4  0  1 .7  0 .4 .4  0  0
0  1  0  0  0 .7 .7  0  0  0  1  0
1  0  1  1  1  0  0 .7 .7 .4  0  1
1  0 .7  1  1  1 .7 .7 .7 .7  0  1
1  1  0  0 .7 .7 .7 .7  0  0  1  1
1  1  1  1  0  0  0  0  1  1  1  1
1  1  1  1  1  1  1  1  1  1  1  1

画像として再レンダリングすると、これは実際にはポケボールであることがわかります。

image:https://assets.digitalocean.com/articles/python3_dogfilter/RwAXIGE.png [絵としてのポケボール]

これで、白黒画像とグレースケール画像がどのように数値で表されるかがわかりました。 色を導入するには、より多くの情報をエンコードする方法が必要です。 画像の高さと幅は「+ h x w +」で表されます。

画像:https://assets.digitalocean.com/articles/python3_dogfilter/sctg0YN.png [画像]

現在のグレースケール表現では、各ピクセルは0と1の間の1つの値です。 同様に、画像のサイズは `+ h x w x 1 `であると言えます。 つまり、画像内のすべての `(x、y)+`の位置には1つの値しかありません。

image:https://assets.digitalocean.com/articles/python3_dogfilter/58GGRPe.png [グレースケール画像]

色表現では、0〜1の3つの値を使用して各ピクセルの色を表します。 1つの数字は「赤の度合い」に対応し、1つは「緑の度合い」に対応し、最後の数字は「青の度合い」に対応します。これを_RGBカラースペース_と呼びます。 これは、画像内のすべての `(x、y)`位置に対して、3つの値 `(r、g、b)`があることを意味します。 その結果、画像は `+ h x w x 3 +`になりました。

image:https://assets.digitalocean.com/articles/python3_dogfilter/kXL8Mms.png [カラー画像]

ここでは、各数値の範囲は0〜1ではなく0〜255ですが、考え方は同じです。 数字の異なる組み合わせは、濃い紫の「(102、0、204)」や明るいオレンジの「(255、153、51)」など、異なる色に対応します。 要点は次のとおりです。

  1. 各画像は、高さ、幅、カラーチャネルの3つの次元を持つ数字のボックスとして表されます。 この数字のボックスを直接操作することは、画像を操作することと同じです。

  2. このボックスを平らにして、数字のリストにすることもできます。 このようにして、画像はhttps://www.khanacademy.org/math/precalculus/vectors-precalc[vector]になります。 後で、画像をベクターと呼びます。

これで、画像が数値で表現される方法が理解できたので、顔に犬のマスクを適用する準備が整っています。 犬マスクを適用するには、子画像の値を非白の犬マスクピクセルに置き換えます。 最初に、単一の画像を使用します。 手順2で使用した画像からこの顔の一部をダウンロードします。

wget -O assets/child.png https://assets.digitalocean.com/articles/python3_dogfilter/alXjNK1.png

image:https://assets.digitalocean.com/articles/python3_dogfilter/alXjNK1.png [顔のトリミング]

さらに、次の犬のマスクをダウンロードします。 このチュートリアルで使用される犬のマスクは、私自身の図面であり、現在CC0ライセンスの下でパブリックドメインにリリースされています。

画像:https://assets.digitalocean.com/articles/python3_dogfilter/ED32BCs.png [犬のマスク]

`+ wget +`でこれをダウンロードしてください:

wget -O assets/dog.png https://assets.digitalocean.com/articles/python3_dogfilter/ED32BCs.png

`+ step_4_dog_mask_simple.py +`という新しいファイルを作成します。このファイルには、犬のマスクを顔に適用するスクリプトのコードが保持されます。

nano step_4_dog_mask_simple.py

Pythonスクリプトに次の定型文を追加し、OpenCVおよび `+ numpy +`ライブラリをインポートします。

step_4_dog_mask_simple.py

"""Test for adding dog mask"""

import cv2
import numpy as np


def main():
   pass

if __name__ == '__main__':
   main()

`+ main `関数の ` pass +`を、元の画像と犬のマスクをメモリに読み込むこれらの2行で置き換えます。

step_4_dog_mask_simple.py

...
def main():
   face = cv2.imread('assets/child.png')
   mask = cv2.imread('assets/dog.png')

次に、犬のマスクを子供に合わせます。 ロジックは以前に行ったものよりも複雑なので、コードをモジュール化するために「+ apply_mask 」と呼ばれる新しい関数を作成します。 画像を読み込む2行の直後に、「 apply_mask +」関数を呼び出す次の行を追加します。

step_4_dog_mask_simple.py

...
   face_with_mask = apply_mask(face, mask)

`+ apply_mask `という新しい関数を作成し、それを ` main +`関数の上に配置します。

step_4_dog_mask_simple.py

...
def apply_mask(face: np.array, mask: np.array) -> np.array:
   """Add the mask to the provided face, and return the face with mask."""
   pass

def main():
...

この時点で、ファイルは次のようになります。

step_4_dog_mask_simple.py

"""Test for adding dog mask"""

import cv2
import numpy as np


def apply_mask(face: np.array, mask: np.array) -> np.array:
   """Add the mask to the provided face, and return the face with mask."""
   pass


def main():
   face = cv2.imread('assets/child.png')
   mask = cv2.imread('assets/dog.png')
   face_with_mask = apply_mask(face, mask)

if __name__ == '__main__':
   main()

`+ apply_mask `関数を作成しましょう。 私たちの目標は、子供の顔にマスクを適用することです。 ただし、犬のマスクのアスペクト比を維持する必要があります。 そのためには、犬のマスクの最終寸法を明示的に計算する必要があります。 「 apply_mask 」関数内で、「 pass +」を、両方の画像の高さと幅を抽出する次の2行で置き換えます。

step_4_dog_mask_simple.py

...
   mask_h, mask_w, _ = mask.shape
   face_h, face_w, _ = face.shape

次に、どのディメンションを「さらに縮小」する必要があるかを判断します。正確には、2つの制約のうち、より厳密なものが必要です。 次の行を `+ apply_mask +`関数に追加します:

step_4_dog_mask_simple.py

...

   # Resize the mask to fit on face
   factor = min(face_h / mask_h, face_w / mask_w)

次に、このコードを関数に追加して、新しい形状を計算します。

step_4_dog_mask_simple.py

...
   new_mask_w = int(factor * mask_w)
   new_mask_h = int(factor * mask_h)
   new_mask_shape = (new_mask_w, new_mask_h)

`+ resize +`関数は整数次元を必要とするため、ここでは数値を整数にキャストします。

次のコードを追加して、犬のマスクのサイズを新しい形状に変更します。

step_4_dog_mask_simple.py

...

   # Add mask to face - ensure mask is centered
   resized_mask = cv2.resize(mask, new_mask_shape)

最後に、スクリプトを実行した後、サイズ変更された犬のマスクが正しいことを再確認できるように、イメージをディスクに書き込みます。

step_4_dog_mask_simple.py

   cv2.imwrite('outputs/resized_dog.png', resized_mask)

完成したスクリプトは次のようになります。

step_4_dog_mask_simple.py

"""Test for adding dog mask"""
import cv2
import numpy as np

def apply_mask(face: np.array, mask: np.array) -> np.array:
   """Add the mask to the provided face, and return the face with mask."""
   mask_h, mask_w, _ = mask.shape
   face_h, face_w, _ = face.shape

   # Resize the mask to fit on face
   factor = min(face_h / mask_h, face_w / mask_w)
   new_mask_w = int(factor * mask_w)
   new_mask_h = int(factor * mask_h)
   new_mask_shape = (new_mask_w, new_mask_h)

   # Add mask to face - ensure mask is centered
   resized_mask = cv2.resize(mask, new_mask_shape)
   cv2.imwrite('outputs/resized_dog.png', resized_mask)


def main():
   face = cv2.imread('assets/child.png')
   mask = cv2.imread('assets/dog.png')
   face_with_mask = apply_mask(face, mask)

if __name__ == '__main__':
   main()

ファイルを保存して、エディターを終了します。 新しいスクリプトを実行します。

python step_4_dog_mask_simple.py

`+ outputs / resized_dog.png +`で画像を開き、マスクが正しくサイズ変更されたことを再確認します。 このセクションで前述した犬のマスクと一致します。

次に、子供に犬のマスクを追加します。 `+ step_4_dog_mask_simple.py `ファイルを再度開き、 ` apply_mask +`関数に戻ります。

nano step_4_dog_mask_simple.py

最初に、サイズが変更されたマスクを記述するコード行を、不要になったため、 `+ apply_mask +`関数から削除します。

   ...

その代わりに、このセクションの最初から画像表現の知識を適用して、画像を変更します。 子画像のコピーを作成することから始めます。 次の行を `+ apply_mask +`関数に追加します:

step_4_dog_mask_simple.py

...
   face_with_mask = face.copy()

次に、犬のマスクが白または白に近いすべての位置を見つけます。 これを行うには、ピクセル値がすべてのカラーチャネルで250未満かどうかを確認します。これは、白に近いピクセルが「+ [255、255、255] +」に近いと予想されるためです。 このコードを追加:

step_4_dog_mask_simple.py

...
   non_white_pixels = (resized_mask < 250).all(axis=2)

この時点で、犬の画像は、多くても子の画像と同じ大きさです。 犬の画像を顔の中央に配置したいので、このコードを `+ apply_mask +`に追加して、犬の画像の中央に必要なオフセットを計算します。

step_4_dog_mask_simple.py

...
   off_h = int((face_h - new_mask_h) / 2)
   off_w = int((face_w - new_mask_w) / 2)

犬の画像からすべての非白ピクセルを子の画像にコピーします。 子の画像は犬の画像よりも大きい場合があるため、子の画像のサブセットを取得する必要があります。

step_4_dog_mask_simple.py

   face_with_mask[off_h: off_h+new_mask_h, off_w: off_w+new_mask_w][non_white_pixels] = \
           resized_mask[non_white_pixels]

次に、結果を返します。

step_4_dog_mask_simple.py

   return face_with_mask

`+ main `関数で、このコードを追加して ` apply_mask +`関数の結果を出力イメージに書き込み、手動で結果をダブルチェックできるようにします。

step_4_dog_mask_simple.py

...
   face_with_mask = apply_mask(face, mask)

完成したスクリプトは次のようになります。

step_4_dog_mask_simple.py

"""Test for adding dog mask"""

import cv2
import numpy as np


def apply_mask(face: np.array, mask: np.array) -> np.array:
   """Add the mask to the provided face, and return the face with mask."""
   mask_h, mask_w, _ = mask.shape
   face_h, face_w, _ = face.shape

   # Resize the mask to fit on face
   factor = min(face_h / mask_h, face_w / mask_w)
   new_mask_w = int(factor * mask_w)
   new_mask_h = int(factor * mask_h)
   new_mask_shape = (new_mask_w, new_mask_h)
   resized_mask = cv2.resize(mask, new_mask_shape)

   # Add mask to face - ensure mask is centered
   face_with_mask = face.copy()
   non_white_pixels = (resized_mask < 250).all(axis=2)
   off_h = int((face_h - new_mask_h) / 2)
   off_w = int((face_w - new_mask_w) / 2)
   face_with_mask[off_h: off_h+new_mask_h, off_w: off_w+new_mask_w][non_white_pixels] = \
        resized_mask[non_white_pixels]

   return face_with_mask

def main():
   face = cv2.imread('assets/child.png')
   mask = cv2.imread('assets/dog.png')
   face_with_mask = apply_mask(face, mask)
   cv2.imwrite('outputs/child_with_dog_mask.png', face_with_mask)

if __name__ == '__main__':
   main()

スクリプトを保存して実行します。

python step_4_dog_mask_simple.py

`+ outputs / child_with_dog_mask.png +`に犬のマスクを持つ子供の次の写真があります。

image:https://assets.digitalocean.com/articles/python3_dogfilter/ZEn0RsJ.png [犬のマスクをつけた子供の写真]

これで、犬のマスクを顔に適用するユーティリティができました。 作成したものを使用して、犬のマスクをリアルタイムで追加しましょう。

手順3で中断したところから再開します。 `+ step_3_camera_face_detect.py `を ` step_4_dog_mask.py +`にコピーします。

cp step_3_camera_face_detect.py step_4_dog_mask.py

新しいスクリプトを開きます。

nano step_4_dog_mask.py

最初に、スクリプトの上部にあるNumPyライブラリをインポートします。

step_4_dog_mask.py

import numpy as np
...

次に、前の作業の `+ apply_mask `関数を、この新しいファイルの ` main +`関数の上に追加します。

step_4_dog_mask.py

def apply_mask(face: np.array, mask: np.array) -> np.array:
   """Add the mask to the provided face, and return the face with mask."""
   mask_h, mask_w, _ = mask.shape
   face_h, face_w, _ = face.shape

   # Resize the mask to fit on face
   factor = min(face_h / mask_h, face_w / mask_w)
   new_mask_w = int(factor * mask_w)
   new_mask_h = int(factor * mask_h)
   new_mask_shape = (new_mask_w, new_mask_h)
   resized_mask = cv2.resize(mask, new_mask_shape)

   # Add mask to face - ensure mask is centered
   face_with_mask = face.copy()
   non_white_pixels = (resized_mask < 250).all(axis=2)
   off_h = int((face_h - new_mask_h) / 2)
   off_w = int((face_w - new_mask_w) / 2)
   face_with_mask[off_h: off_h+new_mask_h, off_w: off_w+new_mask_w][non_white_pixels] = \
        resized_mask[non_white_pixels]

   return face_with_mask
...

次に、 `+ main +`関数でこの行を見つけます:

step_4_dog_mask.py

   cap = cv2.VideoCapture(0)

その行の後に次のコードを追加して、犬のマスクをロードします。

step_4_dog_mask.py

   cap = cv2.VideoCapture(0)



   ...

次に、 `+ while +`ループで、次の行を見つけます。

step_4_dog_mask.py

       ret, frame = cap.read()

次の行を追加して、画像の高さと幅を抽出します。

step_4_dog_mask.py

       ret, frame = cap.read()

       ...

次に、バウンディングボックスを描画する `+ main `の行を削除します。 検出された顔を反復処理する「 for +」ループに次の行があります。

step_4_dog_mask.py

       for x, y, w, h in rects:
       ...

       ...

その代わりに、フレームをトリミングするこのコードを追加します。 審美的な目的のために、顔よりわずかに大きい領域をトリミングします。

step_4_dog_mask.py

       for x, y, w, h in rects:
           # crop a frame slightly larger than the face
           y0, y1 = int(y - 0.25*h), int(y + 0.75*h)
           x0, x1 = x, x + w

検出された顔がエッジに近すぎる場合にチェックを導入します。

step_4_dog_mask.py

           # give up if the cropped frame would be out-of-bounds
           if x0 < 0 or y0 < 0 or x1 > frame_w or y1 > frame_h:
               continue

最後に、マスク付きの顔を画像に挿入します。

step_4_dog_mask.py

           # apply mask
           frame[y0: y1, x0: x1] = apply_mask(frame[y0: y1, x0: x1], mask)

スクリプトが次のようになっていることを確認します。

step_4_dog_mask.py

"""Real-time dog filter

Move your face around and a dog filter will be applied to your face if it is not out-of-bounds. With the test frame in focus, hit `q` to exit. Note that typing `q` into your terminal will do nothing.
"""

import numpy as np
import cv2


def apply_mask(face: np.array, mask: np.array) -> np.array:
   """Add the mask to the provided face, and return the face with mask."""
   mask_h, mask_w, _ = mask.shape
   face_h, face_w, _ = face.shape

   # Resize the mask to fit on face
   factor = min(face_h / mask_h, face_w / mask_w)
   new_mask_w = int(factor * mask_w)
   new_mask_h = int(factor * mask_h)
   new_mask_shape = (new_mask_w, new_mask_h)
   resized_mask = cv2.resize(mask, new_mask_shape)

   # Add mask to face - ensure mask is centered
   face_with_mask = face.copy()
   non_white_pixels = (resized_mask < 250).all(axis=2)
   off_h = int((face_h - new_mask_h) / 2)
   off_w = int((face_w - new_mask_w) / 2)
   face_with_mask[off_h: off_h+new_mask_h, off_w: off_w+new_mask_w][non_white_pixels] = \
        resized_mask[non_white_pixels]

   return face_with_mask

def main():
   cap = cv2.VideoCapture(0)

   # load mask
   mask = cv2.imread('assets/dog.png')

   # initialize front face classifier
   cascade = cv2.CascadeClassifier("assets/haarcascade_frontalface_default.xml")

   while(True):
       # Capture frame-by-frame
       ret, frame = cap.read()
       frame_h, frame_w, _ = frame.shape

       # Convert to black-and-white
       gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
       blackwhite = cv2.equalizeHist(gray)

       # Detect faces
       rects = cascade.detectMultiScale(
           blackwhite, scaleFactor=1.3, minNeighbors=4, minSize=(30, 30),
           flags=cv2.CASCADE_SCALE_IMAGE)

       # Add mask to faces
       for x, y, w, h in rects:
           # crop a frame slightly larger than the face
           y0, y1 = int(y - 0.25*h), int(y + 0.75*h)
           x0, x1 = x, x + w

           # give up if the cropped frame would be out-of-bounds
           if x0 < 0 or y0 < 0 or x1 > frame_w or y1 > frame_h:
               continue

           # apply mask
           frame[y0: y1, x0: x1] = apply_mask(frame[y0: y1, x0: x1], mask)

       # Display the resulting frame
       cv2.imshow('frame', frame)
       if cv2.waitKey(1) & 0xFF == ord('q'):
           break

   # When everything done, release the capture
   cap.release()
   cv2.destroyAllWindows()


if __name__ == '__main__':
   main()

ファイルを保存して、エディターを終了します。 その後、スクリプトを実行してください。

python step_4_dog_mask.py

これで、リアルタイムのドッグフィルターが実行されました。 このスクリプトは、画像内の複数の顔でも機能するため、友だちを集めて自動犬化を行うことができます。

image:https://assets.digitalocean.com/articles/python3_dogfilter/g9CiUD1.gif [作業犬フィルターのGIF]

これで、このチュートリアルの最初の主要な目的、つまりSnapchat風のドッグフィルターを作成することが終わりました。 次に、顔の表情を使用して、顔に適用される犬のマスクを決定します。

ステップ5-最小二乗法を使用して基本的な顔の感情分類子を作成する

このセクションでは、感情分類子を作成して、表示された感情に基づいて異なるマスクを適用します。 笑顔の場合、フィルターはコーギーマスクを適用します。 眉をひそめると、パグマスクが適用されます。 途中で、機械学習の概念を理解して議論するための基本である_最小二乗_フレームワークを探ります。

データの処理方法と予測の作成方法を理解するために、まず機械学習モデルについて簡単に説明します。

検討するモデルごとに2つの質問をする必要があります。 現時点では、これらの2つの質問でモデルを区別できます。

  1. 入力:モデルにはどのような情報が与えられますか?

  2. 出力:モデルが予測しようとしているものは何ですか?

大まかに言えば、目標は感情分類のモデルを開発することです。 モデルは以下のとおりです。

  1. 入力:与えられた顔の画像。

  2. 出力:対応する感情を予測します。

model: face -> emotion

使用するアプローチは、_最小二乗_です。ポイントのセットを取得し、最適なラインを見つけます。 次の画像に示されている最適なラインがモデルです。

画像:https://assets.digitalocean.com/articles/python3_dogfilter/lYQDlWs.png [最小二乗]

回線の入力と出力を考えてみましょう。

  1. 入力:指定された `+ x +`座標。

  2. 出力:対応する$ y $座標を予測します。

least squares line: x -> y

感情の分類に最小二乗法を使用するには、入力「+ x 」が顔を表し、出力「 y +」が感情を表す必要があります。

  • + x→ face +: `+ x `に_one_番号を使用する代わりに、 ` x `に値の_vector_を使用します。 したがって、「 x 」は顔の画像を表すことができます。 記事http://alvinwan.com/understanding-least-squares/#ordinary-least-squares[Ordinary Least Squares]は、 ` x +`に値のベクトルを使用できる理由を説明しています。

  • + y→ emotion +:各感情は数字に対応します。 たとえば、「angry」は0、「sad」は1、「happy」は2です。 このようにして、「+ y 」は感情を表すことができます。 ただし、この行は「 y 」値0、1、2を出力するように制約されていません。 可能なy値は無限にあり、1.2、3.5、または10003.42の可能性があります。 これらの「 y +」値をクラスに対応する整数に変換するにはどうすればよいですか? 詳細と説明については、記事http://alvinwan.com/understanding-least-squares/#one-hot-encoding[One-Hot Encoding]を参照してください。

この背景知識を活用して、ベクトル化された画像とワンホットエンコードラベルを使用して、単純な最小二乗分類器を構築します。 次の3つの手順でこれを達成します。

  1. データの前処理:このセクションの冒頭で説明したように、サンプルはベクトルであり、各ベクトルは顔の画像をエンコードします。 ラベルは感情に対応する整数であり、これらのラベルにワンホットエンコーディングを適用します。

  2. モデルの指定とトレーニング:閉形式の最小二乗解、 `+ w ^ * +`を使用します。

  3. モデルを使用して予測を実行します。`+ Xw ^ * + `のargmaxを取得して、予測された感情を取得します。

始めましょう。

最初に、データを含むディレクトリを設定します。

mkdir data

次に、2013年の顔の感情分類https://www.kaggle.com/c/challenges-in-representation-learning-facial-expression-recognition-challenge[competition]から、Pierre-Luc CarrierとAaron CourvilleによってキュレーションされたデータをダウンロードしますKaggleで]。

wget -O data/fer2013.tar https://bitbucket.org/alvinwan/adversarial-examples-in-computer-vision-building-then-fooling/raw/babfe4651f89a398c4b3fdbdd6d7a697c5104cff/fer2013.tar

`+ data +`ディレクトリに移動し、データを解凍します。

cd data
tar -xzf fer2013.tar

次に、最小二乗モデルを実行するスクリプトを作成します。 プロジェクトのルートに移動します。

cd ~/DogFilter

スクリプト用の新しいファイルを作成します。

nano step_5_ls_simple.py

Pythonボイラープレートを追加し、必要なパッケージをインポートします。

step_5_ls_simple.py

"""Train emotion classifier using least squares."""

import numpy as np

def main():
   pass

if __name__ == '__main__':
   main()

次に、データをメモリにロードします。 `+ main `関数の ` pass +`を次のコードに置き換えます:

step_5_ls_simple.py

   # load data
   with np.load('data/fer2013_train.npz') as data:
       X_train, Y_train = data['X'], data['Y']

   with np.load('data/fer2013_test.npz') as data:
       X_test, Y_test = data['X'], data['Y']

ラベルをワンホットエンコードします。 これを行うには、 `+ numpy +`で単位行列を作成し、ラベルのリストを使用してこの行列にインデックスを付けます。

step_5_ls_simple.py

   # one-hot labels
   I = np.eye(6)
   Y_oh_train, Y_oh_test = I[Y_train], I[Y_test]

ここでは、単位行列の「+ i 」番目の行が「 i 」番目のエントリを除いてすべてゼロであるという事実を使用します。 したがって、i番目の行はクラス ` i `のラベルのワンホットエンコーディングです。 さらに、 ` numpy `の高度なインデックスを使用します。ここで、 ` [a、b、c、d] [[1、3]] = [b、d] +`です。

`(X ^ TX)^ {-1} +`の計算は、市販のハードウェアでは時間がかかりすぎます。これは、 ` X ^ TX `が400万を超える値を持つ ` 2304×2304 +`行列であるため、この時間を短縮するためです。最初の100個の機能のみを選択します。 このコードを追加:

step_5_ls_simple.py

...
   # select first 100 dimensions
   A_train, A_test = X_train[:, :100], X_test[:, :100]

次に、次のコードを追加して、閉形式の最小二乗解を評価します。

step_5_ls_simple.py

...
   # train model
   w = np.linalg.inv(A_train.T.dot(A_train)).dot(A_train.T.dot(Y_oh_train))

次に、トレーニングおよび検証セットの評価関数を定義します。 これを `+ main +`関数の前に置きます:

step_5_ls_simple.py

def evaluate(A, Y, w):
   Yhat = np.argmax(A.dot(w), axis=1)
   return np.sum(Yhat == Y) / Y.shape[0]

ラベルを推定するために、各サンプルで内積を取り、 `+ np.argmax +`を使用して最大値のインデックスを取得します。 次に、正しい分類の平均数を計算します。 この最後の数字はあなたの正確さです。

最後に、このコードを `+ main `関数の最後に追加して、作成したばかりの ` evaluate +`関数を使用してトレーニングと検証の精度を計算します。

step_5_ls_simple.py

   # evaluate model
   ols_train_accuracy = evaluate(A_train, Y_train, w)
   print('(ols) Train Accuracy:', ols_train_accuracy)
   ols_test_accuracy = evaluate(A_test, Y_test, w)
   print('(ols) Test Accuracy:', ols_test_accuracy)

スクリプトが次と一致することを再確認します。

step_5_ls_simple.py

"""Train emotion classifier using least squares."""

import numpy as np


def evaluate(A, Y, w):
   Yhat = np.argmax(A.dot(w), axis=1)
   return np.sum(Yhat == Y) / Y.shape[0]

def main():

   # load data
   with np.load('data/fer2013_train.npz') as data:
       X_train, Y_train = data['X'], data['Y']

   with np.load('data/fer2013_test.npz') as data:
       X_test, Y_test = data['X'], data['Y']

   # one-hot labels
   I = np.eye(6)
   Y_oh_train, Y_oh_test = I[Y_train], I[Y_test]

   # select first 100 dimensions
   A_train, A_test = X_train[:, :100], X_test[:, :100]

   # train model
   w = np.linalg.inv(A_train.T.dot(A_train)).dot(A_train.T.dot(Y_oh_train))

   # evaluate model
   ols_train_accuracy = evaluate(A_train, Y_train, w)
   print('(ols) Train Accuracy:', ols_train_accuracy)
   ols_test_accuracy = evaluate(A_test, Y_test, w)
   print('(ols) Test Accuracy:', ols_test_accuracy)


if __name__ == '__main__':
   main()

ファイルを保存し、エディターを終了して、Pythonスクリプトを実行します。

python step_5_ls_simple.py

次の出力が表示されます。

Output(ols) Train Accuracy: 0.4748918316507146
(ols) Test Accuracy: 0.45280545359202934

このモデルでは、列車の精度は47.5%です。 45.3%の精度を得るために、検証セットでこれを繰り返します。 3者間分類問題の場合、45.3%が推測をかなり上回っており、33 \%です。 これが感情検出の最初の分類子であり、次のステップでは、この最小二乗モデルから構築して精度を向上させます。 精度が高いほど、感情ベースの犬のフィルターは、検出された各感情に対して適切な犬のフィルターをより確実に見つけることができます。

ステップ6-入力を特徴付けることによる精度の改善

より表現力豊かなモデルを使用して、精度を高めることができます。 これを達成するために、入力を「機能化」します。

元の画像は、位置( +0、0 +)が赤、( +1、0 +)が茶色などであることを示しています。 特徴のある画像は、画像の左上に犬、中央に人などがいることを示しています。 機能化は強力ですが、その正確な定義はこのチュートリアルの範囲外です。

https://people.eecs.berkeley.edu/%7Ebrecht/papers/07.rah.rec.nips.pdf [ランダム基底ガウス行列を使用した放射基底関数(RBF)カーネルの近似]を使用します。 このチュートリアルでは詳しく説明しません。 代わりに、これを高次の特徴を計算するブラックボックスとして扱います。

前のステップで中断したところから続行します。 前のスクリプトをコピーして、適切な開始点を用意します。

cp step_5_ls_simple.py step_6_ls_simple.py

エディターで新しいファイルを開きます。

nano step_6_ls_simple.py

まず、特徴的なランダムマトリックスを作成します。 繰り返しますが、新しい機能スペースでは100個の機能のみを使用します。

次の行を見つけて、「+ A_train 」と「 A_test +」を定義します。

step_6_ls_simple.py

   # select first 100 dimensions
   A_train, A_test = X_train[:, :100], X_test[:, :100]

`+ A_train `と ` A_test +`のこの定義のすぐ上に、ランダムな特徴マトリックスを追加します。

step_6_ls_simple.py

   # select first 100 dimensions
   A_train, A_test = X_train[:, :100], X_test[:, :100]  ...

次に、「+ A_train 」と「 A_test +」の定義を置き換えます。 このランダムな特徴化を使用して、_design_マトリックスと呼ばれるマトリックスを再定義します。

step_6_ls_simple.py

   A_train, A_test = X_train.dot(W), X_test.dot(W)

ファイルを保存して、スクリプトを実行します。

python step_6_ls_simple.py

次の出力が表示されます。

Output(ols) Train Accuracy: 0.584174642717
(ols) Test Accuracy: 0.584425799685

この機能により、58.4%の列車精度と58.4%の検証精度が実現し、検証結果が13.1%改善されました。 Xマトリックスを「100 x 100」にトリミングしましたが、100の選択は任意です。 また、「+ X 」行列を「+1000 x 1000」または「50 x 50」にトリミングすることもできます。 「+ x 」の次元が「 d x d 」であるとします。 Xを「 d x d 」に再トリミングし、新しいモデルを再計算することで、「 d +」の値をさらにテストできます。

「+ d 」の値をさらに試してみると、テストの精度が61.7%に4.3%向上していることがわかります。 次の図では、 ` d `を変更する際の新しい分類子のパフォーマンスを考慮しています。 直感的に、「 d +」が増加すると、元のデータをますます使用するため、精度も向上するはずです。 ただし、グラフはバラ色の絵を描くのではなく、負の傾向を示します。

image:https://assets.digitalocean.com/articles/python3_dogfilter/cfKxdJ9.png [特徴化された通常の最小二乗のパフォーマンス]

データを保持するにつれて、トレーニングの精度と検証の精度のギャップも大きくなります。 これは、モデルがすべてのデータに一般化できない表現を学習している_overfitting_の明確な証拠です。 過剰適合に対処するために、複雑なモデルにペナルティを課してモデルを「正規化」します。

通常の最小二乗目的関数を正則化用語で修正し、新しい目的を与えます。 新しい目的関数は「リッジ回帰」と呼ばれ、次のようになります。

min_w |Aw- y|^2 + lambda |w|^2

この方程式では、「+ lambda 」は調整可能なハイパーパラメーターです。 ` lambda = 0 `を方程式に代入すると、リッジ回帰が最小二乗になります。 方程式に「 lambda = infinity 」を接続すると、ゼロ以外の「 w 」が無限の損失を招くため、最良の「 w +」はゼロでなければなりません。 結局のところ、この目的は閉じた形式のソリューションももたらします:

w^* = (A^TA + lambda I)^{-1}A^Ty

引き続き特徴のあるサンプルを使用して、モデルを再度トレーニングし、再評価します。

エディターで再度 `+ step_6_ls_simple.py +`を開きます:

nano step_6_ls_simple.py

今回は、新しいフィーチャスペースの次元を「+ d = 1000 」に増やします。 次のコードブロックに示すように、「 d 」の値を「+100」から「1000」に変更します。

step_6_ls_simple.py

...
   d =
   W = np.random.normal(size=(X_train.shape[1], d))
...

次に、 `+ lambda = 10 ^ {10} `の正則化を使用してリッジ回帰を適用します。 ` w +`を定義する行を次の2行に置き換えます。

step_6_ls_simple.py

...
   # train model

   1e10 * I

次に、このブロックを見つけます。

step_6_ls_simple.py

...
 ols_train_accuracy = evaluate(A_train, Y_train, w)
 print('(ols) Train Accuracy:', ols_train_accuracy)
 ols_test_accuracy = evaluate(A_test, Y_test, w)
 print('(ols) Test Accuracy:', ols_test_accuracy)

次のものに置き換えます。

step_6_ls_simple.py

...

 print('(ridge) Train Accuracy:', evaluate(A_train, Y_train, w))
 print('(ridge) Test Accuracy:', evaluate(A_test, Y_test, w))

完成したスクリプトは次のようになります。

step_6_ls_simple.py

"""Train emotion classifier using least squares."""

import numpy as np

def evaluate(A, Y, w):
   Yhat = np.argmax(A.dot(w), axis=1)
   return np.sum(Yhat == Y) / Y.shape[0]

def main():
   # load data
   with np.load('data/fer2013_train.npz') as data:
       X_train, Y_train = data['X'], data['Y']

   with np.load('data/fer2013_test.npz') as data:
       X_test, Y_test = data['X'], data['Y']

   # one-hot labels
   I = np.eye(6)
   Y_oh_train, Y_oh_test = I[Y_train], I[Y_test]
   d = 1000
   W = np.random.normal(size=(X_train.shape[1], d))
   # select first 100 dimensions
   A_train, A_test = X_train.dot(W), X_test.dot(W)

   # train model
   I = np.eye(A_train.shape[1])
   w = np.linalg.inv(A_train.T.dot(A_train) + 1e10 * I).dot(A_train.T.dot(Y_oh_train))

   # evaluate model
   print('(ridge) Train Accuracy:', evaluate(A_train, Y_train, w))
   print('(ridge) Test Accuracy:', evaluate(A_test, Y_test, w))

if __name__ == '__main__':
   main()

ファイルを保存し、エディターを終了して、スクリプトを実行します。

python step_6_ls_simple.py

次の出力が表示されます。

Output(ridge) Train Accuracy: 0.651173462698
(ridge) Test Accuracy: 0.622181436812

列車の精度が65.1%に低下すると、検証の精度が0.4%から62.2%にさらに改善されます。 多数の異なる「+ d +」でもう一度再評価すると、リッジ回帰のトレーニングと検証の精度のギャップが小さくなります。 言い換えれば、リッジ回帰はオーバーフィッティングの影響を受けにくいということです。

image:https://assets.digitalocean.com/articles/python3_dogfilter/gzGBSGo.png [特徴化されたolsおよびリッジ回帰のパフォーマンス]

これらの追加の拡張機能を備えた最小二乗法のベースラインパフォーマンスは、かなりよく機能します。 トレーニング時間と推論時間を合わせて、最高の結果を得るために20秒以内で完了します。 次のセクションでは、さらに複雑なモデルについて説明します。

ステップ7-PyTorchで畳み込みニューラルネットワークを使用して顔感情分類器を構築する

このセクションでは、最小二乗法の代わりにニューラルネットワークを使用して、2番目の感情分類器を構築します。 繰り返しますが、私たちの目標は、入力として顔を受け入れ、感情を出力するモデルを作成することです。 最終的に、この分類子は適用する犬マスクを決定します。

簡単なニューラルネットワークの視覚化と紹介については、http://alvinwan.com/understanding-neural-networks/ [Understanding Neural Networks]の記事を参照してください。 ここでは、_PyTorch_と呼ばれる深層学習ライブラリを使用します。 広く使用されている多くのディープラーニングライブラリがあり、それぞれにさまざまな長所と短所があります。 PyTorchは、始めるのに特に適した場所です。 このニューラルネットワーク分類器を実装するには、最小二乗分類器で行ったように、再び3つの手順を実行します。

  1. データの前処理:ワンホットエンコーディングを適用してから、PyTorch抽象化を適用します。

  2. モデルの指定とトレーニング:PyTorchレイヤーを使用してニューラルネットワークをセットアップします。 最適化ハイパーパラメーターを定義し、確率的勾配降下を実行します。

  3. モデルを使用して予測を実行します。ニューラルネットワークを評価します。

`+ step_7_fer_simple.py +`という名前の新しいファイルを作成します

nano step_7_fer_simple.py

必要なユーティリティをインポートし、データを保持するPython classを作成します。 ここでのデータ処理では、トレインデータセットとテストデータセットを作成します。 これを行うには、PyTorchの `+ Dataset +`インターフェースを実装します。これにより、PyTorchの組み込みのデータパイプラインを読み込んで、顔感情認識データセットに使用できます。

step_7_fer_simple.py

from torch.utils.data import Dataset
from torch.autograd import Variable
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import numpy as np
import torch
import cv2
import argparse


class Fer2013Dataset(Dataset):
   """Face Emotion Recognition dataset.

   Utility for loading FER into PyTorch. Dataset curated by Pierre-Luc Carrier
   and Aaron Courville in 2013.

   Each sample is 1 x 1 x 48 x 48, and each label is a scalar.
   """
   pass

+ Fer2013 Dataset`クラスの + pass`プレースホルダーを削除します。 その代わりに、データホルダーを初期化する関数を追加します。

step_7_fer_simple.py

   def __init__(self, path: str):
       """
       Args:
           path: Path to `.np` file containing sample nxd and label nx1
       """
       with np.load(path) as data:
           self._samples = data['X']
           self._labels = data['Y']
       self._samples = self._samples.reshape((-1, 1, 48, 48))

       self.X = Variable(torch.from_numpy(self._samples)).float()
       self.Y = Variable(torch.from_numpy(self._labels)).float()
...

この機能は、サンプルとラベルをロードすることから始まります。 次に、データをPyTorchデータ構造にラップします。

PyTorchが期待する `+ Dataset `インターフェースを実装するために必要なため、 ` init `関数の直後に、 ` len +`関数を追加します。

step_7_fer_simple.py

...
   def __len__(self):
       return len(self._labels)

最後に、サンプルとラベルを含むhttps://www.digitalocean.com/community/tutorials/understanding-dictionaries-in-python-3[dictionary]を返す `+ getitem +`メソッドを追加します。

step_7_fer_simple.py

   def __getitem__(self, idx):
       return {'image': self._samples[idx], 'label': self._labels[idx]}

ファイルが次のようになっていることを再確認します。

step_7_fer_simple.py

from torch.utils.data import Dataset
from torch.autograd import Variable
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import numpy as np
import torch
import cv2
import argparse


class Fer2013Dataset(Dataset):
   """Face Emotion Recognition dataset.
   Utility for loading FER into PyTorch. Dataset curated by Pierre-Luc Carrier
   and Aaron Courville in 2013.
   Each sample is 1 x 1 x 48 x 48, and each label is a scalar.
   """

   def __init__(self, path: str):
       """
       Args:
           path: Path to `.np` file containing sample nxd and label nx1
       """
       with np.load(path) as data:
           self._samples = data['X']
           self._labels = data['Y']
       self._samples = self._samples.reshape((-1, 1, 48, 48))

       self.X = Variable(torch.from_numpy(self._samples)).float()
       self.Y = Variable(torch.from_numpy(self._labels)).float()

   def __len__(self):
       return len(self._labels)

   def __getitem__(self, idx):
       return {'image': self._samples[idx], 'label': self._labels[idx]}

次に、 `+ Fer2013Dataset `データセットをロードします。 ファイルの末尾の ` Fer2013Dataset +`クラスの後に次のコードを追加します。

step_7_fer_simple.py

trainset = Fer2013Dataset('data/fer2013_train.npz')
trainloader = torch.utils.data.DataLoader(trainset, batch_size=32, shuffle=True)

testset = Fer2013Dataset('data/fer2013_test.npz')
testloader = torch.utils.data.DataLoader(testset, batch_size=32, shuffle=False)

このコードは、作成した `+ Fer2013Dataset `クラスを使用してデータセットを初期化します。 次に、トレインと検証セットについて、データセットを ` DataLoader +`でラップします。 これにより、データセットは後で使用できるように反復可能に変換されます。

健全性チェックとして、データセットユーティリティが機能していることを確認します。 `+ DataLoader +`を使用してサンプルデータセットローダーを作成し、そのローダーの最初の要素を出力します。 ファイルの最後に次を追加します。

step_7_fer_simple.py

if __name__ == '__main__':
   loader = torch.utils.data.DataLoader(trainset, batch_size=2, shuffle=False)
   print(next(iter(loader)))

完成したスクリプトが次のようになっていることを確認します。

step_7_fer_simple.py

from torch.utils.data import Dataset
from torch.autograd import Variable
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import numpy as np
import torch
import cv2
import argparse


class Fer2013Dataset(Dataset):
   """Face Emotion Recognition dataset.
   Utility for loading FER into PyTorch. Dataset curated by Pierre-Luc Carrier
   and Aaron Courville in 2013.
   Each sample is 1 x 1 x 48 x 48, and each label is a scalar.
   """

   def __init__(self, path: str):
       """
       Args:
           path: Path to `.np` file containing sample nxd and label nx1
       """
       with np.load(path) as data:
           self._samples = data['X']
           self._labels = data['Y']
       self._samples = self._samples.reshape((-1, 1, 48, 48))

       self.X = Variable(torch.from_numpy(self._samples)).float()
       self.Y = Variable(torch.from_numpy(self._labels)).float()

   def __len__(self):
       return len(self._labels)

   def __getitem__(self, idx):
       return {'image': self._samples[idx], 'label': self._labels[idx]}

trainset = Fer2013Dataset('data/fer2013_train.npz')
trainloader = torch.utils.data.DataLoader(trainset, batch_size=32, shuffle=True)

testset = Fer2013Dataset('data/fer2013_test.npz')
testloader = torch.utils.data.DataLoader(testset, batch_size=32, shuffle=False)

if __name__ == '__main__':
   loader = torch.utils.data.DataLoader(trainset, batch_size=2, shuffle=False)
   print(next(iter(loader)))

エディターを終了し、スクリプトを実行します。

python step_7_fer_simple.py

これにより、次のhttps://en.wikipedia.org/wiki/Tensor[tensors]のペアが出力されます。 データパイプラインは、2つのサンプルと2つのラベルを出力します。 これは、データパイプラインが起動し、準備ができていることを示しています。

Output{'image':
(0 ,0 ,.,.) =
  24   32   36  ...   173  172  173
  25   34   29  ...   173  172  173
  26   29   25  ...   172  172  174
     ...         ⋱        ...
 159  185  157  ...   157  156  153
 136  157  187  ...   152  152  150
 145  130  161  ...   142  143  142
    ⋮

(1 ,0 ,.,.) =
  20   17   19  ...   187  176  162
  22   17   17  ...   195  180  171
  17   17   18  ...   203  193  175
     ...         ⋱        ...
   1    1    1  ...   106  115  119
   2    2    1  ...   103  111  119
   2    2    2  ...    99  107  118
[torch.LongTensor of size 2x1x48x48]
, 'label':
1
1
[torch.LongTensor of size 2]
}

データパイプラインが機能することを確認したら、 `+ step_7_fer_simple.py `に戻ってニューラルネットワークとオプティマイザーを追加します。 ` step_7_fer_simple.py +`を開きます。

nano step_7_fer_simple.py

最初に、前の反復で追加した最後の3行を削除します。

step_7_fer_simple.py

# Delete all three lines

その代わりに、3つの畳み込み層とそれに続く3つの完全に接続された層を含むPyTorchニューラルネットワークを定義します。 これを既存のスクリプトの最後に追加します。

step_7_fer_simple.py

class Net(nn.Module):
   def __init__(self):
       super(Net, self).__init__()
       self.conv1 = nn.Conv2d(1, 6, 5)
       self.pool = nn.MaxPool2d(2, 2)
       self.conv2 = nn.Conv2d(6, 6, 3)
       self.conv3 = nn.Conv2d(6, 16, 3)
       self.fc1 = nn.Linear(16 * 4 * 4, 120)
       self.fc2 = nn.Linear(120, 48)
       self.fc3 = nn.Linear(48, 3)

   def forward(self, x):
       x = self.pool(F.relu(self.conv1(x)))
       x = self.pool(F.relu(self.conv2(x)))
       x = self.pool(F.relu(self.conv3(x)))
       x = x.view(-1, 16 * 4 * 4)
       x = F.relu(self.fc1(x))
       x = F.relu(self.fc2(x))
       x = self.fc3(x)
       return x

次に、ニューラルネットワークを初期化し、損失関数を定義し、次のコードをスクリプトの最後に追加して、最適化ハイパーパラメーターを定義します。

step_7_fer_simple.py

net = Net().float()
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)

2つの_エポック_のトレーニングを行います。 ここでは、_epoch_を、すべてのトレーニングサンプルが1回だけ使用されたトレーニングの反復として定義します。

最初に、データセットローダーから「+ image」と「+ label」を抽出し、それぞれをPyTorch「+ Variable」にラップします。 次に、フォワードパスを実行してから、損失とニューラルネットワークを逆伝播します。 それを行うには、スクリプトの最後に次のコードを追加します。

step_7_fer_simple.py

for epoch in range(2):  # loop over the dataset multiple times

   running_loss = 0.0
   for i, data in enumerate(trainloader, 0):
       inputs = Variable(data['image'].float())
       labels = Variable(data['label'].long())
       optimizer.zero_grad()

       # forward + backward + optimize
       outputs = net(inputs)
       loss = criterion(outputs, labels)
       loss.backward()
       optimizer.step()

       # print statistics
       running_loss += loss.data[0]
       if i % 100 == 0:
           print('[%d, %5d] loss: %.3f' % (epoch, i, running_loss / (i + 1)))

スクリプトは次のようになります。

step_7_fer_simple.py

from torch.utils.data import Dataset
from torch.autograd import Variable
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import numpy as np
import torch
import cv2
import argparse


class Fer2013Dataset(Dataset):
   """Face Emotion Recognition dataset.

   Utility for loading FER into PyTorch. Dataset curated by Pierre-Luc Carrier
   and Aaron Courville in 2013.

   Each sample is 1 x 1 x 48 x 48, and each label is a scalar.
   """
   def __init__(self, path: str):
       """
       Args:
           path: Path to `.np` file containing sample nxd and label nx1
       """
       with np.load(path) as data:
           self._samples = data['X']
           self._labels = data['Y']
       self._samples = self._samples.reshape((-1, 1, 48, 48))

       self.X = Variable(torch.from_numpy(self._samples)).float()
       self.Y = Variable(torch.from_numpy(self._labels)).float()

   def __len__(self):
       return len(self._labels)


   def __getitem__(self, idx):
       return {'image': self._samples[idx], 'label': self._labels[idx]}


trainset = Fer2013Dataset('data/fer2013_train.npz')
trainloader = torch.utils.data.DataLoader(trainset, batch_size=32, shuffle=True)

testset = Fer2013Dataset('data/fer2013_test.npz')
testloader = torch.utils.data.DataLoader(testset, batch_size=32, shuffle=False)


class Net(nn.Module):
   def __init__(self):
       super(Net, self).__init__()
       self.conv1 = nn.Conv2d(1, 6, 5)
       self.pool = nn.MaxPool2d(2, 2)
       self.conv2 = nn.Conv2d(6, 6, 3)
       self.conv3 = nn.Conv2d(6, 16, 3)
       self.fc1 = nn.Linear(16 * 4 * 4, 120)
       self.fc2 = nn.Linear(120, 48)
       self.fc3 = nn.Linear(48, 3)

   def forward(self, x):
       x = self.pool(F.relu(self.conv1(x)))
       x = self.pool(F.relu(self.conv2(x)))
       x = self.pool(F.relu(self.conv3(x)))
       x = x.view(-1, 16 * 4 * 4)
       x = F.relu(self.fc1(x))
       x = F.relu(self.fc2(x))
       x = self.fc3(x)
       return x

net = Net().float()
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)


for epoch in range(2):  # loop over the dataset multiple times

   running_loss = 0.0
   for i, data in enumerate(trainloader, 0):
       inputs = Variable(data['image'].float())
       labels = Variable(data['label'].long())
       optimizer.zero_grad()

       # forward + backward + optimize
       outputs = net(inputs)
       loss = criterion(outputs, labels)
       loss.backward()
       optimizer.step()

       # print statistics
       running_loss += loss.data[0]
       if i % 100 == 0:
           print('[%d, %5d] loss: %.3f' % (epoch, i, running_loss / (i + 1)))

コードを確認したら、ファイルを保存してエディターを終了します。 次に、この概念実証トレーニングを開始します。

python step_7_fer_simple.py

ニューラルネットワークのトレーニングとして、次のような出力が表示されます。

Output[0,     0] loss: 1.094
[0,   100] loss: 1.049
[0,   200] loss: 1.009
[0,   300] loss: 0.963
[0,   400] loss: 0.935
[1,     0] loss: 0.760
[1,   100] loss: 0.768
[1,   200] loss: 0.775
[1,   300] loss: 0.776
[1,   400] loss: 0.767

その後、他の多くのPyTorchユーティリティを使用してこのスクリプトを拡張し、モデルの保存と読み込み、トレーニングと検証の精度の出力、学習率のスケジュールの微調整などを行うことができます。 0.01の学習率と0.9の運動量で20エポックのトレーニングを行った後、当社のニューラルネットワークは87.9%の訓練精度と75.5%の検証精度を達成し、これまでで最も成功した最小二乗アプローチに対して6.8%の改善を達成しました。 。 これらの追加機能を新しいスクリプトに追加します。

ライブカメラフィードが使用する最終的な顔の感情検出器を保持する新しいファイルを作成します。 このスクリプトには、上記のコードとコマンドラインインターフェイス、および後で使用するインポートしやすいバージョンのコードが含まれています。 さらに、より正確なモデルのために、事前に調整されたハイパーパラメーターが含まれています。

nano step_7_fer.py

次のインポートから始めます。 これは以前のファイルと一致しますが、さらにOpenCVを `+ import cv2。+`として含めます

step_7_fer.py

from torch.utils.data import Dataset
from torch.autograd import Variable
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import numpy as np
import torch
import cv2
import argparse

これらのインポートの直下で、 `+ step_7_fer_simple.py +`のコードを再利用してニューラルネットワークを定義します。

step_7_fer.py

class Net(nn.Module):
   def __init__(self):
       super(Net, self).__init__()
       self.conv1 = nn.Conv2d(1, 6, 5)
       self.pool = nn.MaxPool2d(2, 2)
       self.conv2 = nn.Conv2d(6, 6, 3)
       self.conv3 = nn.Conv2d(6, 16, 3)
       self.fc1 = nn.Linear(16 * 4 * 4, 120)
       self.fc2 = nn.Linear(120, 48)
       self.fc3 = nn.Linear(48, 3)

   def forward(self, x):
       x = self.pool(F.relu(self.conv1(x)))
       x = self.pool(F.relu(self.conv2(x)))
       x = self.pool(F.relu(self.conv3(x)))
       x = x.view(-1, 16 * 4 * 4)
       x = F.relu(self.fc1(x))
       x = F.relu(self.fc2(x))
       x = self.fc3(x)
       return x

繰り返しになりますが、 `+ step_7_fer_simple.py +`のFace Emotion Recognitionデータセットのコードを再利用し、このファイルに追加します。

step_7_fer.py

class Fer2013Dataset(Dataset):
   """Face Emotion Recognition dataset.
   Utility for loading FER into PyTorch. Dataset curated by Pierre-Luc Carrier
   and Aaron Courville in 2013.
   Each sample is 1 x 1 x 48 x 48, and each label is a scalar.
   """

   def __init__(self, path: str):
       """
       Args:
           path: Path to `.np` file containing sample nxd and label nx1
       """
       with np.load(path) as data:
           self._samples = data['X']
           self._labels = data['Y']
       self._samples = self._samples.reshape((-1, 1, 48, 48))

       self.X = Variable(torch.from_numpy(self._samples)).float()
       self.Y = Variable(torch.from_numpy(self._labels)).float()

   def __len__(self):
       return len(self._labels)

   def __getitem__(self, idx):
       return {'image': self._samples[idx], 'label': self._labels[idx]}

次に、いくつかのユーティリティを定義して、ニューラルネットワークのパフォーマンスを評価します。 最初に、ニューラルネットワークの予測感情を単一の画像の真の感情と比較する「+ evaluate +」関数を追加します。

step_7_fer.py

def evaluate(outputs: Variable, labels: Variable, normalized: bool=True) -> float:
   """Evaluate neural network outputs against non-one-hotted labels."""
   Y = labels.data.numpy()
   Yhat = np.argmax(outputs.data.numpy(), axis=1)
   denom = Y.shape[0] if normalized else 1
   return float(np.sum(Yhat == Y) / denom)

次に、最初の関数をすべての画像に適用する `+ batch_evaluate +`という関数を追加します。

step_7_fer.py

def batch_evaluate(net: Net, dataset: Dataset, batch_size: int=500) -> float:
   """Evaluate neural network in batches, if dataset is too large."""
   score = 0.0
   n = dataset.X.shape[0]
   for i in range(0, n, batch_size):
       x = dataset.X[i: i + batch_size]
       y = dataset.Y[i: i + batch_size]
       score += evaluate(net(x), y, False)
   return score / n

次に、事前学習済みのモデルを使用して、画像を取り込んで予測感情を出力する「+ get_image_to_emotion_predictor +」という関数を定義します。

step_7_fer.py

def get_image_to_emotion_predictor(model_path='assets/model_best.pth'):
   """Returns predictor, from image to emotion index."""
   net = Net().float()
   pretrained_model = torch.load(model_path)
   net.load_state_dict(pretrained_model['state_dict'])

   def predictor(image: np.array):
       """Translates images into emotion indices."""
       if image.shape[2] > 1:
           image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
       frame = cv2.resize(image, (48, 48)).reshape((1, 1, 48, 48))
       X = Variable(torch.from_numpy(frame)).float()
       return np.argmax(net(X).data.numpy(), axis=1)[0]
   return predictor

最後に、次のコードを追加して、他のユーティリティを活用するための `+ main +`関数を定義します:

step_7_fer.py

def main():
   trainset = Fer2013Dataset('data/fer2013_train.npz')
   testset = Fer2013Dataset('data/fer2013_test.npz')
   net = Net().float()

   pretrained_model = torch.load("assets/model_best.pth")
   net.load_state_dict(pretrained_model['state_dict'])

   train_acc = batch_evaluate(net, trainset, batch_size=500)
   print('Training accuracy: %.3f' % train_acc)
   test_acc = batch_evaluate(net, testset, batch_size=500)
   print('Validation accuracy: %.3f' % test_acc)


if __name__ == '__main__':
   main()

これにより、事前学習済みのニューラルネットワークが読み込まれ、提供された顔感情認識データセットでのパフォーマンスが評価されます。 具体的には、このスクリプトは、トレーニングに使用した画像の正確性と、テスト目的のために別の画像セットを出力します。

ファイルが次と一致することを再確認します。

step_7_fer.py

from torch.utils.data import Dataset
from torch.autograd import Variable
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import numpy as np
import torch
import cv2
import argparse

class Net(nn.Module):
   def __init__(self):
       super(Net, self).__init__()
       self.conv1 = nn.Conv2d(1, 6, 5)
       self.pool = nn.MaxPool2d(2, 2)
       self.conv2 = nn.Conv2d(6, 6, 3)
       self.conv3 = nn.Conv2d(6, 16, 3)
       self.fc1 = nn.Linear(16 * 4 * 4, 120)
       self.fc2 = nn.Linear(120, 48)
       self.fc3 = nn.Linear(48, 3)

   def forward(self, x):
       x = self.pool(F.relu(self.conv1(x)))
       x = self.pool(F.relu(self.conv2(x)))
       x = self.pool(F.relu(self.conv3(x)))
       x = x.view(-1, 16 * 4 * 4)
       x = F.relu(self.fc1(x))
       x = F.relu(self.fc2(x))
       x = self.fc3(x)
       return x


class Fer2013Dataset(Dataset):
   """Face Emotion Recognition dataset.
   Utility for loading FER into PyTorch. Dataset curated by Pierre-Luc Carrier
   and Aaron Courville in 2013.
   Each sample is 1 x 1 x 48 x 48, and each label is a scalar.
   """

   def __init__(self, path: str):
       """
       Args:
           path: Path to `.np` file containing sample nxd and label nx1
       """
       with np.load(path) as data:
           self._samples = data['X']
           self._labels = data['Y']
       self._samples = self._samples.reshape((-1, 1, 48, 48))

       self.X = Variable(torch.from_numpy(self._samples)).float()
       self.Y = Variable(torch.from_numpy(self._labels)).float()

   def __len__(self):
       return len(self._labels)

   def __getitem__(self, idx):
       return {'image': self._samples[idx], 'label': self._labels[idx]}


def evaluate(outputs: Variable, labels: Variable, normalized: bool=True) -> float:
   """Evaluate neural network outputs against non-one-hotted labels."""
   Y = labels.data.numpy()
   Yhat = np.argmax(outputs.data.numpy(), axis=1)
   denom = Y.shape[0] if normalized else 1
   return float(np.sum(Yhat == Y) / denom)


def batch_evaluate(net: Net, dataset: Dataset, batch_size: int=500) -> float:
   """Evaluate neural network in batches, if dataset is too large."""
   score = 0.0
   n = dataset.X.shape[0]
   for i in range(0, n, batch_size):
       x = dataset.X[i: i + batch_size]
       y = dataset.Y[i: i + batch_size]
       score += evaluate(net(x), y, False)
   return score / n


def get_image_to_emotion_predictor(model_path='assets/model_best.pth'):
   """Returns predictor, from image to emotion index."""
   net = Net().float()
   pretrained_model = torch.load(model_path)
   net.load_state_dict(pretrained_model['state_dict'])

   def predictor(image: np.array):
       """Translates images into emotion indices."""
       if image.shape[2] > 1:
           image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
       frame = cv2.resize(image, (48, 48)).reshape((1, 1, 48, 48))
       X = Variable(torch.from_numpy(frame)).float()
       return np.argmax(net(X).data.numpy(), axis=1)[0]
   return predictor


def main():
   trainset = Fer2013Dataset('data/fer2013_train.npz')
   testset = Fer2013Dataset('data/fer2013_test.npz')
   net = Net().float()

   pretrained_model = torch.load("assets/model_best.pth")
   net.load_state_dict(pretrained_model['state_dict'])

   train_acc = batch_evaluate(net, trainset, batch_size=500)
   print('Training accuracy: %.3f' % train_acc)
   test_acc = batch_evaluate(net, testset, batch_size=500)
   print('Validation accuracy: %.3f' % test_acc)


if __name__ == '__main__':
   main(

ファイルを保存して、エディターを終了します。

前と同様に、顔検出器を使用して、事前に訓練されたモデルパラメーターをダウンロードし、次のコマンドを使用して `+ assets +`フォルダーに保存します。

wget -O assets/model_best.pth https://github.com/alvinwan/emotion-based-dog-filter/raw/master/src/assets/model_best.pth

スクリプトを実行して、事前学習済みモデルを使用および評価します。

python step_7_fer.py

これは以下を出力します。

OutputTraining accuracy: 0.879
Validation accuracy: 0.755

この時点で、非常に正確な顔感情分類器を作成しました。 本質的に、私たちのモデルは、幸せで、悲しく、驚いた顔の10分の8を正しく明確にすることができます。 これはかなり良いモデルであるため、この顔感情分類器を使用して、どの犬マスクを顔に適用するかを決定できます。

ステップ8-感情ベースのドッグフィルターの仕上げ

真新しい顔感情分類器を統合する前に、動物マスクを選択する必要があります。 ダルメーションマスクとシープドッグマスクを使用します。

image:https://assets.digitalocean.com/articles/python3_dogfilter/HveFdkg.png [ダルメーションマスク] + image:https://assets.digitalocean.com/articles/python3_dogfilter/E9ax7PI.png [シープドッグマスク]

これらのコマンドを実行して、両方のマスクを「+ assets」フォルダーにダウンロードします。

wget -O assets/dalmation.png https://assets.digitalocean.com/articles/python3_dogfilter/E9ax7PI.png  # dalmation
wget -O assets/sheepdog.png https://assets.digitalocean.com/articles/python3_dogfilter/HveFdkg.png  # sheepdog

次に、フィルターでマスクを使用します。 `+ step_4_dog_mask.py +`ファイルを複製することから始めます:

cp step_4_dog_mask.py step_8_dog_emotion_mask.py

新しいPythonスクリプトを開きます。

nano step_8_dog_emotion_mask.py

スクリプトの先頭に新しい行を挿入して、感情予測子をインポートします。

step_8_dog_emotion_mask.py

from step_7_fer import get_image_to_emotion_predictor
...

次に、 `+ main()+`関数で、次の行を見つけます。

step_8_dog_emotion_mask.py

   mask = cv2.imread('assets/dog.png')

以下に置き換えて、新しいマスクをロードし、すべてのマスクをタプルに集約します。

step_8_dog_emotion_mask.py

   mask0 = cv2.imread('assets/dog.png')
   mask1 = cv2.imread('assets/dalmation.png')
   mask2 = cv2.imread('assets/sheepdog.png')
   masks = (mask0, mask1, mask2)

改行を追加してから、このコードを追加して感情予測を作成します。

step_8_dog_emotion_mask.py

   # get emotion predictor
   predictor = get_image_to_emotion_predictor()

これで、 `+ main +`関数は次と一致するはずです。

step_8_dog_emotion_mask.py

def main():
   cap = cv2.VideoCapture(0)

   # load mask
   mask0 = cv2.imread('assets/dog.png')
   mask1 = cv2.imread('assets/dalmation.png')
   mask2 = cv2.imread('assets/sheepdog.png')
   masks = (mask0, mask1, mask2)

   # get emotion predictor
   predictor = get_image_to_emotion_predictor()

   # initialize front face classifier
   ...

次に、次の行を見つけます。

step_8_dog_emotion_mask.py

           # apply mask
           frame[y0: y1, x0: x1] = apply_mask(frame[y0: y1, x0: x1], mask)

「+#apply mask +」行の下に次の行を挿入して、予測子を使用して適切なマスクを選択します。

step_8_dog_emotion_mask.py

           # apply mask

           frame[y0: y1, x0: x1] = apply_mask(frame[y0: y1, x0: x1], mask)

完成したファイルは次のようになります。

step_8_dog_emotion_mask.py

"""Test for face detection"""

from step_7_fer import get_image_to_emotion_predictor
import numpy as np
import cv2

def apply_mask(face: np.array, mask: np.array) -> np.array:
   """Add the mask to the provided face, and return the face with mask."""
   mask_h, mask_w, _ = mask.shape
   face_h, face_w, _ = face.shape

   # Resize the mask to fit on face
   factor = min(face_h / mask_h, face_w / mask_w)
   new_mask_w = int(factor * mask_w)
   new_mask_h = int(factor * mask_h)
   new_mask_shape = (new_mask_w, new_mask_h)
   resized_mask = cv2.resize(mask, new_mask_shape)

   # Add mask to face - ensure mask is centered
   face_with_mask = face.copy()
   non_white_pixels = (resized_mask < 250).all(axis=2)
   off_h = int((face_h - new_mask_h) / 2)
   off_w = int((face_w - new_mask_w) / 2)
   face_with_mask[off_h: off_h+new_mask_h, off_w: off_w+new_mask_w][non_white_pixels] = \
        resized_mask[non_white_pixels]

   return face_with_mask

def main():

   cap = cv2.VideoCapture(0)
   # load mask
   mask0 = cv2.imread('assets/dog.png')
   mask1 = cv2.imread('assets/dalmation.png')
   mask2 = cv2.imread('assets/sheepdog.png')
   masks = (mask0, mask1, mask2)

   # get emotion predictor
   predictor = get_image_to_emotion_predictor()

   # initialize front face classifier
   cascade = cv2.CascadeClassifier("assets/haarcascade_frontalface_default.xml")

   while True:
       # Capture frame-by-frame
       ret, frame = cap.read()
       frame_h, frame_w, _ = frame.shape

       # Convert to black-and-white
       gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
       blackwhite = cv2.equalizeHist(gray)

       rects = cascade.detectMultiScale(
           blackwhite, scaleFactor=1.3, minNeighbors=4, minSize=(30, 30),
           flags=cv2.CASCADE_SCALE_IMAGE)

       for x, y, w, h in rects:
           # crop a frame slightly larger than the face
           y0, y1 = int(y - 0.25*h), int(y + 0.75*h)
           x0, x1 = x, x + w
           # give up if the cropped frame would be out-of-bounds
           if x0 < 0 or y0 < 0 or x1 > frame_w or y1 > frame_h:
               continue
           # apply mask
           mask = masks[predictor(frame[y:y+h, x: x+w])]
           frame[y0: y1, x0: x1] = apply_mask(frame[y0: y1, x0: x1], mask)

       # Display the resulting frame
       cv2.imshow('frame', frame)
       if cv2.waitKey(1) & 0xFF == ord('q'):
           break

   cap.release()
   cv2.destroyAllWindows()

if __name__ == '__main__':
   main()

エディターを保存して終了します。 スクリプトを起動します。

python step_8_dog_emotion_mask.py

今すぐ試してみてください! 笑顔は「幸せ」として登録され、元の犬が表示されます。 中立的な顔または眉をひそめている人は「悲しい」として登録し、ダルメシアンを譲ります。 「驚き」の顔、素敵な大きなあごのドロップは、牧羊犬をもたらします。

画像:https://assets.digitalocean.com/articles/python3_dogfilter/JPavHJl.gif [感情に基づく犬のフィルターのGIF]

これで、感情に基づく犬のフィルターとコンピュータービジョンへの進出が終わりました。

結論

このチュートリアルでは、コンピュータービジョンを使用して顔検出器と犬のフィルターを作成し、機械学習モデルを使用して、検出された感情に基づいてマスクを適用します。

機械学習は広く適用可能です。 ただし、機械学習を適用する際には、各アプリケーションの倫理的意味を考慮するのは開業医次第です。 このチュートリアルで作成したアプリケーションは楽しい演習でしたが、モデルをトレーニングするために独自のデータを提供するのではなく、OpenCVと既存のデータセットに依存して顔を識別したことに注意してください。 使用されるデータとモデルは、プログラムの動作に大きな影響を与えます。

たとえば、候補者に関するデータでモデルがトレーニングされた求人検索エンジンを想像してください。 人種、性別、年齢、文化、第一言語、またはその他の要因など。 そしておそらく開発者は、スパース性を強制するモデルをトレーニングしました。これにより、特徴空間が性差の大部分を説明する部分空間に縮小されます。 その結果、このモデルは、主に性別に基づいて、求職者の検索や企業選択のプロセスにも影響を与えます。 次に、モデルの解釈が難しく、特定の機能が何に対応するかわからない、より複雑な状況を考えてみましょう。 詳細については、UCバークレーのMoritz Hardt教授によるhttp://ai.googleblog.com/2016/10/equality-of-opportunity-in-machine.html [機械学習の機会の平等]をご覧ください。

機械学習には非常に大きな不確実性が存在する可能性があります。 このランダムさと複雑さを理解するには、数学的直観と​​確率的思考スキルの両方を開発する必要があります。 実践者として、機械学習の理論的基盤を掘り下げるのはあなた次第です。