2011/05/02

形状マッチングで文字認識をさせようとして失敗してみた

Web カメラから撮影した数字を自動的に認識できたらいいなぁと思い、こんなものを作ってみた。

Webカメラで撮った数字を認識しているところ。背景のコマンドプロンプトには識別した数値が表示されている。
その他のウィンドウ / 左:カメラの映像、右上:2値化してトリミングした画像、右下:対応するテンプレート画像。
一応、プログラム自体はできたのだけど、精度が悪過ぎて到底使い物にならなかった

恥の記録として残しておく事にする。



プログラムを書く前に、0 ~ 9 の数字の「お手本」となる画像を用意する必要がある。それぞれに、0.jpg1.jpg、…、9.jpg という連番を振っておく。

これをソースと同じフォルダに入れて、以下のプログラムを書いてみた。

#include<cv.h>
#include<cxcore.h>
#include<highgui.h>

#include<climits>
#include<iostream>
#include<string>

#pragma comment(lib,"cv200.lib")
#pragma comment(lib,"cxcore200.lib")
#pragma comment(lib,"highgui200.lib")

const int level = 110;  // 二値化する際の閾値

int main() {

    CvCapture* capture = NULL;
    if((capture = cvCreateCameraCapture(0)) == NULL) 
        return -1;

    cvNamedWindow("カメラ映像", CV_WINDOW_AUTOSIZE);
    cvNamedWindow("識別対象", CV_WINDOW_AUTOSIZE);
    cvNamedWindow("識別結果", CV_WINDOW_AUTOSIZE);

    IplImage* rgbImg;
    IplImage* grayImg;
    IplImage* targetArea;

    // ============================
    // 連番ファイルを配列に読み込み
    // ============================
    IplImage* templateNumbers[10];
    for(int i = 0; i < 10; ++i) {
        std::ostringstream oss;
        oss << i << ".jpg";
        
        templateNumbers[i] = 
            cvLoadImage(oss.str().c_str(), 
                CV_LOAD_IMAGE_GRAYSCALE);
        
    }
    targetArea = cvCreateImage(cvSize(200, 200), 
        IPL_DEPTH_8U, 1);

    // ====================
    // カメラからキャプチャ
    // ====================
    rgbImg = cvQueryFrame(capture);
    grayImg = cvCreateImage(
        cvSize(rgbImg->width, rgbImg->height),
            rgbImg->depth, 1);

    while(1) {
        rgbImg = cvQueryFrame(capture);

        // ======
        // 二値化
        // ======
        cvCvtColor(rgbImg, grayImg, CV_BGR2GRAY);
        cvThreshold(grayImg, grayImg, 
            level, 255, CV_THRESH_BINARY);

        // 識別対象枠の表示
        int fieldSize = (int)(rgbImg->height * 0.9);
        cvRectangle(rgbImg,
            cvPoint(    
                (rgbImg->width  - fieldSize) / 2,
                (rgbImg->height - fieldSize) / 2),
            cvPoint(
                (rgbImg->width  + fieldSize) / 2,
                (rgbImg->height + fieldSize) / 2),
            cvScalar(0, 0, 255));

        cvShowImage("カメラ映像", rgbImg);

        // =======
        // ROI設定
        // =======
        cvSetImageROI(grayImg, 
            cvRect((grayImg->width - fieldSize) / 2, 
                   (grayImg->height - fieldSize) / 2, 
                fieldSize, fieldSize));

        // ==============
        // 形状マッチング
        // ==============
        double min = 9999;
        int index = 0;
        for(int i = 0; i <= 9; ++i) {
            cvResize(grayImg, targetArea, 1);
            double tmp = cvMatchShapes(targetArea, 
                templateNumbers[i], 
                    CV_CONTOURS_MATCH_I3, 0);

            if (min > tmp) {
                min = tmp;
                index = i;
            }
        }

        // ========
        // 結果表示
        // ========
        cvShowImage("識別対象", targetArea);
        cvShowImage("識別結果", templateNumbers[index]);
 
        cvResetImageROI(grayImg);
        std::cout << index << "\n";   

        if( cvWaitKey(33) == 'q') break;
    }

    cvReleaseCapture(&capture);
    cvReleaseImage(&rgbImg);
    cvReleaseImage(&grayImg);

    cvDestroyWindow("カメラ映像");
    cvDestroyWindow("識別対象");
    cvDestroyWindow("識別結果");

    return 0;
}

理論としては、お手本画像の輪郭とカメラ画像の識別対象領域の輪郭それぞれの Hu 不変モーメントを比較し、最も適合するものを結果として表示している……はずなのだが、全くうまくいかない。

お手本画像をもうちょっとマシなものに変えればいいのだろうか。

ちなみに、なんでこんな事をしたかったかというと、カメラでナンバープレイスの問題を撮るだけで自動的に解答を導いてくれるプログラムができたら素敵だなと感じたからである。

こんな面倒な事をやるくらいなら、普通に解いた方が早い気もしてきた。



それはそうと、ぼくの環境(Windows Vista + Visual C++ 2008 + OpenCV 2.0)では、IplImage (や、CvCapture 等)を cv::Ptr で包むと、いろいろややこしいバグが生じるようだ。

試しに、rgbImage の宣言部を
cv::Ptr<IplImage> rgbImg;
に書き換えて、さらに
cvReleaseImage(&rgbImg);
をコメントアウトしてからビルド・実行したところ、こんなエラーが出た。

ステップ実行を駆使してエラーの発生源を突き止めたところ、なんと
cvCvtColor(rgbImg, grayImg, CV_BGR2GRAY);
であった。たぶん、この関数の中では超ストイックな処理でメモリアクセスが行われていて、スマートポインタなどという軟弱きわまりない機構の利用などは設計者の想定外だったのだろう。

このバグのせいで数時間に亘って頭を抱える事となり、原因究明後はメモリの確保・解放を自力でやる事によって一応の解決をみた。

2 件のコメント:

  1. こんにちは。通りすがりの物です。
    拝見させて頂き大変参考になりました。m(_ _)m

    cvMatchShapesについては以前同様な事が起きて、
    cvFindContoursで求めたテンプレート画像、
    ソース画像、両方の輪郭情報を渡すと
    多少思った通りの結果になりました。

    2値化画像などから取得する輪郭情報の種類や
    近似の仕方によっても精度が変わるようですので
    色々と試すことはできそうです。
    (複雑な形状では難しいのかも知れません。。)

    そもそもcvMatchShapes先生に2値化画像を
    渡せば同様なことをしてくれると
    期待してたのですが…

    ナンプレ解読ソフト楽しみにしてますw

    返信削除
  2. コメントありがとうございます∩( ・ω・)∩

    お返事が遅れて申し訳ありませんでした。

    > cvMatchShapesについては以前同様な事が起きて、
    > cvFindContoursで求めたテンプレート画像、
    > ソース画像、両方の輪郭情報を渡すと
    > 多少思った通りの結果になりました。

    貴重な情報、感謝します。ぜひ参考にしたいと思います。

    さてさて。

    OpenCV の形状マッチングで使用されている Hu モーメントは、『回転に対する不変性』が保証されています。

    この副作用として、精度をギリギリまで上げたとしても、相似形に近い「6」と「9」の識別で行き詰ってしまう事がわかっています。

    この問題の解決は、OpenCV に用意されている関数だけでは難しそうですので、また少し頭を使う必要がありそうです(・ω・lll)

    返信削除

ひとことどうぞφ(・ω・,,)