当Mat为多通道图像时,如果如果我们将其内容输出到终端,则可以看出其列数为Mat::cols的n倍,当然n为Mat的通道数。虽是如此,但是Mat::cols的数值并没有随之改变。遍历整幅图像时,如果是灰度图,那么一般是按<uchar>类型存放;如果是RGB彩图,那么一般是按<Vec3b>类型存放。
灰度图的存放格式:
多通道的图像中,每列并列存放通道数量的子列,如RGB三通道彩色图:
注意通道的顺序反转了:BGR。通常情况内存足够大的话图像的每一行是连续存放的,也就是在内存上图像的所有数据存放成一行,这中情况在访问时可以提供很大方便。
在实际应用中,我们很多时候需要对图像降色彩,因为256*256*256实在太多了,在图像颜色聚类或彩色直方图时,我们需要用一些代表性的颜色代替丰富的色彩空间,我们的思路是将每个通道的256种颜色用64种代替,即将原来256种颜色划分64个颜色段,每个颜色段取中间的颜色值作为代表色。
下面的例子对Mat图像进行扫描,并对像素点的像素进行压缩,压缩间隔为div=64,并比较扫描压缩及压缩的效率,效率最高的是采用.ptr及减少循环次数来遍历图像,并采用位操作来对图像像素进行压缩。
访问图像中的像素:
最高效的方法:C操作符[ ]
一般来说图像行与行之间往往存储是不连续的,但是有些图像可以是连续的,Mat提供了一个检测图像是否连续的函数isContinuous()。当图像连通时,我们就可以把图像完全展开,看成是一行。
//利用.ptr和*++进行图像像素遍历
Void ScanImage(const Mat& im, Mat& outImage, int div=64)
{
CV_Assert(im.depth()!=sizeof(uchar));
int channels=im.channels();
int rows=im.rows();
int cols=im.cols() * channels;
outImage.create(im.size(),im.type());
if (im.isContinuous())
{
cols*=rows;
rows=1; //用 isContinuous()函数来判断图像数组是否为连续的,一行。
}
int i,j;
for (i=0;i<rows;i++)
{
const uchar* inData=im.ptr<uchar>(i);
uchar* outData=outImage.ptr<uchar>(j);
for (j=0;j<cols;j++)
{
*outData++ = *inData++/div*div+div/2;
}
}
}
用指针除了用上面的方法外,还可以用指针来索引固定位置的像素:
image.step返回图像一行像素元素的个数(包括空白元素),image.elemSize()返回一个图像像素的大小。
&image.at<uchar>(i,j)=image.data+i*image.step+j*image.elemSize();
高效的方法:用指针来遍历图像
//利用.ptr和数组下标进行图像像素遍历
Void ScanImage(const Mat& im, Mat& outImage,int div=64)
{
//创建与原图像等尺寸的图像
outImage.create(im.size(),im.type());
int rows=im.rows;
//将3通道转为1通道
int nl=im.cols()*im.channels();
for (int i=0;i<rows;i++)
{
//每一行图像的指针
const uchar* inData=im.ptr<uchar>(i);
uchar* outIData=outImage.prt<uchar>(i);
for (int j=0;j<nl;j++)
{
outData[j]=inData[j]/div*div+div/2; //减少图像中颜色总数的关键算法:if div=64,then the total number of colors is 4*4*4;整数除法时,向下取整
}
}
}
从上面的例子中可以看出,取出图像中第i行数据的指针:image.ptr<uchar>(i)。
值得说明的是:程序中将三通道的数据转换为1通道,在建立在每一行数据元素之间在内存里是连续存储的,每个像素三通道像素按顺序存储。也就是一幅图像数据最开始的三个值,是最左上角的那像素的三个通道的值。
但是这种用法不能用在行与行之间,因为图像在OpenCV里的存储机制问题,行与行之间可能有空白单元。这些空白单元对图像来说是没有意思的,只是为了在某些架构上能够更有效率,比如intel MMX可以更有效的处理那种个数是4或8倍数的行。但是我们可以申明一个连续的空间来存储图像,这个话题引入下面最为高效的遍历图像的机制。
void ScanImage(const Mat& im, Mat& outImage,int div=64)
{
//创建与原图像等尺寸的图像
outImage.create(im.size(),im.type());
int rows = im.rows;
int cols = im.cols * im.channels();
int n = static_cast<int>(log(static_cast<double>(div))/log(2.0)); //div=64,n=6
uchar mask = 0xFF<<n;
//遍历图像的每个像素
for (int i=0;i<rows;i++)
{
uchar *inData = im.ptr<uchar>(i);
uchar *outData = outImage.ptr<uchar>(i);
for (int j=0;j<cols;j++)
{
*outData++ = *inData&mask + div/2;
}
}
}
普通方法: at<typename>(i,j) 图像坐标法
void ScanImage(const Mat& im, Mat& outImage,int div=64)
{
for (int i=0;j<im.rows;i++)
{
for (j=0;j<im.cols;j++)
{
outImage.at<Vec3b>(i,j)[0]=im.at<Vec3b>(i,j)[0]/div*div+div/2;
outImage.at<Vec3b>(i,j)[1]=im.at<Vec3b>(i,j)[1]/div*div+div/2;
outImage.at<Vec3b>(i,j)[2]=im.at<Vec3b>(i,j)[2]/div*div+div/2;
}
}
}
通过上面的例子我们可以看出,at方法取图像中的点的用法:
image.at<uchar>(i,j):取出灰度图像中i行j列的点。
image.at<Vec3b>(i,j)[k]:取出彩色图像中i行j列第k通道的颜色点。其中uchar,Vec3b都是图像像素值的类型,不要对Vec3b这种类型感觉害怕,其实在core里它是通过typedef Vec<T,N>来定义的,N代表元素的个数,T代表类型。
更简单一些的方法:OpenCV定义了一个Mat的模板子类为Mat_,它重载了operator()让我们可以更方便的取图像上的点。
Mat_<uchar> im=image;
im(i,j)=im(i,j)/div*div+div/2;
安全的方法:迭代器遍历
相比用指针直接访问可能出现越界问题,迭代器绝对是非常安全的方法:
下面的方法可以让我们来为图像中的像素声明一个迭代器:
MatIterator_<Vec3b> it;
Mat_<Vec3b>::iterator it;
如果迭代器指向一个const图像,则可以用下面的声明:
MatConstIterator<Vec3b> it; 或者
Mat_<Vec3b>::const_iterator it;
下面我们用迭代器来简化上面的colorReduce程序:
1 void colorReduce(const Mat& image,Mat& outImage,int div)
2 {
3 outImage.create(image.size(),image.type());
4 MatConstIterator_<Vec3b> it_in=image.begin<Vec3b>();
5 MatConstIterator_<Vec3b> itend_in=image.end<Vec3b>();
6 MatIterator_<Vec3b> it_out=outImage.begin<Vec3b>();
7 MatIterator_<Vec3b> itend_out=outImage.end<Vec3b>();
8 while(it_in!=itend_in)
9 {
10 (*it_out)[0]=(*it_in)[0]/div*div+div/2;
11 (*it_out)[1]=(*it_in)[1]/div*div+div/2;
12 (*it_out)[2]=(*it_in)[2]/div*div+div/2;
13 it_in++;
14 it_out++;
15 }
16 }
如果你想从第二行开始,则可以从image.begin<Vec3b>()+image.rows开始。
图像的邻域操作
很多时候,我们对图像处理时,要考虑它的邻域,比如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)。
1 void ImgFilter2d(const Mat &image,Mat& result)
2 {
3 result.create(image.size(),image.type());
4 int nr=image.rows;
5 int nc=image.cols*image.channels();
6 for(int i=1;i<nr-1;i++)
7 {
8 const uchar* up_line=image.ptr<uchar>(i-1);//指向上一行
9 const uchar* mid_line=image.ptr<uchar>(i);//当前行
10 const uchar* down_line=image.ptr<uchar>(i+1);//下一行
11 uchar* cur_line=result.ptr<uchar>(i);
12 for(int j=1;j<nc-1;j++)
13 {
14 cur_line[j]=saturate_cast<uchar>(5*mid_line[j]-mid_line[j-1]-mid_line[j+1]-
15 up_line[j]-down_line[j]);
16 }
17 }
18 // 把图像边缘像素设置为0
19 result.row(0).setTo(Scalar(0));
20 result.row(result.rows-1).setTo(Scalar(0));
21 result.col(0).setTo(Scalar(0));
22 result.col(result.cols-1).setTo(Scalar(0));
23 }
上面的程序有以下几点需要说明:
1,staturate_cast<typename>是一个类型转换函数,程序里是为了确保运算结果还在uchar范围内。
2,row和col方法返回图像中的某些行或列,返回值是一个Mat。
3,setTo方法将Mat对像中的点设置为一个值,Scalar(n)为一个灰度值,Scalar(a,b,c)为一个彩色值。
总结:常用方式
Mat某个像素的读取:下面指针这种运行效率高。如果你的Mat是CV_32F,也只需要将里面的uchar改成float就行了。
Mat image(rows, cols, CV_8UC1);
for (int j=0; j<image.rows; j++)
{
uchar* pdata= image.ptr<uchar>(j);
for (int i=0; i<image.cols; i++)
{
uchar data=*pdata++; //或者uchar data=pdata[i];
}
}
cv::Mat img(rows, cols, CV_8UC3);
for (int i = 0; i < img.rows; i++)
{
// 获取第i行首像素指针
cv::Vec3b* p1 = img.ptr<cv::Vec3b>(i);
for (int j = 0; j < img.cols; j++)
{
//取img的bgr
uchar b = p1[j][0];
uchar g = p1[j][1];
uchar r = p1[j][2];
}
}
Mat某个像素的修改:指针法是最高效的。如果你的Mat是CV_32F,也只需要将里面的uchar改成float就行了。
//设置灰度图像素
cv::Mat grayImg(rows, cols, CV_8UC1);
for(int i = 0; i < grayImg.rows; ++i)
{
uchar *p = grayImg.ptr<uchar>(i);
for(int j = 0; j < grayImg.cols; ++j)
{
p[j] = (i + j)%255;
}
}
//设置彩色图像素
cv::Mat colorImg(rows, cols, CV_8UC3);
for(int i = 0; i < colorImg.rows; ++i)
{
cv::Vec3b *p3 = colorImg.ptr<cv::Vec3b>(i);
for(int j = 0; j < colorImg.cols; ++j)
{
p3[j][0] = (i + j)%255;
p3[j][1] = (i + j)%255;
p3[j][2] = 0;
}
}