人間の目は「明るさ」の変化には敏感でも「色」の変化には比較的鈍感である、と言われています。画像の圧縮でも、色の情報をある程度省略する事で圧縮率を上げる事が多いようです。今回は、この色情報の省略による画像圧縮を試してみる事にしましょう。
YCrCb色差のブロック化
色の情報を省略するためには、まず画像を色の情報と明るさの情報に分けて扱えるようにする必要があります。今回は、RGBのピクセル列をYCrCbに変換してそのCrCb成分を省略してみましょう。RGB⇔YCrCb間には、
| Y | = 0.299R+0.587G+0.114B |
|---|---|
| Cr | = 0.500R-0.419G-0.081B |
| Cb | = -0.169R-0.332G+0.500B |
| R | = Y+1.402Cr |
| G | = Y-0.714Cr-0.344Cb |
| B | = Y+1.772Cb |
という関係があり、Yが明るさの、CrCbが色の情報になっています。このうち、Yについてはすべてのピクセルを記録しますが、CrCbについてはいくつかのピクセルで平均化してしまいます。つまり、色差情報を個々のピクセル毎に記録するのではなく、いくつかのピクセルで「共有」するわけです。すべてのピクセルのYCrCbを記録すると1ピクセル3バイト必要ですが、例えば隣り合う2ピクセルでCrCbを共有するとこの2ピクセルで必要なバイト数はYに2バイトCrCbで2バイトの計4バイト。2バイト分、約33パーセント情報を圧縮できるわけですね。
もちろん、このような圧縮を行えば画像の情報が欠落するわけですから画質が落ちます。色差を平均化すると、「色の変化」が激しい部分では色がおかしくなりますし、色差を平均する範囲を広げると色がにじんだ感じになってしまうでしょう。しかし、写真などでは比較的色の変化は穏やかなのでかなり効果的な圧縮になります。
RGB-YCrCbの変換処理
では、具体的なプログラムを作ってみます。まず、CrCbを平均化し共有する範囲を指定しないといけませんね。これは、変数dwWV, dwHVで指定するようにしました。dwWVが範囲の横幅でdwHVが高さです。つまり、dwWV×dwHVピクセルを一つのブロックとしてこのブロックでCrCbを平均化、共有する事になります。画像の幅をdwWidth高さをdwHeightとすると、画像全体のブロック数はdwWidth/dwWV×dwHeight/dwHVとなり、ブロックに含まれるピクセルの色差が圧縮される事になるわけです。
RGBとYCrCbの変換処理は、RGBとYCrCbの変換で行ったのと同様の処理を行います。ただし、今回はCrCbについては指定された範囲で平均化して記録し、YCrCbの情報を読み込んでRGBのピクセル列を作成する時も、指定された範囲でCrCbを共有する点に注意してください。
24ビットフルカラーDIBのRGBピクセル列から色差を圧縮したYCrCbへの変換処理では、Yはすべてのピクセル毎にCrCbはdwWV×dwHVのブロック毎に記録するので、
for (i=0;i<dwHeight/dwHV;i++)
for (j=0;j<dwWidth/dwWV;j++) {
for (k=0;k<dwHV;k++)
for (l=0;l<dwWV;l++) {
(j*dwWV+l,i*dwHV+k)のYを記録
(j*dwWV+l,i*dwHV+k)のCrCbを加算
}
CrCbをdwWV*dwHVで割って平均化
ブロック(j,i)のCrCbを記録
}
という流れにしてみましょう。YCrCbの情報を記録するファイルは先頭の16バイトに画像の幅・高さとブロックの幅・高さを各4バイトで記録して、その後にY、そしてYの後に各ブロックのCrCbを並べてみました。つまり、Yは先頭から17バイト目からdwWidth×dwHeightバイト分、CrCbは17+dwWidth*dwHeightバイト目からdwWV*dwHV*2バイト分記録されます。ですので、(x, y)のYCrCbは、
Y=16+x+y*dwWidth Cr=16+dwWidth*dwHeight+(x/dwWV)*2+y*(dwWidth/dwWV)*2 Cb=16+dwWidth*dwHeight+(x/dwWV)*2+y*(dwWidth/dwWV)*2+1
の位置にある事になります。このうち、(dwWidth/dwWV)*2はブロックを記録している領域1列分の幅ですね。
今回のプログラムでは、24ビットDIBのピクセル列lpPixelからYCrCbを記録したバッファlpWorkを作成し、それをファイルtest.yuvに書き込んでいます。この処理では、まず
dwSize=dwWidth*dwHeight+((dwWidth/dwWV)*(dwHeight/dwHV))*2+16; lpWork=GlobalAlloc(GPTR,dwSize); /* ビットマップサイズと色差圧縮レベルを記録 */ *((LPDWORD)lpWork)=dwWidth; *((LPDWORD)lpWork+1)=dwHeight; *((LPDWORD)lpWork+2)=dwWV; *((LPDWORD)lpWork+3)=dwHV;
としてバッファを確保し先頭に画像とブロックの大きさを記録してから、以下のようにしてバッファにYCrCbを記録するようにしました。後は、このバッファをファイルに書き出せばピクセル列lpPixelをYCrCbに変換したtest.yuvの完成です。
for (i=0;i<dwHeight/dwHV;i++)
for (j=0;j<dwWidth/dwWV;j++) {
u=0;
v=0;
for (k=0;k<dwHV;k++)
for (l=0;l<dwWV;l++) {
b=lpPixel[(j*dwWV+l)*3+(i*dwHV+k)*dwLength];
g=lpPixel[(j*dwWV+l)*3+(i*dwHV+k)*dwLength+1];
r=lpPixel[(j*dwWV+l)*3+(i*dwHV+k)*dwLength+2];
y=0.299*r+0.587*g+0.114*b;
if (y<0)
y=0;
if (y>255)
y=255;
/* 各ピクセルのyを記録 */
lpWork[(j*dwWV+l)+(i*dwHV+k)*dwWidth+16]=(BYTE)y;
u+=0.5*r-0.419*g-0.081*b+128;
v+=-0.169*r-0.332*g+0.5*b+128;
}
/* CrCbを平均化 */
u=u/(dwWV*dwHV);
v=v/(dwWV*dwHV);
if (u<0)
u=0;
if (u>255)
u=255;
if (v<0)
v=0;
if (v>255)
v=255;
/* CrCbを記録 */
lpWork[dwWidth*dwHeight+j*2+i*(dwWidth/dwWV)*2+16]=(BYTE)u;
lpWork[dwWidth*dwHeight+j*2+i*(dwWidth/dwWV)*2+17]=(BYTE)v;
}
次に、こうして出来たtest.yuvを読み込む処理です。まず、バッファlpWorkにファイルを読み込んだら、先頭から画像とブロックの大きさを取得しDIBバッファの横幅dwLengthを計算します。
dwWidth=*((LPDWORD)lpWork); dwHeight=*((LPDWORD)lpWork+1); dwWV=*((LPDWORD)lpWork+2); dwHV=*((LPDWORD)lpWork+3); dwLength=dwWidth*3;
続いてDIBの作成。
lpDIB=GlobalAlloc(GPTR,sizeof(BITMAPINFO)+dwLength*dwHeight); /* メモリイメージ用ポインタ設定 */ lpbiInfo=(LPBITMAPINFO)lpDIB; lpPixel=(LPBYTE)lpDIB+sizeof(BITMAPINFO); /* ビットマップのヘッダ作成 */ lpbiInfo->bmiHeader.biSize=sizeof(BITMAPINFOHEADER); lpbiInfo->bmiHeader.biWidth=dwWidth; lpbiInfo->bmiHeader.biHeight=dwHeight; lpbiInfo->bmiHeader.biPlanes=1; lpbiInfo->bmiHeader.biBitCount=24;
DIBが出来たら、ファイル内のYCrCbからRGBのピクセル列を作っていきます。この処理は、Yを各ピクセル毎にCrCbをブロック毎に取得してそのYCrCbをRGBに変換するので、以下のようになります。
for (i=0;i<dwHeight/dwHV;i++)
for (j=0;j<dwWidth/dwWV;j++) {
u=lpWork[dwWidth*dwHeight+j*2+i*(dwWidth/dwWV)*2+16]-128;
v=lpWork[dwWidth*dwHeight+j*2+i*(dwWidth/dwWV)*2+17]-128;
for (k=0;k<dwHV;k++)
for (l=0;l<dwWV;l++) {
y=lpWork[(j*dwWV+l)+(i*dwHV+k)*dwWidth+16];
r=y+1.402*v;
g=y-0.714*u-0.344*v;
b=y+1.772*v;
if (r<0)
r=0;
if (r>255)
r=255;
if (g<0)
g=0;
if (g>255)
g=255;
if (b<0)
b=0;
if (b>255)
b=255;
lpPixel[(j*dwWV+l)*3+(i*dwHV+k)*dwLength]=(BYTE)b;
lpPixel[(j*dwWV+l)*3+(i*dwHV+k)*dwLength+1]=(BYTE)g;
lpPixel[(j*dwWV+l)*3+(i*dwHV+k)*dwLength+2]=(BYTE)r;
}
}
プログラム
実行したら、ウインドウにフルカラー24ビットで幅・高さが4の倍数ピクセルのBMPファイルをドロップしてください。これでtest.yuvが作成されます。次に、このtest.yuvをドロップするとファイル内のYCrCbからRGBピクセル列を作成し表示するので、元画像と比べてみましょう。
色差の圧縮率は、dwWV,dwHVで指定します。最初はそれぞれ4ピクセルになっているので、ピクセルの圧縮率は16*3/(16+2)で3倍近い圧縮率になります。YCrCbの内、16バイトが明るさで2バイトが色の情報になっているわけですね。ブロックを大きくしても色の情報量2バイトは変わらないので、ブロックを大きくしていくと圧縮率は3倍に向かって向上していく事になります。dwWV,dwHVを変えながら、BMPとtest.yuvのファイルサイズを比べてみましょう。かなり圧縮しても、写真などならほぼ同様に見えますね。なお、ビットマップの幅はdwWVの高さはdwHVの倍数(かつ幅・高さとも4の倍数)になっている必要があるので注意してください。