對BMP圖像進行5*5的卷積核運算

未完待續(源代碼已完成,因爲作業還沒


結束,因此不便於上傳源代碼,後續會補


上的)....

#在做並行計算的時候有這麼一個作業

從bmp圖片文件中讀取圖像像素數據,使用5×5的卷積核,步長爲1,對該圖像進行卷積運算,MPI並行實現圖像卷積過程。請同學們認真查看附件,明確要求。

要求:

1. 將卷積核卷積後的像素矩陣輸出到文本文檔以供驗證。

2. 計算程序並行部分的運行時間!!在該程序段首尾加入返回時間戳的函數並顯示各個核的運算時間

3. 給出並行部分的整體運行時間並對比2核與4核的並行加速比與效率。

4. 寫一份實驗報告說明實驗思路,實驗過程,創新和優化部分

5. 程序測試環境爲linux

1. 卷積邊緣點時,採用空白點按照0處理,對應於opencv的border_constant模式。

2. 卷積核統一使用5x5高斯卷積核。圖片現場給定。寬度會是32的整數倍。

3. 結果允許與實際結果有絕對值爲1的誤差



卷積核是什麼

卷積是圖像處理常用的方法,給定輸入圖像,在輸出圖像中每一個像素是輸入圖像中一個小區域中像素的加權平均,其中權值由一個函數定義,這個函數稱爲卷積核,
比如說卷積公式:R(u,v)=∑∑G(u-i,v-j)f(i,j) ,其中f爲輸入,G爲卷積核。


步長是什麼

請參考下面文章裏面的示例3


卷積如何計算

以下面的動態圖通俗直觀地來講,底層的虛線矩陣就代表原始矩陣,陰影矩陣就代表卷積核,陰影矩陣每次移動一格,意思就是步長是1,上層的實線有顏色的矩陣就是輸出矩陣,輸出矩陣的每個元素的計算方法就如動態圖所演示的


最好再參考一下這篇博文幫助理解計算原理(重點看後面的圖片)以及填充邊

數字圖像處理:基本算法-卷積和相關

1、像素數量不變full,假定輸入矩陣爲x*x, 卷積核爲m*m,則輸出矩陣爲(x-3+1+(3-1))*(x-3+1+(3-1)),也就是x*x


2、假定輸入矩陣爲x*x, 假定輸入矩陣爲x*x, 卷積核爲m*m,則輸出矩陣爲(x-3+1+(3-2))*(x-3+1+(3-2)),也就是(x-1)*(x-1)

........

m、假定輸入矩陣爲x*x, 假定輸入矩陣爲x*x, 卷積核爲m*m(3*3爲例),則輸出矩陣爲(x-3+1+(3-3))*(x-3+1+(3-3)),也就是(x-2)*(x-2)



高斯卷積核的生成(C/C++代碼)

高斯公式如下

    

C++實現生成高斯核的代碼

#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
void gen_gs() {
	int i, j;
	double sigma = 1;
	const int N = 5;
	double gussian[N][N];
	double sum = 0.0;

	for (i = 0; i<N; i++)
	{
		for (j = 0; j<N; j++)
		{
			gussian[i][j] = exp(-((i - N / 2)*(i - N / 2) + (j - N / 2)*(j - N / 2)) / (2.0*sigma*sigma));
			sum += gussian[i][j];
		}
	}
	FILE *fp;
	fp = fopen("gs.txt", "w");
	for (i = 0; i<N; i++)
	{
		for (j = 0; j<N; j++)
		{
			gussian[i][j] /= sum;
			fprintf(fp,"%f ", gussian[i][j]);
		}
		fprintf(fp,"\n");
	}
}
int main() {
	gen_gs();
	return 0;
}

計算優化

用兩個級聯的3*3的卷積核來代替一個5*5的卷積核。(7*7的可以換成3重的3*3的)

具體的原理可以參考下面的知乎問答。

爲什麼一個5*5的卷積核可以用兩個3*3的卷積核代替,一個7*7的卷積核可以用三個的3*3卷積核代替?

(建議初學者瞭解一下變形卷積核、可分離卷積?卷積神經網絡中十大拍案叫絕的操作。這裏對卷積的原理及其發展做了非常清楚的介紹)

使用一個5*5卷積核和兩個級聯的3*3卷積核的參數量和計算量的對比

參數對比

參數個數僅和卷積核大小相關

 5*5兩個級聯的3*3
參數個數對比5*5+1=26    (3*3+1)*2=20
  更少參數

計算量對比

輸入記爲x,爲了方便討論假設padding=0,stride=1。此時卷積計算公式 output =( input – kernel + 2padding) / stride + 1簡化爲output = input – kernel + 1。

  • 5*5卷積:有(x-5+1)* (x-5+1)個輸出點,每個輸出點對應5*5次乘法和5*5次加法(5*5次乘法的結果求和再加上b,一共5*5+1個數相加,所以需要5*5次加法)
  • 3*3卷積:第一個3*3卷積有(x-3+1)*(x-3+1)個輸出點,每個輸出點對應3*3次乘法和3*3次加法,第二個3*3卷積的輸入是(x-3+1)*(x-3+1),在其上做卷積有(x-3+1 -3+1)* (x-3+1-3+1)個輸出點,每個輸出點對應3*3次乘法和3*3次加法。


綜上,當x<22/7 或者10<x ,兩個3*3的卷積核在參數個數和計算量上都佔優勢。

在計算卷積的時候,特別是我這裏是用來做BMP圖像的卷積計算,x的值一般比較大,所以選擇用兩個級聯的3*3的卷積核來代替5*5的卷積核

https://www.cnblogs.com/hejunlin1992/p/7624807.html



下面預計要寫的東西有:

MPI並行編程(這個需要,但是不打算寫了,因爲主要是個人學習筆記,這部分已經學習過了,但更重要的是覺得自己並不足以寫出比網上教程更好的介紹,就略了)

2018-4-30續

參考bmp文件格式解析BMP文件格式詳解

BITMAPFILEHEADER fileHead;
fileHead.bfType = 0x4D42;//bmp類型

			//bfSize是圖像文件4個組成部分之和
fileHead.bfSize = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + colorTablesize + lineByte*height;

上面是BMP圖像的C++代碼格式的表示,可以看到一幅bmp圖像分爲:文件頭、信息頭、顏色表(僅灰度圖像有)、像素值4個部分,我們最終需要讀取的就是第4部分的數據信息。前面的BITMAPFILEHEADER和BITMAPINFOHEADER這是兩個系統定義的結構體。結構體的定義我也順便貼出來吧:

typedef struct tagBITMAPFILEHEADER {
        WORD    bfType;
        DWORD   bfSize;
        WORD    bfReserved1;
        WORD    bfReserved2;
        DWORD   bfOffBits;
} BITMAPFILEHEADER, FAR *LPBITMAPFILEHEADER, *PBITMAPFILEHEADER;
typedef struct tagBITMAPINFOHEADER{
        DWORD      biSize;
        LONG       biWidth;
        LONG       biHeight;
        WORD       biPlanes;
        WORD       biBitCount;
        DWORD      biCompression;
        DWORD      biSizeImage;
        LONG       biXPelsPerMeter;
        LONG       biYPelsPerMeter;
        DWORD      biClrUsed;
        DWORD      biClrImportant;
} BITMAPINFOHEADER, FAR *LPBITMAPINFOHEADER, *PBITMAPINFOHEADER;

下面是讀取BMP圖像並輸出像素數據到文件,另外再將同一張圖片的各個部分輸出形成原圖的一份拷貝的演示代碼,以供參考

#define _CRT_SECURE_NO_WARNINGS

#include<math.h>
#include <iomanip> 
#include <stdlib.h>
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <iostream>
#include <fstream>
using namespace std;
//---------------------------------------------------------------------------------------
//以下該模塊是完成BMP圖像(彩色圖像是24bit RGB各8bit)的像素獲取,並存在文件名爲xiang_su_zhi.txt中
unsigned char *pBmpBuf;//讀入圖像數據的指針

int bmpWidth;//圖像的寬
int bmpHeight;//圖像的高
RGBQUAD *pColorTable;//顏色表指針

int biBitCount;//圖像類型,每像素位數 8-灰度圖 24-彩色圖

			   //-------------------------------------------------------------------------------------------
			   //讀圖像的位圖數據、寬、高、顏色表及每像素位數等數據進內存,存放在相應的全局變量中
bool readBmp(char *bmpName)
{
	FILE *fp = fopen(bmpName, "rb");//二進制讀方式打開指定的圖像文件

	if (fp == 0)
		return 0;

	//跳過位圖文件頭結構BITMAPFILEHEADER

	fseek(fp, sizeof(BITMAPFILEHEADER), 0);

	//定義位圖信息頭結構變量,讀取位圖信息頭進內存,存放在變量head中

	BITMAPINFOHEADER head;

	fread(&head, sizeof(BITMAPINFOHEADER), 1, fp); //獲取圖像寬、高、每像素所佔位數等信息

	bmpWidth = head.biWidth;	//寬度用來計算每行像素的字節數

	bmpHeight = head.biHeight;  // 像素的行數

	biBitCount = head.biBitCount;//定義變量,計算圖像每行像素所佔的字節數(必須是4的倍數)
	int lineByte = (bmpWidth * biBitCount / 8 + 3) / 4 * 4;//灰度圖像有顏色表,且顏色表表項爲256 (可以理解爲lineByte是對bmpWidth的以4爲步長的向上取整)

	if (biBitCount == 8)
	{
		//申請顏色表所需要的空間,讀顏色表進內存
		pColorTable = new RGBQUAD[256];
		fread(pColorTable, sizeof(RGBQUAD), 256, fp);
	}

	//申請位圖數據所需要的空間,讀位圖數據進內存

	pBmpBuf = new unsigned char[lineByte * bmpHeight];
	cout << "lineByte" << lineByte << " bmpHeight" << bmpHeight << " bibitCount"<<biBitCount << endl;
	fread(pBmpBuf, 1, lineByte * bmpHeight, fp);

	fclose(fp);//關閉文件
	return 1;//讀取文件成功
}

