QUOIT Blog

[python][OpenCV] 画像認識プログラム作ってみた

この記事は3年以上前の記事です。情報が古い場合がありますのでお気を付け下さい。

ここのところpython + OpenCVを触っていたので、このへんで成果発表といきます。
ただの研究なので、本当にたいしたことはやっていないわけですが…

もともとはLeapMotionの開発をするために、たまには新しい言語に手を出してみようとpythonを使い始めたので、そのまま使ってみるかーくらいの気持ちでOpenCVもpythonで使うことにしました。
(LeapMotionの記事はまた別途アップしますね)

…が、python + OpenCVの情報は非常に数が少ない…
日本語での情報は下記のブログがとても参考になりました。

tataboxの備忘録

なお、この記事ではpythonやOpenCVの導入方法は割愛します。
環境によっても異なりますので、調べてみて下さい。
私はMacOSX(10.7)環境で試しました。
MacOSXは最初からPythonが入っているのですが、新しいバージョンを入れたりしています。

今回のお題

さて今回は、Webカメラの画像を解析して、特定の画像がその中にあれば丸を描く、というものにしました。

おおまかな手順としては、以下のようになります。

(1)判定元になる画像をOpenCVで読み込む
(2)OpenCVでWebカメラの画像を取得し、読み込む
(3)変換した画像を比較して、共通点を算出
(4)共通点が5カ所以上あったら、取得した共通点の座標平均値を算出してセンターの座標を決める
(5)センターの座標の周りに円を描いて描画する

肝になる部分は(3)の「比較→共通点を算出」ですが、OpenCVを導入した際に入っているサンプルに含まれていたので、ほぼ丸パクリしています。
自分で組んだわけではないのですごくアレですが、せめて内容の解説くらいはしたいと思います。

まず最初に、完成したプログラムのコードを記載します。

# coding: utf-8

import numpy as np
import cv2
import cv2.cv as cv

def compareImg(detector,kp1,desc1,npimg):# 画像を比較する関数

	# キャプチャ画像の特徴量の検出
	kp2, desc2 = detector.detectAndCompute(npimg, None)

	# BFMatcherをNORM_L2で対応付け
	matcher = cv2.BFMatcher(cv2.NORM_L2)
	# クエリ集合の各ディスクリプタに対して、最も良い上位2個のマッチを取得
	matches = matcher.knnMatch(desc1, trainDescriptors = desc2, k = 2)

	# 距離許容比率
	ratio = 0.75

	# 座標を抽出
	mkp = []
	for m in matches:
		if len(m) == 2 and m[0].distance < m[1].distance * ratio:
			m = m[0]
			mkp.append( kp2[m.trainIdx] )
	matchedXY = np.float32([kp.pt for kp in mkp])

	# 共通点が5ポイント以上あったらその中央の座標を返す
	if len(matchedXY) >= 5:
		x,y = 0,0;
		for i in matchedXY:
			x += i[0]
			y += i[1]
		avgx = int(x / len(matchedXY))
		avgy = int(y / len(matchedXY))
		return (avgx,avgy)

if __name__ == '__main__':

	# キャプチャから探し出す画像ファイルを指定
	comImg = './compare.png'
	# 比較元の画像を読み込む(読み込んだ時点でグレースケールのNumpy配列)
	comImgNp = cv2.imread(comImg, 0)

	# 検出器
	detector = cv2.SIFT()
	# 特徴量の検出
	kp1, desc1 = detector.detectAndCompute(comImgNp, None)

	# 表示用のウィンドウを準備
	cv.NamedWindow("camera", 1)

	# カメラからのビデオキャプチャを初期化
	capture = cv.CaptureFromCAM(0)

	# キャプチャの横幅を取得
	camW = int(cv.GetCaptureProperty(capture,cv.CV_CAP_PROP_FRAME_WIDTH))
	# キャプチャの高さを取得
	camH = int(cv.GetCaptureProperty(capture,cv.CV_CAP_PROP_FRAME_HEIGHT))

	# グレースケール変換用のデータ領域を確保
	grayImage = cv.CreateImage ((camW,camH), cv.IPL_DEPTH_8U, 1)

	while True:
		# フレームの画像をキャプチャ
		img = cv.QueryFrame(capture)

		if img != None:# キャプチャ開始直後はデータが入っていないので、判定を入れておく
			# キャプチャ画像をグレースケールに変換
			cv.CvtColor(img,grayImage,cv.CV_RGB2GRAY);
			# グレースケール画像をCvMat配列に変換(元はIplimage構造体)
			mat = cv.GetMat(grayImage)
			# CvMat配列をNumpy配列に変換
			npimg = np.asarray(mat)

			# 画像を比較して、中央の座標を取得
			retXY = compareImg(detector,kp1,desc1,npimg);
			if(retXY != None):

				# 元のキャプチャ画像をNumpy配列に変換
				mat2 = cv.GetMat(img)
				baseimg = np.asarray(mat2)

				# 取得した座標を中心点として円を描く
				cv2.circle(baseimg,retXY,100,(255,0,0))
				# 円を描いた画像を描画
				cv2.imshow("camera", baseimg)

			else:# 座標が返ってこなければ元の画像を表示
				# キャプチャした画像をウィンドウに表示
				cv.ShowImage("camera", img)

		if cv.WaitKey(10) == 27: 
			break
	cv.DestroyAllWindows()

それでは、解説を始めます。

手順1.判定元になる画像をOpenCVで読み込む

今回は自分の顔写真を判定元にしました。
OpenCVで画像を比較するにはグレースケールの画像である必要があります。
下記コードでは、最初から強制的にグレースケールで画像を読み込んでいます。

