OpenCV C++(二)----图像数字化
一、Mat类
Mat
:Matrix
的缩写,代表矩阵或者数组的意思。该 类的声明在头文件opencv2\core\core.hpp中, 所以使用Mat
类时要引入该头文件。
1.1、Mat类的构造函数
构造Mat
对象相当于构造了一个矩阵(数组),需要四个基本要素:行数(高)、列数(宽)、通道数及其数据类型。
Mat(int rows, int cols, int type);
其中,
- rows代表矩阵的行数
- cols代表矩阵的列数
- type代表类型, 包括通道数及其 数据类型, 可以设置为
CV_8UC(n)
、CV_8SC(n)
、CV_16SC(n)
、CV_16UC(n)
、CV_32SC(n)
、CV_32FC(n)
、CV_64FC(n)
。
其中8U
、8S
、16S
、16U
、32S
、32F
、64F
前面的数字代表Mat中每一个数值所占的bit数, 而 1byte=8bit, 所以,32F
就是占4字节的float类型,64F
是占8字节的doule类型,32S
是占4字 节的int类型,8U
是占1字节的uchar类型, 其他的类似;
C(n)
代表通道数,当n=1时, 即构造单通道矩阵或称二维矩阵, 当n>1时, 即构造多通道矩阵即三维矩阵, 直观上就是n个二维矩阵组成的三维矩阵。
Mat(Size size, int type);
其中,需要注意的是, Size的第一个元素是矩阵的列数(宽),第二个元素是矩阵的行数(高),即先存 宽, 再存高。即Size(int cols,int rows)
Mat m;
m.create(2,3,CV_32FC1);
m.create(Size(3,2),CV_32FC1);
1.2、初始化Mat类
Mat o=Mat::ones(2,3,CV_32FC1);
Mat m=Mat::zeros(Size(3,2),CV_32FC1);
Mat m=(Mat_<int>(2,3)<<1,2,3,4,5,6);
1.3、获取单通道Mat的基本信息
1. 使用成员变量rows和 cols获取矩阵的行数和列数
//构造矩阵
Mat m=(Mat_<int>(3,2)<<1,2,3,4,5,6);
//矩阵的行数
cout<<"行数:"<<m.rows<<endl;
//矩阵的列数
cout<<"列数:"<<m.cols<<endl;
2. 使用成员函数size() 获取矩阵的尺寸
Size size=m.size();
cout<<"尺寸:"<<size<<endl;
3. 使用成员函数 channels() 得到矩阵的通道数
cout<<"通道数:"<<m.channels()<<endl;
4. 使用成员函数 total()得到矩阵的行数乘以列数, 即面积。 注意和通道数无关, 返回的不是矩阵中数据的个数
cout<<"面积:"<<m.total()<<endl;
5. 使用成员变量 dims 得到矩阵的维数。 显然对于单通道矩阵来说就是一个二维矩阵, 对于多通道矩 阵来说就是一个三维矩阵。
cout<<"维数:"<<m.dims<<endl;
1.4、访问单通道Mat对象中的值
1. 利用成员函数at
//构造单通道矩阵
Mat m=(Mat_<int>(3,2)<<11,22,33,44,55,66);
//通过for循环打印M中的每一个值
for(int r=0;r<m.rows;r++)
{
for(int c=0;c<m.cols;c++)
{
cout<<m.at<int>(r,c)<<",";//第r行第c列的值
cout<<m.at<int>(Point(c,r))<<",";//等价于上面一行代码
}
cout<<endl'
}
2. 利用成员函数ptr
对于Mat
中的数值在内存中的存储, 每一行的值是存储在连续的内存区域中的, 通过成员函数ptr
获得指向每一行首地址的指针。 仍以“利用成员函数at
”部分的m存储为例, m中所有的值在内存中的存储方式如图2-1所示, 其中如果行与行之间的存储是有内存间隔的, 那么间隔也是相等的。
for(int=0;r<m.rows;r++)
{
//得到矩阵m的第r行行首的地址
const int *ptr=m.ptr<int>(r);
//打印第r行的所有值
for(int c=0;c<m.cols;c++)
{
cout<<ptr[c]<<",";
}
cout<<endl;
}
1. 利用成员函数 isContinuous和ptr
每一行的所有值存储在连续的内存区域中, 行与行之间可能会有间隔, 如果isContinuous
返回值为true
, 则代表行与行之间也是连续存储的, 即所有的值都是连续存储的。
if(m.isContinuous())
{
//得到矩阵m的第一个值的地址
int *ptr=m.ptr<int>(0);
for(int n=0;c<m.rows*m.cols;n++)
{
cout<<ptr[n]<<",";
}
}
2. 利用成员变量step和data
对于单通道矩阵来说,step[0]
代表每一行所占的字节数,而如果有间隔的话, 这个间隔也作为字节数的一部分被计算在内;step[1]
代表每一个数值所占的字节数,data
是指向第一个数值 的指针, 类型为uchar
。 所以, 无论哪一种情况, 如访问一个int
类型的单通到矩阵的第r行 第c列的值, 都可以通过以下代码来实现。
*((int*)(m.data+m.step[0]*r+c*m.step[1]))
1.5、向量类Vec
默认是列向量
//构造一个长度为3,数据类型为int并且初始化为11、22、33的列向量
Vec<int,3> vi(11,22,33);
cout<<"向量的行数"<<vi.rows<<endl;
cout<<"向量的列数<<vi.cols<<endl;
cout<<"访问滴0个元素:"<<vi[0]<<endl;
OpenCV为向量类的声明取了一个别名,在matx.hpp
401行开始 例如:
typedef Vec<uchar, 2> Vec2b;
typedef Vec<uchar, 3> Vec3b;
typedef Vec<uchar, 4> Vec4b;
typedef Vec<int, 2> Vec2i;
typedef Vec<int, 3> Vec3i;
...
单通道矩阵的每一个元素都是一个数值, 多通道矩阵的每一个元素都可以看作一个向量。
1.6、构造多通道的Mat对象
Mat mm=(Mat_<Vec3f>(2,2)<<Vec3f(1,1,1),Vec3f(2,2,2),Vec3f(3,3,3),Vec3f(4,4,4));
//打印第0行第0列的元素值
int r=0;
int c=0;
cout<<mm.at<Vec3f>(r,c)<<endl;
其余同单通道方法,只是类型变成了向量Vec
。
(1)分离通道
vector<Mat> planes;
split(mm,planes);
(2)合并通道
//三个单通道矩阵
Mat plane0=(Mat_<int>(2,2)(1,2,3,4);
Mat plane1=(Mat_<int>(2,2)(11,12,13,14);
Mat plane2=(Mat_<int>(2,2)(21,22,23,24);
//用三个单通道矩阵初始化一个数组
Mat plane[]={plane0,plane1,plane2};
Mat mat;
merge(plane,3,mat);
//将三个单通道矩阵一次放入vector容器中
vector<Mat> plane;
plane.push_back(plane0);
plane.push_back(plane1);
plane.push_back(plane2);
Mat mat;
merge(plane,mat);
1.7、获得Mat中某一区域的值
1. 使用成员函数row(i) 或 col(j) 得到矩阵的第i行或者第 j列
2. 使用成员函数rowRange或 colRange得到矩阵的连续行或者连续列
Range(int _start,int _end);
这是一个左闭右开的序列[_start, _end),比如Range(2, 5)
其实产生的是2、 3、 4 的序列,不包括5, 常用作rowRange
和colRange
的输入参数,从而访问矩阵中的连续行或者连续列
Mat r_range=mm.rowRange(Range(2,4));
//Mat r_range=mm.rowRange(2,4);等价于上面
for(int r=0;r<r_range.rows;r++)
{
for(int c=0;c<r_range.cols;c++)
{
cout<<r_range.at<int>(r,c)<<",";
}
cout<<endl;
}
需要特别注意的是, 成员函数row
、 col
、 rowRange
、 colRange
返回的矩阵其实是指向原矩阵的;有时候, 我们只访问原矩阵的某些行或列, 但是不改变原矩阵的值,需要使用复制的方法
3. 使用成员函数 clone和copy To
Mat r_range=mm.rowRange(2,4).clone();
Mat c_range=mm.colRange(1,3).copyTo(c_range);
4. 使用 Rect类
Rect的构造函数
Rect_(_Tp _x, _Tp _y, _Tp _width, _Tp _height);
Rect_(const Rect_& r);
Rect_(const Point_<_Tp>& org, const Size_<_Tp>& sz);
Rect_(const Point_<_Tp>& pt1, const Point_<_Tp>& pt2);
Mat ROI1=mm(Rect(2,1,2,2));
Mat roi2=mm(Rect(Point(2,1),Size(2,2)));
Mat roi3=mm(Rect(Point(2,1),Point(3,2)));
但是与使用colRange
和rowRange
类似, 这样得到的矩形区域是指向原矩阵的, 要改变roi
中的值, matrix
也会发生变化, 如果不想这样, 则仍然可以使用clone
或者copyTo
。
二、矩阵的运算
2.1、加法运算
矩阵的加法就是两个矩阵对应位置的数值相加
Mat src1=(Mat_<uchar>(2,2)<<11,22,33,60);
Mat src2=(Mat_<uchar>(2,2)<<191,192,193,204);
Mat dst=src1+src2;
注意:
- 60+204=264,但是,实际打印出来的值是255,因为两个矩阵的数据类 型都是uchar, 所以用“+”运算符计算出来的和也是uchar类型的, 但是uchar类型范围的最大值是255, 所以只好将264截断为255。
- 两个Mat的数据类型必须是一 样的,否则会报错,也就是用“+”求和是比较严格的 。
- 一个数值与一个Mat对象相 加, 也可以使用“+”运算符, 但是无论这个数值是什么数据类型, 返回的
Mat
的数据类型 都与输入的Mat相同 。
为了弥补“+”运算符的这两个缺点, 我们可以使用OpenCV提供的另一 个函数
void add(InputArray src1, InputArray src2, OutputArray dst,InputArray mask = noArray(), int dtype = -1);
使用add函数时, 输入矩阵的数据类型可以不同, 而输出矩阵的数据类型 可以根据情况自行指定。 需要特别注意的是, 如果给dtype
赋值为-1, 则表示dst
的数据类型和src1
、 src2
是相同的, 也就是只有当src1
和src2
的数据类型相同时,才有可能令 dty pe=-1
,否则仍然会报错。
Mat dst;
add(src1,src2,dst,Mat(),CV_64FC1);
2.2、减法运算
矩阵的减法与加法类似
Mat dst=src1-src2;
注意:
- 输出值不会最小为0,这是 因为
src1
和src2
均是uchar
类型的, 所以返回的dst
也是uchar
类型的;而uchar
类型的最小范围是0, 所以会将小于0的数值截断为0。 - Mat对象与一个数值相减, 也可以使用“-”运算符。
当然, 也存在与“+”运算符 一样的不足, OpenCV提供的函数:
void subtract(InputArray src1, InputArray src2, OutputArray dst,
InputArray mask = noArray(), int dtype = -1);
可以实现不同的数据类型的Mat
之间做减法运算, 其与add函数类似。
2.3、点乘运算
矩阵的点乘即两个矩阵对应位置的数值相乘。
Mat dst=src1.mul(src2);
注意
从打印结果就可以看出,也是对大于255的数值做了截断处理。 所以为了不损失精度,可以将两个矩阵设置为int
、 float
等数值范围更大的数据类型。
对于Mat
的点乘, 也可以利用OpenCV提供的函数:
void multiply(InputArray src1, InputArray src2,
OutputArray dst, double scale = 1, int dtype = -1);
这里的dst=sclae*src1*src2
, 即在点乘结果的基础上还可以再乘以系数scale
。
2.4、点除运算
点除运算与点乘运算类似, 是两个矩阵对应位置的数值相除。
Mat dst=src2/src1;
注意
- 除数为0没有意义,但是OpenCV在 处理这种分母为0的除法运算时,默认得到的值为0。
- 用一个数值与Mat对象相除也可以使用“/”运算符, 且返回的Mat的数据类型与输入的Mat的数据类型相同, 与输入数值 的数据类型是没有关系的。
对于Mat
的点除, 也可以利用OpenCV提供的函数:
divide(InputArray src1, InputArray src2, OutputArray dst,
double scale = 1, int dtype = -1);
2.5、乘法运算
相当于卷积
Mat dst=src1*src2;
注意:
- 对于
Mat
对象的乘法, 需要注意两个Mat
只能同时是float
或者double
类型, 对于其他数据类型的矩阵做乘法会报错。 - 两个双通道矩阵也可以相乘,这里是把Mat对象当做了复数矩阵,其中第一个通道存放的是所有值的实部,第二个通道存放的是对应的每一个虚部,也就是将
Vec2f
看作一个复数, 比如Vec2f(1, 2)
可以看作1+2i
。
对于Mat的乘法, 还可以使用OpenCV提供的gemm
函数来实现。
void gemm(InputArray src1, InputArray src2, double alpha,
InputArray src3, double beta, OutputArray dst, int flags = 0);
注意:gemm
也只能接受CV_32FC1
、 CV_64FC1
、 CV_32FC2
、 CV_64FC2
数据类型的Mat
该函数通过flags
控制src1
、 src2
、 src3
是否转置来实现矩阵之间不同的运算, 当将flags
设置为不同的参数时, 输出矩阵为:
当然,flags
可以组合使用, 比如需要src2
和src3
都进行转置, 则令flags=GEMM_2_T+GEMM_3_T
。
2.6、其他运算
开平方运算
void sqrt(InputArray src, OutputArray dst);
注意:sqrt
的输入矩阵的数据类型只能是 CV_32F
或者CV_64F
幂指数运算
void pow(InputArray src, double power, OutputArray dst);
三、灰度图像数字化
Mat imread( const String& filename, int flags = IMREAD_COLOR );
void imshow(const String& winname, InputArray mat);
四、彩色图像数字化
灰度图像的每一个像素都是由一个数字量化的, 而彩色图像的每一个像素都是由三个数字组成的向量量化的。 最常用的是由R
、 G
、B
三个分量来量化的, RGB
模型使用加 性色彩混合以获知需要发出什么样的光来产生给定的色彩, 源于使用阴极射线管(CRT) 的彩色电视, 具体色彩的值用三个元素的向量来表示, 这三个元素的数值分别代表三种基色: Red、Green、Blue的亮度。 假设每种基色的数值量化成m=2^n个数, 如同8位灰度 图像一样, 将灰度量化成28=256个数。 RGB图像的红、绿、蓝三个通道的图像都是一张8 位图, 因此颜色的总数为2563 =16777216, 如(0, 0, 0) 代表黑色,(255, 255, 255) 代表白色, (255, 0, 0) 代表红色。
对于彩色图像的每一个方格, 我们可以理解为一个Vec3b
。 需要注意的是, 每一个像素的向量不是按照R
、 G
、 B
分量排列的, 而是按照B
、 G
、 R
顺序排列的, 所以通过split
函数分离通道后, 先后得到的是B
、 G
、 R
通道。
Mat img=imread("apple.jpg",CV_LOAD_IMAGE_GRAYSCALE);
if(img.empty())
{
return -1;
}
imshow("BGR",img);
vector<Mat> planes;
split(img,planes);
imshow("B",planes[0]);
imshow("G",planes[1]);
imshow("R",planes[2]);
waitKey(0);
在OpenCV中实现将彩色像素(一个向量) 转化为灰度像素(一个数值) 的公式如 下: