OpenCV基础知识
昨天略微讲了一下怎么配置,今天重装了OpenCV4.5版本的,发现已经没有那么麻烦了,不用CMake自己编译了。
言归正传,英语好的小伙伴当然就是官网最好啦:
OpenCV官方Tutorial
我就充当一下搬运工!!
Mat
Mat很容易延伸到我们认识的一个单词即matrix,矩阵。
大家都知道,一张图片在计算机中的表示实际上就是一个矩阵。矩阵中的值对应的就是每个像素点的值。
Mat实际上是OpenCV封装好的一个类,其主要包含两部分:
头部分存储了图片的长宽,存储地址已经用何种方式进行存储。还有一个部分就是指针,指向数据域。
矩阵存储和运算对内存和CPU来说都是消耗巨大的。于是为了不让我们一次又一次地浪费大量空间去存储矩阵,OpenCV有一套自己的机制,在用拷贝构造时我们不用去保存大量的数据,只要让指针指向同一片区域即可。
下面是一个测试:
#include<opencv2/opencv.hpp>
#include<iostream>
using namespace std;
using namespace cv;
const string file_path = "D://C++//kingjames//Debug//favorite.jpg";
int main() {
Mat A, C;
A = imread(file_path, IMREAD_COLOR);
Mat B(A); // 拷贝构造
C = A;
imshow("Victoria", B); // 对B进行测试
waitKey(0);
return 0;
}
只要我们能出现一张窗口名为Victoria的照片并且和文件路径下的长一样就是成功的。
总而言之,上面的测试代码会导致A,B,C三个对象的数据指针都指向统一地址,这样一来三者无论谁对图像动了手脚都是联动的!
这时候我们就要清楚,当一个矩阵或者说一块地址共属于多个对象时最后使用该矩阵的对象会负责回收内存!
那我们自然会想到我们怎么做到不要这么一种联动呢?
比如我手头有一张Trump.jpg,第一份我想给他做猪头,第二份我想给他做成狗。那么我不能让猪影响第一张原图啊。
不要慌,我们有这样两个函数,如下:
#include<opencv2/opencv.hpp>
#include<iostream>
using namespace std;
using namespace cv;
const string file_path = "D://C++//kingjames//Debug//favorite.jpg";
int main() {
Mat A;
A = imread(file_path, IMREAD_COLOR);
Mat B = A.clone();
Mat C;
A.copyTo(C);
A = Mat::zeros(B.size(), CV_8UC3);
imshow("black", A);
waitKey(0);
imshow("Vic", B);
waitKey(0);
return 0;
}
可以看到我们对A做了一个全0处理,实际上就是对A动刀子,变成全黑了。这样以后我们再对B进行操作,只要B没变化就证明我们不是复制的指针而是数据本身!(即在内存中另外开辟空间存储数据后令新的指针指向这片内存)。
这样一来就验证完成了!
图片存储模式
我们人眼认知图片是按照RGB模式来的,但在OpenCV中我们是按BGR格式来存储的。
有接触过多媒体或者说数字图像的话应该有HSV,HLS这样可以分解为色度,饱和度,亮度的存储模式;也有YCbCr采样空间(我是真忘了)。
反正重点在于,不同的存储模式,其对应的可以域(值域空间)是不一样的。这就要求我们要正确的使用数据结构(数据形式)。我们不能说总是为了避免差错用兼容性最高的去装取一个矩阵,这可是一个大矩阵啊,会耗掉很多memory的!
Mat基础操作
我们可以精细化,或者说具体地创建一个自己想得到的矩阵:
比如今天我很不爽我觉得今天的人,事,物很250。
这个时候我们要一个一行三列的向量,此时还无需矩阵,向量就是Scalar。
如下:
#include<opencv2/opencv.hpp>
#include<opencv2/imgcodecs.hpp>
#include<iostream>
using namespace std;
using namespace cv;
int main() {
Scalar A(250, 250, 250);
cout << A << endl;
return 0;
}
**输出一下会发现最后是四个数即[250, 250, 250, 0],这是因为构造函数默认第四个为0。**这一来就更不爽了,不就想输出250嘛,这机器也好250哦居然给我蹦个0。于是就有了下面这个:
#include<opencv2/opencv.hpp>
#include<opencv2/imgcodecs.hpp>
#include<iostream>
using namespace std;
using namespace cv;
int main() {
Mat A(3, 3, CV_8UC3, Scalar(250, 250, 250));
cout << A << endl;
return 0;
}
如果没错的话,输出是这样的:
[250, 250, 250, 250, 250, 250, 250, 250, 250;
250, 250, 250, 250, 250, 250, 250, 250, 250;
250, 250, 250, 250, 250, 250, 250, 250, 250]
我们来分析一下,如果把[250,250,250]看成一个整体的话,是不是刚好就是我们指定的前两个参数,即3 * 3(三行三列)。
那么那个CV_8UC3代表是实际上就是存储模式,8bit的unsigned char类型,3就代表一个像素有三个通道。就好比RGB图像一个像素点有R, G, B三个分量一样。
create方法不能指定像我们想要的值,而只能指定形状大小和数据类型。
#include<opencv2/opencv.hpp>
#include<opencv2/imgcodecs.hpp>
#include<iostream>
using namespace std;
using namespace cv;
int main() {
Mat M;
M.create(4, 4, CV_8UC2);
cout << "M = " << endl << " " << M << endl << endl;
return 0;
}
得到的结果如下:
M =
[205, 205, 205, 205, 205, 205, 205, 205;
205, 205, 205, 205, 205, 205, 205, 205;
205, 205, 205, 205, 205, 205, 205, 205;
205, 205, 205, 205, 205, 205, 205, 205]
两个连通分量为一个整体,刚好是4行4列。
接下来是Matlab风格的初始化:
#include<opencv2/opencv.hpp>
#include<opencv2/imgcodecs.hpp>
#include<iostream>
using namespace std;
using namespace cv;
int main() {
Mat E = Mat::eye(4, 4, CV_64F); // 对角矩阵
cout << "E = " << endl << " " << E << endl << endl;
Mat O = Mat::ones(2, 2, CV_32F); // 全1矩阵
cout << "O = " << endl << " " << O << endl << endl;
Mat Z = Mat::zeros(3, 3, CV_8UC1); // 全0矩阵
cout << "Z = " << endl << " " << Z << endl << endl;
return 0;
}
这就不输出了,都比较简单。
还有一些怪异的构造方式:
#include<opencv2/opencv.hpp>
#include<opencv2/imgcodecs.hpp>
#include<iostream>
using namespace std;
using namespace cv;
int main() {
// 指定数据类型 指定大小 后面跟上数据
Mat C = (Mat_<double>(3, 3) << 0, -1, 0, -1, 5, -1, 0, -1, 0);
cout << "C = " << endl << " " << C << endl << endl;
// 指定数据类型但没有指定大小,最后用reshape变为3行
C = (Mat_<double>({ 0, -1, 0, -1, 5, -1, 0, -1, 0 })).reshape(3);
cout << "C = " << endl << " " << C << endl << endl;
// C++11支持 序号1实际上就是第二行
Mat RowClone = C.row(1).clone();
cout << "RowClone = " << endl << " " << RowClone << endl << endl;
return 0;
}
输出应该都能猜到:
C =
[0, -1, 0;
-1, 5, -1;
0, -1, 0]
C =
[0, -1, 0;
-1, 5, -1;
0, -1, 0]
RowClone =
[-1, 5, -1]
我们还可以用随机值来填充一个已存在的矩阵,调用randu函数,同时要指定随机值的上下界:
(上下界的参数类型时Array,即得用Scalar才行)
#include<opencv2/opencv.hpp>
#include<opencv2/imgcodecs.hpp>
#include<iostream>
using namespace std;
using namespace cv;
int main() {
Mat R = Mat(3, 2, CV_8UC3);
randu(R, Scalar(0, 0, 0), Scalar(255, 255, 255));
cout << "R=" << endl << R << endl;
return 0;
}
Mat输出格式
其实很多,但不准备逐一将,只说一个,就拿Python举例,最后会以List嵌套格式进行输出:
#include<opencv2/opencv.hpp>
#include<opencv2/imgcodecs.hpp>
#include<iostream>
using namespace std;
using namespace cv;
int main() {
Mat R = Mat::eye(4, 4, CV_64F);
cout << "R (python) = " << endl << format(R, Formatter::FMT_PYTHON) << endl << endl;
return 0;
}
输出长这样:
R (python) =
[[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]]
感兴趣的看这张截图:
OpenCV支持的其他数据类型
用来表示一个二维点的Point2f,用来表示一个三维点的Point3f,用一个容器vector来创建Mat。
如下:
#include<opencv2/opencv.hpp>
#include<opencv2/imgcodecs.hpp>
#include<iostream>
#include<vector>
using namespace std;
using namespace cv;
int main() {
Point2f P(5, 1);
cout << "Point (2D) = " << P << endl << endl;
Point3f P3f(2, 6, 7);
cout << "Point (3D) = " << P3f << endl << endl;
vector<float> v;
v.push_back((float)CV_PI); v.push_back(2); v.push_back(3.01f);
cout << "Vector of floats via Mat = " << Mat(v) << endl << endl;
return 0;
}
输出结果如下:
Point (2D) = [5, 1]
Point (3D) = [2, 6, 7]
Vector of floats via Mat = [3.1415927;
2;
3.01]
接下来会是如何遍历一张图像,很简单但我们总要去找最合适最省时的。
然后后续就开始图像的一些简单操作。(蒙版操作(我记得ps术语叫蒙版),改变图像的亮度等,融合两张图像······)
所以离图像的一些算法相关的知识还有一点距离,不能急,慢慢来吧。