顔検出 基礎
一番簡単なオブジェクト検出の手法が、Haar-like特徴に基づくカスケード型分類器(Haar Feature-based Cascade Classifiers)というのを用いるやつ。
OpenCVには顔や目などに関して学習済みのデータが同梱されているので、これを使うことで簡単に画像から顔を検出できる。
ここではhaarcascade_frontalface_alt2.xml
というのを使う。他との違いはあんまりよく分かってない。
import cv2
from os import path
cascades_dir = path.normpath(path.join(cv2.__file__, '..', '..', '..', '..', 'share', 'OpenCV', 'haarcascades'))
cascade_f = cv2.CascadeClassifier(path.join(cascades_dir, 'haarcascade_frontalface_alt2.xml'))
img = cv2.imread('sugyan.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
faces = cascade_f.detectMultiScale(gray)
for (x, y, w, h) in faces:
cv2.rectangle(gray, (x, y), (x + w, y + h), (0, 0, 0), 2)
こんな感じで、簡単に顔の領域を検出してマーキングすることができる。
before:
after:
傾き(回転)に対応する
ところで最近は色んなアイドルさんが自撮り画像を上げてくれてたりする。
最近とても気になっているのが 虹のコンキスタドール の、「ののた」こと奥村野乃花ちゃん。
ののた可愛い。
それはともかく、、こういった自撮り画像、結構な角度で傾いているものだったりする。
OpenCVでのHaar Feature-based Cascade Classifiersによる顔検出はちょっとでも傾きがあると精度が一気に変わってしまうようで、先述の例のような真正面の顔が期待できない場合はそのままではほぼ使えない。ののたの自撮りは傾いているものが多くて厳しい。
ということで 以下の記事を参考に、元画像を徐々に回転したものを生成して、それぞれを対象に繰り返し検出を試みる。
斜辺サイズの枠を用意して、中央に元画像を配置してそれを中心に回転行列をかけて画像を変換。
import math
import numpy
...
rows, cols, colors = img.shape
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
hypot = int(math.hypot(rows, cols))
frame = numpy.zeros((hypot, hypot), numpy.uint8)
frame[(hypot - rows) * 0.5:(hypot + rows) * 0.5, (hypot - cols) * 0.5:(hypot + cols) * 0.5] = gray
for deg in range(-50, 51, 5):
M = cv2.getRotationMatrix2D((hypot * 0.5, hypot * 0.5), -deg, 1.0)
rotated = cv2.warpAffine(frame, M, (hypot, hypot))
faces = cascade_f.detectMultiScale(rotated)
for (x, y, w, h) in faces:
cv2.rectangle(rotated, (x, y), (x + w, y + h), (0, 0, 0), 2)
とりあえず5°ずつのstepでやってみた結果が以下。
40°まで回転させたところでようやく顔を検出している。35°のところでは全然関係ない箇所が誤検出されている。
ちなみにこのへんの精度はある程度detectMultiScale
の引数を変更することで調整することもできる。
例えば第2引数のscaleFactor
をデフォルトの1.1
から1.05
にすると
のようになり、35°〜50°でも顔が検出できるようになるのだけど、10°, 15°, 25°など誤検出が増える。
そこで第3引数のminNeighbors
をデフォルトの3
から4
に増やすと
となり、誤検出が消える。
ただこのへんはケースにもよるので「どの値が一番良い」みたいなものは一概には言えなそう。あと処理速度にも影響があってscaleFactor
を小さくすると処理量が増大する。
顔が検出できたら さらにその検出された領域で両目を検出してみる。これはhaarcascade_eye.xml
という別のファイルを使うだけで、同じ要領でできる。デフォルトの引数のままで35°付近の結果を試してみると
cascade_e = cv2.CascadeClassifier(path.join(cascades_dir, 'haarcascade_eye.xml'))
...
for deg in range(30, 50):
M = cv2.getRotationMatrix2D((hypot * 0.5, hypot * 0.5), -deg, 1.0)
rotated = cv2.warpAffine(frame, M, (hypot, hypot))
faces = cascade_f.detectMultiScale(rotated)
if len(faces) > 0:
(x, y, w, h) = faces[0]
roi = rotated[y:y + h, x:x + w]
eyes = cascade_e.detectMultiScale(roi)
for (ex, ey, ew, eh) in eyes:
cv2.rectangle(roi, (ex, ey), (ex + ew, ey + eh), (0, 0, 0), 2)
のようになる。33°, 35°はそもそも顔領域が変な位置で検出されている場合で、当然ながら目は見つからない。その他のものでは 正しく目の部分を検出できている場合もあれば全然ちがう部分を検出しまくってグロいことになっている場合もある。ののたゴメン…。
1°の変化もだいぶ結果が違う。。とは言え多くの場合は目を検出できているので、例えば顔の下半分領域で検出されたものは目なはずないので無視する、とかフィルタリングはできると思う。
とりあえず、顔領域から目を2つ検出した場合のみを「正しく顔を検出できた」と見なして、そのときの顔の中心、目の中心の座標を取り 回転行列の逆変換をかけてやれば、元画像における顔や目の中心座標が取得できる。複数の回転角度で取得されて重複している場合は 検出した両目がより水平に近い(つまりatan2(y, x)
が最も0に近い)ものを選択する、などするとより良い結果を得られるかな、ということでそうしてみた。
のが今のコード。
https://github.com/sugyan/face-detector/blob/cc22fd576416f79f291674e756cc00e4841289f9/lib/detector.py
数十行くらいでこれくらいのロジックが書けてしまうしpython + cv2便利〜。