ページ 11

識別しやすい色の選び方

Posted: 2011年1月14日(金) 16:10
by たろ
お世話になります。
黒か白の背景上の、「カラフルな識別しやすい色」を選ぶ方法で悩んでいます。コメント頂ければ幸いです。

環境: WinXP Home SP3, VC2008Express, BCC551, Win32API, C言語

棒グラフや折れ線グラフを描くプログラムを作ろうとしています。グラフの背景は白か黒を考えています。
Excelでいう「データ系列」がたくさんあり、1つのグラフに何種類もの系列の棒グラフ折れ線グラフを詰め込みます。
同じ色を使うと見にくいため、各系列に見やすいカラフルな色を割り振って描画したいと思っています。
その「見やすい識別しやすい色」を選ぶ方法について悩んでいます。

自分で適当に考えた方式でサンプルを作ってみていますが、うまく色を選べているのか?いまいちよくわかりません。
その方式とは、「色相環の上で、できるだけ離れた角度の色を順番に使っていく」、という方法です。
※Wikipediaの「HSV色空間」を参考にしています。
http://ja.wikipedia.org/wiki/HSV%E8%89% ... A%E9%96%93
0度(真上)からスタートした場合、次は180度(真下)、次は90度か270度のどちらか、・・・
という具合に、角度的にできるだけ離れた点を選んでいき、RGB値を決めるという感じです。

そうして順番に360個まで選んでいった色を、画面に並べるプログラムを作ってみました。実行すると、格子状?に
色が並んだウィンドウを表示します。左上から右方向に、色相環上できるだけ離れた色を順番に並べたつもりです。

コード:

//
//	>cl FarColor.c
//	>bcc32 -W FarColor.c
//
#pragma comment( lib, "user32.lib" )
#pragma comment( lib, "gdi32.lib" )
#include <windows.h>
#include <stdio.h>
#include <string.h>

//
//	できるだけ離れた角度を順番に求めるための管理構造体
//	角度は整数値0~359の360個
//
typedef struct {
	int		StartAngle;			// 開始角
	int		NextAngle;			// 次の角度
	BYTE	UsedAngle[360];		// 使用済み角度フラグ(360個ぶん)
} FarAngleEnumerator[1];

//
//	色構造体(32bitRGBA)
//
typedef union
{
	UINT32		Pixel;
	struct {
		BYTE	B,G,R,A;
	};
} PIXEL32;


PIXEL32		ColorArray[360];		// 離れた色を順番に格納する配列
HWND		hWnd = NULL;			// ウィンドウハンドル


//
//	角度算出の初期化
//	angle = 開始角度
//
void FarAngleEnumeratorInit( FarAngleEnumerator fe, int angle )
{
	// 使用済み角度:最初はぜんぶ未使用
	memset( fe->UsedAngle, 0, sizeof(fe->UsedAngle) );

	// 開始角度
	fe->StartAngle = angle;
	fe->UsedAngle[angle] = 1;	// 使用済み

	// 次の角度
	fe->NextAngle = angle;
}

//
//	角度を0~359の範囲になおす
//
int Angle360( int angle )
{
	angle %= 360;
	if( angle < 0 ) angle += 360;
	return angle;
}

//
//	次の離れた角度を求めて返す
//
int FarAngleNext( FarAngleEnumerator fe )
{
	float degree;

	// まず最も遠い反対側(180度)が空いてるか調べる
	fe->NextAngle = Angle360( fe->NextAngle + 180 );
	if( !fe->UsedAngle[fe->NextAngle] )
	{
		// 空いてるので使用済みにして返却
		fe->UsedAngle[fe->NextAngle] = 1;
		return fe->NextAngle;
	}

	// 180度は空いてなかったので、90渡、45度・・を調べる
	for( degree=180; degree>=1; degree/=2 )
	{
		float delta;

		for( delta=degree/2; delta<=180; delta+=degree )
		{
			// 角度プラス方向とマイナス方向の2つを調べる
			int plus = Angle360( fe->NextAngle + delta );
			int minus = Angle360( fe->NextAngle - delta );

			// どちらか空いていれば使用済みフラグ立てて返却
			if( !fe->UsedAngle[plus] )
			{
				fe->UsedAngle[plus] = 1;
				fe->NextAngle = plus;
				return plus;
			}
			if( !fe->UsedAngle[minus] )
			{
				fe->UsedAngle[minus] = 1;
				fe->NextAngle = minus;
				return minus;
			}
		}
	}
	// 見つからなかった場合は開始角度を返す
	return fe->StartAngle;
}

