OpenCVの画像処理


ここからは実際にOpenCVを使ったコードを書き始めていくこととする。
ここでリファレンスを一緒に開いておくと、疑問点を解消しやすいかもしれない。

1.画像表示

まずは、もっとも基本的な処理として、画像表示を行うプログラムを作成する。
OpenCVのHelloWorldといったところだろう。ここで簡単な「生成->実行->解放」の流れを掴んでいただきたい。

以下のソースコードは「画像を読み込み、それをウィンドウに表示、その後何らかのキーが押された時点で処理終了」という流れである。
コード中、関数などに「cv」と付いているものは、ほぼOpenCVのものだと思っていただいて問題ない。
また、解説都合上、エラー処理などは一切省いているので、そこは了承願いたい。

画像表示プログラム

#include "cv.h"
#include "highgui.h"
// 画像表示
int main(int argc, char* argv[])
{
	// 画像ファイルポインタの宣言
	IplImage* img;
	// 読み込み画像ファイル名
	char imgfile[] = "lena.jpg";

	// 画像の読み込み
	img = cvLoadImage(imgfile, CV_LOAD_IMAGE_ANYCOLOR | CV_LOAD_IMAGE_ANYDEPTH);

	// 画像の表示用ウィンドウ生成
	cvNamedWindow("lena", CV_WINDOW_AUTOSIZE);

	// 指定したウィンドウ内に画像を表示する
	cvShowImage("lena", img);

	// キー入力待ち
	cvWaitKey(0);

	// 指定したウィンドウの破棄を行う
	cvDestroyWindow("lena");

	// 生成した画像ヘッダ、及びそのデータ領域を解放する
	cvReleaseImage(&img);

	return 0;
}

実行結果

画像表示の実行結果

「画像を読み込み」「ウィンドウに表示」「何らかキーが」と言ったところで色々と思いついた人、もしくは「大変そう」と思った人もいるかもしれないが、ソースコードを見ていただくと分かるとおり、全てOpenCVを使って実現可能である。

cvLoadImage() - リファレンス

まず、画像読み込みは「cvLoadImage()」を使用することができる。

IplImage* cvLoadImage( const char* filename, int flags = CV_LOAD_IMAGE_COLOR );

cvLoadImageは、「画像Path」「カラー/グレースケールの選択、及びデプス(Depth)」を引数に取る。
今回読み込みに使う画像は、OpenCVのサンプルフォルダにある「lena.jpg」である。
また、色及びデプスは、入力画像「lena.jpg」から変更するつもりはないので、今回は「CV_LOAD_IMAGE_ANYCOLOR | CV_LOAD_IMAGE_ANYDEPTH」を引数としている。これにより、入力画像に出来る限り忠実な情報として読み出すことが出来る。

第2引数へ他にどんな値を設定でき、どのような効果があるかはリファレンスの「cvLoadImage」をご覧いただきたい。

読み込まれた画像は、全てIPL画像形式(IplImage形式)のデータとして取得される。OpenCVでは、ほぼIplImage形式を使って処理するため、一度実現したい処理を書いてしまえば、あとはデータをいかに取得するか、という点だけになる。例えば、このcvLoadImageは、以下のようなフォーマットに対応しており、このサンプルで表示可能である。

対応フォーマット
Windows bitmapsBMP / DIB
JPEG filesJPEG / JPG / JPE
Portable Network GraphicsPNG
Portable image formatPBM / PGM / PPM
Sun rastersSR / RAS
TIFF filesTIFF / TIF
OpenEXR HDR imagesEXR
JPEG 2000 imagesjp2

cvNamedWindow() - リファレンス

複雑なGUIが必要でない場合は、ウィンドウ生成に「cvNamedWindow()」を使用することができる。

int cvNamedWindow( const char* name, int flags = CV_WINDOW_AUTOSIZE );

cvNamedWindowは「ウィンドウタイトル、及びウィンドウ識別名称」「ウィンドウのフラグ」を引数に取る。

