X Window System, Client Programming, No.5


中間色の表現


赤、緑、青の原色のうちどれか1つのの色に注目して考えます。 ある原色の明るさが 0 〜 maxlevel の範囲(すなわち maxlevel+1通り)で 表現されているとします。 これを nlevel 通りの明るさにマップしなければならなくなったとすると、 (maxlevel+1)通りが nlevel 通りに対応づけられるので、もとの明るさを (maxlevel+1)/nlevel の長さに分割して考えればよいことになります。 すなわち、元の明るさが x であったとすると、これは 0〜 (nlevel-1) の範囲のうち x * nlevel / (maxlevel+1) 番目にマップされるわけです。

0 〜 (nlevel - 1) の範囲で i番目のレベルが持つ明るさは i * maxlevel/(nlevel-1) です。

maxlevel=255, nlevel=4の場合を考えましょう。

赤、緑、青の各原色についてそれぞれ4段階の明るさに分けると 4^3=64色必要となります。 各原色が0から255までの値を取るとすると、XColor構造体で 指定する値の計算は以下のようになります。

色の割り当て
#define RSIZE	4
#define GSIZE	4
#define BSIZE	4

Display *dpy;
XColor defs[RSIZE][GSIZE][BSIZE];

void init_cmap() {
    Colormap cmap;
    int i,j,k;
  
    cmap=DefaultColormap(dpy,scr);
    for (i=0; i<RSIZE; ++i) {
	for (j=0; j<GSIZE; ++j) {
	    for (k=0; k<BSIZE; ++k) {
		defs[i][j][k].red=(255 * i / (RSIZE-1)) << 8;
		defs[i][j][k].green=(255 * j / (GSIZE-1))<< 8;
		defs[i][j][k].blue=(255 * k / (BSIZE-1)) << 8;
		defs[i][j][k].flags=DoRed|DoGreen|DoBlue;
		if (!XAllocColor(dpy,cmap,&defs[i][j][k])) {
		    fprintf(stderr,"XAllocColor failed\n");
		    exit(-1);
		}
	    }
	}
    }
}

原色値からピクセル値を計算する方法

(maxlevel+1) 段階で表現された『原色の成分値 x1 』を 別のnlevel段階の値 x2 に変換するには

    x2 = x1 × nlevel/(maxlevel+1)
を計算します。したがって、赤(red)、緑(green)、青(blue)の値が 与えられたときにどのカラーセルを使って描画すべきかを 計算する式は以下のようになります。

中間色の参照
unsigned int r,g,b;
XGCValues xgcv;
    ...
r = RSIZE * red/256;
g = GSIZE * green/256;
b = BSIZE * blue/256;
xgcv.foreground = defs[r][g][b].pixel;

プログラム例

RGBファイルの中で赤緑青の原色値が各8bitで表現されて 並んでいるとします。 すると、320×240のRGB画像を4×4×4=64色で表示する プログラム x12.c は以下のようになります。


x12.c
#include <stdio.h>
#include <X11/Xos.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>

#define WIDTH	320
#define HEIGHT	240

#define RSIZE	4
#define GSIZE	4
#define BSIZE	4

Display *dpy;
int scr;
Window win;
GC gc;
XColor defs[RSIZE][GSIZE][BSIZE];

void init_cmap() {
    Colormap cmap;
    int i,j,k;

    cmap=DefaultColormap(dpy,scr);
    for (i=0; i<RSIZE; ++i) {
	for (j=0; j<GSIZE; ++j) {
	    for (k=0; k<BSIZE; ++k) {
		defs[i][j][k].red=(255 * i / (RSIZE-1)) << 8;
		defs[i][j][k].green=(255 * j / (GSIZE-1))<< 8;
		defs[i][j][k].blue=(255 * k / (BSIZE-1)) << 8;
		defs[i][j][k].flags=DoRed|DoGreen|DoBlue;
		if (!XAllocColor(dpy,cmap,&defs[i][j][k])) {
		    fprintf(stderr,"XAllocColor failed\n");
		    exit(-1);
		}
	    }
	}
    }
}

