對圖像的像素進行操作,我們可以實現空間增強,反色等目的。讓我們先來看一下內存空間中圖像矩陣,也就是Mat的矩陣數值部分是怎麼存儲的:
如果圖像是一幅灰度圖像,他就像這樣,從左到右,從上到下,依次是矩陣的每一行每一列,這時候矩陣M(i,j)的值自然就是當前點的灰度值了。
而對於一幅彩色圖像,由於它的像素分量channel並不是一個,所以每一列又分爲了幾個channel。拿常見的RGB圖像來說,就像這樣:
從這張圖上,就可以比較清楚地看出來在內存中矩陣是如何存儲多channel圖像的了。這裏要注意的是在RGB模型中,每一個子列依次爲BGR,也就是正好是顛倒的,第一個分量是藍色,第二個是綠色,第三個是紅色。
清楚了圖像在內存中的存儲方式,我們也就可以來進行像素值的操作了。在這裏,我們舉這樣一個例子。我們對一幅灰度圖像的灰度值進行變換:
小於100的灰度值被統一映射爲0;100到200之間的灰度值被映射爲100;大於200的灰度值被映射爲200.
主函數如下:
int main()
{
string picName="lena.jpg";
Mat A=imread (picName,CV_LOAD_IMAGE_GRAYSCALE); //讀入灰度圖像
uchar table[256]; //映射表,規定了變換前後灰度值的對應關係 table[gray_value_before]=gray_value_after
for (int i=0;i<256;i++)
{
table[i]=i/100*100; //這裏利用了C++的語言特性i/100只會留下整數部分
}
imshow("變換前",A);
Mat B=ChangeImg (A,table); //變換函數
imshow ("變換後",B);
waitKey ();
return 0;
}
首先,我們用指針方式對圖像的像素點灰度值進行操作:
Mat ChangeImg(Mat &img,const uchar* table)
{
CV_Assert(img.depth ()!=sizeof(uchar)); //聲明只對深度8bit的圖像操作
int channels=img.channels (); //獲取圖像channel
int nrows=img.rows; //矩陣的行數
int ncols=img.cols*channels; //矩陣的總列數=列數*channel分量數
if (img.isContinuous ()) //判斷矩陣是否連續,若連續,我們相當於只需要遍歷一個一維數組
{
ncols*=nrows;
nrows=1; //一維數組
}
//遍歷像素點灰度值
for (int i=0;i<nrows;i++)
{
uchar *p=img.ptr<uchar>(i); //獲取行地址
for (int j=0;j<ncols;j++)
{
p[j]=table[p[j]]; //修改灰度值
}
}
return img;
}
這裏,我們獲取了每一行開始處的指針,然後遍歷至該行末尾。如果矩陣是以連續方式存儲的,我們只需請求一次指針、然後一路遍歷下去就行。彩色圖像的情況有必要加以注意:因爲三個通道的原因,我們需要遍歷的元素數目也是3倍。
或者,我們可以使用data。data會從Mat中返回指向矩陣第一行第一列的指針。注意如果該指針爲NULL則表明對象裏面無輸入,所以這是一種簡單的檢查圖像是否被成功讀入的方法。當矩陣是連續存儲時,我們就可以通過遍歷 data 來掃描整個圖像。例如,一個灰度圖像,其操作如下:
uchar* p = img.data;
for( unsigned int i =0; i < ncol*nrows; ++i)
*p++ = table[*p];
或者,更安全的方法,我們可以使用迭代器。在迭代法中,所需要做的僅僅是獲得圖像矩陣的begin和end,然後增加迭代直至從begin到end。將*操作符添加在迭代指針前,即可訪問當前指向的內容。
Mat& ScanImageAndReduceIterator(Mat& I, const uchar* const table)
{
// accept only char type matrices
CV_Assert(I.depth() != sizeof(uchar));
const int channels = I.channels();
switch(channels)
{
case 1:
{
MatIterator_<uchar> it, end;
for( it = I.begin<uchar>(), end = I.end<uchar>(); it != end; ++it)
*it = table[*it];
break;
}
case 3:
{
MatIterator_<Vec3b> it, end;
for( it = I.begin<Vec3b>(), end = I.end<Vec3b>(); it != end; ++it)
{
(*it)[0] = table[(*it)[0]];
(*it)[1] = table[(*it)[1]];
(*it)[2] = table[(*it)[2]];
}
}
}
return I;
}
注意,在這裏對3通道的圖像進行操作的時候,使用到了Vec3b。Vec3b作爲一個對三元向量的數據結構,用在這裏正好是能夠表示RGB的三個分量。如果對於彩色圖像,仍然用uchar的話,則只能獲得3通道中的B分量。比如我們可以這樣打印出圖像的RGB三個分量:
for (int i=0;i<img.rows;i++)
{
const Vec3b* Mpoint=img.ptr <Vec3b>(i);
for (int j=0;j<img.cols;j++)
{
Vec3b intensity=*(Mpoint+j);
cout<<"R:"<<int(intensity[2])<<" G"<<int(intensity[1])<<" B"<<int(intensity[0])<<" ";
}
cout<<endl;
}
這裏使用指針,當然也可以使用上面的迭代器。
然而,OpenCV裏面已經有了相應函數可以讓我們更加方便地對像素進行操作,那便是LUT函數,而且推薦使用OpenCV的內建函數,因爲已經針對芯片做了優化設計,使得速度有很大提升。
函數原型爲:void LUT(InputArray src, InputArray lut, OutputArray dst, int interpolation=0 )
實現的映射關係如下所示:
也就是說比如原來src中值爲1會映射爲table[1]所對應的值再加上d。
所以上面的操作,我們其實只需要使用LUT函數就可以了。結合我們自己設計的table表,就能夠實現對圖像的操作。
int main()
{
string picName="lena.jpg";
Mat A=imread (picName,CV_LOAD_IMAGE_GRAYSCALE); //讀入灰度圖像
Mat lookUpLut(1,256,CV_8UC1); //建立一個256個元素的映射表
imshow ("變換前",A);
for (int i=0;i<256;i++)
{
lookUpLut.at<uchar>(i)=i/100*100;
}
Mat B;
LUT (A,lookUpLut,B);
imshow ("變換後",B);
waitKey ();
return 0;
}
下面的圖就是效果啦~~~