OpenCV 学习笔记(2) Manipulating the Pixels (上)

本节导读:

  • Accessing pixel values
  • Scanning an image with pointers
  • Scanning an image with iterators

Introduction

  Fundamentally, an image is a matrix of numerical values.


Accessing pixel values(存取像素值)

salt-and-pepper noise(椒盐噪声)

Definition

  Salt-and-pepper noise is a particular type of noise in which some pixels are replaced by a white or a black pixel.(This type of noise can occur in faulty communication when the value of some pixels is lost during transmission)

Add salt-and-pepper noise to an image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void salt(cv::Mat &image, int n)
{

for(int k = 0; k < n; ++k)
{
int i = rand() % image.cols; //column, 列,image's width
int j = rand() % image.rows; //row, 行 image's height

if(1 == image.channels()) //gray-level image
{
image.at<uchar>(j, i) = 255;
}
else if(3 == image.channels()) //color image
{
image.at<cv::Vec3b>(j, i)[0] = 255;
image.at<cv::Vec3b>(j, i)[1] = 255;
image.at<cv::Vec3b>(j, i)[2] = 255;
}
}
}

The Difference of gray-level image and color image

  In the case of a gray-level image, the number 255 is assigned to the single 8-bit value. For a color image, you need to assign 255 to the three primary color channels in order to obtain a white pixel.

How to access the different attributes of an image.

  For element access, cv::Mat has the method at(int y, int x).但是返回类型必须要在编译的时候知道,所以程序员必须给定类型,这也是为什么 at method 用模版来实现,所以调用at method用如下方式,举例说明:

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

  这里我们需要保证指定的类型是矩阵中的类型,对于上述例子,必须保证ucharimage的元素类型。In color images, each pixel is associated with three components: the red, green, and blue channels.Therefore, a cv::Mat containing a color image will return a vector of three 8-bit values.
  opencv定义了short vectors :cv::Vec3b.It is a vector of 3 unsigned chars.

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

  2-element and 4-element vectors(cv::Vec2b and cv::Vec4b)s:short, i:int, f:float, d:double, 定义成模版类cv::Vec<T,N> where T is the type and N is the number of vector element.

1
typedef Vec<double, 3> Vt;

Use cv::Mat_ to access pixels

  Using at method is cumbersome(笨重的;累赘的;难处理的)because the returned type must be specified as a template argument for each call. So it is possible to use the cv::Mat_ class which is a template subclass of cv::Mat.
  Among the extra methods, there is the operator () allowing direct access to matrix elements. Therefore, if image is a reference to a uchar matrix, then one can write:

1
2
cv::Mat_<uchar> im2= image; // im2 refers to image
im2(50,100)= 0; // access to row 50 and column 100

  cv::Mat_的元素类型在创建实例的时候已经声明,操作符()在编译期就知道需要返回的数据类型。他和at返回值也是一样的,所以使用()看起来更加简洁方便一点。


Scanning an image with pointers(使用指针遍历图像)

In-place transformation

  Definition: The transformation is directly applied to the input image, which is called an in-place transformation.

双重循环遍历像素

代码

1
2
3
4
5
6
7
8
9
10
11
12
int nl = image.rows;	//number of lines
int nc = image.cols * image.channels(); //total number of elements per line

for(int j = 0; j < nl; ++j)
{
uchar *data = image.ptr<uchar>(j); //get the address of row j
for(int i = 0; i < nc; ++i)
{
//降低图像中的颜色数目,个人愚见:书上仅仅是通过降低图像颜色这个示例来告诉我们如何去用指针遍历像素
data[i] = data[i]/div*div + div/2;
}
}

  这里颜色降低的原理就是降低维度,一般彩色图像是0~255,现在变成32个维度,即某通道的强度值是0~7时,用4来表示,为8~15时,用12来表示,大概就是这么个意思,我觉得没必要去细究。

作用原理

  Opencv中默认使用BGR的通道顺序,因此第一个通道通常是蓝色的。一个W*H的彩色图像,需要W*H*3个uchar构成的内存块。但是处于效率的考虑,每行可能会填补一些额外像素,因为行的长度是4或8的倍数,一些多媒体处理芯片(如Intel的MMX架构)可以更高校的处理图像。这些额外的像素不会被显示或者保存,填补的值会被忽略。opencv将填补后的一行的长度指定为关键字。如果没有对图像进行填补,图像的有效长度就等于图像的真实宽度。step代表以字节为单位的图像的有效宽度。即使图像类型元素不是uchar,step依然代表着一行的字节数
  像素的大小可以有elemSize method得到:对于一个三通道的short型矩阵(CV_16SC3),elemSize返回6。
  通道数可以由channels method得到:例如:int nc = image.cols * image.channels();
  为了简化指针运算,cv::Mat提供了ptr函数可以得到图像任意行的首地址。如下:

