世界一いらない人工知能??OpenCVを用いたカワウソ分類器作成奮闘記

こんにちは!アイデミー研修生の川内と申します。

突然ですが、みなさんカワウソってご存知ですか?? 可愛いですね〜〜。
よく犬と猫どっち派とか聞かれますが僕は断然カワウソ派です。

OpenCVというのを使うとデフォルトで作成されているモデルを用いて人間の顔が検出することが出来ます。OpenCVについては下記のリンクをご覧ください。

機械学習のためのOpenCV入門

OpenCVで物体検出器を作成① 基礎知識【開発会社プロフェッサ】

人の顔の画像の特徴量を抽出することにより学習するのですが、学習させるモデルにおいてはHaar-like特徴というのを用いています。Haar-like特徴は、簡単に言うと画像の明暗差により特徴を捉えます。例えば人間顔で言えば目は黒く、目元は明るいといった特徴をたくさん取ることで、人間の顔の特徴全体を捉える感じです。

Haar-likeについて(英語で書かれています)

Face Detection using Haar Cascades — OpenCV 3.0.0-dev documentation

和訳されたサイトもありました。

Haar Cascadesを使った顔検出 — OpenCV-Python Tutorials 1 documentation

ものは試しで早速やってみましょう

社長の石川です。良い顔していますね。

# -*- coding: utf-8 -*-

import cv2

#HAAR分類器の顔検出用の特徴量
cascade_path = "/usr/local/opt/opencv/share/OpenCV/haarcascades/haarcascade_frontalface_alt.xml"


image_path = "sample.jpg"

color = (255, 255, 255) #白

#ファイル読み込み
image = cv2.imread(image_path)

#カスケード分類器の特徴量を取得する
cascade = cv2.CascadeClassifier(cascade_path)

#物体認識(顔認識)の実行
#image – CV_8U 型の行列.ここに格納されている画像中から物体が検出されます
#objects – 矩形を要素とするベクトル.それぞれの矩形は,検出した物体を含みます
#scaleFactor – 各画像スケールにおける縮小量を表します
#minNeighbors – 物体候補となる矩形は,最低でもこの数だけの近傍矩形を含む必要があります
#flags – このパラメータは,新しいカスケードでは利用されません.古いカスケードに対しては,cvHaarDetectObjects 関数の場合と同じ意味を持ちます
#minSize – 物体が取り得る最小サイズ.これよりも小さい物体は無視されます
facerect = cascade.detectMultiScale(image, scaleFactor=1.1, minNeighbors=1, minSize=(1, 1))

if len(facerect) > 0:
    #検出した顔を囲む矩形の作成
    for rect in facerect:
        cv2.rectangle(image, tuple(rect[0:2]),tuple(rect[0:2]+rect[2:4]), color, thickness=2)

    #認識結果の保存
    cv2.imwrite("face_detected.jpg", image)

さて結果がこちらになります。

f:id:shoichitech:20180519155552j:plain

うまく出来ていますね。こんな感じでカワウソの画像を認識して顔を四角で囲ってみたくなりました。誰も使いません。世界一いらない人工知能と言っても過言ではないでしょう。ただ、カワウソが好きという理由だけで作ってみようと思い立ちました。下記のリンクの方が猫の顔検出モデルを作成されていたのでそれを参考に作ってみるという方針を立てました。画像さえ集められればモデル構築出来そうということが分かりました。

ねこと画像処理 part 2 – 猫検出 (モデル配布) « Rest Term

画像収集

さて、方針は決まって後は画像を集めるのですがここが一番の肝です。今回はFlickrという画像共有サイトのAPIを利用して画像を集めました。利用方法については下記を参考にしました。

Flickr APIを使って画像ファイルをダウンロードする

本来であれば正解データ7000枚、不正解データ3000枚ほど必要なのですがflickr APIではカワウソの画像400枚ほどしか集められませんでした。こうやればたくさん集められるよ!!というのを知っている方がいらっしゃれば教えてくださいm(_ _)m。不正解データは同様にしてパンダの画像を集めました。

正解データの例

f:id:shoichitech:20180519164511j:plain

不正解データの例

f:id:shoichitech:20180520214210j:plain

以下に画像取得の時に利用したスクリプトを載せておきます

import os

import time
import traceback

import flickrapi
from urllib.request import urlretrieve

import sys
from retry import retry

flickr_api_key = ""
secret_key = ""

keyword = sys.argv[1]


@retry()
def get_photos(url, filepath):
    urlretrieve(url, filepath)
    time.sleep(1)


if __name__ == '__main__':

    flicker = flickrapi.FlickrAPI(flickr_api_key, secret_key, format='parsed-json')
    response = flicker.photos.search(
        text=keyword,
        per_page=1000,
        media='photos',
        sort='relevance',
        safe_search=1,
        extras='url_n,license'
    )
    photos = response['photos']

    try:
        if not os.path.exists('./image-data/' + keyword):
            os.mkdir('./image-data/' + keyword)

        for photo in photos['photo']:
            try:
                url_q = photo['url_n']
                filepath = './image-data/' + keyword + '/' + keyword + "-" + photo['id'] + '.jpg'
                get_photos(url_q, filepath)
            except KeyError:
                print("error!!!")

    except Exception as e:
        traceback.print_exc()

正解データのラベル付け

さて、なんとか画像を集めることが出来たのですが、正解データの画像のどの座標の位置にカワウソの顔があるかを指定していかなければなりません。githubにブラウザで画像データの座標を記録出来るプログラムを作られてる方がいたのでそちらを利用させていただくことにしました。 github.com

f:id:shoichitech:20180519161056p:plain こんな感じで地道にカワウソの顔を囲っていきます。

画像が赤い四角形で囲まれた状態でNEXTボタンを押すと正解データに座標と共に分類され、何もない状態で押すと不正解データに分類されます。

しかし、カワウソの画像でも横顔のデータなどは正解データとしては不適切でそういった画像の時はSKIPボタンでどちらにも分類されないようにしました。集めたカワウソの画像の中には横顔だったり、二次元のキャラクターのカワウソも含まれていたので正解データに分類された画像は5割程となりました。

f:id:shoichitech:20180519164659j:plain 正解データとはならない写真の例

正解データ、不正解データに分類するのにものすごく時間かかりました・・・・。正解、不正解合わせて800枚ほどのデータが集まったのですが3時間くらいひたすら画像とにらめっこして分類してました。macのトラックパッドにクリックしすぎて指がおかしくなりましたが、ありとあらゆるカワウソの写真を見れて幸せな時間でもありました笑。

正解のデータのテキストファイルです。【画像ファイル名 対象の物体の数 x座標 y座標 width height】 の順に記録されます。

info.dat
static/img/otter-6814063158.jpg  1  50 24 66 50
static/img/otter-4494710692.jpg  1  174 30 52 38
static/img/otter-8885141162.jpg  1  78 42 67 60
static/img/otter-25450661078.jpg  1  102 36 69 49
static/img/otter-14377745688.jpg  1  38 46 152 97
static/img/otter-32616826780.jpg  1  44 22 133 101
static/img/otter-26964747057.jpg  1  57 55 37 24
static/img/otter-27695436712.jpg  1  64 26 84 67
static/img/otter-5105713030.jpg  1  113 35 75 54
static/img/otter-7394889242.jpg  1  35 73 51 43
static/img/otter-27807883499.jpg  1  89 31 77 50
static/img/otter-33148797350.jpg  1  99 42 37 36
static/img/otter-7941157504.jpg  1  126 14 58 41
static/img/otter-32646878413.jpg  1  49 89 35 27
static/img/otter-33520353216.jpg  1  110 49 52 46
.
.
.

モデル構築

そんなこんなで苦労して分類したデータをOpencvを用いて学習させてみました。まずOpenCVに認識してもらうために、正解データをバイナリファイルに変換する処理を行います。


opencv_createsamples -info info.dat -vec kawauso.vec -num 175

numはサンプル数です。学習させるために以下のコマンドを実行します。

opencv_traincascade -data kawauso/ -vec kawauso.vec -bg bg.txt -numPos 157 -numNeg 376 -featureType HAAR -mode ALL
  • data モデルの保存先です。kawausoというディレクトリを作成しておきました。
  • numPos 正解データの要素数を指定しているのですが、サンプル数×0.9くらいがいいらしいです。

OpenCVのtraincascadeのnumPos引数はvecファイル内のサンプル数より少ない数を設定すること – takminの書きっぱなし備忘録

  • featureType 機械学習の特徴量を指定しています。先ほど述べたHaar-Like特徴を用いていました。

検証

構築したモデルを使用してカワウソの顔が検出されるか実験してみました。

f:id:shoichitech:20180519170337j:plain

テスト画像 f:id:shoichitech:20180519200718j:plain

検証結果 なかなか良さそうです。他の画像でも検証してみました。

f:id:shoichitech:20180520184920j:plain f:id:shoichitech:20180520190008p:plain f:id:shoichitech:20180519212107j:plain

いい感じに検出されていますね。

f:id:shoichitech:20180519201304j:plain

友人の飼ってる猫。くろまるって言います。

カワウソ検出器では検出されませんでした。

f:id:shoichitech:20180519201449j:plain f:id:shoichitech:20180519212042j:plain

軍団カワウソ。うーん。やはり改善の余地がありそうです。

f:id:shoichitech:20180520184505j:plain

ラッコです。顔が似ているのか検出されました笑

検証の時に使用したスクリプトです。

import sys
import cv2 as cv

def detect(imagefilename, cascadefilename):
    srcimg = cv.imread(imagefilename)
    if srcimg is None:
        print('cannot load image')
        sys.exit(-1)
    dstimg = srcimg.copy()
    cascade = cv.CascadeClassifier(cascadefilename)
    if cascade.empty():
        print('cannnot load cascade file')
        sys.exit(-1)
    objects = cascade.detectMultiScale(srcimg, 1.1, 3)
    for (x, y, w, h) in objects:
        print(x, y, w, h)
        cv.rectangle(dstimg, (x, y), (x + w, y + h), (0, 0, 255), 2)
    return dstimg

if __name__ == '__main__':
    result = detect('otter1.jpg', './kawauso/cascade.xml')
    cv.imwrite('otter1_result.jpg', result)

感想

教師データをもう少し用意できたらよかったです。

機械学習は前処理が8割なんてもんじゃない。99%が前処理といっても過言じゃないと思いました。便利なライブラリやツールがある中で泥臭い作業こそ機械学習では必要なのだと体感しました。 こんなやり方あるよというのがありましたらぜひ教えてください!!

プログラミング未経験からでもAIスキルが身につくAidemy Premium




PythonやAIプログラミングを学ぶなら、オンライン制スクールのAidemy Premiumがおすすめです。
「機械学習・ディープラーニングに興味がある」
「AIをどのように活用するのだろう?」
「文系の私でもプログラミング学習を続けられるだろうか?」
少しでも気になることがございましたら、ぜひお気軽にAidemy Premiumの【オンライン無料相談会】にご参加いただき、お悩みをお聞かせください!