c++版opencv长文指南
- 1、配置opencv库
- 2、学习路线
- 3、入门知识
- 3.1 图像读取与显示
- 3.2 图像色彩空间转换
- 3.3 图像对象的创建与赋值
- 3.4 图像像素的读写操作
- 3.5 图像像素的算术操作
- 3.6 滚动条操作
- 3.7 键盘响应
- 3.8 opencv自带颜色表操作
- 3.9 图像像素的逻辑操作
- 3.10 通道分离与合并
- 3.11 图像色彩空间转换
- 3.12 像素值统计
- 3.13 图像几何形状绘制
- 3.14 随机数与随机颜色
- 3.15 多边形填充与绘制
- 3.16 鼠标操作与响应
- 3.17 图像像素类型转换和归一化
- 3.18 图像放缩与插值
- 3.19 图像反转
- 3.20 图像旋转
- 3.21 读取视频流
- 3.22 视频处理与保存
- 3.23 一维图像直方图
- 3.24 二维图像直方图
- 3.25 直方图均衡化
- 3.26 图像卷积操作
- 3.27 高斯模糊
- 3.28 高斯双边模糊
- 3.29 应用案例:人脸检测
1、配置opencv库
1.1 下载
OpenCV的安装实际上是将OpenCV的库路径添加到我们现有的项目路径中。通常有两种方法可以完成这一操作:
1、一种是自己下载OpenCV的源代码,并在此基础上编译生成库文件(如lib或dll);
2、另一种则是直接下载已经编译好的库文件。
我们可以选择第二种方式,直接使用预编译的库文件。最新版OpenCV Lib 下载链接为:
https://sourceforge.net/projects/opencvlibrary/files/latest/download
1.2 配置
下载并解压OpenCV后,将其解压到一个固定目录,比如:“D:\program\opencv”。稍后,我们需要将这个路径作为库
和头文件
的路径,加入到C++程序的项目中。
在Visual Studio (VS)中,由于每个项目都是独立编译
的,所以每个项目都有自己的“属性设置”
。换句话说,你可以通过右键点击项目名称并选择“属性”来配置该项目的编译规则。
接下来,我们将在属性窗口中配置OpenCV路径,具体步骤如下:
1.打开Visual Studio并加载你的项目。
2.通过右键点击项目名称
并选择“属性”
来配置该项目的编译规则。
1.2.1 配置包含目录
找到“VC++目录”。其中,紫色区域表示是要在什么环境下运行就在什么环境下配置。比如我要在Debug版下配置x64的。这里我选择为所有配置(既支持Release,同时支持Debug版)
在“包含目录”
中,添加OpenCV的include和opencv2文件夹路径。
1.2.2 配置库含目录
在“库目录”
中,添加OpenCV的lib文件夹路径。
1.2.3 配置链接器
切换到“链接器”选项卡,点击“输入”项,在“附加依赖项”中,添加相应的OpenCV库文件名称。
这个其实就在1.2.2节中库文件目录下,后缀d代表是debug版本。如果是不带后缀的,在vs中使用debug的版本就会报错。
1.2.4 配置系统环境变量
将:
D:\program\opencv\build\x64\vc15\bin
添加到电脑系统的环境变量中。
通过以上步骤,你就完成了OpenCV在VS项目中的路径配置。
2、学习路线
3、入门知识
3.1 图像读取与显示
语法:
cv::imread(); // 读取
cv::imshow(); // 显示
cv::namedWindow(); // 创建窗口
cv::waitKey(0); // 等待用户键盘输入
cv::destroyAllWindows(); // 销毁窗口
#include<iostream>
#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main()
{
cv::Mat src = cv::imread("C:/Users/Desktop/02.jpg");
// 以灰度显示
//cv::Mat src = cv::imread("C:/Users/Desktop/02.jpg", cv::IMREAD_GRAYSCALE);
if (src.empty())
{
cout << "不能够正确加载图片" << endl;
return -1;
}
cv::namedWindow("my_window", cv::WINDOW_FREERATIO);
cv::imshow("my_window", src); // 默认是WINDOW_AUTOSIZE
cv::waitKey(0);
cv::destroyAllWindows();
system("pause");
return 0;
}
3.2 图像色彩空间转换
语法:
cv::cvtColor(); // 改变颜色
#include<iostream>
#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main()
{
cv::Mat src = cv::imread("C:/Users/Desktop/02.jpg");
if (src.empty())
{
cout << "不能够正确加载图片" << endl;
return -1;
}
cv::Mat hsv, gray;
cv::cvtColor(src, hsv, cv::COLOR_BGR2HSV);
cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY);
cv::imshow("RGB", src);
cv::imshow("HSV", hsv);
cv::imshow("GRAY", gray);
// 保存
cv::imwrite("C:/Users/Desktop/hsv.jpg", hsv);
cv::imwrite("C:/Users/Desktop/gray.jpg", gray);
cv::waitKey(0);
cv::destroyAllWindows();
system("pause");
return 0;
}
3.3 图像对象的创建与赋值
c++中的Mat创建,其实和Python中的Numpy很像,都是表示矩阵。
Mat的基本结构如下:
头部存放了一些属性:图像的宽高、dtype类型、通道数等;
直接赋值创建1个新Mat对象,指针还是指向同一个Data Block;但克隆 / 拷贝则会创建1个新的内存空间。
使用c++版的opencv时,只要记住:
1、c++中的cv基本上和opencv-python中的语法类似,比如:
cv2.imread()
cv::imread();
2、c++中的cv中的Mat和python中的Numpy语法类似,比如:
np.zeros()
Mat::zeros();
Mat和cv两者不要打架,参考着python中的来记。
3.3.1 图像对象的创建
语法:
cv::Mat::ones(); // 创建像素值全1的二维矩阵
cv::Mat::zeros(); // 创建像素值全0的二维矩阵
cv::Scalar(127, 127, 127); // 创建标量
Mat类可以存储的数据类型包含double、float、uchar(unsigned char)以及自定义的模板等。
一切图像皆Mat
,c++版opencv中但凡涉及到矩阵
处理的都是Mat,其余很多都是通过cv::进行调用。
// 创建空白图像
// c++中Mat类似python中的Numpy
// CV_8UC1 8位无符号字符(unsigned char) 1:单通道, 3:3通道
//Mat m3 = Mat::zeros(cv::Size(8, 8), CV_8UC1);
//Mat m3 = Mat::zeros(src.size(), src.type();
Mat m3 = Mat::zeros(8, 8, CV_8UC3);
// 宽度为:8 高度为:8 通道数:3
cout << "宽度为:" << m3.cols << " 高度为:" << m3.rows << " 通道数:" << m3.channels() << endl;
cout << m3 << endl;
// 单通道没啥区别,3通道时,只在每个像素的第1个通道为1,其余两个通道像素值为0
m3 = Mat::ones(cv::Size(8, 8), CV_8UC3);
// 即使直接赋值,仍然会出现这样的现象
//m3 = 127;
// Scalar可以解决 c++中的操作符重载全部可以运用在Mat中
m3 = cv::Scalar(127, 127, 127);
cout << m3 << endl;
3.3.2 图像对象的赋值
语法:
src.clone(); // 图像拷贝克隆
src.copyTo(m2); // 图像拷贝克隆
m1 = m2; // 图像拷贝克隆
cv::Mat src = cv::imread("C:/Users/Desktop/02.jpg");
cv::Mat m1, m2;
// 克隆 、 拷贝则会创建1个新的内存空间。
m1 = src.clone();
src.copyTo(m2);
// 直接赋值创建1个新Mat对象,指针还是指向同一个Data Block;
// 所以这里的m3的像素值改变,而src也会随着一起改变。
cv::Mat m3 = src;
m3 = cv::Scalar(0, 0, 255);
cv::imshow("res", src)
3.4 图像像素的读写操作
语法:
src.at<uchar>(y, x);; // 单通道图像获取某个像素值
src.at<cv::Vec3b>(y, x); // RGB三通道图像获取某个像素值
src.ptr<uchar>(y); // 获取单通道图像某一行的指针
c++中的像素遍历和访问方式:
1、数组遍历
#include<iostream>
#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main()
{
cv::Mat src = cv::imread("C:/Users/Desktop/02.jpg");
int w = src.cols;
int h = src.rows;
int dims = src.channels();
for (int y = 0; y < h; y++)
{
for (int x = 0; x < w; x++)
{
if (dims == 1)
{
// 灰度图像
// at方法是Mat里面的1个函数模板,所以要指定模板的数据类型
int pv = src.at<uchar>(y, x);
src.at<uchar>(y, x) = 255 - pv;
}
else if (dims == 3)
{
// RGB图像
// 是一个模板类 cv::Vec 的特化版本,专门用于存储 3 个 uchar(无符号字符)值,通常表示 BGR 通道。
// 是一个用于表示固定大小的三维向量的类型,专门用于图像处理中的颜色数据
cv::Vec3b bgr = src.at<cv::Vec3b>(y, x);
src.at<cv::Vec3b>(y, x)[0] = 255 - bgr[0];
src.at<cv::Vec3b>(y, x)[1] = 255 - bgr[1];
src.at<cv::Vec3b>(y, x)[2] = 255 - bgr[2];
}
}
}
cv::imshow("img", src);
cv::waitKey(0);
cv::destroyAllWindows();
system("pause");
return 0;
}
2、指针方式遍历
#include<iostream>
#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main()
{
cv::Mat src = cv::imread("C:/Users/Desktop/02.jpg");
int w = src.cols;
int h = src.rows;
int dims = src.channels();
for (int y = 0; y < h; y++)
{
// 使用 src.ptr<uchar>(y) 获取当前行的指针;
// current_y 是指向图像第 y 行第一个像素的指针。
uchar* current_y = src.ptr<uchar>(y);
for (int x = 0; x < w; x++)
{
if (dims == 1)
{
// 灰度图像
int pv = *current_y;
*current_y++ = 255 - pv;
}
else if (dims == 3)
{
// RGB图像
*current_y++ = 255 - *current_y;
*current_y++ = 255 - *current_y;
*current_y++ = 255 - *current_y;
}
}
}
cv::imshow("img", src);
cv::waitKey(0);
cv::destroyAllWindows();
system("pause");
return 0;
}
3.5 图像像素的算术操作
1、可以和标量Scalar进行加减乘除;
2、可以和创建的图像本身(全0、全1后进行赋初始值)的算术操作
3.5.1 通过运算符重载来实现
int main()
{
cv::Mat src = cv::imread("C:/Users/Desktop/02.jpg");
cv::Mat dst;
dst = src + cv::Scalar(50, 50, 50); // 加法
//dst = src - cv::Scalar(50, 50, 50); // 减法
//dst = src / cv::Scalar(2, 2, 2); // 除法
// 两个图形相乘,是不可以用标量进行操作的
// penCV 并不支持直接用 * 操作符对 cv::Mat 和 cv::Scalar 进行运算
//dst = src * cv::Scalar(2, 2, 2);
// 使用 cv::multiply 函数
cv::Mat m = cv::Mat::zeros(src.size(), src.type());
m = cv::Scalar(2, 2, 2);
cv::multiply(src, m, dst);
cv::imshow("加法操作", dst);
cv::waitKey(0);
cv::destroyAllWindows();
system("pause");
return 0;
}
3.5.2 通过数组遍历来实现
int main()
{
cv::Mat src = cv::imread("C:/Users/Desktop/02.jpg");
cv::Mat m = cv::Mat::zeros(src.size(), src.type());
m = cv::Scalar(50, 50, 50);
cv::Mat dst = cv::Mat::zeros(src.size(), src.type());
// 加法
// 从上到下,从左到右,先遍历行(图像的高)
for (int y = 0; y < src.rows; y++)
{
for (int x = 0; x < src.cols; x++)
{
// 获取原始图像的每个像素值
cv::Vec3b p1 = src.at<cv::Vec3b>(y, x);
// 获取m图像的每个像素值
cv::Vec3b p2 = m.at<cv::Vec3b>(y, x);
// saturate_cast方法在cv中常用, 将像素值约束在0-255之间
dst.at<cv::Vec3b>(y, x)[0] = cv::saturate_cast<uchar>(p1[0] + p2[0]);
dst.at<cv::Vec3b>(y, x)[1] = cv::saturate_cast<uchar>(p1[1] + p2[1]);
dst.at<cv::Vec3b>(y, x)[2] = cv::saturate_cast<uchar>(p1[2] + p2[2]);
}
}
cv::imshow("加法操作", dst);
cv::waitKey(0);
cv::destroyAllWindows();
system("pause");
return 0;
}
3.5.3 通过api来实现
实际上,opencv底层实现了优化,优先调用api
。
语法:
cv::add(src, m, dst); // 加法
cv::subtract(src, m, dst); // 减法
cv::multiply(src, m, dst); // 乘法
cv::divide(src, m, dst); // 除法
cv::saturate_cast<uchar>(p); // 将p的像素值约束在0-255之间
int main()
{
cv::Mat src = cv::imread("C:/Users/Desktop/02.jpg");
cv::Mat m = cv::Mat::zeros(src.size(), src.type());
m = cv::Scalar(50, 50, 50);
cv::Mat dst = cv::Mat::zeros(src.size(), src.type());
// 加法
cv::add(src, m, dst);
// 减法
cv::subtract(src, m, dst);
// 除法
cv::divide(src, m, dst);
cv::imshow("加法操作", dst);
cv::waitKey(0);
cv::destroyAllWindows();
system("pause");
return 0;
}
3.6 滚动条操作
语法:
cv::createTrackbar(); // 创建滚动条
cv::addWeighted(src, 1.0, m, 0, lightness, dst); // 图像加权
3.6.1 调整图像亮度
#include<iostream>
#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;
// 全局变量
cv::Mat src, dst, m;
int max_value = 100; // 滑块的最大值
int lightness = 50; // 初始亮度值
void on_track(int, void*)
{
m = cv::Scalar(lightness, lightness, lightness);
cv::add(src, m, dst);
// 显示结果
cv::imshow("调节亮度", dst);
cv::waitKey(0);
}
int main()
{
src = cv::imread("C:/Users/Desktop/02.jpg");
m = cv::Mat::zeros(src.size(), src.type());
dst = cv::Mat::zeros(src.size(), src.type());
cv::namedWindow("调节亮度", cv::WINDOW_FREERATIO);
cv::createTrackbar("Value Bar:", "调节亮度", &lightness, max_value, on_track);
// 手动调用回调函数以初始化显示
on_track(lightness, 0);
system("pause");
return 0;
}
3.6.2 参数传递
#include<iostream>
#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;
void on_lightness(int lightness, void* userdata)
{
cv::Mat src = *((cv::Mat*)userdata);
cv::Mat m = cv::Mat::zeros(src.size(), src.type());
m = cv::Scalar(lightness, lightness, lightness);
cv::Mat dst = cv::Mat::zeros(src.size(), src.type());
// dst = alpha * src + beta * m + gamma
// alpha = 1, beta = 0, gamma =0, dst = alpha + gamma 就是调整亮度了
cv::addWeighted(src, 1.0, m, 0, lightness, dst);
// 显示结果
cv::imshow("亮度和对比度调整", dst);
cv::waitKey(0);
}
void on_contrast(int lightness, void* userdata)
{
cv::Mat src = *((cv::Mat*)userdata);
cv::Mat m = cv::Mat::zeros(src.size(), src.type());
m = cv::Scalar(lightness, lightness, lightness);
cv::Mat dst = cv::Mat::zeros(src.size(), src.type());
double contrast = lightness / 100.0;
// alpha = contrast, beta = 0, gamma = 0, dst = src * alpha 就是调整亮度了
cv::addWeighted(src, contrast, m, 0.0, 0, dst);
// 显示结果
cv::imshow("亮度和对比度调整", dst);
cv::waitKey(0);
}
int main()
{
cv::Mat src = cv::imread("C:/Users/15198/Desktop/微信图片_20240625171102.jpg");
int max_value = 100; // 滑块的最大值
int lightness = 50; // 初始亮度值
int contrast_value = 100; // 默认对比度
cv::namedWindow("亮度和对比度调整", cv::WINDOW_FREERATIO);
cv::createTrackbar("Value Bar:", "亮度和对比度调整", &lightness, max_value, on_lightness, (void*)&src);
cv::createTrackbar("Contrast Bar:", "亮度和对比度调整", &contrast_value, max_value, on_contrast, (void*)&src);
// 手动调用回调函数以初始化显示
on_lightness(lightness, &src);
system("pause");
return 0;
}
3.7 键盘响应
cv::waitKey的返回类型是int
。它返回按键的 ASCII 码值
(如果有按键被按下),如果在指定的等待时间内没有按键被按下,则返回 -1。
ASCII 码的基本概念
字符与数值的对应关系:每个 ASCII 字符都有一个对应的数值。例如,大写字母 ‘A’ 的 ASCII 码是 65,小写字母 ‘a’ 的 ASCII 码是 97。
数字字符:数字字符 ‘0’ 到 ‘9’ 的 ASCII 码分别是 48 到 57。例如,字符 ‘1’ 的 ASCII 码是 49。
#include<iostream>
#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main()
{
cv::Mat src = cv::imread("C:/Users/15198/Desktop/微信图片_20240625171102.jpg");
cv::namedWindow("my_Window", cv::WINDOW_FREERATIO);
cv::imshow("my_Window", src);
cv::Mat dst = cv::Mat::zeros(src.size(), src.type());
cv::namedWindow("键盘响应结果", cv::WINDOW_FREERATIO);
while (true)
{
int c = cv::waitKey(100);
if (c == 27) // key esc
{
break;
}
if (c == 49) // key 数字1
{
// 可以做一些自定义操作来配合键盘响应
cv::cvtColor(src, dst, cv::COLOR_BGR2GRAY);
}
if (c == 50) // key 数字2
{
// 可以做一些自定义操作来配合键盘响应
cv::cvtColor(src, dst, cv::COLOR_BGR2HSV);
}
//cout << c << endl;
cv::imshow("键盘响应结果", dst);
}
system("pause");
return 0;
}
3.8 opencv自带颜色表操作
语法:
cv::applyColorMap(src, dst, COLORMAP); // 将颜色映射应用于灰度图像
一共21种,其实cv底层都能看到。
int main()
{
cv::Mat src = cv::imread("C:/Users/Desktop/02.jpg");
cv::Mat dst = cv::Mat::zeros(src.size(), src.type());
cv::applyColorMap(src, dst, cv::COLORMAP_JET);
cv::namedWindow("result", cv::WINDOW_FREERATIO);
cv::imshow("result", dst);
cv::waitKey(0);
system("pause");
return 0;
}
3.9 图像像素的逻辑操作
图像像素除了加、减、乘、除这些算数操作,还有逻辑操作:
语法:
// 矩形的左上角坐标和宽、高
cv::Rect(int x, int y, int width, int height);
// thickness: 矩形边框的厚度。默认值是 1。设置为 -1 时会填充整个矩形。
void cv::rectangle(InputOutputArray img, const Rect& rec, const Scalar& color, int thickness = 1, int lineType = LINE_8, int shift = 0);
cv::bitwise_and(m1, m2, dst); // 与运算
cv::bitwise_or(m1, m2, dst); // 或运算
cv::bitwise_not(m1, dst); // 或运算
cv::bitwise_xor(m1, m2, dst); // 异或运算
int main()
{
cv::Mat m1 = cv::Mat::zeros(cv::Size(256, 256), CV_8UC3);
cv::Mat m2 = cv::Mat::zeros(cv::Size(256, 256), CV_8UC3);
// 绘制矩形
// cv::Rect(x, y, w, h) 矩形的左上角坐标和宽、高
cv::rectangle(m1, cv::Rect(100, 100, 80, 80), cv::Scalar(255, 255, 0), -1);
cv::rectangle(m2, cv::Rect(150, 150, 80, 80), cv::Scalar(0, 255, 255), -1);
// 像素位操作
cv::Mat dst;
// m1和m2进行与运算,只留下G通道与为运算后为1的颜色
//cv::bitwise_and(m1, m2, dst);
// 或运算
//cv::bitwise_or(m1, m2, dst);
// 非运算
cv::bitwise_not(m1, dst);
cv::imshow("m1", m1);
cv::imshow("m2", m2);
cv::imshow("dst", dst);
cv::waitKey(0);
system("pause");
return 0;
}
3.10 通道分离与合并
语法:
vector<cv::Mat>v;
cv::split(src, v); // 将图像的三通分离并存放在vector容器中
cv::merge(v, dst); // 将vector容器中的三通道进行合并
cv::mixChannels(); // 不同的通道之间进行数据混合的函数。允许从一个或多个源图像的通道中提取数据,并将这些数据放入目标图像的指定通道中。
int main()
{
cv::Mat src = cv::imread("C:/Users/Desktop/02.jpg");
vector<cv::Mat>v;
cv::split(src, v);
// 单通道的话就是灰度图
cv::imshow("B", v[0]);
cv::imshow("G", v[1]);
cv::imshow("R", v[2]);
cv::Mat dst;
// v的内容与src一样,此时dst是src的原始图像
cv::merge(v, dst);
v[0] = 0;
// 这个是将B通道像素值置0,再合并三通道
cv::merge(v, dst);
cv::imshow("merge", dst);
cv::waitKey(0);
cv::destroyAllWindows();
system("pause");
return 0;
}
最好是建议将原始图像和目标图像进行通道分离后再进行mixChannels
#include<iostream>
#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;
#include<vector>
int main()
{
// 将原始图像进行通道分离
cv::Mat src = cv::imread("C:/Users/15198/Desktop/微信图片_20240625171102.jpg");
vector<cv::Mat>Vsrc;
cv::split(src, Vsrc);
// 将目标图像进行通道分离
cv::Mat dst;
vector<cv::Mat>Vdst;
cv::split(src, Vdst);
// 从源图像的第 0、2、1 通道到目标图像的第 1、2、0 通道
int from_to[] = { 0, 2, 1, 1, 2, 0 };
// 3 表示 from_to 数组中有 3 个通道映射对需要处理。每个映射对包含两个整数
cv::mixChannels(Vsrc, Vdst, from_to, 3);
// 通道合并
cv::merge(Vdst, dst);
cv::imshow("result", dst);
cv::waitKey(0);
cv::destroyAllWindows();
system("pause");
return 0;
}
3.11 图像色彩空间转换
语法:
cv::cvtColor(); // 转变颜色空间
cv::inRange(); // 用于图像阈值化和分割的函数。它可以用于检测图像中的像素是否在指定的颜色范围内
有时候RGB色彩空间不一定能够区分出我们想要的前景,此时,我们可以转换到另一个色彩空间,如:HSV、YCbCr、YUV等。比如下方的例子是提取绿色的前景,而参考HSV中绿色的范围,可以用cv::inRange()来选出mask,再结合src.copyTo(back, mask)得到我们想要的前景
int main()
{
// 将原始图像进行通道分离
cv::Mat src = cv::imread("C:/Users/Desktop/02.jpg");
cv::Mat Hsv, mask;
cv::cvtColor(src, Hsv, cv::COLOR_BGR2HSV);
// 以要提取绿色背景为例
cv::inRange(Hsv, cv::Scalar(35, 43, 46), cv::Scalar(77, 255, 255), mask);
cv::Mat back = cv::Mat::zeros(src.size(), src.type());
back = cv::Scalar(40, 40, 200);
// 将前景部分设置为1,背景设置为0
cv::bitwise_not(mask, mask);
// copyTo方法有个函数有个函数重载版本,只拷贝第二个参数mask部分的像素到目标图像上
src.copyTo(back, mask);
cv::imshow("result", mask);
cv::waitKey(0);
cv::destroyAllWindows();
system("pause");
return 0;
}
3.12 像素值统计
语法:
// 都可以选掩模图像,用于计算mask区域的均值和标准差。
// 主要用于单通道图像。如果处理多通道图像,它只会处理第一个通道的像素值。
cv::minMaxLoc(grayImage, &minVal, &maxVal, &minLoc, &maxLoc); // 计算图像或图像区域的最小值和最大值,以及它们的位置。
// 以处理单通道或多通道图像,对每个通道分别计算均值和标准差。
cv::meanStdDev(colorImage, mean, stddev); // 计算图像或图像区域的均值和标准差。
cv::Mat m;
m.size(); // 返回图像的宽x高
m.total(); // 返回图像中的像素数(WxHxC) / 矩阵中元素的个数
int main()
{
// 将原始图像进行通道分离
cv::Mat src = cv::imread("C:/Users/Desktop/02.jpg");
vector<cv::Mat>v;
cv::split(src, v);
double minv, maxv;
cv::Point minLoc, maxLoc;
cv::Mat mean, stddev;
for (int i = 0; i < v.size(); i++)
{
cout << "当前通道为:" << i + 1 << endl;
cv::minMaxLoc(v[0], &minv, &maxv, &minLoc, &maxLoc);
cout << "Min Value: " << minv << " at (" << minLoc.x << ", " << minLoc.y << ")" << endl;
cout << "Max Value: " << maxv << " at (" << maxLoc.x << ", " << maxLoc.y << ")" << endl;
}
cv::meanStdDev(src, mean, stddev);
cout << "mean = " << mean << " stddev = " << stddev << endl;
// mean和stddev输出的就是Mat,也就是矩阵
// 可以通过索引来输出
for (int i = 0; i < mean.total(); i++)
{
cout << "第" << i + 1 << "个通道的均值和标准差如下:" << endl;
cout << "mean = " << mean.at<double>(i) << endl;
cout << "stddev = " << stddev.at<double>(i) << endl;
}
system("pause");
return 0;
}
3.13 图像几何形状绘制
语法:
注意事项:
1、绘制图像的lineType参数,默认是LINE_8(值为 8):使用 8-邻域连接线段(常见的直线绘制模式)。LINE_AA(值为 16):使用抗锯齿线条,这种模式使线条看起来更平滑,减少锯齿现象。
2、thickness >= 1时,表示线宽;thickness = -1时,表示填充
。
cv::Size(640, 480); // 指定图像的尺寸或矩形的尺寸。
cv::Point p1(10, 20); // 表一个二维坐标点,具有 x 和 y 坐标。
cv::RotatedRect(p1, cv::Size(100, 100), 0.0); // 表示一个带有旋转角度的矩形区域。它包括矩形的中心位置、大小(宽度和高度)以及旋转角度。
cv::Rect rect(10, 20, 100, 50); // 代表一个矩形区域,具有左上角的坐标 (x, y)、宽度和高度。
// 绘制矩形, thickness为-1表示填充
cv::rectangle(image, Rect(100, 100, 200, 100), Scalar(0, 255, 0), 2);
// 绘制圆形, thickness为-1表示填充
cv::circle(image, Point(200, 200), 100, Scalar(0, 0, 255), 3);
// 绘制直线
cv::line(image, Point(50, 50), Point(350, 350), Scalar(255, 0, 0), 2);
// 绘制椭圆, thickness为-1表示填充
cv::ellipse(image, RotatedRect(Point(200, 200), Size(100, 50), 45), Scalar(255, 255, 0), 2);
int main()
{
cv::Mat src = cv::imread("C:/Users/Desktop/102.jpg");
cv::Rect rect1(100, 100, 250, 300);
cv::Point p1(100, 100);
cv::Point p2(300, 400);
cv::RotatedRect r1(p1, cv::Size(100, 200), 0.0);
// 绘制矩形
cv::rectangle(src, rect1, cv::Scalar(0, 0, 255), -1);
// 绘制圆形
cv::circle(src, p1, 15, cv::Scalar(0, 255, 0), 2);
// 绘制直线
cv::line(src, p1, p2, cv::Scalar(255, 0, 0), 2);
// 绘制椭圆
cv::ellipse(src, r1, cv::Scalar(255, 255, 0), -1);
cv::imshow("window", src);
cv::waitKey(0);
cv::destroyAllWindows();
system("pause");
return 0;
}
3.14 随机数与随机颜色
语法:
// cv::RNG(Random Number Generator)用于生成随机数。rng(12345) 是使用种子 12345 初始化一个随机数生成器对象。
cv::RNG rng(12345); // 初始化随机数生成器
// 生成一个范围在 0 到 255 之间的随机整数。这通常用于生成随机颜色值或其他需要随机性的场景。
rng.uniform(0, 255)
int main()
{
cv::Mat canvas = cv::Mat::zeros(cv::Size(512, 512), CV_8UC3);
cv::RNG rng(12345);
while (true)
{
int x1 = rng.uniform(0, canvas.cols);
int y1 = rng.uniform(0, canvas.rows);
int x2 = rng.uniform(0, canvas.cols);
int y2 = rng.uniform(0, canvas.rows);
int b = rng.uniform(0, 255);
int g = rng.uniform(0, 255);
int r = rng.uniform(0, 255);
cv::line(canvas, cv::Point(x1, y1), cv::Point(x2, y2), cv::Scalar(b, g, r));
cv::imshow("result", canvas);
cv::waitKey(10);
}
system("pause");
return 0;
}
3.15 多边形填充与绘制
注意事项:
1、存放1个多边形的所有顶点可以用容器
语法:
// 这个不可以设置thickness为-1 填充
cv::polylines(image, points, true, cv::Scalar(0, 255, 0), 2); // 绘制多边形的边框。
cv::fillPoly(image, std::vector<std::vector<cv::Point>>{points}, cv::Scalar(0, 0, 255)); // 填充多边形内部。
// 这个可以设置thickness为-1 填充
cv::drawContours(image, contours, -1, cv::Scalar(0, 255, 0), 2); // 绘制轮廓。
int main()
{
cv::Mat canvas = cv::Mat::zeros(cv::Size(512, 512), CV_8UC3);
vector<cv::Point>pts;
cv::Point p1(100, 100);
cv::Point p2(350, 100);
cv::Point p3(450, 280);
cv::Point p4(320, 450);
cv::Point p5(80, 400);
pts.push_back(p1);
pts.push_back(p2);
pts.push_back(p3);
pts.push_back(p4);
pts.push_back(p5);
vector<cv::Point>pts2;
cv::Point t1(50, 100);
cv::Point t2(10, 100);
cv::Point t3(50, 580);
cv::Point t4(100, 100);
pts2.push_back(t1);
pts2.push_back(t2);
pts2.push_back(t3);
pts2.push_back(t4);
// 想要一次性绘制多个多边形,就用大容器存放多个多边形
vector<vector<cv::Point>>contours;
contours.push_back(pts);
contours.push_back(pts2);
// 这里的多边形绘制线,thickness 不能设置 -1 为填充
//cv::polylines(canvas, pts, true, cv::Scalar(0, 0, 255), 2);
// 填充多边形
//cv::fillPoly(canvas, pts, cv::Scalar(0, 0, 255), 2, 0);
// 也是绘制多边形,thickness设置1为填充
cv::drawContours(canvas, contours, 0, cv::Scalar(0, 0, 255), -1);
cv::imshow("result", canvas);
cv::waitKey(0);
system("pause");
return 0;
}
3.16 鼠标操作与响应
注意事项:
1、将原始图像先clone
到temp
,最后将一些结果绘制到temp上,这个方法要会。
语法:
cv::setMouseCallback("Image", onMouse); // 设置鼠标事件的回调函数。
// 有点像python中numpy切片一样
image(cv::Rect(sp.x, sp.y, w, h)); // 从图像中提取了一个 wxh 的ROI区域出来。
#include<iostream>
#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;
#include<vector>
// 这里需要将鼠标点击的点和松开的点这两个点设置为全局变量
// 因为当点击后,松开会再次调用on_draw,这个时候sp就会走默认值0
// 每次绘制左上角的点开始,就不对了。
cv::Point sp;
cv::Point ep;
cv::Mat temp;
void on_draw(int event, int x, int y, int flags, void* userdata)
{
// void* 首先必须先制定数据类型,强转成(cv::Mat*)userdata
// 再解引用成Mat
cv::Mat image = *((cv::Mat*)userdata);
// 触发鼠标左击按下事件
if (event == cv::EVENT_LBUTTONDOWN)
{
sp.x = x;
sp.y = y;
//cout << "开始的点坐标为:" << sp << endl;
}
// 触发鼠标左击松开事件
else if (event == cv::EVENT_LBUTTONUP)
{
ep.x = x;
ep.y = y;
int w = ep.x - sp.x;
int h = ep.y - sp.y;
cout << "开始的点坐标为:" << sp << endl;
cout << "结束的点坐标为:" << ep << endl;
// 判断矩形的宽高是否是正数
if (w > 0 && h > 0)
{
cv::rectangle(image, cv::Rect(sp.x, sp.y, w, h), cv::Scalar(0, 0, 255));
temp.copyTo(image);
cv::imshow("鼠标绘制", image);
cv::imshow("ROI", image(cv::Rect(sp.x, sp.y, w, h)));
// 当鼠标松开的时候,应该将sp的坐标重置,完成当前矩形绘制,不然MOVE会继续绘制
sp.x = -1;
sp.y = -1;
}
}
// 触发鼠标移动事件
else if (event == cv::EVENT_MOUSEMOVE)
{
// 这里一定要判断sp是不是大于0
// 不然就是:鼠标压根没点击,只要移动鼠标就开始画矩形了。
if (sp.x > 0 && sp.y > 0)
{
ep.x = x;
ep.y = y;
int w = ep.x - sp.x;
int h = ep.y - sp.y;
// 判断矩形的宽高是否是正数
if (w > 0 && h > 0)
{
temp.copyTo(image);
cv::rectangle(image, cv::Rect(sp.x, sp.y, w, h), cv::Scalar(0, 0, 255));
}
cv::imshow("鼠标绘制", image);
}
}
}
int main()
{
cv::Mat canvas = cv::Mat::zeros(cv::Size(512, 512), CV_8UC3);
temp = canvas.clone();
cv::namedWindow("鼠标绘制", cv::WINDOW_AUTOSIZE);
cv::setMouseCallback("鼠标绘制", on_draw, (void*)(&canvas));
cv::imshow("鼠标绘制", canvas);
cv::waitKey(0);
system("pause");
return 0;
}
3.17 图像像素类型转换和归一化
常见的归一化:
1、NORM_L1:每个像素除以像素值之和;
2、NORM_L2:每个像素除以像素值的平方和的平方根;
3、NORM_INF:每个像素值÷最大值;
4、NORM_MINMAX:(x-min)/(max-min)。
语法:
src.convertTo(dst, CV_32FC3); // 转换图像的数据类型
cv::normalize(dst, dst_normal, 1.0, 0.0, cv::NORM_MINMAX); // 将 dst 中的值归一化到 [0, 1] 范围
注意事项:
1、如果不将CV_8UC3转换成CV_32FC3 ,而是直接CV_8UC3做归一化,8位无符号型通过imshow可视化时,像素值是:0-255;而直接归一化到0-1之间,都是0.几的小数就是几乎是0,全黑图像了;
2、如果将CV_8UC3转换成CV_32FC3,没做
归一化,可视化也是不对的,opencv中对float可视化,必须在0-1之间;
3、所以,一定先
转float浮点型,再
做归一化。
int main()
{
cv::Mat src = cv::imread("C:/Users/Desktop/02.jpg");
cout << "src type = " << src.type() << endl;
cv::Mat dst;
// 直接对CV_8UC3进行归一化
cv::normalize(src, dst, 1.0, 0.0, cv::NORM_MINMAX);
cv::imshow("result", dst);
cv::waitKey(0);
system("pause");
return 0;
}
正确代码示例:
int main()
{
cv::Mat src = cv::imread("C:/Users/Desktop/02.jpg");
cout << "src type = " << src.type() << endl;
cv::Mat dst, dst_normal;
// CV_8UC3、CV_32FC3 8和32都是每个字节是几个bit,U和F代表数据类型:无符号char、float型,C3代表channel3=3
// CV_32S 32位整型(s表示整型)
src.convertTo(dst, CV_32FC3);
cout << "dst type = " << dst.type() << endl;
// 转换成float型后,就可以进行归一化了
cv::normalize(src, dst_normal, 1.0, 0.0, cv::NORM_MINMAX);
cout << "dst_normal type = " << dst.type() << endl;
cv::imshow("result", dst_normal);
cv::waitKey(0);
system("pause");
return 0;
}
3.18 图像放缩与插值
语法:
cv::resize(src, dst, newSize); // 改变图像的尺寸
int main()
{
cv::Mat src = cv::imread("C:/Users/Desktop/02.jpg");
int w = src.rows;
int h = src.cols;
cv::Mat min_src, max_src;
cv::resize(src, min_src, cv::Size(w / 2, h / 2), 0, 0, INTER_LINEAR);
cv::resize(src, min_src, cv::Size(w * 1.5, h * 1.5), 0, 0, INTER_LINEAR);
cv::imshow("缩小图", min_src);
cv::imshow("放大图", min_src);
cv::waitKey(0);
system("pause");
return 0;
}
3.19 图像反转
语法:
cv::flip(src, dst, 0); // 垂直翻转图像
cv::flip(src, dst, 1); // 水平翻转
cv::flip(src, dst, -1); // 同时水平和垂直翻转图像
int main()
{
cv::Mat src = cv::imread("C:/Users/Desktop/02.jpg");
cv::Mat dst;
// 0表示上下反转,1表示左右翻转,-1表示上下左右翻转
cv::flip(src, dst, -1);
cv::namedWindow("图像反转", WINDOW_FREERATIO);
cv::imshow("图像反转", dst);
cv::waitKey(0);
system("pause");
return 0;
}
3.20 图像旋转
语法:
cv::Point pt1(10, 20); // 整数坐标
cv::Point2f pt2(10.5, 20.5); // 浮点数坐标
// 返回一个 2x3 的矩阵 M(生成的矩阵 M 包含旋转和平移信息。),用于将图像旋转 angle 角度(以 center 为中心),并且按 scale 缩放。
cv::Mat M = cv::getRotationMatrix2D(center, angle, scale); // 生成二维旋转矩阵
cv::Mat M = cv::getRotationMatrix2D(cv::Point2f(src.cols / 2.0, src.rows / 2.0), 45.0, 1.0); // 应用仿射变换到图像。
int main()
{
cv::Mat src = cv::imread("C:/Users/Desktop/02.jpg");
int w = src.rows;
int h = src.cols;
cv::Mat dst, M;
// 获取旋转矩阵
M = cv::getRotationMatrix2D(cv::Point2f(w / 2, h / 2), 45, 1.0);
cv::warpAffine(src, dst, M, src.size());
cv::namedWindow("图像旋转", WINDOW_FREERATIO);
cv::imshow("图像旋转", dst);
cv::waitKey(0);
system("pause");
return 0;
}
但其实,旋转后的图像按照上述代码,图像的size还是沿用原图的话,旋转后的图片会显示不全。
真正的,旋转后的图像的宽和旋转前的图像宽度,关系如下:
new_w = w * cos(angle) + h * sin(angle)
new_h = w * sin(angle) + h * cos(angle)
int main()
{
cv::Mat src = cv::imread("C:/Users/Desktop/02.jpg");
int w = src.cols;
int h = src.rows;
cv::Mat dst, M;
// 获取旋转矩阵
M = cv::getRotationMatrix2D(cv::Point2f(w / 2, h / 2), 45, 1.0);
cout << M.at<double>(0, 2) << endl;
cout << M.at<double>(1, 2) << endl;
double cos = abs(M.at<double>(0, 0));
double sin = abs(M.at<double>(0, 1));
// 求出新的旋转后的图像的宽高
int new_w = w * cos + h * sin;
int new_h = w * sin + h * cos;
// M矩阵的第3列是仿射变换的平移部分,保证了图像在旋转和缩放之后,旋转中心保持在原始图像的中心位置。
// 所以这里要计算出,图像在新的宽高下的中心点,计算出旋转矩阵的中心的偏移量
// 使旋转后的图像保持在新图像的中心位置。
M.at<double>(0, 2) = M.at<double>(0, 2) + (new_w / 2 - w / 2);
M.at<double>(1, 2) = M.at<double>(1, 2) + (new_h / 2 - h / 2);
cv::warpAffine(src, dst, M, cv::Size(new_w, new_h));
cv::namedWindow("图像旋转", WINDOW_FREERATIO);
cv::imshow("图像旋转", dst);
cv::waitKey(0);
system("pause");
return 0;
}
注意事项:
1、旋转矩阵如下,所以可以通过api获取旋转矩阵M后,通过.at方法
获取cos和sin的值。
2、旋转矩阵M是1个2x3的矩阵,第三列是是仿射变换的平移部分
,保证了图像在旋转和缩放之后,旋转中心保持在原始图像的中心位置
。
3.21 读取视频流
语法:
// source 可以是摄像头的编号(如0),也可以是视频文件的路径。
cv::VideoCapture cap(source); // 读取视频流
cap.get(CAP_PROP_FRAME_HEIGHT); // 视频帧的高度度
cap.get(CAP_PROP_FRAME_HEIGHT); // 视频帧的宽度
cap.get(CAP_PROP_FRAME_COUNT); // 视频的总帧数
cap.get(CAP_PROP_FPS); // 视频的帧率
int main()
{
cv::VideoCapture cap(0);
cv::Mat src;
while (true)
{
cap.read(src);
if (src.empty())
{
break;
}
cv::flip(src, src, 1);
cv::imshow("video", src);
cv::waitKey(10);
}
cv::destroyAllWindows();
cap.release();
system("pause");
return 0;
}
3.22 视频处理与保存
语法:
// filename: 保存视频的文件名(如 "output.avi")
// codec: 视频编解码器的四字符代码(如 cv::VideoWriter::fourcc('M', 'J', 'P', 'G'))。
// fps: 每秒帧数。
// frameSize: 帧的大小,如 cv::Size(width, height)。
// isColor: 帧是否是彩色的布尔值。
cv::VideoWriter writer(filename, codec, fps, frameSize, isColor); // 保存视频流
writer.write(frame); // 写入帧到视频文件
int main()
{
cv::VideoCapture cap(0);
int frame_width = cap.get(CAP_PROP_FRAME_HEIGHT);
int frame_height = cap.get(CAP_PROP_FRAME_HEIGHT);
// 视频的总帧数
int count = cap.get(CAP_PROP_FRAME_COUNT);
int fps = cap.get(CAP_PROP_FPS);
// 创建视频存储对象
cv::VideoWriter writer("D:/test.mp4", cap.get(CAP_PROP_FOURCC), fps, cv::Size(frame_width, frame_height), true);
cv::Mat src;
while (true)
{
cap.read(src);
if (src.empty())
{
break;
}
cv::flip(src, src, 1);
cv::imshow("video", src);
writer.write(src);
if (cv::waitKey(1) == 27)
{
// 按下ESC键退出
break;
}
}
cv::destroyAllWindows();
cap.release();
writer.release();
system("pause");
return 0;
}
3.23 一维图像直方图
图像直方图是图像像素值的统计学特征、计算代价较小,具有图像平移、旋转、缩放不变性
等众多优点,广泛地应用于图像处理的各个领域,特别是灰度图像的阈值分割、基于颜色的图像检索以及图像分类、反向投影跟踪。常见的分为:
1、灰度
直方图
2、颜色
直方图
Bins是指直方图的大小范围,对于像素值取值在0~255之间的,最少有256个bin,此外还可以有16、32、48、128等,256除以bin的大小应该是整数倍。
语法:
// 直方图参数
int histSize = 256; // bin 数量
float range[] = {0, 256}; // 像素值范围
const float* histRange = {range};
// 存储直方图的矩阵
cv::Mat hist;
// 计算直方图
// 参数1 images: 图像的指针数组,类型为 cv::Mat。通常情况下,我们只需要处理一张图像,因此可以传递该图像的指针。
// 参数2 nimages: 图像的数量,通常为1(如果有多张图像,可以传递多个图像的指针)。
// 参数3 channels: 指向通道索引的指针。对于彩色图像,你可以使用 {0, 1, 2} 分别代表 B、G、R 通道;对于灰度图像,使用 {0} 表示单通道。
// 参数4 mask // 参数5 hist: 输出矩阵
// 参数6 dims: 直方图的维度。对于灰度图像是1维,对于彩色图像是3维(分别对应 B、G、R 通道)。
// 参数7 histSize: 直方图的大小,是一个整型数组,指定每一维的 bin 数量。例如,{256} 表示灰度图像的 256 个 bin。
// 参数8 ranges: 每一维的取值范围,是一个浮点数组的指针。例如 {0, 256} 表示像素值范围从0到255。
cv::calcHist(&img, 1, 0, cv::Mat(), hist, 1, &histSize, &histRange);
// 归一化直方图
cv::normalize(hist, hist, 0, 255, cv::NORM_MINMAX);
注意事项:
1、一定要做归一化
,归一化将直方图的值调整到一个统一的范围(如0到255),使得在不同的图像或实验中,可以比较直方图的相对形状而不受图像总像素数的影响。
2、在直方图中,**bin(桶)**是用来存储像素值频率的区间。每个 bin 对应一个特定的像素值范围,用于计算该范围内像素值的数量。
假设你有一个灰度图像,像素值范围是 0 到 255,你选择 256 个 bin:
每个 bin 会覆盖像素值的一个小区间,例如第一个 bin 对应 0,第二个 bin 对应 1,以此类推。
直方图显示了每个 bin 中的像素数量,帮助分析图像的亮度分布。
int main()
{
cv::Mat src = cv::imread("C:/Users/Desktop/02.jpg");
// 三通道分离
vector<cv::Mat>bgr_plane;
cv::split(src, bgr_plane);
// 定义参数变量
const int channels[1] = { 0 };
// 像素值范围是:0-255
float hranges[2] = { 0,255 };
// 总共256个灰度级
const int bins[1] = { 256 };
const float* ranges[1] = { hranges };
// 计算BGR三通道的直方图
cv::Mat b_hist, g_hist, r_hist;
// 1代表多少张图,0代表1个通道,第2个1代表直方图是1维的
// 这里的直方图b_hist维度是1x256(取决于bin的个数),代表着直方图有 256 个 bins(箱子),展示了每个灰度值(0 到 255)在该通道中的频率。
cv::calcHist(&bgr_plane[0], 1, 0, cv::Mat(), b_hist, 1, bins, ranges);
cv::calcHist(&bgr_plane[1], 1, 0, cv::Mat(), g_hist, 1, bins, ranges);
cv::calcHist(&bgr_plane[2], 1, 0, cv::Mat(), r_hist, 1, bins, ranges);
// 显示直方图
int hist_w = 512;
int hist_h = 400;
// bins有256个灰度级,画布的宽度是512,512 / 256 = 2,相当于每个bin占2个像素
int bin_w = cvRound((double)hist_w / bins[0]);
cv::Mat histImage = cv::Mat::zeros(hist_w, hist_h, CV_8UC3);
// 归一化直方图,不超过画布的高度许可范围
cv::normalize(b_hist, b_hist, 0, histImage.rows, NORM_MINMAX, -1, cv::Mat());
cv::normalize(g_hist, g_hist, 0, histImage.rows, NORM_MINMAX, -1, cv::Mat());
cv::normalize(r_hist, r_hist, 0, histImage.rows, NORM_MINMAX, -1, cv::Mat());
// 绘制直方图
for (int i = 1; i < bins[0]; i++)
{
// 因为图像坐标系的原点在左上角,所以
// 这里x轴坐标是每个bin,bin_w * (i - 1),bin_w * (i)代表着往下移一个bin。
// y轴 = 画布高度 - 当前bin的直方图值
// 相当于每循环1次,就连接2个点
cv::line(histImage, cv::Point(bin_w * (i - 1), hist_h - cvRound(b_hist.at<float>(i - 1))),
cv::Point(bin_w * (i), hist_h - cvRound(b_hist.at<float>(i))), cv::Scalar(255, 0, 0), 2, 8, 0);
cv::line(histImage, cv::Point(bin_w * (i - 1), hist_h - cvRound(g_hist.at<float>(i - 1))),
cv::Point(bin_w * (i), hist_h - cvRound(g_hist.at<float>(i))), cv::Scalar(0, 255, 0), 2, 8, 0);
cv::line(histImage, cv::Point(bin_w * (i - 1), hist_h - cvRound(r_hist.at<float>(i - 1))),
cv::Point(bin_w * (i), hist_h - cvRound(r_hist.at<float>(i))), cv::Scalar(0, 0, 255), 2, 8, 0);
}
// 显示直方图
cv::namedWindow("直方图", WINDOW_AUTOSIZE);
cv::imshow("直方图", histImage);
cv::waitKey(0);
system("pause");
return 0;
}
3.24 二维图像直方图
语法:
#include<iostream>
#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;
#include<vector>
int main()
{
cv::Mat src = cv::imread("C:/Users/Desktop/02.jpg");
cv::Mat hsv, hs_hist;
cv::cvtColor(src, hsv, COLOR_BGR2HSV);
// 因为在HSV色彩空间下,H是0-180,S是0-255,所以H分为30个间隔,S分32个间隔
int hbins = 30, sbins = 32;
int hist_bins[] = { hbins, sbins};
float h_range[] = { 0,180 };
float s_range[] = { 0,256 };
const float* hs_ranges[] = { h_range,s_range };
// H和S对应的通道
int hs_channels[] = { 0,1 };
// 将整个hsv图像输入,1代表1张图,hs_channels取H和S两个通道,Mask为空(整个图像输入)
// 2代表二维的,再把划分多少个bin,以及像素值的取值范围填进来
// 最后hs_hist的维度就是32x30,两个维度上bin的个数
cv::calcHist(&hsv, 1, hs_channels, cv::Mat(), hs_hist, 2, hist_bins, hs_ranges);
// hs_hist相当于1个二维图像,先进行归一化
double maxVal = 0;
cv::minMaxLoc(hs_hist, 0, &maxVal, 0, 0);
int scale = 10;
cv::Mat hist2d_image = cv::Mat::zeros(sbins * scale, hbins * scale, CV_8UC3);
for (int h = 0; h < hbins; h++)
{
for (int s = 0; s < sbins; s++)
{
float binVal = hs_hist.at<float>(h, s);
int intensity = cvRound(binVal * 255 / maxVal);
cv::rectangle(hist2d_image, cv::Point(h * scale, s * scale),
cv::Point((h + 1) * scale - 1, (s + 1) * scale - 1), cv::Scalar::all(intensity), -1);
}
}
cv::imshow("hist2d_image", hist2d_image);
cv::waitKey(0);
system("pause");
return 0;
}
3.25 直方图均衡化
图像直方图均衡化可以用于图像增强、对输入图像进行直方图均衡化处理,提升后续对象检测的准确率在OpenCV人脸检测的代码演示中已经很常见。此外对医学影像图像与卫星遥感图像也经常通过直方图均衡化来提升图像质量。
int main()
{
cv::Mat src = cv::imread("C:/Users/Desktop/02.jpg");
cv::Mat gray, dst;
cv::cvtColor(src, gray, COLOR_BGR2GRAY);
cv::equalizeHist(gray, dst);
cv::imshow("dst", dst);
cv::waitKey(0);
system("pause");
return 0;
}
3.26 图像卷积操作
语法:
// 每个像素的值是其邻域像素值的平均值。
cv::blur(src, dst, ksize); // 均值滤波
均值卷积
,因为每一个卷积核大小内都是线性组合后求平均,会将像素值大的往下拉,像素值过小的往上拉,因为将图像的对比度减小了,作用是图像模糊
。
int main()
{
cv::Mat src = cv::imread("C:/Users/Desktop/02.jpg");
cv::Mat dst;
cv::blur(src, dst, cv::Size(13, 13), cv::Point(-1, -1));
// 也支持水平方向上的卷积
//cv::blur(src, dst, cv::Size(3, 1), cv::Point(-1, -1));
// 也支持垂直方向上的卷积
//cv::blur(src, dst, cv::Size(1, 3), cv::Point(-1, -1));
cv::namedWindow("原图", WINDOW_FREERATIO);
cv::namedWindow("图像模糊", WINDOW_FREERATIO);
cv::imshow("原图", src);
cv::imshow("图像模糊", dst);
cv::waitKey(0);
cv::destroyAllWindows();
system("pause");
return 0;
}
3.27 高斯模糊
均值模糊是卷积核的系数完全一致
,高斯模糊考虑了中心像素距离的影响,对距离中心像素使用高斯分布公式生成不同
的权重系数给卷积核,然后用此卷积核完成图像卷积得到输出结果就是图像高斯模糊
之后的输出。
区别
:
均值卷积没有焦虑中心
像素对卷积核输出的贡献,而高斯卷积考虑了。
越靠近中心,系数越大。
语法:
// 通过高斯函数对图像进行平滑处理。相比均值滤波,高斯滤波对中心像素的影响更大,边缘部分的影响则较小。
// kernel size越大 / Sigma越大,模糊效果越厉害
// 高斯核的大小(宽度,高度),必须是奇数且大于1。
cv::GaussianBlur(src, dst, ksize, sigmaX, sigmaY); // 高斯滤波
int main()
{
cv::Mat src = cv::imread("C:/Users/Desktop/02.jpg");
cv::Mat dst;
cv::GaussianBlur(src, dst, cv::Size(3, 3), 15);
// 当卷积核设置窗口大小后,Sigma的设置就无效了,会根据kernel size计算Sigma
// 当卷积核设置窗口为0后,会根据Sigma的设置反算kernel size
//cv::GaussianBlur(src, dst, cv::Size(0, 0), 15);
cv::namedWindow("原图", WINDOW_FREERATIO);
cv::namedWindow("高斯模糊", WINDOW_FREERATIO);
cv::imshow("原图", src);
cv::imshow("高斯模糊", dst);
cv::waitKey(0);
cv::destroyAllWindows();
system("pause");
return 0;
}
3.28 高斯双边模糊
均值模糊、高斯模糊后,其实在一定程度上损坏了图像的原有信息,比如:轮廓。
而,高斯双边模糊
可以较高程度地保存下来,所以又称之为:边缘滤波
。
下图,如果是中心点刚好在两个颜色的交界处,高斯模糊就会降低两个颜色之间的差异,从而使得界限变得很模糊。
可以,再产生一个卷积核,卷积核的系数根据中心像素对周围像素值的差异来决定,像素值与大,就离这个中心像素点越远,系数就该越小。权重的和 = 色彩空间 + 坐标空间
。
语法:
// 这种滤波方法在平滑图像的同时能够保持边缘细节。它通过考虑空间距离和像素值差异来加权邻域像素的影响,因此比均值滤波和高斯滤波更适合去噪而保持边缘清晰。
int d = 9; // 邻域直径
double sigmaColor = 75.0; // 颜色空间的标准差
double sigmaSpace = 75.0; // 坐标空间的标准差
cv::bilateralFilter(src, dst, d, sigmaColor, sigmaSpace); // 双边滤波
int main()
{
cv::Mat src = cv::imread("C:/Users/15198/Desktop/02.jpg");
cv::Mat dst;
cv::bilateralFilter(src, dst, 0, 100, 10);
cv::namedWindow("原图", WINDOW_FREERATIO);
cv::namedWindow("高斯双边模糊", WINDOW_FREERATIO);
cv::imshow("原图", src);
cv::imshow("高斯双边模糊", dst);
cv::waitKey(0);
cv::destroyAllWindows();
system("pause");
return 0;
}
3.29 应用案例:人脸检测
#include<iostream>
#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main()
{
string pftxt_file_path = "D:/opencv_face_detector.txt";
// TensorRt格式的文件
string pb_file_path = "D:/opencv_face_detector_uint8.pb";
cv::dnn::Net mynet = cv::dnn::readNetFromTensorflow(pb_file_path, pftxt_file_path);
cv::Mat frame;
cv::VideoCapture cap(0);
while (true)
{
cap.read(frame);
cv::imshow("result", frame);
// 图像预处理
cv::Mat blob = cv::dnn::blobFromImage(frame, 1.0, cv::Size(300, 300), cv::Scalar(104, 177, 123));
// 将图片输入进网络
mynet.setInput(blob);
// 前向传播
cv::Mat probs = mynet.forward();
// 1x1xNx7
// 7 第3个值是置信度,后面4个值是左上、右下坐标。
// 初始化1个Nx7的Mat
// 传递 probs 矩阵中数据的指针,ptr<float>() 返回一个指向 probs 中 float 数据的指针。
// 这个指针会被用来初始化 detectMat,使 detectMat 共享 probs 中的内存数据。
cv::Mat detectMat(probs.size[2], probs.size[3], CV_32F, probs.ptr<float>());
for (int h = 0; h < detectMat.rows; h++)
{
float conf = detectMat.at<float>(h, 2);
if (conf > 0.5)
{
float x1 = detectMat.at<float>(h, 3) * frame.cols;
float y1 = detectMat.at<float>(h, 4) * frame.rows;
float x2 = detectMat.at<float>(h, 5) * frame.cols;
float y2 = detectMat.at<float>(h, 6) * frame.rows;
cv::rectangle(frame, cv::Rect(x1, x2, x2 - x1, y2 - y1), cv::Scalar(0, 0, 255));
}
}
cv::imshow("results", frame);
char c = cv::waitKey(1);
if (c == 27)
{
break;
}
}
cv::waitKey(0);
cv::destroyAllWindows();
system("pause");
return 0;
}