もくじ

ラベリング手法

画像処理において,ラベリングを使うことはよくある.
ただ,OpenCVではラベリングに関する関数は定義されていません.
そこで,奈良先端科学技術大学院大学の井村さんという方が作成されたLabeling.hを使います.(入手先)
あと,これを参考にサンプルプログラムを作成されたmasayoshiさんのサイトを参考にします.

プログラムでは,Labeling.hを呼出し,
ラベリングを使う関数の中で以下のようにラベリングクラスを呼び出します.

LabelingBS labeling;

ここでは簡単のため,ラベリング変数はlabelingとしました. あとは,ラベリングを行う関数で,実行するだけです.

IplImage *src;

....

IplImage *dst = cvCreateIMage( cvGetSize( src ), IPL_DEPTH_16S, 1 );
labeling.Exec((uchar *)src->imageData, 
              (uchar *)dst->imageData,
              src->width, src->height, true, AREA_THRE );

それぞれの変数やら何やらの説明をすると,srcは入力画像,dstは出力画像
ただ,この場合,入力画像は1か0が画素値として入った2値画像でないといけません.
labeling.Execの引数はそれぞれの画像のポインタを示しています.
第3引数,第4引数は画像のサイズを表します.
第5引数は領域の大きい順番にラベルをソートするかどうかを表します.
そうするのが一番いいと思うので,今回はtrueとしています.
最後のAREA_THREは領域の最小面積です.この面積以下の領域は考慮しません.

その他の使用法については上の井村さんのサイトにある使用法を参考にしてください.
まぁソートさえしてしまえば,一番領域の大きいラベルは 1になるはずなので,
出力画像の画素値が1の所を一番大きい領域として処理すればいいと思います.

画像バッファの切り出し

テンプレートマッチングなどをする際に,初めに用意するテンプレートを作成することがある.
画像の切り出し方として2種類ほどあるようで・・・まぁ他にもあるでしょうが・・・

まず一つ目.こっちは自分がよく使う方法

CvPoint cp;
IplImage *input_image;
IplImage *temp_img = cvCreateImage( cvSize( temp_width, temp_height ), IPL_DEPTH_8U, 3);

....

cvGetRectSubPix( input_image, template_image, cvPointTo32f( cp ));

変数の説明をするとcpはテンプレートの入力画像中の中心座標.
CvPointTo32fとしているのはcvGetRectSubPix?は入力画像からテンプレートの大きさ分だけ切り出す関数だが,
指定は画像の中心の座標であるため,double型の32bit形式でないといけないからであるからである.
input_imageは入力画像,temp_width, temp_heightはテンプレートの大きさとなっている.
....が間に入ってるのは,その間に入力画像の設定やら中心座標の設定やらをやると考えられるため.

次に二つ目.後輩のプログラムを見て知った方法.

CvPoint sp;
IplImage *input_image;

....

CvRect rect = cvRect( sp.x, sp.y, rect_width, rect_height );
cvSetImageROI( input_image, rect );
IplImage *template_image = cvCreateImage(cvSize( rect_width, rect_height ),IPL_DEPTH_8U,3);
cvCopy( input_image, template_image );
cvResetImageROI( input_image );

この方法は入力画像を一度こっちが指定するサイズに変換してコピーする方法.
まぁわかりやすいっちゃ〜わかりやすい.
ただ,最後のcvResetImageROIを忘れると
入力画像はテンプレートと同じサイズの画像のまま動くことになる.
ちなみに混同しやすいかもしれないが,
前者の方法はテンプレートの中心座標の指定,後者の方法はテンプレートの左上の座標指定である.

色データの抽出

色抽出やラベリングをする際,色のデータを把握するということは重要なことである.
しかし,OpenCVに格納されている色のデータはポインタ形式で行われており,
色空間によってはチャンネルが3つとなりややこしくなっている.
そこで,以下のようにすると色のデータが取れちゃったりする.

仮定:取りたいポイントの座標を(x,y)とする.

