前言:
欢迎来到本博客
本专栏主要结合OpenCV和C++来实现一些基本的图像处理算法并详细解释各参数含义,适用于平时学习、工作快速查询等,随时更新。
具体食用方式:可以点击本专栏【OpenCV快速查找(更新中)】–>搜索你要查询的算子名称或相关知识点,或者通过这篇博客通俗易懂OpenCV(C++版)详细教程——OpenCV函数快速查找(不断更新中)]查阅你想知道的知识,即可食用。
支持:如果觉得博主的文章还不错或者您用得到的话,可以悄悄关注一下博主哈,如果三连收藏支持就更好啦!这就是给予我最大的支持!
矩阵运算在学习其他课程(矩阵论、线性代数、高数)多少都有涉及。在数字图像处理中,可以理解为图像即多维矩阵,在图像处理中一般用到的关于矩阵的运算包括:加法、减法、点乘、点除、乘法等,下面我们一一介绍每个运算算子用法及注意点。
矩阵加法:两个矩阵对应位置的数值相加。例如:
第一种方式:很简单,就如同数学加法。使用OpenCV重载的“+”
运算符:
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main(int argc, char**argv) {
/*1.1 加法运算*/
Mat m1 = (Mat_<uchar>(3, 2) << 11, 12, 21, 22, 31, 32);
Mat m2 = (Mat_<uchar>(3, 2) << 12, 11, 22, 21, 32, 31);
Mat dst = m1 + m2;
cout << "加法运算结果为:" << dst << endl;
return 0;
}
我们可以看到200+150应该等于350,得到得结果却是255。原因是两个矩阵的数据类型都是uchar
,所以用“+”
运算符计算出来的和也是uchar
类型的,但是uchar
类型范围的最大值是255,所以只好将350截断为255。
但是,利用“+”
运算符计算Mat的和还有一点需要特别注意:两个Mat的数据类型必须是一样的,否则会报错,也就是用“+”求和是比较严格的。
同时,一个数值与一个Mat对象相加,也可以使用“+”
运算符,但是无论这个数值是什么数据类型,返回的Mat的数据类型都与输入的Mat相同。比如一个数值与上面的uchar类型的m2相加,代码如下:
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main(int argc, char**argv) {
/*1.1 加法运算*/
Mat m1 = (Mat_<uchar>(3, 2) << 200, 12, 21, 22, 31, 32);
Mat m2 = (Mat_<uchar>(3, 2) << 150, 11, 22, 21, 32, 31);
Mat dst = m1 + m2;
cout << "加法运算结果为:" << dst << endl;
float value = 50;
Mat dst1 = value + m2;
cout << "50+矩阵m2结果为:" << dst1 << endl;
return 0;
}
注意:如果value是float
类型的,但是因为m2是uchar
类型的,所以返回的dst1也是uchar
类型的,结果超过255都会截断为255。
第二种方式:上面介绍中可以看出,第一种方式有一定得局限性,所以我们可以利用OpenCV得函数add()
,结构如下:
void add(InputArray srcl, InputArray src2, OutputArray dst, InputArray mask=noArray(), int dtype=-1)
案例如下:
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main(int argc, char**argv) {
/* 函数add*/
Mat m1 = (Mat_<uchar>(3, 2) << 200, 12, 21, 22, 31, 32);
Mat m2 = (Mat_<float>(3, 2) << 150, 11, 22, 21, 32, 31);
Mat dst;
add(m1, m2, dst, Mat(), CV_64FC1);
cout << "加法运算add()结果为:" << dst << endl;
return 0;
}
案例中以看出,add()
函数输入矩阵的数据类型可以不同,而输出矩阵的数据类型可以根据情况自行指定。
需要特别注意的是:如果给dtype赋值为-1,则表示dst的数据类型和src1、src2是相同的,也就是只有当src1和src2的数据类型相同时,才有可能令dtype=-1
,否则会报错。
两个向量也可以做加法运算,比如:
Vec3f v1 = Vec3f(1, 2, 3);
Vec3f v2 = Vec3f(4,5,6);
Vec3f dstV = v1 + v2;
cout << dstV;
结果:
[5,7,9]
矩阵的减法与加法类似,但是也有几点注意点。
第一种方式:也很简单,就如同数学减法。使用OpenCV重载的“-”
运算符:
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main(int argc, char**argv) {
/*1.2 减法运算*/
/* 运算符 - */
Mat m1 = (Mat_<uchar>(3, 2) << 200, 12, 21, 22, 31, 32);
Mat m2 = (Mat_<uchar>(3, 2) << 150, 11, 22, 21, 32, 31);
Mat dst = m1 - m2;
cout << "减法运算结果为:" << dst << endl;
return 0;
}
同样我们可以看到12-11应该等于-1,以此类推,得到得结果却是0。原因也是两个矩阵的数据类型都是uchar
,所以用“-”
运算符计算出来的和也是uchar
类型的,但是uchar类型范围的最小值是0,所以只好将负数截断为0。
第二种方式:和加法一样,Mat对象与一个数值相减,也可以使用“-”
运算符。当然,也存在与“+”
运算符一样的不足。所以OpenCV提供了subtract()
函数,结构如下:
void subtract(InputArray srcl, InputArray src2, OutputArray dst, InputArray mask=noArray(), int dtype=-1)
可以实现不同的数据类型的Mat之间做减法运算,与add()
函数类似,这里不作详细叙述,可以利用上面代码进行修改即可。
矩阵的点乘即两个矩阵对应位置的数值相乘。
对于两个矩阵的点乘,可以利用Mat的成员函数mul()
,两个Mat对象的数据类型必须相同才可以进行点乘,返回的矩阵的数据类型不变。我们依旧以m1和m2矩阵为例:
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main(int argc, char**argv) {
/*1.3 点乘运算*/
Mat m1 = (Mat_<uchar>(3, 2) << 200, 12, 21, 22, 31, 32);
Mat m2 = (Mat_<uchar>(3, 2) << 150, 11, 22, 21, 32, 31);
Mat dst = m1.mul(m2);
cout << "点乘运算结果为:" << dst << endl;
return 0;
}
结果可以看出,点乘后对大于255的数值做了截断处理。为了不损失精度,可以将两个矩阵设置为int
、float
等数值范围更大的数据类型。
OpenCV提供了multiply()
函数:
void multiply(InputArray srcl, InputArray src2, OutputArray dst, double scale=1, int dtype=-1)
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main(int argc, char**argv) {
/*点乘运算multiply() */
Mat m1 = (Mat_<uchar>(3, 2) << 200, 12, 21, 22, 31, 32);
Mat m2 = (Mat_<float>(3, 2) << 150, 11, 22, 21, 32, 31);
Mat dst;
multiply(m1,m2,dst,1.0, CV_64FC1);
cout << "multiply()点乘运算结果为:" << dst << endl;
return 0;
}
multiply()
函数使用起来更加灵活,矩阵m1和m2的数据类型不一定需要相同,可以通过参数dtype
指定输出矩阵的数据类型。
注意:第四个参数:scale(dst=sclae*m1*m2
),即在点乘结果的基础上还可以再乘以系数scale
。
点除运算与点乘运算类似,是两个矩阵对应位置的数值相除。
第一种方式:与“+”
和“-”
类似,可以使用“/”
运算符,两个Mat的数据类型相同且返回的Mat也是该数据类型。
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main(int argc, char**argv) {
/*点除运算 / */
Mat m1 = (Mat_<uchar>(3, 2) << 200, 12, 21, 22, 31, 32);
Mat m2 = (Mat_<uchar>(3, 2) << 150, 11, 22, 21, 32, 31);
Mat dst = m1 / m2;
cout << "/点除运算结果为:" << dst << endl;
return 0;
}
需要注意:如果分母为0的除法运算时,默认得到的值为0。
同样,用一个数值与Mat对象相除也可以使用“/”
运算符,且返回的Mat的数据类型与输入的Mat的数据类型相同,与输入数值的数据类型是没有关系的。
第二种方式:使用“/”
运算符和上面一样会存在一定局限性,OpenCV提供了divide()
函数:
void divide(InputArray srcl, InputArray src2, OutputArray dst, double scale=1, int dtype=-1)
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main(int argc, char**argv) {
/*点除运算 divide() */
Mat m1 = (Mat_<uchar>(3, 2) << 200, 12, 21, 22, 31, 32);
Mat m2 = (Mat_<float>(3, 2) << 150, 11, 22, 21, 32, 31);
Mat dst;
divide(m1, m2, dst, 1.0, CV_64FC1);
cout << "divide()点除运算结果为:" << dst << endl;
return 0;
}
同样注意:第四个参数:scale(dst=sclae*m1/m2
),即在点除结果的基础上还可以再乘以系数scale
。
对于两个矩阵的乘法(矩阵乘法相关知识,线性代数等相关课程详细讲解了,有需要可以自行查阅相关资料,了解矩阵乘法运算规则),不同于上面得点乘运算。例如:
使用'*'
运算符对两个矩阵进行乘法运算:
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main(int argc, char**argv) {
/* 乘法运算* */
Mat m1 = (Mat_<float>(3, 2) << 11, 12, 21, 22, 31, 32);
Mat m2 = (Mat_<float>(2, 3) << 11, 12, 13, 21, 22, 23);
Mat dst = m1 * m2;
cout << "*乘法运算结果为:" << dst << endl;
return 0;
}
对于Mat对象的乘法,需要注意两个Mat只能同时是float或者double类型,对于其他数据类型的矩阵做乘法会报错。
上面介绍了两个float或者double类型的单通道矩阵的乘法。例如:
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main(int argc, char**argv) {
/* 双通道乘法运算*/
Mat s1 = (Mat_<Vec2f>(2, 1) << Vec2f(1, 2), Vec2f(3, 4));
Mat s2 = (Mat_<Vec2f>(1, 2) << Vec2f(5,6), Vec2f(7, 8));
Mat dst1 = s1 * s2;
cout << "双通道*乘法运算结果为:" << endl;
for (int i = 0; i < dst1.rows; i++)
{
for (int j = 0; j < dst1.cols; j++)
{
cout << dst1.at<Vec2f>(i, j) << " ";
}
cout << endl;
}
//cout << "双通道*乘法运算结果为:" << dst1 << endl;
return 0;
}
奇怪的是:怎么会有负数,两个双通道矩阵也可相乘吗?
实际上,其实这里是将s1和s2这两个双通道Mat对象当作了两个复数矩阵。第一通道存放的是所有值的实部,第二通道存放的是对应的每一个虚部。例如,Vec2f(1,2)
可以看作1+2i
。
结果可以很明显地看出,将Vec2f看作了一个复数,例如输出的[-7,16]
分别代表复数-7+16i
的实部和虚部。
关于矩阵乘法运算,OpenCV也提供了gemm()
函数来实现。
void gemm(InputArray src1, InputArray src2, double alpha, InputArray src3, double beta, OutputArray dst, int flags=0 )
src1:输入类型是CV_ 32F或者CV_64F的单或双通道矩阵;
src2:输入矩阵,类型与src1一致;
alpha:src1与src2相乘后得系数;
src3:输入矩阵,类型与src1一致;
beta:src3的系数;
dst:输出矩阵;
flags:标识符; 0:没有转置;GEMM_1_T:表示src1转置;GEMM_2_T:表示src2转置;GEMM_3_T:表示src3转置;即:
flags可以组合使用,比如需要src2和src3都进行转置,则令flags=GEMM_2_T+GEMM_3_T
。
当输入矩阵src1、src2、src3是双通道矩阵时,代表是复数矩阵,其中第一通道存储实部,第二通道存储虚部。
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main(int argc, char**argv) {
/* 乘法运算gemm() */
Mat m1 = (Mat_<float>(3, 2) << 11, 12, 21, 22, 31, 32);
Mat m2 = (Mat_<float>(2, 3) << 11, 12, 13, 21, 22, 23);
Mat dst ;
gemm(m1, m2, 1.0, NULL, 0, dst, 0);
cout << "gemm乘法运算结果为:" << dst << endl;
return 0;
}
结果与'*'
是等价的。需要注意的是:gemm()
也只能接受CV_32FC1、CV_64FC1、CV_32FC2、CV_64FC2
数据类型的Mat,这一点与使用'*'
也是一样的。
我们讨论的对数和指数运算是对矩阵中的每一个数值进行相应的运算。除此之外,我们也可以使用for循环对矩阵中的每一个数值进行相应的运算。
针对指数和对数运算,OpenCV分别提供了exp()
和log()
函数(这里log是以e为底的)。
需要注意的是:这两个函数的输入矩阵的数据类型只能是CV_32F
或者CV_64F
,否则会报错。
(1) 指数运算
指数运算函数exp()
表示: e x e^x ex
exp(InputArray src1,OutputArray dst)
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main(int argc, char**argv) {
/* 指数运算exp() */
Mat m1 = (Mat_<float>(3, 2) << 11, 12, 21, 22, 31, 32);
Mat dst;
exp(m1, dst);
cout << "指数运算exp()结果为:" << dst << endl;
return 0;
}
(2) 对数运算
对数运算函数log()
表示: log e x \log_ex logex= ln x \ln x lnx
log(InputArray src1,OutputArray dst)
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main(int argc, char**argv) {
/* 对数运算log() */
Mat m1 = (Mat_<float>(3, 2) << 11, 12, 21, 22, 31, 32);
Mat dst;
log(m1, dst);
cout << "对数运算log()结果为:" << dst << endl;
return 0;
}
注意:除了有log()
运算(以e为底),也有以10、2为底等等对数函数。
同样,我们讨论的幂指数和开平方运算是对矩阵中的每一个数值进行相应的运算。也是可以使用for循环对矩阵中的每一个数值进行相应的运算,
针对幂指数和开平方运算,OpenCV分别提供了pow()
和sqrt()
函数 。
同样,需要注意的是:sqrt()
的输入矩阵的数据类型只能是CV_32F
或者CV_64F
,而pow()
的输入矩阵的数据类型不受限制,且输出矩阵与输入矩阵的数据类型相同。
(1) 幂指数运算
对数运算函数pow()
表示: x n x^n xn,以2次方为例:
pow(InputArray src1,scale,OutputArray dst)
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main(int argc, char**argv) {
/* 幂指数运算pow()*/
Mat m1 = (Mat_<float>(3, 2) << 11, 12, 21, 22, 31, 32);
Mat dst;
pow(m1,2,dst);
cout << "幂指数运算pow()结果为:" << dst << endl;
return 0;
}
如果数据类型为Mat m1 = (Mat_
,经过幂指数运算的dst
的数据类型仍然是uchar
,大于255的数值也会截断为255。
(2) 开平方运算
开平方运算函数sqrt()
表示: x \sqrt x x
sqrt(InputArray src1,OutputArray dst)
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main(int argc, char**argv) {
/* 开平方运算sqrt()*/
Mat m1 = (Mat_<float>(3, 2) << 11, 12, 21, 22, 31, 32);
Mat dst;
sqrt(m1, dst);
cout << "开平方运算sqrt()结果为:" << dst << endl;
return 0;
}
关于矩阵的运算先了解这些最基本的函数即可,OpenCV还提供了很多矩阵运算函数,如取绝对值、求逆、取最大值函数,后面博客中会用到这些函数,到时我们再详细讨论,这边给出其他矩阵运算函数表(供参考,部分函数详细内容可查阅相关资料,或关注本专栏后面博客):
函数名 | 注释 | 备注 |
---|---|---|
add() | 矩阵加法 | 详见本章内容 |
scaleAdd() | 矩阵加法,带有缩放因子 | dst(I) = scale * src1(I) + src2(I) |
subtract() | 矩阵减法 | 详见本章内容 |
multiply() | 矩阵点乘 | 矩阵逐元素乘法,同mul()函数,与A*B区别 |
gemm() | 广义的矩阵乘法操作 | 详见本章内容 |
divide() | 矩阵逐元素除法,与A/B区别 | 详见本章内容 |
abs() | 对每个元素求绝对值 | abs(InputArray src) |
absdiff() | 两个矩阵的差的绝对值 | absdiff(InputArray src1,InputArray src2,OutputArray dst) |
exp() | 指数运算 | 详见本章内容 |
pow() | 幂运算 | 详见本章内容 |
log() | 对数运算 | 详见本章内容 |
sqrt() | 开平方运算 | 详见本章内容 |
min, max() | 每个元素的最小值或最大值返回这个矩阵 dst(I) = min(src1(I), src2(I)), max同 | min(InputArray src1,InputArray src2) |
minMaxLoc() | 定位矩阵中最小值、最大值的位置 | minMaxLoc( const Mat& src, double* minVal, double* maxVal=0, Point* minLoc=0, Point* maxLoc=0, const Mat& mask=Mat() ); |
compare() | 返回逐个元素比较结果的矩阵 | —— |
bitwise_and, bitwise_not, bitwise_or, bitwise_xor | 每个元素进行位运算,分别是和、非、或、异或, max同 | —— |
randu | 以Uniform分布产生随机数填充矩阵 | 同 RNG::fill(mat, RNG::UNIFORM)) |
randn | 以Normal分布产生随机数填充矩阵 | 同 RNG::fill(mat, RNG::NORMAL)) |
theRNG() | 随机打乱一个一维向量的元素顺序返回一个默认构造的RNG类的对象theRNG()::fill(…) | —— |
reduce() | 矩阵缩成向量 | —— |
repeat() | 矩阵拷贝的时候指定按x/y方向重复 | —— |
split() | 多通道矩阵分解成多个单通道矩阵 | 见上一章 |
merge() | 多个单通道矩阵合成一个多通道矩阵 | 见上一章 |
mixChannels() | 矩阵间通道拷贝 | 如Rgba[]到Rgb[]和Alpha[] |
sort, sortIdx() | 为矩阵的每行或每列元素排序 | —— |
setIdentity() | 设置单元矩阵 | —— |
completeSymm() | 矩阵上下三角拷贝 | —— |
inRange() | 检查元素的取值范围是否在另两个矩阵的元素取值之间,返回验证矩阵 | —— |
sum() | 求矩阵的元素和 | —— |
mean() | 求均值 | —— |
meanStdDev() | 均值和标准差 | —— |
countNonZero() | 统计非零值个数 | —— |
cartToPolar, polarToCart() | 笛卡尔坐标与极坐标之间的转换 | —— |
flip() | 矩阵翻转 | —— |
transpose() | 矩阵转置 | —— |
trace() | 矩阵的迹 | —— |
determinant() | 行列式 | det(A) |
eigen() | 矩阵的特征值和特征向量 | —— |
invert() | 矩阵的逆或者伪逆 | 比较 Mat::inv() |
magnitude() | 向量长度计算 | dst(I) = sqrt(x(I)2 + y(I)2) |
Mahalanobis() | Mahalanobis距离计算 | —— |
phase() | 相位计算,即两个向量之间的夹角 | —— |
norm() | 求范数,1-范数、2-范数、无穷范数 | —— |
normalize() | 标准化 | —— |
mulTransposed() | 矩阵和它自己的转置相乘 AT * A | dst = scale(src - delta)T(src - delta) |
convertScaleAbs() | 先缩放元素再取绝对值,最后转换格式为8bit型 | —— |
calcCovarMatrix() | 计算协方差阵 | —— |
solve() | 求解1个或多个线性系统或者求解最小平方问题(least-squares problem) | —— |
solveCubic() | 求解三次方程的根 | —— |
solvePoly() | 求解多项式的实根和重根 | —— |
dct, idct() | 正、逆离散余弦变换 | idct同dct(src, dst, flags |
dft, idft() | 正、逆离散傅立叶变换 | idft同dft(src, dst, flags |
LUT() | 查表变换 | —— |
getOptimalDFTSize() | 返回一个优化过的DFT大小 | —— |
mulSpecturms() | 两个傅立叶频谱间逐元素的乘法 | —— |
最后,长话短说,大家看完就好好动手实践一下,切记不能三分钟热度、三天打鱼,两天晒网。OpenCV是学习图像处理理论知识比较好的一个途径,大家也可以自己尝试写写博客,来记录大家平时学习的进度,可以和网上众多学者一起交流、探讨,有什么问题希望大家可以积极评论交流,我也会及时更新,来督促自己学习进度。希望大家觉得不错的可以点赞、关注、收藏。
今天的文章就到这里啦~
喜欢的话,点赞、收藏⭐️、关注哦 ~