256色カラーテーブルへの減色

フルカラー24ビットDIBを8ビットDIB(256色カラーテーブル)に減色してみます。ただし、今回は減色しながらカラーテーブルを作成するのではなく予め作成されたカラーテーブルの中から近い色を探し出す処理を試してみました。

カラーテーブルの作成

 今回使うカラーテーブルは、RGB各6段階の216色に32段階のグレースケールを加えたものです。割とまんべんなく色を含んでいるので、このような感じのパレットは256色環境でよく使われるみたいですね。このカラーテーブルを作成するには、以下のようにして256個の要素からなるRGBQUAD型の配列を作成します。

  for (i=0;i<6;i++) /* カラーテーブル作成 */
      for (j=0;j<6;j++)
          for (k=0;k<6;k++) {

              rgbPal[i*36+j*6+k].rgbRed=k*51;
              rgbPal[i*36+j*6+k].rgbGreen=j*51;
              rgbPal[i*36+j*6+k].rgbBlue=i*51;

          }

  for (i=1;i<32;i++) {

      rgbPal[i+223].rgbRed=i*8;
      rgbPal[i+223].rgbGreen=i*8;
      rgbPal[i+223].rgbBlue=i*8;

  }

  rgbPal[255].rgbRed=255;
  rgbPal[255].rgbGreen=255;
  rgbPal[255].rgbBlue=255;
 これで0〜215にRGB6段階216色、224〜255に32段階のグレースケールを持つカラーテーブルが出来ました。今回はウインドウの上の方にこのカラーテーブルを色見本として表示しておくので、続いてこのカラーテーブルを表示するための8ビットDIBを作成しましょう。

  /* 色見本用のメモリ確保・ポインタ設定 */
  lpPDIB=GlobalAlloc(GPTR,sizeof(BITMAPINFO)+255*sizeof(RGBQUAD)+512*32);
  lpPInfo=(LPBITMAPINFO)lpPDIB;
  lpPPixel=(LPBYTE)lpPDIB+sizeof(BITMAPINFO)+255*sizeof(RGBQUAD);

  /* BITMAPINFO設定 */
  lpPInfo->bmiHeader.biSize=sizeof(BITMAPINFOHEADER);
  lpPInfo->bmiHeader.biWidth=512;
  lpPInfo->bmiHeader.biHeight=32;
  lpPInfo->bmiHeader.biPlanes=1;
  lpPInfo->bmiHeader.biBitCount=8;
  lpPInfo->bmiHeader.biCompression=BI_RGB;

  /* カラーテーブルをDIBのカラーテーブルにコピー */
  CopyMemory((LPBYTE)lpPInfo+sizeof(BITMAPINFOHEADER),rgbPal,256*4);

 ヘッダとカラーテーブルを設定したら、ピクセル列を描きます。色見本は一つの色を2×32ピクセル、全体を512×32ピクセルとし、周りを黒い線で囲んでみました。

  for (i=0;i<32;i++) /* 色を描く */
      for (j=0;j<256;j++)
          FillMemory(lpPPixel+j*2+i*512,2,j);

  /* 左右の縦線 */
  for (i=0;i<32;i++) {

      lpPPixel[i*512]=0;
      lpPPixel[i*512+511]=0;

  }

  /* 上下の横線 */
  FillMemory(lpPPixel,512,0);
  FillMemory(lpPPixel+31*512,512,0);