unsigned char *pixel = (unsigned char*) (IplImageのバッファ名)->
                            imageData+ y*imagebuf->widthStep + x*imagebuf->nChannels;

こうすることで,RGB空間なら

pixel[2]に赤,pixel[1]に緑,pixel[0]に青

という状態でデータが入っている.

顔認識プログラム

OpenCVで用いられている顔認識に関しては,サンプルのプログラムを用いるのがわかりやすい.
しかし,プログラムをそのまま使うと,どの一般化画像を使って顔を認識したのかがわからない.
そこで,顔の一般化されたファイルを使おうとすると,

opencv-0.9.6/data/haarcascades/

内に存在するファイルを用いるのがよい.
ただ,使い方がちょっとばかり変な使い方であるので,ここに示しておく.

あらかじめ,使いたい.xmlファイルをプログラムの実行ファイルのあるディレクトリにコピーしておく.
その後,実行時に,

./facedetect --cascade="(使いたい.xmlのファイル名)" (顔抽出したい画像)

とする.
.xmlの編集の仕方はまだわかっていないので,分かっている方は普通に教えてください.

閾値処理

ノイズ除去処理でよく使う閾値処理.
とりあえず,OpenCVでは端的に関数が組み込まれていたりする.

cvThreshold( const CvArr* src, CvArr* dst, double threshold, double maxValue,  int thresholdType );

第1引数が入力画像,第2引数が出力画像,第3画像が閾値,第4引数は後で詳しく…,で第5引数は方法.
 
    thresholdType -> CV_THRESH_BINARY:
        閾値より上はmaxValue,それ以外は0で出力

    thresholdType -> CV_THRESH_BINARY_INV:
        閾値より上は0,それ以外はmaxValueで出力

    thresholdType -> CV_THRESH_TRUNC:
        閾値より上は閾値,それ以外は元々の画素を出力

    thresholdType -> CV_THRESH_TOZERO:
        閾値より上は元々の画素,それ以外は0を出力

    thresholdType -> CV_THRESH_TOZERO_INV:
        閾値より上は0,それ以外は元の画素を出力

これらを使うと例えば100以上200以下の閾値処理をして2値化してくれと頼まれた時には,

cvThreshold( src, dst, 200, 255, CV_THRESH_TOZERO_INV );
cvThreshold( dst, src, 100, 255, CV_THRESH_BINARY );

と記述すると,元の画像の所に2値化したものが入ってくれるのでメモリ的にかなりいいかも.
まぁ実際に使うとしたら入力画像と2値画像は別にするから2行目のsrcは別の画像バッファにするのがいいだろうと…

テンプレートマッチングの工夫

OpenCVにおけるテンプレートマッチングはグレースケール画像(輝度画像)で行われるので精度が悪い.
精度を持たせようとすると,RGBの3つのチャンネルを全て使ったテンプレートマッチングが望ましい.
なので,2つのやり方を試してみた.

  1. 画像を3チャンネルから1チャンネルに変換して計算する.

これは画像の1ピクセルにRGBの画像情報が入っているのだが, これを3ピクセルに引き伸ばし,1ピクセルに1チャンネル情報を格納するようにするもの.

IplImage *imgA;
CvMat imgA_mat_hdr;
IplImage imgA_gray_hdr, *imgA1;
cvReshape( imgA, &imgA_mat_hdr, 1 );
imgA1 = cvGetImage( &imgA_mat_hdr, &imgA_gray_hdr );

cvReshapeでまず,imgA(4行目に行くまでに読込をしておく)をヘッダimgA_mat_hdrに一時的に格納
この時ヘッダimgA_mat_hdrの状態としては((imgAの横幅)*(imgAの縦)*3)という状態になっている.
次の行のGetImage?において,imgA_gray_hdrというヘッダに読み込みなおし,その上でimgA1に格納する.
ちなみにimgA_gray_hdrにおけるサイズは(imgAの横幅の3倍)*(imgAの縦)という状態になっている.
これにより元画像,およびテンプレートを横幅3倍に引き伸ばし,計算させます.

  1. それぞれのチャンネルごとの輝度画像に分割し,計算する.

