はじめに
OpenCVとdlib、またそれぞれのDNN(ディープニューラルネットワーク)実装を使って、顔検出の簡単な比較をしてみようと思います。検出した顔領域を矩形(四角形)で囲む、良くあるプログラムを書いてみました。
比較項目は以下の4つです。
結論としては、検出精度はdlibに軍配が上がり、一方、実行速度という点ではOpenCVが優れている印象でした。今回は導入ということで簡単な比較を行いましたが、次回以降、少しずつ突っ込んだ内容にしていきたいと思います。
画像は以下のフリー素材を使用します。
比較
OpenCV版
OpenCVは画像処理で広く使われているライブラリです。OpenCVでは標準でHaar特徴ベース *1のカスケード分類器が提供されています。事前にカスケードファイル(haarcascade_frontalface_alt.xml)を取得し、実行ディレクトリに配置してください。
ソースコードは以下の通りです。
# -*- coding: utf-8 -*- import argparse import numpy as np # OpenCVモジュールのインポート import cv2 # カスケードファイルのパス CASCADE_PATH = './haarcascade_frontalface_alt.xml' def imread(filename, flags=cv2.IMREAD_COLOR, dtype=np.uint8): try: n = np.fromfile(filename, dtype) img = cv2.imdecode(n, flags) return img except Exception as e: print(e) return None def main(): # 画像の読み込み img = imread(args.input) # 画像をグレースケール変換 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # カスケード分類器の読み込み cascade = cv2.CascadeClassifier(CASCADE_PATH) # 顔検出の実行 faces = cascade.detectMultiScale(gray) # 検出結果の可視化 img_copy = img.copy() for (x, y, w, h) in faces: img_copy = cv2.rectangle(img_copy, (x, y), (x+w, y+h), (255, 0, 0), 2) # 結果の表示 cv2.imshow('img', img_copy) cv2.waitKey(0) cv2.destroyAllWindows() # コマンドラインで指定された場合、実行結果を保存 if args.output: cv2.imwrite(args.output, img_copy) return if __name__ == '__main__': ap = argparse.ArgumentParser() ap.add_argument("-i", "--input", required=True, help="path to input image") ap.add_argument("-o", "--output", default=None, help="path to output image") args = ap.parse_args() main()
以下で実行できます。image.jpg
の部分はご自身の環境に合わせて変更してください。
$ python face_detection_opencv.py --input "image.jpg"
こちらが実行結果です。横顔が検出されませんでした。
画像として保存する場合はoutput
オプションに出力するファイル名を付加してください。(以降共通です)
$ python face_detection_opencv.py --input "image.jpg" --output "fd_opencv.jpg"
dlib版
続いてdlibです。dlibはC++ベースの機械学習ライブラリで、顔検出においては、HOG特徴量とSVMが使われているようです。
ソースコードはこちらです。OpenCV版と大きな違いはありません。
# -*- coding: utf-8 -*- import argparse import numpy as np # OpenCV,dlibモジュールのインポート import cv2 import dlib def imread(filename, flags=cv2.IMREAD_COLOR, dtype=np.uint8): try: n = np.fromfile(filename, dtype) img = cv2.imdecode(n, flags) return img except Exception as e: print(e) return None def main(): # 画像の読み込み img = imread(args.input) # dlibの検出器 detector = dlib.get_frontal_face_detector() # 画像をRGB変換 img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 顔検出の実行 dets = detector(img_rgb, 2) # 検出結果の可視化 img_copy = img.copy() for rect in dets: cv2.rectangle(img_copy, (rect.left(), rect.top()), (rect.right(), rect.bottom()), (255, 0, 0), 2) # 結果の表示 cv2.imshow('img', img_copy) cv2.waitKey(0) cv2.destroyAllWindows() # コマンドラインで指定された場合、実行結果を保存 if args.output: cv2.imwrite(args.output, img_copy) return if __name__ == '__main__': ap = argparse.ArgumentParser() ap.add_argument("-i", "--input", required=True, help="path to input image") ap.add_argument("-o", "--output", default=None, help="path to output image") args = ap.parse_args() main()
同じように実行します。
$ python face_detection_dlib.py --input "image.jpg"
一点、cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
で画像をBGRからRGBに変換しているのにご注意ください。本来、OpenCVを使わずに画像を読み込めば必要の無い処理ですが、今回は画像の入出力を共通化するためにこのようにしています。
`detector()`で顔検出を行います。引数の2
はupsample_num_times
という引数で、デフォルト値の0
から調整しています。upsample_num_times
の詳細は後述します。
実行結果は下図です。OpenCV版と同じような結果になりました。
OpenCV(DNN)版
OpenCV3.3からdnnモジュールが追加され、DNNを使えるようになりました。フレームワークはCaffe, Tensorflow, PyTorchなどがサポートされています。今回はCaffeを利用します。コードの一部および学習済みモデルは*2から拝借しました。
# -*- coding: utf-8 -*- import argparse import numpy as np # OpenCVモジュールのインポート import cv2 # DNNモデルのパス PROTOTXT_PATH = './deploy.prototxt.txt' WEIGHTS_PATH = './res10_300x300_ssd_iter_140000.caffemodel' # 信頼度の閾値 CONFIDENCE = 0.5 def imread(filename, flags=cv2.IMREAD_COLOR, dtype=np.uint8): try: n = np.fromfile(filename, dtype) img = cv2.imdecode(n, flags) return img except Exception as e: print(e) return None def main(): # 画像の読み込み img = imread(args.input) # モデルの読み込み net = cv2.dnn.readNetFromCaffe(PROTOTXT_PATH, WEIGHTS_PATH) # 300x300に画像をリサイズ、画素値を調整 (h, w) = img.shape[:2] blob = cv2.dnn.blobFromImage(cv2.resize(img, (300, 300)), 1.0, (300, 300), (104.0, 177.0, 123.0)) # 顔検出の実行 net.setInput(blob) detections = net.forward() # 検出結果の可視化 img_copy = img.copy() for i in range(0, detections.shape[2]): confidence = detections[0, 0, i, 2] if confidence > CONFIDENCE: box = detections[0, 0, i, 3:7] * np.array([w, h, w, h]) (startX, startY, endX, endY) = box.astype("int") cv2.rectangle(img_copy, (startX, startY), (endX, endY), (255, 0, 0), 2) # 結果の表示 cv2.imshow('img', img_copy) cv2.waitKey(0) cv2.destroyAllWindows() # コマンドラインで指定された場合、実行結果を保存 if args.output: cv2.imwrite(args.output, img_copy) return if __name__ == '__main__': ap = argparse.ArgumentParser() ap.add_argument("-i", "--input", required=True, help="path to input image") ap.add_argument("-o", "--output", default=None, help="path to output image") args = ap.parse_args() main()
Caffeでは、DNNのネットワーク構造が記述されたprototxtと、ネットワークの重みが保存されたcaffemodelというファイルがそれぞれ必要になります。PROTOTXT_PATH
とWEIGHTS_PATH
に各ファイルのパスを記述します。
blobFromImage()
では、画像をDNNの入力サイズである300×300に変換(リサイズ)しています。また、各成分の画素値の平均を入力画像から差し引くMean Subtractionを行います。(104.0, 177.0, 123.0)
はBGRの成分に対応しており、この値を入力画像から差し引きます。
実行します。
$ python face_detection_opencv_dnn.py --input "image.jpg"
全員検出することができました。横顔もしっかり検出していますね。
dlib(DNN)版
dlibでもDNNを利用することが可能です。dlibのリポジトリから作者が作成した学習済みモデル(mmod_human_face_detector.dat)をダウンロードします。Max-Margin Object Detection(MMOD)という、これまた作者が考案したアルゴリズムが用いられています。
github.com
# -*- coding: utf-8 -*- import argparse import numpy as np # OpenCV,dlibモジュールのインポート import cv2 import dlib def imread(filename, flags=cv2.IMREAD_COLOR, dtype=np.uint8): try: n = np.fromfile(filename, dtype) img = cv2.imdecode(n, flags) return img except Exception as e: print(e) return None def main(): # 画像の読み込み img = imread(args.input) # 学習済みモデルの読み込み detector = dlib.cnn_face_detection_model_v1("./mmod_human_face_detector.dat") # 画像をRGB変換 img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 顔検出の実行 dets = detector(img_rgb, 2) # 検出結果の可視化 img_copy = img.copy() for rect in dets: cv2.rectangle(img_copy, (rect.rect.left(), rect.rect.top()), (rect.rect.right(), rect.rect.bottom()), (255, 0, 0), 2) # 結果の表示 cv2.imshow('img', img_copy) cv2.waitKey(0) cv2.destroyAllWindows() # コマンドラインで指定された場合、実行結果を保存 if args.output: cv2.imwrite(args.output, img_copy) return if __name__ == '__main__': ap = argparse.ArgumentParser() ap.add_argument("-i", "--input", required=True, help="path to input image") ap.add_argument("-o", "--output", default=None, help="path to output image") args = ap.parse_args() main()
実行します。
$ python face_detection_dlib_dnn.py --input "image.jpg"
OpenCV(DNN)と同じく、横顔含め全員検出することができました。
画像の追加
ここまでで思ったほど差が出なかったので、比較用の画像をもう一枚用意しました。検出対象が多いので、それなりに差が出るのではないでしょうか。
ソースコードは先ほどと同じなので、全ての結果を全て並べて表示します。
dlib
OpenCV(DNN)
所感
簡単な比較に留まりましたが、精度という点ではdlibが良さそうな印象です。その代わり、実行速度はdlibが最も遅く、メモリ消費も激しいです。先述したupsample_num_times
の引数は、値を大きくするほど実行時間とメモリ消費が大きくなります。これは、検出において顔とみなす最小サイズ(Windowサイズ)が、デフォルト値=0で80×80、1の場合40×40、2の場合20×20、という風に小さくなり、処理時間が4倍ずつ大きくなるためです*3。Windowサイズが小さくなるにつれて、走査にかかる時間が大きくなるというわけですね。
OpenCVのカスケード分類器は速度が最速でした。dnnモジュールについては、バストアップ等、顔が大きく映し出された画像の検出は上手く行きそうです。