調和技研 技術ブログ

調和技研で取り組んでいる技術を公開していきます

OpenCVとdlibの顔検出機能の比較

はじめに

OpenCVとdlib、またそれぞれのDNN(ディープニューラルネットワーク)実装を使って、顔検出の簡単な比較をしてみようと思います。検出した顔領域を矩形(四角形)で囲む、良くあるプログラムを書いてみました。

比較項目は以下の4つです。

結論としては、検出精度はdlibに軍配が上がり、一方、実行速度という点ではOpenCVが優れている印象でした。今回は導入ということで簡単な比較を行いましたが、次回以降、少しずつ突っ込んだ内容にしていきたいと思います。

画像は以下のフリー素材を使用します。

f:id:chowagiken_kin:20190627174658j:plain

比較

OpenCV

OpenCVは画像処理で広く使われているライブラリです。OpenCVでは標準でHaar特徴ベース *1のカスケード分類器が提供されています。事前にカスケードファイル(haarcascade_frontalface_alt.xml)を取得し、実行ディレクトリに配置してください。

github.com


ソースコードは以下の通りです。

    # -*- 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"

こちらが実行結果です。横顔が検出されませんでした。

f:id:chowagiken_kin:20190627174449j:plain

画像として保存する場合は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()`で顔検出を行います。引数の2upsample_num_timesという引数で、デフォルト値の0から調整しています。upsample_num_timesの詳細は後述します。

実行結果は下図です。OpenCV版と同じような結果になりました。

f:id:chowagiken_kin:20190627174523j:plain

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_PATHWEIGHTS_PATHに各ファイルのパスを記述します。

blobFromImage()では、画像をDNNの入力サイズである300×300に変換(リサイズ)しています。また、各成分の画素値の平均を入力画像から差し引くMean Subtractionを行います。(104.0, 177.0, 123.0)はBGRの成分に対応しており、この値を入力画像から差し引きます。

実行します。

$ python face_detection_opencv_dnn.py --input "image.jpg"

f:id:chowagiken_kin:20190627174543j:plain

全員検出することができました。横顔もしっかり検出していますね。

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)と同じく、横顔含め全員検出することができました。

f:id:chowagiken_kin:20190628131531j:plain

画像の追加

ここまでで思ったほど差が出なかったので、比較用の画像をもう一枚用意しました。検出対象が多いので、それなりに差が出るのではないでしょうか。

f:id:chowagiken_kin:20190627174605j:plain

ソースコードは先ほどと同じなので、全ての結果を全て並べて表示します。

dlib

f:id:chowagiken_kin:20190627174630j:plain

OpenCV(DNN)

f:id:chowagiken_kin:20190627174639j:plain

dlib(DNN)

f:id:chowagiken_kin:20190628131854j:plain

検出数ではdlib(dnn)が最も多く、OpenCV(DNN)では一人も検出できませんでした。共通して、横顔やうつむき顔は検出が難しくなることが分かります。OpenCV(DNN)では画像をリサイズする関係で、画像に占める顔領域が小さくなり、検出が難しくなったと考えられます。

所感

簡単な比較に留まりましたが、精度という点ではdlibが良さそうな印象です。その代わり、実行速度はdlibが最も遅く、メモリ消費も激しいです。先述したupsample_num_timesの引数は、値を大きくするほど実行時間とメモリ消費が大きくなります。これは、検出において顔とみなす最小サイズ(Windowサイズ)が、デフォルト値=0で80×80、1の場合40×40、2の場合20×20、という風に小さくなり、処理時間が4倍ずつ大きくなるためです*3。Windowサイズが小さくなるにつれて、走査にかかる時間が大きくなるというわけですね。

OpenCVのカスケード分類器は速度が最速でした。dnnモジュールについては、バストアップ等、顔が大きく映し出された画像の検出は上手く行きそうです。

さいごに

今回の題材以外にも、Google Vision APIによる顔検出や、顔認識などをテーマに連載にしていければと思います。