主頁 >  其他 > 純C++實作24位bmp格式圖片的讀取和修飾

純C++實作24位bmp格式圖片的讀取和修飾

2021-11-06 09:38:03 其他

問題:現有一張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部分組成:

  1. 檔案頭
  2. 檔案資訊頭
  3. 調色板(24位位圖無)
  4. 影像顏色資訊

在此只討論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>影像顏色資訊

  1. 像素的存盤順序是從下到上,從左到右,在檔案中以類似一維陣列的方法線性存盤,
  2. 每個像素的顏色資訊每3個位元組一組,按BGR的順序存放,
  3. 其中每個位元組只存一個顏色值,顏色值范圍是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)

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 網閘典型架構簡述

    網閘架構一般分為兩種:三主機的三系統架構網閘和雙主機的2+1架構網閘。 三主機架構分別為內端機、外端機和仲裁機。三機無論從軟體和硬體上均各自獨立。首先從硬體上來看,三機都用各自獨立的主板、記憶體及存盤設備。從軟體上來看,三機有各自獨立的作業系統。這樣能達到完全的三機獨立。對于“2+1”系統,“2”分為 ......

    uj5u.com 2020-09-10 02:00:44 more
  • 如何從xshell上傳檔案到centos linux虛擬機里

    如何從xshell上傳檔案到centos linux虛擬機里及:虛擬機CentOs下執行 yum -y install lrzsz命令,出現錯誤:鏡像無法找到軟體包 前言 一、安裝lrzsz步驟 二、上傳檔案 三、遇到的問題及解決方案 總結 前言 提示:其實很簡單,往虛擬機上安裝一個上傳檔案的工具 ......

    uj5u.com 2020-09-10 02:00:47 more
  • 一、SQLMAP入門

    一、SQLMAP入門 1、判斷是否存在注入 sqlmap.py -u 網址/id=1 id=1不可缺少。當注入點后面的引數大于兩個時。需要加雙引號, sqlmap.py -u "網址/id=1&uid=1" 2、判斷文本中的請求是否存在注入 從文本中加載http請求,SQLMAP可以從一個文本檔案中 ......

    uj5u.com 2020-09-10 02:00:50 more
  • Metasploit 簡單使用教程

    metasploit 簡單使用教程 浩先生, 2020-08-28 16:18:25 分類專欄: kail 網路安全 linux 文章標簽: linux資訊安全 編輯 著作權 metasploit 使用教程 前言 一、Metasploit是什么? 二、準備作業 三、具體步驟 前言 Msfconsole ......

    uj5u.com 2020-09-10 02:00:53 more
  • 游戲逆向之驅動層與用戶層通訊

    驅動層代碼: #pragma once #include <ntifs.h> #define add_code CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS) /* 更多游戲逆向視頻www.yxfzedu.com ......

    uj5u.com 2020-09-10 02:00:56 more
  • 北斗電力時鐘(北斗授時服務器)讓網路資料更精準

    北斗電力時鐘(北斗授時服務器)讓網路資料更精準 北斗電力時鐘(北斗授時服務器)讓網路資料更精準 京準電子科技官微——ahjzsz 近幾年,資訊技術的得了快速發展,互聯網在逐漸普及,其在人們生活和生產中都得到了廣泛應用,并且取得了不錯的應用效果。計算機網路資訊在電力系統中的應用,一方面使電力系統的運行 ......

    uj5u.com 2020-09-10 02:01:03 more
  • 【CTF】CTFHub 技能樹 彩蛋 writeup

    ?碎碎念 CTFHub:https://www.ctfhub.com/ 筆者入門CTF時時剛開始刷的是bugku的舊平臺,后來才有了CTFHub。 感覺不論是網頁UI設計,還是題目質量,賽事跟蹤,工具軟體都做得很不錯。 而且因為獨到的金幣制度的確讓人有一種想去刷題賺金幣的感覺。 個人還是非常喜歡這個 ......

    uj5u.com 2020-09-10 02:04:05 more
  • 02windows基礎操作

    我學到了一下幾點 Windows系統目錄結構與滲透的作用 常見Windows的服務詳解 Windows埠詳解 常用的Windows注冊表詳解 hacker DOS命令詳解(net user / type /md /rd/ dir /cd /net use copy、批處理 等) 利用dos命令制作 ......

    uj5u.com 2020-09-10 02:04:18 more
  • 03.Linux基礎操作

    我學到了以下幾點 01Linux系統介紹02系統安裝,密碼啊破解03Linux常用命令04LAMP 01LINUX windows: win03 8 12 16 19 配置不繁瑣 Linux:redhat,centos(紅帽社區版),Ubuntu server,suse unix:金融機構,證券,銀 ......

    uj5u.com 2020-09-10 02:04:30 more
  • 05HTML

    01HTML介紹 02頭部標簽講解03基礎標簽講解04表單標簽講解 HTML前段語言 js1.了解代碼2.根據代碼 懂得挖掘漏洞 (POST注入/XSS漏洞上傳)3.黑帽seo 白帽seo 客戶網站被黑帽植入劫持代碼如何處理4.熟悉html表單 <html><head><title>TDK標題,描述 ......

    uj5u.com 2020-09-10 02:04:36 more
