如何操作图像的像素--Mat和vector




1)读入图片---imread()

Mat imread(const String& filename,int flags = IMREAD_COLOR);

理解函数第一步理解参数:

①读图片地址

函数识别不是依靠文件的后缀名,而是依靠内容的编码格式;

const string&filename 是一个常引用;

这里先解释一下引用的概念和为什么要使用引用?优势何在?

引用就是某一变量(目标)的一个别名,对引用的操作与对变量直接操作完全一样。

【例1】:int a; int &ra=a; //定义引用ra,它是变量a的引用,即别名

引用本身不占存储单元,系统也不给引用分配存储单元。故:对引用求地址,就是对目标变量求地址。&ra与&a相等。

首先要明白,单纯给某个变量取别名用处不大,

引用主要用于函数参数传递中,为了解决大块数据或对象的传递效率和空间不如意的问题

引用作为函数参数

引用的一个重要作用就是作为函数的参数。以前的C语言中函数参数传递是值传递,如果有大块数据作为参数传递的时候,采用的方案往往是指针,因为 这样可以避免将整块数据全部压栈,可以提高程序的效率。

例:

void swap(int &p1, int &p2) //此处函数的形参p1, p2都是引用

{ int p; p=p1; p1=p2; p2=p; }

main( )

{

 int a,b;

 cin>>a>>b; //输入a,b两变量的值

 swap(a,b); //直接以变量a和b作为实参调用swap函数

 cout<<a<< ' ' <<b; //输出结果

}

总结:形参传递,指针传递,引用传递函数参数的区别:

形参传递函数参数:当发生函数调用时,首先要给形参分配内存单元,再将实参复制到形参的内存单元中,分两步走。很明显的劣势就是,当实参的传递的数据量很大时,显然效率很低。

指针传递函数参数:当发生函数调用时,同样首先要给形参分配存储单元,(分配一个指针变量用来存放指向实参的地址);相较于上一种方法,它2省掉了复制实参这一过程,效率提高了;但是还存在一个问题,需要使用指针变量名进行操作,降低了阅读性,程序的可读性变差,易出错。

引用传递函数参数:使用引用与传递指针所达到的效果是一样的;

先定义int a,b; 当发生函数调用swap(a,b)时; 背调函数的形参由于是引用,所以被调函数的形参的地址就是实参的地址,这样就连在使用指针作为函数参数的分配存储单元用来存放实参地址的那一步也省略掉了,相较于使用第一种方法同时省略掉了两步,效率提高了很多。

所以在被调函数中对形参变量的操作就是对其相应的目标对象(在主调函数中)的操作。


常引用

  常引用声明方式:const 类型标识符 &引用名=目标变量名;

  用这种方式声明的引用,不能通过引用对目标变量的值进行修改,从而使引用的目标成为const,达到了引用的安全性。

  【例3】:

int a ;

const int &ra=a;

ra=1; //错误

a=1; //正确

引用作为返回值格式:

类型标识符 &函数名(形参列表及类型说明)

②选择读入图片的类型:

这个很重要,  它可以指导将原图读取时进行一定的转换;

默认的自带值是1;即3通道的bgr图像。

类型主要包括以下几种:

enum ImreadModes {

       IMREAD_UNCHANGED  = -1, //!< If set, return the loaded image as is (with alpha channel, otherwise it gets cropped).

       IMREAD_GRAYSCALE  = 0,  //!< If set, always convert image to the single channel grayscale image.

       IMREAD_COLOR      = 1,  //!< If set, always convert image to the 3 channel BGR color image.

       IMREAD_ANYDEPTH   = 2,  //!< If set, return 16-bit/32-bit image when the input has the corresponding depth, otherwise convert it to 8-bit.

       IMREAD_ANYCOLOR   = 4,  //!< If set, the image is read in any possible color format.

       IMREAD_LOAD_GDAL  = 8   //!< If set, use the gdal driver for loading the image.

     };

-1值已经不再使用,可以忽略;

0值表示单通道的灰度图像;

1表示3通道的bgr图像

