3DSのプチコン3号をどうにかしたいという目標を持ったので、3DSタッチパネルディスプレイに表示されるソフトウェアキーボードを、OpenCVを使って位置を認識させる、ということを試してみました。
まずはあらかじめ、キーボードの写真を撮っておきます。撮影にはNexus5の内蔵カメラを使いました。画像が若干傾いていたので、撮った写真をgimpで加工し、ほぼ長方形になるよう調整しました。
この画像から、キーを一つ切り出します。とりあえず「1」のキーを切り取ってみました。
キーボードの全体画像からこの「1」のキーを検出する方法として、テンプレートマッチングを行ってみます。OpenCVのサイトにそのものずばりのC++サンプルが掲載されています。自分はどこかのページで見たコードを参考に以下のようなものを作成しました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | #include <iostream> #include <opencv2/core/core.hpp> #include <opencv2/imgproc/imgproc.hpp> #include <opencv2/highgui/highgui.hpp> int main( int argc, char **argv) { cv::Mat search_img = cv::imread( "../petitcom-kbd-fix.png" , 1); cv::Mat tmp_img = cv::imread( "../petit-kbd-1.png" , 1); cv::Mat result_img; cv::matchTemplate(search_img, tmp_img, result_img, CV_TM_CCOEFF_NORMED); cv::Rect roi_rect(0, 0, tmp_img.cols, tmp_img.rows); cv::Point max_pt; double maxVal; cv::minMaxLoc(result_img, NULL, &maxVal, NULL, &max_pt); roi_rect.x = max_pt.x; roi_rect.y = max_pt.y; std::cout<< "(" << max_pt.x << "," << max_pt.y << ") score=" << maxVal << std::endl; cv::rectangle(search_img, roi_rect, cv::Scalar(0, 0, 255, 3)); std::cout << "rectangle: (" << roi_rect.x << ", " << roi_rect.y << ") size (" << roi_rect.width << ", " << roi_rect.height << ")" << std::endl; cv::imwrite( "output.png" , search_img); return 0; } |
search_imgを対象画像とし、tmp_imgにマッチする領域をtemplateMatch関数で算出します。アルゴリズムの選択には、特に何も考えずサンプルのまま(CV_TM_CCOEFF_NORMED/Collection coefficient)にしました。これにより、演算結果がresult_imgに代入されます。
その結果から、minMaxLoc関数を用いてresult_imgから値が最大となる位置を算出します。cv::Matは行列データ型であり、minMaxLocは行列内の最大値、最小値を持つ座標とその値を取得する関数です。今回の場合、必要なのは最大値の座標だけです。
サンプルプログラムではrectangle関数で該当する矩形領域に線を引いています。
自分はC++に不慣れなので、同じことをPythonでやってみようとしました。いろいろと試行錯誤の結果、以下のようなコードになりました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | import cv2 src = cv2.imread( "../petitcom-kbd-fix.png" , 1 ) tmp = cv2.imread( "../petit-kbd-1.png" , 1 ) res = cv2.matchTemplate(src, tmp, cv2.TM_CCOEFF_NORMED) (minval, maxval, minloc, maxloc) = cv2.minMaxLoc(res) print "({0}, {1}) score = {2}\n" . format (maxloc[ 0 ], maxloc[ 1 ], maxval) (h, w, d) = tmp.shape rect_1 = (maxloc[ 0 ], maxloc[ 1 ]) rect_2 = (maxloc[ 0 ] + w, maxloc[ 1 ] + h) print "({0}, {1}) score = {2}\n" . format (maxloc[ 0 ], maxloc[ 1 ], maxval) print "size ({0}, {1})\n" . format (w, h) cv2.rectangle(src, rect_1, rect_2, 0x00ff00 ) cv2.imwrite( "py-output.png" , src) |
rectangleに与えるべき引数がC++とはちょっと異なります。C++では矩形の起点と高さ+幅を与えるのに対し、Pythonでは2つの対角上の頂点の座標を指定します。最初この違いに気付かなくて、思うような結果にならず悩みました。
今回はスケールが同一の画像でのテンプレートマッチングだったので期待通りの結果を得ることができましたが、実際の利用をする場合には異なるサイズや、若干傾いた画像、他に余計なものが写りこんでいる画像などを相手にする必要があるので、もっといろいろな下処理などが必要になるものと思われますが、まずは第一歩を進めることができました。