//
//	HSV色空間
//	http://ja.wikipedia.org/wiki/HSV%E8%89%B2%E7%A9%BA%E9%96%93
//	HSVからRGBへの変換
//
PIXEL32 HSVtoRGB( float H, float S, float V )
{
	float	R, G, B;
	PIXEL32	px;

	if( H<0.0 ) H = 0.0; else if( H>360.0 ) H = 360.0;
	if( S<0.0 ) S = 0.0; else if( S>1.0 ) S = 1.0;
	if( V<0.0 ) V = 0.0; else if( V>1.0 ) V = 1.0;

	if( S==0.0 )
	{
		// 1. もしSが0.0と等しいなら、
		R = G = B = V;
	}
	else
	{
		// 2. Sがゼロでない場合、
		int		Hi = (int)( H / 60 ) % 6;
		float	f = H / 60 - Hi;
		float	p = V * ( 1 - S );
		float	q = V * ( 1 - f * S );
		float	t = V * ( 1 - ( 1 - f ) * S );

		switch( Hi ) {
		case 0: R = V, G = t, B = p; break;
		case 1: R = q, G = V, B = p; break;
		case 2: R = p, G = V, B = t; break;
		case 3: R = p, G = q, B = V; break;
		case 4: R = t, G = p, B = V; break;
		case 5: R = V, G = p, B = q; break;
		default: R = G = B = 0.0;
		}
	}
	// 0.0~1.0を0~255に変換
	px.R = (BYTE)( R * 255 );
	px.G = (BYTE)( G * 255 );
	px.B = (BYTE)( B * 255 );
	px.A = 255;
	return px;
}

//
//	色配列を作成する
//
void ColorArrayCreate( void )
{
	#define	START_ANGLE	0		// 開始角度
	FarAngleEnumerator	fe;
	int					angle;
	int					i, j;

	ZeroMemory( ColorArray, sizeof(ColorArray) );

	// 開始角度
	FarAngleEnumeratorInit( fe, START_ANGLE );
	// HSV→RGB変換
	ColorArray[0] = HSVtoRGB( (float)START_ANGLE, 1.0, 1.0 );

	for( i=1; i<360; i++ )
	{
		// 角度を求めて
		angle = FarAngleNext( fe );
		if( angle==START_ANGLE ) break;	// 見つからなければおわり

		// HSV→RGB変換して配列に追加
		ColorArray[i] = HSVtoRGB( (float)angle, 1.0, 1.0 );
	}

	if( i<360 ) MessageBox(hWnd,"360個より少ない数で終わってしまいました","",0);

	// 角度ぜんぶ(360個)使ったかチェック
	for( i=0; i<360; i++ )
	{
		if( !fe->UsedAngle[i] )
		{
			char msg[128];
			_snprintf(msg,sizeof(msg),"少なくとも角度%uが使われていません",i);
			MessageBox(hWnd,msg,"",0);
			break;
		}
	}
	// 重複する値がないかチェック
	for( i=0; i<360; i++ )
	{
		for( j=0; j<360; j++ )
		{
			if( i==j ) continue;
			if( ColorArray[i].Pixel==ColorArray[j].Pixel )
			{
				char msg[128];
				_snprintf(msg,sizeof(msg),"角度%uと角度%uが重複しています",i,j);
				MessageBox(hWnd,msg,"",0);
				break;
			}
		}
	}
}

//
//	よくわからないので配列をソートして表示してみるためのソート関数。
//	きれいにグラデーション状に並ぶかと思いきや、中央付近でなぜか離れた色が並んでしまう・・
//	上下はきれいに並んでいるが・・
//
typedef int (*QSORT_COMPARE)( const void*, const void* );
int ColorCompare( const PIXEL32* p1, const PIXEL32* p2 )
{
	return (p1->Pixel > p2->Pixel)? 1 : (p2->Pixel > p1->Pixel)? -1 : 0;
}
void ColorArraySort( void )
{
	qsort( ColorArray, 360, sizeof(PIXEL32), (QSORT_COMPARE)ColorCompare );
}