1
uchar *data = image.ptr<uchar>(j);	//返回第j行的首地址

  此外,之前提到的颜色缩减(降低图像颜色数目),还可以通过位运算实现,如下:(当作一个编程技巧)

1
2
uchar mask = 0xFF << n;	//e.g. for div = 16, mask = 0xF0
data[i] = (data[i] & mask) + div/2; //这种实现方法就要比上面的data[i] = data[i]/div*div + div/2;高明了

高效遍历连续图像

  考虑到效率,图像有可能在行尾补充若干个像素,但是当不填补的时候,图像可以被视为一个常委W*H的一位数组。高效遍历连续图像的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void colorReduce(cv::Mat &image, int div = 64)
{

int nl = image.rows;
int nc = image.cols * image.channels();
if(image.isContinus()) //判断是否为连续图像,即若不包含填充像素,则返回true
{
nc = nc * nl;
nl = 1;
}
for(int j = 0; j < nl; ++j)
{
uchar *data = image.ptr<uchar>(j);
for(int i = 0; i < nc; ++i)
{
data[i] = data[i]/2;
}
}
}

或者使用reshape来完成上述功能

1
2
3
4
5
6
if(image.isContinus())
{
image.reshape(1, image.cols * image.rows);
}
int nl = image.rows;
int nc = image.cols * image.channels();

  reshape不需要内存拷贝或者重新分配就能改变矩阵的维度。上述方法在处理小图像的时候很有优势。(书上说的,至于为啥,我猜是调用uchar *data = image.ptr<uchar>(j);每个图像只用一次?)

底层指针运算

  在类cv::Mat中,图像数据以unsigned char 形式保存在一块内存中。(那其他类型的也是这样?感觉有点问题)这块内存的首地址可以通过data成员变量得到,如下方式:

1
2
uchar *data = image.data;	//图像的首地址指针
data += image.step; //到下一行

还可以通过如下方式或许第j行,第i列的像素地址:

1
2
//(j,i)处的像素地址为&image.at(j, i);
data = image.data + j * image.step + i * image.elemSize();

但是不建议用这种方式(书上说不建议的),不适用regions of interest


Scanning an image with iterators(使用迭代器遍历图像)

  OpenCV为cv::Mat提供了与STL迭代器兼容的迭代器。

实现方法

  一个cv::Mat实例的迭代器可以通过创建一个cv::MatIterator_的实例来得到。类似于子类cv::Mat_,下划线意味着cv::MatIterator_是一个模板类,用迭代器来存取图像的元素,必须在编译期就要知道图像元素的数据类型。

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

或者使用定义在Mat_内部的迭代器类型:

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

重写颜色缩减函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void colorReduce(cv::Mat &imgCopy, int div = 64)
{

//下面这两种方法都可以
// Mat_<Vec3b>::iterator it = imgCopy.begin<Vec3b>();
// Mat_<Vec3b>::iterator itend = imgCopy.end<Vec3b>();
MatIterator_<Vec3b> it = imgCopy.begin<Vec3b>();
MatIterator_<Vec3b> itend = imgCopy.end<Vec3b>();
for(; it != itend; ++it)
{
(*it)[0] = (*it)[0] / 2; //上面代码除以div啥的,我这里只是为了示范迭代器使用,就没有写那么麻烦
(*it)[1] = (*it)[1] / 2;
(*it)[2] = (*it)[2] / 2;
//处理的是彩色图像,所以这里迭代器返回的是cv::Vec3b,每个颜色分量可以通过操作符[]得到。
}
//可以不用for循环,用while
/*
while(it != itend)
{
····
++it;
}
*/

}

  有时候不放心,可以用cv::MatConstIterator_,防止后面自己手贱。此外我们在用迭代器访问的时候,可能会觉得使用模板函数begin每次需要指定类型,很麻烦,这里有种更简洁的方法,可以通过cv::Mat_的实例来获得。

1
2
3
cv::Mat_<cv::Vec3b> cimage = image;
cv::Mat_<cv::Vec3b>::iterator it = cimage.begin();
cv::Mat_<cv::Vec3b>::iterator itend = cimage.end();