1 本日の実習

マーカは色の付いた正方形を用いるものとし,カメラ画像から,
特定の色の領域を抽出するプログラムを作成する.

具体的には左側の入力画像(各画素8ビット3成分)から,右側の出力画像
(各画素は0か255で,255なら対象とするマーカの色領域)を得る.

input result

2 ベースとなるプログラム

先週の「カメラから画像を取得しウィンドウに表示するプログラム」を元にする.

CameraTest

3 画像とMatクラス

画像は画素がx軸方向,y軸方向に並んだものである.
OpenCV v2 では,画像は Mat クラスに格納される.
Mat クラスに格納された各画素が,どのような型の数値を何個保持しているかは,
画像の種類によって異なる.
典型的には,各成分8ビット(1バイト)で,カラー画像なら3成分,
グレイスケール画像から1成分である.
Mat インスタンスの情報はメンバとメソッドを使って知ることができる.

Mat frame;

cout << "dims: " << frame.dims << endl;
cout << "cols(=width): " << frame.cols << endl;
cout << "rows(=height): " << frame.rows << endl;
cout << "elemSize1: " << frame.elemSize1() << endl;
cout << "channels: " << frame.channels() << endl;

上から順に,次元(画像の場合は2),横画素数,縦画素数,各成分のサイズ(単位はバイト),
成分(チャネル)数,である.
Mat クラスの詳しい情報については,以下を参照のこと.

4 全体の方針: 色に基づく判別

処理の基本は,各画素の画素値に基づいて,その画素がマーカであるか
そうでないかを判断すればよい.
マーカの色に応じて,マーカと判断するべき画素値の各成分の最大や最小を
適切に設定する必要がある.

5 方法1: 画素単位で処理を進める

画素単位の二重ループをまわし,その中で画素値を取得して判定を行う.
直感的だが,低速である.

5.1 結果を格納する Mat インスタンスの作成

Mat クラスは,OpenCVの各関数の結果を格納する際には,基本的には
自動的に適切なサイズや画素値の型が決定されるが,方法1のように
自分で各画素へのアクセスを行うような場合には,宣言時にサイズや型を
指定する必要がある.
以下に,frame と同じサイズで,画素が8ビット1成分のMatインスタンス
resultを宣言する例を示す.

Mat result(frame.size(), CV_8UC1);

5.2 画素値の取り出し方法

Matクラスに格納されている画素値へのアクセス方法はいくつかあるが,
直接座標を指定する場合にはat()メソッドを用いる.
Matクラスの画素が様々な型を格納できるため,at()は関数テンプレートに
なっている.8ビット3成分の画像frameから画素値を取得する方法は以下の通り.

Vec3b c = frame.at<Vec3b>(y, x);
unsigned char red, green, blue;
red = c[2];
green = c[1];
blue = c[0];

また8ビット1成分の画像resultに画素値を設定する方法は以下の通り.

unsigned char c = 255;
result.at<unsigned char>(y, x);

1画素ずつ処理を進めるプログラムの,メイン部分はこんな感じ.

	while (1) {
		Mat frame;
		cap >> frame;

		Mat result(frame.size(), CV_8UC1);

		for (int y = 0; y < frame.rows; y++) {
			for (int x = 0; x < frame.cols; x++) {
				
/* ここを適切に埋める */

			}
		}

		imshow(window_input, frame);
		imshow(window_result, result);

		int key = waitKey(10);
		if (key == 3 || key == 27 || key == 'q') {
			break;
		}
	}

6 方法2: チャネル単位で処理を進める

よりOpenCVらしい方法として,画像をチャネル(ここでは赤成分の画像,緑成分の
画像,青成分の画像)に分解して,各成分画像に対して閾値処理を行う
方法がある.

6.1 カラー画像を成分画像に分解する

split()関数を用いる.

Mat channels[3];
split(frame, channels);

典型的な例を下図に示す.順に,入力,赤,緑,青である.

inputred  greenblue

6.2 成分画像に対して閾値処理を行う

OpenCVでは閾値処理を行う関数がいくつか用意されている.

6.2.1 threshold()関数

ある値(閾値)と画素値を比較し,その結果に応じて出力画像の画素値を決定する.
入力画像は1チャネルでなければならない.

threshold(src, dst, thresh, max_value, type);

引数は順に,入力画像,出力画像,閾値,設定される値,閾値処理の方法 である.
閾値処理の方法はいくつかあるが,THRESH_BINARY と THRESH_BINARY_INV を
とりあえず知っておく.THRESH_BINARYは閾値thresh以上の値の画素値をmax_value
に,それ以外を0にする.THRESH_BINARY_INVはthresh以下ならmax_valueに,
それ以外なら0になる.以下の例は,入力,THRESH_BINARY (閾値128),THRESH_BINARY_INV(閾値128)の画像である.

gradation result_thresh result_thresh_inv

6.2.2 inRange() 関数

threshold()では一つの閾値しか設定できない.これに対して,inRange()関数では
上限と下限を設定することができる.

inRange(src, lower, upper, dst);

引数は順に,入力画像,下限,上限,出力画像である.threshold()では
条件を満たす画素の出力値を指定できたが,inRange()では固定で255となる.以下の例は,下限64,上限192で処理した結果である.

gradation result_inrange

6.3 閾値処理を施したチャネルを統合する

赤成分の条件を満たす画像,緑成分の条件を満たす画像,青成分の条件を満たす
画像から,これら3条件を同時に満たす画像を生成したい.
画素単位で論理積(AND)を取ればよいが,Matクラスでは演算子 & や | が
オーバーロードされており,整数型と同じ感覚で論理積や論理和を実行できる.

画像 a と 画像 b の論理演算の例を以下に示す.順に,画像a,画像b,a & b,a | b である.

src_a src_bresult_andresult_or

Mat a = Mat::zeros(Size(256, 256), CV_8UC1);
Mat b = Mat::zeros(Size(256, 256), CV_8UC1);

circle(a, Point(96, 128), 80, 255, -1);
circle(b, Point(160, 128), 80, 255, -1);

Mat result_and;
Mat result_or;
result_and = a & b;
result_or = a | b;

以上をまとめると,チャネルごとに処理するプログラムの骨組はこんな感じ.

	while (1) {
		Mat frame;
		cap >> frame;

		Mat channels[3];
		// ここに書く: frame を channels に分解

		Mat binarized[3];
		// ここに書く: channelsのそれぞれに対して閾値処理した結果をbinarizedに格納

		Mat result;
		// ここに書く: binarizedから最終的なマーカ領域を算出

		// 以下,ウィンドウへの表示と,キー入力判別(従来と同じ)
	}

7 色空間の変換

我々が通常よく扱うのはRGB色空間だが,HSV色空間に変換した方が
ある特定の色の抽出には便利なことが多い.
HSV色空間については以下を参照.
https://ja.wikipedia.org/wiki/HSV%E8%89%B2%E7%A9%BA%E9%96%93

OpenCVでカメラやファイルから画像を1フレーム得ると,
各画素にはBGRの順に成分の値が入っている.
cvtColor()関数を用いると,色空間の変換ができる.

Mat source; // RGB色空間(画素内の成分の並びはBGRの順)
Mat result;

cvtColor(source, result, CV_BGR2HSV);

変換後のHSV空間では,Hは0から179の,Sは0から255の,Vは0から255の値を取る.

後はチャネル画像にsplit()で分割して,閾値処理…とRGB色空間と同じ流れでよい.

8 トラックバーの設置

様々なパラメータを変えてテストするごとにコンパイルするのは面倒なので,
トラックバーを設置するのがよい.

char *window_result = "result";
namedWindow(window_result, CV_WINDOW_AUTOSIZE);

int threshold_hue;

createTrackbar("hue", "result", &threshold_hue, 179);

この例では,result という名前の Window に,hue という名前のトラックバーを設置し,
値は変数 threshold_hue と連動し,最大値は179である.
トラックバーをいじるとthreshold_hueの値が自動的に変化するので,処理の中ではこの変数を適宜参照すればよい.