2表示如果图像是16/32bit深度的图像,则返回相应深度的图像,否则就转换为8bit图像再返回。

4表示任意的图像格式

8表示使用gbal驱动器来加载图像


第一步完成了图像的载入;



二 接下来将图像保存

Mat img =imread(filename,0);

保存到一个Mat类对象中。

接下来介绍Mat

首先要定义一个概念:图片在mat类中存在的形式:存储的图像不管是彩色的还是灰度图像,都是二维的矩阵,

具体的存储格式如下:

(1)灰度图像的格式:

如何操作图像的像素--Mat和vector

具体的形式:
1 2 3 4 5 6
2 3 4 5 6 7
3 4 5 6 7 8 
4 5 6 7 8 9
上图中的row0,column0 中的 0,0代表第0行,第0列---对应到具体的形式里面就是标红的1;
同样的,对于彩色的BGR图像,
row0,column0代表(B,G,R)
具体的形式:
1 2 3      4 5 6      7 8 9
7 8 9      4 5 6      1 4 7
1 5 9      7 5 3      6 5 4
  依然是一个二维的矩阵,与灰度图不同的是,opencv会把三列作为一个column,
因此,img.cols等于BGR图像的列数,并不等于矩阵的列数,之间对应的关系为3倍的关系。
(2)彩色图像的格式:
如何操作图像的像素--Mat和vector

Mat 类定义于core.hpp中,申请一个Mat类型的对象本质上是进行一次动态的内存分配;只是不需要用户去管理内存。过程与c语言进行一次动态内存分配一样;

分配内存的结果主要有两部分:

1)矩阵头的处理,包括定义矩阵的大小(可改变,非固定的),存储方式,存储地址;

2)指向矩阵中具体值的指针,主要用于操作元素值;


接下来看看Mat类存储数据的方式:像素(元素)是以什么样的形式放在mat中。

在mat中存储数据的类型包括CV_8UC3(矩阵中元素的类型:无符号(U)的8bit深度,像素为3通道的像素(C-channel))

Mat_<uchar>CV_8U0-255

Mat_<char>CV_8S -128..127

Mat_<int>CV_32S...

Mat_<float>CV_32F...

Mat_<double>CV_64F...


存储的数据类型反映的是数据的存储深度,这是一个不太好理解的概念,简而言之,数据在矩阵的中存储深度代表内存存储一个元素所要使用的空间大小,举例:一个CV_8UC1的矩阵存储一个元素使用8bit空间,同理,32u表示使用32bit空间;


既然数据元素是存放在mat中,我们就有必要好好了解一下mat;

在未将数据放进去之前,我们先自己创建一个对象,知道一下mat对象由那些要素组成?


1)创建Mat矩阵

创建对象使用c++中的构造函数:常见的如下

  1. Mat::Mat(); //default  
  2. Mat::Mat(int rows, int cols, int type);  //cv::Mat M(100, 90, CV_32FC3); 
  3. Mat::Mat(Size size, int type);  
  4. Mat::Mat(int rows, int cols, int type, const Scalar& s);  //cv::Mat M(7,7,CV_32FC2,Scalar(1,3));  
  5. Mat::Mat(Size size, int type, const Scalar& s);
  6. Mat(int ndims, const int* sizes, int type, const Scalar& s);  
  7. //double m[3][3] = {{a, b, c}, {d, e, f}, {g, h, i}};  
  8. //cv::Mat M = cv::Mat(3, 3, CV_64F, m);  
  9. Mat::Mat(const Mat& m); 

参数说明:rows表示矩阵和行数,cols几列;type表示元素的类型,是CV_8UC1,CV_8UC3等等;

size表示矩阵尺寸如果使用img.size()来返回,则返回的是矩阵中一共有多少个元素 ;??????

const scalar &s用于初始化元素值,当然对于图像来说,就等于给图像中每个像素初始化了颜色。

Mat &m :copy矩阵头给新的mat对象,不复制数据,只复制头。其实就是引用。




查找图像中的任意一点位置,读取图像中任意一点。


这里有一个地方需要注意的:

图片的像素属性   与 图片的矩阵的属性   中的是不同的;

矩阵的列=像素的列*通道数;

在操作图像的像素(pixels)时,本质上是操作图像的矩阵(matrix);

在Mat中我们怎么区分与定义这些名词的区别?这一点容易出错,在下面的例子中,我们来区分两者的不同。

step定义了矩阵的整体布局,根据它,我们就能找到mat中每一个像素

在下面的解释中,人为的定义了两个概念:

1)像素阵

2)矩阵


举例:定义一个像素阵:

int size[]={5,8,10}

Mat img(3,size,CV_8UC3,Scalar::all(0);

如图所示:

如何操作图像的像素--Mat和vector

首先,这个像素阵是3通道的,我们在这里将这个3通道的像素阵分解成一个3个1通道的像素阵(=矩阵)

这3个矩阵的大小是相同的,

step求像素阵空间大小;以一个数组的形式出现。它定义了矩阵的整体布局;

step[0]、step[1]、step[2],step[0]表示面的大小,step[1]表示行的大小,step[2]表示像素的大小。

如果是三通道的话,step就是三维数组,

step[2]表示像素阵的元素的占用内存空间的大小,step[2]=1byte(8U)*3(C3)=3byte

step[1]表示像素阵一行占用的内存空间的大小,step[1]=像素阵中的元素大小step[2]*cols=3byte*10=30byte

step[0]表示像素阵占用的内存空间的大小,step[0]=step[1]*rows=30byte*8=240byte

注:rows,cols专指像素阵的行列数。


size求像素阵元素的个数;

size[2]==1;

size[1]表示像素阵的一行元素个数,size[1]=10;

size[0]表示像素阵元素总数,size[0]=size[1]*rows=10*8=80;


step1

elemSize()求像素占内存空间的大小

示例中像素为3通道,8U,所以elemSize()=3*1byte=3byte

elemSize1()求矩阵值(像素中的一个元素)占用内存空间的大小

elemSize1()=1byte

可以看出,通过step的寻址方式是以像素为单位去找的(非矩阵),它要操作的是像素,最小单位是像素,

所以step[1]对应的是像素大小;而通过elemSize1的寻址方式是以通道为最小单位,它要操作的通道,所以elemSize对应的是每个通道的大小

举例:

如何操作图像的像素--Mat和vector

如图一个rows=5,cols=3的2维像素集合。数据类型为CV_8UC3,首先它是一个3通道数据。


现在我们想访问像素M(4,2)【下标从0开始】。看看step如何操作实现找到数据的?


现在M点的地址=M.data+M.step[0]*rows+M.step[1]*cols;

其中,rows=4,cols=2;M.data指向矩阵的其实地址,也就是左上角的第一个蓝色。图中颜色加深一点。

step[0]表示的是每一行元素的数据大小,就是一行一共有多少个元素,本例中为9个。

M.data+M.step[0]*rows,这时候指针就跑到了第五行第一个蓝色那里。

step[1]表示的是一个元素的数据大小,也就是一个像素由多少个8bit排列起来,显然这里是3,

所以M.data+M.step[0]*rows+M.step[1]*cols;最终指针指向最后一个像素点的B通道。


这里我们来总结一下:

对于任意一幅图片,我们要熟悉图片的各属性:

图片属性 属性值
img.width()
img.height()
通道 img.channels()
  img.dims
   
   
   
   
   



图片中数据属性

   
img.rows() 行数
img.cols() 列数
img.data 图像第一个数据的指针
img.depth() 图像深度
img.step  
   
img.elemSize() 单个像素的大小(三个通道组成,也就是3
img.elemSize1() 每个通道数据大小
   




2)复制Mat矩阵

Mat矩阵的复制分为浅复制和深复制,又叫部分复制和完全复制。

浅复制只复制Mat的第一部分即,矩阵头,不复制数据,复制和被复制者是数据共享的,换句话说,改变了被复制对象的元素值,同样会改变复制者的值。

深复制则是复制了数据,也是我们传统意义上的复制。深拷贝使用clone()或者copyTo();

  1. Mat a;  
  2. Mat b = a; //a "copy" to b  
  3. Mat c(a); //a "copy" to c  
  4.   
  5. //注意:深拷贝  
  6. Mat a;  
  7. Mat b = a.clone(); //a copy to b  
  8. Mat c;  
  9. a.copyTo(c); //a copy to c 




重点部分:开始操作图像中的矩阵:



操作图像中的单个元素;

1)at()方法

