1. OpenCV 数据结构与基本绘图
1.1 基础图像容器 Mat
1.1.1 Mat 的拷贝
在 OpenCV 中,Mat 由两个数据部分组成:矩阵头(包含矩阵尺寸、存储方法、存储地址等信息)和一个指向存储所有像素值的矩阵(根据所选存储方法的不同,矩阵可以是不同的维数)的指针。
OpenCV 是一个图像处理库,囊括了大量的图像处理函数,为了解决问题通常要使用库中的多个函数,因此在函数中传递图像是常有的事。同时不要忘了我们正在讨论的是计算量很大的图像处理算法,因此,除非万不得已,不应该进行大图像的复制,因为这会降低程序的运行速度。为了解决此问题,OpenCV 使用了引用计数机制。其思路是让每个 Mat 对象有自己的信息头,但共享同一个矩阵。这通过让矩阵指针指向同一地址而实现。而拷贝构造函数则只复制信息头和矩阵指针,而不复制矩阵。以下是测试代码:
#include <opencv2\opencv.hpp>
using namespace cv;
void ModifyImage(Mat E)
{
for (int i = 0; i < E.rows; ++i)
{
for (int j = 0; j < E.cols / 2; ++j)
{
E.at<Vec3b>(i, j) = Vec3b(0, 0, 0);
}
}
return;
}
int main()
{
Mat A, B;
A = imread("C:/Users/13071/Desktop/pkq.jpg");
// 直接将 A 等号赋值给 B,操作 B 影响 A
// B = A;
// for (int i = 0; i < B.rows; ++i)
// {
// for (int j = 0; j < B.cols / 2; ++j)
// {
// B.at<Vec3b>(i, j) = Vec3b(0, 0, 0);
// }
// }
// imwrite("C:/Users/13071/Desktop/pkq_1.jpg", A);
// 使用拷贝构造函数, 操作 C 影响 A
// Mat C(A);
// for (int i = 0; i < C.rows; ++i)
// {
// for (int j = 0; j < C.cols / 2; ++j)
// {
// C.at<Vec3b>(i, j) = Vec3b(0, 0, 0);
// }
// }
// imwrite("C:/Users/13071/Desktop/pkq_2.jpg", A);
// 获取部分图像区域的引用 操作部分区域同样影响 A
// Mat D(A, Rect(10, 10, 100, 100));
// for (int i = 0; i < D.rows; ++i)
// {
// for (int j = 0; j < D.cols / 2; ++j)
// {
// D.at<Vec3b>(i, j) = Vec3b(0, 0, 0);
// }
// }
// imwrite("C:/Users/13071/Desktop/pkq_3.jpg", A);
// 函数传参
ModifyImage(A);
imwrite("C:/Users/13071/Desktop/pkq_4.jpg", A);
return 0;
}
结论:对于 Mat 类对象,无论是直接赋值、使用拷贝构造函数还是用于函数传参(不传引用)均复制的是 Mat 类对象的矩阵头和矩阵指针,不会重新分配新的地址空间。多个引用指向同一块内存空间。
Mat 对象的矩阵指针可以通过 mat.data 获得
使用 copyTo 函数和 clone 函数,均会重新开辟新的内存空间,不同引用之间操作不会互相影响。
#include <opencv2\opencv.hpp>
using namespace cv;
int main()
{
Mat A, B;
A = imread("C:/Users/13071/Desktop/pkq.jpg");
// A.copyTo(B);
B = A.clone();
for (int i = 0; i < B.rows; ++i)
{
for (int j = 0; j < B.cols / 2; ++j)
{
B.at<Vec3b>(i, j) = Vec3b(0, 0, 0);
}
}
imwrite("C:/Users/13071/Desktop/pkq_1.jpg", A);
return 0;
}
1.1.2 Mat 对象的创建
Mat 对象不仅是一个图像容器,还可以用来操作多维矩阵。
可以使用 Mat 的运算符 << 输出 Mat 的内容(只对二维图像有效)
#include <opencv2\opencv.hpp>
#include <iostream>
using namespace std;
using namespace cv;
int main()
{
// 直接构造函数创建
// 图像类型 CV_[位数][是否带符号][类型前缀]C[通道数]
Mat A(2, 2, CV_8UC3, Scalar(0, 0, 255));
// cout << A << endl;
// 使用构造函数创建高维 Mat 对象
int sz[3] = {10, 10, 10};
Mat B(3, sz, CV_8UC3, Scalar(0, 255, 0));
// 使用 Create 函数创建
Mat C;
C.create(4, 4, CV_8UC3);
// 使用 Matlab 的方式创建,使用 Mat 对象的成员函数
Mat D, E, F;
D = Mat::eye(4, 4, CV_8UC3);
cout << "D:" << endl << D << endl;
E = Mat::zeros(4, 4, CV_8UC3);
cout << "E:" << endl << E << endl;
F = Mat::ones(4, 4, CV_8UC3);
cout << "F:" << endl << F << endl;
/* ------------------------------------------------------------------
D:
[ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0;
0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0;
0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0;
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0]
E:
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0;
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0;
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0;
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
F:
[ 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0;
1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0;
1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0;
1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0]
------------------------------------------------------------------ */
return 0;
}
1.2 常用数据结构和函数
1.2.1 Point 类
表示了二维坐标系下的点,其中Point_<int>、Point2i、Point 相互等价,Point_<float>、point2f 相互等价。可以直接使用 cout << 对 Point 类进行输出。
类似的三维坐标空间对应存在 Point3i、Point3d 等
1.2.2 Scalar 类
常被用于表示像素值
Scalar(a, b, c);
abc 依次表示 蓝色分量、绿色分量和红色分量
1.2.3 Size 类
Size(width, height);
用于定义宽高尺寸
1.2.4 Rect 类
#include <opencv2\opencv.hpp>
#include <iostream>
using namespace std;
using namespace cv;
int main()
{
// 定义一个矩形,分别表示左上角坐标,宽和高
Rect rect1(1, 1, 10, 10);
Rect rect2(5, 5, 10, 10);
// 区域求交
Rect rect3 = rect1 & rect2;
// 区域求并
Rect rect4 = rect1 | rect2;
cout << "rect3" << endl << rect3 << endl;
cout << "rect4" << endl << rect4 << endl;
// 平移
Point point(1, 1);
Rect rect5 = rect1 + point;
cout << "rect5" << endl << rect5 << endl;
// 放大【在原先长宽的基础上加上新增的长宽】
Size size(20, 20);
Rect rect6 = rect1 + size;
cout << "rect6" << endl << rect6 << endl;
return 0;
}
1.2.5 cvtColor() 函数
#include <opencv2\opencv.hpp>
#include <iostream>
using namespace std;
using namespace cv;
int main()
{
Mat srcImage, dstImage;
srcImage = imread("C:/Users/13071/Desktop/pkq.jpg");
cvtColor(srcImage, dstImage, COLOR_BGR2GRAY);
imwrite("C:/Users/13071/Desktop/pkq_1.jpg", dstImage);
return 0;
}
2. core 组件进阶
2.1 访问图像中的像素
2.1.1 图像在内存中的存储方式
2.1.2 颜色空间缩减
将现有颜色空间(颜色数)映射到一个更小的颜色空间。
2.1.3 LUT 函数的使用
用于批量进行图像元素查找、扫描与操作图像。
2.1.4 计时函数
#include <opencv2\opencv.hpp>
#include <iostream>
using namespace std;
using namespace cv;
int main()
{
double time0 = static_cast<double>(getTickCount());
for (int i = 0; i < 1e8; ++i) {}
double time1 = static_cast<double>(getTickCount());
cout << (time1 - time0) / getTickFrequency() << "s" << endl;
return 0;
}
以秒为单位进行计时
2.1.5 访问图像中像素的三种方式
#include <opencv2\opencv.hpp>
#include <iostream>
using namespace std;
using namespace cv;
// 使用 c 语言操作符 [] 进行访问元素
// void ColorReduce(const Mat& inputImage, Mat& outputImage, int div)
// {
// outputImage = inputImage.clone();
// int rows = outputImage.rows;
// int cols = outputImage.cols * outputImage.channels();
// for (int i = 0; i < rows; ++i)
// {
// // ptr 用于获取指定行的首地址
// uchar* p = outputImage.ptr<uchar>(0);
// for (int j = 0; j < cols; ++j)
// {
// p[j] = p[j] / div * div + div / 2;
// }
// }
// return;
// }
// 使用迭代器访问元素
// void ColorReduce(const Mat& inputImage, Mat& outputImage, int div)
// {
// outputImage = inputImage.clone();
// Mat_<Vec3b>::iterator it = outputImage.begin<Vec3b>();
// Mat_<Vec3b>::iterator itend = outputImage.end<Vec3b>();
//
// while (it != itend)
// {
// (*it)[0] = (*it)[0] / div * div + div / 2;
// (*it)[1] = (*it)[1] / div * div + div / 2;
// (*it)[2] = (*it)[2] / div * div + div / 2;
// ++it;
// }
// return;
// }
// 动态计算地址
void ColorReduce(const Mat& inputImage, Mat& outputImage, int div)
{
outputImage = inputImage.clone();
int rows = outputImage.rows;
int cols = outputImage.cols;
for (int i = 0; i < rows; ++i)
{
for (int j = 0; j < cols; ++j)
{
// at 方法获取图像中 (i, j) 位置的元素
outputImage.at<Vec3b>(i, j)[0] = outputImage.at<Vec3b>(i, j)[0] / div * div + div / 2;
outputImage.at<Vec3b>(i, j)[1] = outputImage.at<Vec3b>(i, j)[1] / div * div + div / 2;
outputImage.at<Vec3b>(i, j)[2] = outputImage.at<Vec3b>(i, j)[2] / div * div + div / 2;
}
}
return;
}
int main()
{
Mat image = imread("C:/Users/13071/Desktop/pkq.jpg");
Mat dstImage;
double time0 = static_cast<double>(getTickCount());
ColorReduce(image, dstImage, 32);
double time1 = static_cast<double>(getTickCount());
cout << (time1 - time0) / getTickFrequency() << "s" << endl;
imwrite("C:/Users/13071/Desktop/dst.jpg", dstImage);
return 0;
}
2.2 ROI区域图像叠加和图像混合
2.2.1 感兴趣区域ROI
选取原图像中的一块感兴趣区域,然后将子图像拷贝过去。
#include <opencv2\opencv.hpp>
#include <iostream>
using namespace std;
using namespace cv;
int main()
{
Mat srcImage = imread("C:/Users/13071/Desktop/shida.jpg");
Mat logoImage = imread("C:/Users/13071/Desktop/pkq.jpg");
Mat imageRoi = srcImage(Rect(1000, 600, logoImage.cols, logoImage.rows));
Mat mask = imread("C:/Users/13071/Desktop/pkq.jpg", 0);
logoImage.copyTo(imageRoi, mask);
imwrite("C:/Users/13071/Desktop/shida_1.jpg", srcImage);
return 0;
}
2.2.2 计算数组加权和:addWeighted() 函数使用
设置两个图像在新图像中所占的比重,然后两个图像按比重混合
用于混合的两个图像需要尺寸和通道数相等
2.3 分离颜色通道、多通道图像混合
2.3.1 通道分离:split() 函数
#include <opencv2\opencv.hpp>
#include <iostream>
using namespace std;
using namespace cv;
int main()
{
Mat srcImage = imread("C:/Users/13071/Desktop/shida.jpg");
vector<Mat> channels;
split(srcImage, channels);
imwrite("C:/Users/13071/Desktop/shida_1.jpg", channels[0]);
imwrite("C:/Users/13071/Desktop/shida_2.jpg", channels[1]);
imwrite("C:/Users/13071/Desktop/shida_3.jpg", channels[2]);
return 0;
}
2.3.2 通道合并:merge() 函数
2.4 图像对比度,亮度值调整
2.4.1 理论依据
调整图像对比度和亮度通常可以使用以下公式,其中 f(i, j) 为原图像中的像素,g(i, j) 为输出图像的像素,参数 a 用于调节对比度,参数 b 用于调节亮度。
g
(
i
,
j
)
=
a
∗
f
(
i
,
j
)
+
b
g(i, j)=a^{*} f(i, j)+b
g(i,j)=a∗f(i,j)+b
2.5 离散傅里叶变换
注:本文主要是自己看书时的一些记录。by — 《OpenCV3编程入门》