Pythonを利用したOpenCVプログラミング入門

Tweet


OpenCVプログラミング入門です.Pythonプログラミングです.OpenCVプログラミングの実例を使って短時間でちょっとしたメディアアート作品を作れるようにします.


画像出力

画像を作って絵を描いてファイル出力してみましょう.

# -*- coding: utf-8 -*-
import numpy as np
import cv2
def main():
    image = np.zeros((480, 640, 3), dtype=np.uint8)
    cv2.circle(image, (320,240), 200, (0,127,255), -1)
    cv2.imwrite('image.jpg', image)
   
if __name__ == '__main__':
    main()

image = np.zeros((480, 640, 3), dtype=np.uint8)
【np】OpenCVの画像はnumpy配列で表す.
【zeros】ゼロで埋まった行列を作成.
【(480,640,3),dtype=np.uint8】480行,640列,3チャンネルの8ビット符号なし整数の画像.

cv2.circle(image, (320,240), 200, (0,127,255), -1)
【circle】円を描く関数.関数の使い方はウェブで調べる.
【(0,127,255)】OpenCVはRGBではなくBGRの順.

cv2.imwrite('image.jpg', image)
【imwrite】画像ファイルとして出力する関数.

ファイルが書き込めない場合,まずは基本的なことを確認する.書き込めないディスク,管理者権限がないと書き込めないディレクトリ,ファイルが使用中,不正なファイル名,ディスクが壊れている,ファイル名が長すぎる,ファイルサイズが0,ファイルサイズが2GB以上,ディスク容量が足りない,プログラムにバグがある,ファイル名・ディレクトリ名に日本語が使われている,ファイル名・ディレクトリ名に空白(スペース)が使われている,….

例えば'image.jpg'と書いたとき,それはどのディレクトリなのか?そのソースコードと同じディレクトリ?その開発環境で設定されているデフォルトの開発用のディレクトリ?その開発環境で今見ているディレクトリ?それとも?

np.zerosでは(480,640,3)のように縦のサイズが先で横のサイズが後だが,circleでは(320,240)のように,横方向の座標が先で縦方向の座標が後.座標の指定方法は関数によって違うので注意.縦・行・y軸・高さ,横・列・x軸・幅,どちらが先か.

【Python】
image = np.zeros((480, 640, 3), dtype=np.uint8)のimageのように,変数は定義しなくてもすぐ使える.データ型を指定する必要はない.
1行につき1つの命令を書くので,C言語のように行末に「;」をつけない.
(480,640,3)のように丸カッコでタプル(組)を作る.
Pythonのコードにはデータ型はほとんど出てこないが,データ型を気にしなくていいわけではない.np.uint8のようにデータ型を指定しないといけない場合も多い.データ型をよく考えて実装すること.
省略可能な引数に対して,省略せずに引数を与えたい場合,dtype=np.uint8のように関数を呼び出す.途中の引数を飛ばすこともできる.


カメラ画像をファイル出力

パソコンに接続されたカメラ映像をファイル出力してみましょう.

# -*- coding: utf-8 -*-
import cv2
def main():
    capture = cv2.VideoCapture(0)
    _, image = capture.read()
    cv2.imwrite('image.jpg', image)
   
if __name__ == '__main__':
    main()

cv2.VideoCapture(0)
【0】引数は0以上の整数でカメラの番号を指定する.

_, image = capture.read()
【image】read関数の返り値の2つ目がカメラ映像.

パソコンが認識したそれぞれのカメラには0番から整数が割り振られている.まずは0番で試してみて,うまいくいかなったら1番,…のように試していく.
「自分のパソコンに接続されているカメラは1つだから0番でうまくいくはず」という思い込みは禁物.「仮想的なカメラ」も存在するので,目視で確認したカメラの数と,OSが認識しているカメラの数は異なる場合がある.
「以前は0番でうまくいったのに今はうまくいかない」.何もしなくても番号が変わることはある.

どのカメラも動作しない場合.プログラムにバグがある.カメラがOSに認識されていない.カメラのデバイスドライバが落ちた.OpenCVでは扱えない特殊なカメラ.レンズキャップを外していない.他のソフトがカメラを使っている(ソフトでカメラ映像を表示していなくても,ソフト内部ではカメラを開いている可能性もある).カメラを制御するライブラリが初期化に時間がかかっている.その開発環境ではカメラ入出力に対応していない.その他.

【Python】
_, frame = capture.read()にあるように,read関数の返り値は2つ組(ペア)のタプル.タプルを複数の変数にアンパックすることもできる.必要ないものは「_」と書けばいい.


カメラ画像をウィンドウ表示

パソコンに接続されたカメラ映像をウィンドウに表示してみましょう.

# -*- coding: utf-8 -*-
import cv2
def main():
    capture = cv2.VideoCapture(1)
    while True:
        _, frame = capture.read()
        cv2.imshow('My OpenCV Program', frame)
        if cv2.waitKey(1) & 0xFF == 0x1B:
            break
    cv2.destroyAllWindows()
   
if __name__ == '__main__':
    main() 