8ビットDIB作成と近似色検索

 では、いよいよ減色です。まず、減色対象となるフルカラーDIB(lpInfolpPixel)と同じ大きさの8ビットDIBを作成し、ここに結果を書き出す事にしましょう。ここでは、dwWidthが元画像の横幅、dwHeightが元画像の高さ、dwLengthがピクセル列バッファの横幅になっています。

  if (dwWidth % 4==0) /* バッファの1ラインの長さを計算 */
      dwWLength=dwWidth;
  else
      dwWLength=dwWidth+(4-(dwWidth) % 4);

  /* DIB用メモリ確保・ポインタ設定 */
  lpWDIB=GlobalAlloc(GPTR,sizeof(BITMAPINFO)+255*sizeof(RGBQUAD)+dwWidth*dwWLength);
  lpWInfo=(LPBITMAPINFO)lpWDIB;
  lpWPixels=(LPBYTE)lpWDIB+sizeof(BITMAPINFO)+255*sizeof(RGBQUAD);

  lpWInfo->bmiHeader.biSize=sizeof(BITMAPINFOHEADER);
  lpWInfo->bmiHeader.biWidth=dwWidth;
  lpWInfo->bmiHeader.biHeight=dwHeight;
  lpWInfo->bmiHeader.biPlanes=1;
  lpWInfo->bmiHeader.biBitCount=8;
  lpWInfo->bmiHeader.biCompression=BI_RGB;

  /* カラーテーブルをコピー */
  CopyMemory((LPBYTE)lpWInfo+sizeof(BITMAPINFOHEADER),rgbPal,256*4);

 これで、先ほど作成したカラーテーブルを持つ元画像と同じ大きさの8ビットDIBが出来ました。後は、このDIBの各ピクセルにカラーテーブルから元画像に近い色を探して書き込んで行くだけです。

 今回は、RGBそれぞれを軸とする3次元の「色空間」を考え、この空間内で元画像のピクセルの色との「距離」が最小になるカラーテーブルの色を探すようにしました。具体的には、元画像の各ピクセルとカラーテーブルのRGB成分の差を計算し、その差を二乗して加えます。これが各ピクセルとカラーテーブル内の色の色空間内での「距離」(の二乗)になるので、この値が最も小さくなるカラーテーブルの色を探すわけです。
 例えば、あるピクセルの色が(r1,g1,b1)、カラーテーブル内のある色が(r2,g2,b2)なら、この2つの色の距離の二乗は以下の式で求まります。

  (r1-r2)2+(g1-g2)2+(b1-b2)2

 実際に色を探す時には、この式で各ピクセル毎にカラーテーブル内の全色との距離を求め、その距離が最小になる色を書き出して行きます。

  for (i=0;i<dwHeight;i++)
      for (j=0;j<dwWidth;j++) {

          b=lpPixel[j*3+i*dwLength];
          g=lpPixel[j*3+i*dwLength+1];
          r=lpPixel[j*3+i*dwLength+2];

          no=0;

          for (k=0;k<256;k++) { /* 最も近い色を検索 */

              pr=rgbPal[k].rgbRed;
              pg=rgbPal[k].rgbGreen;
              pb=rgbPal[k].rgbBlue;

              /* ピクセルとカラーテーブル要素の距離計算 */
              dwDis=(r-pr)*(r-pr)+(g-pg)*(g-pg)+(b-pb)*(b-pb);

       /* 色空間内の距離が最小の色を保存 */
              if (k==0)
                  dwMin=dwDis;
              else
                  if (dwMin>dwDis) { /* これまでで距離最小の色 */

                      dwMin=dwDis; /* 最小値保存 */
                      no=k; /* カラーテーブル番号保存 */

                   }

          }

          lpWPixels[j+i*dwWLength]=no; /* ピクセルの色決定 */

      }

 この処理では、素直に元画像の各ピクセル毎にカラーテーブル内のすべての色との距離を求め距離が最小になる色を新しいビットマップの対応ピクセルの色にしています。

プログラム

 まず、24ビットフルカラービットマップファイルをウインドウにドロップして読み込んでください。次に、「減色」ボタンをクリックすると減色を行い結果を表示します。

 結果を見ると、鮮やかで色の変化が少ないセル画調のビットマップはかなり良い感じですね。逆に、グラデーションや彩度の低い画像はかなり苦しい。特に、彩度の低い画像ではグレースケールになってしまう部分が多いので、彩度が低い画像の減色ではカラーテーブルにグレースケールを入れない方が良いのかもしれません。

プログラムソース表示

 なお、今回の減色処理はかなり時間がかかるのであまり大きなビットマップを処理しない方が良いでしょう。


戻る