これは画像におけるRGBそれぞれに対する輝度の画像に分け,それぞれで計算するというやり方だ.

IplImage *imgA[4];
for(int i=1; i<4; i++){
    imgA[i] = cvCreateImage(cvGetSize(imgA[0]), IPL_DEPTH_8U, 1);
}
cvCvtPixToPlane(imgA[0], imgA[1], imgA[2], imgA[3], 0);

1行目の時点でimgAの読込をしておく.
cvCvtPixToPlane?における最後の引数というのはカラー以外にも何か1チャンネルあるらしく,そのためらしい.
あと,これにより結果的に3つのテンプレートマッチングの結果バッファが生じるが,
この3つの結果バッファの加算にはcvScaleAddを用いる.

void cvScaleAdd( const CvArr* A, CvScalar S, const CvArr* B, CvArr* C );

ちなみにこれにより生じる式は C=A*S + B となっている.
あと,SはcvScalarとなっているが,1の場合は,CvScalar?(1,1,1,1)とすればよい.

以上の2つである.ただ,計算コストに関しては1の方が2の約3倍となっており,
後者の方法を使う事が望ましいと思われる.

テンプレートマッチング

テンプレートマッチングを行うには,OpenCVの関数cvMatchTemplate?を用いる.

void cvMatchTemplate( const CvArr* I, const CvArr* T, CvArr* result, int method );

ここで,第1引数は元画像,第2引数はテンプレート画像,第3引数は結果格納用バッファ,methodは下参照.

    method=CV_TM_SQDIFF:
        R(x,y)=sumx',y'[T(x',y')-I(x+x',y+y')]2

    method=CV_TM_SQDIFF_NORMED:
        R(x,y)=sumx',y'[T(x',y')-I(x+x',y+y')]2/sqrt[sumx',y'T(x',y')2.sumx',y'I(x+x',y+y')2]
	
    method=CV_TM_CCORR:
        R(x,y)=sumx',y'[T(x',y').I(x+x',y+y')]

    method=CV_TM_CCORR_NORMED:
        R(x,y)=sumx',y'[T(x',y').I(x+x',y+y')]/sqrt[sumx',y'T(x',y')2.sumx',y'I(x+x',y+y')2]
	
    method=CV_TM_CCOEFF:
        R(x,y)=sumx',y'[T'(x',y').I'(x+x',y+y')],
        where T'(x',y')=T(x',y') - 1/(w.h).sumx",y"T(x",y") (mean template brightness=>0)
        I'(x+x',y+y')=I(x+x',y+y') - 1/(w.h).sumx",y"I(x+x",y+y") (mean patch brightness=>0)

    method=CV_TM_CCOEFF_NORMED:
        R(x,y)=sumx',y'[T'(x',y').I'(x+x',y+y')]/sqrt[sumx',y'T'(x',y')2.sumx',y'I'(x+x',y+y')2]

また,このテンプレートのやり方を用いる時に注意すべき点というのは,
結果を格納するバッファのサイズである.
imgAとimgTの大きさが分かっていれば,結果格納バッファのサイズは,

横:imgA->width - imgT->width +1
縦:imgA->height - imgT->height +1

となる.これは確かめるとすぐにわかる.

キーボードの利用

キーボードのコマンドを使って何かをしたくなる時に,
cvWaitKey?を用いるとこの事は可能になる.
とりあえず例を見たほうがわかりやすいので,一例を下に示す.

例:
int key;
key = cvWaitKey(0);
if(key == 'q') printf("今入力したのはqです.\n");

この際にcvWaitKey?ではかれるのはint型整数であり,
内部引数(この場合0)はキー入力を待つ時間であり,0の場合は入力があるまで待つというものだ.
ちなみに"q"とすると,char型のqであるが,'q'とするとint型のqのアスキーコードである.