cv2.imshow('My OpenCV Program', frame)
【imshow】imshow関数で画像を表示.

if cv2.waitKey(1) & 0xFF == 0x1B:
【waitKey】waitKey関数に1を指定すると,1ミリ秒待って,キーボードが押されたらそのキーコードを返し,キーボードが押されていないなら即座にプログラムが続行する.このプログラムではESCを押すと終了する.

cv2.destroyAllWindows()
【destroyAllWindows】ウィンドウを閉じる.

最後にdestroyAllWindowsを呼び出してウィンドウを閉じること.プログラムを終了するとウィンドウが自動的に閉じられるプログラミング言語や開発環境もあるが,そうではないものもあるので念のためdestroyAllWindowsを呼んだほうがいい.

【Python】
while文の範囲はインデントで表す.Pythonではインデントもプログラムの一部.C言語のように適当にインデントしてはいけない.


カメラの映像に文字を表示

カメラ映像に文字列を書いてみましょう.

# -*- coding: utf-8 -*-
import numpy as np
import cv2
def main():
    capture = cv2.VideoCapture(1)
    if not capture.isOpened():
        print('Cannot open camera')
        return
    numberframe = 0
    start = cv2.getTickCount()
    while True:
        numberframe += 1
        end = cv2.getTickCount()
        numbertime = np.floor((end - start) / cv2.getTickFrequency())
        ret, frame = capture.read()
        if ret:
            message = 'Frame %d' % numberframe
            cv2.putText(frame, message, (100, 100), cv2.FONT_HERSHEY_SIMPLEX, 2, (0, 0, 0), 2)
            message = 'Time %d' % numbertime
            cv2.putText(frame, message, (100, 200), cv2.FONT_HERSHEY_SIMPLEX, 2, (0, 0, 0), 2)
            cv2.imshow('My OpenCV Program', frame)
        key = cv2.waitKey(1) & 0xFF
        if key == 0x1B:
            break
        if key == ord(' '):
            cv2.imwrite('screenshot.jpg', frame)
    capture.release()
    cv2.destroyAllWindows()
   
if __name__ == '__main__':
    main() 

if not capture.isOpened():
ちゃんとカメラを開くことができたかどうかを確認したほうがいい.

ret, frame = capture.read()
if ret:
ちゃんとカメラ映像を取得できたかどうかを確認したほうがいい.

capture.release()
使い終わったカメラは閉じたほうがいい.

cv2.putText(frame, message, (100, 100), cv2.FONT_HERSHEY_SIMPLEX, 2, (0, 0, 0), 2)
文字列の描画にはputText関数を使う.

【Python】
C言語のようなインクリメント「++」演算子はないので,「+=1」のように1を足すコードを書く.
C言語のsprintf文の代わりにPythonではmessage = 'Frame %d' % numberframeのように実装すればいい.
ord関数でstr型をASCIIコードの数値に変換できる.


画像ファイルの読み込み

画像ファイルを読み込んで表示してみましょう.

# -*- coding: utf-8 -*-
import cv2
def main():
    input = cv2.imread('image.jpg')
    cv2.imshow('My OpenCV Program', input)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
   
if __name__ == '__main__':
    main()

cv2.waitKey(0)
waitKeyの引数が0の場合,キーボードを押すまで待ち続ける.キーボードが押されたらプログラムが続行する.このプログラムの場合はプログラムが終了する.

ファイルが読み込めない場合,まずは基本的なことを確認する.ファイルがない,ファイルがおかしい,ファイルサイズが0,ファイルサイズが2GB以上,ファイルが壊れている,ファイルが使用中,ファイル名が間違っている,プログラムにバグがある,ファイル名・ディレクトリ名に日本語が使われている,ファイル名・ディレクトリ名に空白(スペース)が使われている,….

例えば'image.jpg'と書いたとき,それはどのディレクトリなのか?そのソースコードと同じディレクトリ?その開発環境で設定されているデフォルトの開発用のディレクトリ?その開発環境で今見ているディレクトリ?それとも?


画像に画像を貼り付ける

画像に画像を重ね合わせてみましょう(合成,重畳).

# -*- coding: utf-8 -*-
import numpy as np
import cv2
def main():
    bgimg = cv2.imread('background.jpg')
    fgimg = cv2.imread('foreground.jpg')
    mat = np.array([[0.5, 0.0, 300.0], [0.0, 0.5, 300.0]], dtype=np.float32)
    rows, cols, ch = bgimg.shape
    cv2.warpAffine(fgimg, mat, (cols, rows), bgimg, borderMode=cv2.BORDER_TRANSPARENT)
    cv2.imshow('My OpenCV Program', bgimg)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
   
if __name__ == '__main__':
    main() 

foreground.jpg

background.jpg

画像に画像を貼り付ける方法として,配列の要素をコピーする方法もあるが,その場合,不正なメモリアクセスをしないよう,画像の外にはみ出ない工夫をしなければいけない.warpAffine関数を使った場合は,画像の外にはみ出て貼り付けても適切に処理される.また,貼り付ける位置の指定だけでなく,拡大・縮小や回転もできる.