「ウィンドウタイトル、及びウィンドウ識別名称」は、今回、画像データそのままに"lena"としている。
この引数に指定した名前が、ウィンドウタイトルに表示される。また、指定した名称が、そのままそのウィンドウの識別情報として用いられるため、生成したウィンドウに対して、何らかの処理を行いたい場合にも利用される。

また「ウィンドウのフラグ」には、現在「CV_WINDOW_AUTOSIZE」のみのサポートとなっている。この設定では、実際に画像イメージを表示する際にウィンドウサイズを調整し、ユーザによるウィンドウの自由調整はできない。

cvShowImage() - リファレンス

画像データを生成しておいたウィンドウに表示したい場合には「cvShowImage()」を使用する。

void cvShowImage( const char* name, const CvArr* image );

cvShowImageは「表示を行うウィンドウの識別名」「画像データ」を引数に取る。

「表示を行うウィンドウの識別名」は、先に生成しておいた"lena"である。
これにより処理対象は"lena"の名を持つウィンドウとなり、第2引数に指定された「画像データ」を対象"lena"ウィンドウに表示することができる。

cvWaitKey() - リファレンス

生成したウィンドウで、ユーザからの入力待ちを行いたい場合には「cvWaitKey()」を使用する。
このプログラムでは、実行すると処理が即時に完了してしまうので、処理Waitとして利用している。

int cvWaitKey( int delay = 0);

cvWaitKeyは「遅延時間(ms)」を引数に取る。

「遅延時間」は、今回0にしている。遅延時間はミリ秒であるが、0を設定することで完全に無限待ち状態になる。
0を超える数値を入力した場合、その遅延時間分だけユーザからのキーイベントを待つ。その時間を越えた時点で待機状態は解かれ、cvWaitKey()から抜ける。その際、cvWaitKey()の戻り値には「-1」が返るため、判定材料として利用できる。

cvDestroyWindow() - リファレンス

生成したウィンドウを破棄する場合には「cvDestroyWindow()」を使用する。

void cvDestroyWindow( const char* name );

cvDestroyWindowは「破棄対象のウィンドウ識別名」を引数に取る。

表示を終え、キー入力を受けた処理は、ウィンドウの破棄を行うため"lena"を引数としている。

cvReleaseImage() - リファレンス

取得した画像イメージのために生成されたデータや領域を解放するには「cvReleaseImage()」を使用する。

void cvReleaseImage( IplImage** image );

cvReleaseImageは「破棄対象の画像データのポインタ」を引数に取る。

不要になった"lena.jpg"の画像イメージを解放するため、"img"のポインタを引数としている。

以上がOpenCVの基本処理「生成->実行->解放」である。
この処理の流れさえ把握できれば、他の画像処理は「実行」部分に注力するだけでよく、また静止画からカメラなどの動画入力に変わった場合でも、処理の基本的な流れが変わらないので導入しやすい。

一度、このサンプルコードを実行した後は、画像データのパスを引数に取るように変更するなどして、弄ってみると良い。

2.エッジ検出

今回は、指定された画像から「エッジ」を検出するプログラムを作成する。
エッジ検出では、他の画像・動画処理を行う上でも役に立つ「空画像データ生成」「グレースケール化」を行うので、ぜひ処理を把握して頂きたい

以下のソースコードは「画像読込、エッジ検出を行うための準備、エッジ検出、ウィンドウ表示、キー入力待ち、処理終了」という流れである。
途中に「エッジ検出を行うための準備」と「エッジ検出」が加わった以外には、基本処理と流れは変わらない。

エッジ検出プログラム

#include "cv.h"
#include "highgui.h"
// エッジ検出
int main(int argc, char* argv[])
{
	// 入力用画像
	IplImage* img;
	// 出力用画像
	IplImage* img2;
	// 読み込み画像ファイル名
	char imgfile[] = "lena.jpg";

	// 画像をグレースケールで読み込む
	img = cvLoadImage(imgfile, CV_LOAD_IMAGE_GRAYSCALE);
	// 指定に基づいた画像ヘッダ、及びそのデータ領域を生成する
	img2 = cvCreateImage(cvGetSize(img), IPL_DEPTH_8U, 1);

	// エッジ検出
	cvCanny(img, img2, 64, 128);

	// 画像の表示用ウィンドウ生成
	cvNamedWindow("lena", CV_WINDOW_AUTOSIZE);
	// 指定したウィンドウ内に画像を表示する
	cvShowImage("lena", img2);

	// キー入力待ち
	cvWaitKey(0);

	// 指定したウィンドウの破棄を行う
	cvDestroyWindow("lena");
	// 入力画像の解放
	cvReleaseImage(&img);
	// 出力画像の解放
	cvReleaseImage(&img2);

	return 0;
}

実行結果

エッジ検出の結果

新しく使用したのは「cvCreateImage()」「cvCanny()」である。
ただし、今回は画像をグレースケールで読み込むため、cvLoadImage()の引数に変更があるところにも注目してほしい。

cvLoadImage()

第2引数が今回は「CV_LOAD_IMAGE_GRAYSCALE」になっている。この引数指定により、指定された画像データ"lena.jpg"をグレイスケールで読み込むことができる。

グレイスケール読み込みの結果

cvCreateImage() - リファレンス

画像ヘッダとそのデータ領域を生成するには「cvCreateImage()」を使用する。

IplImage* cvCreateImage( CvSize size, int depth, int channels );

cvCreateImageは「生成する画像の高さと幅」「画像要素のデプス」「画像要素毎のチャネル」を引数に取る。

今回は、エッジ検出の受け口としての「空画像データ」を用意するために使用している。

「生成する画像の高さと幅」は、矩形のピクセル精度でのサイズを示す「CvSize」で渡す必要がある。
直接CvSizeを生成しても良いが、他に使いまわす予定はないので、今回は画像データを指定すると、そのCvSizeを出力してくれる「GetSize()」を利用している。

「画像要素のデプス」は、今回生成する画像のデプスを指定する。
デプスは「色深度」で、この生成する画像の画素1要素が何bitで表現されるかを示す値である。
用意された設定可能な値は以下の通りである。確認にはリファレンスをご覧いただきたい。

デプス設定値表
IPL_DEPTH_8U符号無し 8 ビット整数
IPL_DEPTH_8S符号有り 8 ビット整数
IPL_DEPTH_16U符号無し 16 ビット整数
IPL_DEPTH_16S符号有り 16 ビット整数
IPL_DEPTH_32S符号有り 32 ビット整数
IPL_DEPTH_32F単精度浮動小数点数
IPL_DEPTH_64F倍精度浮動小数点数

基本的に特殊な理由(例えば2値化する場合)がない限りは、8bitで問題のないことが多い。

「画像要素毎のチャネル」は、今回生成する画像のチャネルを「1〜4」で指定する。
チャネルは「1画素を何要素で表現するか」を示す値である。例えば、濃淡だけを表すグレースケールなら「1」、通常カラーのRGBであれば「3」、それに透過(アルファチャネル)を加え「4」といった具合である。
今回は、画像を「グレースケール」で読み込んでいるため、引数には1を指定している。

cvCanny() - リファレンス

エッジ検出を行うには「cvCanny()」を使用する。
OpenCVにおけるエッジ検出には何種類か存在するが、これはCannyアルゴリズムを利用したエッジ検出が実装されている。他のアルゴリズムについては、リファレンスを参照願いたい。

void cvCanny( const CvArr* image, CvArr* edges, double threshold1, double threshold2 , int aperture_size = 3);

cvCannyは「対象画像」「検出後画像」「閾値1」「閾値2」「Sobel演算子のアパーチャサイズ」を引数に取る。
この中で「Sobel演算子のアパーチャサイズ」については、デフォルトを使用し、説明は割愛する。

「対象画像」にはエッジ検出を行いたい画像データ、「検出後画像」では「対象画像」から得られたエッジデータの受け口を引数に指定する。

「閾値1」と「閾値2」には、Cannyアルゴリズムで使用される2つの閾値を指定する。2つの閾値について簡単に触れておくが、Cannyアルゴリズムや閾値についての詳細で正しい知識は、各自での情報収集に任せたい。
引数に指定された2つ閾値のうち、大きい値がCannyアルゴリズムで利用される「強edge」、小さい値が「弱edge」として扱われる。
「強edge」は画素変化が著しい、つまりは「エッジ」の可能性を持つ箇所を検出するのに用いられる。この「強edge」の値は、エッジの検出に強い影響を及ぼす。
「弱edge」は、検出されたエッジが繋がっているか否かを判定するのに用いられる。この値により「強edge」で検出されたエッジがどこまで続いているか、という結果に強い影響を及ぼす。

左から(0, 0)、(0, 128)、(64, 128)、(128, 128)

(0, 0)のエッジ検出結果 (0, 128)のエッジ検出結果 (64, 128)のエッジ検出結果 (128, 128)のエッジ検出結果

3.画像拡縮

今回は、指定された画像の「拡大・縮小」を行うプログラムを作成する。
拡縮では、他の画像・動画処理を行う上でも役に立つ「イメージサイズの変更」を行うので、ぜひ処理を把握して頂きたい

以下のソースコードは「画像読込、拡縮を行うための準備、拡縮、ウィンドウ表示、キー入力待ち、処理終了」という流れである。
画像はそれぞれ「元画像(origin)」と「元画像を縮小したもの(small)」「元画像を拡大したもの(large)」を、専用のウィンドウにて表示するようになっているが、「拡縮を行うための準備」と「拡縮」が加わった以外には、基本処理と流れはそれほど変わらない。

画像拡縮プログラム

#include "cv.h"
#include "highgui.h"
// 画像拡縮
int main(int argc, char* argv[])
{
	// 入力用画像
	IplImage* inputImage;
	// 出力用画像(縮小版)
	IplImage* outputSmallImage;
	// 出力用画像(拡大版)
	IplImage* outputLargeImage;
	// 読み込み画像ファイル名
	char imgPath[] = "lena.jpg";

	// 画像データ読み込み
	inputImage = cvLoadImage(imgPath, CV_LOAD_IMAGE_ANYDEPTH | CV_LOAD_IMAGE_ANYCOLOR);

	// 指定に基づいた画像ヘッダ、及びそのデータ領域を生成する
	// 1/2画像用の画像ヘッダとデータ領域を生成する
	CvSize smallSize = cvSize(inputImage->width / 1.2, inputImage->height / 1.2);
	outputSmallImage = cvCreateImage(smallSize, inputImage->depth, inputImage->nChannels);
	// 2/1画像用の画像ヘッダとデータ領域を生成する
	CvSize largeSize = cvSize(inputImage->width * 1.2, inputImage->height * 1.2);
	outputLargeImage = cvCreateImage(largeSize, inputImage->depth, inputImage->nChannels);

	// 実際の画像データを伸縮する
	cvResize(inputImage, outputSmallImage);
	cvResize(inputImage, outputLargeImage);

	// 表示用のウィンドウを生成する
	cvNamedWindow("small");
	cvNamedWindow("origin");
	cvNamedWindow("large");

	// 指定したウィンドウ内に画像を表示する
	cvShowImage("small", outputSmallImage);
	cvShowImage("origin", inputImage);
	cvShowImage("large", outputLargeImage);

	// キー入力待ち
	cvWaitKey(0);

	// ウィンドウの破棄
	cvDestroyWindow("small");
	cvDestroyWindow("origin");
	cvDestroyWindow("large");
	
	// 画像の解放
	cvReleaseImage(&outputSmallImage);
	cvReleaseImage(&inputImage);
	cvReleaseImage(&outputLargeImage);

	return 0;
}

実行結果

画像拡縮の結果

新しく使用したのは「cvResize()」である。
ただし、今回は画像のサイズを変更するため、cvCreateImage()で前もって準備を行っているところにも注目してほしい。

cvCreateImage()

今回は縮小後と拡大後の画像データを受け取るため、引数にそれぞれ元画像の高さと幅を「1.2」で拡縮計算したサイズを指定している。
今回引数内でGetSize()すると長くなるので、先にCvSizeで生成しているが、基本的に処理内容は変わらない。
デプスとチャネルに関しては、元画像と変わらないため、元画像のデータをそのまま引数として渡している

cvResize() - リファレンス

画像のサイズ変更には「cvResize()」を使用する。

void cvResize( const CvArr* src, CvArr* dst, int interpolation = CV_INTER_LINEAR);

cvResizeは「対象画像」「処理後画像」「補間方法」を引数に取る。

「対象画像」にはサイズ変更を行いたい画像データ、「処理後画像」では「対象画像」に指定されたサイズ変更と補間処理を施した画像データの受け口を指定する。

「補間方法」は、デフォルトで「CV_INTER_LINEAR」が指定される。その為、先のソースコードでは省略している。
「CV_INTER_LINEAR」以外の補間方法については以下通り。補間方法の実行結果例については補間のサンプルがあるので、ご覧いただきたい。

補間方法設定値
CV_INTER_NN最近隣接補間
CV_INTER_LINEARバイリニア補間 (デフォルト)
CV_INTER_AREAピクセル領域の関係を用いてリサンプリングする
CV_INTER_CUBICバイキュービック補間

4.画像回転

今回は、指定された画像を幾何変換で「回転させる」プログラムを作成する。
幾何変換自体は「回転」だけを行うためのものではないので、詳しくはリファレンスや各種情報を参照願いたい。

以下のソースコードは「画像読込、回転を行うための準備、回転、ウィンドウ表示、キー入力待ち、処理終了」という流れである。
途中に「回転を行うための準備」と「回転」が加わった以外には、基本処理と流れは変わらない。

画像回転プログラム

#include "cv.h"
#include "highgui.h"
// 画像回転
int main(int argc, char* argv[])
{
	// 入力用画像
	IplImage* img;
	// 出力用画像
	IplImage* img2;
	// 読み込み画像ファイル名
	char imgfile[] = "lena.jpg";
	// 回転行列
	CvMat* rotationMat;

	// 画像をカラーで読み込む
	img = cvLoadImage(imgfile, CV_LOAD_IMAGE_COLOR);
	// 指定に基づいた画像ヘッダ、及びそのデータ領域を生成する
	img2 = cvCreateImage(cvGetSize(img), IPL_DEPTH_8U, 3);

	// 回転行列領域の確保
	rotationMat = cvCreateMat(2 ,3 ,CV_32FC1);
	// 30度の回転行列を求める
	cv2DRotationMatrix(cvPoint2D32f(img->height / 2, img->width / 2), 30, 1, rotationMat);

	// 回転
	cvWarpAffine(img,img2,rotationMat);

	// 画像表示
	cvNamedWindow("lena", CV_WINDOW_AUTOSIZE);
	cvShowImage("lena", img2);
	cvWaitKey(0);

	// 終了処理
	cvDestroyWindow("lena");
	cvReleaseImage(&img);
	cvReleaseImage(&img2);
	cvReleaseMat(&rotationMat);

	return 0;
}

実行結果

画像回転の結果

新しく使用したのは「cvCreateMat()」「cv2DRotationMatrix()」「cvWarpAffine()」である。
基本的に画像回転を行う処理として、上の3つの手順を覚えておくとよい。

cvCreateMat() - リファレンス

画像のアフィン変換用に行列データ(マトリクス)を必要とするため、「cvCreateMat()」を使用し、内部ヘッダとデータ領域を生成する。
これにより生成された行列は行単位で保存され、すべての行は4バイトでアライメントされる。

