OpenCV 学习(像素操作 Manipuating the Pixels)
最后更新于:2022-04-01 11:25:23
## 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;
~~~
下面是个简单的例子,打开一副彩色图像,在上面随机的添加一些噪声。原始图像如下:
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-26_571f1db4085f8.jpg "")
核心的代码:
~~~
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;
}
~~~
处理后的图像如下:
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-26_571f1db436d46.jpg "")
像上面这样每次用 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
}
~~~
利用默认参数应用于我们的测试图像后得到的结果如下:
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-26_571f1db4667b6.jpg "")
上面的代码中有这么一行,是用来计算一行像素有多少个字节的。当然这个前提是每个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 这样的用法。