np.array([[0.5, 0.0, 300.0], [0.0, 0.5, 300.0]], dtype=np.float32)
この例の場合,0.5倍して,左上が(300,300)の場所に貼り付ける,2行3列のアフィン変換行列を表す.

rows, cols, ch = bgimg.shape
shapeで配列のサイズを取得できる.画像の場合は,画像の高さ,幅,チャンネル数が得られる.


画像に透過画像を貼り付ける

画像に透過型PNG画像を重ね合わせてみましょう(合成,重畳).

# -*- coding: utf-8 -*-
import numpy as np
import cv2
def main():
    bgimg = cv2.imread('background.jpg', cv2.IMREAD_COLOR)
    fgimg = cv2.imread('foreground.png', cv2.IMREAD_UNCHANGED)
    bgb, bgg, bgr = cv2.split(bgimg)
    fgb, fgg, fgr, fga = cv2.split(fgimg)
    rows, cols, ch = bgimg.shape
    warpb = np.zeros((rows, cols), np.uint8)
    warpg = np.zeros((rows, cols), np.uint8)
    warpr = np.zeros((rows, cols), np.uint8)
    warpa = np.zeros((rows, cols), np.uint8)
    mat = np.array([[0.5, 0.0, 300.0], [0.0, 0.5, 300.0]], dtype=np.float32)
    cv2.warpAffine(fgb, mat, (cols, rows), warpb, borderMode=cv2.BORDER_TRANSPARENT)
    cv2.warpAffine(fgg, mat, (cols, rows), warpg, borderMode=cv2.BORDER_TRANSPARENT)
    cv2.warpAffine(fgr, mat, (cols, rows), warpr, borderMode=cv2.BORDER_TRANSPARENT)
    cv2.warpAffine(fga, mat, (cols, rows), warpa, borderMode=cv2.BORDER_TRANSPARENT)
    bgb = bgb / 255.0
    bgg = bgg / 255.0
    bgr = bgr / 255.0
    warpb = warpb / 255.0
    warpg = warpg / 255.0
    warpr = warpr / 255.0
    warpa = warpa / 255.0
    bgb = (1.0 - warpa) * bgb + warpa * warpb
    bgg = (1.0 - warpa) * bgg + warpa * warpg
    bgr = (1.0 - warpa) * bgr + warpa * warpr
    result = cv2.merge((bgb, bgg, bgr))
    cv2.imshow('My OpenCV Program', result)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
   
if __name__ == '__main__':
    main() 

foreground.png

background.jpg

pngファイルは透過型のpngファイルを用意すること.

bgimg = cv2.imread('background.jpg', cv2.IMREAD_COLOR)
fgimg = cv2.imread('foreground.png', cv2.IMREAD_UNCHANGED)
bgb, bgg, bgr = cv2.split(bgimg)
fgb, fgg, fgr, fga = cv2.split(fgimg)
split関数で各チャンネルを分離することができる.

result = cv2.merge((bgb, bgg, bgr))
merge関数で複数チャンネルをまとめることができる.

rows, cols, ch = bgimg.shape
warpb = np.zeros((rows, cols), np.uint8)
warpg = np.zeros((rows, cols), np.uint8)
warpr = np.zeros((rows, cols), np.uint8)
warpa = np.zeros((rows, cols), np.uint8)
mat = np.array([[0.5, 0.0, 300.0], [0.0, 0.5, 300.0]], dtype=np.float32)
cv2.warpAffine(fgb, mat, (cols, rows), warpb, borderMode=cv2.BORDER_TRANSPARENT)
cv2.warpAffine(fgg, mat, (cols, rows), warpg, borderMode=cv2.BORDER_TRANSPARENT)
cv2.warpAffine(fgr, mat, (cols, rows), warpr, borderMode=cv2.BORDER_TRANSPARENT)
cv2.warpAffine(fga, mat, (cols, rows), warpa, borderMode=cv2.BORDER_TRANSPARENT)
warpAffine関数による変形は各チャンネルそれぞれで実行.その際の背景は黒.

bgb = bgb / 255.0
bgg = bgg / 255.0
bgr = bgr / 255.0
warpb = warpb / 255.0
warpg = warpg / 255.0
warpr = warpr / 255.0
warpa = warpa / 255.0
各チャンネルを255で割って,0~1の浮動小数点数にする.ウェブで見かけるスプライト表示のプログラム例の多くはビット演算だが,このプログラムではアルファブレンディングで計算している.

bgb = (1.0 - warpa) * bgb + warpa * warpb
bgg = (1.0 - warpa) * bgg + warpa * warpg
bgr = (1.0 - warpa) * bgr + warpa * warpr
アルファ値は0~1で,pngファイルの場合,0が透明,1が不透明.計算式は「(1-α)×背景+α×前景」.α値が0なら,(1-α)×背景+α×前景=背景.α値が1なら,(1-α)×背景+α×前景=前景.pngの透過部分は背景になって,pngの不透明部分はpngの画像になる.


領域分割

