各種圖片編碼格式詳解
感謝xiangism
常見的圖片格式有bmp, jpg(jpeg), png, gif, webp等。
影象基本資料結構
要講圖片格式還先得從影象的基本資料結構說起。在計算機中, 影象是由一個個畫素點組成,畫素點就是顏色點,而顏色最簡單的方式就是用RGB或RGBA表示, 如圖所示
(圖1)
(圖2)
如果有A通道就表明這個影象可以有透明效果。
R,G,B每個分量一般是用一個位元組(8位)來表示,所以圖(1)中每個畫素大小就是3*8=24點陣圖, 而圖(2)中每個畫素大小是4*8=32位。
這裡有三點需要說明:
一、影象y方向正立或倒立
影象是二維資料,資料在記憶體中只能一維儲存,二維轉一維有不同的對應方式。比較常見的只有兩種方式: 按畫素“行排列”從上往下或者從下往上。
如圖所示的影象有9個畫素點,如果從上往下排列成一維資料是(123456789), 如果是從下往上排列則為(789456123)。
只所以會有這種區別是因為,前一種是以計算機圖形學的螢幕座標系為參考(右上為原點,y軸向下 ),而另後一種是以標準的數學座標系為參考(右下為原點,y軸向上)。這兩個座標系只是y值不一樣,互相轉換的公式為:
y2 = height-1-y1
y1,y2分別為畫素在兩個座標系中的y座標,height為影象的高度。
不過好像只有bmp圖片格式以及windows下的GDI,GDI+是從下往上排列,其它比如DirectX,OpenGL,Cocoa(NSImage, UIImage),OpenCV等都是從上往下排列。
二、RGB排列順序
不同圖形庫中每個畫素點中RGBA的排序順序可能不一樣。上面說過畫素一般會有RGB,或RGBA四個分量,那麼在記憶體中RGB的排列就有6種情況,如下:
- RGB
- RBG
- GRB
- GBR
- BGR
- BRG
RGBA的排列有24種情況,這裡就不全部列出來了。
不過一般只會有RGB,BGR, RGBA, RGBA, BGRA這幾種排列據。 絕大多數圖形庫或環境是BGR/BGRA排列,cocoa中的NSImage或UIImage是RGBA排列。
三、畫素32位對齊
如果是RGB24點陣圖,會存在一個32位對齊的問題——
在x86體系下,cpu一次處理32整數倍的資料會更快,影象處理中經常會按行為單位來處理畫素。24點陣圖,寬度不是4的倍數時,其行位元組數將不是32整數倍。這時可以採取在行尾新增冗餘資料的方式,使其行位元組數為32的倍數。
比如,如果影象寬為5畫素,不做32位對齊的話,其行位數為24*5=120,120不是32的倍數。是32整數倍並且剛好比120大的數是128,也就只需要在其行尾新增1位元組(8位)的冗餘資料即可。(一個以空間換時間的例子)
有個公式可以輕鬆計算出32位對齊後每行應該佔的位元組數
byteNum = ((width * 24 + 31) & ~31)>>3;
注意結果是位元組數,如果想知道位數,還得x8
圖片格式的必要性
如果將影象原始格式直接儲存到檔案中將會非常大,比如一個5000*5000 24點陣圖,所佔檔案大小為5000*5000*3位元組=71.5MB, 其大小非常可觀。
如果用zip或rar之類的通用演算法來壓縮畫素資料,得到的壓縮比例通常不會太高,因為這些壓縮演算法沒有針對影象資料結構進行特殊處理。
於是就有了jpeg,png等格式,同樣是影象壓縮演算法jpeg和png也有不同的適用場景,具體在下文再闡述。
所以可以總結如下: jpeg,png檔案之於影象,就相當於zip,rar格式之於普通檔案(用zip,rar格式對普通檔案進行壓縮)。
BMP格式
bmp格式沒有壓縮畫素格式,儲存在檔案中時先有檔案頭、再影象頭、後面就都是畫素資料了,上下顛倒儲存。
用windows自帶的mspaint工具儲存bmp格式時,可以發現有四種bmp可供選擇:
單色: 一個畫素只佔一位,要麼是0,要麼是1,所以只能儲存黑白資訊
16色點陣圖: 一個畫素4位,有16種顏色可選
256色點陣圖: 一個畫素8位,有256種顏色可選
24位點陣圖: 就是圖(1)所示的點陣圖,顏色可有2^24種可選,對於人眼來說完全足夠了。
這裡為了簡單起見,只詳細討論最常見的24點陣圖的bmp格式。
現在來看其檔案頭和圖片格式頭的結構:
檔案頭資訊 | ||
欄位 | 大小(位元組) | 描述 |
bfType | 2 | 一定為19778,其轉化為十六進位制為0x4d42,對應的字串為BM |
bfSize | 4 | 檔案大小 |
bfReserved1 | 2 | 一般為0 |
bfReserved2 | 2 | 一般為0 |
bfOffBits | 4 | 從檔案開始處到畫素資料的偏移,也就是這兩個結構體大小之和 |
bmp圖片結構頭 | ||
欄位 | 大小(位元組) | 描述 |
biSize | 4 | 此結構體的大小 |
biWidth | 4 | 影象的寬 |
biHeight | 4 | 影象的高 |
biPlanes | 2 | 影象的幀數,一般為1 |
biBitCount | 2 | 一畫素所佔的位數,一般是24 |
biCompression | 4 | 一般為0 |
biSizeImage | 4 | 畫素資料所佔大小,即上面結構體中檔案大小減去偏移(bfSize-bfOffBits) |
biXPelsPerMeter | 4 | 一般為0 |
biXPelsPerMeter | 4 | 一般為0 |
biClrUsed | 4 | 一般為0 |
biClrImportant | 4 | 一般為0 |
本來在windows平臺下wingdi.h檔案中已經有這些結構的定義,不過為了不依賴與windows,實現為跨平臺,本人將wingdi.h中的這兩個結構“偷用”出來了。程式碼如下:
Bmp結構體由於bmp格式比較簡單,本人已實現了一份簡單的c++程式碼,具有讀取、儲存bmp圖片的功能,只支援24位的bmp格式。
程式碼在 http://git.oschina.net/xiangism/blogData 的“常見圖片格式詳解/ImageDemo/BmpDemo”資料夾中。
雖然這裡只建立了vs2008專案,但程式碼在linux, mac平臺下都可以編譯通過。
需要說明的是為了統一處理,將bmp讀取到LBitmap::m_pixel中時就將其轉化為32位從上往下排列的影象格式了。並且會有y座標的轉化。
所以在讀取的時候會有一個temp_line先儲存檔案中的24位資料,再轉化為32位資料。在儲存時也是先將32位資料轉化到temp_line的24位資料上,然後再寫入檔案。(如果僅僅是處理bmp,那麼這麼多的一個A通道是冗餘資料,但後面處理png圖片時就會用到這個A通道)
如果用上面的程式碼來讀取如圖所示的圖片(放大8倍後的顯示圖):
右上角畫素為RGB(255, 128, 0)
1 ln::LBitmap bmp; 2 bmp.ReadBmp(L"one.bmp"); 3 unsigned char *p = bmp.Pixel(0, 0); 4 printf("%d, %d, %d\n", p[0], p[1], p[2]); //顯示左上角的畫素值 5 bmp.WriteBmp(L"out.bmp"); //儲存到檔案,可以測試是否能正確讀取和儲存bmp
執行的結果為: 0,128,255
可以看出畫素分佈為BGR
ps:
- bmp格式也是可以壓縮.
- bmp格式也可以有顏色板。顏色板就是一個顏色的索引,上面說過bmp格式一個畫素可以只有2個,16個或256個取值。就拿單色點陣圖來說明,預設為0對應RGB(0,0,0) 1,對應RGB(255, 255, 255)
如果顏色板這樣定義:
0對應 RGB(255,0, 0)紅
1對應 RGB(0, 255, 0)綠
這樣黑白圖就成了紅綠圖
JPEG格式
- jpeg是有失真壓縮格式, 將畫素資訊用jpeg儲存成檔案再讀取出來,其中某些畫素值會有少許變化。在儲存時有個質量引數可在[0,100]之間選擇,引數越大圖片就越保真,但圖片的體積也就越大。一般情況下選擇70或80就足夠了。
- jpeg沒有透明資訊。
- jpeg比較適合用來儲存相機拍出來的照片,這類影象用jpeg壓縮後的體積比較小。其使用的具體演算法核心是離散餘弦變換、Huffman編碼、算術編碼等技術,有興趣的同學可以在網上找一大堆資料,本文就不詳細介紹了。
接下來要介紹一個有關jpeg非常實用的技術——
jpeg格式支援不完全讀取整張圖片,即可以選擇讀取原圖、1/2、1/4、1/8大小的圖片
比如5000*5000的一張大圖,可以只讀取將其縮小成1/8後即625*625大小的圖片。 這樣比先完全讀取5000*5000的影象,再用演算法縮小成625*625大小不知快多少倍。
如果應用需求只需要一張小圖時,這種讀取方式就可以大顯身手了。
在c程式碼中讀取jpeg一般是使用libjpeg, 這個庫提供了不完全讀取圖片的功能。
給ln::LBitmap新增有關jpeg的介面,如下ReadJpeg()第三個引數fraction可取值為1,2,4,8,分別對應1/1,1/2,1/4,1/8
JpegAPI具體的實現在JpegDemo
用上面的函式進行jpeg的讀取和儲存的測試
``` ln::LBitmap bmp; bmp.ReadBmp(L"one.bmp"); unsigned char *p = bmp.Pixel(0, 0); printf("%d, %d, %d\n", p[0], p[1], p[2]); bmp.WriteJpeg(L"one.jpg", 90); ```
讀取one.bmp圖片,然後儲存成jpeg格式,one.jpg放大後顯示如下
發現左上角的顏色發生了變化,並且也影響到周圍的畫素,就算將上面WriteJpeg()第二個引數換成100,也還是這種效果,這是Jpeg格式無法避免的問題
但如果讀取一張風景照,再儲存成Jpeg,就幾乎看不出有什麼差別了。
android平臺下實現jpeg預讀
BitmapFactory.Options opt = new BitmapFactory.Options(); opt.inJustDecodeBounds = true; BitmapFactory.decodeFile(info.fullPath, opt); //這裡僅僅只讀取jpeg的大小 opt.inJustDecodeBounds = false; if (opt.outWidth > opt.outHeight) { opt.inSampleSize = opt.outWidth / phSize;//hpSize是允許的圖片寬高的最大值 } else { opt.inSampleSize = opt.outHeight / phSize; } Bitmap b = BitmapFactory.decodeFile(info.fullPath, opt);
將BitmapFactory.Options的inJustDecodeBounds 設定為true後,就只會讀取Jpeg的大小,而不會去解析畫素資料。然後再設定inSampleSize後,就可以根據這個值來讀取適當大小的圖片,研究android的原始碼後可以發現底層也是呼叫的libjpeg庫來實現。
ios,mac
本人還沒有在ios/mac中發現如何預讀jpeg的官方API。Apple對圖形、影象、多媒體領域提供了豐富介面,如果這個功能真沒實現就太令我驚訝了! 不過ObjectC完全相容C,可以呼叫libjpeg庫來實現這個功能。
.NET下僅讀取jpeg的大小
下面是用c#僅僅讀取jpeg寬高(沒有解析畫素資料), 直接用C#讀取1/2,1/4,1/8還不知道如何實現
FileStream stream = new FileStream(path, FileMode.Open); Image img = Image.FromStream(stream, false, false); //關鍵是將第三個引數設定為false Console.WriteLine("size: {0},{1}", img.Width, img.Height);
jpeg批量轉化工具
用相機拍出來的原始jpeg圖片是高保真質量, 所佔檔案體積非常大,本人寫了一個批量轉化的工具,可以將jpeg的質量都轉化成80, 影象的寬高不變, 這時人眼幾乎看不出有什麼差別, 但其體積只有原來的1/3. 如果有大量的照片需要儲存時, 節約的空間就很客觀了。實現原理很簡單, 就是讀取jpeg檔案, 然後再儲存.
用c#實現的,程式碼量非常少,在此貼出全部原始碼
Exif資訊
另外jpeg檔案一般有一個附屬的exif資訊,這個資訊中有影象大小,拍攝時間,拍攝的相關引數,照片方向,影象縮圖等資訊。
用相機拍出來的jpeg都會有這個資訊。如果照片方向不是正立的話,在讀取到畫素取後,還得按exif所指明的方向將影象旋轉下。mspaint程式就沒有做這個處理,有些圖片用picasa檢視和用mspaint檢視方向就不一樣。當然為了簡單起見,上面的LBitmap中也自動忽略了exif資訊及其影象拍攝時的方向。
如果不用讀取1/2,1/4,1/8的方法,也可以從exif中來讀取縮圖,但這個縮圖一般很小。
說到exif,不得不說一款用perl實現的命令列工具:exiftool。幾乎所有的多媒體檔案(影象、音樂、視訊)都可以用這個工具來檢視其有關資訊,當然如果不是jpeg檔案就是指廣義上的"exif"。在git中有已經編譯好可執行檔案exiftool.exe。使用方法是將這個檔案放到系統路徑下,然後在想檢視的檔案路徑下執行 exiftool filename
在實現BatchJpeg工具時如果僅僅用上面實現的LBitmap來讀取,儲存, 將會失去exif資訊, 而相片的拍攝時間等資訊又很重要, 所以還得用另一個庫exiv2來讀取寫入exif。如果用c#, 用上面的程式碼exif資訊會自動保留下來。默默地向c#致敬。
intelJpeg庫
如果在win32環境下對jpeg IO速度有很高的要求,可以使用interlJpeg庫,不開源,但提供有*.h,*.lib檔案。這個庫可以大大提高jpg讀取、儲存速度。
當時分別用c#和c實現了jpeg批量轉化工具, 在處理大量圖片時發現c#用時居然只有c的一半。太奇怪了,按理說,c的速度比c#應該快才對啊, 而實事是c慢了這麼多。 最後發現問題就在libjpeg上,用了intetJpeg後速度就和c#差不多了(猜想.NET內部也是用intelJpeg來處理jpeg)。
PNG格式
- png是一種無失真壓縮格式, 壓縮大概是用行程編碼演算法。
- png可以有透明效果。
- png比較適合適量圖,幾何圖。 比如本文中出現的這些圖都是用png儲存,比用joeg儲存體積要小。
再強調一下: jpeg比較適合儲存色彩“雜亂”的拍攝圖片,png比較適合儲存方几何特徵比較強的向量圖。
png可能有24點陣圖和32點陣圖之分。32點陣圖就是帶有alpha通道的圖片。
將圖片a繪製到另一幅圖片b上,如果圖片a沒有alpha通道,那麼就會完全將b圖片的畫素給替換掉。而如果有alpha通道,那麼最後覆蓋的結果值將是c = a*alpha + b*(1-alpha)
再對LBitmap新增png的支援。
新增介面如下:
static bool ReadPngSize(const wchar_t *path, int *width, int *height); static bool IsPngFile(const wchar_t *filename); bool ReadPng(const wchar_t *filename); bool WritePng(const wchar_t *filename);
具體實現在PngDemo中。有呼叫libpng庫,並且libpng庫依賴zlib庫(由此可以看出png演算法有用到常規的壓縮演算法)。
GIF格式
上面提到的bmp,jpeg,png圖片都只有一幀,而gif可以儲存多幀影象,如圖所示
libgif庫可以用來讀取gif圖片。gif中有個引數可以控制圖片變化的快慢。在程式中可以使用這個引數,也可以自己定義一個引數,這就是為什麼gif圖片,在不同程式中檢視時其變化速度不一樣。
webp
google開發的一種有損、透明圖片格式,相當於jpeg和png的合體,google聲稱其可以把圖片大小減少40%。
一個強大的格式庫,CxImage
CxImage幾乎可以讀取任何圖片格式
下面是其標頭檔案中的巨集定義:
#define CXIMAGE_SUPPORT_WINDOWS 1 #define CXIMAGE_SUPPORT_EXIF 1 #define CXIMAGE_SUPPORT_BMP 1 #define CXIMAGE_SUPPORT_GIF 1 #define CXIMAGE_SUPPORT_JPG 1 #define CXIMAGE_SUPPORT_PNG 1 #define CXIMAGE_SUPPORT_ICO 1 #define CXIMAGE_SUPPORT_TIF 1 #define CXIMAGE_SUPPORT_TGA 1 #define CXIMAGE_SUPPORT_PCX 1 #define CXIMAGE_SUPPORT_WBMP 1 #define CXIMAGE_SUPPORT_WMF 1 #define CXIMAGE_SUPPORT_JP2 1 #define CXIMAGE_SUPPORT_JPC 1 #define CXIMAGE_SUPPORT_PGX 1 #define CXIMAGE_SUPPORT_PNM 1 #define CXIMAGE_SUPPORT_RAS 1 #define CXIMAGE_SUPPORT_MNG 1 #define CXIMAGE_SUPPORT_SKA 1 #define CXIMAGE_SUPPORT_RAW 1 #define CXIMAGE_SUPPORT_PSD 1
CxImage在針對特定格式時,也是呼叫了其它圖片庫(比如libjpeg, libpng, libtiff)。由於CxImage太過龐大,如果不想使用其全部程式碼,可以自己從中“偷取”特定圖片格式的讀取、儲存程式碼。