OpenCVでQRコード認識プログラムを書いてみる(前編)
プログラマとして仕事をしていて、画像認識プログラムのニーズが最近高まってきている気配が感じられます。
OpenCVを利用した顔認識プログラムは、一部界隈で数年前に流行っていたような気がしたのですが、QRコードを画像認識するような処理はあまりオープンにされているものを見たことがありません。そこでこの機会に思い立ち、トライしてみることにしました。
QRコード認識の為のアルゴリズムは、下記のURLを参考にさせて頂いています。
http://www.adobe.com/jp/devnet/flash/articles/qr_code_reader.html
今回は前編ということで、QRコード認識処理のうち以下までを実装してみました。
- 画像データをグレースケールに変換
- 画像データの二値化
- 画像データのラベリング
- ラベリングされた矩形からQRコード切り出しシンボルを検出
実はこの段階で画像内でのQRコードの位置のみなら割り出せているので、単純なマーカーパターンの検出(対象のパターンが画像のどこに存在するか)であれば、ここまでの実装と類似の処理で実現することが出来ると思います。
例えば、適当にQRコードを生成したものを、iPod touchのカメラで撮影・保存した画像
に対して本プログラムを適用すると、
QRコードの切り出しシンボルの位置を自動検出することが出来ています。(赤い四角が自動検出されたシンボル)
それでは以下で、具体的な実装について説明していきたいと思います。
1. グレースケールに変換
グレースケールへの変換は、OpenCVの色空間変換用の関数 "cvCvtColor" をそのまま使うことが出来ます。
cvCvtColor(src_img, gray_img, CV_BGR2GRAY);
2. 二値化
グレースケール変換を掛けた画像を、ある閾値を以て"白"または"黒"に変換します。
これについても、OpenCVの閾値処理関数 "cvThreshold" が利用出来ます。
cvThreshold(gray_img, bin_img, 0, 255, CV_THRESH_BINARY|CV_THRESH_OTSU);
3. ラベリング
QRコードの形状を認識するためには「黒っぽい四角」に見える部分を切り出す必要があります。
ラベリング処理は、こちらを見て、"Blob extraction library" なるものを使用しました。
ちなみに、リンク先にある "Blob extraction library" はリンク切れになっていて、正しいリンクは以下になります。
http://opencv.willowgarage.com/wiki/cvBlobsLib
で、このライブラリを使用すると、ラベリング処理は以下の一発で終わります。
blobs = CBlobResult(bin_img, NULL, 100, false);
但しこのままだと、大量の黒点が認識されてしまうので、矩形の面積でフィルタリングし、小さすぎるもの・大きすぎるものを取り除きます。
フィルタリングの閾値は、カメラの解像度/QRコードの大きさなどで調整していく必要があると思います。
blobs.Filter(blobs, B_INCLUDE, CBlobGetArea(), B_INSIDE, MIN_BLOB_AREA, MAX_BLOB_AREA);
4. 切り出しシンボルの検出
画像に写っているのがQRコードであるかどうかを判定する為に、QRコードの切り出しシンボルを検出します。
具体的には、ラベリングされた各矩形に対して、縦方向・横方向にドット走査し、「黒白黒白黒」の模様が「1:1:3:1:1」の比率になっている場合のみ、切り出しシンボルであると判定します。
さきほどリンクを紹介させて頂いたこちらに説明されているActionScriptのコードをC++/OpenCVに移植する形で実装しました。
若干冗長なソースで気になるのですが、以下のように実装しました。
/* * QRコードの切り出しシンボルかどうかを判定する */ int isSymbol(CBlob blob, IplImage *img) { int array[5]; int index; uchar previousPixel; double target; /* *横方向の走査 */ memset(array, 0, sizeof(array)); index = -1; previousPixel = 255; for (int x = (int)blob.MinX(); x < (int)blob.MaxX(); x++) { uchar pixel; int y; y = ((int)blob.MaxY() + (int)blob.MinY()) / 2; // 多くのサンプルでは"img->widthStep * y + x * 3"となっているが、 // 3という数字は画像のチャネル数なので、正しくは"img->nChannels"と指定しなければならないようだ。 pixel = img->imageData[img->widthStep * y + x * img->nChannels]; // 最初に白が出てきたら無視 if (index == -1 && pixel == 255) { continue; } else { if (previousPixel != pixel) { index++; previousPixel = pixel; // 黒白黒白黒と検出したらbreak if (index >= 5){ break; } } array[index]++; } } target = 0.25 * (array[0] + array[1] + array[3] + array[4]); if ((array[2] > (target*2.5)) && (array[2] < (target*3.5))) { return 1; } /* * 縦方向の走査 */ memset(array, 0, sizeof(array)); index = -1; previousPixel = 255; for (int y = (int)blob.MinY(); y < (int)blob.MaxY(); y++) { uchar pixel; int x; x = ((int)blob.MaxX() + (int)blob.MinX()) / 2; pixel = img->imageData[img->widthStep * y + x * img->nChannels]; // 最初に白が出てきたら無視 if (index == -1 && pixel == 255) { continue; } else { if (previousPixel != pixel) { index++; previousPixel = pixel; // 黒白黒白黒と検出したらbreak if (index >= 5){ break; } } array[index]++; } } target = 0.25 * (array[0] + array[1] + array[3] + array[4]); if ((array[2] > (target*2.5)) && (array[2] < (target*3.5))) { return 1; } return 0; }
ソースコード
これまでの各ステップを組み合わせた完成版のソースコードは以下の通りです。
#include <opencv/cv.h> #include <opencv/highgui.h> #include "BlobResult.h" #define MIN_BLOB_AREA 1000 #define MAX_BLOB_AREA 50000 // QRコードの切り出しシンボルかどうかを判定 int isSymbol(CBlob blob, IplImage *img); int main (int argc, char **argv) { IplImage *src_img, *gray_img, *bin_img; CBlobResult blobs; // 引数から画像を読み込む if (argc != 2 || (src_img = cvLoadImage(argv[1], CV_LOAD_IMAGE_ANYDEPTH | CV_LOAD_IMAGE_ANYCOLOR)) == 0) return -1; gray_img = cvCreateImage(cvGetSize(src_img), IPL_DEPTH_8U, 1); bin_img = cvCreateImage(cvGetSize(src_img), IPL_DEPTH_8U, 1); // (1) 画像をグレースケールに変換 cvCvtColor(src_img, gray_img, CV_BGR2GRAY); // (2) 画像の二値化 cvThreshold(gray_img, bin_img, 0, 255, CV_THRESH_BINARY|CV_THRESH_OTSU); // (3) ラベリング blobs = CBlobResult(bin_img, NULL, 100, false); // (4) 矩形の面積でフィルタリング(小さすぎる矩形、大きすぎる矩形を除去) blobs.Filter(blobs, B_INCLUDE, CBlobGetArea(), B_INSIDE, MIN_BLOB_AREA, MAX_BLOB_AREA); // (5) QRコードの切り出しシンボルを検出 for (int i = 0; i < blobs.GetNumBlobs(); i++) { CBlob blob; blob = blobs.GetBlob(i); // シンボルの場合、矩形を描画する if (isSymbol(blob, bin_img)) { CvPoint p1, p2; p1.x = (int)blob.MinX (); p1.y = (int)blob.MinY (); p2.x = (int)blob.MaxX (); p2.y = (int)blob.MaxY (); cvRectangle(src_img, p1, p2, CV_RGB(255, 0, 0), 2, 8, 0); } } cvNamedWindow("Image", CV_WINDOW_AUTOSIZE); cvShowImage("Image", src_img); cvWaitKey(0); cvDestroyWindow("Image"); cvReleaseImage(&src_img); cvReleaseImage(&bin_img); cvReleaseImage(&gray_img); return 0; }
今回は、OpenCVを利用してQRコードの形状を認識するところまで実装しました。
「前編」と書いてしまった手前「後編」も近いうちに書いてみたいと思います。