前景と背景の分割.前景の一部を赤で指定.背景の一部を青で指定.

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

import numpy as np
import cv2

def main():
    inputImage = cv2.imread('image.bmp', cv2.IMREAD_COLOR)
    inputMask = cv2.imread('mask.bmp', cv2.IMREAD_COLOR)
    if inputImage is None:
        return
    if inputMask is None:
        return
    if inputImage.shape != inputMask.shape:
        return
    rows, cols, _ = inputImage.shape
    strokeImage = inputImage.copy()
    processMask = np.zeros((rows, cols), dtype=np.uint8)
    regionImage = inputMask.copy()
    compositeImage = inputImage.copy()
    fgImage = inputImage.copy()

    for y in range(0, rows):
        for x in range(0, cols):
            b, g, r = inputMask[y, x]
            if r == 255 and g == 0 and b == 0:
                processMask[y, x] = cv2.GC_FGD
                strokeImage[y, x] = (0, 0, 255)
            elif r == 0 and g == 0 and b == 255:
                processMask[y, x] = cv2.GC_BGD
                strokeImage[y, x] = (255, 0, 0)
            else:
                processMask[y, x] = [cv2.GC_PR_FGD, cv2.GC_PR_BGD][(x + y) % 2]

    bgdModel = np.zeros((1,65), dtype=np.float64)
    fgdModel = np.zeros((1,65), dtype=np.float64)
    iterCount = 4
    cv2.grabCut(inputImage, processMask, None, bgdModel, fgdModel, iterCount, mode=cv2.GC_INIT_WITH_MASK)

    for y in range(0, rows):
        for x in range(0, cols):
            compositeImage[y, x] = compositeImage[y, x] / 2
            category = processMask[y, x]
            if category == cv2.GC_FGD or category == cv2.GC_PR_FGD:
                regionImage[y, x] = (0, 0, 255 if category == cv2.GC_FGD else 127)
                compositeImage[y, x] = compositeImage[y, x] + (0, 0, 128)
            elif category == cv2.GC_BGD or category == cv2.GC_PR_BGD:
                regionImage[y, x] = (255 if category == cv2.GC_BGD else 127, 0, 0)
                compositeImage[y, x] = compositeImage[y, x] + (128, 0, 0)
                fgImage[y, x] = (255, 255, 255)

    cv2.imwrite('stroke.bmp', strokeImage)
    cv2.imwrite('region.bmp', regionImage)
    cv2.imwrite('composite.bmp', compositeImage)
    cv2.imwrite('foreground.bmp', fgImage)

if __name__ == '__main__':
    main()
 

image.bmp

mask.bmp

cv2.grabCut(img,mask,rect,bgdModel,fgdModel,iterCount,mode)
iterCountは反復回数.1~10ぐらい.
bgdModelとfgdModelはサイズ(1,65)のfloat64型を用意するだけ.使わない.
modeはGC_INIT_WITH_MASKを指定する.
rectはNoneを指定する.
maskに1チャンネル8bitで入力画像imgと同じサイズのnumpy行列を指定する.

maskは以下の4つの値のいずれか.
GC_BGD(値は0)ユーザが背景だと指定した画素を表す
GC_FGD(値は1)ユーザが前景だと指定した画素を表す
GC_PR_BGD(値は2)背景である可能性のある画素を表す
GC_PR_FGD(値は3)前景である可能性のある画素を表す

※注意:maskを0に初期化したままにしてはいけない(GC_BGDなので背景のまま変化がない)

ユーザが指定した画素にGC_FGDやGC_BGDを設定する.
それ以外の画素にGC_PR_FGDやGC_PR_BGDを設定する.

grabCut関数の領域分割結果はGC_PR_FGDやGC_PR_BGDとして上書きして出力される.

【Python】
コードの途中でif文を書く場合(C言語で言う3項演算子)
真の場合 if 条件 else 偽の場合

【Python】
b=a 参照渡し.bの内容を変えるとaの内容まで変わってしまう.
b=a.copy() コピー(クローン).bの内容を変えてもaの内容は変わらない.numpyのcopyは深いコピー.


顔検出

顔を検出してみましょう.

# -*- coding: utf-8 -*-
import cv2
def main():
    cascade = cv2.CascadeClassifier('C:\\Users\\Daisuke\\anaconda3\\Lib\\site-packages\\cv2\\data\\haarcascade_frontalface_default.xml')
    image = cv2.imread('image.jpg')
    faces = cascade.detectMultiScale(image)
    for x, y, w, h in faces:
        cv2.rectangle(image, (x, y), (x + w, y + h), (0, 0, 255))
    cv2.imshow('My OpenCV Program', image)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
   
if __name__ == '__main__':
    main()

Haar-like特徴によるAdaBoost(いわゆるViola-Jones).OpenCVの関数として用意されていて,顔については学習済みモデルも用意されている.

