摘要
我們在圖像處理時經常會用到遍歷圖像像素點的方式,在OpenCV中一般有四種圖像遍歷的方式,在這里我們通過像素變換的點操作來實現對圖像亮度和對比度的調整。
數據格式千萬不要搞錯:
uchar對應的是CV_8U,char對應的是CV_8S,int對應的是CV_32S,float對應的是CV_32F,double對應的是CV_64F。
補充: 圖像變換可以看成
- 像素變換——點操作
- 鄰域變換——區域操作(卷積,特征提取,梯度計算等)
對於點操作:
q(i,j)=αf(i,j)+β
其中f(i,j)是輸入點像素值,q(i,j)是輸出點像素值。
1,數組遍歷-- at<typename>(i,j)
說明:就是把圖像看成二維矩陣,at(i,j)索引坐標位置,單通道直接得到坐標位置對應的像素值,三通道就這個位置代表了像素值的一維數組;
Mat類提供了一個at的方法用於取得圖像上的點,它是一個模板函數,可以取到任何類型的圖像上的點。這里選用參數α=1.5,β=0.5來提高圖像亮度。
int main(int argc, char** argv) { Mat src; src = imread("D:/opencv練習圖片/薛之謙.jpg"); imshow("Image", src); //創建一個和原圖一致的空白圖像 Mat dst = Mat::zeros(src.size(), src.type()); for (int i = 0; i < src.rows; i++) { for (int j = 0; j < src.cols; j++) { if (src.channels() == 1) //單通道遍歷 { dst.at<uchar>(i, j) = src.at<uchar>(i, j) + 100; } else if (src.channels() ==3)//三通道遍歷 { //通過數組遍歷獲取圖像每個點 float b = src.at<Vec3b>(i, j)[0]; float g = src.at<Vec3b>(i, j)[1]; float r = src.at<Vec3b>(i, j)[2]; //進行點操作后賦值給空白圖像dst float alpha = 1.5; float beta = 0.5; dst.at<Vec3b>(i, j)[0] = saturate_cast<uchar>(b*alpha + beta); dst.at<Vec3b>(i, j)[1] = saturate_cast<uchar>(g*alpha + beta); dst.at<Vec3b>(i, j)[2] = saturate_cast<uchar>(r*alpha + beta); } } } imshow("點操作", dst); waitKey(0); return 0; }
saturate_cast<uchar>是溢出保護,在進行像素的乘法后很容易造成像素點的值超出0-255的范圍,因此使用saturate_cast<uchar>確保像素值始終在0-255的范圍內。
2,指針遍歷法
OpenCV中cv::Mat類提供了成員函數ptr得到圖像任意行的首地址。ptr函數是一個模板函數,如:src.ptr<uchar>(i)
說明:ptr指針尤其固定格式,就是先把圖像看成(src.rows,1)的圖像,ptr獲取每個位置的地址,地址位置隱藏了列的數據,由於列表名就是列表的地址,所以ptr獲取的地址就是此行中列這樣一維數據的列表名稱。這樣通過下標就可以獲取像素值
int main(int argc, char** argv) { Mat src; src = imread("D:/opencv練習圖片/薛之謙.jpg"); imshow("Image", src); //創建一個和原圖一致的空白圖像 Mat dst = Mat::zeros(src.size(), src.type()); int width ; //判斷圖像是否連續 if (src.isContinuous() && dst.isContinuous()) { // 將3通道轉換為1通道 width = src.cols * src.channels(); } for (int i = 0; i < src.rows; i++) { // 獲取第i行的首地址 const uchar* src_rows = src.ptr<uchar>(i); uchar* dst_ptr = dst.ptr<uchar>(i); //像素點操作處理 for (int j = 0; j < width; j++) { dst_ptr[j] = saturate_cast<uchar>(src_rows[j] *1.5 + 0.5); dst_ptr[j + 1] = saturate_cast<uchar>(src_rows[j + 1] *1.5 + 0.5); dst_ptr[j + 2] = saturate_cast<uchar>(src_rows[j + 2] *1.5 + 0.5); } } imshow("點操作", dst); waitKey(0); return 0; }
程序中將三通道的數據轉換為1通道,是建立在每一行數據元素之間在內存里是連續存儲的。但在opencv中由於的存儲機制問題,行與行之間可能有空白單元,因此Mat提供了一個檢測圖像是否連續的函數isContinuous(),當圖像連通時,我們就可以把圖像完全展開,看成是一行。
針對at和ptr有很多人容易理解at,卻理解不了ptr,下面講一個用at生成ptr模式的解析例子:
說明:這是為了對比at和ptr而增加的,主要是獲取at(i,0)位置處的地址,將其看成數值名稱,通過下標索引像素值,和ptr原理一樣,只是獲取地址的方式不一樣(數組名)
1️⃣遍歷灰度圖像像素方法:(采用at方法,使用ptr模式)
for (int i = 0; i < src.rows; i++) { //將灰度圖片看成(src.rows,1)維度的二維矩陣,獲取(i,0)數據的地址 uchar* src_rows_ptr = &(src.at<uchar>(i, 0)); uchar* dst_rows_ptr = &(dst.at<uchar>(i, 0)); for (int j = 0; j < src.cols; j++) { //將(i,0)數據的地址下的內容看成是一維數組,(i,0)數據的地址是一維數組的名字 dst_rows_ptr[j] = src_rows_ptr[j] + 100; } }
2️⃣遍歷彩色圖像像素方法:(采用at方法,使用ptr模式)
for (int i = 0; i < src.rows; i++) { //將彩色圖片看成(src.rows,1)維度的二維矩陣,獲取(i,0)數據的地址 Vec3b* src_rows_ptr = &(src.at<Vec3b>(i, 0)); Vec3b* dst1_rows_ptr = &(dst1.at<Vec3b>(i, 0)); for (int j = 0; j < src.cols; j++) { //將(i,0)數據的地址下的內容看成是二維數組,(i,0)數據的地址是二維數組的名字 dst1_rows_ptr[j][0] = src_rows_ptr[j](0) + 100; dst1_rows_ptr[j][1] = src_rows_ptr[j](1) + 100; dst1_rows_ptr[j][2] = src_rows_ptr[j](2) + 100; } }
綜上所述:使用ptr指針效率非常高,大家普遍使用的是at和ptr方法;使用的時候,一定要規范格式;
其中:at<類型>(i,j)
ptr<類型>(i)
3、迭代器遍歷
迭代器是專門用於遍歷數據集合的一種非常重要的特殊的類,用其遍歷隱藏了在給定集合上元素迭代的具體實現方式。迭代器方法是一種更安全的用來遍歷圖像的方式,首先獲取到數據圖像的矩陣起始,再通過遞增迭代實現移動數據指針。
1、迭代器Matlterator_ Matlterator_是Mat數據操作的迭代器,:begin()表示指向Mat數據的起始迭代器,:end()表示指向Mat數據的終止迭代器。
2、迭代器Mat_ OpenCV定義了一個Mat的模板子類為Mat_,它重載了operator()讓我們可以更方便的取圖像上的點。
int main(int argc, char** argv)
{
Mat src;
src = imread("D:/opencv練習圖片/薛之謙.jpg");
imshow("Image", src);
// 初始化圖像迭代器
Mat_<Vec3b>::iterator it = src.begin<Vec3b>();
Mat_<Vec3b>::iterator itend = src.end<Vec3b>();
while (it != itend)
{
//像素點操作
(*it)[0] = saturate_cast<uchar>((*it)[0]*1.5+0.5);
(*it)[1] = saturate_cast<uchar>((*it)[1] * 1.5 + 0.5);
(*it)[2] = saturate_cast<uchar>((*it)[2] * 1.5 + 0.5);
it++;
}
imshow("點操作", src);
waitKey(0);
return 0;
}
經測試,得到與數組遍歷一樣的效果。
4、核心函數LUT
LUT(LOOK -UP-TABLE)查找表。簡言之:在一幅圖像中,假如我們想將圖像某一灰度值換成其他灰度值,用LUT就很好用。這樣可以起到突出圖像的有用信息,增強圖像的光對比度的作用對某圖像中的像素值進行替換。。
在圖像處理中,對於一個給定的值,將其替換成其他的值是一個很常見的操作,OpenCV 提供里一個函數直接實現該操作LUT函數
函數 API
void LUT(InputArray src, InputArray lut, OutputArray dst); //src表示的是輸入圖像(可以是單通道也可是3通道) //lut表示查找表(查找表也可以是單通道,也可以是3通道; //...如果輸入圖像為單通道,那查找表必須為單通道; //...若輸入圖像為3通道,查找表可以為單通道,也可以為3通道; //...若為單通道則表示對圖像3個通道都應用這個表,若為3通道則分別應用 ) //dst表示輸出圖像
如何使用該函數?
- 首先我們建立一個mat型用於查表
- 然后我們調用函數 (I 是輸入 J 是輸出):
LUT(I, lookUpTable, J);
LUT函數的作用:
(1)改變圖像中像素灰度值
通過構建查找表,圖片0-100灰度的像素灰度就變成0,101-200的變成100,201-255的就變成255。
int main(int argc, char** argv) { Mat src,dst1,dst3; src = imread("D:/opencv練習圖片/薛之謙.jpg"); imshow("Image", src); //查找表,數組的下標對應圖片里面的灰度值 //例如lutData[20]=0;表示灰度為20的像素其對應的值0. uchar lutData[256]; for (int i = 0; i < 256; i++) { if (i <= 100) lutData[i] = 0; if (i > 100 && i <= 200) lutData[i] = 100; if (i > 200) lutData[i] = 255; } Mat lut(1, 256, CV_8UC1, lutData); LUT(src, lut, dst1); imshow("LUC", dst1); waitKey(0); return 0; }
(2)顏色空間縮減
如果矩陣元素存儲的是單通道像素,使用uchar (無符號字符,即0到255之間取值的數)那么像素可有256個不同值。但若是三通道圖像,這種存儲格式的顏色數就是256*256*256個(有一千六百多萬種)。用如此之多的顏色可能會對我們的算法性能造成嚴重影響。其實有時候,僅用這些顏色的一小部分,就足以達到同樣效果。
這種情況下,常用的一種方法是 顏色空間縮減 。其做法是:將現有顏色空間值除以某個輸入值,以獲得較少的顏色數。例如,顏色值0-9的取為0,10-19的取為10,以此類推。就把256個不同值划分為26個,大大減少運算時間。
uchar 類型的值除以 int 值,結果仍是 char 。因為結果是char類型的,所以求出來小數也要向下取整。利用這一點,剛才提到在 uchar 定義域中進行的顏色縮減運算就可以表達為下列形式:
這樣的話,簡單的顏色空間縮減算法就可由下面兩步組成:
一、遍歷圖像矩陣的每一個像素
二、對像素應用上述公式。
下面將圖像壓縮級設置為20(即0-19變為0,20-39變為20…)
int main(int argc, char** argv) { Mat src,dst; src = imread("D:/opencv練習圖片/薛之謙.jpg"); imshow("Image", src); uchar table[256]; Mat lut(1, 256, CV_8U);//創建查找表 int divideWith = 20; //壓縮級 20灰度為1級 for (int i = 0; i < 256; ++i) { table[i] = divideWith * (i / divideWith);//顏色縮減運算 } uchar *p = lut.data; for (int i = 0; i < 256; ++i) { p[i] = table[i];//這樣就實現了利用查找表table的方法來替換源圖像中的數據, //這對圖像就不是加減乘除這種計算了,而全部是直接去查詢表中找對應的值然后再替換。 } LUT(src, lut, dst); imshow("LUT", dst); waitKey(0); return 0; }
效率探討
一般圖像規模比較大的話,圖像的遍歷是一項相當耗時的工作,因此為提高效率,以下幾點值得我們注意:
- 對於可提前計算的變量應避免寫在循環體內;如
int cols=img.cols*img.channels(); for(int i=0;i<cols;i++) //而不是 // for(int i=p;i<img.cols*img.channels();i++)
- 在以上四種圖像遍歷方法中,從效率來看使用 OpenCV 內置函數LUT可以獲得最快的速度,這是因為OpenCV庫可以通過英特爾線程架構啟用多線程。其次,指針遍歷最快,迭代器遍歷次之,at方法遍歷最慢。一般情況下,我們只有在對任意位置的像素進行讀寫時才考慮at方法。
最后順便提一下圖像的鄰域操作😁
很多時候,我們對圖像處理時,要考慮它的鄰域,比如3*3是我們常用的,這在圖像濾波、去噪中最為常見,下面我們介紹如果在一次圖像遍歷過程中進行鄰域的運算。
下面我們進行一個簡單的濾波操作,濾波算子為[0 –1 0;-1 5 –1;0 –1 0]。它可以讓圖像變得尖銳,而邊緣更加突出。核心公式即:sharp(i.j)=5*image(i,j)-image(i-1,j)-image(i+1,j)-image(i,j-1)-image(i,j+1)。
int main(int argc, char** argv) { Mat src,dst; src = imread("D:/opencv練習圖片/薛之謙.jpg"); imshow("Image", src); ImgFilter2d(src, dst); imshow("filter", dst); waitKey(0); return 0; } //構建濾波函數 void ImgFilter2d(const Mat &image, Mat& result) { result.create(image.size(), image.type()); int nr = image.rows; int nc = image.cols*image.channels(); for (int i = 1; i < nr - 1; i++) { //用指針遍歷獲取當前行,上一行,下一行 const uchar* up_line = image.ptr<uchar>(i - 1);//指向上一行 const uchar* mid_line = image.ptr<uchar>(i);//當前行 const uchar* down_line = image.ptr<uchar>(i + 1);//下一行 uchar* cur_line = result.ptr<uchar>(i);//創建結果圖像指針 for (int j = 1; j < nc - 1; j++) { //核心公式 cur_line[j] = saturate_cast<uchar>(5 * mid_line[j] - mid_line[j - 1] - mid_line[j + 1] -up_line[j] - down_line[j]); } } // 把圖像邊緣像素設置為0 result.row(0).setTo(Scalar(0)); result.row(result.rows - 1).setTo(Scalar(0)); result.col(0).setTo(Scalar(0)); result.col(result.cols - 1).setTo(Scalar(0)); }