if __name__ == '__main__':

	# キャプチャから探し出す画像ファイルを指定
	comImg = './compare.png'
	# 比較元の画像を読み込む(読み込んだ時点でグレースケールのNumpy配列)
	comImgNp = cv2.imread(comImg, 0)

OpenCVを使うにあたって、配列の形式がいろいろあるので理解するのに苦労しました…
今回は、Iplimage、CvMat、Numpyを使い分ける必要が出てきます。
comImgNp変数には、Numpy配列が入ります。

また、今回は「Webカメラの画像を毎回取得→特徴量を検出→比較元画像と比較」というフローになりますが、
比較元画像の特徴量は変化しないため、ループの度に読み込むのは無駄になってしまいます。
そこで、先に比較元画像の特徴量を検出しておきます。

	# 検出器
	detector = cv2.SIFT()
	# 特徴量の検出
	kp1, desc1 = detector.detectAndCompute(comImgNp, None)

手順2.OpenCVでWebカメラの画像を取得し、読み込む

次に、Webカメラの画像を取得します。

	# カメラからのビデオキャプチャを初期化
	capture = cv.CaptureFromCAM(0)

	# キャプチャの横幅を取得
	camW = int(cv.GetCaptureProperty(capture,cv.CV_CAP_PROP_FRAME_WIDTH))
	# キャプチャの高さを取得
	camH = int(cv.GetCaptureProperty(capture,cv.CV_CAP_PROP_FRAME_HEIGHT))

まずCaptureFromCAMで初期化し、キャプチャする画像の横幅と高さを取得します。
Webカメラからの画像はもちろんカラー画像ですのでグレースケールに変換する必要があります。
そこで、画像描画用のデータ領域を確保しておき、キャプチャした画像をグレースケールで書き出します。

	# グレースケール変換用のデータ領域を確保
	grayImage = cv.CreateImage ((camW,camH), cv.IPL_DEPTH_8U, 1)

	while True:
		# フレームの画像をキャプチャ
		img = cv.QueryFrame(capture)
		if img != None:# キャプチャ開始直後はデータが入っていないので、判定を入れておく
			# キャプチャ画像をグレースケールに変換
			cv.CvtColor(img,grayImage,cv.CV_RGB2GRAY);
			# グレースケール画像をCvMat配列に変換(元はIplimage構造体)
			mat = cv.GetMat(grayImage)
			# CvMat配列をNumpy配列に変換
			npimg = np.asarray(mat)

「while True:」のループ内でWebカメラからの画像を連続キャプチャします。
また、比較が出来るようにNumpy配列に変換しています。

手順3.変換した画像を比較して、共通点を算出

さて、肝になる部分です。

			# 画像を比較して、中央の座標を取得
			retXY = compareImg(detector,kp1,desc1,npimg);

ここで別の関数に渡っていますので、そちらを確認します。

def compareImg(detector,kp1,desc1,npimg):# 画像を比較する関数

	# キャプチャ画像の特徴量の検出
	kp2, desc2 = detector.detectAndCompute(npimg, None)

比較元画像と同じように、渡ってきたキャプチャ画像の特徴量を検出します。
ここから画像の比較になります。

	# BFMatcherをNORM_L2で対応付け
	matcher = cv2.BFMatcher(cv2.NORM_L2)
	# クエリ集合の各ディスクリプタに対して、最も良い上位2個のマッチを取得
	matches = matcher.knnMatch(desc1, trainDescriptors = desc2, k = 2)

	# 距離許容比率
	ratio = 0.75

	# 座標を抽出
	mkp = []
	for m in matches:
		if len(m) == 2 and m[0].distance < m[1].distance * ratio:
			m = m[0]
			mkp.append( kp2[m.trainIdx] )
	matchedXY = np.float32([kp.pt for kp in mkp])

このあたりの理解は自分も追いついていません…
自分なりに理解したコメントを書いておりますが、間違いがあるかもしれません。あしからず。

検出した特徴量同士を比較して、特徴の距離が近い箇所を取得します。
取得した距離を座標として取得しなおし、ディクショナリ型で格納します。

手順4.共通点が5カ所以上あったら、取得した共通点の座標平均値を算出してセンターの座標を決める

関数の最後で、座標が5ポイント以上あったら平均値を求めてその値を返します。


	# 共通点が5ポイント以上あったらその中央の座標を返す
	if len(matchedXY) >= 5:
		x,y = 0,0;
		for i in matchedXY:
			x += i[0]
			y += i[1]
		avgx = int(x / len(matchedXY))
		avgy = int(y / len(matchedXY))
		return (avgx,avgy)

なんかディクショナリに格納するときにまとめてやっちゃったほうがスマートな気がしますが…

手順5.センターの座標の周りに円を描いて描画する

返ってきた座標を元に、その座標を中心とする円を描きます。
座標がなければ元の画像を描画します。

			if(retXY != None):

				# 元のキャプチャ画像をNumpy配列に変換
				mat2 = cv.GetMat(img)
				baseimg = np.asarray(mat2)

				# 取得した座標を中心点として円を描く
				cv2.circle(baseimg,retXY,100,(255,0,0))
				# 円を描いた画像を描画
				cv2.imshow("camera", baseimg)

			else:# 座標が返ってこなければ元の画像を表示
				# キャプチャした画像をウィンドウに表示
				cv.ShowImage("camera", img)

以上で終わりです。

まだまだOpenCVは触り始めたばかりで不理解なところが多いです。
曖昧な説明&間違っている箇所があると思いますので、ご指摘頂けると幸いですm(_ _)m

余談…
ほんとはSIFTじゃなくてORBにしたいんですが、調整がうまくいってないので勉強中

Comments are closed.