//-----------------------------------------------------------------------------------------
//給定一個圖像位圖數據、寬、高、顏色表指針及每像素所佔的位數等信息,將其寫到指定文件中
bool saveBmp(char *bmpName, unsigned char *imgBuf, int width, int height, int biBitCount, RGBQUAD *pColorTable)
{

	//如果位圖數據指針爲0,則沒有數據傳入,函數返回

	if (!imgBuf)
		return 0;

	//顏色表大小,以字節爲單位,灰度圖像顏色表爲1024字節,彩色圖像顏色表大小爲0

	int colorTablesize = 0;

	if (biBitCount == 8)
		colorTablesize = 1024;//8*128

	//待存儲圖像數據每行字節數爲4的倍數

	int lineByte = (width * biBitCount / 8 + 3) / 4 * 4;

	//以二進制寫的方式打開文件

	FILE *fp = fopen(bmpName, "wb");

	if (fp == 0)
		return 0;

	//申請位圖文件頭結構變量,填寫文件頭信息

	BITMAPFILEHEADER fileHead;

	fileHead.bfType = 0x4D42;//bmp類型

							 //bfSize是圖像文件4個組成部分之和阿

	fileHead.bfSize = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + colorTablesize + lineByte*height;

	fileHead.bfReserved1 = 0;

	fileHead.bfReserved2 = 0;

	//bfOffBits是圖像文件前3個部分所需空間之和

	fileHead.bfOffBits = 54 + colorTablesize;

	//寫文件頭進文件

	fwrite(&fileHead, sizeof(BITMAPFILEHEADER), 1, fp);

	//申請位圖信息頭結構變量,填寫信息頭信息

	BITMAPINFOHEADER head;

	head.biBitCount = biBitCount;

	head.biClrImportant = 0;

	head.biClrUsed = 0;

	head.biCompression = 0;

	head.biHeight = height;

	head.biPlanes = 1;

	head.biSize = 40;

	head.biSizeImage = lineByte*height;

	head.biWidth = width;

	head.biXPelsPerMeter = 0;

	head.biYPelsPerMeter = 0;

	//寫位圖信息頭進內存

	fwrite(&head, sizeof(BITMAPINFOHEADER), 1, fp);

	//如果灰度圖像,有顏色表,寫入文件 

	if (biBitCount == 8)
		fwrite(pColorTable, sizeof(RGBQUAD), 256, fp);

	//寫位圖數據進文件

	fwrite(imgBuf, height*lineByte, 1, fp);

	//關閉文件

	fclose(fp);

	return 1;

}

