【実験】Raspberry Pi 上の OpenCV に PaPeRo i の顔を学習させてみる

Raspberry Pi 上の OpenCV でPaPeRo i に人の顔を数えさせる」では、あらかじめ用意されていた学習結果データを使用して人の顔を認識させましたが、開発したいアプリケーションによっては、「人の顔ではなく、別な物を認識させたい」というケースもあるかと思います。
今回はその一例として、PaPeRo i の顔の画像をOpenCVに学習させ、その後、PaPeRo i の画像を含む資料の印刷物をPaPeRo i に見せて、そこに含まれる PaPeRo i の台数を PaPeRo i に発話させてみます。
使用した Raspberry Pi は 「Raspberry Pi 上の OpenCV でPaPeRo i に人の顔を数えさせる」と同じく Raspberry Pi 3 model B v1.2、RaspbianはStretchです。
今回の結果は残念ながら、あまり思わしいものではありませんでしたが、学習に使用する画像枚数の増加等により、改善できる可能性はあると思います。

前提

以下の説明は、「Raspberry Pi 上の OpenCV でPaPeRo i に人の顔を数えさせる」の作業が実施済である事を前提とします。

画像ファイルの準備

学習に使用する画像として、下記を準備します。

・ポジティブ画像(PaPeRo i の顔の画像) → 1枚:paperoi.png
・ネガティブ画像(PaPeRo i の顔が写っていない画像) → 30枚:img001.png ~ img030.png

ポジティブ画像は、複数の画像を自分で用意する方法と、学習の準備段階で1枚の画像から複数枚自動生成する方法がありますが、今回は準備の手間を省くため、後者の方法をとります。
ネガティブ画像についても、本来はもっと多い方がよいと思うのですが、今回は30枚で試す事にします。

画像の作成方法としましては、PaPeRo i アプリ紹介ページに掲載されているパンフレットの画像からPaPeRo i の顔を切り出してポジティブ画像とし、ネガティブ画像は同パンフレットやシミュレータのスクリーンショットからPaPeRo i 以外の部分を切り出して作成しました。
また、ポジティブ画像については、元画像が1枚のみである事から、学習の過程で背景を顔の一部と解釈される事を懸念し、背景を白く塗りつぶして使用しました。

作業手順(Raspberry Pi)

(1) 作業場所を作成します。

$ cd ~/papero
$ mkdir traincascade
$ cd traincascade
$ mkdir pos
$ mkdir vec
$ mkdir neg
$ mkdir cascade
$ cd cascade
$ mkdir paperoi

ディレクトリを作成したら、ポジティブ画像(paperoi.png)を~/papero/traincascade/pos の下へ、ネガティブ画像(img001.png~img030.png)を ~/papero/traincascade/neg の下へ、それぞれ配置します。
ここまでの作業で、ディレクトリ構成は下記のようになります。

~/
  papero/
    pypapero.py
    traincascade/
      pos/
        paperoi.png
      vec/
      neg/
        img001.png
        img002.png
        ...
        img030.png
      cascade/
        paperoi/

(2) opencv_createsamples でポジティブ画像をベクトル化します。

$ cd ~/papero/traincascade
$ opencv_createsamples -img ./pos/paperoi.png -vec ./vec/paperoi.vec -bgcolor 255 -num 50

-num 50 の指定により、元画像であるpaperoi.pngをXYZ軸に対してデフォルトの範囲でランダムに回転した50枚の画像が内部的に生成され、それらの画像から1つのベクトルファイル paperoi.vec が生成されます。

Raspberry Pi のデスクトップ上に開いたターミナルから実行する場合、

$ opencv_createsamples -img ./pos/paperoi.png -vec ./vec/paperoi.vec -num 50 -bgcolor 255 -show

のように、-show オプションを付ける事により、内部的に生成された画像を見る事ができます。画像は1枚ずつ表示され、Enterキーで次の画像に進みます。

(3) ネガティブ画像のリストを作ります。

$ find ./neg -name "*.png" > nglist.txt

(4) 学習を行い、学習結果を生成します。

$ opencv_traincascade -data ./cascade/paperoi/ -vec ./vec/paperoi.vec -bg ./nglist.txt -numPos 45 -numNeg 30

-numPos の後の数字でポジティブ画像の枚数を指定するのですが、paperoi.vec に含まれる全画像数である50を指定すると、画像のうちの一部が「ポジティブ画像としてふさわしくない」と判断された場合にエラーとなる為、9割である45を指定しています。

学習が開始されると、下記のように、学習の進捗状況が表示されます。

PARAMETERS:
cascadeDirName: ./cascade/paperoi/
vecFileName: ./vec/paperoi.vec
bgFileName: ./nglist.txt
numPos: 45
numNeg: 30
numStages: 20
precalcValBufSize[Mb] : 1024
precalcIdxBufSize[Mb] : 1024
acceptanceRatioBreakValue : -1
stageType: BOOST
featureType: HAAR
sampleWidth: 24
sampleHeight: 24
boostType: GAB
minHitRate: 0.995
maxFalseAlarmRate: 0.5
weightTrimRate: 0.95
maxDepth: 1
maxWeakCount: 100
mode: BASIC
Number of unique features given windowSize [24,24] : 162336

===== TRAINING 0-stage =====
<BEGIN
POS count : consumed   45 : 45
NEG count : acceptanceRatio    30 : 1
Precalculation time: 1
+----+---------+---------+
|  N |    HR   |    FA   |
+----+---------+---------+
|   1|        1|        0|
+----+---------+---------+
END>
Training until now has taken 0 days 0 hours 0 minutes 3 seconds.

===== TRAINING 1-stage =====
<BEGIN
POS count : consumed   45 : 45
NEG count : acceptanceRatio    30 : 0.148515
Precalculation time: 1
+----+---------+---------+
|  N |    HR   |    FA   |
+----+---------+---------+
|   1|        1|        0|
+----+---------+---------+
END>
Training until now has taken 0 days 0 hours 0 minutes 7 seconds.

...

===== TRAINING 6-stage =====
<BEGIN
POS count : consumed   45 : 45
NEG count : acceptanceRatio    30 : 3.83055e-06
Precalculation time: 0
+----+---------+---------+
|  N |    HR   |    FA   |
+----+---------+---------+
|   1|        1|        0|
+----+---------+---------+
END>
Training until now has taken 0 days 0 hours 6 minutes 14 seconds.

===== TRAINING 7-stage =====
<BEGIN
POS count : consumed   45 : 45
NEG count : acceptanceRatio    0 : 0
Required leaf false alarm rate achieved. Branch training terminated.

 学習が完了すると、-data オプションで指定した ./cascade/paperoi/ の下に、cascade.xml というファイルが生成されます。
 また、学習の途中で Ctrl+C で強制終了した場合も、./cascade/paperoi/ の下に中間ファイルが残されているため、次回同じオプションで opencv_traincascade を実行すると、中断した stage から学習が再開されます。

 学習にかかる時間は opencv_createsamples によりランダムに生成される画像に依存して変動するようですが、5~7分程度でした。

(5) 下記の内容をコピー・ペーストして、cv_numpaperoi.py を作成し、~/papero の下に置きます。
※ ****ユーザ名****、****パスワード**** の部分につきましては、PaPeRo i に一般ユーザでログインする際に使用するユーザ名とパスワードに置き換えて下さい。

import argparse
import time
from enum import Enum

from paramiko import SSHClient,AutoAddPolicy
from scp import SCPClient
import cv2

import pypapero


class State(Enum):
    st0 = 10
    st1 = 11
    st2 = 12
    st3 = 13
    st4 = 14
    end = 999


def main(papero, host, do_disp):
    prev_time = time.monotonic()
    past_time = 0
    interval_time = 0
    state = State.st0
    first = True
    print("HOST=" + host)
    PORT = 22
    USER = "****ユーザ名****"
    PSWD = "****パスワード****"
    scp = None
    ssh = SSHClient() 
    ssh.set_missing_host_key_policy(AutoAddPolicy())
    ssh.connect(host, port=PORT, username=USER, password=PSWD)
    scp = SCPClient(ssh.get_transport())
    cascade_file = 'traincascade/cascade/paperoi/cascade.xml'
    face_cascade = cv2.CascadeClassifier(cascade_file)
    num_face_now = 0
    num_face = 0
    while state != State.end:
        messages = papero.papero_robot_message_recv(0.1)
        now_time = time.monotonic()
        delta_time = now_time - prev_time
        prev_time = now_time
        if messages is not None:
            msg_dic_rcv = messages[0]
        else:
            msg_dic_rcv = None
        if papero.errOccurred != 0:
            print("------Error occured(main()). Detail : " + papero.errDetail)
            break
        if state == State.st0:
            papero.send_start_speech("検知したパペロの数を発話します。座布団のボタンで終了します。")
            past_time = 0.0
            state = State.st1
        elif state == State.st1:
            past_time += delta_time
            if past_time > 0.5:
                papero.send_get_speech_status()
                state = State.st2
        elif state == State.st2:
            if msg_dic_rcv is not None:
                if msg_dic_rcv["Name"] == "getSpeechStatusRes":
                    if str(msg_dic_rcv["Return"]) == "0":
                        state = State.st3
                    else:
                        past_time = 0
                        state = State.st1
        elif state == State.st3:
            past_time += delta_time
            if past_time >= interval_time:
                papero.send_take_picture("JPEG", filename="tmp.jpg", camera="VGA")
                past_time = 0
                state = State.st4
        elif state == State.st4:
            if msg_dic_rcv is not None:
                if msg_dic_rcv["Name"] == "takePictureRes":
                    scp.get("/tmp/tmp.jpg")
                    img = cv2.imread("tmp.jpg")
                    if img is not None:
                        gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
                        faces = face_cascade.detectMultiScale(gray, 1.3, 5)
                        num_face_now = len(faces)
                        print("num_face_now=" + str(num_face_now))
                        if num_face_now != num_face:
                            papero.send_start_speech("パペロは"+str(num_face_now)+"台です")
                        num_face = num_face_now
                        if do_show:
                            for (x,y,w,h) in faces:
                                cv2.rectangle(img, (x,y), (x+w,y+h), (255,0,0), 2)
                            cv2.imshow('image', img)
                            cv2.waitKey(1)
                    state = State.st3
        if msg_dic_rcv is not None:
            if msg_dic_rcv["Name"] == "detectButton":
                state = State.end


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description = "Usage:")
    parser.add_argument("host", type=str, help = "Host IP address")
    parser.add_argument("-img", help = "Display image", action='store_true')
    command_arguments = parser.parse_args()
    simulator_id = ""
    robot_name = ""
    host = command_arguments.host
    do_show = command_arguments.img
    ws_server_addr = "ws://" + host + ":8088/papero"
    papero = pypapero.Papero(simulator_id, robot_name, ws_server_addr)
    main(papero, host, do_show)
    papero.papero_cleanup()

Raspberry Pi 上の OpenCV でPaPeRo i に人の顔を数えさせる」で作成したcv_numface.pyでは、

face_cascade = cv2.CascadeClassifier(cascade_file)

で使用するcascade_fileへの代入文が、

cascade_file = '/usr/local/share/OpenCV/haarcascades/haarcascade_frontalface_default.xml'

となっていましたが、今回の cv_numpaperoi.py では

cascade_file = 'traincascade/cascade/paperoi/cascade.xml'

のように、今回の学習で生成された cascade.xml を指定しています。
また、数える物が変わったのに合わせて、発話するセリフも若干変えています。

(6) cv_numpaperoi.py を実行します。

$ cd ~/papero
$ python3 cv_numpaperoi.py PaPeRoiのIPアドレス

実行すると、PaPeRo i が「検知したパペロの数を発話します。座布団のボタンで終了します。」と発話した後、カメラに映ったPaPeRo i の顔を認識したらその数をカウントし、数に変化があった時に、その数を発話するようになります。

Raspberry pi のデスクトップ上に開いたターミナルでで実行する場合、

$ python3 cv_numpaperoi.py PaPeRoiのIPアドレス -img

のように -img オプションをつけて実行すると、カメラで撮影された画像と検知した顔の範囲が表示されます。

結果

今回の実験では、学習に使用した画像枚数が少なかったためか、PaPeRo i が掲載されている資料の印刷物を PaPero i に見せても、時々しか認識されず、複数写っている PaPeRo i が全てカウントされる事は、ほとんどありませんでした。
ポジティブ画像の背景が白一色であったせいか、PaPeRo i の背景が白一色でない画像についての認識状況が特に悪いようでした。
opencv_createsamples の-num オプションで指定する生成画像枚数を100及び200に変え、opencv_traincascade の -numPos の数字を90及び180に変えてみても、状況は改善しませんでした。
学習時間については -numPos の値を90にした時でも1分程度で学習が完了する場合もあり、画像枚数が増えれば学習時間が必ずしも増えるというわけではないようです。

今回、ポジティブ画像は1枚の画像から生成しましたが、背景を変えたものをいくつか追加すれば、認識精度が向上するかもしれません。
ただし、複数枚のポジティブ画像を自分で用意する場合、opencv_createsamples の自動回転機能は使えませんので、回転させた画像を含めて自前で作る必要があります。

画像枚数の増加と別PCでの学習

試しにopencv_createsamples の-num オプションで指定する生成画像枚数を1000にしてみた所、ベクトルファイルの作成には成功するものの、opencv_traincascade で -numPos 900 を指定すると、メモリ不足のためか途中で終了してしまい、Raspberry Pi 上では学習を完了させる事ができませんでした。
そこで、OpenCVをインストールした別PC(Windows10)で学習を行い、学習結果の cascade.xml を Raspberry Pi 上の ~/papero/traincascade/cascade/paperoi/ の下に配置し、cv_numpaperoi.py を実行してみました。
ポジティブ画像の元画像やネガティブ画像を増強したわけではないので、認識状況はやはり改善しませんでしたが、WindowsPC等の別マシンで作成した cascade.xml を Raspberry Pi 上で使う事については問題ないようです。
メモリ容量・CPUパワー等により Raspberry Pi 上での学習に問題がある場合は、基本性能の高い別PCで学習させるとよいでしょう。


0