opencv——图像遍历以及像素操作
摘要
我们在图像处理时经常会用到遍历图像像素点的方式,在OpenCV中一般有四种图像遍历的方式,在这里我们通过像素变换的点操作来实现对图像亮度和对比度的调整。
数据格式千万不要搞错:
uchar对应的是CV_8U,char对应的是CV_8S,int对应的是CV_32S,float对应的是CV_32F,double对应的是CV_64F。
补充: 图像变换可以看成
- 像素变换——点操作
- 邻域变换——区域操作(卷积,特征提取,梯度计算等)
对于点操作:
q(i,j)=αf(i,j)+β
其中f(i,j)是输入点像素值,q(i,j)是输出点像素值。
1,数组遍历-- at<typename>(i,j)
说明:就是把图像看成二维矩阵,at(i,j)索引坐标位置,单通道直接得到坐标位置对应的像素值,三通道就这个位置代表了像素值的一维数组;
Mat类提供了一个at的方法用于取得图像上的点,它是一个模板函数,可以取到任何类型的图像上的点。这里选用参数α=1.5,β=0.5来提高图像亮度。
int main(int argc, char** argv) { Mat src; src = imread("D:/opencv练习图片/薛之谦.jpg"); imshow("Image", src); //创建一个和原图一致的空白图像 Mat dst = Mat::zeros(src.size(), src.type()); for (int i = 0; i < src.rows; i++) { for (int j = 0; j < src.cols; j++) { if (src.channels() == 1) //单通道遍历 { dst.at<uchar>(i, j) = src.at<uchar>(i, j) + 100; } else if (src.channels() ==3)//三通道遍历 { //通过数组遍历获取图像每个点 float b = src.at<Vec3b>(i, j)[0]; float g = src.at<Vec3b>(i, j)[1]; float r = src.at<Vec3b>(i, j)[2]; //进行点操作后赋值给空白图像dst float alpha = 1.5; float beta = 0.5; dst.at<Vec3b>(i, j)[0] = saturate_cast<uchar>(b*alpha + beta); dst.at<Vec3b>(i, j)[1] = saturate_cast<uchar>(g*alpha + beta); dst.at<Vec3b>(i, j)[2] = saturate_cast<uchar>(r*alpha + beta); } } } imshow("点操作", dst); waitKey(0); return 0; }
saturate_cast<uchar>是溢出保护,在进行像素的乘法后很容易造成像素点的值超出0-255的范围,因此使用saturate_cast<uchar>确保像素值始终在0-255的范围内。
2,指针遍历法
OpenCV中cv::Mat类提供了成员函数ptr得到图像任意行的首地址。ptr函数是一个模板函数,如:src.ptr<uchar>(i)
说明:ptr指针尤其固定格式,就是先把图像看成(src.rows,1)的图像,ptr获取每个位置的地址,地址位置隐藏了列的数据,由于列表名就是列表的地址,所以ptr获取的地址就是此行中列这样一维数据的列表名称。这样通过下标就可以获取像素值
int main(int argc, char** argv) { Mat src; src = imread("D:/opencv练习图片/薛之谦.jpg"); imshow("Image", src); //创建一个和原图一致的空白图像 Mat dst = Mat::zeros(src.size(), src.type()); int width ; //判断图像是否连续 if (src.isContinuous() && dst.isContinuous()) { // 将3通道转换为1通道 width = src.cols * src.channels(); } for (int i = 0; i < src.rows; i++) { // 获取第i行的首地址 const uchar* src_rows = src.ptr<uchar>(i); uchar* dst_ptr = dst.ptr<uchar>(i); //像素点操作处理 for (int j = 0; j < width; j++) { dst_ptr[j] = saturate_cast<uchar>(src_rows[j] *1.5 + 0.5); dst_ptr[j + 1] = saturate_cast<uchar>(src_rows[j + 1] *1.5 + 0.5); dst_ptr[j + 2] = saturate_cast<uchar>(src_rows[j + 2] *1.5 + 0.5); } } imshow("点操作", dst); waitKey(0); return 0; }
程序中将三通道的数据转换为1通道,是建立在每一行数据元素之间在内存里是连续存储的。但在opencv中由于的存储机制问题,行与行之间可能有空白单元,因此Mat提供了一个检测图像是否连续的函数isContinuous(),当图像连通时,我们就可以把图像完全展开,看成是一行。
针对at和ptr有很多人容易理解at,却理解不了ptr,下面讲一个用at生成ptr模式的解析例子:
说明:这是为了对比at和ptr而增加的,主要是获取at(i,0)位置处的地址,将其看成数值名称,通过下标索引像素值,和ptr原理一样,只是获取地址的方式不一样(数组名)
1️⃣遍历灰度图像像素方法:(采用at方法,使用ptr模式)
for (int i = 0; i < src.rows; i++) { //将灰度图片看成(src.rows,1)维度的二维矩阵,获取(i,0)数据的地址 uchar* src_rows_ptr = &(src.at<uchar>(i, 0)); uchar* dst_rows_ptr = &(dst.at<uchar>(i, 0)); for (int j = 0; j < src.cols; j++) { //将(i,0)数据的地址下的内容看成是一维数组,(i,0)数据的地址是一维数组的名字 dst_rows_ptr[j] = src_rows_ptr[j] + 100; } }
2️⃣遍历彩色图像像素方法:(采用at方法,使用ptr模式)
for (int i = 0; i < src.rows; i++) { //将彩色图片看成(src.rows,1)维度的二维矩阵,获取(i,0)数据的地址 Vec3b* src_rows_ptr = &(src.at<Vec3b>(i, 0)); Vec3b* dst1_rows_ptr = &(dst1.at<Vec3b>(i, 0)); for (int j = 0; j < src.cols; j++) { //将(i,0)数据的地址下的内容看成是二维数组,(i,0)数据的地址是二维数组的名字 dst1_rows_ptr[j][0] = src_rows_ptr[j](0) + 100; dst1_rows_ptr[j][1] = src_rows_ptr[j](1) + 100; dst1_rows_ptr[j][2] = src_rows_ptr[j](2) + 100; } }
综上所述:使用ptr指针效率非常高,大家普遍使用的是at和ptr方法;使用的时候,一定要规范格式;
其中:at<类型>(i,j)
ptr<类型>(i)
3、迭代器遍历
迭代器是专门用于遍历数据集合的一种非常重要的特殊的类,用其遍历隐藏了在给定集合上元素迭代的具体实现方式。迭代器方法是一种更安全的用来遍历图像的方式,首先获取到数据图像的矩阵起始,再通过递增迭代实现移动数据指针。
1、迭代器Matlterator_ Matlterator_是Mat数据操作的迭代器,:begin()表示指向Mat数据的起始迭代器,:end()表示指向Mat数据的终止迭代器。
2、迭代器Mat_ OpenCV定义了一个Mat的模板子类为Mat_,它重载了operator()让我们可以更方便的取图像上的点。
int main(int argc, char** argv)
{
Mat src;
src = imread("D:/opencv练习图片/薛之谦.jpg");
imshow("Image", src);
// 初始化图像迭代器
Mat_<Vec3b>::iterator it = src.begin<Vec3b>();
Mat_<Vec3b>::iterator itend = src.end<Vec3b>();
while (it != itend)
{
//像素点操作
(*it)[0] = saturate_cast<uchar>((*it)[0]*1.5+0.5);
(*it)[1] = saturate_cast<uchar>((*it)[1] * 1.5 + 0.5);
(*it)[2] = saturate_cast<uchar>((*it)[2] * 1.5 + 0.5);
it++;
}
imshow("点操作", src);
waitKey(0);
return 0;
}
经测试,得到与数组遍历一样的效果。
4、核心函数LUT
LUT(LOOK -UP-TABLE)查找表。简言之:在一幅图像中,假如我们想将图像某一灰度值换成其他灰度值,用LUT就很好用。这样可以起到突出图像的有用信息,增强图像的光对比度的作用对某图像中的像素值进行替换。。
在图像处理中,对于一个给定的值,将其替换成其他的值是一个很常见的操作,OpenCV 提供里一个函数直接实现该操作LUT函数
函数 API
void LUT(InputArray src, InputArray lut, OutputArray dst); //src表示的是输入图像(可以是单通道也可是3通道) //lut表示查找表(查找表也可以是单通道,也可以是3通道; //...如果输入图像为单通道,那查找表必须为单通道; //...若输入图像为3通道,查找表可以为单通道,也可以为3通道; //...若为单通道则表示对图像3个通道都应用这个表,若为3通道则分别应用 ) //dst表示输出图像
如何使用该函数?
- 首先我们建立一个mat型用于查表
- 然后我们调用函数 (I 是输入 J 是输出):
LUT(I, lookUpTable, J);
LUT函数的作用:
(1)改变图像中像素灰度值
通过构建查找表,图片0-100灰度的像素灰度就变成0,101-200的变成100,201-255的就变成255。
int main(int argc, char** argv) { Mat src,dst1,dst3; src = imread("D:/opencv练习图片/薛之谦.jpg"); imshow("Image", src); //查找表,数组的下标对应图片里面的灰度值 //例如lutData[20]=0;表示灰度为20的像素其对应的值0. uchar lutData[256]; for (int i = 0; i < 256; i++) { if (i <= 100) lutData[i] = 0; if (i > 100 && i <= 200) lutData[i] = 100; if (i > 200) lutData[i] = 255; } Mat lut(1, 256, CV_8UC1, lutData); LUT(src, lut, dst1); imshow("LUC", dst1); waitKey(0); return 0; }
(2)颜色空间缩减
如果矩阵元素存储的是单通道像素,使用uchar (无符号字符,即0到255之间取值的数)那么像素可有256个不同值。但若是三通道图像,这种存储格式的颜色数就是256*256*256个(有一千六百多万种)。用如此之多的颜色可能会对我们的算法性能造成严重影响。其实有时候,仅用这些颜色的一小部分,就足以达到同样效果。
这种情况下,常用的一种方法是 颜色空间缩减 。其做法是:将现有颜色空间值除以某个输入值,以获得较少的颜色数。例如,颜色值0-9的取为0,10-19的取为10,以此类推。就把256个不同值划分为26个,大大减少运算时间。
uchar 类型的值除以 int 值,结果仍是 char 。因为结果是char类型的,所以求出来小数也要向下取整。利用这一点,刚才提到在 uchar 定义域中进行的颜色缩减运算就可以表达为下列形式:
这样的话,简单的颜色空间缩减算法就可由下面两步组成:
一、遍历图像矩阵的每一个像素
二、对像素应用上述公式。
下面将图像压缩级设置为20(即0-19变为0,20-39变为20…)
int main(int argc, char** argv) { Mat src,dst; src = imread("D:/opencv练习图片/薛之谦.jpg"); imshow("Image", src); uchar table[256]; Mat lut(1, 256, CV_8U);//创建查找表 int divideWith = 20; //压缩级 20灰度为1级 for (int i = 0; i < 256; ++i) { table[i] = divideWith * (i / divideWith);//颜色缩减运算 } uchar *p = lut.data; for (int i = 0; i < 256; ++i) { p[i] = table[i];//这样就实现了利用查找表table的方法来替换源图像中的数据, //这对图像就不是加减乘除这种计算了,而全部是直接去查询表中找对应的值然后再替换。 } LUT(src, lut, dst); imshow("LUT", dst); waitKey(0); return 0; }
效率探讨
一般图像规模比较大的话,图像的遍历是一项相当耗时的工作,因此为提高效率,以下几点值得我们注意:
- 对于可提前计算的变量应避免写在循环体内;如
int cols=img.cols*img.channels(); for(int i=0;i<cols;i++) //而不是 // for(int i=p;i<img.cols*img.channels();i++)
- 在以上四种图像遍历方法中,从效率来看使用 OpenCV 内置函数LUT可以获得最快的速度,这是因为OpenCV库可以通过英特尔线程架构启用多线程。其次,指针遍历最快,迭代器遍历次之,at方法遍历最慢。一般情况下,我们只有在对任意位置的像素进行读写时才考虑at方法。
最后顺便提一下图像的邻域操作😁
很多时候,我们对图像处理时,要考虑它的邻域,比如3*3是我们常用的,这在图像滤波、去噪中最为常见,下面我们介绍如果在一次图像遍历过程中进行邻域的运算。
下面我们进行一个简单的滤波操作,滤波算子为[0 –1 0;-1 5 –1;0 –1 0]。它可以让图像变得尖锐,而边缘更加突出。核心公式即:sharp(i.j)=5*image(i,j)-image(i-1,j)-image(i+1,j)-image(i,j-1)-image(i,j+1)。
int main(int argc, char** argv) { Mat src,dst; src = imread("D:/opencv练习图片/薛之谦.jpg"); imshow("Image", src); ImgFilter2d(src, dst); imshow("filter", dst); waitKey(0); return 0; } //构建滤波函数 void ImgFilter2d(const Mat &image, Mat& result) { result.create(image.size(), image.type()); int nr = image.rows; int nc = image.cols*image.channels(); for (int i = 1; i < nr - 1; i++) { //用指针遍历获取当前行,上一行,下一行 const uchar* up_line = image.ptr<uchar>(i - 1);//指向上一行 const uchar* mid_line = image.ptr<uchar>(i);//当前行 const uchar* down_line = image.ptr<uchar>(i + 1);//下一行 uchar* cur_line = result.ptr<uchar>(i);//创建结果图像指针 for (int j = 1; j < nc - 1; j++) { //核心公式 cur_line[j] = saturate_cast<uchar>(5 * mid_line[j] - mid_line[j - 1] - mid_line[j + 1] -up_line[j] - down_line[j]); } } // 把图像边缘像素设置为0 result.row(0).setTo(Scalar(0)); result.row(result.rows - 1).setTo(Scalar(0)); result.col(0).setTo(Scalar(0)); result.col(result.cols - 1).setTo(Scalar(0)); }