当Mat为多通道图像时,如果如果我们将其内容输出到终端,则可以看出其列数为Mat::cols的n倍,当然n为Mat的通道数。虽是如此,但是Mat::cols的数值并没有随之改变。遍历整幅图像时,如果是灰度图,那么一般是按<uchar>类型存放;如果是RGB彩图,那么一般是按<Vec3b>类型存放。

灰度图的存放格式:

opencv mat for循环优化_指针

多通道的图像中,每列并列存放通道数量的子列,如RGB三通道彩色图:

opencv mat for循环优化_遍历_02

注意通道的顺序反转了: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;
    }
}