問題:現有一張bmp圖片,要求將它讀取到程式中并進行灰度化、水平翻轉、模糊、茶色濾鏡四種效果的一種,并輸出新圖片,如下所示:

命令列輸入:

其中:
引數1:-b/g/s/r,先后表示blur(模糊),grey(灰度化),sepia(褐色),row reverse(水平翻轉)
引數2:源檔案名
引數3:新檔案名
當我第一次接觸到這個問題時,是無從下手的,但在查閱了不少資料之后,整整一天,我成功地只用C++實作了打開、修飾、保存bmp檔案的功能!
目錄
1.bmp檔案的基本資訊
(1).bmp檔案的種類
(2).bmp檔案結構(重點)
1>檔案頭
2>資訊頭
3>調色板(不作討論)
4>影像顏色資訊
2.實作思路
3.定義相關類、結構體
檔案頭BmpFileHeader
記憶體對齊和#pragma pack(n)
資訊頭BmpFileInfoHeader
顏色結構體RGBTriple
圖片類bmp
4.讀檔案
(1).基礎知識
(2).讀檔案的準備作業
(3).讀檔案頭和資料頭
(4).讀取影像顏色資訊
5.寫檔案
6.修飾圖片
(1).灰度化
(2).棕色濾鏡效果
(3).水平翻轉
(4).模糊
7.main函式
命令列傳參
具體實作
1.bmp檔案的基本資訊
(1).bmp檔案的種類

打開Windows自帶的畫圖軟體,發現bmp的存盤格式有好幾種,
- 單色位圖:只有黑白兩種顏色,每個像素占1位(1/8位元組)
- 16色位圖:每個像素占4位(1/2位元組)
- 256色位圖:每個像素占8位(1位元組)
- 24位位圖(真彩色):每個像素占24位(3位元組),每個位元組存盤R/G/B三種中的一種顏色數值(0~255)
每個像素占的位數被稱為位深度(biBitCount,在后面會用到),可以在圖片的屬性->詳細資訊中查看,

(2).bmp檔案結構(重點)
bmp檔案資料由4部分組成:
- 檔案頭
- 檔案資訊頭
- 調色板(24位位圖無)
- 影像顏色資訊
在此只討論24位位圖即真彩色的問題,至于其他的bmp檔案種類不做討論,
先放出圖片:

1>檔案頭
| bfType | 如果是bmp檔案,值為“BM”,對應十進制為19778 |
| bfSize | 檔案總大小 |
| bfReserved1 | 保留字1,一般為0 |
| bfReserved2 | 保留字2,一般為0 |
| bfOffBits | 檔案起始位置距真正的影像資訊的距離 |
2>資訊頭
| biSize | 資訊頭大小,24位圖中為40 |
| biWidth | 影像寬度(px),即水平方向的像素個數 |
| biHeight | 影像高度(px),即垂直方向的像素個數 |
| biPlanes | 一般為1 |
| biBitCount | 位深度,重要,決定了bmp的型別 |
| biCompression | 是否壓縮,一般為0 |
| biSizeImages | 影像顏色資訊占用的實際位元組數,包括了對齊所需的0 |
| biXPelsPerMeter | 水平解析度 |
| biYPelsPerMeter | 垂直解析度 |
| biClrUsed | 一般為0 |
| biClrImportant | 一般為0 |
注意:我們可能會發現 biWidth*3*biHeight與biSizeImages并不一樣,這是為什么呢?接下來會解釋,
3>調色板(不作討論)
4>影像顏色資訊
- 像素的存盤順序是從下到上,從左到右,在檔案中以類似一維陣列的方法線性存盤,
- 每個像素的顏色資訊每3個位元組一組,按BGR的順序存放,
- 其中每個位元組只存一個顏色值,顏色值范圍是0~255,用無符號char型存盤,
三個圖就能說明問題:



但是這些資料真的如此緊密地排列嗎?
對于寬為4的倍數的圖片(如:1024px),確實如此,每一行的像素資料存完后,緊挨著存盤下一行像素的資料,行與行的資料之間沒有空隙,
但對于寬度不是4的倍數的圖片(如:474px),每一行的像素資料存盤完后,會自動空出幾個位元組,直到這一行的位元組數為4的倍數為止,
直接呈上圖片:

biWidth=4時:很好,不用補任何0,因為4*3=12已經是4的倍數

biWidth=5時:糟糕,5*3=15不是4的倍數,要補一個0才能是4的倍數16
所以,在讀取寬度不是4的倍數的圖片時,一行的資料讀完后,要跳過幾個位元組才能讀到下一行的資料,跳過位元組的個數,我取名為offset,它的計算方法如下:
offset = (fileInFoHeader.biWidth * 3) % 4;
if (offset != 0) {
offset = 4 - offset;
}
現在我可以解釋2>中末尾提到的問題了,
例子:現在有一張寬度為474px,高度為842px的圖片,
不考慮offset時:
474*3*842=1197324(Byte)
考慮時:
(474*3)%4=2
offset=4-2=2(Byte)
每行位元組數:474*3+2=1424(Byte)
影像資料總位元組數:1424*842=1199008(Byte)
誰對誰錯?看看圖就知道了,

它們的差值:1199008-1197324= 1684(Byte),而1684=842*2,因為每一行末尾有2位元組的空隙,那么,842行的空隙積累起來,正好就是1684位元組,
debug的結果說明,影像資料占用的實際位元組數是考慮了偏移的,這些資料在存的時候就已經有空隙,因此我們寫檔案的時候也要刻意的寫入空隙,不然系統無法讀取我們生成的新圖片,這一點在后面很關鍵!
2.實作思路
首先要把bmp檔案讀進來,由以上的分析,應該把bmp的檔案頭、資訊頭、影像資料分開讀取,
然后要生成新bmp檔案,應該要依次寫入檔案頭、資訊頭、影像資料,
最后實作影像處理功能,這些用于影像處理的函式封裝在一個單獨的頭檔案中,使用時傳入函式指標即可,
3.定義相關類、結構體
定義檔案頭、資訊頭結構體(因為它們不需要任何函式),里面存放與檔案相關的屬性,各個屬性的大小參考一開始時的bmp檔案結構圖,2位元組一般定義成unsigned short,4位元組一般定義成unsigned int.
定義bmp類(因為它需要定義函式),里面最重要的是一個存放”顏色“結構體物件的陣列,用于接收讀出的影像顏色資料,還有一個int型的offset,一個檔案頭結構體物件,一個資料頭結構體物件,定義讀檔案和寫檔案兩個函式,
很自然地,需要一個”顏色“結構體物件,它有三個屬性B/G/R,
注意:結構體中,所有屬性的定義順序必須和檔案存盤的資訊順序一致,否則在讀檔案時得到的資料會混亂!也就是:必須根據bmp檔案結構來!
檔案頭BmpFileHeader
#pragma pack(2)//注意這里
struct BmpFileHeader {
unsigned short bfType;
unsigned int bfSize;
unsigned short bfReserved1;
unsigned short bfReserved2;
unsigned int bfOffBits;
};
尤其要注意#pragma pack(2),沒有這一行,這個結構體占用的空間大小就不是所有屬性大小之和,換句話說,它清除了屬性與屬性之間記憶體的”空洞“,明白這個,對讀檔案操作極其重要!
記憶體對齊和#pragma pack(n)
在此簡短地說一下記憶體對齊問題,用于測驗的代碼如下:
#include <iostream>
using namespace std;
struct BmpFileHeader {
unsigned short bfType;
unsigned int bfSize;
unsigned short bfReserved1;
unsigned short bfReserved2;
unsigned int bfOffBits;
};
int main()
{
cout << sizeof(BmpFileHeader)<< endl;//16
return 0;
}
但是,這些資料占用的空間按理來說是2+4+2+2+4=14(Byte)才對呀,為什么會輸出16呢?
首先放上原理:
結構體的屬性是按定義的順序來存放的,
結構體一般有很多屬性,取其中占記憶體最大的一個,它所占的位元組數為默認對齊模數,
假定第一個屬性的存放地址為0,后來的資料在存放時,取自身資料大小和對齊模數二者的最小值min,尋找離自己最近的而且是min整數倍的地址,把資料存到那里,
字有點多,不好理解,對不對?還是老規矩,畫圖:




給這個結構體指定#pragma pack(2)會怎樣呢?
#include <iostream>
using namespace std;
#pragma pack(2)
struct BmpFileHeader {
unsigned short bfType;
unsigned int bfSize;
unsigned short bfReserved1;
unsigned short bfReserved2;
unsigned int bfOffBits;
};
int main()
{
cout << sizeof(BmpFileHeader)<< endl;//14
return 0;
}
輸出14,與我們先前預想的相符,
#pragma pack(n)的作用:為當前結構體指定新的對齊模數n,


資訊頭BmpFileInfoHeader
struct BmpFileInFoHeader {
unsigned int biSize;
int biWidth = 0, biHeight = 0;
unsigned short biPlanes;
unsigned short biBitCount;
unsigned int biCompression, biSizeImages;
int biXPelsPerMeter, biYPelsPerMeter;
unsigned int biClrUsed, biClrImportant;
};
這里不用加#pragma pack()的原因是:這些屬性按默認對齊模數4正好可以按順序無空隙地存盤,請大家自行驗證,
顏色結構體RGBTriple
它放在一個單獨的頭檔案(RGBTriple.h)中,注意里面的#pragma once,防止頭檔案重復包含,
也要注意,屬性的定義順序與習慣的不同,它只能是BGR,這是為了使讀取時資料存放的順序正確,
#pragma once
struct RGBTriple {
unsigned char blue;
unsigned char green;
unsigned char red;
};
圖片類bmp
class bmp {
private:
int offset;//行尾的空隙
RGBTriple* surface;//存圖片顏色資料的陣列
BmpFileHeader fileHeader;//檔案頭
BmpFileInFoHeader fileInFoHeader;//資料頭
public:
void readPic(const char* fileName);//讀檔案
void writePic(void (*myMethod)(int,int,RGBTriple*), const char* outFileName);//寫檔案
};
surface是一個指標,指向堆中的一個存放著RGBTriple結構體物件的陣列,那個陣列將在讀檔案時被創建,并一直保留到程式結束,
讀檔案和寫檔案的兩個方法可以接收字串作為檔案名,寫檔案的方法還可以接收一個函式指標,指定在寫檔案之前,對圖片進行的修飾操作,
檔案頭結構體、資訊頭結構體、圖片類定義在同一個頭檔案(bmpFile.h)中,完整代碼如下:
#pragma once
#include<fstream>
#include<iostream>
#include"RGBTriple.h"
#define BMPTYPE 19778
using namespace std;
#pragma pack(2)
struct BmpFileHeader {
unsigned short bfType;
unsigned int bfSize;
unsigned short bfReserved1;
unsigned short bfReserved2;
unsigned int bfOffBits;
};
struct BmpFileInFoHeader {
unsigned int biSize;
int biWidth = 0, biHeight = 0;
unsigned short biPlanes;
unsigned short biBitCount;
unsigned int biCompression, biSizeImages;
int biXPelsPerMeter, biYPelsPerMeter;
unsigned int biClrUsed, biClrImportant;
};
class bmp {
private:
int offset;
RGBTriple* surface;
BmpFileHeader fileHeader;
BmpFileInFoHeader fileInFoHeader;
public:
void readPic(const char* fileName);
void writePic(void (*myMethod)(int,int,RGBTriple*), const char* outFileName);
};
4.讀檔案
(1).基礎知識
C++的fstream頭檔案提供了檔案輸入流ifstream和檔案輸出流ofstream,
兩種流物件在使用時有一些相同的步驟:
- 創建流物件
- 打開檔案:open(“檔案路徑”,打開方式)
- (可選)檢測檔案是否打開:is_open()
- 使用后關閉流:close()
常見的打開方式:
| ios::in | 讀檔案 |
| ios::out | 寫檔案 |
| ios::app | 打開檔案時,游標在檔案末尾 |
| ios::trunc | 如果存在同名檔案,就洗掉它創建新檔案 |
| ios::binary | 以二進制的方式讀/寫檔案 |
幾個不同的打開方式可以用 |(單豎線)連接,表示這種打開方式同時具有兩種含義,
一個完整的使用ifstream的例子:
#include<fstream>
#include<iostream>
using namespace std;
int main(){
ifstream ifs;
ifs.open("aaa.bmp",ios::in | ios::binary);
if(!ifs.is_open()){
cout<<"檔案未打開!"<<endl;
}
//讀取...
ifs.close();
}
ifstream的特有方法:read();ofstream的特有方法:write();它們的函式原型如下:
basic_istream& __CLR_OR_THIS_CALL read(_Elem* _Str, streamsize _Count);
引數1:char*型別,表示待讀入的資料的“去路”
引數2:一次讀入資料的位元組總數
basic_ostream& __CLR_OR_THIS_CALL write(const _Elem* _Str, streamsize _Count);
引數1:char*型別,表示待寫入的資料的來源
引數2:一次寫入資料的位元組總數
(2).讀檔案的準備作業
新建bmpFile.cpp檔案,書寫bmp類兩個函式的空實作,
#include"bmpFile.h"
void bmp::readPic(const char* fileName) {
}
void bmp::writePic(void (*myMethod)(int,int,RGBTriple*),const char* outFileName) {
}
注意到bmpFile.h中已經引入了頭檔案fsteram,故直接使用其中的結構即可,
#include"bmpFile.h"
void bmp::readPic(const char* fileName) {
ifstream ifs;
ifs.open(fileName, ios::in|ios::binary);
if (!ifs.is_open()) {
cout << "Can't open the file." << endl;
return;
}
//do something
ifs.close();
}
void bmp::writePic(void (*myMethod)(int,int,RGBTriple*),const char* outFileName) {
}
注意ios::binary,它指定以二進制的方式讀檔案,如果少了它,程式雖不出錯,但輸出的圖片卻是一團黑(親測),
(3).讀檔案頭和資料頭
按先前的思路來,先讀檔案頭,再讀資料頭,剩下的就是影像資訊了,
ifs.read((char*)&fileHeader,sizeof(BmpFileHeader));
注意里面的強制型別轉換,之所以把fileHeader結構體物件的地址轉換成char*型,就是因為read()函式只接收char*型的地址,
讀完檔案頭之后,有個問題:要是讀進來的檔案根本不是bmp型別怎么辦?那么后面的操作不就失去意義了嗎?
所以我們緊接著添加一個if陳述句來判斷讀的是不是bmp型別,如果不是,就結束整個函式,由bmp檔案頭結構可知,其中的bfType如果不是19778,檔案就不是bmp型別,19778已經在bmpFile.h中被定義為宏常量BMPTYPE,
if(fileHeader.bfType!=BMPTYPE){
cout<<"檔案型別不正確!"<<endl;
return;
}
接著才讀取資料頭,還是一樣的問題,讀的bmp檔案不是24位的怎么辦?非24位的bmp,我們是不能處理的,只能再加上一個判斷,如果不是24位就結束整個函式,
ifs.read((char*)&fileInFoHeader, sizeof(BmpFileInFoHeader));
if (fileInFoHeader.biBitCount != 24) {
cout << "invalid!" << endl;
return;
}
(4).讀取影像顏色資訊
現在終于可以開始讀取影像顏色資訊啦!但在此之前,我們要解決空隙的問題,在讀取資料頭之后,我們獲得了影像寬度biWidth,這時才可以計算offset的大小,
offset = (fileInFoHeader.biWidth * 3) % 4;
if (offset != 0) {
offset = 4 - offset;
}
還要考慮一個問題:我們必須使用一個存放RGBTriple物件的陣列來存整張圖片的顏色資訊,
這個問題可以分解成兩個小問題:
Q1:存在堆疊里還是存在堆里?
答案顯而易見,一般圖片的長寬在1000px以上的不在少數,如果存在堆疊里,堆疊很可能會溢位,
Q2:定義一維陣列還是二維陣列?
我們很自然地會想用二維陣列,因為圖片就是按行、列存盤的,但實際上不行,因為盡管new是動態分配記憶體,二維陣列的第二維仍然必須是一個常量,否則new不知道應該回傳什么型別的行指標(有關這方面的知識,可以參考其他文章),編譯會報錯,我們可以用一種“降維”的方法解決這個問題,就是new一個超長的一維陣列,它的長度是圖片的(長*寬),
用于存放影像顏色資料的陣列surface是一個RGBTriple型的指標,我們new一個新的一維陣列,將陣列首地址賦給surface,
surface = new RGBTriple[fileInFoHeader.biHeight * fileInFoHeader.biWidth];
到目前為止,我們終于可以開始讀bmp檔案中最重要的資訊啦!
還記得嗎?bmp檔案的像素存盤順序:從下到上,從左到右,因此我們如果要想在surface存入正常順序的像素資料(從上到下,從左到右),在向surface中存資料時就有講究,先讀出的像素資料要靠后存盤,
一次讀入3個位元組(BGR),存入surface的某一元素(即:一個RGBTriple物件)中,
這里說句題外話,我遇到過這樣一個問題:我一開始為RGBTriple定義了無參、有參建構式,可以用B、G、R三個引數創建新的RGBTriple物件,接著寫了這些代碼:
for (int i = fileInFoHeader.biHeight-1;i >=0;i--) {
int ured=0,ublue=0,ugreen=0;
for (int j = 0;j < fileInFoHeader.biWidth;j++) {
ifs.read((char*)(&ured), sizeof(char));
ifs.read((char*)(&ugreen), sizeof(char));
ifs.read((char*)(&ublue), sizeof(char));
RGBTriple rgb(ublue,ugreen,ured);
*(surface+(fileInFoHeader.biWidth * i + j))=rgb;
}
if (offset != 0) {
char ign;
for (int k = 0;k < offset;k++) {
ifs.read(&ign,sizeof(char));
}
}
}
這是一個不太好發現的錯誤,細心的小伙伴可能已經看出,紅色與藍色的讀取順序反了,這導致輸出圖片的顏色整體有些偏差,就像這樣(我其實也可以把它叫做藝術品?):