CvMat* cvCreateMat( int rows, int cols, int type );

cvCreateMatは「行数」「列数」「行列要素の種類」を引数に取る。

「行数」「列数」はそのまま行列のサイズ指定を意味する。今回は後のcv2DRotationMatrix()の利用を考え「2x3」を指定している。

「行列要素の種類」は、次のフォーマットに準じた指定を行う。

CV_[bit_depth](S|U|F)C[number_of_channels]

例えば、「符号無し8ビット1チャネル」はCV_8UC1、「符号有り32ビット2チャネル」はCV_32SC2は、「浮動小数点32ビット3チャネル」ならばCV_32FC3と記述する。

cv2DRotationMatrix() - リファレンス

目的の30度の2次元回転のアフィン行列を計算するため、「cv2DRotationMatrix()」を使用する。

CvMat* cv2DRotationMatrix( CvPoint2D32f center, double angle, double scale, CvMat* map_matrix );

cvCreateMatは「(対象画像内の)回転中心」「度単位の回転角度」「等方性スケーリング係数」「2x3行列の受け口へのポインタ」を引数に取る。

「(対象画像内の)回転中心」はそのまま対象画像のどこを中心として回転させるかを指定する。引数には、浮動小数点型(単精度)座標系による2次元の点を表す「CvPoint2D32f」で渡す必要がある。
CvPoint2D32fはインライン関数になっているので、引数に「x, y」を指定してやることで、簡単に生成することができる。

「度単位の回転角度」「等方性スケーリング係数」はそれぞれそのまま、回転角度とスケーリング係数を指定する。今回は「30」「1」を指定している。

「2x3行列の受け口へのポインタ」は、計算された行列のポインタを受け取る行列のポインタを指定する。

cvWarpAffine() - リファレンス

ここまで用意した情報を元に元画像のアフィン変換を行う為、「cvWarpAffine()」を使用する。

void cvWarpAffine( onst CvArr* src, CvArr* dst, const CvMat* map_matrix, int flags = CV_INTER_LINEAR + CV_WARP_FILL_OUTLIERS, CvScalar fillval = cvScalarAll(0) );

cvWarpAffineは「対象画像」「変換後画像」「2x3行列」「補間方法」「対応の取れない点に対して与える値」を引数に取る。

「対象画像」には回転させたい画像データ、「変換後画像」では「対象画像」で処理した画像データの受け口を指定する。

「2x3行列」には、先に回転計算を終えた行列データを指定する。

「補間方法」については、先の「cvResize()」と同様の補間方法に加え「CV_WARP_FILL_OUTLIERS」「CV_WARP_INVERSE_MAP 」を組み合わせることができる。内容についてはリファレンスを参照願いたい。

「対応の取れない点に対して与える値」に関しては、そのまま変換時に対応が取れなかった場合に与える値を指定するが、特に理由がなければデフォルトで問題ない。

まとめ

以上が基本的な画像処理についてのサンプルコードである。他にも多様な処理が存在しているが、これら基本的な処理から流れを把握することで、新たな画像処理の方法の手掛りを掴めは、すぐにコードを書いて試してゆけるはずである。
通常の画像処理はここで終え、次からは「顔認識」について紹介してゆく。


参考資料

IplImage形式

これはIPL、正式名称「Intel Image Processing Library」という画像処理用ライブラリで使われている、画像処理を高速に行うための独自仕様の画像形式である。
つまり、このIPLで使われていた「IplImage」が、上記のIPL以外にも画像処理を行うOpenCV内でも利用されているわけである。

IPLそのものはIntelプロセッサに最適化されているため、大抵のマシン環境でかなり高速に処理可能なライブラリ・・・であった。
現在、IPLはかなり古いもので現在ではIntelからの入手はできない。必要になった場合はウェブ上を探す必要がある。
現状のIPLの行く末を知りたい場合は、「Intel IPP」で検索すると良い。