//
//	ウィンドウプロシージャ
//
LRESULT CALLBACK WindowProc( HWND hwnd, UINT msg, WPARAM wp, LPARAM lp )
{
	switch( msg )
	{
		case WM_CREATE:
		{
			hWnd = hwnd;
			ColorArrayCreate();
			//ColorArraySort();
			return 0;
		}
		case WM_PAINT:
		{
			// 色配列を画面に並べて表示する
			PAINTSTRUCT	ps;
			HDC			dc;
			dc = BeginPaint( hwnd, &ps );
			if( dc )
			{
				#define	BOX_SIZE		30	// 1色の正方形大きさ(ピクセル)
				#define	BOX_PER_LINE	20	// 1行に並べる個数
				int	i, top=0;
				for( i=0; i<360; i++ )
				{
					PIXEL32	px = ColorArray[i];
					// PatBltの使い方
					// http://wisdom.sakura.ne.jp/system/winapi/win32/win103.html
					SelectObject( dc, CreateSolidBrush( RGB(px.R,px.G,px.B) ) );
					PatBlt( dc, (i%BOX_PER_LINE)*BOX_SIZE, top, BOX_SIZE, BOX_SIZE, PATCOPY );
					DeleteObject( SelectObject( dc, GetStockObject(WHITE_BRUSH) ) );
					// 改行
					if( (i%BOX_PER_LINE)==(BOX_PER_LINE-1) ) top += BOX_SIZE;
				}
				EndPaint( hwnd, &ps );
			}
			return 0;
		}
		case WM_DESTROY:
			PostQuitMessage(0);
			return 0;
	}
	return DefWindowProc( hwnd, msg, wp, lp );
}

int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdLine, int nCmdShow )
{
	WNDCLASS	wc;
	MSG			msg;

	ZeroMemory( &wc, sizeof(wc) );
	wc.lpfnWndProc		= WindowProc;
	wc.hInstance		= hInstance;
	wc.hCursor			= LoadCursor( NULL, IDC_ARROW );
	wc.hbrBackground	= (HBRUSH)( COLOR_WINDOW+1 );
	wc.lpszClassName	= "FarColor";
	
	RegisterClass( &wc );

	CreateWindow
	(
			"FarColor",
			"FarColor",
			WS_OVERLAPPEDWINDOW |WS_VISIBLE,
			CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
			NULL,
			NULL,
			hInstance,
			NULL
	);

	loop:switch( (int)GetMessage( &msg, NULL, 0, 0 ) )
	{
		case -1: break;		// エラー終了
		case  0: break;		// WM_QUIT正常終了
		default:
		// メッセージ処理
		TranslateMessage( &msg );
		DispatchMessage( &msg );
		goto loop;
	}
	return msg.wParam;
}
ですが、この色の並びは「識別しやすい」色の順番になっているのでしょうか?
1行目(20個)くらいなら大丈夫そうですが、2行目以降まで使うと、似たような色になってしまい、
判別できないのではないか?と思います。色相環からそんなにたくさん選ぶのは無理があるのでしょうか・・。

明度や彩度も変えたらいいのかな?とも思いますが、アルゴリズム的なものがわかりません。
「識別しやすい色」を選ぶアルゴリズムはあるのでしょうか?
参考サイトなどありましたらコメント頂けるとうれしいです。

よろしくお願いします。

Re: 識別しやすい色の選び方

Posted: 2011年1月14日(金) 16:32
by softya(ソフト屋)
色相学とか、そういう話ではないですが経験として聞いてください。
1つの折れ線グラフに色があるとして線の細さ次第ですが、せいぜい6~8色が限界だと思います。
人間の目は結構相対的に色を識別するので、相当色相が離れてないと違う色として認識出来ないってのが理由なんじゃないかと思ってます。
あと脳の補完という厄介な問題もあるので、無い色を見てしまう可能性もありますね。
http://www.yukawanet.com/archives/2608579.html

折れ線グラフにする場合は、破線やら違う形状にしないと厳しいと思います。

Re: 識別しやすい色の選び方

Posted: 2011年1月14日(金) 18:42
by たろ
softya(ソフト屋)さん、ありがとうございます。

なるほど6~8色が限界な感触・・そうすると、明度・彩度をうまく使えたとしても・・20色くらいが限度なのかな。
紹介いただいた、緑が青に見えてしまう絵はすごいですね、不思議すぎます。どう見ても青にしか見えません。
たしかに発生しそうな話です。

実は、50種類以上の系列を1枚のグラフに詰め込みたいと思っていたのですが、色だけで識別するのは無謀な気がしてきました。
単色塗りつぶしでなく、模様をつけるとか、グラフを何枚かに分けるとか、別の方法を考えようかなと思います。

ありがとうございました。

Re: 識別しやすい色の選び方

Posted: 2011年1月15日(土) 19:53
by たろ
解決といたします。ありがとうございました。