OpenCV 学习(像素操作 Manipuating the Pixels)

来源:互联网 发布:7.0采集助手数据库 编辑:程序博客网 时间:2024/05/16 14:20

OpenCV 学习(像素操作 Manipuating the Pixels)

OpenCV 虽然提供了许多类型的图像处理函数,可以对图像进行各种常见的处理,但是总会有些操作时没有的,这时我们就需要自己来操纵像素,实现我们需要的功能。今天就来讲讲 OpenCV 进行像素级操作的几种方法,并做个比较。

在 OpenCV 中,图像用矩阵来表示,对应的数据类型为 cv::Mat 。 cv::Mat 功能很强大,矩阵的元素可以为字节、字、浮点数、数组等多种形式。对于灰度图像,每个像素用一个 8 bit 字节来表示,对彩色图像,每个像素是一个三个元素的数组,分别存储 BGR 分量,这里大家没看错,就是 BGR 而不是 RGB,每个像素三个字节,第一个字节是蓝色分量,别问我为啥设计成这样,我也不知道。

访问单个像素 (at 函数)

cv::Mat 类有个 at(int y, int x) 方法,可以访问单个像素。但是我们知道cv::Mat 可以存储各种类型的图像。在调用这个函数时必须要指定返回的像素的类型,因为 at 函数是模板函数。

如果是灰度图,我们知道像素是以无符号字符型变量的形式存储的。那么要像下面这样访问。

image.at<uchar>(j,i)= value;

如果图像是24位真彩色的,那么可以这样:

image.at<cv::Vec3b>(j,i)[channel]= value;

下面是个简单的例子,打开一副彩色图像,在上面随机的添加一些噪声。原始图像如下:

这里写图片描述

核心的代码:

cv::Mat image = cv::imread("Q:\\test.jpg", CV_LOAD_IMAGE_COLOR);for(int k = 0; k < 1000; k++){    int i = rand() % image.cols;    int j = rand() % image.rows;    image.at<cv::Vec3b>(i, j)[0] = 255;    image.at<cv::Vec3b>(i, j)[1] = 255;    image.at<cv::Vec3b>(i, j)[2] = 255;}

处理后的图像如下:

这里写图片描述

像上面这样每次用 at 函数时都指定类型很繁琐。这时可以利用 cv::Mat 的派生类,cv::Mat_ 类,这个类是模板类。在建立这个类的实例时就要指定类型,之后就无需每次使用时再指定类型了。下面是个例子。

cv::Mat_ <cv::Vec3b> ima = image;cv::namedWindow("Origin image", cv::WINDOW_NORMAL);cv::imshow("Origin image", image);for(int k = 0; k < 1000; k++){    int i = rand() % ima.cols;    int j = rand() % ima.rows;    ima(i, j)[0] = 255;    ima(i, j)[1] = 255;    ima(i, j)[2] = 255;}

这个代码处理后的效果是相同的。

上面的程序中有个

ima = image;

这里又涉及到 OpenCV 的一个特性,就是普通的矩阵拷贝操作都是所谓的浅拷贝。也就是说这样操作后 ima 和 image 共享相同的图像数据。

如果我们想要真正的复制图像数据。这时可以用 clone() 方法。类似下面的代码:

cv::Mat_ <cv::Vec3b> ima = image.clone();

这样之后 ima 和 image 就完全独立了。

利用指针遍历图像像素

经常,我们的算法需要遍历图像的全部像素。这时用 at 函数就会很慢。更高效的访问图像像素的方式是利用指针。

简单的说,我们通常是去获得一行像素的头指针。如果图像是灰度的,则类似这样操作。

uchar* data = image.ptr<uchar>(j);

如果图像是 24 位彩色的,则可以这样:

cv::Vec3b * data = image.ptr<cv::Vec3b> (j);

实际上,即使是彩色图像,也可以用一个 uchar 型指针去指向。只要我们自己去计算要访问的像素相对行首的位置偏移是多少。比如下面的函数,可以处理灰度图像,也能处理彩色图像,作用是缩减图像中使用到的颜色。

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}

利用默认参数应用于我们的测试图像后得到的结果如下:

这里写图片描述

上面的代码中有这么一行,是用来计算一行像素有多少个字节的。当然这个前提是每个channel 占用一个字节。

int nc= image.cols * image.channels();

如果每个 channel 占用多个字节的话,上面的公式就是错误的了,这时我们可以这样计算。

int nc = image.cols * image.elemSize();

image.elemSize() 得到的是每个像素的字节数。乘以一行有多少个像素,正好就是一行有多少个字节。

上面的例子中,我们用了两重循环来遍历图像中的每一个像素。实际上,因为我们对每个像素进行的操作是相同的,我们根本不需要确定某个像素是哪一行的。因此上面的代码还可以进一步优化,只用一个循环来完成。

但是这时我们要特别注意,有些图像所占的内存空间是不连续的。在一行像素结束后,可能会空出一小块内存。之所以会这样是因为有些 CPU 的指令对数据有内存对其要求。这样虽然浪费了一些空间,但是运算起来会更快速。

图像所占内存是否是连续的可以利用 isContinuous() 来得到。如果是连续的则可以用一个循环将所有像素都处理完。下面是代码,这个代码兼顾了内存连续与不连续两种情况,内存不连续时就退化为两重循环:

void colorReduce2(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    }}

如果我们要获得图像数据的首地址,还可以这样:

uchar *data = image.data;

对于二维图像数据来说,每行图像所占据的字节数由成员变量 step 来存储。因此:

data += image.step;

使得 data 指向下一行图像的内存首地址。

当然,上面这些操作都是比较低级的指针操作,不建议使用。

利用 iterators 来遍历图像数据

C++ 的标准模板库(STL)中大量的使用到了 iterator。OpenCV 也模仿 STL 显示了自己的一套 iterator。

OpenCV 中设计了 cv::MatIterator_ 类,这个类与 cv::Mat_ 类似,也是模板类。将这个类实例化时需要指定具体的类型。比如下面的代码:

cv::MatIterator_<cv::Vec3b> it;

另一种使用方法如下:

cv::Mat_<cv::Vec3b>::iterator it;

如果我们只是用 iterator 来读取像素值而不改变它,则可以用常量型 iterator.

cv::MatConstIterator_<cv::Vec3b> it;cv::Mat_<cv::Vec3b>::const_iterator it;

上面的例子用 iterator 重写后代码如下:

void colorReduce3(cv::Mat &image, int div=64){    // obtain iterator at initial position    cv::Mat_<cv::Vec3b>::iterator it = image.begin<cv::Vec3b>();    // obtain end position    cv::Mat_<cv::Vec3b>::iterator itend = image.end<cv::Vec3b>();    // loop over all pixels    for ( ; it!= itend; ++it)    {        // process each pixel ---------------------        (*it)[0] = (*it)[0] / div * div + div / 2;        (*it)[1] = (*it)[1] / div * div + div / 2;        (*it)[2] = (*it)[2] / div * div + div / 2;    }}

这种方式有利也有弊,最大的缺点是这个代码只能处理 24 位真彩色图像。优点是无需关注内存是否连续的问题了。相对来说,利用 iterator 的代码的运算速度比直接指针操作还是要稍微的慢一点。

各种方法的速度比较

上面介绍了几种访问图像像素的方法,在不考虑效率的前提下,这些方法都很好,可以实现同样的功能。但是在计算机视觉应用场景中,计算效率(运行速度)经常是我们必须要考虑的关键因素。

因此这里专门用一个小节来比较各种方法的运行速度。
为了完整性,下面也给出了一个用 at函数访问像素的 colorReduce 函数。

void colorReduce0(cv::Mat &image, int div=64){    int nl = image.rows; // number of lines    int nc = image.cols;    for (int j=0; j<nl; j++)    {        for (int i=0; i<nc; i++)        {            // process each pixel ---------------------            image.at<cv::Vec3b>(j,i)[0]=                    image.at<cv::Vec3b>(j,i)[0]/div*div + div/2;            image.at<cv::Vec3b>(j,i)[1]=                    image.at<cv::Vec3b>(j,i)[1]/div*div + div/2;            image.at<cv::Vec3b>(j,i)[2]=                    image.at<cv::Vec3b>(j,i)[2]/div*div + div/2;            // end of pixel processing ----------------        } // end of line    }}

回顾一下我们实现的几种方法。

  • colorReduce0(): 使用 at 函数访问像素
  • colorReduce1(): 两重循环,用指针访问像素
  • colorReduce2(): 当图像内存连续时用一重循环访问所有像素
  • colorReduce3(): 用 iterator 访问像素

利用 cv::getTickCount() 来计算各个函数的运行时间。

  • colorReduce0(): 240579
  • colorReduce1(): 22363
  • colorReduce2(): 21202
  • colorReduce3(): 77573

结果一目了然,colorReduce0 运行的最慢,其次是 colorReduce3。

colorReduce1 和 colorReduce2 相差的不多。因此,我们写程序时,应尽可能的采用 colorReduce2 或 colorReduce1 这样的用法。

1 0
原创粉丝点击