Java實現24位真彩轉換為8位灰度圖片
Windows下的點陣圖檔案即我們通常所熟悉的BMP圖片,其儲存結構的格式可以在WINGDI.h檔案中找到定義。BMP檔案大體上分為四個部分:
1. 點陣圖檔案頭(BITMAPFILEHEADER)
2. 點陣圖資訊頭(BITMAPINFOHEADER)
3. 調色盤(PALETTE)
4. 點陣圖資料(IMAGEDATA)
根據顏色深度的不同,影象上的一個畫素可以用一個或者多個位元組表示,它由n/8所確定(n是位深度,1位元組包含8個數據位)。這裡需要注意的是,對於調色盤(也叫顏色查詢表LUT(LookUpTable),索引表),不是每一種型別的點陣圖都有的。對於24位的真彩色RGB點陣圖,就是沒有調色盤的,原因:如果用調色盤,總共有2的24次種顏色,所以表示每種顏色的索引也需要24位,和直接用3B來表示RGB資料一樣,還得加上一個調色盤的容量,完全是吃飽了撐著。所以調色盤只是對於16,8,4,1位位深的點陣圖來說的。
首先來看下256階點陣圖(位深度為8)的前兩個部分資料。以下為16進位制值:
圖 一
1. BITMAPFILEHEADER(對應圖一的0000H -- 000DH)
在WINGDI.h中該部分定義如下:
typedef struct tagBITMAPFILEHEADER { WORD bfType; DWORD bfSize; WORD bfReserved1; WORD bfReserved2; DWORD bfOffBits; } BITMAPFILEHEADER, FAR *LPBITMAPFILEHEADER, *PBITMAPFILEHEADER;
前兩個位元組(42H,4DH)為ASCII碼,代表BM,這是在Windows下BMP檔案的識別符號,對應結構體中的bfType;
接下來的四個位元組(36H,75H,02H,00H )代表的是檔案大小,這裡高位在後,所以檔案大小應該是27536H = 161078位元組,對應結構體中的bfSize;
然後為四個位元組的保留值,總為0,對應bfReserved1,bfReserved2;
最後四個位元組(36H,04H,00H,00H)是影象資料的地址,即檔案頭+資訊頭+調色盤的長度,這裡 436H = 1078位元組。我們來算下這是怎麼得出的,檔案頭已經分析過了,為2+4+2+2+4 = 14個位元組,檔案資訊頭固定為40個位元組,對於8位點陣圖,其調色盤為256*4 = 1024個位元組。所以1024+40+14 = 1078。
2. BITMAPINFOHEADER(對應圖一的000EH --> 0035H)
其結構體定義為:
typedef struct tagBITMAPINFOHEADER{
DWORD biSize;
LONG biWidth;
LONG biHeight;
WORD biPlanes;
WORD biBitCount;
DWORD biCompression;
DWORD biSizeImage;
LONG biXPelsPerMeter;
LONG biYPelsPerMeter;
DWORD biClrUsed;
DWORD biClrImportant;
} BITMAPINFOHEADER, FAR *LPBITMAPINFOHEADER, *PBITMAPINFOHEADER;
各欄位的含義如下:
地址(Hex) |
值 |
描述 |
對應欄位 |
000EH-0011H |
28H,00H,00H,00H |
bmp點陣圖資訊頭大小,28H = 40D,即點陣圖資訊頭固定為40個位元組; |
biSize |
0012H-0015H |
90H,01H,00H,00H |
影象寬度(畫素),190H = 400D,即該影象寬400個畫素; |
biWidth |
0016H-0019H |
90H,01H,00H,00H |
影象高度(畫素),該影象寬也是400個畫素; |
biHeight |
001AH-001BH |
01H,00H |
必須是一,不用考慮; |
biPlanes |
001CH-001DH |
08H,00H |
影象的位深度,此處為8; |
biBitCount |
001EH-0021H |
全為00H |
壓縮方式,0代表不壓縮; |
biCompression |
0022H-0025H |
00H,71H,02H,00H |
實際的點陣圖資料大小,27100H = 160000,即資料大小為400*400 = 160000位元組; |
biSizeImage |
0026H-0029H |
全為00H |
目標裝置的水平解析度; |
biXPelsPerMeter |
002AHà002DH |
全為00H |
目標裝置的垂直解析度; |
biYPelsPerMeter |
002EH-0031H |
全為00H |
本圖象實際用到的顏色數,如果該值為零,則用到的顏色數為2的biBitCount次方; |
biClrUsed |
0032H-0035H |
00 01 00 00 |
指定本圖象中重要的顏色數,如果該值為零,則認為所有的顏色都是重要的。 |
biClrImportant |
3. 調色盤(從0036H開始)
當然,這裡是對那些需要調色盤的點陣圖檔案而言的。有些點陣圖,如真彩色圖,是不需要調色盤的,BITMAPINFOHEADER後直接是點陣圖資料。調色盤實際上是一個數組,共有biClrUsed個元素(如果該值為零,則有2的biBitCount次方個元素)。陣列中每個元素的型別是一個RGBQUAD結構,佔4個位元組,其定義如下:
typedef struct tagRGBQUAD {
BYTE rgbBlue;
BYTE rgbGreen;
BYTE rgbRed;
BYTE rgbReserved;
} RGBQUAD;
調色盤的一個元素為4個位元組,前三個分別為顏色值,後一個為保留值,總是0。
4. 點陣圖資料
對於用到調色盤的點陣圖,圖象資料就是該畫素顏在調色盤中的索引值,對於真彩色圖,圖象資料就是實際的R,G,B值。256色點陣圖,一個位元組表示1個畫素。而對於真彩色圖,三個位元組才能表示1個畫素。
知道bmp點陣圖的格式,就可以實現從24位真彩色到8位灰度圖的轉換了。我用的是java,首先把24位的真彩轉換為24位的灰度值,這個轉換很簡單,用J2SE的API就可以,也可以自己實現,先讀取每個畫素的RGB值,再用公式就可以計算出灰度值,再寫進每個畫素的RGB值中。
不同的RGB空間,灰階的計算公式有所不同,常見的幾種RGB空間的計算灰階的公式如下:
1、 簡化 sRGB IEC61966-2.1 [gamma=2.20]
Gray = (R^2.2 * 0.2126 + G^2.2 * 0.7152 + B^2.2 * 0.0722)^(1/2.2)
2、 Adobe RGB (1998) [gamma=2.20]
Gray = (R^2.2 * 0.2973 + G^2.2 * 0.6274 + B^2.2 * 0.0753)^(1/2.2)
3、 Apple RGB [gamma=1.80]
Gray = (R^1.8 * 0.2446 + G^1.8 * 0.6720 + B^1.8 * 0.0833)^(1/1.8)
4、 ColorMatch RGB [gamma=1.8]
Gray = (R^1.8 * 0.2750 + G^1.8 * 0.6581 + B^1.8 * 0.0670)^(1/1.8)
5、 簡化 KODAK DC Series Digital Camera [gamma=2.2]
Gray = (R^2.2 * 0.2229 + G^2.2 * 0.7175 + B^2.2 * 0.0595)^(1/2.2)
然後把得到的24位灰度(此時叫偽灰度,因為僅僅是把RGB值設為了一樣)圖片,轉換為8位即256色的灰度圖片。我的做法是先構造檔案頭,資訊頭,調色盤,再把源圖片的RGB值只保留一個寫進每個畫素。Java程式碼如下:
private void Bit24_to_256Index( )
{
// 將影象檔案讀出,資料儲存在Byte陣列
try
{
inputImage = ImageIO.read( new File(fileString) );
}
catch (IOException e1)
{
e1.printStackTrace();
}
width = inputImage.getWidth();
height= inputImage.getHeight();
ByteArrayOutputStream bos = new ByteArrayOutputStream( width*height*4 + 54);
try
{
ImageIO.write( inputImage, "BMP", bos);
} catch (IOException e)
{
e.printStackTrace();
}
bSrcfile = bos.toByteArray();
// 新檔案的長度(b)=資料部分+調色盤(1024)+點陣圖資訊頭+點陣圖檔案頭
bDestfile = new byte[ width*height+1078 ];
// 開始構造位元組陣列
bDestfile[0] = bSrcfile[0]; // 00H : 42H
bDestfile[1] = bSrcfile[1]; // 01H : 4DH
// 檔案大小(B)
int fileLength = width * height + 1078 ;
byte[] btLen = int2bytes(fileLength);
switch( btLen.length )
{
case 1:
bDestfile[2] = btLen[0];
break;
case 2:
bDestfile[3] = btLen[0];
bDestfile[2] = btLen[1];
break;
case 3:
bDestfile[4] = btLen[0];
bDestfile[3] = btLen[1];
bDestfile[2] = btLen[2];
break;
case 4:
bDestfile[5] = btLen[0];
bDestfile[4] = btLen[1];
bDestfile[3] = btLen[2];
bDestfile[2] = btLen[3];
}
// 資料的偏移地址固定為1078(436H)
bDestfile[10] = 54; // 36H
bDestfile[11] = 4; // 04H
for( int i = 14; i <= 27; i++ )
{
bDestfile[i] = bSrcfile[i];
}
bDestfile[28] = 8; // 2^8 = 256
// 資料大小欄位
int biSizeImage = width * height; // 對256色圖來講,資料部分的大小為長*高
byte[] btSI = int2bytes(biSizeImage);
switch( btSI.length )
{
case 1:
bDestfile[34] = btSI[0];
break;
case 2:
bDestfile[35] = btSI[0];
bDestfile[34] = btSI[1];
break;
case 3:
bDestfile[36] = btSI[0];
bDestfile[35] = btSI[1];
bDestfile[34] = btSI[2];
break;
case 4:
bDestfile[37] = btSI[0];
bDestfile[36] = btSI[1];
bDestfile[35] = btSI[2];
bDestfile[34] = btSI[3];
}
for( int i = 38; i <= 53; i++ )
{
bDestfile[i] = bSrcfile[i];
}
byte bRGB = 0;
// 寫調色盤 36H(54) --> 435H(1077)
for( int i = 54; i <= 1077; i += 4, bRGB ++ )
{
bDestfile[i] = bRGB;
bDestfile[i+1] = bRGB;
bDestfile[i+2] = bRGB;
bDestfile[i+3] = 0; // rgbReserved, 保留值為零
}
// 轉換影象資料部分
for( int i = 1078, j = 54; i < bDestfile.length; i++, j += 3 )
{
bDestfile[i] = bSrcfile[j];
}
outputImage = new BufferedImage( width, height, BufferedImage.TYPE_BYTE_GRAY);
ByteArrayInputStream in = new ByteArrayInputStream( bDestfile ); //將b作為輸入流;
try {
outputImage = ImageIO.read( in );
} catch (IOException e) {
e.printStackTrace();
}
try
{
ImageIO.write( outputImage, "BMP",new File( fileString ));
}
catch(Exception ex)
{
ex.printStackTrace();
}
}
函式int2bytes實現把int型的資料轉換為byte型的陣列,程式碼:
static byte[] int2bytes(int num)
{
byte[] b=new byte[4];
int mask=0xff;
for(int i=0;i<4;i++)
{
b[i]=(byte)(num>>>(24-i*8));
}
return b;
}
經過這兩步變換,就可以將真彩的轉換為8位灰度了。