//----------------------------------------------------------------------------------------
//以下爲像素的讀取函數
void doIt()
{

	//讀入指定BMP文件進內存

	char readPath[] = "nx.BMP";

	readBmp(readPath);

	//輸出圖像的信息

	cout << "width=" << bmpWidth << " height=" << bmpHeight << " biBitCount=" << biBitCount << endl;

	//循環變量,圖像的座標

	//每行字節數

	int lineByte = (bmpWidth*biBitCount / 8 + 3) / 4 * 4;

	//循環變量,針對彩色圖像,遍歷每像素的三個分量

	int m = 0, n = 0, count_xiang_su = 0;

	//將圖像左下角1/4部分置成黑色

	ofstream outfile("圖像像素.txt", ios::in | ios::trunc);

	if (biBitCount == 8) //對於灰度圖像
	{
		//------------------------------------------------------------------------------------
		//以下完成圖像的分割成8*8小單元,並把像素值存儲到指定文本中。由於BMP圖像的像素數據是從
		//左下角:由左往右,由上往下逐行掃描的
		int L1 = 0;
		int hang = 63;
		int lie = 0;
		//int L2=0;
		//int fen_ge=8;
		for (int fen_ge_hang = 0; fen_ge_hang<8; fen_ge_hang++)//64*64矩陣行循環
		{
			for (int fen_ge_lie = 0; fen_ge_lie<8; fen_ge_lie++)//64*64列矩陣循環
			{
				//--------------------------------------------
				for (L1 = hang; L1>hang - 8; L1--)//8*8矩陣行
				{
					for (int L2 = lie; L2<lie + 8; L2++)//8*8矩陣列
					{
						m = *(pBmpBuf + L1*lineByte + L2);
						outfile << m << " ";
						count_xiang_su++;
						if (count_xiang_su % 8 == 0)//每8*8矩陣讀入文本文件
						{
							outfile << endl;
						}
					}
				}
				//---------------------------------------------
				hang = 63 - fen_ge_hang * 8;//64*64矩陣行變換
				lie += 8;//64*64矩陣列變換
						 //該一行(64)由8個8*8矩陣的行組成
			}
			hang -= 8;//64*64矩陣的列變換
			lie = 0;//64*64juzhen
		}
	}

	//double xiang_su[2048];
	//ofstream outfile("xiang_su_zhi.txt",ios::in|ios::trunc);
	if (!outfile)
	{
		cout << "open error!" << endl;
		exit(1);
	}
	else if (biBitCount == 24)
	{//彩色圖像
		for (int i = 0; i<bmpHeight; i++)
		{
			for (int j = 0; j<bmpWidth; j++)
			{
				for (int k = 0; k<3; k++)//每像素RGB三個分量分別置0才變成黑色
				{
					//*(pBmpBuf+i*lineByte+j*3+k)-=40;
					m = *(pBmpBuf + i*lineByte + j * 3 + k);
					outfile << m << " ";
					count_xiang_su++;
					if (count_xiang_su % 8 == 0)
					{
						outfile << endl;
					}
					//n++;
				}
				n++;
			}


		}
	}
	cout << "總的像素個素爲:" << count_xiang_su << endl;
	cout << "----------------------------------------------------" << endl;
	//將圖像數據存盤

	char writePath[] = "nvcpy.BMP";//圖片處理後再存儲

	saveBmp(writePath, pBmpBuf, bmpWidth, bmpHeight, biBitCount, pColorTable);

	//清除緩衝區,pBmpBuf和pColorTable是全局變量,在文件讀入時申請的空間

	delete[]pBmpBuf;

	if (biBitCount == 8)
		delete[]pColorTable;
}

void main()
{
	doIt();
}


可能遇到的問題

黑線的問題:是因爲分發數據的時候導致的,要得到x行的結果,分發的時候就必須有x+m-1行的原始數據(m代表卷積核的規格,不足部分使用0填充)


部分優化及分析

優化一

首先聲明:這裏我一定要說清楚,貼出我的一段錯誤代碼,但是主要是優化的思路及分析(個人感覺這個思路比較有mark的必要)。錯誤的代碼分析完了之後會附上正確的代碼

在計算行座標時每計算一個像素就會多出3*5*5次加法和3*5*5次乘法,這樣總的計算次數就會多出3*5*5*height*width次加法和乘法

同樣的列座標多出3*5*5*height*width次加法

如果只是將3次計算統一成一次計算,那隻會將多餘的計算量減少爲原來的1/3.而事實上注意到在卷積的過程中這個行座標的計算在不同像素之間計算時也是有很多的重疊部分(同一行的像素)因此考慮對同一行多餘計算進行統一。

改進如下

對行座標的多餘計算總共只需要height次乘法和height*width*5次加法。

對列座標的多餘計算總共只需要5*5*height*width。

共計減少了(3*5*5* width- 1)* height次乘法

共計減少了(2*3*5-1-5)*5*height*width次加法,

實際上必須的乘法和加法爲3*5*5*height*width次,

必須的加法爲3*(5*5-1)* height*width次,

這樣優化之後,相當於把程序做的乘法次數減少了將近50%,加法次數減少到原來的46%(我在計算的時候把浮點乘和整數乘的時間看做相等來進行計算,記得去年做矩陣乘的時候這兩個相乘的時間好像就是差不太多)

綜上,經過這步優化,程序的計算效率大約提升爲優化前的2倍。

下面是正確的代碼(其實優化效果沒有上面錯誤的那麼明顯,僅僅是減少了一些加法而已)



優化二:考慮cache的緩存數據更新的資源消耗

優化方法:改變循環的嵌套次序

優化前

優化後

這一步優化可以很明顯看出不會對計算結果產生影響,但是在計算的順序上有了很大的變化。

line=i+a放在外層循環,lie=j+b放在內層循環,很容易理解,先對一行進行計算,然後對下一行進行計算,

優化的原理是cache有一個預取技術,假如你去了R[0][0]位置的元素,它就會預測你可能還會使用臨近的R[0][1]以及更多的元素,然後會幫你把它一起取到緩存,而下一個要用的正好就是這個,所以直接在緩存中就可以找到,節省了存取時間,利用這個原理,以及矩陣的順序存儲的特性,我們計算完一行之後,再去計算下一行,而不是像原來那樣一個卷積核一個卷積核地計算,因爲那樣會有5行的跨度,極大地浪費了緩存的資源。




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章