int init_xwin(int width, int height) {
    XGCValues gcv;
    XSetWindowAttributes xswa;
  
    if ((dpy = XOpenDisplay(NULL)) == NULL) {
	fprintf(stderr, "cant open display\n");
	exit(1);
    }
    scr = DefaultScreen(dpy);
    xswa.background_pixel = BlackPixel(dpy,scr);
    xswa.border_pixel = BlackPixel(dpy,scr);
    xswa.event_mask = ExposureMask | ButtonPressMask;
    win=XCreateWindow(dpy,RootWindow(dpy,scr),0,0,width,height,1,
		      CopyFromParent,CopyFromParent,CopyFromParent,
		      (CWBackPixel|CWBorderPixel|CWEventMask),&xswa);
    init_cmap();
    gcv.foreground = WhitePixel(dpy,scr);
    gcv.background = BlackPixel(dpy,scr);
    gcv.line_width = 1;
    gc = XCreateGC(dpy,win,(GCForeground|GCBackground|GCLineWidth),&gcv);
    XMapWindow(dpy,win);
}

void draw_rgb(unsigned char *data, int w, int h) {
    int x,y;
    unsigned int r,g,b;
    unsigned long red,green,blue;
    XGCValues xgcv;
    unsigned char *s=data;

    for (y=0; y<h; ++y) {
	for (x=0; x<w; ++x) {
	    red = *s++; green = *s++; blue = *s++;
	    r = RSIZE*red/256; /* ここでディザをかける */
	    g = GSIZE*green/256; /* ここでディザをかける */
	    b = BSIZE*blue/256; /* ここでディザをかける */
	    xgcv.foreground = defs[r][g][b].pixel;
	    XChangeGC(dpy,gc,GCForeground,&xgcv);
	    XDrawPoint(dpy,win,gc,x,y);
	}
    }
}

int main(int argc, char **argv) {
    int ac=argc;
    char **av=argv;
    char *fname="sample.rgb";
    int width=WIDTH, height=HEIGHT;
    FILE *fp;
    int i;
    unsigned char *data,*s;

    while (--ac) {
	++av;
	if (!strcmp(*av,"-f")) {
	    if (! --ac) goto Usage;
	    fname = *++av;
	} else if (!strcmp(*av,"-w")) {
	    if (! --ac) goto Usage;
	    width = atoi(*++av);
	} else if (!strcmp(*av,"-h")) {
	    if (! --ac) goto Usage;
	    height = atoi(*++av);
	} else {
	Usage:
	    fprintf(stderr,"usage: %s -w width -h height -f filename\n",argv[0]);
	    exit(-1);
	}
    }
    if ((data=(unsigned char *)malloc(width*height*3)) == NULL) {
	fprintf(stderr,"can not malloc\n");
	exit(-1);
    }
    if ((fp=fopen(fname,"r")) == NULL) {
	fprintf(stderr,"%s: can not open %s\n",argv[0],fname); exit(-1);
    }
    for (i=0,s=data; i<width*height*3; ++i) {
	*s++ = (unsigned char) getc(fp);
    }
    fclose(fp);
    init_xwin(width,height);
    for (;;) {
	XEvent ev;
	XNextEvent(dpy,&ev);
	switch (ev.type) {
	  case Expose:
	    draw_rgb(data,width,height);
	    break;
	  case ButtonPress:
	    exit(0);
	    break;
	  default:
	    break;
	}
    }
}

演習用のRGBファイルの例としては ~nitta/pro2w/tmp.rgb が 用意してあります。 しかし、できるだけ自分で画像を用意してRGB形式に変換して 使ってみることを勧めます。

ppmファイルをrgbファイルに変換するには ppm2rgb を使います。
ppm2rgbの使用方法
PROMPT$<I>~nitta/bin/ppm2rgb  < PPMファイル  > RGBファイル</I> <IMG SRC="/local-icons/enter.gif">
    (例)PROMPT$ <I>~nitta/bin/ppm2rgb  < tmp.ppm  >  tmp.rgb</I> <IMG SRC="/local-icons/enter.gif">

画像ファイルのフォーマット変換

giftopnmgif→pnm
ppmtogifppm→gif
pnmscalepnmファイルの大きさ変更
cjpegjpegファイルへの変換
djpegjpegファイルからの変換
xvいろいろな形式を扱える
変換例
  [GIFファイル→RGBファイル]
	giftopnm  tmp.gif | ~nitta/bin/ppm2rgb > tmp.rgb 
  [JPEG ファイル→RGBファイル]
	djpeg -ppm  tmp.jpg | ~nitta/bin/ppm2rgb > tmp.rgb 

ディザ法

ディザ

「多くの色を用いた画像」を少ない色で表現するには、 元の画像上の点の色を「使える色のうちで最も近い色」に 対応づけるだけでは不十分です。 元の画像にあった細かな色の変化が表現できず、 平坦に塗りつぶされてしまった画像になってしまいます。 そうならないためには、中間色を、少ない色を混ぜ合わせて 表現します。すなわち、対応づけられる色を適当な割合で 変化させる (= 『震えさせる』、ditherは「震える」という意味があります) ことにより、ある点の色が遠目には混ざりあって 元の色に近い色に見えるようにするのです。 これをディザ (dither)法といいます。