最新发布
  • 2023年最新微信小程式抓包教程

    01 開門見山 隔一個月發一篇文章,不過分。 首先回顧一下《微信系結手機號資料庫被脫庫事件》,我也是第一時間得知了這個訊息,然后跟蹤了整件事情的經過。下面是這起事件的相關截圖以及近日流出的一萬條資料樣本: 個人認為這件事也沒什么,還不如關注一下之前45億快遞資料查詢渠道疑似在近日復活的訊息。 訊息是 ......

    uj5u.com 2023-04-20 08:48:24 more
  • web3 產品介紹:metamask 錢包 使用最多的瀏覽器插件錢包

    Metamask錢包是一種基于區塊鏈技術的數字貨幣錢包,它允許用戶在安全、便捷的環境下管理自己的加密資產。Metamask錢包是以太坊生態系統中最流行的錢包之一,它具有易于使用、安全性高和功能強大等優點。 本文將詳細介紹Metamask錢包的功能和使用方法。 一、 Metamask錢包的功能 數字資 ......

    uj5u.com 2023-04-20 08:47:46 more
  • vulnhub_Earth

    前言 靶機地址->>>vulnhub_Earth 攻擊機ip:192.168.20.121 靶機ip:192.168.20.122 參考文章 https://www.cnblogs.com/Jing-X/archive/2022/04/03/16097695.html https://www.cnb ......

    uj5u.com 2023-04-20 07:46:20 more
  • 從4k到42k,軟體測驗工程師的漲薪史,給我看哭了

    清明節一過,盲猜大家已經無心上班,在數著日子準備過五一,但一想到銀行卡里的余額……瞬間心情就不美麗了。最近,2023年高校畢業生就業調查顯示,本科畢業月平均起薪為5825元。調查一出,便有很多同學表示自己又被平均了。看著這一資料,不免讓人想到前不久中國青年報的一項調查:近六成大學生認為畢業10年內會 ......

    uj5u.com 2023-04-20 07:44:00 more
  • 最新版本 Stable Diffusion 開源 AI 繪畫工具之中文自動提詞篇

    🎈 標簽生成器 由于輸入正向提示詞 prompt 和反向提示詞 negative prompt 都是使用英文,所以對學習母語的我們非常不友好 使用網址:https://tinygeeker.github.io/p/ai-prompt-generator 這個網址是為了讓大家在使用 AI 繪畫的時候 ......

    uj5u.com 2023-04-20 07:43:36 more
  • 漫談前端自動化測驗演進之路及測驗工具分析

    隨著前端技術的不斷發展和應用程式的日益復雜,前端自動化測驗也在不斷演進。隨著 Web 應用程式變得越來越復雜,自動化測驗的需求也越來越高。如今,自動化測驗已經成為 Web 應用程式開發程序中不可或缺的一部分,它們可以幫助開發人員更快地發現和修復錯誤,提高應用程式的性能和可靠性。 ......

    uj5u.com 2023-04-20 07:43:16 more
  • CANN開發實踐:4個DVPP記憶體問題的典型案例解讀

    摘要:由于DVPP媒體資料處理功能對存放輸入、輸出資料的記憶體有更高的要求(例如,記憶體首地址128位元組對齊),因此需呼叫專用的記憶體申請介面,那么本期就分享幾個關于DVPP記憶體問題的典型案例,并給出原因分析及解決方法。 本文分享自華為云社區《FAQ_DVPP記憶體問題案例》,作者:昇騰CANN。 DVPP ......

    uj5u.com 2023-04-20 07:43:03 more
  • msf學習

    msf學習 以kali自帶的msf為例 一、msf核心模塊與功能 msf模塊都放在/usr/share/metasploit-framework/modules目錄下 1、auxiliary 輔助模塊,輔助滲透(埠掃描、登錄密碼爆破、漏洞驗證等) 2、encoders 編碼器模塊,主要包含各種編碼 ......

    uj5u.com 2023-04-20 07:42:59 more
  • Halcon軟體安裝與界面簡介

    1. 下載Halcon17版本到到本地 2. 雙擊安裝包后 3. 步驟如下 1.2 Halcon軟體安裝 界面分為四大塊 1. Halcon的五個助手 1) 影像采集助手:與相機連接,設定相機引數,采集影像 2) 標定助手:九點標定或是其它的標定,生成標定檔案及內參外參,可以將像素單位轉換為長度單位 ......

    uj5u.com 2023-04-20 07:42:17 more
  • 在MacOS下使用Unity3D開發游戲

    第一次發博客,先發一下我的游戲開發環境吧。 去年2月份買了一臺MacBookPro2021 M1pro(以下簡稱mbp),這一年來一直在用mbp開發游戲。我大致分享一下我的開發工具以及使用體驗。 1、Unity 官網鏈接: https://unity.cn/releases 我一般使用的Apple ......

    uj5u.com 2023-04-20 07:40:19 more