在多数的图像处理任务,为了执行一个计算任务,需要遍历图像的所有像素.考虑到大量的像素数据需要被访问,用一个有效率的方法去做这个事情是很有必要的.本节和下一节会用不同的方式展示如何用循环遍历图像.本节使用指针的方法.
我们会用一个简单的任务举例如何遍历图像:减少一幅图像的颜色数.
彩色图像是由三个通道的像素组成的.每个通道的亮度值分别对应三原色(红绿蓝).因为这些值是8位unsigned char类型的,总共的颜色数为256×256×256,总共超过了一千六百万种颜色.因此,为了减少分析图像的复杂性,有时减少图像的颜色数是有用的.一个简单的方法是把RGB颜色空间再分成相等大小的空间.例如,如果图像颜色减少为每8个像素取一个像素值,你最终只会得到32×32×32种颜色.原始图像中的每一种颜色被减少后的图像新的颜色值代替,这个新的颜色值是,它原来属于小的颜色空间的中间值.
因此,这个基本的颜色缩减算法是很简单的.如果N是缩减因子,然后对图像中的每个像素,然后这个像素的每个通道的值除以N(整除,因此需要注意有数据丢失).然后把这个结果乘以N,最后的结果会略小于输入的像素值.然后再加加上N/2,会得到在两个与N相乘相邻区间的中间值.对于每个8位通道重复这个处理,你会得到总共256/N×256/N×256/N可能的颜色值.
我们用下面的颜色缩减函数实现:
void colorReduce(cv::Mat &image, int div=64);为用户提供了一个图像和每通道的缩减系数.在这里,这个功能是in-place的,即图像的输入和处理返回的结果都是一个参数.参见There's more...了解一些通用的输入和输出参数的表示方法.
这个处理方法是通过一个二重循环遍历所有的像素值:
void colorReduce(cv::Mat &image, int div=64) { int nl= image.rows; // number of lines // total number of elements per line int nc= image.cols * image.channels(); for (int j=0; j<nl; j++) { // get the address of row j uchar* data= image.ptr<uchar>(j); for (int i=0; i<nc; i++) { // process each pixel --------------------- data[i]= data[i]/div*div + div/2; // end of pixel processing ---------------- } // end of line } }这个功能可以使用下面的代码片段测试
// read the image image= cv::imread("boldt.jpg"); // process the image colorReduce(image); // display the image cv::namedWindow("Image"); cv::imshow("Image",image);我们会得到如下图像(注意观察图像的颜色):
在一幅彩色图像中,第一个三字节的图像缓冲区给出了左上角像素的3个颜色通道值,下一个三字节表示第一行第二个像素的值.如此类推(注意,在OpenCV中,通道默认顺序是BGR,所以蓝色是第一个通道).一幅图像的宽(W)和高(H),会需要占用W×H×3 uchars的内存块大小.然而,由于效率的原因,行的长度会被一些额外的像素填充.这是因为一些多媒体处理芯片(如Intel MMX构架)在图像的宽是4或者是8的整倍数时效率更高.很显然,这些多出的像素并不显示和保存,他们的值是被忽略的.OpenCV定义这个不定的宽度为一个关键字.显然,如果图像如果没有被额外的像素填充时,图像的宽度和真实宽度是一致的.属性cols给你图像的宽(列数),属性rows给出图像的高.step属性给出有效的宽度(用字节数表示).甚至你的图像数据类型为除unchar其他的类型,step仍然给出一行的字节数.像素大小通过elemSize方法给出(例如,对于一个3通道的短整型矩阵(CV_16SC3),elemSize会返回6).图像的通道数会通过nchannels方法返回(灰度图像为1,彩色图像为3).最后,在一个矩阵中total方法会返回像素总数(图像也是一个矩阵).
每行真实的像素数给出如下:
int nc= image.cols * image.channels();为了简化指针的运算,cv::Mat类提供了一个方法可以获取每一行的地址.这就是ptr方法.这是一个模版方法会返回第j行的指针
uchar* data= image.ptr<uchar>(j);
*data++= *data/div*div + div2;
这个颜色缩减的功能仅仅通过一个方法实现.我们有其他的方法可以实现同样的功能.图像功能函数一般允许设置不同的输入,输出图像.如果考虑到图像数据的连续性,遍历图像循环将会更有效率.最终,我们可以使用低等级的指针遍历图像数据.以下我们会讨论以上内容.
在我们的例子中,颜色的减少是通过利用整数的除法分层,将结果分为最近的整数层中.
data[i]= data[i]/div*div + div/2;这个减少颜色的功能还可以使用取模运算,放回div(1维的减少因子)最近的倍数:
data[i]= data[i] – data[i]%div + div/2;但是这个计算会有一点慢,因为它需要访问每个像素两次.
还有一种使用按位运算的方法.如果我们限制换算系数为2的幂,div = pow(2,n),然后掩盖像素第一个n bits的值,会得到div最低的倍数.这个掩盖操作是一个简单的位运算实现的:
// mask used to round the pixel value uchar mask= 0xFF<<n; // e.g. for div=16, mask= 0xF0这个颜色缩减功能将由下式给出:
data[i]= (data[i]&mask) + div/2;通常,按位操作是非常有效率的,当要求效率的时候,按位操作是很有用的.
在我们的例子中,图像的转化直接就修改了输入图像,我们称为in-place方式.使用这种方式,我们不需要额外的图像接受输出结果,直接在内存上进行操作.但是,在一些应用中,用户希望能够完整的保存原始图像.这样,用户首先就必须先创建原始图像的副本,然后再进行处理.注意,完全创建一幅图像的副本,使用clone方法是最简单的,例如:
// read the image image= cv::imread("boldt.jpg"); // clone the image cv::Mat imageClone= image.clone(); // process the clone // orginal image remains untouched colorReduce(imageClone); // display the image result cv::namedWindow("Image Result"); cv::imshow("Image Result",imageClone);通过定义了一个额外的重载函数,让用户选择是否使用in-place的处理方法.函数的参数如下:
void colorReduce(const cv::Mat &image, // input image cv::Mat &result, // output image int div=64);注意 这个输入图像现在被定义为const类型,这意味着这个图像不会被函数修改.当需要in-place处理时,可以把输入和输出指定为同一个图像.
colorReduce(image,image);如何不需要,提供一个cv::Mat的对象,例如:
cv::Mat result; colorReduce(image,result);这里的关键是,首先验证输入和输出图像是否分配了相同的数据空间,大小和像素类型是否匹配.为了方便检测在函数内部使用cv::Mat的creat方法创建一个矩阵.使用这个方法时,需要重新指定大小和类型.如果这个矩阵已经分配了大小和类型,这个方法不会执行分配操作,该方法仅仅会返回实例.因此,我们的函数,需要首先调用create方法去创建一个和输入图像相同大小和类型矩阵(如果必须的话):
result.create(image.rows,image.cols,image.type());注意:create总是创建一个连续的图像,这是一个没有任何填充的图像.内存块被分配为total()*elemSize()的大小.这个循环使用了两个指针:
for (int j=0; j<nl; j++) {
// get the addresses of input and output row j
const uchar* data_in= image.ptr<uchar>(j);
uchar* data_out= result.ptr<uchar>(j);
for (int i=0; i<nc; i++) {
// process each pixel ---------------------
data_out[i]= data_in[i]/div*div + div/2;
// end of pixel processing ---------------- } // end of line
当输入和输出为同一个图像时,这个功能函数会和第一个提出的方法完全等价.如果输出为另一个图像,这个函数将被正确的执行,无论这个图像在函数调用之前是否被分配.
我们先前的解释,由于效率的原因,每一行的最后可能被额外的像素填充.然而,当图像没有被额外像素填充时,这个图像可以被看成是一个总共有W×H个像素的一维数组.cv::Mat的方法可以告诉我们图像是否被填充.如果 isContinuous 方法返回true说明没有包含填充像素.
在一些具体的处理算法中,通过一个循环(或更多)处理图像时,利用图像数据的连续性具有优势.我们的处理函数可以写为如下形式:
void colorReduce(cv::Mat &image, int div=64) { int nl= image.rows; // number of lines int nc= image.cols * image.channels(); if (image.isContinuous()) { // then no padded pixels nc= nc*nl; nl= 1; // it is now a 1D array } // this loop is executed only once // in case of continuous images for (int j=0; j<nl; j++) { uchar* data= image.ptr<uchar>(j); for (int i=0; i<nc; i++) { // process each pixel --------------------- data[i]= data[i]/div*div + div/2; // end of pixel processing ---------------- } // end of line } }现在,首先测试图像是否是连续的.如果图像不包含填充像素,我们通过把宽为1.把高设置为W×H.消除外部循环.注意 ,有一个reshape的方法在这里可以被使用.如下:
if (image.isContinuous()) { // no padded pixels image.reshape(1, // new number of channels image.cols*image.rows) ; // new number of rows } int nl= image.rows; // number of lines int nc= image.cols * image.channels();这个reshape方法在没有任何内存复制和重新分配的情况下改变了矩阵的维度.第一参数是新的通道数,第二个参数是新的行数.列数自动进行重新分配了.
在这些实现中,内层循环按顺序处理所有图像像素.这中方法的主要优势是:当几个小图像同时扫描到相同的循环中.
在cv::Mat类中,图像数据包含了一个个unsigned char类型的内存块.第一个内存元素的地址返回一个unchar类型的指针.所以,在你图像循环开始,应当这样写:
uchar *data= image.data;
// address of pixel at (j,i) that is &image.at(j,i) data= image.data+j*image.step+i*image.elemSize();但是,虽然这个是可以在我们的例子中使用,但是这种方式是不推荐的.除了易于出错,这个方法不能在感兴趣的区域使用.感兴趣的区域将在本章最后讨论.