OpenCVでスクリーンショット保存

こんにちは!
くろんです。

今回はOpenCVを使って画面のスクリーンショット画像を保存してみたいと思います。

まぁ、そこまで大変でもないので適当にコードとか載せていきます。

やること

1. WINAPIにてスクリーン情報取得
2. スクリーン情報より画面のピクセル情報を取る
3. OpenCVの画像フォーマットにピクセル情報を入れ込む

準備

- OpenCV準備(https://opencv.org/releases/からDL)
- 開発環境準備 -> VisualStudio2019使用
- 新規プロジェクト作成

OpenCVは今回、4.2.0を使用しました。

Windowsなら ↑ の赤枠からexe拾ってきて

展開フォルダ内のbuildから

bin\*.dll
include
x64\(VCバージョン)\*.lib

を自分のプロジェクトフォルダに持ってくると楽ですよ!

OpenCVを新規プロジェクトで使えるようにするためにはリンカーの設定を行います。

1. 新規プロジェクトを開き、「プロジェクト」タブ→「プロパティ」選択
2. 構成を「Release」に設定(Debugは同じく設定)
3. 「リンカー」→「全般」より「追加のライブラリディレクトリ」を選択
4. x64\(VCバージョン)\*.libより持ってきた*.libファイルがあるフォルダを選択
※opencv_world420.libとopencv_world420d.libというファイル名にdがついているものとそうでいないものがあると思うがこれらはRelease用かDebug用かで変わっている。dがついているものがDebug用

5. 「リンカー」→「入力」より「追加の依存ファイル」選択
6. 4.にて指定したフォルダに入っている*.libファイルをすべて依存ファイルとして指定
ex. Release用として設定していたら「 opencv_world420.lib」を追加

ついでにプロパティページにて「C/C++」内の「追加のインクルードディレクトリ」に includeフォルダを指定しておくと後で楽になります。

最後に動的ライブラリ(*.dll)を実行ファイルと同じ場所に置きます。
ビルドすると*.exeができるかと思うのでbin\*.dllをコピーします。

準備はこれでOKなはずです。

まとめ

- リンカーを設定
  - 追加のライブラリディレクトリ
  - 追加の依存ファイル
- 動的ライブラリ(*.dll)を実行ファイル(*.exe)と同じ場所に置く

コード書く

↓ 全体

HWND hwnd = GetDesktopWindow();
RECT rect;

GetWindowRect(hwnd, &rect);
unsigned int width, height;
width = rect.right;
height = rect.bottom;

BITMAPINFO bmpInfo;

bmpInfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bmpInfo.bmiHeader.biWidth = width;
bmpInfo.bmiHeader.biHeight = height;
bmpInfo.bmiHeader.biPlanes = 1;
bmpInfo.bmiHeader.biBitCount = 24;
bmpInfo.bmiHeader.biCompression = BI_RGB;

LPDWORD lpPixel;
HBITMAP hBitmap;
HDC hMemDC;
HDC hdc = GetDC(hwnd);
hBitmap = CreateDIBSection(hdc, &bmpInfo, DIB_RGB_COLORS, (void**)&lpPixel, NULL, 0);

hMemDC = CreateCompatibleDC(hdc);
SelectObject(hMemDC, hBitmap);

BitBlt(hMemDC, 0, 0, width, height, hdc, 0, 0, SRCCOPY);

cv::Mat bMat = cv::Mat(height, width, CV_8UC3);
bMat.data = (uchar*)lpPixel;

cv::Mat flipImage;
cv::flip(bMat, flipImage, 0);

cv::imwrite("test.jpg", flipImage);

まず

HWND hwnd = GetDesktopWindow();
RECT rect;

GetWindowRect(hwnd, &rect);
unsigned int width, height;
width = rect.right;
height = rect.bottom;

ここらへんでスクリーンの幅・高さとディスプレイのハンドルを取得します。

次に

BITMAPINFO bmpInfo;

bmpInfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bmpInfo.bmiHeader.biWidth = width;
bmpInfo.bmiHeader.biHeight = height;
bmpInfo.bmiHeader.biPlanes = 1;
bmpInfo.bmiHeader.biBitCount = 24;
bmpInfo.bmiHeader.biCompression = BI_RGB;

ここでBitmap形式で取得するときに必要な情報を設定します。(今回はOpenCVのMatフォーマットになるのでBitmap関係ないですがスクリーンの画素情報を取得するための儀式として書きます)

そして

LPDWORD lpPixel;
HBITMAP hBitmap;
HDC hMemDC;
HDC hdc = GetDC(hwnd);
hBitmap = CreateDIBSection(hdc, &bmpInfo, DIB_RGB_COLORS, (void**)&lpPixel, NULL, 0);

hMemDC = CreateCompatibleDC(hdc);
SelectObject(hMemDC, hBitmap);

Bitmap形式で画素を取得する準備を行います。

Bitmap形式でスクリーンの画素情報を取得するときにはBitBltを使用します。

BitBlt(hMemDC, 0, 0, width, height, hdc, 0, 0, SRCCOPY);

最後にOpenCVのMatフォーマットに入れ込みます

cv::Mat bMat = cv::Mat(height, width, CV_8UC3);
bMat.data = (uchar*)lpPixel;

cv::Mat flipImage;
cv::flip(bMat, flipImage, 0);

cv::imwrite("test.jpg", flipImage);

biBitCountで24(8bit3チャンネル)を指定していたのでMatフォーマットは CV_8UC3タイプを選択。

bMat.data はMatファイルの画素情報に当たる部分でそこにBitmap形式でスクリーン画素情報が入っている lpPixelの情報を入れています。

cv::flipで画像を上下反転しているのですがこれはデータの並びが逆なのでそのままだと反転して画像ができてしまうためです。

cv::imwriteは画像の保存をしてくれるOpenCVの関数です。

こんな感じにするとスクリーンショットをOpenCVのMatフォーマットにできます!!

※あっ、この方法だとディスプレイ設定で拡大表示とかして100%以外だとスクリーンの一部しか取得できない状態になるので気を付けてください。

↑ を回避するためにはもう少しごにょごにょやってあげる必要があります。

Sleepとsleep_for

こんにちは!!
くろんです

今回はWindowsにてC++で処理を待たせる関数、Sleep()とstd::this_thread::sleep_for()の違いについて考えたいと思います。

先に結論を
- C++11にて追加されているので、現在の標準はこっち?
- ナノ秒での指定ができるので精度が高い
- 時間でも指定ができるので最大指定時間も大きい

Sleep()はWindowsAPIにある関数になります。

使用すると指定された相対時間分、現在のスレッドを止めてくれます。
指定はmillisecondsなので1000分の1秒にて指定ができます。

sleep_for()はC++11にて追加された関数です。

こちらもSleep()同様に指定された相対時間分、現在のスレッドを止めます。
こちらはSleep()とは異なりnanosecondsまで指定ができます。

milliseconds = 1 / 1000 秒
microseconds = 1 / 1000 milliseconds
nanoseconds = 1 / 1000 microseconds

なのでsleep_for()はSleep()の100万分の1の精度で指定できることになります。

100万分の1の精度で指定できるということはなんか色々性能が高そうな感じがしたので、実際のコードで測定をしてみました。

以下、検証コード

#include <chrono>
#include <thread>
#include <Windows.h>

void winSleepLoop(int waitMilliseconds, int loopTimes)
{
	for(int i = 0; i < loopTimes; i++) {
		Sleep(static_cast<DWORD>(waitMilliseconds));
	}
}

void threadSleepLoop(int waitNanoseconds, int loopTimes)
{
	for(int i = 0; i < loopTimes; i++) {
		std::chrono::nanoseconds ns(waitNanoseconds);
		std::this_thread::sleep_for(ns);
	}
}

int main(int argc, char* argv[])
{
	LARGE_INTEGER freq;
	QueryPerformanceFrequency(&freq);

	LARGE_INTEGER start, end;
	QueryPerformanceCounter(&start);

	// wait time(milli sec)
	winSleepLoop(100, 100);

	QueryPerformanceCounter(&end);

	double duration = (end.QuadPart - start.QuadPart)
						/ static_cast<double>(freq.QuadPart);
	printf("winSleep = %f\n", duration);

	QueryPerformanceCounter(&start);

	// wait time(nano sec)
	threadSleepLoop(100000000, 100);

	QueryPerformanceCounter(&end);
	
	duration = (end.QuadPart - start.QuadPart)
				/ static_cast<double>(freq.QuadPart);
	printf("threadSleep = %f\n", duration);

	return 0;
}

検証結果

winSleep = 10.057575
threadSleep = 10.049585

検証結果…まったく変わりませんでした!!

結果はあまり変わらなかったということで処理性能的にはどっちを選んでも大丈夫そうですね。

少しくらい誤差でるかなと思っていたので残念です。

ちなみにSleepでは時間をDWORDで指定するのですが、こちらはunsigned longで定義されているので数値範囲は 0 ~ 4,294,967,295 となります。
maxの数値は日数で表すと約50日分になります。

なので、Sleep()一つでは50日以上処理を待たせることができません。待たせる必要があるのかという素朴な疑問は置いておきますが…

ただ、sleep_for()ではchronoで定義されている型を使用して時間での指定ができるので、50日以上待たせることが可能となります!!

以上を踏まえてsleep_for()を使うメリットはこんな感じですかね。

  • C++11にて追加されているので、現在の標準はこっち?
  • ナノ秒での指定ができるので精度が高い
  • 時間でも指定ができるので最大指定時間も大きい

まぁ、面倒でなければC++で書くのはsleep_for()のほうがいいかと思います。

では