1. 程式人生 > >常見圖片格式詳解

常見圖片格式詳解

size 行為 spn pen huffman androi inux 大堆 aging

標明原作者信息 http://www.cnblogs.com/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格式

  1. jpeg是有損壓縮格式, 將像素信息用jpeg保存成文件再讀取出來,其中某些像素值會有少許變化。在保存時有個質量參數可在[0,100]之間選擇,參數越大圖片就越保真,但圖片的體積也就越大。一般情況下選擇70或80就足夠了。
  2. jpeg沒有透明信息。
  3. 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#實現的,代碼量非常少,在此貼出全部源碼

技術分享 技術分享
  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     }
技術分享

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格式

  1. png是一種無損壓縮格式, 壓縮大概是用行程編碼算法。
  2. png可以有透明效果。
  3. 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太過龐大,如果不想使用其全部代碼,可以自己從中“偷取”特定圖片格式的讀取、保存代碼。

版權聲明:本文來自xiangism的博客。 僅供參考、學習使用。若需轉載或引用本文中的方法,請標明原作者信息 http://www.cnblogs.com/xiangism 。商業用途請聯系博主

常見圖片格式詳解