プログラムdeタマゴ

nodamushiの著作物は、文章、画像、プログラムにかかわらず全てUnlicenseです

HSVカラーピッカー用の画像の生成

今回の話で作ろうとしているのはこれ↓の真ん中の四角いところ。

(saiのキャプチャ)


これをある原色(H=h,S=1,V=1)の色の上に一枚のグレースケールの画像を重ねて表現しようという、よくある一般的な話です。

てか、誰かこれ文章化しておいてくれても良いんじゃない?
私が単純にググっても見つけられなかっただけの話かもしれないけど。(書いてあるサイトあったら教えてください。リンク張っておきます。)
と言うわけで、文章化しておく。


結論

面倒くさい人(主にいつか読む私)の為に先に結論から述べておく。
作成する画像は以下の様な物。

左下が原点で、上方向正の縦軸V(明度)、右方向正の横軸S(彩度)である。
四角形の一辺の長さは1とする。
このとき、領域内の点(s,v)の透明度a、および輝度cは

  • a = 1 - s*v
  • c = (1-s)*v/a

である。ただし、a=0となる場合、cは何でも良い

この値は0〜1になっているので、必要に応じてn倍して使えばよろしい。



先に示した図だと単に上から下へのグラデーションじゃん、と思うかもしれないけど、不透明度を削除すると輝度変化はこんな感じになってる。

フォトショップで作るのは難しそうだね。プログラムするしかありません。

実際に赤(H=0,S=1,V=1)に重ねた図



理論的な話

下地の色をc0、重ねる色をc1、その不透明度をaとすると、重ねた結果生成される色c2は
c2=(1-a)*c0+a*c1  (1)
となる。なお、断りがない限り、変数の値は0〜1である。
これは一般的に用いられている単純な式である。これの正しさ如何については議論しない。また、重ね合わせの計算がこれに従わない場合も考慮しない。

色空間HSVで(h,s,v)(hは0〜360)の色C1を、(h,1,1)の色C0に不透明度a、輝度cの灰色を重ねることで表現することを考える。このaとcの二つの値を求める。(先にそれができるのかできないのか、という議論が必要になると思われるかもしれないが、最終的にできるという結論になるので無視しよう)


C1(h,s,v)をsRGB色空間への写像により得られる色を(R,G,B)、C0(h,1,1)の対応を(r,g,b)とする。

写像関数(Wikipediaより)




少し見通しを良くするためにHSV空間を拡張して(H,S,V,Hi,f,p,q,t)と表現する。追加されたのは上式に出てくる変数で、H,S,Vによって一意に決まる。
C0は(h,1,1,Hi,f,0,1-f,f)である。
C1は(h,s,v,Hi,f,v(1-s),v(1-fs),v{1-(1-f)s})である。
ここで1-fをkとすると、
C0=(h,1,1,Hi,f,0,1-f,1-k)
C1=(h,s,v,Hi,f,v(1-s),v(1-fs),v(1-ks) )



次に、Wikipediaから引用した式を見て貰うと、r,g,bの値はHiに応じて使われる変数が異なるため6通りも話をしなくてはならない。ということはなく、すぐ気がつくと思うが、全ての場合で必ずVの項とpの項が出てくる。後の一つはqかtかのどちらかだが、今回求めなくてはならないのは不透明度aと輝度cの二つなので、連立式は二つあれば十分。よって、Vとpだけ見れば良い。



(1)式に、c0=C0,c1=(a,c),c2=C1を代入し((a,c)は不透明度a,輝度cの灰色)、Vとpの出てくる色の項2つだけを取り出すと

  • v = (1-a)+ac  (2)
  • v(1-s)=ac   (3)

となるから、(2)に(3)を代入すると
a=1-v*s
が得られる。また、(3)式を両辺aで割れば
c=v(1-s)/a
が得られる。


これで、すでにa,cは得られたのだが、もう一つの項qまたはtに代入しても良いか確かめておく。(1)式に、c0=C0,c1=(a,c),c2=C1を代入し、q項が出てくる色について見ると、(色があると仮定する)
v(1-fs)=(1-a)(1-f)+ac = vs(1-f)+v(1-s)=v(s-fs+1-s)=v(1-fs)
となり、成立。同様にtの場合もkをfと文字を置き換えれば完全に同じなので成立。よって、(a,c)をC0に重ねることでC1を得ることができる事を確かめた。

Javaソース例

importは略。

public class HSVIcon implements Icon{

	private int size;
	private Image sv;
	private double h=0;
	private Color rgb = Color.red;

	public HSVIcon(int size) {
		if(size < 1)throw new IllegalArgumentException("size:"+size);
		this.size = size;
		createGrayImage();
	}

	//ここが今回のお話のメイン。
	private void createGrayImage(){
		int[] map = new int[size*size];
		int l = size-1;
		for(int v=l,y=0;y<size;v--,y++){
			double V = (double)v/l;
			for(int s=0;s<size;s++){
				double S = (double)s/l;
				double A = 1-V*S;
				double C;
				if(A==0){
					C=0;
				}
				else{
					C=(1-S)*V/A;
				}
				int a=(int)(255*A);
				int c=(int)(255*C);
				map[y*size+s] = a<<24 | c<<16 | c <<8 | c;
			}
		}
		MemoryImageSource s = new MemoryImageSource(size, size, map, 0, size);
		sv = Toolkit.getDefaultToolkit().createImage(s);
	}

	public Image getSVImage(){
		return sv;
	}

	//速度は必要ないから適当
	public void setH(double h){
		while(h<=0){
			h+=360;
		}
		h%=360;
		h/=360;
		this.h=h;
		int c = Color.HSBtoRGB((float)h, 1, 1);
		rgb = new Color(c);
	}

	public double getH(){
		return h*360;
	}

	public int getRGB(double s,double v){
		return Color.HSBtoRGB((float)h,(float)s,(float)v);
	}

	@Override
	public void paintIcon(Component c, Graphics g, int x, int y) {
		Color co = g.getColor();
		g.setColor(rgb);
		g.fillRect(x, y, size, size);
		g.drawImage(sv, x, y, null);
		g.setColor(co);
	}

	@Override
	public int getIconWidth() {
		return size;
	}

	@Override
	public int getIconHeight() {
		return size;
	}
}