作了幾年有關圖形、圖像的工做,對圖片格式算是小有經驗,在此寫成一文章總結下。雖然一開始並不想講很理論的東西,但寫完後發現幾乎全是理論,細想一下關於圖片格式的知識自己就是理論的東西,囧~~ 那就力求用最簡單的方式將這些「理論」講清楚吧。linux
常見的圖片格式有bmp, jpg(jpeg), png, gif, webp等。android
要講圖片格式還先得從圖像的基本數據結構提及。在計算機中, 圖像是由一個個像素點組成,像素點就是顏色點,而顏色最簡單的方式就是用RGB或RGBA表示, 如圖所示ios
(圖1)c++
(圖2)git
若是有A通道就代表這個圖像能夠有透明效果。web
R,G,B每一個份量通常是用一個字節(8位)來表示,因此圖(1)中每一個像素大小就是3*8=24位圖, 而圖(2)中每一個像素大小是4*8=32位。算法
這裏有三點須要說明:c#
圖像是二維數據,數據在內存中只能一維存儲,二維轉一維有不一樣的對應方式。比較常見的只有兩種方式: 按像素「行排列」從上往下或者從下往上。
windows
如圖所示的圖像有9個像素點,若是從上往下排列成一維數據是(123456789), 若是是從下往上排列則爲(789456123)。
只因此會有這種區別是由於,前一種是以計算機圖形學的屏幕座標系爲參考(右上爲原點,y軸向下 ),而另後一種是以標準的數學座標系爲參考(右下爲原點,y軸向上)。這兩個座標系只是y值不同,互相轉換的公式爲:數據結構
y2 = height-1-y1
y1,y2分別爲像素在兩個座標系中的y座標,height爲圖像的高度。
不過好像只有bmp圖片格式以及windows下的GDI,GDI+是從下往上排列,其它好比DirectX,OpenGL,Cocoa(NSImage, UIImage),OpenCV等都是從上往下排列。
不一樣圖形庫中每一個像素點中RGBA的排序順序可能不同。上面說過像素通常會有RGB,或RGBA四個份量,那麼在內存中RGB的排列就有6種狀況,以下:
RGBA的排列有24種狀況,這裏就不所有列出來了。
不過通常只會有RGB,BGR, RGBA, RGBA, BGRA這幾種排列據。 絕大多數圖形庫或環境是BGR/BGRA排列,cocoa中的NSImage或UIImage是RGBA排列。
若是是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格式沒有壓縮像素格式,存儲在文件中時先有文件頭、再圖像頭、後面就都是像素數據了,上下顛倒存儲。
用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中的這兩個結構「偷用」出來了。代碼以下:
1 //bmp文件頭 2 #pragma pack(push) 3 #pragma pack(2) 4 typedef struct tagBITMAPFILEHEADER { 5 unsigned short bfType; // 19778,必須是BM字符串,對應的十六進制爲0x4d42,十進制爲19778 6 unsigned int bfSize; // 文件大小 7 unsigned short bfReserved1; // 0 8 unsigned short bfReserved2; // 0 9 unsigned int bfOffBits; // 從文件頭到像素數據的偏移,也就是這兩個結構體的大小之和 10 } BITMAPFILEHEADER; 11 #pragma pack(pop) 12 13 //bmp圖像頭 14 typedef struct tagBITMAPINFOHEADER { 15 unsigned int biSize; // 此結構體的大小 16 int biWidth; // 圖像的寬 17 int biHeight; // 圖像的高 18 unsigned short biPlanes; // 1 19 unsigned short biBitCount; // 24 20 unsigned int biCompression; // 0 21 unsigned int biSizeImage; // 像素數據所佔大小, 這個值應該等於上面文件頭結構中bfSize-bfOffBits 22 int biXPelsPerMeter; // 0 23 int biYPelsPerMeter; // 0 24 unsigned int biClrUsed; // 0 25 unsigned int biClrImportant;// 0 26 } BITMAPINFOHEADER;
因爲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:
接下來要介紹一個有關jpeg很是實用的技術——
jpeg格式支持不徹底讀取整張圖片,便可以選擇讀取原圖、1/二、1/四、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
``` 在上面LBitmap的基本上加入下面5個函數: // 不讀取像素數據,只讀取jpeg文件的大小, 用指針*width, *height作爲傳出參數, 返回值bool返回文件是否爲jpeg格式 static bool ReadJpegSize(const wchar_t *path, int *width, int *height); // 判斷文件是否爲jpeg格式 static bool IsJpegFile(const wchar_t *fileame); // 讀取jpeg,fraction可取值爲1, 2, 4, 8 bool ReadJpeg(const wchar_t *filename, int fraction = 1); // 按照width, height的大小讀取合適的jpeg大小, 所得的圖像大小不會超過width*height bool ReadFitJpeg(const wchar_t *filename, int width, int height); // 保存jpeg,quality範圍是[0, 100] bool WriteJpeg(const wchar_t *filename, int quality = 80); ```
具體的實如今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,就幾乎看不出有什麼差異了。
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中發現如何預讀jpeg的官方API。Apple對圖形、圖像、多媒體領域提供了豐富接口,若是這個功能真沒實現就太令我驚訝了! 不過ObjectC徹底兼容C,能夠調用libjpeg庫來實現這個功能。
下面是用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的質量都轉化成80, 圖像的寬高不變, 這時人眼幾乎看不出有什麼差異, 但其體積只有原來的1/3. 若是有大量的照片須要保存時, 節約的空間就很客觀了。實現原理很簡單, 就是讀取jpeg文件, 而後再保存.
用c#實現的,代碼量很是少,在此貼出所有源碼
1 class Program 2 { 3 static string src_path; 4 static long small_size = 0; 5 6 private static ImageCodecInfo GetCodecInfo(string mimeType) 7 { 8 ImageCodecInfo[] CodecInfo = ImageCodecInfo.GetImageEncoders(); 9 10 foreach (ImageCodecInfo ici in CodecInfo) { 11 if (ici.MimeType == mimeType) 12 return ici; 13 } 14 return null; 15 } 16 17 static void SaveImage(string path) 18 { 19 FileStream stream = new FileStream(path, FileMode.Open, FileAccess.Read); 20 21 if (stream.Length == 0) { 22 stream.Close(); 23 return; 24 } 25 26 byte[] file_data = new byte[stream.Length]; 27 stream.Read(file_data, 0, (int)stream.Length); 28 Stream mem = new MemoryStream(file_data); 29 30 long old_size = stream.Length; 31 32 try { 33 Image img = new Bitmap(mem); 34 stream.Close(); 35 36 EncoderParameter p = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, 80L); 37 EncoderParameters ps = new EncoderParameters(1); 38 39 ps.Param[0] = p; 40 41 img.Save(path, GetCodecInfo("image/jpeg"), ps); 42 43 FileStream f = new FileStream(path, FileMode.Open, FileAccess.Read); 44 long new_size = f.Length; 45 f.Close(); 46 47 small_size += old_size - new_size; 48 49 } catch (System.Exception ex) { 50 51 } finally { 52 stream.Close(); 53 } 54 55 } 56 57 static void ConvertOneImage(string path, bool is_save) 58 { 59 if (is_save) { 60 string new_name = src_path + path.Substring(path.LastIndexOf('\\')); 61 File.Copy(path, new_name, true); 62 } 63 64 SaveImage(path); 65 } 66 67 static void ShowSize(string path) 68 { 69 FileStream stream = new FileStream(path, FileMode.Open); 70 Image img = Image.FromStream(stream, false, false); 71 stream.Close(); 72 //Console.WriteLine("{0} {1}", img.Width, img.Height); 73 if (img.Width == 0) { 74 Console.WriteLine("Error"); 75 } 76 } 77 78 static void BatchJpeg() 79 { 80 string path = Application.ExecutablePath; 81 path = path.Substring(0, path.LastIndexOf('\\')); 82 83 src_path = path + "\\" + "src"; 84 //Console.WriteLine(src_path); 85 86 Console.WriteLine("批量轉化jpeg圖片,保證其圖片質量的前提下減小其存儲大小"); 87 Console.WriteLine("若想保存原圖片,其按y(原圖將放在src文件夾下), 不然按任意鍵開始處理"); 88 89 ConsoleKeyInfo key = Console.ReadKey(); 90 bool is_save = false; 91 92 if (key.KeyChar == 'y' || key.KeyChar == 'Y') { 93 is_save = true; 94 95 Directory.CreateDirectory("src"); 96 } 97 98 string[] files = Directory.GetFiles(path, "*.jpg"); 99 100 Stopwatch sw = new Stopwatch(); 101 sw.Start(); 102 103 for (int i = 0; i < files.Length; ++i) { 104 ConvertOneImage(files[i], is_save); 105 //string s = files[i].Substring(files[i].LastIndexOf('\\') + 1); 106 //Console.WriteLine((i + 1).ToString() + "/" + files.Length.ToString() + " \t " + s); 107 108 //ShowSize(files[i]); 109 } 110 111 sw.Stop(); 112 113 Console.WriteLine("*********已結束,按任意鍵結束********"); 114 double v = (small_size * 1.0 / (1024 * 1024)); 115 116 Console.WriteLine("共減小 " + v.ToString("0.00##") + "M 的存儲空間"); 117 Console.WriteLine("耗時:" + sw.Elapsed.TotalSeconds.ToString("0.00##") + "秒"); 118 Console.ReadKey(); 119 } 120 121 static void Main(string[] args) 122 { 123 BatchJpeg(); 124 //SaveImage("E:\\img - 副本.JPG"); 125 //string path = "E:\\cpp_app\\LiteTools\\JpegBatch\\bin\\Release\\img - 副本.JPG"; 126 //File.Delete(path); 127 } 128 }
另外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#致敬。
若是在win32環境下對jpeg IO速度有很高的要求,可使用interlJpeg庫,不開源,但提供有*.h,*.lib文件。這個庫能夠大大提升jpg讀取、保存速度。
當時分別用c#和c實現了jpeg批量轉化工具, 在處理大量圖片時發現c#用時竟然只有c的一半。太奇怪了,按理說,c的速度比c#應該快纔對啊, 而實事是c慢了這麼多。 最後發現問題就在libjpeg上,用了intetJpeg後速度就和c#差很少了(猜測.NET內部也是用intelJpeg來處理jpeg)。
再強調一下: 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算法有用到常規的壓縮算法)。
上面提到的bmp,jpeg,png圖片都只有一幀,而gif能夠保存多幀圖像,如圖所示
libgif庫能夠用來讀取gif圖片。gif中有個參數能夠控制圖片變化的快慢。在程序中可使用這個參數,也能夠本身定義一個參數,這就是爲何gif圖片,在不一樣程序中查看時其變化速度不同。
google開發的一種有損、透明圖片格式,至關於jpeg和png的合體,google聲稱其能夠把圖片大小減小40%。
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太過龐大,若是不想使用其所有代碼,能夠本身從中「偷取」特定圖片格式的讀取、保存代碼。