cascade = cv2.CascadeClassifier('C:\\Users\\Daisuke\\anaconda3\\Lib\\site-packages\\cv2\\data\\haarcascade_frontalface_default.xml')
学習済みモデルはOpenCVが入っているディレクトリにある.どのディレクトリにあるかは,その人の環境による.ネットの情報を参考にしたり,ファイルを検索したりしてがんばって探そう.見つからなくても,ウェブでファイルが手に入るので,それを使ってもいい.顔以外も,様々な学習済みモデルをネットで公開している人もいるので,それらを使ってみるのもいいかも.自分で学習データを集めて学習させれば,自分の検出したい物を検出することができる.

faces = cascade.detectMultiScale(image)
for x, y, w, h in faces:
detectMultiScale関数の返り値に顔検出結果が格納される.検出位置の左上の(x,y)座標と,幅と高さが返ってくる.それが検出された顔の分だけ返ってくる.


SIFTによるマッチング

物体を検出してみましょう.

# -*- coding: utf-8 -*-
import numpy as np
import cv2
def main():
    marker = cv2.imread('marker.jpg')
    camera = cv2.imread('camera.jpg')
    description = cv2.xfeatures2d.SIFT_create()
    kp1, des1 = description.detectAndCompute(marker, None)
    kp2, des2 = description.detectAndCompute(camera, None)
    bf = cv2.BFMatcher()
    matches = bf.knnMatch(des1, des2, k=2)
    good = []
    for m, n in matches:
        if m.distance < 0.7 * n.distance:
            good.append(m)
    src_pts = np.float32([ kp1[m.queryIdx].pt for m in good ]).reshape(-1,1,2)
    dst_pts = np.float32([ kp2[m.trainIdx].pt for m in good ]).reshape(-1,1,2)
    if len(good) > 10:
        M, _ = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0)
        if M is not None:
            h, w, _ = marker.shape
            pts = np.float32([ [w/2,h/2] ]).reshape(-1,1,2)
            dst = cv2.perspectiveTransform(pts,M)
            centerx = np.int32(dst[0][0][0])
            centery = np.int32(dst[0][0][1])
            cv2.line(camera, (centerx - 20, centery), (centerx + 20, centery), (0, 0, 255), 2)
            cv2.line(camera, (centerx, centery - 20), (centerx, centery + 20), (0, 0, 255), 2)
    cv2.imshow('My OpenCV Program', camera)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
   
if __name__ == '__main__':
    main() 

marker.jpg

camera.jpg

複雑な模様の物体のほうが検出しやすい.実際に使用するカメラで撮影した画像をテンプレートとして使用したほうがいい.

実際にOpenCVに入っているORB,AKAZE,BRISKなど色々試してみた結果,SIFTしかまともに検出できなかった.SIFT以外だとKAZEがそこそこ性能がいい.

SIFTとSURFはライセンスの関係で,OpenCVのバージョンによっては,デフォルトではインストールされなかったり,別のディレクトリにあったり,xfeatures2dとは異なるクラス構造になっていたりするので,ウェブ上の情報を見ながら,なんとかがんばってSIFTが使えるようにしてください.pip install opencv-pythonだとSIFTがインストールされず,pip install opencv-contrib-pythonでSIFTがインストールされる,ということらしいです.

ウェブの情報によると,SIFTは2020/3/6に特許が切れたが,SURFは2020年現在,まだ特許が継続しているようです.

description = cv2.xfeatures2d.SIFT_create()
SIFT_create関数でSIFTを初期化
バージョンによってはcv2.SIFT_create()であることに注意

kp1, des1 = description.detectAndCompute(marker, None)
kp2, des2 = description.detectAndCompute(camera, None)
detectAndComputeで特徴点を検出する.返り値の1つ目の要素は検出された点(keypoint)で,そのptに点の2次元座標が入っている.返り値の2つ目の要素は記述子(descriptor),つまり特徴量.

bf = cv2.BFMatcher()
matches = bf.knnMatch(des1, des2, k=2)
BFMatcherのknnMatchで同じ特徴点同士を対応づける.
より高速なk-nn法を使うと低速になるので注意.高速な手法は「探索」が速いが「前処理」は遅い.前処理で木やテーブルやグラフなどを構築するので.1フレームごとに前処理を計算しなおすので遅くなる.高速な手法が有効なのは,探索すべきデータベースが変わらない状態で,探索を多数回おこなう場合.実際,このプログラムでやってみたところ,FLANNのknnMatchのほうがBrute ForceのknnMatchより10倍ほど遅かった.

matches = bf.knnMatch(des1, des2, k=2)
good = []
for m, n in matches:
    if m.distance < 0.7 * n.distance:
        good.append(m)
knnMatch関数の返り値はマッチング結果.knnMatchでk=2を指定することで,一致する上位2つを見つける.
distanceは相違度(距離).2つの点の特徴量がどれだけ違うかを表す.

【上の例】10<0.7*100が成り立つ.1位のペアはかなり似ている.2番目に似ている点とはかなり違う.この特徴点は他の特徴点とは違って独特.
→このマッチングは信頼できる.

【下の例】40<0.7*50が成り立たない.1番似ている点とはそれほど似ていない.2番目も似たような点.似た特徴点がたくさんあって,どの点とも似ている.
→このマッチングは信頼できない.

