凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

画像を2値化するクラス

龍神録プログラミングの館(http://dixq.net/rp/)の第54〜56章に掲載されている「画像弾幕」の作成を目標とします.現在作成中の愉繰郷はC++で作った,と*建前上*言えることを目標としているので,お勉強も兼ねてクラスにして見たいと思います.今回は,第54章(http://dixq.net/rp/54.html)『画像→弾幕 変換ツールを作ってみよう .1』の内容を扱います.
とは言うものの,中身は殆どDixqさんと同じコードです.

2値化クラス

LoadBmp.h

#include <string>
#include <boost/numeric/ublas/matrix.hpp>

/*! @brief 各ピクセルの情報を格納 */
typedef struct {
	unsigned char BGR[3];	//!< Blue, Green, Redの順番
} _COLOR_TABLE;

/*! @brief BMPを読み込むクラス */
class CLoadBmp {
private:
	const std::string FileName;	//!< 読み込んだBMPファイル名
	int Width;					//!< BMPの横幅px
	int Height;					//!< BMPの縦幅px
	short Bit;					//!< BMPのビット数

	//! BMPのファイルデータ
	boost::numeric::ublas::matrix<_COLOR_TABLE> ColorTable;

	//! 2値化フラグ BLACK/WHITEを格納
	boost::numeric::ublas::matrix<bool> BitTable;
	
	/*! @brief ファイルを読み込み各フィールドをセット */
	bool LoadFile();

	/*! @brief 2値化をする */
	void Binarization();

public:
	/*! 
	@brief コンストラクタ
	@param[in] fileName 読み込むBMPファイル名
	*/
	CLoadBmp(const char* fileName);
	
	/*! @brief BMPの横幅pxを得る */
	int GetWidth() const;

	/*! @breif BMPの縦幅pxを得る */
	int GetHeight() const;

	/*! @brief BMPのビット数を得る */
	int GetBit() const;

	/*! @brief BMPをそのまま描画 */
	void Draw() const;

	/*! @brief 2値化したBMPを描画 */
	void DrawBinary() const;
};

将来的に2値化以外にも,種々の色の弾に対応できたら,と思ってメンバ関数BinarizationはLoadFileと分けておきました.

LoadBmp.cpp

#include <DxLib.h>
#include <fstream>
#include "LoadBmp.h"

#define BMP_HEADER_SIZE (54)		// ビットマップのヘッダサイズ
#define BMP_BINARY_OFFSET (128)		// ビットマップ2値化の閾値輝度

using namespace std;

CLoadBmp::CLoadBmp(const char* fileName)
: FileName(fileName), Width(0), Height(0), Bit(0)
{
	// 各フィールドをセット
	if (!LoadFile()) return;
	
	// 2値化
	Binarization();
}

bool CLoadBmp::LoadFile()
{
	// ファイルの読み込み
	ifstream ifs(FileName.c_str(), ios::in | ios::binary);
	if (ifs.fail()) {
		printfDx("file open error: %s\n", FileName.c_str());
		return false;
	}

	// ファイルサイズのチェック
	size_t fileSize = (size_t)ifs.seekg(0, ios::end).tellg();
	if (fileSize <= BMP_HEADER_SIZE) {
		printfDx("file size error: %s\n", FileName.c_str());
		return false;
	}
	ifs.seekg(0, ios::beg);

	// 横幅
	ifs.seekg(0x0012, ios::beg);
	ifs.read(reinterpret_cast<char*>(&Width), sizeof(int));

	// 縦幅
	ifs.seekg(0x0016, ios::beg);
	ifs.read(reinterpret_cast<char*>(&Height), sizeof(int));

	// ビット数
	ifs.seekg(0x001C, ios::beg);
	ifs.read(reinterpret_cast<char*>(&Bit), sizeof(short));

	// カラー/ビットテーブルのサイズをセット
	ColorTable.resize(Width, Height);
	BitTable.resize(Width, Height);

	// 各ピクセルを読み込み
	ifs.seekg(BMP_HEADER_SIZE, ios::beg);
	for (int j=Height-1; j>=0; j--) {
		for (int i=0; i<Width; i++) {
			for (int k=0; k<3; k++) {
				char* data = reinterpret_cast<char*>(&ColorTable(i,j).BGR[k]);
				ifs.get(*data);
			}
		}
	}

	return true;
}

void CLoadBmp::Binarization()
{
	for (int i=0; i<Width; i++) {
		for (int j=0; j<Height; j++) {
			// 輝度平均を調べる
			int sum = 0;
			for (int k=0; k<3; k++) {
				sum += ColorTable(i,j).BGR[k];
			}
			sum /= 3;

			// 2値化
			BitTable(i,j) = (sum < BMP_BINARY_OFFSET) ? 1 : 0;
		}
	}
}

void CLoadBmp::Draw() const
{
	for (int x=0; x<Width; x++) {
		for (int y=0; y<Height; y++) {
			int Color = GetColor(ColorTable(x,y).BGR[2],ColorTable(x,y).BGR[1],ColorTable(x,y).BGR[0]);
			DrawPixel(x, y, Color);
		}
	}
}

void CLoadBmp::DrawBinary() const
{
	static int
		Black = GetColor(0,0,0),
		White = GetColor(255,255,255);
	DrawBox(0, 0, Width, Height, White, TRUE);
	for (int x=0; x<Width; x++) {
		for (int y=0; y<Height; y++) {
			if (BitTable[x][y]) DrawPixel(x, y, Black);
		}
	}
}

int CLoadBmp::GetWidth() const
{
	return Width;
}

int CLoadBmp::GetHeight() const
{
	return Height;
}

int CLoadBmp::GetBit() const
{
	return Bit;
}

めぼしい変更は,ビットマップの縦横 / ビット数を取得する部分くらいです.

実行

以下のコードをmain.cppとして実行しました.なお,KeyMouse.hは51章(http://dixq.net/rp/51.html)のものをそのまま用いています.

#include <DxLib.h>
#include "LoadBmp.h"
#include "KeyMouse.h"

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
{
	// BMPの読み込み
	CLoadBmp bmp("bmp/hecomi.bmp");
	
	// スクリーンサイズ変更
	SetGraphMode(bmp.GetWidth(), bmp.GetHeight(), 16);

	// Windowサイズ変更許可
	SetWindowSizeChangeEnableFlag(TRUE);

	// 初期化&描画スクリーンセット
	if (DxLib_Init() == 1 || SetDrawScreen(DX_SCREEN_BACK) != 0) {
		return -1;
	}

	// ループ
	bool Flag = true;
	int Key[256];
	while (
		ProcessMessage() == 0 &&
		ClearDrawScreen() == 0 &&
		GetHitKeyStateAll_2(Key) == 0 &&
		Key[KEY_INPUT_ESCAPE] == 0
	) {
		// スペースを押すと2値化
		Flag = !(Key[KEY_INPUT_SPACE]);

		if (Flag) {
			bmp.DrawBinary();
		} else {
			bmp.Draw();
		}

		// 反映
		ScreenFlip();
	}

	DxLib_End();
	return 0;
}

結果

通常時:

スペース押下時:

ってかこの画像では2値化の意味殆ど無い気がしますが,上手く動作していますね.
ただ,上手くいかないBMPファイルもありました.原因は今のところ不明で,正直,上手く動くファイルを使えばいいので解決しなくてもいいかなぁ.現在原因解明中です.

参考資料

ビットマップ読み込み時のエラー処理部に使わさせていただきました.boost使っても良かったのですが(matrix使ってるし),中身が良く判らないのでヤメておきました.

Dixqさんのページにも書かれておりましたが,ビットマップの中身に関しては,こちらのサイトを参考にさせていただきました.