1. 图像数字化
1.1 初始OpenCV中的Mat类
1.1.1 初始Mat
Mat类的构造函数如下:
Mat(int rows, int cols, int type)
其中,type代表类型,包括通道数及其数据类型。
CV_8UC(n):占1字节的uchar类型
CV_8SC(n):占1字节的char类型
CV_16UC(n):占2字节的ushort类型
CV_16SC(n):占2字节的short类型
CV_32SC(n):占4字节的int类型
CV_32FC(n):占4字节的float类型
CV_64FC(n):占8字节的double类型
S–代表–signed int-----有符号整形
U–代表–unsigned int-----无符号整形
F–代表–float-----单精度浮点型
C(n)代表通道数,这里括号可省略。
n=1:单通道矩阵/二维矩阵/灰度图片
n=3:三通道矩阵/三维矩阵/RGB彩色图片
n=4:四通道矩阵/三维矩阵/带Alph通道的RGB图片
Mat的构造函数也可采用如下形式:
Mat(Size(int cols, int rows), int type)
其中使用了OpenCV的Size类。需要注意的是,Size的顺序是列×行。
1.1.2 构造单通道Mat对象
Mat对象的构造有如下三种方法:
//构造2行3列的矩阵
Mat m = Mat(2, 3, CV_32FC1);
//也可以直接借助Size对象
Mat m = Mat(Size(3, 2), CV_32FC1);
//也可以使用Mat中的成员函数create完成Mat对象的构造
Mat m;
m.create(2, 3, CV_32FC1);
m.create(Size(3, 2), CV_32FC1);
0矩阵的构造方式
Mat o = Mat::zeros(2, 3, CV_32FC1);
Mat o = Mat::zeros(Size(3, 2), CV_32FC1);
1矩阵的构造方式
Mat m = Mat::ones(2, 3, CV_32FC1);
Mat m = Mat::ones(Size(3, 2), CV_32FC1);
快速创建矩阵的方式
Mat m = (Mat_<int>(2,3) << 1,2,3,4,5,6); //2行3列
1.1.3 获得单通道Mat的基本信息
Mat m = (Mat_<int>(2,3) << 1,2,3,4,5,6);
//获取矩阵的行数
cout << m.rows << endl;
//获取矩阵的列数
cout << m.cols << endl;
//获取矩阵的尺寸
Size size = m.size();
cout << size << endl; //[2×3]
//获取矩阵的通道数
cout << m.channels() << endl;
//获取矩阵的面积(矩阵的行数乘以列数)
cout << m.total() << endl;
//获取矩阵的维数(单通道矩阵维数为2,多通道矩阵维数为3)
cout << m.dims << endl;
1.1.4 访问单通道Mat对象中的值
1. 利用成员函数at
//格式
m.at<float>(r, c); //访问第r行第c列的值
2. 利用成员函数ptr
对于Mat中的数值在内存中的存储,每一行的值是存储在连续的内存区域中的,通过成员函数ptr获得指向每一行首地址的指针。
可以利用成员函数ptr返回的指针访问m中的值。
for(int r = 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;
}
3. 利用成员函数isContinuous和ptr
对于Mat中的数值在内存中的存储,每一行的值是存储在连续的内存区域中的,但是行与行之间可能有间隔。
如果isContinuous返回值为true,则代表行与行之间也是连续存储的,即所有的值都是连续存储的。用法如下:
if(m.isContinuous())
{
//得到矩阵m的第一个值的地址
int * ptr = m.ptr<int>(0);
//利用操作符[]取值
for(int n = 0; n < m.rows * m.cols; n++)
cout << ptr[n] << ",";
}
4. 利用成员变量step和data
step[0]:表示每一行所占的字节数(如果行与行之间有间隔的话,包括间隔)
step[1]:表示每一个数值所占的字节数
data:表示指向第一个数值的指针
//访问一个int类型的单通道矩阵的第r行第c列的值
*((int *)(m.data + m.step[0] * r + m.step[1] * c));
总结
从取值效率上说,直接使用指针的形式取值是最快的,使用at是最慢的;
从可读性上说,使用at是最高的,直接使用指针的形式取值是最低的。
1.1.5 向量类Vec
这里的向量可以理解为数学意义上的列向量
//构造一个_cn×1的列向量,数据类型为_Tp
Vec<Typename _Tp, int _cn>;
//构造一个3×1的列向量,数据类型为int,初始化为21、32、14
Vec<int, 3> vi(21, 32, 14);
利用[]或者()都可以访问向量中的值
cout << vi[0] << endl;
cout << vi(1) << endl;
OpenCV为向量类的声明取了一个别名,例如:
typedef Vec<uchar, 3> Vec3b;
typedef Vec<int, 2> Vec2i;
typedef Vec<float, 4> Vec4f;
typedef Vec<double, 3> Vec3d;
...
//具体可查看opencv2/core/core.hpp
1.1.6 构造多通道Mat对象
//构造一个2行2列的float类型的三通道矩阵
Mat mm = (Mat_<Vec3f>(2,2)<<Vec3f(1,11,21), Vec3f(2,12,32), Vec3f(3,13,23), Vec3f(4,24,34));
1.1.7 访问多通道Mat对象中的值
1. 利用成员函数at
//获得第r行第c列的元素值
cout << mm.at<Vec3f>(r,c) << ","; //[1,11,21],[2,12,32],[3,13,23],[4,24,34]
2. 利用成员函数ptr
for(int r = 0; r < mm.rows; r++)
{
//得到矩阵mm的第r行首的地址
Vec3f * ptr = m.ptr<Vec3f>(r);
//打印第r行的所有值
for(int c = 0; c < mm.cols; c++)
{
cout << ptr[c] << ","; //打印结果与使用成员函数at是相同的
}
cout << endl;
}
3. 利用成员函数isContinuous和ptr
if(mm.isContinuous())
{
//得到矩阵m的第一个值的地址
Vec3f * ptr = mm.ptr<Vec3f>(0);
//利用操作符[]取值
for(int n = 0; n < mm.rows * mm.cols; n++)
cout << ptr[n] << ",";
}
4. 利用成员变量data和step
//访问一个int类型的单通道矩阵的第r行第c列的值
*((Vec3f *)(mm.data + mm.step[0] * r + mm.step[1] * c));
5. 分离通道
void cv::split(cv::InputArray m, cv::OutputArrayOfArrays mv)
Mat mm = (Mat_<Vec3f>(2,2)<<Vec3f(1,11,21),Vec3f(2,12,32),Vec3f(3,13,23),Vec3f(4,24,34));
vector<Mat> planes;
split(mm,planes);
cout<<planes[1].at<float>(0,0)<<endl; //11
若原矩阵为三通道矩阵,如mm。则分离后:
planes[0]:代表每一个像素点第1个数组成的单通道矩阵
planes[1]:代表每一个像素点第2个数组成的单通道矩阵
planes[2]:代表每一个像素点第3个数组成的单通道矩阵
6. 合并通道
void cv::merge(const cv::Mat *mv, size_t count, cv::OutputArray dst)
//三个单通道矩阵
Mat plane0 = (Mat_<int>(2,2) << 1, 2, 3, 4);
Mat plane1 = (Mat_<int>(2,2) << 5, 6, 7, 8);
Mat plane2 = (Mat_<int>(2,2) << 9, 10, 11, 12);
//用三个单通道矩阵初始化一个数组
Mat plane[] = {plane0, plane1, plane2};
//合并为一个多通道矩阵
Mat mat;
merge(plane, 3, mat);
也可以:
//void merge(InputArrayOfArrays mv, OutputArray dst)
//三个单通道矩阵
Mat plane0 = (Mat_<int>(2,2) << 1, 2, 3, 4);
Mat plane1 = (Mat_<int>(2,2) << 5, 6, 7, 8);
Mat plane2 = (Mat_<int>(2,2) << 9, 10, 11, 12);
//将三个单通道矩阵依次放入vector容器中
vector<Mat> plane;
plane.push_back(plane0);
plane.push_back(plane1);
plane.push_back(plane2);
//合并为一个多通道矩阵
Mat mat;
merge(plane, mat);
1.1.8 获得Mat中某一区域的值
1. 使用成员函数row(i)或col(j)得到矩阵的第i行或者第j列
int r = 1;
int c = 0;
//矩阵的第r行
Mat mr = m.row(r);
//矩阵的第c列
Mat mc = m.col(c);
注意:返回值仍然是一个单通道的Mat类型
2. 使用成员函数rowRange或colRange得到矩阵的连续行或者连续列
Mat matrix = (Mat_<int>(5,5)<<1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25);
//使用OpenCV的Range类,用于构造[_start,_end)的连续整数序列
Mat r_range = matrix.rowRange(Range(2,4)); //索引从0开始
//也可以省略Range()
Mat r_range = matrix.rowRange(2,4);
Mat c_range = matrix.colRange(1,3);
注意:此时若更改r_range或c_range的某个值,原矩阵也会发生改变。
3. 使用成员函数clone和copyTo
可以解决上一点提到的问题。
//clone
Mat r_range = matrix.rowRange(2, 4).clone();
//copyTo
Mat r_range;
matrix.rowRange(2, 4).copyTo(r_range);
注意:此时若更改r_range或c_range的某个值,原矩阵不会发生改变。
4. 使用Rect类
Mat roi1 = matrix(Rect(Point(2,1),Point(3,2))).clone(); //左上角的坐标,右下角的坐标
Mat roi2 = matrix(Rect(2,1,2,2).clone(); //x,y,宽度,高度
Mat roi3 = matrix(Rect(Point(2,1),Size(2,2))).clone(); //左上角的坐标,尺寸
1.2 矩阵的运算
1.2.1 加法运算
Mat src1 = (Mat_<uchar>(2,3) << 23, 123, 90, 100, 250, 0);
Mat src2 = (Mat_<uchar>(2,3) << 125, 150, 60, 100, 10, 40);
Mat dst = src1 + src2;
注意:
- 两个矩阵的数据类型必须相同,否则会报错;
- 一个数值可以与一个Mat对象相加,但是无论这个数值是什么数据类型,返回的Mat的数据类型都与输入的Mat相同;
- 若相加的结果超出了某类型的最大值,可能会将结果截断为最大值。
为了解决加号+的问题,可以使用add
函数:
void cv::add(cv::InputArray src1, cv::InputArray src2, cv::OutputArray dst, cv::InputArray mask = noArray(), int dtype = -1)
Mat src1 = (Mat_<uchar>(2,3) << 23, 123, 90, 100, 250, 0);
Mat src2 = (Mat_<uchar>(2,3) << 125, 150, 60, 100, 10, 40);
Mat dst;
add(src1,src2,dst);
//输入矩阵的数据类型可以不同
//输出矩阵的数据类型可以根据情况自行指定
add(src1,src2,dst,Mat(),CV_64FC1);
注意:此处Mat()代表空的矩阵[],该参数值在需要掩模mask操作时会用到,其余时刻使用Mat()即可。
两个向量也可以做加法运算:
Vec3f v1 = Vec3f(1, 2, 3);
Vec3f v2 = Vec3f(10, 1, 12);
Vec3f v = v1 + v2; //[11,3,15]
1.2.2 减法运算
Mat src1 = (Mat_<uchar>(2,3) << 23, 123, 90, 100, 250, 0);
Mat src2 = (Mat_<uchar>(2,3) << 125, 150, 60, 100, 10, 40);
Mat dst = src1 - src2;
注意:
- 两个矩阵的数据类型必须相同,否则会报错;
- 一个Mat对象可以与一个数值相减,但是无论这个数值是什么数据类型,返回的Mat的数据类型都与输入的Mat相同;
- 若相加的结果超出了某类型的最小值,可能会将结果截断为最小值。
为了解决减号-的问题,可以使用subtract
函数:
void cv::subtract(cv::InputArray src1, cv::InputArray src2, cv::OutputArray dst, cv::InputArray mask = noArray(), int dtype = -1)
Mat src1 = (Mat_<uchar>(2,3) << 23, 123, 90, 100, 250, 0);
Mat src2 = (Mat_<uchar>(2,3) << 125, 150, 60, 100, 10, 40);
Mat dst;
subtract(src1,src2,dst);
//输入矩阵的数据类型可以不同
//输出矩阵的数据类型可以根据情况自行指定
subtract(src1,src2,dst,Mat(),CV_64FC1);
两个向量也可以做减法运算:
Vec3f v1 = Vec3f(1, 2, 3);
Vec3f v2 = Vec3f(10, 1, 12);
Vec3f v = v1 - v2; //[-9,1,-9]
1.2.3 点乘运算(对应位置的数值相乘)
Mat src1 = (Mat_<uchar>(2,3) << 23, 123, 90, 100, 250, 0);
Mat src2 = (Mat_<uchar>(2,3) << 125, 150, 60, 100, 10, 40);
Mat dst = src1.mul(src2);
注意:
- 两个矩阵的数据类型必须相同,否则会报错;
- 若相加的结果超出了某类型的最大值,可能会将结果截断为最大值。
为了解决.mul的问题,可以使用multiply
函数:
void cv::multiply(cv::InputArray src1, cv::InputArray src2, cv::OutputArray dst, double scale = (1.0), int dtype = -1)
Mat src1 = (Mat_<uchar>(2,3) << 23, 123, 90, 100, 250, 0);
Mat src2 = (Mat_<uchar>(2,3) << 125, 150, 60, 100, 10, 40);
Mat dst;
multiply(src1,src2,dst);
//输入矩阵的数据类型可以不同
//输出矩阵的数据类型可以根据情况自行指定
subtract(src1,src2,dst,1.0,CV_64FC1);
注意:这里的dst = scale * src1 * src2,即在点乘结果的基础上还需要再乘以系数scale。
1.2.4 点除运算(对应位置的数值相除)
Mat src1 = (Mat_<uchar>(2,3) << 23, 123, 90, 100, 250, 0);
Mat src2 = (Mat_<uchar>(2,3) << 125, 150, 60, 100, 10, 40);
Mat dst = src2 / src1;
/*
[ 5, 1, 1;
1, 0, 0]
*/
注意:
- 两个矩阵的数据类型必须相同,否则会报错;
- 若相除的结果超出了某类型的最大或最小值,可能会将结果截断为最大或最小值。
- 如果分母为0,则相除的结果默认为0。
为了解决相除运算符/的问题,可以使用divide
函数:
void cv::divide(cv::InputArray src1, cv::InputArray src2, cv::OutputArray dst, double scale = (1.0), int dtype = -1)
Mat src1 = (Mat_<uchar>(2,3) << 23, 123, 90, 100, 250, 0);
Mat src2 = (Mat_<uchar>(2,3) << 125, 150, 60, 100, 10, 40);
Mat dst;
divide(src2,src1,dst);
//输入矩阵的数据类型可以不同
//输出矩阵的数据类型可以根据情况自行指定
subtract(src2,src1,dst,1.0,CV_64FC1);
/*
[ 5, 1, 1;
1, 0, 0]
*/
注意:如果分母为0,则相除的结果默认为0。
1.2.5 乘法运算(线性代数中的点乘)
Mat src1 = (Mat_<float>(2,3) << 1, 2, 3, 4, 5, 6);
Mat src2 = (Mat_<float>(3,2) << 6, 5, 4, 3, 2, 1);
Mat dst = src1 * src2;
注意:==对于Mat对象的乘法,两个Mat只能同时是float类型或者double类型。==对于其他数据类型的矩阵做乘法会报错。
对于Mat的乘法,还可以使用OpenCV提供的gemm
函数来实现。
void cv::gemm(cv::InputArray src1, cv::InputArray src2, double alpha, cv::InputArray src3, double beta, cv::OutputArray dst, int flags = 0)
该函数通过flags控制src1,src2,src3是否转置来实现矩阵之间不同的运算。
将flags设置为不同的参数时,输出矩阵为:
dst | flags |
---|---|
d s t = a l p h a ∗ s r c 1 ∗ s r c 2 + b e t a ∗ s r c 3 dst = alpha * src1 * src2 + beta * src3 dst=alpha∗src1∗src2+beta∗src3 | flags=0 |
d s t = a l p h a ∗ s r c 1 T ∗ s r c 2 + b e t a ∗ s r c 3 dst = alpha * src1^T * src2 + beta * src3 dst=alpha∗src1T∗src2+beta∗src3 | flags=GEMM_1_T |
d s t = a l p h a ∗ s r c 1 ∗ s r c 2 T + b e t a ∗ s r c 3 dst = alpha * src1 * src2^T + beta * src3 dst=alpha∗src1∗src2T+beta∗src3 | flags=GEMM_2_T |
d s t = a l p h a ∗ s r c 1 ∗ s r c 2 + b e t a ∗ s r c 3 T dst = alpha * src1 * src2 + beta * src3^T dst=alpha∗src1∗src2+beta∗src3T | flags=GEMM_3_T |
注意:flags可以组合使用。如需要src2和src3都转置时,则令flags=GEMM_2_T+GEMM_3_T。
同样的,gemm只能接受float类型或者double类型的Mat。
1.2.6 指数运算
void cv::exp(cv::InputArray src, cv::OutputArray dst)
1.2.7 对数运算
//此处log以e为底
void cv::log(cv::InputArray src, cv::OutputArray dst)
1.2.8 幂指数
void cv::pow(cv::InputArray src, double power, cv::OutputArray dst)
注意:经过幂指数运算的dst的数据与src的数据类型相同,因此结果可能会产生截断现象。
1.3 图像数字化
1.3.1 图像的读取与显示
cv::Mat cv::imread(const cv::String &filename, int flags = 1)
flags = IMREAD_COLOR:彩色图像
flags = IMREAD_GRAYSCALE:灰度图像
flags = IMREAD_ANYCOLOR:任意图像
void cv::imshow(const cv::String &winname, cv::InputArray mat)
1.3.2 将RGB彩色图像转换为多通道Mat
对于彩色图像的每一个方格,可以理解为一个Vec3b。
注意:每一个像素的向量不是安装R、G、B分量排列的,而是按照B、G、R顺序排列的。所以通过split函数分离通道后,先后得到的是B、G、R通道。
Mat img = imread("../orange.jpg");
if(img.empty())
return -1;
imshow("BGR", img);
vector<Mat> planes;
split(img, planes); //分离通道
imshow("B",planes[0]); //显示B通道
imshow("G",planes[1]); //显示G通道
imshow("R",planes[2]); //显示R通道