kp1, des1 = description.detectAndCompute(marker, None)
kp2, des2 = description.detectAndCompute(camera, None)
src_pts = np.float32([ kp1[m.queryIdx].pt for m in good ]).reshape(-1,1,2)
dst_pts = np.float32([ kp2[m.trainIdx].pt for m in good ]).reshape(-1,1,2)
どの点とどの点がマッチングしたかを表す添え字(index)はqueryIdxとtrainIdxで取得する.その添え字はknnMatchで引数として与えたリストの添え字であり,detectAndComputeの返り値のリストの添え字.knnMatchの最初の引数で表すリストの添え字がqueryIdxで,2番目の引数で表すリストの添え字がtrainIdx.

if len(good) > 10:
対応点が11組以上見つかったら物体検出に成功したものとする.

M, _ = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0)
findHomographyで対応点同士の間のホモグラフィー変換行列を計算する.

dst = cv2.perspectiveTransform(pts,M)
perspectiveTransformでホモグラフィー変換を計算できる.

このプログラムでは,テンプレート画像の中央の画素位置を変換している.それがカメラ画像のどの画素位置か,ということが得られるので,そこに十字のマークを描画している.

【Python】
[]は空のリスト.appendで要素を追加する.
a=[]
a.append(1)
a.append(2)
a.append(3)
とするとaは[1,2,3]になる.

【Python】
Pythonのfor文はrange-based for文(foreach文).
a=[1,2,3]
b=0
for m in a:
    b+=m
とするとbは6になる.

【Python】
Pythonはリストの中でfor文を書ける.リスト内包表記というらしい.
a=[1,2,4]
b=[1/m for m in a]
とするとbは[1.0,0.5,0.25]になる.

【Python】
reshapeで行列の形を変えることができる.
import numpy as np
a=np.array([1,2,3,4,5,6])
b=a.reshape(2,3)
とするとbは[[1,2,3],[4,5,6]]のnumpy行列になる.
reshape(-1,1,2)であれば,C言語の配列で表現すると[][1][2]ということ.

【Python】
Noneと比較するときは==ではなくisを使う.


トラッキング

物体を追跡する.赤い四角に追跡したい物体を配置します.スペースキーを押します.その物体を動かしてもその位置を検出して,検出位置に赤い四角を描きます.

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

import numpy as np
import cv2

def main():
    tracker = cv2.TrackerKCF_create()
    capture = cv2.VideoCapture(0)
    while True:
        _, camimg = capture.read()
        camimg = cv2.flip(camimg, 1)
        rows, cols, _ = camimg.shape
        x1, x2, y1, y2 = cols / 2 - 50, cols/ 2 + 50, rows / 2 - 50, rows / 2 + 50
        x1, x2, y1, y2 = int(x1), int(x2), int(y1), int(y2)
        cv2.rectangle(camimg, (x1 - 2, y1 - 2), (x2 + 2, y2 + 2), (0, 0, 255), 2)
        message = 'Put object inside and press space'
        cv2.putText(camimg, message, (50, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2)
        cv2.imshow('My OpenCV Program', camimg)
        key = cv2.waitKey(1) & 0xFF
        if key == ord(' '):
            tracker.init(camimg, (x1, y1, x2 - x1, y2 - y1))
            break
    while True:
        _, camimg = capture.read()
        camimg = cv2.flip(camimg, 1)
        _, (x, y, w, h) = tracker.update(camimg)
        cv2.rectangle(camimg, (x - 2, y - 2), (x + w + 2, y + h + 2), (0, 0, 255), 2)
        cv2.imshow('My OpenCV Program', camimg)
        key = cv2.waitKey(1) & 0xFF
        if key == 0x1B:
            break
    capture.release()
    cv2.destroyAllWindows()

if __name__ == '__main__':
    main()

最新のOpenCVはパーティクルフィルタ(particle filter, condensation, MCMC)よりも良いトラッキングアルゴリズムが実装されている.

トラッキングはopencv-contrib-pythonに入っている.パッケージはアンインストールしてからインストールしたほうがいい.
pip uninstall opencv-python
pip uninstall opencv-contrib-python
pip install opencv-python
pip install opencv-contrib-python

cv2.TrackerKCF_createで作成
tracker.initで追跡を開始する領域を指定
tracker.updateで追跡して検出した位置を取得


ゲーム作品の一例

青くて丸い紙切れを手で操作して,画面上に表示されている赤い丸にタッチするゲームを作ってみましょう.

# -*- coding: utf-8 -*-
import numpy as np
import cv2
def main():
    circler = 20
    hit = True
   
    capture = cv2.VideoCapture(0)
    while True:
        _, camimg = capture.read()
        camimg = cv2.flip(camimg, 1)
        rows, cols, ch = camimg.shape
       
        hsvimg = cv2.cvtColor(camimg, cv2.COLOR_BGR2HSV)
        cv2.imshow('Debug window: hsvimg', hsvimg)
       
        binimg = cv2.inRange(hsvimg, (90, 120, 50), (120, 250, 200))
        cv2.imshow('Debug window: binimg', binimg)
       
        playerx = -10000
        playery = -10000
        fillmax = 0.5
       
        box1img = binimg.copy()
        box2img = camimg.copy()
       
        nlabels, _, stats, centroids = cv2.connectedComponentsWithStats(binimg)
       
        for i in range(0, nlabels):
            left = stats[i,cv2.CC_STAT_LEFT]
            top = stats[i,cv2.CC_STAT_TOP]
            width = stats[i,cv2.CC_STAT_WIDTH]
            height = stats[i,cv2.CC_STAT_HEIGHT]
            area = stats[i,cv2.CC_STAT_AREA]
            centerx = np.int32(centroids[i,0])
            centery = np.int32(centroids[i,1])
            cv2.rectangle(box1img, (left, top), (left + width, top + height), 255)
            cv2.rectangle(box2img, (left, top), (left + width, top + height), (0, 0, 255))
            if area > 100 and area < 100000:
                aspect = np.min((width, height)) / np.max((width, height))
                if aspect > 0.7:
                    fill = area / (height * width)
                    if fill > fillmax:                       
                        playerx = centerx
                        playery = centery
                        fillmax = fill
       
        cv2.line(box1img, (playerx - circler, playery), (playerx + circler, playery), 0, 2)
        cv2.line(box1img, (playerx, playery - circler), (playerx, playery + circler), 0, 2)
        cv2.imshow('Debug window: box1img', box1img)
        cv2.line(box2img, (playerx - circler, playery), (playerx + circler, playery), (0, 0, 0), 2)
        cv2.line(box2img, (playerx, playery - circler), (playerx, playery + circler), (0, 0, 0), 2)
        cv2.imshow('Debug window: box2img', box2img)
       
        cv2.line(camimg, (playerx - circler, playery), (playerx + circler, playery), (0, 0, 0), 2)
        cv2.line(camimg, (playerx, playery - circler), (playerx, playery + circler), (0, 0, 0), 2)
       
        if hit:
            circlex = np.random.randint(circler, cols - 2 * circler)
            circley = np.random.randint(circler, rows - 2 * circler)
        cv2.circle(camimg, (circlex, circley), circler, (0, 0, 255), -1)
       
        dist = (playerx - circlex) ** 2 + (playery - circley) ** 2
        if dist < (2 * circler) ** 2:
            hit = True
        else:
            hit = False
       
        cv2.imshow('OpenCV Game', camimg)
       
        key = cv2.waitKey(1) & 0xFF
        if key == 0x1B:
            break
        if key == ord(' '):
            cv2.imwrite('debug-hsvimg.jpg', hsvimg)
            cv2.imwrite('debug-binimg.jpg', binimg)
            cv2.imwrite('debug-box1img.jpg', box1img)
            cv2.imwrite('debug-box2img.jpg', box2img)
            cv2.imwrite('game.jpg', camimg)
    cv2.destroyAllWindows()
   
if __name__ == '__main__':
    main()

camimg = cv2.flip(camimg, 1)
flip関数で左右反転させている.腕の動きと同じ映像にしないと操作しづらいので.

hsvimg = cv2.cvtColor(camimg, cv2.COLOR_BGR2HSV)
cvtColor関数でHSVに変換.HSVの値がどのように表現されているかはウェブで関数の説明を調べる.

binimg = cv2.inRange(hsvimg, (90, 120, 50), (120, 250, 200))
inRange関数で,色相(hue)が90~120,彩度(saturation)が120~250,明度(value)が50~200の画素を抽出している.このプログラムを自分でも実行する人は,各自の環境に合わせてこの数値を適切に調整する必要がある.

nlabels, _, stats, centroids = cv2.connectedComponentsWithStats(binimg)
for i in range(0, nlabels):
二値画像処理で青い丸を検出.あまりロバストな方法ではないので,説明は省略する.

cv2.line(camimg, (playerx - circler, playery), (playerx + circler, playery), (0, 0, 0), 2)
cv2.line(camimg, (playerx, playery - circler), (playerx, playery + circler), (0, 0, 0), 2)
検出された青い丸に十字の図形を描画.

if hit:
    circlex = np.random.randint(circler, cols - 2 * circler)
    circley = np.random.randint(circler, rows - 2 * circler)
赤い丸に当たったら,ランダムに赤い丸の場所を変える.

cv2.circle(camimg, (circlex, circley), circler, (0, 0, 255), -1)
赤い丸を描画.

dist = (playerx - circlex) ** 2 + (playery - circley) ** 2
if dist < (2 * circler) ** 2:
    hit = True
else:
    hit = False
青い丸と赤い丸の距離が近ければ,両者が衝突したことになる.

key = cv2.waitKey(1) & 0xFF
if key == 0x1B:
    break
if key == ord(' '):
    cv2.imwrite('game.jpg', camimg)
ESCキーを押したら終了.スペースキーを押したら画像ファイルに出力.

【Python】
「**」はべき乗(累乗).


ゲーム作品のSIFT版

先程のプログラムをSIFTを使うように変えました.

# -*- coding: utf-8 -*-
import numpy as np
import cv2
def main():
    circler = 20
    hit = True
   
    marker = cv2.imread('marker.jpg')
    marker = cv2.flip(marker, 1)
    description = cv2.xfeatures2d.SIFT_create()
    kp1, des1 = description.detectAndCompute(marker, None)
    h, w, _ = marker.shape
   
    capture = cv2.VideoCapture(0)
    while True:
        _, camimg = capture.read()
        camimg = cv2.flip(camimg, 1)
        rows, cols, _ = camimg.shape
       
        kp2, des2 = description.detectAndCompute(camimg, None)
        bf = cv2.BFMatcher()
        matches = bf.knnMatch(des1, des2, k=2)
        good = []
        for m, n in matches:
            if m.distance < 0.7 * n.distance:
                good.append(m)
        src_pts = np.float32([ kp1[m.queryIdx].pt for m in good ]).reshape(-1,1,2)
        dst_pts = np.float32([ kp2[m.trainIdx].pt for m in good ]).reshape(-1,1,2)
        playerx = -10000
        playery = -10000
        if len(good) > 10:
            M, _ = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0)
            if M is not None:
                pts = np.float32([ [w/2,h/2] ]).reshape(-1,1,2)
                dst = cv2.perspectiveTransform(pts,M)
                playerx = np.int32(dst[0][0][0])
                playery = np.int32(dst[0][0][1])
       
        cv2.line(camimg, (playerx - circler, playery), (playerx + circler, playery), (0, 0, 0), 2)
        cv2.line(camimg, (playerx, playery - circler), (playerx, playery + circler), (0, 0, 0), 2)
       
        if hit:
            circlex = np.random.randint(circler, cols - 2 * circler)
            circley = np.random.randint(circler, rows - 2 * circler)
        cv2.circle(camimg, (circlex, circley), circler, (0, 0, 255), -1)
       
        dist = (playerx - circlex) ** 2 + (playery - circley) ** 2
        if dist < (2 * circler) ** 2:
            hit = True
        else:
            hit = False
       
        cv2.imshow('OpenCV Game', camimg)
       
        key = cv2.waitKey(1) & 0xFF
        if key == 0x1B:
            break
        if key == ord(' '):
            cv2.imwrite('game.jpg', camimg)
    cv2.destroyAllWindows()
   
