OpenCVでQRコード認識プログラムを書いてみる(前編)

プログラマとして仕事をしていて、画像認識プログラムのニーズが最近高まってきている気配が感じられます。

OpenCVを利用した顔認識プログラムは、一部界隈で数年前に流行っていたような気がしたのですが、QRコードを画像認識するような処理はあまりオープンにされているものを見たことがありません。そこでこの機会に思い立ち、トライしてみることにしました。

QRコード認識の為のアルゴリズムは、下記のURLを参考にさせて頂いています。
http://www.adobe.com/jp/devnet/flash/articles/qr_code_reader.html

今回は前編ということで、QRコード認識処理のうち以下までを実装してみました。

  1. 画像データをグレースケールに変換
  2. 画像データの二値化
  3. 画像データのラベリング
  4. ラベリングされた矩形からQRコード切り出しシンボルを検出

実はこの段階で画像内でのQRコードの位置のみなら割り出せているので、単純なマーカーパターンの検出(対象のパターンが画像のどこに存在するか)であれば、ここまでの実装と類似の処理で実現することが出来ると思います。

例えば、適当にQRコードを生成したものを、iPod touchのカメラで撮影・保存した画像
f:id:aquarla:20110306164047j:image
に対して本プログラムを適用すると、
f:id:aquarla:20110306164043p:image
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);

"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コードの形状を認識するところまで実装しました。
「前編」と書いてしまった手前「後編」も近いうちに書いてみたいと思います。