あと,これ書いてる本人が実際にあった現象として,
qを押しているにもかかわらずkeyと'q'が一致しないという現象が起きた.
他のキーでも試してみたが,20bit目になぜか1が入っていて,
1048576(2の20乗)が常に加算されていた.
なので,int型からshort int型にすることで回避した.

Makefileの作成2

Makefileの作成でmakeを実行すると,警告が表示される.
住岡さん曰く,これが原因でカメラ関係がやばくなるということなので,
住岡さん指定の修正を書き込んでおくことにする.

ネット上にあるサンプルプログラムのMakefileを見ると,
普通は $(GCC) となっているところが $(CXX)となっていて,
g++の最新バージョンをコンパイラーとして使用される.
ということで,以下のような修正をする.

$(CXX) → $(GCC)

そしてMakefileの初めに,

GCC = gcc-3.3
GCC = g++-3.3

とし,そのどちらかをコメントアウトしておく.

ちなみに,gccおよびg++のバージョンが4.0の場合の話なので,
おそらく,それ以前のバージョンだと起きないと思われる.

エッジ検出

OpenCVにおけるエッジ検出には何種類か存在するが,
Cannyアルゴリズムによるエッジ検出はなぜかエラーが起きる.(自分だけではないと思う)
ちなみに他のアルゴリズムを紹介しておくと,

Sobelアルゴリズム

cvSobel( const CvArr* src, CvArr* dst, int xorder, int yorder, int aperture_size=3 )
    //第1引数 →入力画像
    //第2引数 →出力画像
    //第3・4引数 →X方向,Y方向の微分回数
    //第5引数 →エッジ計算で考える範囲(3で考えるのが一般的)

ただ,第5引数により出力画像のbit数を変えないといけない.
ちなみに入力を8bit画像(濃淡画像)とすると,
第5引数の2乗だけ各マスに値が必要なので,8*(第5引数の2乗)以上のbit数が必要

数式的記述

sobel.jpg

Laplaceアルゴリズム

cvLaplace( const CvArr* src, CvArr* dst, int aperture_size=3 )
    //第1引数 →入力画像
    //第2引数 →出力画像
    //第3引数 →エッジ計算で考える範囲(3で考えるのが一般的)

これもSobel同様,第5引数によって出力画像のbit数は変わる.

数式的記述

laplace.jpg

Cannyアルゴリズムにおいては入力と出力のbit数が等しいので考えやすい.
しかし,これ書いてる本人がミスしているので書きません.

Makefileの作成

OpenCVにおけるMakefileの書き方はVersionによっても変わってくるので注意

ver0.9.5の場合
CXXFLAGS +=`/usr/bin/opencv-config --cxxflags`
LDFLAGS +=`/usr/bin/opencv-config --libs highgui`
ver0.9.6およびver0.9.7の場合
CXXFLAGS +=`/usr/bin/pkg-config opencv --cflags`
LDFLAGS  +=`/usr/bin/pkg-config opencv --libs`

ただ,私自身出たエラー(ver0.9.7)として,libcxcore.so.O cannot openedみたいなことがあったが, これはlibファイルにおけるシンボリックリンクエラーであり,/usr/libの中にlibcxcore.so.Oがあればいいが, 私の場合,それが存在せず,どうしようもなかった. この場合,LDFLAGSを以下のように直す.

LDFLAGS += -L/usr/local/lib -lcxcore0.9 -lcv0.9 -lhighgui0.9 -lcvaux0.9

もし,エラーが出た場合,リンクミスの可能性があるので,ターミナルで,

pkg-config opencv --cflags
pkg-config opencv --libs

と打って確かめること.

あと,これは強引な手段なので,おすすめしないが,
/usr/local/lib/pkgconfigの内部にあるopencv.pcか,
/usr/lib/pkgconfigの内部にあるopencv.pcのどちらかが,
上記の0.9の書かれたものであるので,0.9の書かれているものをもう一方にコピーする.
ちなみに,おそらく0.9が書かれているのはver0.9.6の方だったと…