if __name__ == '__main__':
    main()

marker.jpg


ゲーム作品のトラッキング版

先程のプログラムをトラッキングを使うように変えました.

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

import numpy as np
import cv2

def main():
    circler = 20
    hit = True

    tracker = cv2.TrackerKCF_create()
    capture = cv2.VideoCapture(0)

    while True:
        _, camimg = capture.read()
        camimg = cv2.flip(camimg, 1)
        rows, cols, _ = camimg.shape

        x, y = int(cols / 2) - 50, int(rows / 2) - 50
        w, h = 100, 100
        cv2.rectangle(camimg, (x - 2, y - 2), (x + w + 2, y + h + 2), (0, 0, 255), 2)
        message = 'Put object inside and press space'
        cv2.putText(camimg, message, (50, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2)
        cv2.imshow('OpenCV Game', camimg)

        key = cv2.waitKey(1) & 0xFF
        if key == ord(' '):
            tracker.init(camimg, (x, y, w, h))
            break
        if key == ord('s'):
            cv2.imwrite('game1.jpg', camimg)
    
    while True:
        _, camimg = capture.read()
        camimg = cv2.flip(camimg, 1)

        _, (x, y, w, h) = tracker.update(camimg)
        playerx = int(x + w / 2)
        playery = int(y + h / 2)
        cv2.line(camimg, (playerx - circler, playery), (playerx + circler, playery), (0, 0, 0), 2)
        cv2.line(camimg, (playerx, playery - circler), (playerx, playery + circler), (0, 0, 0), 2)

        if hit:
            circlex = np.random.randint(circler, cols - 2 * circler)
            circley = np.random.randint(circler, rows - 2 * circler)
        cv2.circle(camimg, (circlex, circley), circler, (0, 0, 255), -1)

        dist = (playerx - circlex) ** 2 + (playery - circley) ** 2
        if dist < (2 * circler) ** 2:
            hit = True
        else:
            hit = False

        cv2.imshow('OpenCV Game', camimg)

        key = cv2.waitKey(1) & 0xFF
        if key == 0x1B:
            break
        if key == ord('s'):
            cv2.imwrite('game2.jpg', camimg)
    
    capture.release()
    cv2.destroyAllWindows()
    
if __name__ == '__main__':
    main()
    


自由作品制作

以上の内容や他のウェブサイトの情報などを活用して,自分なりの楽しいインタラクティブなゲームをプログラミングしてみましょう.TensorflowやTkinterやOpenGLなど,OpenCV以外のライブラリと組み合わせてもいいですよ.

【参考資料】苗村研究室, "OpenCV/OpenGLによる映像処理"
https://nae-lab.org/lecture/OpenCV+OpenGL/


もどる