at(int y,int x)

at()函数用来存取图像中对应坐标为(x,y)的像素(非矩阵)坐标。有一点需要注意的就是,编译期必须要已知图像的数据类型,因为mat类可以存放任意的数据类型,因此at方法的实现是用模板函数实现。

举例:

①已知图像数据类型为uchar,灰度图,要求对(10,12)的像素重新赋值为100;


img.at<uchar>(12,10) = 100;

注:必须已知图像类型at<type>;注意括号里面的坐标是先y,再x;

②已知图像数据类型为uchar,BGR彩色图,要求对(10,12)的像素重新赋值为100;


  1. img.at<cv::Vec3b>(12,10) [0]= 100;//B  
  2. img.at< cv::Vec3b >(12,10) [1]= 100;//G  
  3. img.at< cv::Vec3b >(12,10) [2]= 100;//R

解释Vec3b:

  vector: C++中的一种数据结构,确切的说是一个类.它相当于一个动态的数组,当程序员无法知道自己需要的数组 的规 模多大时,用其来解决问题可以达到最大节约空间的目的.
用法:
1.文件包含:     
           首先在程序开头处加上#include<vector>以包含所需要的类文件vector,还有一定要加上using namespace std;

2.声明一个int向量以替代一维的数组:vector <int> a;(等于声明了一个int数组a[],大小没有指定,可以动态的向 里面添加删除。

比如Vec<uchar, 3>:
其实这句就是定义一个uchar类型的数组,长度为3而已,例如 8U 类型的 RGB 彩色图像可以使用 <Vec3b>,3 通 道 float 类型的矩阵可以使用 <Vec3f>。对于 Vec 对象,可以使用[]符号如操作数组般读写其元素,如:Vec3b color; //用 color 变量描述一种 RGB 颜色
color[0]=255; //0通道的B 分量
color[1]=0; //1通道的G 分量
color[2]=0; //2通道的R 分量

  1. //最普通的操作方式at()遍历所有像素,并且对其进行操作 
  2. int _tmain(int argc, _TCHAR* argv[]) 
  3. { 
  4.     Mat grayimg(256,480,CV_8UC1); 
  5.     Mat colorimg(256,480,CV_8UC3); 
  6.      
  7.     for (int i=0;i<grayimg.rows;i++) 
  8.     { 
  9.         for (int j=0;j<grayimg.cols;j++) 
  10.         { 
  11.             grayimg.at<uchar>(i,j)=0;//对灰度图像素操作赋 
  12.  
  13.  
  14.         } 
  15.     } 
  16.  
  17.     for (int i=0;i<colorimg.rows;i++) 
  18.     { 
  19.         for (int j=0;j<colorimg.cols;j++) 
  20.         { 
  21.             Vec3b pixel; 
  22.             pixel[0]=255; 
  23.             pixel[1]=155; 
  24.             pixel[2]=50; 
  25.             colorimg.at<Vec3b>(i,j)=pixel; 
  26.         } 
  27.     } 
  28.  
  29.     cout<<grayimg.channels(); 
  30.     imshow("grayimg",grayimg); 
  31.     imshow("colorimg",colorimg); 
  32.     waitKey(0); 
  33.     return 0; 
  34. } 

2)使用mat类的ptr()方法配合cols,rows,step,elemsize成员变量,直接进行指针操作。

cols:图像的列数;

rows:图像的行数;

step:图像的宽度;以字节为单位

elemsize 像素的大小;(elemsize1()像素的元素大小);以字节为单位。


