在學習圖象處理的過程中,JPEG是我的第一個攔路虎。一直很想手寫一下JPG的壓縮和解壓的過程,我在網上找到了一些程式碼或者文章,很多都是沒有註釋或者是解釋不夠清楚的。所以特地寫這篇文章記錄自己從無到有寫一個JPEG_Encoder的過程,也能幫助其他學習圖形或者音視訊的童鞋。對於不想看文章的同學,這邊直接上程式碼 https://github.com/Cheemion/JPEG_COMPRESS。 以下是JPEG的壓縮流程。取樣->>離散傅立葉變化->>量化->>哈夫曼壓縮->>寫入jpg檔案. 在進行這些流程之前,必須從BMP檔案中讀取待壓縮的圖片檔案。
圖片引用自"Compressed Image File Formats JPEG, PNG, GIF, XBM, BMP - John Miano"[4]
1.BMP檔案Format
[1]BMP檔案包括如下:
一個檔案頭BITMAPFILEHEADER,裡面包含了檔案的各種資訊。
一個圖片頭BITMAPINFOHEADER,裡面包含了圖片資訊。
一個RGBQUAD array,裡面包含了畫素的對應關係,比如1 代表 RGB(1,1,1)。因為我們用的都是24點陣圖片,所以我們不考慮這一項。
一個Color-index,就是我們的圖片的畫素了,我們只考慮24點陣圖像。
其他需要注意的如下:
BMP檔案的位元組是小端儲存的(BMP format are stored with the least significant bytes first)
BMP圖片的每一行畫素所佔的位元組數必須是4位元組的的整數倍
BMP資料的第一行畫素儲存的是圖片的最後一行的資料(相當於圖片在BMP中是倒置的)
2.BITMAPFILEHEADER(標頭檔案格式)
[2]標頭檔案包含如下的欄位
1 typedef struct tagBITMAPFILEHEADER { 2 WORD bfType; //檔案型別 規定為'BM' 3 DWORD bfSize; //檔案大小 4 WORD bfReserved1; //保留 5 WORD bfReserved2; //保留 6 DWORD bfOffBits; //資料起始地址距離首地址的位置 7 } BITMAPFILEHEADER, *LPBITMAPFILEHEADER, *PBITMAPFILEHEADER;
3.BITMAPINFOHEADER檔案格式
[3]bitMapInfoHeader根據版本的不同包含了不同的結構,我們主要用到了BitMapInfoHeader和BitMapCoreHeader
以下是BitMapInfoHeader
1 typedef struct tagBITMAPINFOHEADER { 2 DWORD biSize; // infoHeader這個標頭檔案的大小 3 LONG biWidth; // 圖片寬多少畫素 4 LONG biHeight; //高多少 5 WORD biPlanes; //預設1 6 WORD biBitCount; //一個畫素點有多少位 7 DWORD biCompression; //是否壓縮過 8 DWORD biSizeImage; //圖片大小 9 LONG biXPelsPerMeter; //never used 10 LONG biYPelsPerMeter; //never used 11 DWORD biClrUsed; //never used 12 DWORD biClrImportant; //never used 13 } BITMAPINFOHEADER, *PBITMAPINFOHEADER;
以下是BitMapCoreHeader, coreHeader就簡單多了,就是少了一些欄位,其他一毛一樣。
typedef struct tagBITMAPCOREHEADER { DWORD bcSize; WORD bcWidth; WORD bcHeight; WORD bcPlanes; WORD bcBitCount; } BITMAPCOREHEADER, *LPBITMAPCOREHEADER, *PBITMAPCOREHEADER;
4.讀取圖片資料
定義常用結構
using byte = unsigned char; using uint = unsigned int; struct RGB { byte blue; byte green; byte red; };
定義BMPReader(用來讀取bmp檔案) 和BMPHeader結構.
1 #pragma once 4 class BMPReader { 5 public: 6 BMPReader() = default; 7 ~BMPReader() { 8 if (data) { 9 delete[] data;
data = nullptr; 10 } 11 } 12 bool open(std::string& path); 13 public: 14 uint height = 0; 15 uint width = 0; 16 uint paddingBytes = 0; 17 RGB* data = nullptr; //實際的資料 18 }; 19 20 //2個位元組對齊, 21 #pragma pack(2) 22 typedef struct { 23 unsigned short bfType = 0x424d; 24 unsigned int bfSize = 0; 25 unsigned short bfReserved1 = 0; 26 unsigned short bfReserved2 = 0; 27 unsigned int bfOffBits = 0; 28 } BitMapFileHeader; 29 30 typedef struct { 31 unsigned int biSize; 32 int biWidth; 33 int biHeight; 34 unsigned short biPlanes; 35 unsigned short biBitCount; 36 unsigned int biCompression; 37 unsigned int biSizeImage; 38 int biXPelsPerMeter; 39 int biYPelsPerMeter; 40 unsigned int biClrUsed; 41 unsigned int biClrImportant; 42 } BitMapInfoHeader; 43 44 typedef struct { 45 unsigned int bcSize; 46 unsigned short bcWidth; 47 unsigned short bcHeight; 48 unsigned short bcPlanes = 1; 49 unsigned short bcBitCount = 24; 50 } BitMapCoreHeader; 51 #pragma pack()
讀取圖片資料到BMPReader的data欄位
bool BMPReader::open(std::string& path) { FILE* file = nullptr; //判斷是否開啟圖片成功 if ((file = fopen(path.c_str(), "rb")) == nullptr) { printf("error occured when opening file:%s", path.c_str()); return false; } BitMapFileHeader fileHeader; //讀取檔案頭 if (fread(&fileHeader, sizeof(fileHeader), 1, file) != 1) { printf("Error - error occured when reading BITMAPFILEHEADER"); return false; } //判斷是不是BMP檔案,通過判斷'BM' if(fileHeader.bfType != 0x4D42) { printf("Error - this is not a BMP file that you're reading"); return false; }
//讀取infoHeader的大小,通過size來判斷是哪個版本的BitMapInfoHeader uint infoHeaderSize; fread(&infoHeaderSize, sizeof(infoHeaderSize), 1, file); fseek(file, -sizeof(infoHeaderSize), SEEK_CUR); //2中infoHeader, 通過size來判斷
//如果是BitMapCoreHeader的話 if (infoHeaderSize == sizeof(BitMapCoreHeader)) { BitMapCoreHeader bitMapCoreHeader; if (fread(&bitMapCoreHeader, sizeof(bitMapCoreHeader), 1, file) != 1) { printf("Error - mal-structure BITMAPCOREHEADER"); return false; } this->width = bitMapCoreHeader.bcWidth; this->height = bitMapCoreHeader.bcHeight; if (bitMapCoreHeader.bcBitCount != 24) { printf("Error - the picture format is not consistent with our programm"); return false; } } else { BitMapInfoHeader bitMapInfoHeader; if (fread(&bitMapInfoHeader, sizeof(bitMapInfoHeader), 1, file) != 1) { printf("Error - mal-structure BITMAPINFOHEADER"); return false; } this->width = bitMapInfoHeader.biWidth; this->height = bitMapInfoHeader.biHeight; if (bitMapInfoHeader.biBitCount != 24) { printf("Error - the picture format is not consistent with our programm"); return false; } } // 必須是4byte的整數倍,算出需要padding多少位元組,也就是需要填補多少位元組// if width = 1, 1 * 3個畫素 * 8位 = 24位, 差一個位元組, paddingSize = 1 // if width = 2, 2 * 3個 * 8位 = 48位 paddingSize = 2 // if width = 3, paddingSize = 3 // if width = 4, paddingSize = 0 this->paddingBytes = width % 4;
//為畫素資料 建立記憶體空間 data = new (std::nothrow) RGB[this->width * this->height]; if (!data) { printf("Error - error when allocating memroy for RGB"); return false; } //跳到data資料的位置 fseek(file, fileHeader.bfOffBits, SEEK_SET); for (uint i = 0; i < height; i++) { //read data一行,一行的讀取放入我們的data if (width != fread(data + (height - 1 - i) * width , sizeof(RGB), width, file)) { printf("Error - something wrong when reading data from BMP file"); delete data; return false; }
//因為可能有paddingSize,所以這邊跳過PaddingSize fseek(file, paddingBytes, SEEK_CUR); } fclose(file); return true;
}
以上全部的程式碼在https://github.com/Cheemion/JPEG_COMPRESS/tree/main/Day1
完結
Thanks for reading, happy lunar new year.
參考資料
[1]https://docs.microsoft.com/en-us/windows/win32/gdi/bitmap-storage
[2]https://docs.microsoft.com/en-us/previous-versions//dd183376(v=vs.85)
[3]https://docs.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapcoreheader