リアルタイム画像処理のための動体視力を求めないprintfデバッグ

#はじめに
リアルタイム画像処理をチューニングすると，計算速度や精度などをリアルタイムに表示しながら速くなったか，精度が維持できているかなどを確認しながら開発することが多くなります．


そのための簡単な確認方法はprintfデバッグとなります．
しかし，そのprintfデバッグも，実行速度が30msなどリアルタイム処理になるとあっという間に下に流れていき，必要な情報を追うためには高い動体視力が要求されます．

以下のような処理とかだと，各`func`が速いとつらいですよね．

```cpp
while(1)
{
	timer.start()
	funcA();
	cout<<timer.stop()<<endl;
	timer.start()
	funcB();
	cout<<timer.stop()<<endl;
	timer.start()
	funcC();
	cout<<timer.stop()<<endl;
}
```

イメージはこんな感じの出力です．
（昔の自分の目はこれが見えていて，問題なかったってのが信じられません．年齢に伴って動体視力も結構落ちます．．．）
![console2.gif](https://qiita-image-store.s3.amazonaws.com/0/67908/8a44a1cc-67b9-10fb-0e1a-4a9bbdfe4f25.gif)

これを何とかするには，以下の動画（[コマンドプロンプトで動くゲーム2(python)](https://www.youtube.com/watch?v=SldtrKnpEKQ)）みたいな感じでコンソール画面を同じ場所にprintf結果を出力するような懐かしのテクニックを駆使すれば問題解決可能です．
ですが，そのためには関係のないコーディングを結構頑張る必要があります．

そこでこの記事では，コンソール出力用のライブラリなどは使わずに，コンソール出力を黒い画像に白い文字を書くことで再現し，目に優しいprintfデバッグを実現します．

#putText
OpenCVでは画像にテキストを書き出す関数として[cv::putText](https://docs.opencv.org/4.0.0/d6/d6e/group__imgproc__draw.html#ga5126f47f883d730f633d74f07456c576)があります．それを活用して今回はコンソール画面を作るクラスを作ってみましょう．

```cpp
void cv::putText(InputOutputArray 	img,
                 const String & 	text,
                 Point 	org,
                 int 	fontFace,
                 double 	fontScale,
                 Scalar 	color,
                 int 	thickness = 1,
                 int 	lineType = LINE_8,
                 bool 	bottomLeftOrigin = false 
)	
```

これを座標をうまく指定して黒い画面に出力してあげればコンソール出力っぽくなります．出力結果は以下のようにできます．

コマンドプロンプトに出力されている文字を追うよりも断然目に優しくなっています．ハズキルーペなんか比ではありません．

![console3.gif](https://qiita-image-store.s3.amazonaws.com/0/67908/119e1197-cc9f-7151-c03a-9d35499832e1.gif)

さらにフォントにはいろいろあります（[リンク](https://docs.opencv.org/4.0.0/d6/d6e/group__imgproc__draw.html#ga0f9314ea6e35f99bb23f29567fc16e11)）が全体的にダサいです．
特に，すべてのフォントが[プロポーショナルフォント](https://ja.wikipedia.org/wiki/%E3%83%97%E3%83%AD%E3%83%9D%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%8A%E3%83%AB%E3%83%95%E3%82%A9%E3%83%B3%E3%83%88)であるためレイアウトが崩れてしまいます．
次は，OSに搭載されているフォントを使って表示してみましょう．

**参考：いろいろなフォント**
![text1.png](https://qiita-image-store.s3.amazonaws.com/0/67908/29864905-a5c3-96a2-62a3-6212e2b9744e.png)
生成コード

```cpp
void fonttest()
{
	namedWindow("font");
	Mat show(Size(640, 320), CV_8UC3);
	String str = "This is OpenCV";
	int n = 0;
	int step1 = 20;
	int step2 = 10;
	putText(show, "OpenCV FONT_HERSHEY_COMPLEX", Point(10, 30), FONT_HERSHEY_COMPLEX, 1, Scalar::all(255));
	n++;
	putText(show, "OpenCV FONT_HERSHEY_COMPLEX_SMALL", Point(10, 30 + step1 * n + step2 * n), FONT_HERSHEY_COMPLEX_SMALL, 1, Scalar::all(255));
	n++;
	putText(show, "OpenCV FONT_HERSHEY_DUPLEX", Point(10, 30 + step1 * n + step2 * n), FONT_HERSHEY_DUPLEX, 1, Scalar::all(255));
	n++;
	putText(show, "OpenCV FONT_HERSHEY_PLAIN", Point(10, 30 + step1 * n + step2 * n), FONT_HERSHEY_PLAIN, 1, Scalar::all(255));
	n++;
	putText(show, "OpenCV FONT_HERSHEY_SCRIPT_COMPLEX", Point(10, 30 + step1 * n + step2 * n), FONT_HERSHEY_SCRIPT_COMPLEX, 1, Scalar::all(255));
	n++;
	putText(show, "OpenCV FONT_HERSHEY_SCRIPT_SIMPLEX", Point(10, 30 + step1 * n + step2 * n), FONT_HERSHEY_SCRIPT_SIMPLEX, 1, Scalar::all(255));
	n++;
	putText(show, "OpenCV FONT_HERSHEY_SIMPLEX", Point(10, 30 + step1 * n + step2 * n), FONT_HERSHEY_SIMPLEX, 1, Scalar::all(255));
	n++;
	putText(show, "OpenCV FONT_HERSHEY_TRIPLEX", Point(10, 30 + step1 * n + step2 * n), FONT_HERSHEY_TRIPLEX, 1, Scalar::all(255));
	
	imshow("font", show);
	waitKey();
}
```

#addText
もし，GUIにQtを有効化している場合，テキスト出力に[addText](https://docs.opencv.org/4.0.0/dc/d46/group__highgui__qt.html#ga4e2751b94f709c848f0658d78007e449)関数が使えます．関数は以下で定義されています．

```cpp

void cv::addText	(	const Mat & 	img,
const String & 	text,
Point 	org,
const String & 	nameFont,
int 	pointSize = -1,
Scalar 	color = Scalar::all(0),
int 	weight = QT_FONT_NORMAL,
int 	style = QT_STYLE_NORMAL,
int 	spacing = 0 
)		
```
概ね，第4引数の`int 	fontFace`が`const String& nameFont,`に変わっただけで使い方はputTextと同じです．大事なことは，引数にstringでフォントの名前が入れられることです．
例えば，引数に`Consolas`（モノスペースフォント）を指定してみましょう．

```cpp
cv::addText(src, "text", Point(100, 100), "Consolas", fontSize, color);
```

![console4.gif](https://qiita-image-store.s3.amazonaws.com/0/67908/9754fd63-8e01-6b9d-95d9-4d93b11f2316.gif)

そうすると，フォントもきれいにかつ出力結果が整形されてきれいに整形されて出力されます．
これでだいぶ目に優しいですね．


#コード
実際使ったコードを以下に張り付けておきます．
なお，この関数は[このプロジェクト](https://github.com/norishigefukushima/OpenCP)からの抜粋です．

基本的な使い方は以下の通りです．

```cpp
ConsoleImage ci(Size(300,300));//consoleの画像サイズを指定
//ci.setIsLineNumber();//この命令で行番号を表示することもできます．
ci("text output");//operator()でテキストが出力可能です．
string str="abc";
ci(str);//stringも出力可能です．
ci("%d",10);//printfの入力も受け付けます．
ci(Scalar(0,255,0),"text");//引数の先頭に色を指定してテキストカラーも変えられます．
ci.show();//画面を出力します．デフォルトが出力をclsします．
waitKey(1);//imshowと同じでwaitKeyを入れないと画像が出ません．
```
行番号を入れて色を少し変えてみた結果

![console_screenshot_05.12.2018.png](https://qiita-image-store.s3.amazonaws.com/0/67908/973a0a3c-f2d1-8169-b4bc-1a37f166638f.png)

```cpp
class ConsoleImage
{
private:
	int count;
	std::string windowName;
	std::vector<std::string> strings;
	bool isLineNumber;
	int fontSize;
	int lineSpaceSize;
public:
	void setFontSize(int size);
	void setLineSpaceSize(int size);

	void setIsLineNumber(bool isLine = true);
	bool getIsLineNumber();
	cv::Mat image;

	void init(cv::Size size, std::string wname);
	ConsoleImage();
	ConsoleImage(cv::Size size, std::string wname = "console");
	~ConsoleImage();

	void printData();
	void clear();

	void operator()(std::string str);
	void operator()(const char *format, ...);
	void operator()(cv::Scalar color, std::string str);
	void operator()(cv::Scalar color, const char *format, ...);
	
	void show(bool isClear = true);
};

void ConsoleImage::setFontSize(int size)
{
	fontSize = size;
}

void ConsoleImage::setLineSpaceSize(int size)
{
	lineSpaceSize = size;
}

void ConsoleImage::init(Size size, string wname)
{
	isLineNumber = false;
	windowName = wname;
	image = Mat::zeros(size, CV_8UC3);
	fontSize = 20;
	lineSpaceSize = 5;
	clear();
	namedWindow(windowName);
}

ConsoleImage::ConsoleImage()
{
	init(Size(640, 480), "console");
}

ConsoleImage::ConsoleImage(Size size, string wname)
{
	init(size, wname);
}

ConsoleImage::~ConsoleImage()
{
	destroyWindow(windowName);
	printData();
}

void ConsoleImage::setIsLineNumber(bool isLine)
{
	isLineNumber = isLine;
}

bool ConsoleImage::getIsLineNumber()
{
	return isLineNumber;
}

void ConsoleImage::printData()
{
	for (int i = 0; i < (int)strings.size(); i++)
	{
		cout << strings[i] << endl;
	}
}

void ConsoleImage::clear()
{
	count = 0;
	image.setTo(0);
	strings.clear();
}

void ConsoleImage::show(bool isClear)
{
	imshow(windowName, image);
	if (isClear)clear();
}

void ConsoleImage::operator()(string src)
{
	this->operator()(Scalar(255, 255, 255), src);
}

void ConsoleImage::operator()(const char *format, ...)
{
	char buff[255];

	va_list ap;
	va_start(ap, format);
	vsprintf(buff, format, ap);
	va_end(ap);

	string a = buff;

	this->operator()(Scalar(255, 255, 255), a);
}

void ConsoleImage::operator()(cv::Scalar color, const char *format, ...)
{
	char buff[255];

	va_list ap;
	va_start(ap, format);
	vsprintf(buff, format, ap);
	va_end(ap);

	string a = buff;

	this->operator()(color, a);
}

void ConsoleImage::operator()(cv::Scalar color, string src)
{
if (isLineNumber)strings.push_back(format("%2d ", count) + src);
		else strings.push_back(src);

		int skip = fontSize + lineSpaceSize;
		cv::addText(image, strings[count], Point(skip, skip + count * skip), "Consolas", fontSize, color);
		//cv::putText(image, strings[count], Point(skip, skip + count * skip), CV_FONT_HERSHEY_COMPLEX_SMALL, 1.0, color, 1);
		
		count++;
}

void consoleTest()
{
	ConsoleImage ci(Size(640, 480));
	CalcTime t;//自作タイマー関数
	int count = 0;
	while (1)
	{
		Mat src;
		Mat dest;
		cout << count << endl;
		cout << "===========================" << endl;
		ci("%d", count);
		ci("===========================");
		{
			CalcTime t("image input", TIME_MSEC);
			src = imread("img/lenna.png");
			ci("image input     : %f ms", t.getTime());
		}
		{
			CalcTime t("box filter", TIME_MSEC);
			boxFilter(src, dest, src.depth(), Size(7, 7));
			ci("box filter      : %f ms", t.getTime());
		}
		{
			CalcTime t("gauss filter", TIME_MSEC);
			GaussianBlur(dest, dest, Size(7, 7), 2);
			ci("gauss filter    : %f ms", t.getTime());
		}
		{
			CalcTime t("median filter", TIME_MSEC);
			medianBlur(dest, dest, 5);
			ci("median filter   : %f ms", t.getTime());
		}
		{
			CalcTime t("bilateralfilter", TIME_MSEC);
			Mat temp = dest.clone();
			bilateralFilter(temp, dest, 7, 2, 2);
			ci("bilateral filter: %f ms", t.getTime());
		}
		cout << endl;
		ci.show();
		imshow("out", dest);
		waitKey(1);
		count++;
	}
}
```

# おわりに

だれかこのコードを"<<"のストリームに拡張してくれるとcoutっぽくなって嬉しいです．
次は，oyngtmhrさんの「オセロの勝敗判定を作ってみる」です！

あとこのGIF動画を作るために[ScreenToGif](https://github.com/NickeManarin/ScreenToGif)っていうツールを使いました．大変素晴らしいです．