コンピュータービジョンを適用してPython3で感情ベースの犬のフィルターを構築する方法
著者は、 Write for DOnations プログラムの一環として、 Girls WhoCodeを選択して寄付を受け取りました。
序章
コンピュータビジョンは、画像やビデオから高次の理解を引き出すことを目的としたコンピュータサイエンスのサブフィールドです。 このフィールドには、オブジェクト検出、画像復元(行列補完)、オプティカルフローなどのタスクが含まれます。 コンピュータービジョンは、自動運転車のプロトタイプ、従業員のいない食料品店、楽しいSnapchatフィルター、モバイルデバイスの顔認証システムなどのテクノロジーを強化します。
このチュートリアルでは、事前にトレーニングされたモデルを使用してSnapchat風の犬用フィルターを作成する際のコンピュータービジョンについて説明します。 Snapchatに慣れていない人のために、このフィルターはあなたの顔を検出し、その上に犬のマスクを重ね合わせます。 次に、顔の感情分類子をトレーニングして、フィルターが感情に基づいて犬のマスクを選択できるようにします。たとえば、幸せの場合はコーギー、悲しい場合はパグなどです。 その過程で、通常の最小二乗法とコンピュータービジョンの両方で関連する概念を探求し、機械学習の基礎を学びます。
チュートリアルを進めるときは、コンピュータービジョンライブラリであるOpenCV
、線形代数ユーティリティ用のnumpy
、およびプロット用のmatplotlib
を使用します。 また、コンピュータビジョンアプリケーションを構築するときに、次の概念を適用します。
- 回帰および分類手法としての通常の最小二乗。
- 確率的勾配神経回路網の基礎。
このチュートリアルを完了する必要はありませんが、これらの数学的概念に精通している場合は、より詳細な説明のいくつかを理解しやすくなります。
- 基本的な線形代数の概念:スカラー、ベクトル、および行列。
- 基本的な微積分:導関数をとる方法。
このチュートリアルの完全なコードは、https://github.com/do-community/emotion-based-dog-filterにあります。
始めましょう。
前提条件
このチュートリアルを完了するには、次のものが必要です。
- 1GB以上のRAMを搭載したPython3のローカル開発環境。 Python 3のローカルプログラミング環境をインストールおよびセットアップする方法に従って、必要なものをすべて構成できます。
- リアルタイムの画像検出を行うための動作するウェブカメラ。
ステップ1—プロジェクトの作成と依存関係のインストール
このプロジェクトのワークスペースを作成し、必要な依存関係をインストールしましょう。 ワークスペースをDogFilter
と呼びます。
mkdir ~/DogFilter
DogFilter
ディレクトリに移動します。
cd ~/DogFilter
次に、プロジェクトの新しいPython仮想環境を作成します。
python3 -m venv dogfilter
環境をアクティブにします。
source dogfilter/bin/activate
プロンプトが変わり、環境がアクティブであることを示します。 次に、このチュートリアルで使用するPythonのディープラーニングフレームワークであるPyTorchをインストールします。 インストールプロセスは、使用しているオペレーティングシステムによって異なります。
macOSで、次のコマンドを使用してPytorchをインストールします。
python -m pip install torch==0.4.1 torchvision==0.2.1
Linuxでは、次のコマンドを使用します。
pip install http://download.pytorch.org/whl/cpu/torch-0.4.1-cp35-cp35m-linux_x86_64.whl pip install torchvision
また、Windowsの場合は、次のコマンドを使用してPytorchをインストールします。
pip install http://download.pytorch.org/whl/cpu/torch-0.4.1-cp35-cp35m-win_amd64.whl pip install torchvision
次に、OpenCV
とnumpy
のパッケージ済みバイナリをインストールします。これらは、それぞれコンピュータビジョンライブラリと線形代数ライブラリです。 前者は画像の回転などのユーティリティを提供し、後者は行列の反転などの線形代数ユーティリティを提供します。
python -m pip install opencv-python==3.4.3.18 numpy==1.14.5
最後に、アセットのディレクトリを作成します。このディレクトリには、このチュートリアルで使用する画像が保持されます。
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です。
次の画像のPexels(CC0、元の画像へのリンク)からすべての顔を検出します。
まず、画像をダウンロードします。 次のコマンドは、ダウンロードしたイメージをchildren.png
としてassets
フォルダーに保存します。
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の 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
は、検出されるオブジェクトの最小許容サイズです。 これよりも小さいオブジェクトは破棄されます。
戻り値のタイプは、タプルのリストです。各タプルには、長方形の最小x、最小y、幅、高さをこの順序で示す4つの数値があります。
検出されたすべてのオブジェクトを反復処理し、 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
を開きます。 ボックスで輪郭が描かれた顔を示す次の画像が表示されます。
この時点で、顔検出器が機能しています。 画像を入力として受け取り、画像内のすべての面の周りに境界ボックスを描画して、注釈付きの画像を出力します。 次に、これと同じ検出をライブカメラフィードに適用してみましょう。
ステップ3—カメラフィードをリンクする
次の目的は、コンピューターのカメラを顔検出器にリンクすることです。 静止画像で顔を検出する代わりに、コンピューターのカメラからすべての顔を検出します。 カメラ入力を収集し、すべての顔を検出して注釈を付けてから、注釈付きの画像をユーザーに表示します。 手順2のスクリプトから続行するので、そのスクリプトを複製することから始めます。
cp step_2_face_detect.py step_3_camera_face_detect.py
次に、エディターで新しいスクリプトを開きます。
nano step_3_camera_face_detect.py
OpenCVの公式ドキュメントにあるこのテストスクリプトのいくつかの要素を使用して、main
関数を更新します。 コンピューターのカメラからライブフィードをキャプチャするように設定されているVideoCapture
オブジェクトを初期化することから始めます。 これをmain
関数の先頭、関数内の他のコードの前に配置します。
step_3_camera_face_detect.py
def main(): cap = cv2.VideoCapture(0) ...
frame
を定義する行から始めて、既存のすべてのコードをインデントし、すべてのコードをwhile
ループに配置します。
step_3_camera_face_detect.py
while True: 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 True: # frame = cv2.imread('assets/children.png') # DELETE ME # Capture frame-by-frame ret, frame = cap.read()
while
ループの最後にあるcv2.imwrite(...)
の行を置き換えます。 画像をディスクに書き込む代わりに、注釈付きの画像をユーザーの画面に表示します。
step_3_camera_face_detect.py
cv2.imwrite('outputs/children_detected.png', frame) # DELETE ME # Display the resulting frame cv2.imshow('frame', frame)
また、プログラムを停止できるように、キーボード入力を監視するコードを追加します。 ユーザーがq
文字をヒットしたかどうかを確認し、ヒットした場合はアプリケーションを終了します。 cv2.imshow(...)
の直後に、次を追加します。
step_3_camera_face_detect.py
... cv2.imshow('frame', frame) if cv2.waitKey(1) & 0xFF == ord('q'): break ...
行cv2.waitkey(1)
は、プログラムを1ミリ秒停止し、キャプチャされた画像をユーザーに表示できるようにします。
最後に、キャプチャを解放して、すべてのウィンドウを閉じます。 これをwhile
ループの外側に配置して、main
機能を終了します。
step_3_camera_face_detect.py
... while True: ... cap.release() cv2.destroyAllWindows()
スクリプトは次のようになります。
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
これにより、カメラがアクティブになり、カメラのフィードを表示するウィンドウが開きます。 あなたの顔はリアルタイムで緑色の四角で囲まれます:
注:物事が機能するために非常に静止している必要がある場合は、部屋の照明が適切でない可能性があります。 あなたとあなたの背景が高いコントラストを持っている明るい部屋に移動してみてください。 また、頭の近くの明るい光は避けてください。 たとえば、太陽に背を向けている場合、このプロセスはうまく機能しない可能性があります。
次の目的は、検出された顔を取得し、それぞれに犬のマスクを重ねることです。
ステップ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
画像はひし形です。 このマトリックスの値を画像として保存する場合。 これにより、次の図が得られます。
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
画像として再レンダリングすると、これが実際にはポケボールであることがわかります。
これで、白黒画像とグレースケール画像が数値でどのように表現されるかがわかりました。 色を導入するには、より多くの情報をエンコードする方法が必要です。 画像の高さと幅はh x w
で表されます。
現在のグレースケール表現では、各ピクセルは0から1までの1つの値です。 同等に、画像のサイズはh x w x 1
であると言えます。 つまり、画像内のすべての(x, y)
位置には1つの値しかありません。
色表現では、0から1までの3つの値を使用して各ピクセルの色を表現します。 1つの数字は「赤の程度」に対応し、1つは「緑の程度」に対応し、最後の数字は「青の程度」に対応します。 これをRGB色空間と呼びます。 これは、画像内のすべての(x, y)
位置に対して、3つの値(r, g, b)
があることを意味します。 その結果、画像はh x w x 3
になります。
ここでは、各番号の範囲は0から1ではなく0から255ですが、考え方は同じです。 数字の組み合わせが異なると、濃い紫(102, 0, 204)
や明るいオレンジ(255, 153, 51)
などの異なる色に対応します。 要点は次のとおりです。
- 各画像は、高さ、幅、カラーチャンネルの3つの次元を持つ数字のボックスとして表されます。 この数字のボックスを直接操作することは、画像を操作することと同じです。
- このボックスをフラット化して、単なる数字のリストにすることもできます。 このようにして、画像はvectorになります。 後で、画像をベクトルと呼びます。
画像が数値でどのように表現されるかを理解したので、犬のマスクを顔に適用する準備が整いました。 犬のマスクを適用するには、子画像の値を白以外の犬のマスクのピクセルに置き換えます。 まず、単一の画像で作業します。 手順2で使用した画像からこの顔の切り抜きをダウンロードします。
wget -O assets/child.png https://assets.digitalocean.com/articles/python3_dogfilter/alXjNK1.png
また、以下の犬用マスクをダウンロードしてください。 このチュートリアルで使用されている犬のマスクは私自身の絵であり、CC0ライセンスの下でパブリックドメインにリリースされています。
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
関数から削除します。これは、不要になったためです。
cv2.imwrite('outputs/resized_dog.png', resized_mask) # delete this line ...
その代わりに、このセクションの最初から画像表現の知識を適用して、画像を変更します。 子画像のコピーを作成することから始めます。 次の行を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) cv2.imwrite('outputs/child_with_dog_mask.png', face_with_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
に犬用マスクを付けた子供の写真が次のように表示されます。
これで、犬のマスクを顔に適用するユーティリティができました。 次に、作成したものを使用して、犬のマスクをリアルタイムで追加しましょう。
手順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) # load mask mask = cv2.imread('assets/dog.png') ...
次に、while
ループで、次の行を見つけます。
step_4_dog_mask.py
ret, frame = cap.read()
その後に次の行を追加して、画像の高さと幅を抽出します。
step_4_dog_mask.py
ret, frame = cap.read() frame_h, frame_w, _ = frame.shape ...
次に、main
で境界ボックスを描画する線を削除します。 この行は、検出された面を反復処理するfor
ループにあります。
step_4_dog_mask.py
for x, y, w, h in rects: ... cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2) # DELETE ME ...
その代わりに、フレームをトリミングするこのコードを追加します。 美的目的のために、顔より少し広い領域をトリミングします。
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
これで、リアルタイムのドッグフィルターが実行されます。 このスクリプトは、写真内の複数の顔でも機能するため、友達を集めて犬の自動化を行うことができます。
これで、Snapchat風の犬用フィルターを作成するというこのチュートリアルの最初の主な目的は終わりです。 次に、顔の表情を使用して、顔に適用される犬のマスクを決定しましょう。
ステップ5—最小二乗法を使用して基本的な顔の感情分類器を構築する
このセクションでは、表示された感情に基づいてさまざまなマスクを適用する感情分類子を作成します。 微笑むと、フィルターはコーギーマスクを適用します。 眉をひそめると、パグマスクが適用されます。 その過程で、機械学習の概念を理解して議論するための基本となる最小二乗フレームワークについて説明します。
データを処理して予測を生成する方法を理解するために、最初に機械学習モデルについて簡単に説明します。
検討するモデルごとに2つの質問をする必要があります。 今のところ、モデルを区別するには、次の2つの質問で十分です。
- 入力:モデルはどのような情報を提供しますか?
- 出力:モデルは何を予測しようとしていますか?
大まかに言えば、目標は感情分類のモデルを開発することです。 モデルは次のとおりです。
- 入力:顔の画像を指定します。
- 出力:対応する感情を予測します。
model: face -> emotion
使用するアプローチは最小二乗です。 一連のポイントを取得し、最適なラインを見つけます。 次の画像に示す最適な線は、私たちのモデルです。
私たちのラインの入力と出力を考えてみましょう。
- 入力:指定された
x
座標。 - 出力:対応する$y$座標を予測します。
least squares line: x -> y
感情の分類に最小二乗法を使用するには、入力x
が顔を表し、出力y
が感情を表す必要があります。
x -> face
:x
にone 番号を使用する代わりに、x
の値のvectorを使用します。 したがって、x
は顔の画像を表すことができます。 記事通常の最小二乗は、x
に値のベクトルを使用できる理由を説明しています。y -> emotion
:各感情は数字に対応します。 たとえば、「怒っている」は0、「悲しい」は1、「幸せ」は2です。 このように、y
は感情を表すことができます。 ただし、この行は、y
値0、1、および2を出力するように制約されていません。 可能なy値は無限にあり、1.2、3.5、または10003.42の可能性があります。 これらのy
値をクラスに対応する整数に変換するにはどうすればよいですか? 詳細と説明については、記事 One-HotEncodingを参照してください。
この背景知識を身に付けて、ベクトル化された画像とワンホットエンコードされたラベルを使用して、単純な最小二乗分類器を構築します。 これは、次の3つのステップで実行できます。
- データの前処理:このセクションの冒頭で説明したように、サンプルはベクトルであり、各ベクトルは顔の画像をエンコードします。 ラベルは感情に対応する整数であり、これらのラベルにワンホットエンコーディングを適用します。
- モデルの指定とトレーニング:閉形式の最小二乗解
w^*
を使用します。 - モデルを使用して予測を実行します。
Xw^*
のargmaxを取得して、予測された感情を取得します。
始めましょう。
まず、データを格納するディレクトリを設定します。
mkdir data
次に、Pierre-LucCarrierとAaronCourvilleによってキュレーションされたデータをKaggleでの2013Face EmotionClassificationコンペティションからダウンロードします。
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
は400万を超える値を持つ2304x2304
マトリックスであるため、コモディティハードウェアでは(X^TX)^{-1}
の計算に時間がかかりすぎるため、最初の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—入力を特徴づけることによる精度の向上
より表現力豊かなモデルを使用して、精度を高めることができます。 これを実現するために、入力をfeaturizeします。
元の画像は、位置(0, 0
)が赤、(1, 0
)が茶色などであることを示しています。 特徴的な画像は、画像の左上に犬がいること、中央に人がいることなどを示している可能性があります。 機能化は強力ですが、その正確な定義はこのチュートリアルの範囲を超えています。
ランダムガウス行列を使用して、動径基底関数(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
d = 100 W = np.random.normal(size=(X_train.shape[1], d)) # 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
の値をさらに試してみると、テスト精度がさらに4.3%向上して61.7%になっていることがわかります。 次の図では、d
を変化させたときの新しい分類器のパフォーマンスを考慮しています。 直感的には、d
が増えると、元のデータをどんどん使用するため、精度も上がるはずです。 ただし、バラ色の絵を描くのではなく、グラフは負の傾向を示しています。
より多くのデータを保持するにつれて、トレーニングと検証の精度の間のギャップも大きくなります。 これは、過剰適合の明らかな証拠です。ここで、モデルは、すべてのデータに一般化できなくなった表現を学習しています。 過剰適合と戦うために、複雑なモデルにペナルティを課すことにより、モデルを正規化します。
通常の最小二乗目的関数を正則化項で修正し、新しい目的を与えます。 新しい目的関数はリッジ回帰と呼ばれ、次のようになります。
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 = 1000 W = np.random.normal(size=(X_train.shape[1], d)) ...
次に、lambda = 10^{10}
の正則化を使用してリッジ回帰を適用します。 w
を定義する行を次の2行に置き換えます。
step_6_ls_simple.py
... # 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))
次に、このブロックを見つけます。
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
をもう一度再評価すると、リッジ回帰のトレーニングと検証の精度の間に小さなギャップが見られます。 言い換えれば、リッジ回帰は過剰適合が少ないということです。
これらの追加の拡張機能を備えた最小二乗法のベースラインパフォーマンスは、適度に良好に機能します。 トレーニングと推論の時間はすべて合わせて、最高の結果を得るのに20秒もかかりません。 次のセクションでは、さらに複雑なモデルについて説明します。
ステップ7—PyTorchで畳み込みニューラルネットワークを使用して顔感情分類器を構築する
このセクションでは、最小二乗法の代わりにニューラルネットワークを使用して2番目の感情分類器を作成します。 繰り返しになりますが、私たちの目標は、顔を入力として受け入れ、感情を出力するモデルを作成することです。 最終的に、この分類子は、適用する犬のマスクを決定します。
ニューラルネットワークの簡単な視覚化と紹介については、ニューラルネットワークについての記事を参照してください。 ここでは、PyTorchというディープラーニングライブラリを使用します。 広く使用されているディープラーニングライブラリは多数あり、それぞれにさまざまな長所と短所があります。 PyTorchは、開始するのに特に適した場所です。 このニューラルネットワーク分類器を暗示するために、最小二乗分類器で行ったように、再び3つのステップを実行します。
- データを前処理します。ワンホットエンコーディングを適用してから、PyTorch抽象化を適用します。
- モデルの指定とトレーニング:PyTorchレイヤーを使用してニューラルネットワークを設定します。 最適化ハイパーパラメータを定義し、確率的勾配降下法を実行します。
- モデルを使用して予測を実行します。ニューラルネットワークを評価します。
step_7_fer_simple.py
という名前の新しいファイルを作成します
nano step_7_fer_simple.py
必要なユーティリティをインポートし、データを保持するPythonクラスを作成します。 ここでのデータ処理では、トレインとテストのデータセットを作成します。 これらを行うには、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
Fer2013Dataset
クラスの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データ構造でラップします。
__init__
関数の直後に、__len__
関数を追加します。これは、PyTorchが期待するDataset
インターフェイスを実装するために必要です。
step_7_fer_simple.py
... def __len__(self): return len(self._labels)
最後に、__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
これにより、次のテンソルのペアが出力されます。 データパイプラインは、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 if __name__ == '__main__': loader = torch.utils.data.DataLoader(trainset, batch_size=2, shuffle=False) print(next(iter(loader)))
その代わりに、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つのエポックのトレーニングを行います。 今のところ、エポックは、すべてのトレーニングサンプルが1回だけ使用されたトレーニングの反復であると定義しています。
まず、データセットローダーからimage
とlabel
を抽出し、それぞれをPyTorchVariable
でラップします。 次に、フォワードパスを実行してから、損失とニューラルネットワークを逆伝播します。 これを行うには、スクリプトの最後に次のコードを追加します。
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%の検証精度を達成し、これまでで最も成功した最小二乗アプローチの66.6%よりもさらに6.8%向上しました。 。 これらの追加のベルとホイッスルを新しいスクリプトに含めます。
ライブカメラフィードが使用する最終的な顔の感情検出器を保持する新しいファイルを作成します。 このスクリプトには、上記のコードと、コマンドラインインターフェイス、および後で使用するコードのインポートが簡単なバージョンが含まれています。 さらに、より精度の高いモデル用に事前に調整されたハイパーパラメータが含まれています。
nano step_7_fer.py
次のインポートから始めます。 これは以前のファイルと一致しますが、import cv2.
としてOpenCVが追加で含まれています
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
の顔感情認識データセットのコードを再利用して次のファイルに追加します。
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—感情ベースの犬のフィルターを完成させる
新しい顔感情分類器を統合する前に、動物用マスクを選択する必要があります。 DalmationマスクとSheepdogマスクを使用します。
次のコマンドを実行して、両方のマスクを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 mask = masks[predictor(frame[y:y+h, x: x+w])] 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
今それを試してみてください! 笑顔は「幸せ」として登録され、元の犬を表示します。 中立的な顔やしかめっ面は「悲しい」として登録され、ダルメーションを生み出します。 素敵な大きなあごのドロップを伴う「サプライズ」の顔は、牧羊犬を生み出します。
これで、感情ベースの犬のフィルターとコンピュータービジョンへの進出は終わりです。
結論
このチュートリアルでは、コンピュータービジョンを使用して顔検出器と犬のフィルターを作成し、機械学習モデルを使用して、検出された感情に基づいてマスクを適用しました。
機械学習は広く適用できます。 ただし、機械学習を適用する際に各アプリケーションの倫理的影響を考慮するのは、実践者の責任です。 このチュートリアルで作成したアプリケーションは楽しい演習でしたが、モデルをトレーニングするために独自のデータを提供するのではなく、OpenCVと既存のデータセットを使用して顔を識別したことを忘れないでください。 使用されるデータとモデルは、プログラムの動作に大きな影響を与えます。
たとえば、モデルが候補者に関するデータでトレーニングされた求人検索エンジンを想像してみてください。 人種、性別、年齢、文化、第一言語、またはその他の要因など。 そして、おそらく開発者は、スパース性を強制するモデルをトレーニングしました。これにより、機能スペースが、性別が分散の大部分を説明するサブスペースに縮小されます。 その結果、このモデルは、主に性別に基づく候補者の求人検索や企業の選択プロセスにも影響を与えます。 ここで、モデルの解釈が難しく、特定の機能が何に対応するかわからない、より複雑な状況を考えてみましょう。 これについて詳しくは、カリフォルニア大学バークレー校のMoritzHardt教授による機械学習における機会の平等をご覧ください。
機械学習には、圧倒的な不確実性が存在する可能性があります。 このランダム性と複雑さを理解するには、数学的な直感と確率論的思考スキルの両方を身に付ける必要があります。 実践者として、機械学習の理論的基盤を掘り下げるのはあなた次第です。