題外話結束...
我們直接一次性地從檔案中讀出3個位元組,存入surface陣列的某個元素中,
for (int i = fileInFoHeader.biHeight-1;i >=0;i--) {
for (int j = 0;j < fileInFoHeader.biWidth;j++) {
ifs.read((char*)(surface+(fileInFoHeader.biWidth * i + j)), sizeof(RGBTriple));
}
if (offset != 0) {
char ign;
for (int k = 0;k < offset;k++) {
ifs.read(&ign,sizeof(char));
}
}
}
內層for回圈結束代表一行像素讀取完畢,此時就要注意空隙問題了,有空隙時,必須跳過空隙,如何跳過空隙呢?定義一個臨時變數ign(ignore的簡寫),把offset個字符回圈讀入ign,最終ign被丟棄,這樣就把用于補齊的0讀走了,再讀取下一行時,讀取檔案的指標就已經指到了真正的資料上,
完整代碼如下:
void bmp::readPic(const char* fileName) {
ifstream ifs;
ifs.open(fileName, ios::in|ios::binary);
if (!ifs.is_open()) {
cout << "Can't open the file." << endl;
return;
}
ifs.read((char*)&fileHeader, sizeof(BmpFileHeader));
if (fileHeader.bfType != BMPTYPE) {
cout << "type error!" << endl;
return;
}
ifs.read((char*)&fileInFoHeader, sizeof(BmpFileInFoHeader));
if (fileInFoHeader.biBitCount != 24) {
cout << "invalid!" << endl;
return;
}
offset = (fileInFoHeader.biWidth * 3) % 4;
if (offset != 0) {
offset = 4 - offset;
}
surface = new RGBTriple[fileInFoHeader.biHeight * fileInFoHeader.biWidth];
for (int i = fileInFoHeader.biHeight-1;i >=0;i--) {
for (int j = 0;j < fileInFoHeader.biWidth;j++) {
ifs.read((char*)(surface+(fileInFoHeader.biWidth * i + j)), sizeof(RGBTriple));
}
if (offset != 0) {
char ign;
for (int k = 0;k < offset;k++) {
ifs.read(&ign,sizeof(char));
}
}
}
ifs.close();
}
5.寫檔案
寫檔案比讀檔案容易,需要使用ofstream物件的write()函式,
用于影像修飾的函式(myMethod)是由函式指標傳入的,這個函式指標的型別是:回傳值void;引數串列:int height,int width,RGBTriple* (實質上是陣列的首地址),
接下來的操作和讀檔案大致相同,只是有幾個注意點:
- open()時必須寫ios::binary,否則也會產生錯誤,這種錯誤與讀檔案時又不同了,并不是輸出烏黑的圖片,而是有一種別樣的顏色濾鏡效果,擺出圖片(不得不說,還挺有藝術感?):

