2018年3月23日金曜日

OpenCVのDNNモジュールをPythonで呼び出し、MobileNetを利用した物体認識をしてみた


背景

OpenCVとは、画像処理機能を提供してくれるライブラリです。
バージョン3.3からDNN(deep neural network: 多層ニューラルネットワーク)モジュールが追加され、学習済みデータを利用した物体認識ができるようになりました。

そのDNNモジュールを利用して、MobileNetという、携帯端末で素早く動くことを目標にして作られているDNNを使って物体認識をしてみました。
このMobileNetは1000種類の物体を画像認識できるように作られていて、画像を処理すると1000種類それぞれの物体が画像内に存在する確率が出力されます。
今回のプログラムでは、その確率が最も高いと判断した物体名をログに出力します。
(処理速度はかないませんが、ideinさんのデモの真似です。)

OpenCVはいくつかのプログラミング言語をサポートしているので、今回は比較的少ない記述量でプログラムを書けるPythonを利用しました。

プログラムを書いてDNNモジュールを呼び出すのは早々とできたのですが、学習済みデータの用意やパラメータの設定などに時間がかかりました。
備忘録を兼ねて、自分のやった内容を共有します。

全体像

  1. 使ったもの
  2. プログラムの説明
  3. まとめ

使ったもの

python3向けのOpenCVバージョン3.3(またはそれ以後)がインストールされたPC

Ubuntu(17.10)なら下記のコマンドでインストールできます。
sudo apt install python3-opencv

aptコマンドでpython3-opencvが無い(Raspbianなど)場合、もしくはaptでインストールできるOpenCVのバージョンが古い場合などは、pipでインストールできることがあります。
sudo apt install pip3
sudo pip3 install opencv_python
sudo apt install libcblas-dev libatlas3-base

Raspbian Liteにpipでインストールした場合は、上記のコマンドに加えて下記のコマンドも必要かもしれません。
sudo apt install cmake git libgtk2.0-dev pkg-config libavcodec-dev libavformat-dev libswscale-dev
sudo apt install python3-dev python3-numpy libtbb2 libtbb-dev libjpeg-dev libpng-dev libtiff-dev libjasper-dev libdc1394-22-dev
sudo apt install libcblas-dev libatlas3-base
sudo apt install libgtk-3-dev libilmbase-dev libopenexr-dev libgstreamer1.0-dev

pipでも3.3未満のバージョンしかインストールできない場合は、ソースコードからビルドする必要があります。
しかし、ビルドの設定は環境によって異なるので、ここでは説明しません。

インストールできたら、下記のコマンドでOpenCVのバージョンを確認できます。
python3
import cv2
cv2.__version__

自分の環境では、バージョン3.3.0をインストールできました。


webカメラ

PCに内蔵されたカメラでも、USBカメラでも、Raspberry PiならPicameraでも、動くことを確認しました。

今回のプログラムの画像取り込みはOpenCVの機能を利用するため、Picameraを使う場合は、raspi-configでカメラを有効にした上で、v4l2デバイスとして認識するコマンドを実行するか、常時v4l2デバイスとしてマウントする設定を行ってください。

MobileNetの学習済みデータ

下記のリポジトリから、CaffeModel形式のMobileNet v2のデータをいただきました。

shicai/MobileNet-Caffe

プログラムの説明

下記のプログラムで、MobileNetを利用した画像認識を行いました。
mobilenet_scan_camera.py
import argparse
import cv2
from cv2 import dnn
import numpy as np
import time

inWidth = 224
inHeight = 224
WHRatio = inWidth / float(inHeight)
inScaleFactor = 0.017
meanVal = (103.94, 116.78, 123.68)
prevFrameTime = None
currentFrameTime = None

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--video", help="number of video device", default=0)
    parser.add_argument("--prototxt", default="mobilenet_v2_deploy.prototxt")
    parser.add_argument("--caffemodel", default="mobilenet_v2.caffemodel")
    parser.add_argument("--classNames", default="synset.txt")
    parser.add_argument("--preview", default=True)

    args = parser.parse_args()
    net = dnn.readNetFromCaffe(args.prototxt, args.caffemodel)
    cap = cv2.VideoCapture(args.video)
    f = open(args.classNames, 'r')
    classNames = f.readlines()
    showPreview = (args.preview == True or args.preview == "True" or args.preview == "true")

    while True:
        ret, frame = cap.read()
        rgbFrame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        blob = dnn.blobFromImage(rgbFrame, inScaleFactor, (inWidth, inHeight), meanVal)
        net.setInput(blob)
        detections = net.forward()

        maxClassId = 0
        maxClassPoint = 0;
        for i in range(detections.shape[1]):
            classPoint = detections[0, i, 0, 0]
            if (classPoint > maxClassPoint):
                maxClassId = i
                maxClassPoint = classPoint

        className = classNames[maxClassId]
        print("class id: ", maxClassId)
        print("class point: ", maxClassPoint)
        print("name: ", className)
        prevFrameTime = currentFrameTime
        currentFrameTime = time.time()
        if (prevFrameTime != None):
            print(1.0 / (currentFrameTime - prevFrameTime), "fps")

        if (showPreview):
            font = cv2.FONT_HERSHEY_SIMPLEX
            size = 1
            color = (255,255,255)
            weight = 2
            cv2.putText(frame, className, (10, 30), font, size, color, weight)
            cv2.putText(frame, str(maxClassPoint), (10, 60), font, size, color, weight)
            cv2.imshow("detections", frame)

        if cv2.waitKey(1) >= 0:
            break

ところどころ解説します。

MobileNetの学習済みデータとして、実行時の引数で指定するファイル名を変えられる形で、下記の3つをファイルを読み込んでいます。
  • mobilenet_v2_deploy.prototxt
  • mobilenet_v2.caffemodel
  • synset.txt
parser = argparse.ArgumentParser()
parser.add_argument("--prototxt", default="mobilenet_v2_deploy.prototxt")
parser.add_argument("--caffemodel", default="mobilenet_v2.caffemodel")
parser.add_argument("--classNames", default="synset.txt")
net = dnn.readNetFromCaffe(args.prototxt, args.caffemodel)
f = open(args.classNames, 'r')
classNames = f.readlines()

MobileNetのprototxtに記載されている数値に応じて、判別に利用する値を設定しています。
name: "MOBILENET_V2"
#  transform_param {
#    scale: 0.017
#    mirror: false
#    crop_size: 224
#    mean_value: [103.94,116.78,123.68]
#  }
input: "data"
input_dim: 1
input_dim: 3
input_dim: 224
input_dim: 224
..

scaleやmean_valが何なのかよく分かっていませんが、それどおりに記述します。
input_dimは1, 3, 224, 224ということで、1つ目が何を意味するのかは分かりませんが、3次元(RGB)の224x224pxの画像を処理できるということだと思います。
動かしてみたところ、入力画像が224x224px以上の大きさであれば、画像内の物体認識を行ってくれるようです。
inWidth = 224
inHeight = 224
inScaleFactor = 0.017
meanVal = (103.94, 116.78, 123.68)

OpenCVのVidwoCaptureで取得できる画像データはBGR形式なので、そのまま判別に使うと精度が悪くなります。
そのため、mobileNetにはRGB形式に変換したデータを渡しています。
ret, frame = cap.read()
rgbFrame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
blob = dnn.blobFromImage(rgbFrame, inScaleFactor, (inWidth, inHeight), meanVal)

MobileNetは藩閥可能な1000種類それぞれの正答率を出力するので、最も値が大きいidを取得し、それの名前をログに出力しています。
maxClassId = 0
maxClassPoint = 0;
for i in range(detections.shape[1]):
    classPoint = detections[0, i, 0, 0]
    if (classPoint > maxClassPoint):
        maxClassId = i
        maxClassPoint = classPoint

className = classNames[maxClassId]
print("class id: ", maxClassId)
print("class point: ", maxClassPoint)
print("name: ", className)

上記のプログラムはMobileNetの学習済みデータと一緒にgithubで公開しているので、下記のようなコマンドで実行できます。
sudo apt install git
git clone git@github.com:asukiaaa/py_opencv_mobilenet_practice.git
cd py_opencv_mobilenet_practice
python3 mobilenet_scan_camera.py

カセットコンロの缶を見せるとヘアスプレーと認識してくれました。


Raspberr Piをssh経由で操作するときなど、プレビューを出したくない場合は、--preview=falseのオプションを付けるとプレビュー無しで動かせます。
python3 mobilenet_scan_camera.py --preview=false

処理速度については、GPUを使えるようにしたノートPCだと15fps位、Raspberry Pi Zeroだと0.28fps位で判別を行えました。

まとめ

OpenCVのDNNをPythonで呼び出して、物体認識を行えました。

何かの参考になれば嬉しいです。



この記事の内容を参考にして、Raspberry Pi Zeroを使って物体認識装置を作ってみました。
良かったらこちらもご覧ください。

Raspberry Pi ZeroとPicameraと0.96インチLCDで物体認識装置を作ってみた

参考

【Windows】【Python】OpenCV3.3.1のdnnモジュールサンプル(mobilenet_ssd_python.py)
opencv/samples/dnn/mobilenet_ssd_accuracy.py
Deep Neural Networks (dnn module) | OpenCV 3.3
Failed to load caffe model in opencv 3.3
opencv3.3.0 with deep learning
models/research/slim

0 件のコメント :

コメントを投稿