影象處理之(24位)BMP旋轉以及映象演算法
阿新 • • 發佈:2019-01-02
在影象處理,圖形學、計算機視覺中,我們經常能夠見到bmp這種格式的圖片;那麼對於我們來說想要處理這種圖片,首先就應當瞭解這種圖片,知己知彼方能百戰不殆。
那麼首先我們來了解一下bmp格式的圖片:
一、瞭解BMP
(一)概述
BMP(全稱Bitmap)是Windows作業系統中的標準影象檔案格式,可以分成兩類:裝置相關點陣圖(DDB)和裝置無關點陣圖(DIB),使用非常廣。它採用位對映儲存格式,除了影象深度可選以外,不採用其他任何壓縮,因此,BMP檔案所佔用的空間很大。BMP檔案的影象深度可選lbit、4bit、8bit及24bit。BMP檔案儲存資料時,影象的掃描方式是按從左到右、從下到上的順序。由於BMP檔案格式是Windows環境中交換與圖有關的資料的一種標準,因此在Windows環境中執行的圖形影象軟體都支援BMP影象格式。
(二)格式特性
瞭解完BMP的通用性之後,其實我們想要處理BMP格式圖片,最關心的還是這個格式的組成是怎樣的,有什麼特點,怎樣處理這個格式的圖片。
因此我們來把重點放在BMP的格式組成上:
BMP其實是圖片格式中最簡單的一種,一般來說,典型的BMP影象資料檔案由四部分組成:
1. 點陣圖檔案頭資料(BITMAPFILEHEADER):這個資料結構包含了BMP影象檔案的型別、大小等資訊;
typedef struct targetBITMAPFILEHEADER{
WORD bfType; //檔案型別,對於點陣圖來說,這一部分為0x4d42
DWORD bfSize; //檔案大小(包含這14位元組)
WORD bfReserved1; //保留字,不考慮
WORD bfReserved2; //保留字,同上
DWORD bfOffBits; //實際點陣圖資料的偏移位元組數,即前三個部分長度之和
}BITMAPFILEHEADER;
2. 點陣圖資訊頭資料(BITMAPINFOHEADER):這個資料結構則是包含了BMP影象資料的寬、高、壓縮方法、定義顏色、佔用空間等等資訊;
typedef struct targetBITMAPINFOHEADER{
DWORD biSize; //指定此結構體的長度,為40
LONG biWidth; //點陣圖寬
LONG biHeight; //點陣圖高
WORD biPlanes; //平面數,為1
WORD biBitCount; //採用顏色位數,可以是1,2,4,8,16,24,新的可以是32
DWORD biCompression; //壓縮方式,可以是0,1,2,其中0表示不壓縮
DWORD biSizeImage; //實際點陣圖資料佔用的位元組數
LONG biXPelsPerMeter; //X方向解析度
LONG biYPelsPerMeter; //Y方向解析度
DWORD biClrUsed; //使用的顏色數,如果為0,則表示預設值(2^顏色位數)
DWORD biClrImportant; //重要顏色數,如果為0,則表示所有顏色都是重要的
}BITMAPINFOHEADER;
3.調色盤(RGBQUAD):其中,這一部分的資料結構是可選擇的,有些為徒需要調色盤,有些點陣圖則不需要(比如24位的真彩圖就不需要);
typedef struct tagRGBQUAD{
BYTE rgbBlue;
BYTE rgbGreen;
BYTE rgbRed;
BYTE rgbReserved;
}RGBQUAD;
4.點陣圖資料:這部分的內容根據BMP點陣圖使用的位數不同而不同,在24位真彩圖中,直接使用RGB,而其他的小於24位的則使用調色盤中顏色的索引值。
typedef struct tagIMAGEDATA
{
BYTE blue;
BYTE green;
BYTE red;
}IMAGEDATA;
二、旋轉BMP的理論準備
在進行編碼之前,我們首先需要知道——旋轉BMP需要什麼樣的準備工作、怎樣去做,那麼仔細考慮一下其實我們面臨的是以下的幾個問題:
1. 首先我們在進行旋轉的時候應當注意在旋轉前與旋轉後的座標對應關係;
2.當我們旋轉之後,如果旋轉之後的畫素點並不是很如人意的落在畫素點上,而是落在臨近的四個畫素點構成的正方形區域內(而且這種情況應該是很常見的一種),我們應當怎樣確定旋轉之後的畫素的顏色值;
3.怎樣對影象進行旋轉;
4.旋轉之後怎樣將整個圖片顯示出來;
(一)問題一的求解
那麼針對於第一個問題,我們要做的就是進行座標變換:如以下的圖片:
這樣我們就完成了第一個問題的求解,這樣我們就得到了變換前後的座標的相互關係。
(二)問題二的求解
對於問題二的求解,我們採取的方式是,雙線性插值的方法來完成計算該點的畫素值。
(三)問題三的求解
我們現在已經完成了這些,我們就可以開始進行一些理論上的第三個問題——即怎樣進行旋轉,我們應當採用怎樣的方式來進行影象的旋轉——
我們的第一個想法可能就是,我們通過原來的畫素點,然後通過問題一種確定的座標變換公式,然後向新建的空白圖層中填充由座標變換得到的資料。但是這樣的方式可能存在一些問題:
其一:在計算過程之中,可能得到的資料並不落在畫素點上;
其二:有些資料可能轉出了現有的空間。(但是這個問題比較好處理,主要是前面的一個問題)
鑑於以上的考慮,我們採用的是先建立新的空白圖層,然後根據新圖層上的畫素點然後計算出對應的原始影象中的畫素點,這時候雖然不一定落在畫素點上,但是我們可以通過問題二的求解方法來完成點P顏色值的計算。
(四)問題四的求解
三、具體程式碼
首先,我們先建立一個bmp的標頭檔案:
/**
* @author Mica_Dai
* @date 2017.10.2
* */
typedef unsigned char BYTE;
typedef unsigned short WORD;
typedef unsigned long DWORD;
typedef long LONG;
typedef struct targetBITMAPFILEHEADER{
DWORD bfSize; //檔案大小(包含這14位元組)
WORD bfReserved1; //保留字,不考慮
WORD bfReserved2; //保留字,同上
DWORD bfOffBits; //實際點陣圖資料的偏移位元組數,即前三個部分長度之和
}BITMAPFILEHEADER;
typedef struct targetBITMAPINFOHEADER{
DWORD biSize; //指定此結構體的長度,為40
LONG biWidth; //點陣圖寬
LONG biHeight; //點陣圖高
WORD biPlanes; //平面數,為1
WORD biBitCount; //採用顏色位數,可以是1,2,4,8,16,24,新的可以是32
DWORD biCompression; //壓縮方式,可以是0,1,2,其中0表示不壓縮
DWORD biSizeImage; //實際點陣圖資料佔用的位元組數
LONG biXPelsPerMeter; //X方向解析度
LONG biYPelsPerMeter; //Y方向解析度
DWORD biClrUsed; //使用的顏色數,如果為0,則表示預設值(2^顏色位數)
DWORD biClrImportant; //重要顏色數,如果為0,則表示所有顏色都是重要的
}BITMAPINFOHEADER;
typedef struct tagRGBQUAD{
BYTE rgbBlue;
BYTE rgbGreen;
BYTE rgbRed;
BYTE rgbReserved;
}RGBQUAD;
然後的工作就主要放在檔案讀取上,我們就不斷的從原始的檔案中讀取資料,然後將處理之後的資料寫入新的檔案當中:這樣來說我們現在需要的做的就是
1. 建立一個新檔案,根據影象的配置等完成資訊頭等資料的寫入,這些基本資訊配置完成之後,我們就可以直接讀取點陣圖資訊了;
2. 申請空間存放新舊影象資訊的資料,然後將影象資訊存放在舊的陣列內;
3. 然後從頭掃描到位,並對每一個掃描的畫素點進行座標變換,然後檢測該變換之後的畫素點是否在原始影象範圍內
a. 如果不在則將新畫素點置為0;
b. 在範圍內,則通過雙線性插值完成顏色值的配置
5. 將變換之後的資料寫入新陣列,然後在寫入檔案內
6. 關閉檔案;
接下來就是我們的第一部的相關程式碼:
注意:在讀取檔案的時候一定要注意,我們讀取一定要按照順序來讀,即一定要對齊,否則可能會資料錯位而導致一系列的bug,最後導致結果圖片無法顯示,資料錯誤。我曾經重複讀取了bftype,最後的結果就是一個400多K的圖片欸處理成為了1.5G的“東西”,同時資料也損壞了。
/**
* @author Mica_Dai
* @Date 2017.10.2
* */
FILE *file, *targetFile;
int rotateX, rotateY;
int write_rotateX, write_rotateY;
BITMAPFILEHEADER bmpFile, writeBmpFile;
BITMAPINFOHEADER bmpInfo, writeBmpInfo;
int angle;
double thelta;
char fileName[20];
WORD bfType;
cout << "please input the bmp file's name : ";
cin >> fileName;
file = fopen(fileName, "rb");
targetFile = fopen("16.bmp", "wb");
/**
* step 1 : 圖片處理第一步,首先是完成將檔案頭,資訊頭等的資料遷移
*/
fread(&bfType, 1, sizeof(WORD), file);
fwrite(&bfType, 1, sizeof(WORD), targetFile);
if (0x4d42 != bfType){
cout << "wrong format!" << endl;
return -1;
}
cout << "please input the rotate angle : ";
cin >> angle;
if (angle % 360 == 270){
angle ++;
}
thelta = (double)(angle * PI / 180);
// 整個變化中bmp的檔案頭是相似的,只有bfsie變化了
fread(&bmpFile, 1, sizeof(BITMAPFILEHEADER), file);
writeBmpFile = bmpFile;
// 整個變換過程之中bmp的資訊頭也是相似的,這個則是biwidth、biheight,以及bisizeimage發生變化
fread(&bmpInfo, 1, sizeof(BITMAPINFOHEADER), file);
writeBmpInfo = bmpInfo;
int width = bmpInfo.biWidth;
int height = bmpInfo.biHeight;
int newWidth = abs(width * cos(thelta) + height * sin(thelta));
// int newHeight = 1054;
int newHeight = abs(width * sin(thelta) + height * cos(thelta));
// int newWidth = 1500;
writeBmpInfo.biWidth = newWidth;
writeBmpInfo.biHeight = newHeight;
// 在計算實際佔用的空間的時候我們需要將寬度為4byte的倍數
int writewidth = WIDTHBYTES(newWidth * writeBmpInfo.biBitCount);
writeBmpInfo.biSizeImage = writewidth * writeBmpInfo.biHeight;
writeBmpFile.bfSize = 54 + writeBmpInfo.biSizeImage;
fwrite(&writeBmpFile, 1, sizeof(BITMAPFILEHEADER), targetFile);
fwrite(&writeBmpInfo, 1, sizeof(BITMAPINFOHEADER), targetFile);
第二步:進行空間的申請以及資料的填充:
/**
* step 2 : 完成空間的申請與分配的準備工作
*/
// 在這裡因為待處理的圖片寬度應當是4byte的倍數,因此我們首先要完成對寬度進行完成4byte的倍數化
int l_width = WIDTHBYTES(width * bmpInfo.biBitCount);
int write_width = WIDTHBYTES(writeBmpInfo.biWidth * writeBmpInfo.biBitCount);
rotateX = width / 2;
rotateY = height / 2;
write_rotateX = newWidth / 2;
write_rotateY = newHeight / 2;
// cout << "writeBmpInfo.biWidth : " << writeBmpInfo.biWidth << endl;
// cout << "writeBmpInfo.biBitCount : " << writeBmpInfo.biBitCount << endl;
// cout << "write_width" << write_width << endl;
// cout << "writewidth" << writewidth << endl;
// 準備工作完成之後,我們現在就要將bmp檔案中的資料檔案存放在一個數組中,因此我們需要申請空間建立陣列來完成資料的存放
BYTE *preData = (BYTE *)malloc(height * l_width);
memset(preData, 0, height * l_width);
BYTE *aftData = (BYTE *)malloc(newHeight * writewidth);
memset(aftData, 0, newHeight * writewidth);
// cout << "i'm here!" << endl;
int OriginalImg = l_width * height;
int LaterImg = writewidth * newHeight;
fread(preData, 1, OriginalImg, file);
第三步:就是整個處理的重頭戲——對於整個影象的旋轉處理:
/**
* step 3 : 完成將影象資訊的遷移
*/
for (int hnum = 0; hnum < newHeight; ++ hnum){
// cout << "hh " << hnum << endl;
for (int wnum = 0; wnum < newWidth; ++ wnum){
// 新資料的下標為index
int index = hnum * writewidth + wnum * 3;
// cout << "index " << index << endl;
// 利用公式計算這個原來的點的地方
double d_original_img_hnum = (wnum - write_rotateX) * sin(thelta) + (hnum - write_rotateY) * cos(thelta) + rotateY;
double d_original_img_wnum = (wnum - write_rotateX) * cos(thelta) - (hnum - write_rotateY) * sin(thelta) + rotateX;
if (d_original_img_hnum < 0 || d_original_img_hnum > height || d_original_img_wnum < 0 || d_original_img_wnum > width){
aftData[index] = 0; // 這個相當於是R
aftData[index + 1] = 0; // 這個相當於是G
aftData[index + 2] = 0; // 這個相當於是B
continue;
}else{
/**
* 我們在這裡使用雙線性插值法來完成對應
*/
int i_original_img_hnum = d_original_img_hnum;
int i_original_img_wnum = d_original_img_wnum;
double distance_to_a_X = d_original_img_wnum - i_original_img_wnum;
double distance_to_a_Y = d_original_img_hnum - i_original_img_hnum;
int original_point_A = i_original_img_hnum * l_width + i_original_img_wnum * 3;
int original_point_B = i_original_img_hnum * l_width + (i_original_img_wnum + 1) * 3;
int original_point_C = (i_original_img_hnum + 1) * l_width + i_original_img_wnum * 3;
int original_point_D = (i_original_img_hnum + 1) * l_width + (i_original_img_wnum + 1) * 3;
if (i_original_img_wnum == width - 1){
// cout << "hhhhh" << endl;
original_point_A = original_point_B;
original_point_C = original_point_D;
}
if (i_original_img_hnum == height - 1){
original_point_C = original_point_A;
original_point_D = original_point_B;
}
aftData[index] = (1 - distance_to_a_X) * (1 - distance_to_a_Y) * preData[original_point_A]
+ (1 - distance_to_a_X) * distance_to_a_Y * preData[original_point_B]
+ distance_to_a_X * (1 - distance_to_a_Y) * preData[original_point_C]
+ distance_to_a_X * distance_to_a_Y * preData[original_point_D];
aftData[index + 1] = (1 - distance_to_a_X) * (1 - distance_to_a_Y) * preData[original_point_A + 1]
+ (1 - distance_to_a_X) * distance_to_a_Y * preData[original_point_B + 1]
+ distance_to_a_X * (1 - distance_to_a_Y) * preData[original_point_C + 1]
+ distance_to_a_X * distance_to_a_Y * preData[original_point_D + 1];
aftData[index + 2] = (1 - distance_to_a_X) * (1 - distance_to_a_Y) * preData[original_point_A + 2]
+ (1 - distance_to_a_X) * distance_to_a_Y * preData[original_point_B + 2]
+ distance_to_a_X * (1 - distance_to_a_Y) * preData[original_point_C + 2]
+ distance_to_a_X * distance_to_a_Y * preData[original_point_D + 2];
}
}
}
第四步:寫入資料以及寫入檔案
/**
* step 4:寫入資料
*/
fwrite(aftData, 1, LaterImg, targetFile);
fclose(file);
fclose(targetFile);
四、旋轉效果:
最後的效果圖如下圖所示:
旋轉前:
旋轉之後:(45°,60°,90°,180°)
五、圖片映象理論準備
對於圖片映象,我們需要坐的理論掌握只有一個就是關於對稱軸怎樣變換:
六、映象編碼
前幾步讀取資料都是一樣的,因此我們,只有在處理圖片這一步不同:於是有如下程式碼:
/**
* @author Mica_Dai
* @Date 2017.10.2
* */
for (int hnum = 0; hnum < height; ++ hnum){
for (int wnum = 0; wnum < width; ++ wnum){
int index = hnum * actual_width + wnum * bCount;
int original_X = width - wnum;
int original_Y = hnum;
for (int i = 0; i < bCount; ++ i){
aftData[index + i] = preData[original_Y * actual_width + original_X * bCount + i];
}
}
}