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

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 lineint nc = image.cols * image.channels();for (int j = 0; j < nl; j++){// get the address of row juchar* 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 linesint nc = image.cols * image.channels();if (image.isContinuous()){// then no padded pixelsnc = nc * nl;nl = 1; // it is now a 1D array}// this loop is executed only once// in case of continuous imagesfor (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;最好的节约是珍惜时间,最大的浪费是虚度年华。

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

相关文章:

你感兴趣的文章:

标签云: