HEXA BLOG

プログラム

HEXA BLOGプログラム2017.9.15

フルカラー画像をRGB565に減色

ラーメン屋でスタンプカード出すのを忘れがちな某(なにがし)です

 

過去にシュンスケさんが32bitカラー画像を16bitカラーに減色する方法を紹介してくれました。

 

32とか16とかその5
↑今回関係するのはこのへん

 

「組織的ディザ法」「誤差分散法」を試して、誤差分散法が結構イイカンジという話でした。

 

私のチームで、シュンスケさんの実装を借用してみたところ、
それなりにイイカンジではありますが、元画像からの劣化がやっぱり気にはなりました。

 

元画像
20170915_original

 

減色後
20170915_rgba4444

 

※画像は「魔法パスワード1111」で使われている背景画像です。
 減色による画質劣化がわかりやすいものを選びました。

 

画像全体にノイズのようなドットが目立ちます

 

そもそもこの元画像、見ての通り不透明な背景画像です。

 

つまり、アルファチャンネルを使っていないので、
RGBA各チャンネルを4bitずつ表現しているRGBA4444形式では、
アルファチャンネルの4bitがすべて無駄になっているということになります。

 

この無駄な4bitをうまく利用してやれば、さらにイイカンジにできそうです

 

16bitカラーと言っても、RGBA4444だけでなく、RGB565という形式もあります。
名前の通り、RGBがそれぞれ565bitで構成されて16bitとなります。

 

アルファチャンネルがないので、不透明画像に適しています。
緑(G)が1bitだけ他のチャンネルより大きいのが面白いですね。
1bit増えると表現可能な色数が倍になりますので、大きな差になります。

 

それでは、シュンスケさんの実装をRGBA4444からRGB565に対応させてみます

 

特定の桁数から、任意の桁数に収まるよう端数を切る処理が必要なので、
例えば、10桁から6桁に落とすなら、上位6桁を持ってきて、下位4桁を捨てる考えでいいでしょう。

 

32bitフルカラーからの減色の場合、1チャンネルあたり8bit(1byte)なので、
Nビットに落とすなら、上位Nビットを取ってくればOKです。

 

// 1ピクセルあたりのバイト数
private const int PIXEL_BYTES = 4;

// 1バイトあたりのビット数
private const int BYTE_BITS = 8;

// 減色モード
private enum Mode
{
	RGBA4444,
	RGB565,
}

// チャネル
private enum Channel
{
	R,
	G,
	B,
	A,
}

private static uint quantizeChannel(uint value, Mode mode, Channel channel)
{
	var bitCount = BYTE_BITS;

	switch(mode) {
	case Mode.RGBA4444:
		bitCount = 4;
		break;
	case Mode.RGB565:
		if(channel == Channel.A) {
			bitCount = 0;
		}
		else if(channel == Channel.G) {
			bitCount = 6;
		}
		else {
			bitCount = 5;
		}
		break;
	default:
		break;
	}

	if(bitCount >= BYTE_BITS) return value;
	if(bitCount <= 0) return 0xFF;

	// 上位bitCountビット分を返す
	var shiftBits = BYTE_BITS - bitCount;
	return value & (~0u << shiftBits);
}

private static uint channelFilter(uint org, int x, int y, ref double[,] err, Mode mode, Channel channel)
{
	// 蓄積された誤差を加えて近似
	var tmp = (err[x, y] + org);
	if(tmp < 0) {
		tmp = 0;
	}
	if(tmp > 255) {
		tmp = 255;
	}
	var c = quantizeChannel((uint)Math.Round(tmp), mode, channel);

	// 誤差を分散して蓄積
	double e = tmp - c;
	for(int i = 0; i < DISPERSION_TBL.Length; ++i) {
		var p = DISPERSION_TBL[i];
		var xIdx = x + p.offsetX;
		var yIdx = y + p.offsetY;
		var xLen = err.GetLength(0);
		var yLen = err.GetLength(1);
		if(xIdx < 0 || xIdx >= xLen ||
		yIdx < 0 || yIdx >= yLen) continue;

		err[xIdx, yIdx] += (p.rate * e);
	}

	return c;
}

private static void filter(ref byte[] bmp, int w, int h, Mode mode)
{
	// 誤差格納用の領域を確保
	var errR = new double[w, h];
	var errG = new double[w, h];
	var errB = new double[w, h];
	var errA = new double[w, h];

	for(int i = 0; i < w; ++i) {
		for(int j = 0; j < h; ++j) {
			errR[i, j] = 0;
			errG[i, j] = 0;
			errB[i, j] = 0;
			errA[i, j] = 0;
		}
	}

	for(int y = 0; y < h; ++y) {
		for(int x = 0; x < w; ++x) {
			var index = (y * w + x) * PIXEL_BYTES;
			uint r = bmp[index + 0];
			uint g = bmp[index + 1];
			uint b = bmp[index + 2];
			uint a = bmp[index + 3];

			r = channelFilter(r, x, y, ref errR, mode, Channel.R);
			g = channelFilter(g, x, y, ref errG, mode, Channel.G);
			b = channelFilter(b, x, y, ref errB, mode, Channel.B);
			a = channelFilter(a, x, y, ref errA, mode, Channel.A);

			var color = Color.FromArgb((int)a, (int)r, (int)g, (int)b);
			bmp[index + 0] = color.R;
			bmp[index + 1] = color.G;
			bmp[index + 2] = color.B;
			bmp[index + 3] = color.A;
		}
	}
}

 

では、出力を比較してみましょう。

 

元画像
20170915_original

 

RGB565
20170915_rgb565

 

RGBA4444
20170915_rgba4444

 

全然違いますね。RGB565なら、ぱっと見では元画像と同じに見えます

 

ところで、Unity(5.4以前)でテクスチャフォーマットを16bit Colorで指定したとき、
元画像が不透明だと自動でRGB565に変換されます(※)が精度は良くないです・・・

 

ここで今回の手法を適用した画像を使うと、Unityの自動変換による劣化をかなり防げます

 

※Unity 5.5からテクスチャフォーマットの直接指定はプラットフォーム別にしかできなくなりました。
 旧バージョンからアップグレードした時には、metaに中途半端な設定が残ることもあるので注意しましょう。
 エディタ上で「RGB 16 bit」と表示されるものが「RGB565」に相当します。
(参考:Unity – Manual: Texture compression formats for platform-specific overrides

 

こういう減色ツールが手軽に使えるものとしてなかったりしますが、
原理は簡単なので作ってしまえば何かと融通がききそうです

RECRUIT

大阪・東京共にスタッフを募集しています。
特にキャリア採用のプログラマー・アーティストに興味がある方は下のボタンをクリックしてください

RECRUIT SITE