[OpenCV2]使用指针遍历图像

在多数的图像处理任务,为了执行一个计算任务,需要遍历图像的所有像素.考虑到大量的像素数据需要被访问,用一个有效率的方法去做这个事情是很有必要的.本节和下一节会用不同的方式展示如何用循环遍历图像.本节使用指针的方法.

Getting ready

我们会用一个简单的任务举例如何遍历图像:减少一幅图像的颜色数.

彩色图像是由三个通道的像素组成的.每个通道的亮度值分别对应三原色(红绿蓝).因为这些值是8位unsigned char类型的,总共的颜色数为256×256×256,总共超过了一千六百万种颜色.因此,为了减少分析图像的复杂性,有时减少图像的颜色数是有用的.一个简单的方法是把RGB颜色空间再分成相等大小的空间.例如,如果图像颜色减少为每8个像素取一个像素值,你最终只会得到32×32×32种颜色.原始图像中的每一种颜色被减少后的图像新的颜色值代替,这个新的颜色值是,它原来属于小的颜色空间的中间值.

因此,这个基本的颜色缩减算法是很简单的.如果N是缩减因子,然后对图像中的每个像素,然后这个像素的每个通道的值除以N(整除,因此需要注意有数据丢失).然后把这个结果乘以N,最后的结果会略小于输入的像素值.然后再加加上N/2,会得到在两个与N相乘相邻区间的中间值.对于每个8位通道重复这个处理,你会得到总共256/N×256/N×256/N可能的颜色值.

How to do it ...

我们用下面的颜色缩减函数实现:

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);
我们会得到如下图像(注意观察图像的颜色):

How it works...

在一幅彩色图像中,第一个三字节的图像缓冲区给出了左上角像素的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;

There's more ...

这个颜色缩减的功能仅仅通过一个方法实现.我们有其他的方法可以实现同样的功能.图像功能函数一般允许设置不同的输入,输出图像.如果考虑到图像数据的连续性,遍历图像循环将会更有效率.最终,我们可以使用低等级的指针遍历图像数据.以下我们会讨论以上内容.

Other color reduction Formulas

在我们的例子中,颜色的减少是通过利用整数的除法分层,将结果分为最近的整数层中.

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;
通常,按位操作是非常有效率的,当要求效率的时候,按位操作是很有用的.

Having input and output arguments

在我们的例子中,图像的转化直接就修改了输入图像,我们称为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 
当输入和输出为同一个图像时,这个功能函数会和第一个提出的方法完全等价.如果输出为另一个图像,这个函数将被正确的执行,无论这个图像在函数调用之前是否被分配.

Efficient scanning of contionus images

我们先前的解释,由于效率的原因,每一行的最后可能被额外的像素填充.然而,当图像没有被额外像素填充时,这个图像可以被看成是一个总共有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方法在没有任何内存复制和重新分配的情况下改变了矩阵的维度.第一参数是新的通道数,第二个参数是新的行数.列数自动进行重新分配了.

在这些实现中,内层循环按顺序处理所有图像像素.这中方法的主要优势是:当几个小图像同时扫描到相同的循环中.

Low-level pointer arithmetics

在cv::Mat类中,图像数据包含了一个个unsigned char类型的内存块.第一个内存元素的地址返回一个unchar类型的指针.所以,在你图像循环开始,应当这样写:

uchar *data= image.data;

这个step方法可以给出一行总共的字节数(包含填充的数据).通常,你会包含行为j 列为 i 像素的地址:

// address of pixel at (j,i) that is &image.at(j,i)     
data= image.data+j*image.step+i*image.elemSize();  
但是,虽然这个是可以在我们的例子中使用,但是这种方式是不推荐的.除了易于出错,这个方法不能在感兴趣的区域使用.感兴趣的区域将在本章最后讨论.














你可能感兴趣的:(C++,opencv,图像处理,opencv2)