第二章 操作像素
在本章,我们会讲述:
处理像素值
用指针扫描图像
用迭代器扫描图像
写高效的图像扫描循环
用相邻的方法扫描图像
展示简单的图像计算
定义感兴趣的区域
【概述】
为了建立计算机图像应用,你必须能够接触图像内容,并且最终修改或者创建图像。这一章中会教你如何操作图像元素,比如像素。你会学
习到如何扫描一幅图像并处理每个像素点。你也会学习到如何高效地做,因为就算是适当的维度的图像也会包含成千上万的像素的。
基本上将,一个图像时一个数值对应的矩阵。这就是OpenCV2用cv::Mat处理图像的原因了。矩阵中的每一个元素代表一个像素。对于一个灰
度图像(黑白图像),像素值是8位的无符号型值(也就是非负数。。),相应地,0代表黑色而255代表白色。对于彩色图像,每个像素的三
个这样的值代表着我们常常说的三原色(红,绿,蓝)。此时一个矩阵元素由三个值生成。
正如前面所讲,OpenCV也允许你用不同类型的像素值创建图像,比如CV_8U或者浮点值CV_32F。这些对于存储很有用的,例如在一些图像处理
过程中的起到媒介作用的图像。大多数操作可以被应用到任何类型的矩阵上,其他的则是需要特定的类型,或者只能和给定数量的通道数量
起作用。所以说,为了避免在编程中犯常见错,对于一个函数或者方法的前提的理解是很重要的。
整个这一章节,我们用如下的一张图片作为输入图片。
【处理像素值】
为了处理矩阵中的每一个元素,你晋级你需要确定其行数和列数。相应的元素(可以是单独的数值也可以是多个通道的图像的向量值),会
被作为返回值返回的。
为了说明对于像素值的直接操作,我们会创建一个简单的函数,它对图像增加了胡椒盐噪声。正如这个名字所暗示的,胡椒盐噪声是噪声中
特定的一种,图像中的一些像素会被黑色或者白色像素替代。这种错误在传输错误的时候会发现,是某些像素值丢失导致的。此情形下,我
们简单地随机选择一些像素并设定为白色。
我们写一个函数用于接收输入的图像。这个图像会被此函数修改。为了这个目的,我们用传递引用的方式。第二个参数是像素的编号,我们
将把这个像素值置为白色:
此函数由一重循环组成,执行了n次,每次把255(对应白色)赋值给一个随机像素点。我们对于灰度图像和彩色图像区别对待,彩图需要分
别赋值给三个channel。
1 void salt(cv::Mat &image, int n){ 2 for(int k=0; k<n; k++){ 3 //rand()是MFC随机函数生成器 4 //在Qt中请使用qrand() 5 int i=qrand()%image.cols; 6 int j=qrand()%image.rows; 7 8 if(image.channels()==1){//对应灰度图像 9 image.at<uchar>(j, i)=255; 10 }else if(image.channels()==3){//彩色图像 11 image.at<cv::Vec3b>(j, i)[0]=255; 12 image.at<cv::Vec3b>(j, i)[1]=255; 13 image.at<cv::Vec3b>(j, i)[2]=255; 14 } 15 } 16 }
你可以通过先前打开过的一张图片作为参数传递给此函数:
1 //open the image 2 cv::Mat image = cv::imread("boldt.jpg"); 3 4 //call function to add noise 5 salt(image, 3000); 6 7 //display image 8 cv::namedWindow("Image"); 9 cv::imshow("Image", image);
发现qrand()不能识别。最后还是用了rand():
1 #include <cv.h> 2 #include <highgui.h> 3 4 using namespace std; 5 using namespace cv; 6 7 void salt(Mat& image, int n){ 8 for(int k=0; k<n; k++){ 9 int i=rand()%image.cols; 10 int j=rand()%image.rows; 11 12 if(image.channels()==1){ 13 image.at<uchar>(j, i)=255; 14 }else if(image.channels()==3){ 15 image.at<Vec3b>(j, i)[0]=255; 16 image.at<Vec3b>(j, i)[1]=255; 17 image.at<Vec3b>(j, i)[2]=255; 18 } 19 } 20 } 21 22 int main(){ 23 Mat image=imread("C:/testdir/Koala.jpg"); 24 salt(image, 3000); 25 namedWindow("Image"); 26 imshow("Image", image); 27 waitKey(0); 28 }
【更多】
使用cv::Mat的at方法有时候很笨重。因为返回的类型必须被规定为模板参
数。当矩阵类型未知时,使用cv::Mat的子类cv::Mat_类是可以的。这个类
定义了少量额外的方法但没有定义更多的数据属性,因此一个指向类的指针
或者引用可以被直接地转化成另外的类。在其他的额外方法中,operator()
方法允许直接访问矩阵元素。因此,如果图像是指向uchar类型的引用,那
么可以这样写:
cv::Mat_<uchar>im2=image;//im2 refers to image
im2(50, 100)=0;//access to row 50 and column 100
由于cv::Mat_类的元素类型在变量创建的时候已经声明过了,operator()方
法在编译的时候知道返回值的类型。不同于写入的时候变shorter,
operator()方法和at方法等效。
【同见】
编写高效扫描图像的循环方法
【用指针扫描图像】
在大多数图像处理过程中,人们为了执行计算需要处理图像的所有像素。考
虑到图像中如此多的像素数量,高效的方式很重要。这一部分,以及下一部
分,会向你展示扫描图像的不同的循环的实现方式。这一部分使用指针来计
算。
【准备阶段】
我们通过完成一个加单的任务来实现图像扫描过程:减少一副图像中的颜色
的数量
彩色图像由3个通道(3-channels)的像素组成.每个像素的每个通道对应着三
原色中的强度值。由于这些值都是8位的无符号字符类型值,颜色数量的总
数是256*256*256,大于1.6亿个颜色。所以为了减少分析时的复杂度,有时
候减少图像的数量是有用的。实现这个目标的一个简答方法是,把RGB空间
分割成大小相等的立方体。例如,你想在每个维度把颜色数量减少8个,那
么你得到的颜色总是就是32*32*32.原图中每个颜色也就变成了颜色衰减后
的图中所属立方体中心位置的颜色值。
基本的颜色减少算法是简单的。若N是减少因子,那么对于图像中每个像素
和每个像素中的通道,都要整除N(余数会丢失)。然后把结果乘以N,所得
到的会比原来输入的图像要小。加上N/2,你会得到两者的中间值。持续着
一过程,你会得到总数为(256/N)^3的可能的颜色数量。
【如何做】
颜色衰减函数这样声明:
void colorReduce(cv::Mat &image, int div=64);
用户提供一张图片和每个通道的衰减因子。这里,此过程恰到好处的完成了
,即:输入图像的像素值被这个函数修改了。更多的输入输出参数参见【更
多】部分。
这个过程很简单,通过创建一个遍历像素的所有值的一个double循环以实现
:
void colorReduce(cv::Mat &image, int div=64){
int nl=image.rows;//行数
//每一行元素总数
int nc=image.cols*image.channels();
for(int j=0; j<nl; j++){
//获取第j行的地址
uchar* data=image.ptr<uchar>(j);
for(int i=0; i<nc; i++){
//处理每个像素
data[i]=data[i]/div*div+div/2;
//处理像素过程结束
}//每一行处理完毕
}
}
这个函数可以用下面代码测试:
//读入图像
image=cv::imread("boldt.jpg"):
//处理图像
colorReduce(image);
//展示图像
cv::namedWindow("Image");
cv::imshow("Image", image);
1 【更多】 2 使用cv::Mat的at方法有时候很笨重。因为返回的类型必须被规定为模板参 3 4 数。当矩阵类型未知时,使用cv::Mat的子类cv::Mat_类是可以的。这个类 5 6 定义了少量额外的方法但没有定义更多的数据属性,因此一个指向类的指针 7 8 或者引用可以被直接地转化成另外的类。在其他的额外方法中,operator() 9 10 方法允许直接访问矩阵元素。因此,如果图像是指向uchar类型的引用,那 11 12 么可以这样写: 13 cv::Mat_<uchar>im2=image;//im2 refers to image 14 im2(50, 100)=0;//access to row 50 and column 100 15 由于cv::Mat_类的元素类型在变量创建的时候已经声明过了,operator()方 16 17 法在编译的时候知道返回值的类型。不同于写入的时候变shorter, 18 19 operator()方法和at方法等效。 20 21 【同见】 22 编写高效扫描图像的循环方法 23 24 【用指针扫描图像】 25 在大多数图像处理过程中,人们为了执行计算需要处理图像的所有像素。考 26 27 虑到图像中如此多的像素数量,高效的方式很重要。这一部分,以及下一部 28 29 分,会向你展示扫描图像的不同的循环的实现方式。这一部分使用指针来计 30 31 算。 32 33 【准备阶段】 34 我们通过完成一个加单的任务来实现图像扫描过程:减少一副图像中的颜色 35 36 的数量 37 38 彩色图像由3个通道(3-channels)的像素组成.每个像素的每个通道对应着三 39 40 原色中的强度值。由于这些值都是8位的无符号字符类型值,颜色数量的总 41 42 数是256*256*256,大于1.6亿个颜色。所以为了减少分析时的复杂度,有时 43 44 候减少图像的数量是有用的。实现这个目标的一个简答方法是,把RGB空间 45 46 分割成大小相等的立方体。例如,你想在每个维度把颜色数量减少8个,那 47 48 么你得到的颜色总是就是32*32*32.原图中每个颜色也就变成了颜色衰减后 49 50 的图中所属立方体中心位置的颜色值。 51 52 基本的颜色减少算法是简单的。若N是减少因子,那么对于图像中每个像素 53 54 和每个像素中的通道,都要整除N(余数会丢失)。然后把结果乘以N,所得 55 56 到的会比原来输入的图像要小。加上N/2,你会得到两者的中间值。持续着 57 58 一过程,你会得到总数为(256/N)^3的可能的颜色数量。 59 60 【如何做】 61 颜色衰减函数这样声明: 62 void colorReduce(cv::Mat &image, int div=64); 63 用户提供一张图片和每个通道的衰减因子。这里,此过程恰到好处的完成了 64 65 ,即:输入图像的像素值被这个函数修改了。更多的输入输出参数参见【更 66 67 多】部分。 68 69 这个过程很简单,通过创建一个遍历像素的所有值的一个double循环以实现 70 71 : 72 void colorReduce(cv::Mat &image, int div=64){ 73 int nl=image.rows;//行数 74 //每一行元素总数 75 int nc=image.cols*image.channels(); 76 77 for(int j=0; j<nl; j++){ 78 //获取第j行的地址 79 uchar* data=image.ptr<uchar>(j); 80 for(int i=0; i<nc; i++){ 81 //处理每个像素 82 data[i]=data[i]/div*div+div/2; 83 //处理像素过程结束 84 }//每一行处理完毕 85 } 86 } 87 这个函数可以用下面代码测试: 88 //读入图像 89 image=cv::imread("boldt.jpg"): 90 //处理图像 91 colorReduce(image); 92 //展示图像 93 cv::namedWindow("Image"); 94 cv::imshow("Image", image);
【如何起作用的】
在彩图对应图像数据中最前面3字节是左上角的三个通道的值,接下来是第
一行第二个像素的三个通道的值,如此下去(注意,opencv默认使用BGR的
顺序表示颜色,所以蓝色是第一个颜色通道)。宽度为W,高度为H的图像需
要W*H*3个ucahr的内存大小。然而,为了高效,行的长度会被添加额外的像
素。这是因为一些多媒体过程的碎片)例如intelMMX建筑物)在行数是4或者
8的倍数的时候能高效处理。明显地,若没有增加这些额外的行那么有效的
宽度和实际宽度相等。数据属性cols给定了图像宽度,rows给定了图像高度
,而tep数据属性以字节为单位给定了图像的宽度。即使你的图像不是uchar
类型的,step仍然以字节为单位。像素元素的大小通过elemSize的形式给出
(例如,对与3通道的短整型矩阵(CV_16SC3),elemSize会返回6)。图像
中的通道数量由nchannels方法给出(如果是灰度图像那么就是1,彩图就是
3)。最终,total这个方法返回矩阵中像素总数。
每一行的像素值数量通过以下方式计算:
int nc=image.cols*image.channels();
为了简化指针计算的过程,cv:Mat类提供了一个方法,能够直接给你图像中
行的地址。这就是ptr方法。这是一个模板方法,例如:
uchar* data=image.ptr<uchar>(j);
注意,在上述处理过程中,我们在每一行都有相同的指针计算,所以可以写
成:
*data++=*data/div*div+div/2; //发现书上写错了。
【其他颜色衰减公式】
data[i]=data[i]-data[i]%div+div/2;
当然亦可在函数中增加参数,不在原图上做修改:
1 #include <cv.h> 2 #include <highgui.h> 3 4 using namespace std; 5 using namespace cv; 6 7 void colorReduce(const Mat &image, Mat &result, int div=64){ 8 int nl=image.rows; 9 int nc=image.cols*image.channels(); 10 result.create(image.rows, image.cols, image.type()); 11 for(int j=0; j<nl; j++){ 12 const uchar* data_in=image.ptr<uchar>(j); 13 uchar* data_out=result.ptr<uchar>(j); 14 for(int i=0; i<nc; i++){ 15 data_out[i]=data_in[i]/div*div+div/2; 16 } 17 } 18 } 19 20 int main(){ 21 Mat image=imread("C:/testdir/Koala.jpg"); 22 Mat result; 23 colorReduce(image, result); 24 namedWindow("Image"); 25 imshow("Image", result); 26 waitKey(0); 27 }
若图像是连续的(即没有添加过额外的行)那么可以变二位为一维处理
1 #include <cv.h> 2 #include <highgui.h> 3 4 using namespace std; 5 using namespace cv; 6 7 void colorReduce(Mat &image, int div=64){ 8 int nl=image.rows; 9 int nc=image.cols*image.channels(); 10 if(image.isContinuous()){ 11 nc=nc*nl; 12 nl=1; 13 //or:image.reshape(1, image.cols*image.rows); 14 //then int nl=image.rows; and int nc=image.cols; 15 } 16 for(int j=0; j<nl; j++){ 17 uchar* data=image.ptr<uchar>(j); 18 for(int i=0; i<nc; i++){ 19 data[i]=data[i]/div*div+div/2; 20 } 21 } 22 } 23 24 int main(){ 25 Mat image=imread("C:/testdir/Koala.jpg"); 26 colorReduce(image); 27 namedWindow("Image"); 28 imshow("Image", image); 29 waitKey(0); 30 }
【低级指针计算】
1 uchar* data=image.data; 2 data+=image.step; //从当前行跳转到要处理的下一行
这样写看似也可以:data=image.data+j*image.step+i*image.elemSize();
但不建议这么做,因为这在感兴趣区域的处理过程中不奏效
【【用迭代器处理图像衰减】】
此部分略。(功能和指针类似,有时间补上。)
【图像扫描至邻居法】
用相邻元素扫描,核心公式:
当前值=5*当期值-上方值-下方值-左方值-右方值
代码
1 #include <cv.h> 2 #include <highgui.h> 3 4 using namespace std; 5 using namespace cv; 6 7 void sharpen(const Mat& image, Mat &result){ 8 result.create(image.size(), image.type()); 9 imshow("image", image); 10 for(int j=1; j<image.rows-1; j++){ 11 const uchar* previous=image.ptr<const uchar>(j-1); 12 const uchar* current=image.ptr<const uchar>(j); 13 const uchar* next=image.ptr<const uchar>(j+1); 14 uchar* output=result.ptr<uchar>(j); 15 for(int i=1; i<image.cols*3; i++){//此处书上写为i<image.cols-1发现只显示1/3宽度 16 *output=saturate_cast<uchar>(5*current[i]-current[i-1] 17 -current[i+1]-previous[i]-next[i]); 18 output++; 19 } 20 } 21 result.row(0).setTo(Scalar(0)); 22 result.row(result.rows-1).setTo(Scalar(0)); 23 result.col(0).setTo(Scalar(0)); 24 result.col(result.cols-1).setTo(Scalar(0)); 25 } 26 int main(){ 27 Mat image=imread("C:/testdir/Koala.jpg"); 28 Mat result; 29 sharpen(image, result); 30 namedWindow("Image"); 31 imshow("Image", result); 32 waitKey(0); 33 return 0; 34 }
使用系统函数filter2D:
#include <cv.h> #include <highgui.h> using namespace std; using namespace cv; void sharpen2D(const Mat& image, Mat &result){ Mat kernel(3, 3, CV_32F, Scalar(0)); kernel.at<float>(1,1)=5.0; kernel.at<float>(0,1)=-1.0; kernel.at<float>(2,1)=-1.0; kernel.at<float>(1,0)=-1.0; kernel.at<float>(1,2)=-1.0; filter2D(image, result, image.depth(), kernel); } int main(){ Mat image=imread("C:/testdir/barcode.bmp"); if(!image.data) return -1; Mat result; sharpen2D(image, result); namedWindow("Image"); imshow("image", image); imshow("result", result); waitKey(0); return 0; }
1 /* 2 *图像合成 3 *要求大小一样才可以 4 */ 5 #include <cv.h> 6 #include <highgui.h> 7 8 using namespace std; 9 using namespace cv; 10 11 int main(){ 12 Mat image1=imread("C:/testdir/me.jpg"); 13 Mat image2=imread("C:/testdir/airplane.jpg"); 14 Mat result; 15 addWeighted(image1, 0.7, image2, 0.9, 0., result); 16 // imshow("image", image); 17 imshow("result", result); 18 imwrite("C:/testdir/we.jpg", result); 19 waitKey(0); 20 return 0; 21 }
1 /* 2 *图像合成 3 *在指定区域合成 4 */ 5 #include <cv.h> 6 #include <highgui.h> 7 8 using namespace std; 9 using namespace cv; 10 11 int main(){ 12 Mat image=imread("C:/testdir/me.jpg"); 13 Mat logo=imread("C:/testdir/logo.jpg"); 14 Mat imageROI=image(Rect(600, 1300, logo.cols, logo.rows)); 15 addWeighted(imageROI, 1.0, logo, 0.3, 0., imageROI); 16 imshow("result", image); 17 imwrite("C:/testdir/me4.jpg", image); 18 waitKey(0); 19 return 0; 20 }
来源:https://www.cnblogs.com/zjutzz/archive/2013/04/30/3051938.html