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

2 件のコメント :

eno さんのコメント...

プログラムを学びたてですが、この記事を参考にしながら楽しませていただいております。
このプログラムの処理をSegmentationに変えることができれば動画のSegmentationもできるでしょうか?

Asuki Kono さんのコメント...

楽しまれているようで嬉しいです。
segmentationが何を意味されているのか良くわからないですが(前者は動画の分割、後者は分類?)、動画をTensorflowで分類するのはどうしたらよいかということでしょうか?

動画を複数の画像に分解すれば、この記事の内容を応用できると思います。
動画の動きを分類したい場合は、自分もよく分かりません。

複数の画像を並べて一枚の画像を作るなどをしたら良いのかもしれませんが、どの程度分類できるかは試してみないと分かりません。
また、それ用の学習済みデータが必要になりますので、データの準備と学習から取り組む必要がありそうです。

このような回答でいかがでしょう?