ptr()方法同样是一个模板类,需要事先知道像素点的类型;


  1. 用指针遍历所有像素 
  2. int _tmain(int argc, _TCHAR* argv[]) 
  3. { 
  4.     Mat grayimg(256,480,CV_8UC1); 
  5.     Mat colorimg(256,480,CV_8UC3); 
  6.  
  7.     for (int i=0;i<grayimg.rows;i++) 
  8.     { 
  9.         uchar *p=grayimg.ptr<uchar>(i);//获取第i行的第一个像素 
  10.         for (int j=0;j<grayimg.cols;j++) 
  11.         { 
  12.             p[j]=(i+j)%255; 
  13.         } 
  14.     } 
  15.     //注意ptr是一个函数,其尖括号是在指明他的类型 
  16.     for (int i=0;i<colorimg.rows;i++) 
  17.     { 
  18.         Vec3b *p=colorimg.ptr<Vec3b>(i); 
  19.         for (int j=0;j<colorimg.cols;j++) 
  20.         { 
  21.             p[0]=255; 
  22.             p[1]=155; 
  23.             p[2]=50; 
  24.         } 
  25.     } 
  26.  
  27.     cout<<grayimg.channels(); 
  28.     imshow("grayimg",grayimg); 
  29.     imshow("colorimg",colorimg); 
  30.     waitKey(0); 
  31.     return 0; 
  32. } 


3)使用迭代器:Interator

  1. //使用迭代器遍历所有像素,并且对其进行操作 
  2. int _tmain(int argc, _TCHAR* argv[]) 
  3. { 
  4.     Mat grayimg(256,480,CV_8UC1); 
  5.     Mat colorimg(256,480,CV_8UC3); 
  6.  
  7.     MatIterator_<uchar> grayst,grayend; 
  8.     grayst=grayimg.begin<uchar>(); 
  9.     grayend=grayimg.end<uchar>(); 
  10.  
  11.     for (;grayst!=grayend;grayst++) 
  12.     { 
  13.         *grayst=rand()%255; 
  14.     } 
  15.  
  16.     MatIterator_<Vec3b> Colorst,Colorend; 
  17.     Colorst=colorimg.begin<Vec3b>(); 
  18.     Colorend=colorimg.end<Vec3b>(); 
  19.     for (;Colorst!=Colorend;Colorst++) 
  20.     { 
  21.         (*Colorst)[0]=rand()%255; 
  22.         (*Colorst)[1]=rand()%255; 
  23.         (*Colorst)[2]=rand()%255; 
  24.     } 
  25.  
  26.     imshow("grayimg",grayimg); 
  27.     imshow("colorimg",colorimg); 
  28.     waitKey(0); 
  29. } 



然而,OpenCV里面已经有了相应函数可以让我们更加方便地对像素进行操作,那便是LUT函数,而且推荐使用OpenCV的内建函数,因为已经针对芯片做了优化设计,使得速度有很大提升。


函数原型为:void LUT(InputArray src, InputArray lut, OutputArray dst, int interpolation=0 )

实现的映射关系如下所示:

如何操作图像的像素--Mat和vector

也就是说比如原来src中值为1会映射为table[1]所对应的值再加上d。

所以上面的操作,我们其实只需要使用LUT函数就可以了。结合我们自己设计的table表,就能够实现对图像的操作。


  1. int main()  
  2. {  
  3.     string picName="lena.jpg";  
  4.     Mat A=imread (picName,CV_LOAD_IMAGE_GRAYSCALE);    //读入灰度图像  
  5.     Mat lookUpLut(1,256,CV_8UC1);   //建立一个256个元素的映射表  
  6.     imshow ("变换前",A);  
  7.     for (int i=0;i<256;i++)   //
  8.     {   //往table中填数据
  9.         lookUpLut.at<uchar>(i)=i/100*100;   //
  10.     }   //
  11.     Mat B;  
  12.     LUT (A,lookUpLut,B);  
  13.     imshow ("变换后",B);  
  14.     waitKey ();  
  15.     return 0;  
  16. }