2.2访问图像像素
前面提到图像处理最基本的是操作图像的像素,学会如何存取其像素,是一切编写图像处理应用的基础。我们通过实现在图像中加入椒盐噪点来说明三种访问像素的方式。椒盐噪点是一种特殊的噪点,顾名思义,它随机地将部分像素设置为白色或黑色。在传输过程中,如果部分像素值丢失,那么这种噪点就会出现。在我们的代码里,将随机挑选若干像素,并将其设置为白色。
直接访问像素
为了访问像素,需要在代码中指定像素 所在的行和列。如果图像是单通道的,返回值是单个数值;如果图像是多通道的,返回值则是一组向量(Vector)。
void MainWindow::saltDirectly(cv::Mat &image, int n)
{
for(int k = 0; k < n; k++){
//rand()是随机数生成函数
int i = rand() % image.cols;
int j = rand() % image.rows;
if(image.channels() == 1){ //gray image
image.at<uchar>(i,j) = 255;
}else if(image.channels() == 3) {//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;
}
}
}
Mat类有若干成员函数 可以获得图像的属性。公有成员变量cols和rows给出了图像的宽和高。成员函数at(int y, int x)可以用来访问像素。但是必须爱编译期间知道图像的数据类型,因为Mat类可以存放任意数据类型的元素。这也是这个函数应用模板函数来实现的原因。这也意味着,当调用该函数时,需要使用以下方式指定数据类型:
image.at<uchar>(i,j) = 255;
注意:一定要确保指定的数据类型要和矩阵中的数据类型相符合。at方法本身不会进行任何数据类型转换。
对于彩色图像,每个像素由三个部分构成:RGB。因此,一个包含彩色图像的Mat会返回一个由8位数组成的向量。OpenCV将此类向量定义为cv::Vec3b,即由三个unsigned char组成的向量。这也解释了为什么访问像素的代码可以写成以下形式:
image.at<cv::Vec3b>(j,i)[channel] = value;
其中,索引值channel标明了颜色通道号。
使用指针访问像素
为了简化指针运算,Mat类提供了ptr函数可以得到图像任意行的首地址。ptr函数是一个模板函数,它返回第j行的首地址。
uchar* data = image.ptr<uchar>(j);
void MainWindow::saltByPointer(cv::Mat &image, int n)
{
for(int k = 0; k < n; k++){
//rand()Is a random number generating function
int i = rand() % image.cols;
int j = rand() % image.rows;
uchar* data = image.ptr<uchar>(j);
for(int c = 0; c < image.channels(); c++ )
data[i*image.channels()+ c] = 255;
}
}
在一个彩色图像中,图像数据缓冲区中的前三个字符对应图像左上角的三个通道值,接下来的三个字节对应第一行的第二个像素。依次类推(注意:OpenCV默认使用BGR,因此第一个通道通常是蓝色)。一个宽为W、高为H的图像需要一个大小由W*H*3个uchar构成的内存块。成员变量cols代表图像的宽度(图像的列数),rows代表图像的高度,step代表以字节为单位的图像的有效宽度。图像的通道数可以由channels方法得到:对于灰度图像来说是1,对于彩色图像来说是3。total函数返回矩阵的像素个数。像素大小可以由elemSize函数得到:对于一个三通道的short型矩阵(CV_16SC3),elemSize返回6。
底层指针运算
在类Mat中,图像数据以unsigned char形式保存在一块内存中。这块内存的首地址可以通过data成员变量得到。data是一个unsigned char类型的指针。
uchar* data = image.data;
从当前行到下一行可以通过对指针加上行宽完成:
data += image.step; //下一行
通常而言,可以通过如下方式获得第j行、第i列像素的地址:
//(j,i)处的像素地址为&image.at(j,i)
data = image.data + j* image.step + i * elemSize();
即使这种方式确实行之有效,我们依然不建议使用这种处理方式。因为这种方式除了容易出错,还不适用带有“感兴趣区域”的图像。
使用迭代器访问像素
在面对对象的编程中,遍历数据集合通常是通过迭代器来完成的。迭代器是一种特殊的类,它专门来遍历集合中的各个元素,同时隐藏了在给定的集合上元素迭代的具体实现方式。这种信息隐藏原则的使用过使得遍历集合更加容易。另外,不管数据类型是什么,我们都可以使用相似的方式遍历集合。标准模板库(STL)为每个容器类型都提供了迭代器。OpenCV同样为Mat类提供了与STL迭代器兼容的迭代器。
void MainWindow::saltByIterator(cv::Mat &image, int n)
{
for(int k = 0; k < n; k++){
//rand()Is a random number generating function
int i = rand() % image.cols;
int j = rand() % image.rows;
cv::Mat_<cv::Vec3b>::iterator it = image.begin<cv::Vec3b>();
it += (j * image.cols + i);
for(int c= 0; c < image.channels(); c++)
(*it)[c] = 255;
}
}
一个Mat类实例的迭代器可以通过创建一个cv::Mat Iterator_的实例来得到。类似于cv::Mat_,下划线意味着cv::Mat Iterator_是一个模板类。之所以如此是由于迭代器来访问像素,就就必须在编译期知道像素的数据类型。一个图像迭代器可以用如下方式声明:
cv::Mat Iterator<cv::Vec3b>::iterator it;
另外一种方式是使用定义在Mat_内部的迭代器类型:
cv::Mat_<cv::Vec3b>::iterator it;
初始位置(图像的左上角)的迭代器通常是通过begin方法得到。对于Mat的实例,可以通过image.begin()获得。也可以通过对迭代器进行代数运算。例如:如果想从图像的第二行开始,那么可以用image.begin() + image.cols来初始化迭代器。集合终止位置的迭代器可以通过end方法得到。但是,end方法得到的迭代器其实已经超过了集合。这也也意味着迭代过程必须在迭代器到达这个位置时结束。end方法得到的迭代器也可以进行代数运算。
原始图片:
添加椒盐噪声后:
推荐阅读
-
浅谈Visual C#进行图像处理(读取、保存以及对像素的访问)
-
Python OpenCV处理图像之图像像素点操作
-
荐 opencv进阶学习笔记3:像素运算和图像亮度对比度调节
-
.net下灰度模式图像在创建Graphics时出现:无法从带有索引像素格式的图像创建graphics对象 问题的解决方案。
-
用python处理图片实现图像中的像素访问
-
全球首发高像素图像传感器 Redmi K30系列下周见:双模5G旗舰
-
【MATLAB图像融合】像素极大/小值选择及函数的调用[3]
-
OpenCV彩色图像像素读取与表示
-
校验两张BMP图像的RGB矩阵有多少个像素点不同
-
【OpenCV】OpenCV访问像素点的三种方式