マップする色に変化を与える方法としては、

の2種類がよく使われます。



ランダム・デイザ

ランダム・ディザは、確率的に値を震えさせる方法です。

たとえば、0〜255の値を4段階にマップする場合を考えます。 255/(4-1)=85 なので、以下のようなマッピングを考えることができます。

    0    1     2     3     ← 4段階の数値
    0   85   170   255     ← 256段階の明るさを4段階にマップ
元の明るさが 130 である点は、130 * 3 /255 = 1.5294... なので (整数で考えれば)1 にマップされることになります。 しかし、より原画像に近い色で表現するためには マッピングされるべきです。 そこで0〜1.0の間の乱数を発生させて、誤差と乱数を比較し、 誤差の方が乱数よりも大きい場合にマップされる値を +1 することにします。 これを一般論で考えると、0〜CMAXの値を CSIZE 段階にマップする場合には 元の明るさ r の点は r * (CSIZE-1)/CMAX にマッピングされます。 したがって誤差は r * (CSIZE-1) /CMAX - [r * (CSIZE-1) /CMAX] となります。



オーダード・ディザ

前もってディザのパターンを用意しておき、点のXY座標によって 誤差の扱いを変える方法をオーダード・ディザといいます。 ディザのパターンは次のBayerマトリクスの4x4のものが実用上 よく利用されています。

ディザ・パターン (Bayerマトリクス)
    2x2:   0 2
           3 1

    4x4:   0  8  2 10
          12  4 14  6
           3 11  1  9
          15  7 13  5

たとえば、0〜255の値を4段階にマップする場合を考えましょう。 255/(4-1)=85 なので、以下のようなマッピングを考えることができます。

    0    1     2     3     ← 4段階の数値
    0   85   170   255     ← 256段階の明るさを4段階にマップ
元の明るさが 130 である点は、130 * 3 /255 = 1.5294... なので (整数で考えれば)1 にマップされることになります。 ランダムディザのところで示した通り、 0〜CMAXの値を CSIZE 段階にマップする場合には 元の明るさ r の点は r * (CSIZE-1)/CMAX にマッピングされます。 したがって誤差は r * (CSIZE-1) /CMAX - [r * (CSIZE-1) /CMAX] となります。

しかし、より原画像に近い色で表現するためには

マッピングされるべきです。

N×Nのディザパターンが bayer[N][N]で与えられているとすると

    誤差 > bayer[x%N][y%N]/(N*N)
のときに、マップされる値を +1 します。 すなわち点の位置(座標)によって誤差の扱いを変えることで、 ある確率で上下の色に振り分けるているわけです。

たとえば、0〜255の値を4段階で表すと

    0    1     2     3     ← 4段階の数値
    0   85   170   255     ← 256段階の明るさを4段階にマップ
となります。このとき 130 という明るさを持つ点は 1.5294... にマップされ、 誤差は 1.5294 - 1.0 = 0.5294... となります。 4x4の大きさのディザパターンを使う場合は
    0.5429... > dither[x%4][y%4]/16
のときだけ、一つ上の値の2にマップすることにします。 計算速度の点からみると、プログラム的には、両辺に 「ディザの大きさ」を掛けて整数で計算する方がよいでしょう。



演習


課題1

上記のクライアント(x12.c)を動作させましょう。

課題2

中間色を計算のところで Bayer's Matrixによるオーダード・ディザを 使うように変更したプログラムを作って下さい。 ファイル名は x13b.c としましょう。

オプション課題3

x13b.c を変更して、誤差の計算を整数で演算するように工夫した プログラム x13d.c を作って下さい。

提出物

課題が完成したら x13b.c を p2r11@nw.tsuda.ac.jp へ送って下さい。 Subject は report としましょう

オプション課題ができた人は、 Subjectを option 3 として x13d.c をp2r11@nw.tsuda.ac.jpに送って下さい。

上記の Email アドレス宛に送ったメイルは http://nw.tsuda.ac.jp/cgi-bin/cgiwrap/p2r11/mailhead でヘッダ部が参照できるます。 Emailを送ったあとで、正しく提出されたかどうか確認しておいて下さい。

提出期限は来週木曜日の8:50a.m. です。


Yoshihisa Nitta

http://nw.tsuda.ac.jp/