- 從surface的height-1索引開始取出資料用于寫入,因為我們讀檔案時,height-1這里存放的是左下角的像素,我們也應該從左下角的像素開始寫才能輸出正向的圖片,
- 寫完檔案之后,surface陣列完成了它的使命,應該被delete掉,因為new的是一個陣列,所以應該使用delete[],以完全釋放記憶體,
完整代碼如下:
void bmp::writePic(void (*myMethod)(int,int,RGBTriple*),const char* outFileName) {
//modify
myMethod(fileInFoHeader.biHeight, fileInFoHeader.biWidth, surface);
//create a new bmp
ofstream ofs;
ofs.open(outFileName, ios::out|ios::binary);
ofs.write((char*)&fileHeader, sizeof(BmpFileHeader));
ofs.write((char*)&fileInFoHeader, sizeof(BmpFileInFoHeader));
//rewrite
for (int i = fileInFoHeader.biHeight - 1;i >= 0;i--) {
for (int j = 0;j < fileInFoHeader.biWidth;j++) {
ofs.write((char*)(surface + (i*fileInFoHeader.biWidth+j)), sizeof(RGBTriple));
}
if (offset != 0) {
char ign=0;
for (int k = 0;k < offset;k++) {
ofs.write(&ign, sizeof(char));
}
}
}
delete[] surface;
ofs.close();
}
6.修飾圖片
本質上是對當前物件里的surface陣列進行原地修改,
新建一個頭檔案helpers.h,給出了四個函式的宣告,新建源程式檔案helpers.cpp提供函式實作,
helpers.h的結構:
#pragma once
#include<iostream>
#include"RGBTriple.h"
using namespace std;
void makeGray(int height , int width , RGBTriple* image);
void makeSpeia(int height, int width, RGBTriple* image);
void rowReverse(int height, int width, RGBTriple* image);
void makeBlur(int height, int width, RGBTriple* image);
helpers.cpp的結構:
#include"helpers.h"
using namespace std;
void makeGray(int height, int width, RGBTriple* image) {
}
void makeSpeia(int height, int width, RGBTriple* image) {
}
void rowReverse(int height, int width, RGBTriple* image) {
}
void makeBlur(int height, int width, RGBTriple* image) {
}
(1).灰度化
原理:取出每個像素的RGB值,三個值求平均數,再將平均數分別賦值給RGB,
void makeGray(int height, int width, RGBTriple* image) {
for (int i = 0;i < height;i++) {
for (int j = 0;j < width;j++) {
int aver = ((image+(i * width + j))->blue+ (image + (i * width + j))->green+ (image + (i * width + j))->red)/3;
(image + (i * width + j))->blue = (image + (i * width + j))->green = (image + (i * width + j))->red = aver;
}
}
}
這個函式的實作相對容易,
(2).棕色濾鏡效果
原理:公式
新Red=原Red*0.393+原Green*0.769+原Blue*0.189;
新Green=原Red*0.349+原Green*0.686+原Blue*0.168;
新Blue=原Red*0.272+原Green*0.534+原Blue*0.131;
這里,隱藏著一個很大的bug!
大家發現,如果對一個白色像素進行操作(255,255,255),會得到(344,306,238),三個顏色值有兩個都已經溢位!
因此需要這么一個邏輯:當檢測到算出的顏色值溢位時,將它重新設定成255.
void makeSpeia(int height, int width, RGBTriple* image) {
int ured = 0 , ugreen=0 , ublue=0;
for (int i = 0;i < height;i++) {
for (int j = 0;j < width;j++) {
ured = ((image + (i * width + j))->red) * 0.393 + ((image + (i * width + j))->green) * 0.769 + ((image + (i * width + j))->blue) * 0.189;
ugreen = ((image + (i * width + j))->red) * 0.349 + ((image + (i * width + j))->green) * 0.686 + ((image + (i * width + j))->blue) * 0.168;
ublue = ((image + (i * width + j))->red) * 0.272 + ((image + (i * width + j))->green) * 0.534 + ((image + (i * width + j))->blue) * 0.131;
if (ured > 255) {
ured = 255;
}
if (ugreen > 255) {
ugreen = 255;
}
if (ublue > 255) {
ublue = 255;
}
(image + (i * width + j))->red = ured;
(image + (i * width + j))->green = ugreen;
(image + (i * width + j))->blue = ublue;
ured = ugreen = ublue = 0;
}
}
}
(3).水平翻轉
原理:外層for遍歷每一行,內層for在行的開頭和結尾定義兩個計數變數,這兩個變數同時向行中心移動,直至它們相等或“錯過”,在每次移動時,交換兩個變數對應像素的R、G、B三個值,
void rowReverse(int height, int width, RGBTriple* image) {
int ured = 0, ugreen = 0, ublue = 0;
for (int i = 0;i < height;i++) {
for (int j = 0,k=width-1;j < k;j++,k--) {
ured = (image + (i * width + j))->red;
(image + (i * width + j))->red = (image + (i * width + k))->red;
(image + (i * width + k))->red = ured;
ugreen = (image + (i * width + j))->green;
(image + (i * width + j))->green = (image + (i * width + k))->green;
(image + (i * width + k))->green = ugreen;
ublue = (image + (i * width + j))->blue;
(image + (i * width + j))->blue = (image + (i * width + k))->blue;
(image + (i * width + k))->blue = ublue;
}
}
}
(4).模糊
原理:


我們對每一個像素進行如此操作之后,每一個像素都對應了自己的一份嶄新的RGB顏色值,得到所有新顏色值之后,用它們依次覆寫掉原來的圖片資料,
最終處理后的圖片如下:

實作上的幾個難點:
- 必須用一個臨時陣列(肯定也在堆中,不然放不下)把新的顏色值存起來,等所有的像素都運算完了,再把臨時陣列中的值統一賦給原陣列,否則,新的顏色值的輸入會影響臨近幾個像素的運算,產生“污染”,統一賦值結束后,記得把臨時陣列delete掉,
- 如何確定3*3網格到底蓋住了幾個像素? 先將目光放在中心格子的左上角那個格子,即image[i-1][j-1],然后看看 i-1 和 j-1 是否越界,本質上說,這兩層for回圈掃描了包括了中心格子在內的9個格子,并統計了有效(在圖片范圍內)的格子數,
以下為完整代碼:
void makeBlur(int height, int width, RGBTriple* image) {
RGBTriple* temp = new RGBTriple[height * width]();
int ured = 0, ugreen=0, ublue = 0;
for (int i = 0;i < height;i++) {
for (int j = 0;j < width;j++) {
//計算實際覆寫的像素數量
int total = 0;
for (int y = i - 1, myCount = 0;myCount < 3;y++,myCount++) {
if (y >= 0 && y < height) {
for (int x = j - 1, myCount1 = 0;myCount1 < 3;x++, myCount1++) {
if (x >= 0 && x < width) {
ured+= (image + (y * width + x))->red;
ugreen += (image + (y * width + x))->green;
ublue += (image + (y * width + x))->blue;
total++;
}
}
}
}
(temp + (i * width + j))->red=(ured/total);
(temp + (i * width + j))->green = (ugreen / total);
(temp + (i * width + j))->blue = (ublue / total);
ured = ugreen = ublue = 0;
}
}
for (int i = 0;i < height;i++) {
for (int j = 0;j < width;j++) {
(image + (i * width + j))->blue = (temp + (i * width + j))->blue;
(image + (i * width + j))->green = (temp + (i * width + j))->green;
(image + (i * width + j))->red = (temp + (i * width + j))->red;
}
}
delete[] temp;
}
7.main函式
我們最后回到一開始的命令列視窗這里:

可以看出,main函式接收了三個引數,分別為:對圖片的修飾方法、原檔案名、新檔案名
命令列傳參
int main(int argc,char* argv[]){
//do something
}
argc:引數總個數
argv:字串陣列,各個元素之間用任意個空白字符隔開
argv[0]是不能被直接讀取的,真正的引數從argv[1]開始依次向后存放,
具體實作
#include <iostream>
#include<cstring>
#include"bmpFile.h"
#include"helpers.h"
using namespace std;
int main(int argc,const char* argv[])
{
for (int i = 1;i < argc;i++) {
cout << argv[i]<<" ";
}
bmp mybmp;
mybmp.readPic(argv[2]);
if (!strcmp(argv[1],"-g")) {
mybmp.writePic(makeGray,argv[3]);
}
else if (!strcmp(argv[1], "-s")) {
mybmp.writePic(makeSpeia, argv[3]);
}
else if (!strcmp(argv[1], "-b")) {
mybmp.writePic(makeBlur, argv[3]);
}
else if (!strcmp(argv[1], "-r")) {
mybmp.writePic(rowReverse, argv[3]);
}
else {
cout << "failed to write!" << endl;
}
return 0;
}
幾個注意點:
- 因為main.cpp中同時包含了helpers.h和bmpFile.h,而它們倆都包含了RGBTriple.h,所以,必須在RGBTriple.h中加上#pragma once,否則鏈接會出錯;
- 不要用==去判斷兩個字串是否相等,因為這只會比較地址值,這里必須用strcmp()函式,
小結:
哈,終于寫完了!這個案例可以幫我們回顧不少學過的知識點,比如指標的運算、檔案流的操作、函式指標、命令列傳參等,也幫助我了解了不少新知識,如bmp檔案的格式和內部存盤方式、記憶體對齊、fstream的read()和write()方法等,
加油,代碼人!
部分參考自:
#Pragma Pack(n)與記憶體分配 pragma pack(push,1)與#pragma pack(1)的區別_Wanda && Aidem -CSDN博客
BMP格式詳解_Tut-CSDN博客_bmp格式
BMP檔案格式詳解(BMP file format)_mjiansun的專欄-CSDN博客_bmp檔案格式
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/349746.html
標籤:其他
上一篇:零基礎怎樣找到好作業
下一篇:OpenCV-Python教程:統計函式~L1、L2、無窮范數、漢明范數(norm,NORM_HAMMING2,NORM_HAMMING)
