QT人工智能篇-opencv

第一章 认识opencv

1. 简单概述

OpenCV是一个跨平台的开源的计算机视觉库,主要用于实时图像处理和计算机视觉应用‌。它提供了丰富的函数和算法,用于图像和视频的采集、处理、分析和显示。OpenCV支持多种编程语言,包括C++、Python、Java等,可以应用于多个领域,如人脸识别、目标检测、图像分割、运动估计等

2. 应用场景

  1. 机器人:用于导航、避障、物体识别等。

  2. 自动驾驶:用于车道检测、行人识别、交通标志识别等。

  3. 医疗影像:用于图像分割、病变检测等。

  4. 安防监控:用于运动检测、人脸识别等。

  5. 增强现实:用于虚拟对象的实时叠加。

  6. 工业检测:用于产品质量检测、缺陷识别等。

  7. 娱乐:用于手势识别、虚拟试衣等。

3. 开发环境搭建

不同版本的 Visual Studio 对 C++ 标准的支持不同,因此需要选择与之兼容的 OpenCV 版本。以下是常见的对应关系:

OpenCV 官方提供了针对不同 Visual Studio 版本的预编译库(Prebuilt Libraries),可以直接下载并使用。以下是常见的对应关系

3.1 下载OpenCV-4.8.0

https://opencv.org/releases/

需要注意的是下载之后的文件后缀是.exe但它其实是一个压缩包

3.2 配置环境变量

将上面两个路径添加到环境变量中,配置成功使用opencv_version命令测试

能直接打印版本信息,说明配置成功

3.3 单个项目配置

新建项目,右键单击项目,选择属性,打开属性配置窗口


除此之外,还有个依赖项,需要配置

说明一下:

  • 建议开始时,选择Release版 ,则添加依赖项的时候就需要选择opencv_world480.lib
  • 另一个是Debug版的

3.4 全局项目配置

打开视图->其他窗口->属性管理器

然后在这个文件中添加下面这段代码:

XML 复制代码
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <ImportGroup Label="PropertySheets" />
  <PropertyGroup Label="UserMacros" />
  <PropertyGroup />
  <ItemDefinitionGroup />
  <ItemGroup />
</Project>

然后重启vs,就会发现已经更新了,并在项目属性中多出了这个东西

说明一下:

  • 接下的操作就是右键文件,点击属性,改变同单个项目配置文件的那三个东西

4. 入门案例

cpp 复制代码
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;

int main()
{
	// 读取图片
	Mat mat = imread("C:\\Users\\46285\\Desktop\\qt_code\\images\\pkq1.png");
	if (mat.empty())
	{
		cout << "图片读取失败" << endl;
		return -1;
	}
	// 创建一个窗口,用来展示图片
	// 窗口大小根据图片自适应
	namedWindow("图片展示", WINDOW_AUTOSIZE);
	// 展示图片到窗口中
	imshow("图片展示", mat);
	// 等待,0表示无限等待,直到按下任意键
	// 如果>0 表示停留N毫秒
	waitKey(0);
	return 1;
}

4.1 imread读取图片

imread功能是加载图像文件成为一个Mat对象:

CV_EXPORTS_W Matimread( const String& filename, intflags = IMREAD_COLOR );

参数说明:

  • 第一个参数为文件路径
  • 第二个参数为标志位 读取文件的格式
    • IMREAD_COLOR(默认):以彩色图像读取**(默认)**,忽略 alpha 通道
    • IMREAD_GRAYSCALE:以灰度图像读取
    • IMREAD_UNCHANGED:保留图像的原始通道数,包括 alpha 通道(如透明 PNG)
    • ....

返回值说明:

  • 返回的是个Mat类型,是一个存储像素的数组
  • 当然如果返回为空,则表示图片读到失败,即有可能是路径错误

4.2 namedWindow创建窗口

namedWindow 是 OpenCV 中用于创建一个窗口的函数

CV_EXPORTS_W void namedWindow(const String& winname, intflags = WINDOW_AUTOSIZE);

参数说明:

  • 第一个参数为窗口的名字
  • 第二个参数为标志位:指定窗口的行为
    • WINDOW_AUTOSIZE**(默认)**:窗口大小自动适应图像,不能手动调整大小
    • WINDOW_NORMAL:允许手动调整窗口大小

4.3 inshow窗口展示

CV_EXPORTS_W voidimshow(const String& winname, InputArraymat);

参数说明:

  • 第一个参数为窗口名字
  • 第二个参数为:显示的图像数据,就是imread读取到的图像

4.4 waitKey等待键盘输入

通常使用这个函数用来长时间显示imshow显示的窗口

CV_EXPORTS_W intwaitKey(intdelay = 0);

参数说明:

  • 当参数为0时:无限等待,直到用户按下任意键
  • > 0:等待指定毫秒数,超时后自动继续程序
  • < 0:和 0 类似,实际效果是无限等待

返回值说明:

  • 返回按键的 ASCII 值或键码(例如 'q' 的值是 113
  • 且如果超时未按键,则返回 -1
cpp 复制代码
void function3(Mat& src)
{
	while (true)
	{
		imshow("图片展示", src);
		// waitKey()等待用户操作
		int c = waitKey(2000);// 等待2秒
		// 27就是esc按键,表明要退出
		if (c == 27)
		{
			cout << c << endl;
			break;
		}
		
	}
}

说明一下:

  • 这段代码捕捉的是esc按键,当esc按下之后,就会退出

4.5 destroyAllWindows

这就是一个销毁所有窗口的函数,使用时 destroyAllWindows(),虽然简单,但是每次还是必须要加上的

5. cvtColor修改图像

cvtColor 是 OpenCV 中用来转换图像颜色空间的函数,比如常见的 BGR → 灰度、BGR → HSV 等。

CV_EXPORTS_W void cvtColor(InputArraysrc, OutputArraydst, intcode, intdstCn = 0);

参数说明:

  • 第一个参数 :通常类型是Mat,就是原始的输入图像

  • 第二个参数 :通常类似是Mat,就是用来接收修改后的图像的

  • 第三个参数 :用于指定颜色转换的类型

    • COLOR_BGR2GRAY 表示为BGR → 灰度图
    • COLOR_BGR2RGB 表示为BGR → RGB
    • COLOR_BGR2HSV 表示为BGR → HSV 色彩空间
    • COLOR_BGR2Lab 表示为BGR → Lab 色彩空间
    • COLOR_GRAY2BGR 表示为灰度图 → BGR(三通道)
    • COLOR_RGB2BGR 表示为RGB → BGR(调换通道顺序)
cpp 复制代码
void function1(Mat& src)
{
	// 处理图像
	Mat temp;
	// 将BGR图像转换为GRAY图像
	cvtColor(src, temp, COLOR_BGR2GRAY);
	namedWindow("灰度", WINDOW_AUTOSIZE);
	imshow("灰度", temp);
}

6. imwrite保存图像

imwrite 是 OpenCV 中用于将图像保存到文件的函数,是图像输出环节的核心函数之一。

CV_EXPORTS_W bool imwrite(const String& filename, InputArrayimg, const std::vector<int>& params = std::vector<int>());

参数说明:

  • 第一个参数:保存的文件路径
  • 第二个参数:为被保存的图像数据
  • 第三个参数(可选):保存时的参数设置,例如压缩质量等
    • PNG IMWRITE_PNG_COMPRESSION 压缩级别,范围 0-9(默认 3)
    • JPEG IMWRITE_JPEG_QUALITY图像质量,范围 0-100(默认 95)
    • WEBP IMWRITE_WEBP_QUALITY 质量参数,范围 1-100
cpp 复制代码
void function2(Mat& src)
{
	// 处理图像
	Mat temp;
	// 将BGR图像转换为GRAY图像
	cvtColor(src, temp, COLOR_BGR2GRAY);
	namedWindow("灰度", WINDOW_AUTOSIZE);
	imshow("灰度", temp);
	
	// 保存图像路径
	imwrite("C:\\Users\\46285\\Desktop\\qt_code\\save.png", temp);
}

第二章 图像基础操作

1. 通道

单通道(灰度图像):0-255,其中0表示黑色,255表示白色,也被称为

三通道(彩色图像) :每个像素由三个数值分别表示蓝色(Blue)绿色(Green) 红色(Red)的强度

其他通道类型 :除了常见的单通道和三通道外,还有四通道和多通道

2. 数据类型

2.1 CV_8U无符号整型

每个像素的值是一个8 位无符号整数,取值范围为 [0, 255]

2.2 CV_32F浮点型

每个像素的值是一个 32 位浮点数,取值范围为 [0.0, 1.0] 或其他浮点范围

3. Mat对象

就是一个多维数组,可以用来表示图像矩阵数据。支持多种数据类型 ,支持多通道数据

Mat包含: 数据头(图像基本信息),数据指针(实际存储像素的内存区域),引用计数

3.1 创建mat对象 && 赋值

cpp 复制代码
void function4(Mat& src)
{
	// 创建一个4*4大小的单通道空白图像
	Mat m1 = Mat::zeros(400, 400, CV_8UC1);

	// 给单通道赋值
	m1 = 255;
	

	// 三通道
	Mat m2 = Mat::zeros(400, 400, CV_8UC3);

	// 给三通道赋值
	m2 = Scalar(0, 0, 255);
	imshow("m2", m2);
}

说明一下:

  • CV_8UC1表示单通道无符号整型
  • CV_8UC3表示三通道无符号整型

3.2 克隆 && 拷贝

Mat对象的拷贝构造赋值重载和C++默认规则都是一样都是浅拷贝

cpp 复制代码
void function5(Mat& src)
{
	Mat m1 = Mat::zeros(400, 400, CV_8UC3);
	Mat m2 = m1;// 拷贝构造是浅拷贝
	Mat m3;
	m3 = m1;// 赋值重载也是浅拷贝

	Mat m4 = m1.clone();// 克隆是深拷贝
	Mat m5;
	m1.copyTo(m5);// 等价于m5 = m1(深拷贝)

}

3.3 其他函数

cv::Mat::rows 返回矩阵的行数。高
cv::Mat::cols 返回矩阵的列数。宽
cv::Mat::channels() 返回矩阵的通道数。
cv::Mat::empty() 检查矩阵是否为空。
cv::Mat::clone() 返回矩阵的深拷贝。
cv::Mat::copyTo() 将矩阵复制到另一个矩阵。
cv::Mat::convertTo() 将矩阵转换为另一种数据类型。
cv::Mat::reshape() 改变矩阵的形状(不改变数据)。

4. 图像像素基本操作

我们可以通过对图像的像素做读写操作,从而修改图像像素的值,改变图像的亮度&对比度

Mat对象本身就是一个保存像素点的二维数组

4.1 图像像素的访问

数组

cpp 复制代码
void function7(Mat& src)
{
	Mat m1 = src.clone();
	int w = m1.cols;
	int h = m1.rows;
	int channel = m1.channels();
	// 通过数组遍历
	for (int row = 0; row < h; row++)
	{
		for (int col = 0; col < w; col++)
		{
			// 单通道 - 灰度图像
			if (channel == 1)
			{
				// 等价于int pv = m1[i][j];
				int pv = m1.at<uchar>(row, col);// 泛型需要传类型
				m1.at<uchar>(row, col) = 255 - pv;
			}
			// 三通道 - 彩色图像
			if (channel == 3)
			{
				Vec3b bgr = m1.at<Vec3b>(row, col);
				m1.at<Vec3b>(row, col)[0] = 255 - bgr[0];
				m1.at<Vec3b>(row, col)[1] = 255 - bgr[1];
				m1.at<Vec3b>(row, col)[2] = 255 - bgr[2];
			}
		}
	}
	imshow("图像修改后", m1);
}

说明一下:

  • 访问单通道Mat数组下标的方法是 对象名.at<类型>(指定行,指定列);
    比如:m1.at<uchar>(row, col)
  • 访问三通道Mat数组下标的方法是 对象名.at<类型>(指定行,指定列)[指定通道];
    比如:<Vec3b >m1.at<Vec3b >(row, col)[0]

指针

cpp 复制代码
// 通过指针遍历
for (int row = 0; row < h; row++)
{
	uchar* current_row = m1.ptr<uchar>(row);
	for (int col = 0; col < w; col++)
	{
		// 单通道 - 灰度图像
		if (channel == 1)
		{
			int pv = *current_row;
			*current_row++ = 255 - pv;
		}
		// 三通道 - 彩色图像
		if (channel == 3)
		{
			*current_row++ = 255 - *current_row;
			*current_row++ = 255 - *current_row;
			*current_row++ = 255 - *current_row;
		}
	}
}
imshow("图像修改后", m1);

4.2 图像像素的加减乘除

自己获取像素的值做计算

cpp 复制代码
void function8(Mat& src)
{
	Mat m1 = src.clone();

	Mat m2 = Mat(m1.size(), m1.type());
	m2 = Scalar(50, 50, 50);

	Mat m3 = Mat(m1.size(), m1.type());
	int w = m1.cols;
	int h = m1.rows;
	int channel = m1.channels();
	// 通过数组遍历
	for (int row = 0; row < h; row++)
	{
		for (int col = 0; col < w; col++)
		{
			Vec3b b1 = m1.at<Vec3b>(row, col);
			Vec3b b2 = m2.at<Vec3b>(row, col);
			// 因为像素的值的范围是0-255,因此做了计算后,如果担心像素的值超过这个范围,就可以使用saturate_cast()函数模板,将值限制在0-255之间
			m3.at<Vec3b>(row, col)[0] = saturate_cast<uchar>(b1[0] - b2[0]);
			m3.at<Vec3b>(row, col)[1] = saturate_cast<uchar>(b1[1] - b2[1]);
			m3.at<Vec3b>(row, col)[2] = saturate_cast<uchar>(b1[2] - b2[2]);
		}
	}
	imshow("加减乘除", m3);
}

说明一下:

  • 由于图像的加减乘除可以会超过[0,255],所以可以加上saturate_cast<uchar>(像素值)

直接使用Mat对象做计算

cpp 复制代码
void function9(Mat& src)
{
	Mat m1 = src.clone();
	Mat dst;
	dst = m1 + Scalar(50, 50, 50);
	imshow("加法", dst);

	dst = m1 - Scalar(50, 50, 50);
	imshow("减法", dst);

	dst = m1 / Scalar(2, 2, 2);
	imshow("除法", dst);

	// opencv不支持这样直接做乘法操作 
	dst = m1 * Scalar(1, 1, 1);
	imshow("乘法", dst);
}

说明一下:

  • 目标图像 = 源图像 + Scalar(像素值,像素值,像素值)
  • 注意: OpenCV不支持 对象名 * Scalar(像素值,像素值,像素值)

使用提供的函数做计算

cpp 复制代码
void function10(Mat& src)
{
	Mat m1 = src.clone();
	Mat m2 = Mat(m1.size(), m1.type());
	m2 = Scalar(50, 50, 50);
	Mat dst;
	// 加法
	add(m1, m2, dst);
	imshow("加法", dst);
	// 减法
	subtract(m1, m2, dst);
	imshow("减法", dst);
	// 乘法
	multiply(m1, Scalar(2, 2, 2), dst);
	imshow("乘法", dst);
	// 除法
	divide(m1, Scalar(2, 2, 2), dst);
	imshow("除法", dst);
}

说明一下:

  • add加法,subtract减法,multiply乘法,divide除法

4.3 图像像素的位运算

与运算 bitwise_and():可以用于提取图像中的特定区域(掩码操作)。等价于&

或运算 bitwise_or():可以用于合并图像或填充区域。等价于|

非运算 bitwise_not():可以用于图像的反色操作。等价于!

异或运算 bitwise_xor():可以用于检测图像差异或生成特殊效果,等价于^

cpp 复制代码
void function11(Mat& src)
{
	Mat m1 = Mat::zeros(Size(255, 255), CV_8UC3);
	Mat m2 = Mat::zeros(Size(255, 255), CV_8UC3);

	rectangle(m1, Point(100, 50), Point(200, 150), Scalar(255, 255, 0), -1, LINE_8);
	rectangle(m2, Point(100, 50), Point(200, 150), Scalar(0, 255, 255), -1, LINE_8);

	imshow("m1", m1);
	imshow("m2", m2);

	Mat dst;
	//bitwise_and(m1, m2, dst);
	//bitwise_or(m1, m2, dst);
	//bitwise_not(m1, dst);
	bitwise_xor(m1, m2, dst);
	imshow("逻辑运算", dst);
}

4.4 addWeighted图像加权融合

addWeighted()是 OpenCV 中用于 图像加权融合 的函数。它可以将两张图像按照指定的权重进行线性组合,生成一张新的图像。这个函数常用于图像混合、透明度调整、图像叠加等场景。

cpp 复制代码
void cv::addWeighted(
    InputArray src1, double alpha,
    InputArray src2, double beta,
    double gamma,
    OutputArray dst,
    int dtype = -1
);

参数说明:

  • src1 :输入的第一张图像(可以是 Mat 或 UMat)。alpha :第一张图像的权重(比例因子)

  • src2 :输入的第二张图像(同样可以是 Mat 或 UMat)。beta :第二张图像的权重(比例因子)

  • gamma :加到最终结果上的一个值(可以理解为整体加亮或者偏移

  • dst:输出图像(结果会存在这里)

  • dtype :可选参数,输出图像的数据类型。默认是 -1,表示和输入图像相同。

src1图像和src2图像大小应该一致,

比例因子 :是0-1之间的浮点

cpp 复制代码
void function12(Mat& src)
{
	Mat m1 = src.clone();
	// 创建一张和m1一样大小的图像,并将图像颜色变成蓝色
	Mat m2 = Mat::zeros(m1.size(), m1.type());
	m2 = Scalar(255, 0, 0);

	// 图像加权融合
	Mat dst;
	addWeighted(m1, 0.5, m2, 0.5, 50, dst);
	imshow("图像加权融合", dst);
}

4.5 applyColorMap伪彩色映射

applyColorMap() 是 OpenCV 中用于 伪彩色映射 的函数。它可以将灰度图像或单通道图像转换为彩色图像,通过应用预定义的颜色映射表(Colormap)来增强图像的视觉效果。伪彩色映射常用于热力图、深度图、医学图像等场景

cpp 复制代码
void cv::applyColorMap(
    InputArray src,
    OutputArray dst,
    int colormap
);

说明一下:

  • src :输入图像,通常是单通道的灰度图CV_8UC1),也可以是已经归一化后的单通道图。

  • dst :输出图像,会是一个彩色的三通道图CV_8UC3

  • colormap :预定义的颜色映射表编号,比如 COLORMAP_JET, COLORMAP_HOT, COLORMAP_COOL,等等。

使用场景:

  • **热力图:**将灰度图像转换为彩色图像,增强视觉效果。
  • **深度图:**将深度信息映射为彩色图像,便于观察。
  • **医学图像:**增强医学图像的对比度,便于诊断。
  • **数据可视化:**将单通道数据(如温度、高度等)映射为彩色图像。
cpp 复制代码
void function13(Mat& src)
{
	Mat m1 = src.clone();
	Mat dst = Mat::zeros(m1.size(), m1.type());
	int i = 0;
	while (true)
	{
		int key = waitKey(500);
		if (key == 27)
		{
			break;
		}
		applyColorMap(m1, dst, i % 21);
		i++;
		imshow("彩色映射", dst);
	}
}

说明一下:

  • 一共有21种 不同的颜色映射,感觉有点像ps的lut

5. 图像通道的分离与操作

在图像处理中,我们可以将一张彩色图像的BGR通道,拆分为多个单通道图像,或者对每个通道进行单独处理,然后再将处理后的通道合并成一张多通道图像

5.1 split通道分离

将多通道图像拆分为多个单通道图像。例如,将 BGR 彩色图像拆分为蓝色(B)、绿色(G)和红色(R)三个单通道图像

cpp 复制代码
void function14(Mat& src)
{
    // 用来存储单个通道的数组
    vector<Mat> channels;
    split(src, channels);
    Mat b = channels[0];
    Mat g = channels[1];
    Mat r = channels[2];
    imshow("b通道", b);
    imshow("g通道", g);
    imshow("r通道", r);
}

5.2 merge通道合并

将处理后的单通道图像重新合并为一张多通道图像

cpp 复制代码
void function14(Mat& src)
{
	// 用来存储单个通道的数组
	vector<Mat> channels;
	split(src, channels);
	Mat b = channels[0];
	Mat g = channels[1];
	Mat r = channels[2];
	//imshow("b通道", b);
	//imshow("g通道", g);
	//imshow("r通道", r);

	channels[2] = 0;    // 屏蔽r通道
	Mat dst = Mat::zeros(src.size(), src.type());
	merge(channels, dst);
	imshow("通道合并", dst);

}

5.3 mixChannel通道混合与重组

交换各个通道的值,比如:将B通道的值交换到G通道

cpp 复制代码
void cv::mixChannels(
    const Mat* src,      // 输入图像数组
    size_t nsrcs,        // 输入图像的数量
    Mat* dst,            // 输出图像数组
    size_t ndsts,        // 输出图像的数量
    const int* fromTo,   // 通道映射关系
    size_t npairs        // 通道映射对的数量
);

参数说明:

  • src:输入图像数组(可以是一个或多个图像)。

  • nsrcs:输入图像的数量。

  • dst:输出图像数组(可以是一个或多个图像)。

  • ndsts:输出图像的数量。

  • fromTo:通道映射关系数组,表示从输入图像的哪个通道复制到输出图像的哪个通道。

  • npairs:通道映射对的数量。

cpp 复制代码
void function14(Mat& src)
{
	Mat dst = Mat::zeros(src.size(), src.type());

	vector<Mat> channels;
	split(src, channels);

	channels[0] = 0;// 屏蔽b通道

	merge(channels, dst);


	int from_to[] = {
		0,1 ,   // 将src的b通道复制到dst的g通道
		1,2 ,   // 将src的g通道复制到dst的r通道
		2,0 };
	mixChannels(&src, 1, &dst, 1, from_to, 3);
	imshow("通道混合", dst);

}

6. 色彩空间转换

6.1 inRange抠图

inRange()是 OpenCV 中用于根据颜色范围提取图像中特定区域的函数。它通常用于颜色过滤阈值化操作,比如提取图像中某种颜色的区域。可以用这个函数来实现类似抠图的操作

满足颜色范围的像素值为 255(白色)不满足颜色范围的像素值为 0(黑色) 可以用它来做一个类似变化背景,抠图这么一个操作

cpp 复制代码
void cv::inRange(
    InputArray src,       // 输入图像(单通道或多通道)
    InputArray lowerb,    // 下限(Scalar 或 Mat)
    InputArray upperb,    // 上限(Scalar 或 Mat)
    OutputArray dst       // 输出二值图像(单通道,CV_8U 类型)
);

参数说明:

  • **src:**输入图像,可以是单通道或多通道图像(例如灰度图或彩色图)。
  • **lowerb:**下限值,表示颜色范围的下限。如果输入是多通道图像,lowerb 应该是一个 Scalar 或 Mat,每个通道对应一个下限值。
  • upperb: 上限值,表示颜色范围的上限。同样,如果输入是多通道图像,upperb 应该是一个 Scalar 或 Mat,每个通道对应一个上限值。
  • dst: 输出图像,是一个二值图像(单通道,CV_8U 类型)。满足条件的像素值为 255,不满足条件的像素值为 0

HSV色彩空间通过色相、饱和度和明度来表示颜色,具有更好的直观性,符合人类对颜色的感知方式。它常用于图像处理中的颜色提取、目标追踪、分割等任务

cpp 复制代码
void function14(Mat& src)
{
	// step1: 将图像转换为HSV
	Mat hsv;
	cvtColor(src, hsv, COLOR_BGR2HSV);

	// step2: 提取图像的指定颜色
	Mat dst;
	inRange(hsv, Scalar(35, 43, 46), Scalar(77, 255, 255), dst);

	// step3: 再将图像去反
	bitwise_not(dst, dst);

	// step4: 设置背景图
	Mat redDst = Mat::zeros(src.size(), src.type());
	redDst = Scalar(40, 40, 200);
	
	// 拷贝原图到redDst
	src.copyTo(redDst, dst);
	imshow("背景改变", redDst);
}

说明一下:

  • 需要使用inRange抠图首先就需要将bgr图像变成HSV图像
  • src.copyTo(redDst, dst);中的第二个参数为mask: 掩码图像,必须是单通道的 8 位图像(CV_8U)。掩码中值为 255 的像素位置会被复制,值为 0 的像素位置不会被复制。

7. 像素统计

7.1 minMaxLoc查找图像中的最小值和最大值及其位置

cpp 复制代码
void cv::minMaxLoc(
    InputArray src,                // 输入图像或矩阵(单通道)
    double* minVal,                // 返回的最小值
    double* maxVal,                // 返回的最大值
    Point* minLoc = nullptr,       // 返回的最小值位置(可选)
    Point* maxLoc = nullptr,       // 返回的最大值位置(可选)
    InputArray mask = noArray()    // 可选的掩码,用于指定计算区域
);

参数说明:

  • src:输入图像或矩阵,必须是单通道(灰度图像)
  • minVal:返回的最小值。
  • maxVal:返回的最大值。
  • minLoc:返回的最小值的位置(cv::Point 类型,可选)。
  • maxLoc:返回的最大值的位置(cv::Point 类型,可选)。
  • mask:可选的掩码,用于指定计算区域。掩码必须是单通道的 8 位图像(CV_8U),且大小与输入图像相同。

7.2 meadStdDev计算图像的均值和标准差

cpp 复制代码
void cv::meanStdDev(
    InputArray src,         // 输入图像或矩阵
    OutputArray mean,       // 输出的均值(每个通道的均值)
    OutputArray stddev,     // 输出的标准差(每个通道的标准差)
    InputArray mask = noArray() // 可选的掩码,用于指定计算区域
);

参数说明:

  • src:输入图像或矩阵,可以是单通道或多通道。

  • mean:输出的均值 ,类型为 cv::Scalar。对于多通道图像,mean 的每个元素对应一个通道的均值。

  • stddev:输出的标准差 ,类型为 cv::Scalar。对于多通道图像,stddev 的每个元素对应一个通道的标准差。

  • mask:可选的掩码,用于指定计算区域。掩码必须是单通道的 8 位图像(CV_8U),且大小与输入图像相同。

cpp 复制代码
void function14(Mat& src)
{
	Mat m1;
	cvtColor(src, m1, COLOR_BGR2GRAY);
	imshow("灰度图像", m1);
	double minVal, maxVal;
	Point minLoc, maxLoc;
	// minMaxLoc()只能用来统计灰度图像的,因此如果是一张彩色图像,需要先将
	// 彩色图像转为灰度图像
	minMaxLoc(m1, &minVal, &maxVal, &minLoc, &maxLoc);
	cout << "图片像素最小值:" << minVal << "   最大值:" << maxVal << endl;
	cout << "最小值在" << minLoc << "    最大值在" << maxLoc << endl;

	// 计算均值和标准差
	Scalar mean, stddev;
	meanStdDev(src, mean, stddev);
	cout << "均值:" << mean[0] << endl;
	cout << "标准差:" << stddev[0] << endl;
}

说明一下:

  • 要进行像素统计,首先这张图片一定要是灰度图像

8. 图像像素转换

8.1 convertTo像素类型转换

将图像的像素数据类型从一种格式转换为另一种(如`uchar`转`float`).Mat对象有提供一个函数convertTo()用于像素类型转换

cpp 复制代码
void cv::Mat::convertTo(
    OutputArray dst,
    int dtype,
    double alpha = 1,
    double beta = 0
) const;

参数说明:

  • dst:输出图像矩阵。

  • dtype:输出图像的数据类型,比如 CV_8U, CV_32F, CV_64F 等等。

  • alpha:可选缩放因子(scale factor)。

  • beta:可选偏移量(shift factor)

cpp 复制代码
void function14(Mat& src)
{
	// 转换为浮点型(CV_32FC3)
	Mat dst_float;
	src.convertTo(dst_float, CV_32FC3, 1.0 / 255.0);
	imshow("float型", dst_float);

	// 转换为双精度型
	Mat dst_double;
	src.convertTo(dst_double, CV_64FC1);
	imshow("双精度型", dst_double);
}

8.2 什么情况下要使用像素类型转换?

  • 它是为了防止计算溢出,比如当图像需要进行数值运算(如乘法、卷积、矩阵运算)时
  • 满足算法输入要求。许多高级算法(如深度学习、滤波、矩阵分解)要求输入为浮点型或特定范围。等等还有很多场景都需要做像素类型转换,来满足一些特定场景

9. 图像像素归一化

9.1 normalize归一化

归一化是指将图像的像素值通过线性或非线性变换映射到特定范围(如 [0,1] 或 [-1,1]),目的是消除数据量纲差异,提升算法鲁棒性。它是图像预处理的核心步骤之一

cpp 复制代码
void cv::normalize(
    InputArray  src,              // 输入图像/矩阵
    OutputArray dst,              // 输出图像/矩阵
    double      alpha = 0,        // 归一化下限(如0.0)
    double      beta = 1,         // 归一化上限(如1.0)
    int         norm_type = NORM_L2,  // 归一化类型
    int         dtype = -1,       // 输出数据类型(默认与src相同)
    InputArray  mask = noArray()  // 可选掩码
);

9.2 为什么需要归一化?

  • 统一数据尺度:避免数值较大的像素主导模型训练(如深度学习)。
  • ​加速收敛:优化算法(如梯度下降)在归一化数据上更快收敛。
  • 适应算法要求:许多算法(如SVM、神经网络)要求输入数据在固定范围内。
  • 增强对比度:例如直方图均衡化就是一种非线性归一化。

9.2 归一化类型

9.3 线性归一化:将像素值线性映射到 [0,1]

cpp 复制代码
void function14(Mat& src)
{
	Mat dst;
	normalize(src, dst, 0.0, 1.0, NORM_MINMAX, CV_32F);
	imshow("归一化", dst);
	for (int i = 0; i < 3; i++)
	{
		for (int j = 0; j < 3; j++)
		{
			cout << (int)src.at<uchar>(i, j) << "\t";
		}
		cout << endl;
	}
	for (int i = 0; i < 3; i++)
	{
		for (int j = 0; j < 3; j++)
		{
			cout << dst.at<float>(i, j) << "\t";
		}
		cout << endl;
	}
}

9.4 均值方差归一化

cpp 复制代码
void function14(Mat& src)
{
	Mat dst;
	src.convertTo(dst, CV_32F);

	// 计算均值和方差
	Scalar mean, stddev;
	meanStdDev(dst, mean, stddev);
	cout << "均值:" << mean[0] << "    方差值:" << stddev << endl;

	// 归一化
	// 将图像的像素值转换为均值为0、标准差为1的标准正态分布
	// 将图像 dst 的每个像素值减去均值 mean[0],使数据分布的中心平移到0。
	// 将平移后的像素值除以标准差 stddev[0],使数据的标准差变为1。
	dst = (dst - mean[0]) / stddev[0];
	meanStdDev(dst, mean, stddev);
	cout << "均值:" << mean[0] << "    方差值:" << stddev << endl;
	imshow("归一化", dst);
}

9.5 归一化到特定范围

比如归一化到-1 到 1

cpp 复制代码
void function14(Mat& src)
{
	Mat dst;
	normalize(src, dst, -1.0, 1.0, NORM_MINMAX, CV_32F);
	imshow("归一化", dst);
	for (int i = 0; i < 3; i++)
	{
		for (int j = 0; j < 3; j++)
		{
			cout << (int)src.at<uchar>(i, j) << "\t";
		}
		cout << endl;
	}
	for (int i = 0; i < 3; i++)
	{
		for (int j = 0; j < 3; j++)
		{
			cout << dst.at<float>(i, j) << "\t";
		}
		cout << endl;
	}
}

第三章 图像几何处理

1. 绘制几何形状的图像

1.1 rectangle绘制矩形

cv::rectangle() 是 OpenCV 中用于在图像上绘制矩形的函数。它可以用来绘制矩形框,常用于目标检测、图像标注、区域标记等任务

cpp 复制代码
void cv::rectangle(
    InputOutputArray img,              // 输入输出图像
    Point pt1,                        // 矩形的一个顶点
    Point pt2,                        // 矩形的对角顶点
    const Scalar& color,              // 矩形的颜色
    int thickness = 1,                // 线条厚度,-1表示的填充该矩形
    int lineType = LINE_8,            // 线条类型
    int shift = 0                     // 点坐标的小数位数
);
cpp 复制代码
void cv::rectangle(
    InputOutputArray img,              // 输入输出图像
    Rect rec,                         // 矩形区域
    const Scalar& color,              // 矩形的颜色
    int thickness = 1,                // 线条厚度
    int lineType = LINE_8,            // 线条类型
    int shift = 0                     // 点坐标的小数位数
);

参数说明:

  • thickness:线条的厚度。如果为负值(如 -1),表示填充矩形。
  • lineType:线条类型,默认为 cv::LINE_8(8-connected line)。其他可选值:cv::LINE_4(4-connected line)或 cv::LINE_AA(抗锯齿线条)。
  • shift:点坐标的小数位数(通常为 0)。
cpp 复制代码
void function14(Mat& src)
{
	// 矩形左上角的点
	Point p1(100, 50);
	// 矩形右下角的点  Point是坐标
	Point p2(250, 250);
	// Point的用法
	//rectangle(src,p1,p2,Scalar(0,0,255),5, LINE_AA);
	rectangle(src, Rect(100, 50, 150, 200), Scalar(0, 255, 0), 5);
	imshow("矩形", src);
}

说明一下:

  • 使用第二个参数画矩形时:Rect(起始点X, 起始点X, 宽, 高)

1.2 circle绘制圆形

cv::circle()是 OpenCV 中用于在图像上绘制圆的函数。它可以用来绘制圆形,常用于目标检测、图像标注、区域标记等任务

cpp 复制代码
void cv::circle(
    InputOutputArray img,              // 输入输出图像
    Point center,                     // 圆心坐标
    int radius,                       // 圆的半径
    const Scalar& color,              // 圆的颜色
    int thickness = 1,                // 线条厚度
    int lineType = LINE_8,            // 线条类型
    int shift = 0                     // 点坐标的小数位数
);
cpp 复制代码
void function14(Mat& src)
{
	Point center(170, 130);
	circle(src, center, 90, Scalar(0, 0, 255), 1);
	imshow("圆形", src);
}

1.3 line绘制线条

cpp 复制代码
void function14(Mat& src)
{
	Point p1(0, 0);
	Point p2(src.cols, src.rows);
	line(src, p1, p2, Scalar(0, 0, 255), 2);
	imshow("线条", src);
}

1.4 ellipse绘制椭圆

cpp 复制代码
void cv::ellipse(
    InputOutputArray img,              // 输入输出图像
    Point center,                     // 椭圆中心
    Size axes,                        // 椭圆的长轴和短轴长度
    double angle,                     // 椭圆的旋转角度(顺时针方向)
    double startAngle,                // 圆弧的起始角度(顺时针方向)
    double endAngle,                  // 圆弧的终止角度(顺时针方向)
    const Scalar& color,              // 椭圆的颜色
    int thickness = 1,                // 线条厚度
    int lineType = LINE_8,            // 线条类型
    int shift = 0                     // 点坐标的小数位数
);
cpp 复制代码
void function14(Mat& src)
{
	// 绘制椭圆
	RotatedRect rrt;
	// 设置椭圆的中心点
	rrt.center = Point(200, 200);
	rrt.size = Size(100, 200);
	rrt.angle = 45;
	ellipse(src, rrt, Scalar(0, 0, 255), 2, LINE_8);
	imshow("图像展示", src);
}

1.5 绘制多边形

方式一: polylines() 绘制多边形 + fillPoly() 填充多边形

cpp 复制代码
void function14(Mat& src)
{
	Point p1(300, 100);
	Point p2(200, 300);
	Point p3(250, 400);
	Point p4(350, 400);
	Point p5(400, 300);
	vector<Point> pts = { p1,p2,p3,p4,p5 };
	// 填充多边形
	// fillPoly(src, pts, Scalar(255, 255, 0), LINE_AA);
	// 绘制多边形 第三个参数,是否闭合多边形
	polylines(src, pts, true, Scalar(0, 0, 255), 2, LINE_AA);
	imshow("图形绘制", src);
}

方法二:drawContours()既绘制又填充

这个函数不是说一次只能绘制一个图形,如果想一次性绘制多个图形,定义多个vector<Point>,统一塞给一个vector<vector<Point>>

cpp 复制代码
void cv::drawContours(
    InputOutputArray image,             // 输入输出图像
    InputArrayOfArrays contours,       // 轮廓集合
    int contourIdx,                    // 要绘制的轮廓索引(-1 表示绘制所有轮廓)
    const Scalar& color,               // 轮廓颜色
    int thickness = 1,                 // 线条厚度
    int lineType = LINE_8,             // 线条类型
    InputArray hierarchy = noArray(),  // 轮廓层级信息
    int maxLevel = INT_MAX,            // 绘制轮廓的最大层级
    Point offset = Point()             // 轮廓坐标的偏移量
);
cpp 复制代码
void function14(Mat& src)
{
	Point p1(300, 100);
	Point p2(200, 300);
	Point p3(250, 400);
	Point p4(350, 400);
	Point p5(400, 300);
	vector<Point> pts = { p1,p2,p3,p4,p5 };
	vector<vector<Point>> contours = { pts };
	// 第三个参数为第几个图形
	drawContours(src, contours, 0, Scalar(255, 255, 0), -1);
	imshow("图形绘制", src);
}

说明一下:

  • drawContours(src, contours, 0, Scalar(255, 255, 0), -1);的第三个参数为几个要绘制的图形

1.6 案例练习-鼠标绘制几何图形

voidonMouse(intevent, intx, inty, intflags, void* userdata)

参数说明:

  • event 事件
  • x y 指的是当前鼠标的坐标
  • flags标志位
  • userdata 传递的参数
cpp 复制代码
Point startP(-1, -1);	// 起始坐标点
Mat temp;				// 临时Mat对象

// step3: 鼠标回调函数
void onMouse(int event, int x, int y, int flags, void* userdata) {
	// 默认规定只能从左往右
	
	//Mat& src = (Mat&)userdata;
	Mat& src = *static_cast<Mat*>(userdata);  // 安全转换
	// 鼠标左键按下,这就是起始点
	if (event == EVENT_LBUTTONDOWN)
	{
		startP.x = x;
		startP.y = y;
	}
	// 鼠标左键抬起,这就是结束点
	else if (event == EVENT_LBUTTONUP)
	{
		int dx = x - startP.x;// 宽
		int dy = y - startP.y;// 高
		// 绘制矩形
		if (dx > 0 || dy > 0)
		{
			Rect rect(startP.x, startP.y, dx, dy);
			// 将选中区域展示在页面上
			imshow("选中区域", src(rect));
			rectangle(src, rect, Scalar(0, 255, 0), 1);
			imshow("鼠标绘制", src);
			// 重置起始点
			startP.x = -1;
			startP.y = -1;
		}
	}
	// 鼠标移动
	else if (event == EVENT_MOUSEMOVE)
	{
		int dx = x - startP.x;
		int dy = y - startP.y;
		if (startP.x > 0 && startP.y > 0)
		{
			// 绘制矩形
			Rect rect(startP.x, startP.y, dx, dy);
			// 如果直接绘制,会将每一次移动的都画出来,所以要在这一次绘制时,擦除之前的
			temp.copyTo(src);// 这个等价于src = temp;(深拷贝)
			rectangle(src, rect, Scalar(0, 255, 0), 1);
			imshow("鼠标绘制", src);
		}
	}
} 

void test(Mat& src)
{
	// step1 创建窗口
	namedWindow("鼠标绘制", WINDOW_AUTOSIZE);
	// step2 将窗口和鼠标事件绑定
	setMouseCallback("鼠标绘制", onMouse, &src);
	imshow("鼠标绘制", src);
	temp = src.clone();
}

说明一下:

  • imshow("选中区域", src(rect)); 将选中区域展示在页面上

2. 图像处理

2.1 resize图像缩放

插值方法,用于计算目标图像的像素值。常用的插值方法有:

  • cv::INTER_NEAREST:最近邻插值(速度快,质量低)。

  • cv::INTER_LINEAR:双线性插值(默认值,速度较快,质量较好)。

  • cv::INTER_CUBIC:双三次插值(速度较慢,质量较高)。

  • cv::INTER_AREA:区域插值(适合缩小图像)。

  • cv::INTER_LANCZOS4:Lanczos 插值(速度最慢,质量最高)

cpp 复制代码
void function1(Mat& src)
{
	Mat zoomin, zoomout;
	int w = src.cols;
	int h = src.rows;
	resize(src, zoomin, Size(w / 2, h / 2), 0, 0);
	resize(src, zoomout, Size(w * 1.5, h * 1.5), 0, 0);

	imshow("缩小", zoomin);
	imshow("放大", zoomout);
}

2.2 flip图像翻转

图像的翻转不是图像的旋转,是沿着x或者y轴,将整张图片类似于照镜子一样的,进行处理

cpp 复制代码
void function1(Mat& src)
{
	Mat dst;
	// = 0: 沿 x 轴(垂直轴)翻转。
	// > 0: 沿 y 轴(水平轴)翻转。 
	// < 0 : 同时沿 x 轴和 y 轴翻转。
	flip(src, dst, -90);
	imshow("图像翻转", dst);
}

说明一下:

  • = 0: 沿 x 轴(垂直轴)翻转。
  • > 0: 沿 y 轴(水平轴)翻转。
  • < 0 : 同时沿 x 轴和 y 轴翻转。

2.3 roi图像裁剪

cpp 复制代码
void function1(Mat& src)
{
	Rect roi(243, 6, 280, 210);

	//裁剪后的图像是原图像的一个子矩阵,修改裁剪后的图像会影响原图像。
	// 如果需要独立的副本,可以使用cv::Mat::clone()方法。
	Mat dst = src(roi).clone();

	imshow("裁剪图像", dst);
	imshow("原图片", src);
}

2.4 warpAffine图像仿射变换

warpAffine() 是 OpenCV 中用于执行仿射变换的函数。仿射变换包括旋转、缩放、平移和剪切等操作

cpp 复制代码
void warpAffine(
    InputArray src, 
    OutputArray dst, 
    InputArray M, 
    Size dsize, 
    int flags = INTER_LINEAR, 
    int borderMode = BORDER_CONSTANT, 
    const Scalar& borderValue = Scalar()
);

参数说明:

  • M: 2x3 的变换矩阵。
  • dsize: 输出图像的大小。
  • flags: 插值方法,默认为 INTER_LINEAR。常见的插值方法有:
    • INTER_NEAREST: 最近邻插值
    • INTER_LINEAR: 双线性插值(默认)
    • INTER_CUBIC: 双三次插值
    • INTER_AREA: 区域插值
  • borderMode: 边界填充模式,默认为 BORDER_CONSTANT。常见的边界模式有:
    • BORDER_CONSTANT: 使用常数值填充边界
    • BORDER_REPLICATE: 复制边界像素
    • BORDER_REFLECT: 反射边界像素
  • borderValue: 当 borderMode 为 BORDER_CONSTANT 时使用的边界填充值,默认为 Scalar(),即黑色。

图像旋转

cpp 复制代码
void function1(Mat& src)
{
	Mat dst, M;
	int w = src.cols;
	int h = src.rows;
	// 矩阵中心点就是原图的中心点
	// 做45度旋转  按照1:1的比例缩放
	M = getRotationMatrix2D(Point(w / 2, h / 2), 45, 1.0);

	// 计算旋转后的图像边界框
	Rect2f bbox = RotatedRect(Point2f(), src.size(), 45).boundingRect2f();

	// 调整平移分量
	M.at<double>(0, 2) += bbox.width / 2.0 - src.cols / 2.0;  // 水平偏移
	M.at<double>(1, 2) += bbox.height / 2.0 - src.rows / 2.0; // 垂直偏移

	// 仿射变换
	warpAffine(src, dst, M, bbox.size(), INTER_LINEAR, BORDER_CONSTANT, Scalar(255, 255, 0));

	imshow("旋转变换", dst);
}

图像平移

cpp 复制代码
void function1(Mat& src)
{
	// 定义平移量
	float tx = 100.0;  // 水平方向平移 100 像素
	float ty = 50.0;   // 垂直方向平移 50 像素

	// 定义平移矩阵
	Mat M = (Mat_<double>(2, 3) << 1, 0, tx, 0, 1, ty);

	// 计算输出图像的大小
	Size dsize(src.cols + tx, src.rows + ty);

	// 应用平移变换
	Mat dst;
	warpAffine(src, dst, M, dsize);

	// 显示结果
	imshow("Translated", dst);
}

2.5 图像透射变换

透射变换(Perspective Transformation) 是一种将图像从一个视角投影到另一个视角的几何变换。它通常用于校正图像的透视畸变,例如将倾斜拍摄的文档图像转换为正面视角,或者将图像投影到另一个平面上。

透射变换是一种非线性变换,它通过一个3x3 的变换矩阵 将图像中的像素映射到新的位置。与仿射变换(Affine Transformation)不同,透射变换可以处理透视效果,因此更适合处理倾斜或变形的图像。

透射变换的数学形式如下:

透射变换的作用包括:

  • 图像校正:将倾斜拍摄的图像(如文档、车牌)转换为正面视角。

  • 虚拟视角生成:将图像投影到另一个平面,生成新的视角。

  • 增强现实:将虚拟物体投影到真实场景中

透射变化实现步骤:

  1. 确定原图像和目标图像的对应点:通常需要手动或自动选择 4 个对应点。

  2. 计算透射变换矩阵 :使用 cv::getPerspectiveTransform函数。

  3. 应用透射变换 :使用 cv::warpPerspective函数。

cpp 复制代码
void function1(Mat& src)
{
	// 原图像的点
	Point2f p1(102, 9);
	Point2f p2(91, 348);
	Point2f p3(503, 382);
	Point2f p4(492, 4);
	vector<Point2f> vps = { p1,p2,p3,p4 };

	// 目标图像的点
	float w = cv::norm(vps[1] - vps[0]);  // 计算宽度
	float h = cv::norm(vps[2] - vps[0]); // 计算高度

	// 变换后的四个点
	Point2f pf1(10, 10);
	Point2f pf2(50, h - 30);
	Point2f pf3(w - 50, h - 70);
	Point2f pf4(w - 40, 50);
	vector<Point2f> vp2 = { pf1,pf2,pf3,pf4 };

	// 计算透射变换矩阵
	Mat M = getPerspectiveTransform(vps, vp2);

	// 应用透射变换
	Mat dst;
	warpPerspective(src, dst, M, src.size());

	imshow("透射变换", dst);
}

第四章 图像灰度化和二值化

1. 灰度化

灰度化是将彩色图像转换为灰度图像的过程,将RGB三个通道的信息合并为一个亮度通道,每个像素值范围为0(黑)到255(白)

cpp 复制代码
void function1(Mat& src)
{
	Mat dst = Mat::zeros(src.size(), src.type());
	cvtColor(src, dst, COLOR_BGR2GRAY);
	imshow("灰度图像", dst);
}

2. threshold二值化

二值化是将灰度图像转换为只有黑白两种颜色的图像,通过设定阈值将像素分为前景(255)和背景(0)

cpp 复制代码
固定阈值:
cv::Mat binaryImage;
cv::threshold(grayImage, binaryImage, 128, 255, cv::THRESH_BINARY);

自适应阈值:
cv::Mat adaptiveBinary;
cv::adaptiveThreshold(grayImage, adaptiveBinary, 255, 
                     cv::ADAPTIVE_THRESH_GAUSSIAN_C,
                     cv::THRESH_BINARY, 11, 2);

Otsu大津算法(自动确定最佳阈值)
                     cv::Mat otsuBinary;
cv::threshold(grayImage, otsuBinary, 0, 255, 
             cv::THRESH_BINARY | cv::THRESH_OTSU);

使用案例

cpp 复制代码
void function1(Mat& src)
{
	Mat dst = Mat::zeros(src.size(), src.type());
	cvtColor(src, dst, COLOR_BGR2GRAY);
	imshow("灰度图像", dst);

	// 二值化-固定阈值
	Mat dst1;
	threshold(dst, dst1, 128, 255, THRESH_BINARY);
	//imshow("二值化", dst1);
	Mat dst2;
	threshold(dst, dst2, 128, 255, THRESH_BINARY_INV);
	//imshow("反二值化", dst2);

	// 自适应阈值:
	Mat dst3;
	threshold(dst, dst3, 0, 255, THRESH_BINARY | THRESH_OTSU);
	imshow("自适应阈值", dst3);
}

第五章 形态学

1. 连通性

连通性(Connectivity)是指判断图像中像素之间是否相互连接的一种规则,常用于二值图像的分析(如查找连通区域、轮廓检测、形态学操作等)。它定义了像素之间的邻接关系,直接影响算法的结果。

1.1 邻接关系

在图像中,最⼩的单位是像素,每个像素周围有8个邻接像素,常⻅的邻接关系有3种:4邻接D邻接,8邻接和,分别如下图所示:

说明一下:

  • 4邻接:像素p(x,y)的4邻域是:(x+1,y);(x-1,y);(x,y+1);(x,y-1),⽤N4(p)表示像素p的4邻接
  • D邻接:像素p(x,y)的D邻域是:对⻆上的点 (x+1,y+1);(x+1,y-1);(x-1,y+1);(x-1,y-1),⽤ND(p)表示像素p的D邻域
  • 8邻接:像素p(x,y)的8邻域是: 4邻域的点 + D邻域的点,⽤N8(p)表示像素p的8邻域

1.2 连通性

连通性决定了哪些相邻像素被视为"连通的"(属于同一区域),连通性是描述区域和边界的重要概念,opencv的连通性规则有:

  • 4连通(4-Connectivity):只考虑像素的上下左右4个直接相邻的像素(水平和垂直方向)。
  • 8连通(8-Connectivity):考虑像素的所有8个邻居(包括对角线方向)。

M连通(Mixed Connectivity,混合连通性) 是介于 4连通(4-Connectivity) 和 8连通(8-Connectivity) 之间的一种折中策略,旨在解决8连通可能导致的过度连接问题(如斜向像素误判为同一物体),同时避免4连通对某些合理连接的漏判。

具体规则如下: ​

  • 如果两个像素是水平或垂直相邻(4连通方向):直接视为连通。
  • 如果两个像素是斜向相邻(对角线方向):只有当它们的至少一个共同4连通邻居属于同一区域时,才视为连通。

2. 形态学操作

形态学转换是基于图像形状的⼀些简单操作。它通常在⼆进制图像上执⾏。腐蚀和膨胀是两个基本的形态学运算符。然后它的变体形式还包括开运算,闭运算,礼帽⿊帽等

2.1 erode腐蚀

腐蚀和膨胀是最基本的形态学操作,腐蚀和膨胀都是针对⽩⾊部分(⾼亮部分)⽽⾔的。

腐蚀的作⽤是消除物体边界点,可以消除⼩于结构元素的噪声点

原理

腐蚀就是⽤⼀个结构元素扫描图像中的每⼀个像素,⽤结构元素中的每⼀个像素与其覆盖的像素做"与"操作,如果都为1,则该像素为1,否则为0。如下所示:

cpp 复制代码
void cv::erode(
    InputArray  src,         // 输入图像
    OutputArray dst,         // 输出图像
    InputArray  kernel,      // 结构元素
    Point       anchor = Point(-1,-1),  // 锚点
    int         iterations = 1,  // 腐蚀次数
    int         borderType = BORDER_CONSTANT,
    const Scalar& borderValue = morphologyDefaultBorderValue()
);

案例

cpp 复制代码
void function1(Mat& src)
{
	// 创建一个5*5的矩形
	Mat mask = getStructuringElement(MORPH_RECT, Size(5, 5));
	Mat dst;
	// 将矩形和原图做腐蚀操作
	erode(src, dst, mask);
	imshow("腐蚀图像", dst);
}

2.2 dilate膨胀

膨胀就是使图像中**⾼亮部分(白色区域)扩张**,效果图拥有⽐原图更⼤的⾼亮区域(白色区域);膨胀是求局部最⼤值的操作,腐蚀是求局部最⼩值的操作。

膨胀的作⽤是将与物体接触的所有背景点合并到物体中,可添补⽬标中的孔洞

原理

⽤⼀个结构元素扫描图像中的每⼀个像素,⽤结构元素中的每⼀个像素与其覆盖的像素做"或"操作,如果都为0,则该像素为0,否则为1。如下图所示,结构A被结构B膨胀后

cpp 复制代码
void function1(Mat& src)
{
	// 创建一个5*5的矩形
	Mat mask = getStructuringElement(MORPH_RECT, Size(5, 5));
	Mat dst;
	// 将矩形和原图做膨胀操作
	dilate(src, dst, mask);
	imshow("膨胀图像", dst);
}

2.3 开闭运算

开运算和闭运算是将腐蚀和膨胀按照⼀定的次序进⾏处理。 但这两者不可逆的,即先开后闭并不能得到原来的图像。

开运算是先腐蚀后膨胀,

  • 作⽤是:分离物体,消除⼩区域。对"白色噪声"有效,但会牺牲物体连通性(可能断开细长部分)。
  • 特点:可以消除小噪声或孤立点。平滑物体边缘,断开狭窄的连接。保留原始物体的大致形状。
  • 应用场景:去除指纹图像中的细小噪声。

闭运算与开运算相反,是先膨胀后腐蚀,

  • 作⽤是消除/"闭合"物体⾥⾯的孔洞,对"黑色噪声"(如孔洞、裂缝)的修复能力更强,因为先膨胀能直接填充它们。
  • 特点:可以填充小孔或裂缝,连接邻近的物体,平滑物体轮廓。
  • 应用场景:填充医学图像中的血管断裂。修复文档中的笔画缺失。
cpp 复制代码
void cv::morphologyEx(
    InputArray  src,               // 输入图像(二值或灰度图)
    OutputArray dst,              // 输出图像
    int         op,               // 形态学操作类型(如MORPH_OPEN)
    InputArray  kernel,           // 结构元素(核)
    Point       anchor = Point(-1,-1),  // 锚点(默认中心)
    int         iterations = 1,   // 操作次数
    int         borderType = BORDER_CONSTANT,  // 边界处理方式
    const Scalar& borderValue = morphologyDefaultBorderValue()  // 边界填充值
);
cpp 复制代码
void test1(Mat& src)
{
	Mat dstOpen;
	Mat mask = getStructuringElement(MORPH_RECT, Size(5, 5));
	morphologyEx(src, dstOpen, MORPH_OPEN, mask);
	imshow("开运算", dstOpen);
}

void test2(Mat& src)
{
	Mat dstClose;
	Mat mask = getStructuringElement(MORPH_RECT, Size(5, 5));
	morphologyEx(src, dstClose, MORPH_CLOSE, mask);
	imshow("闭运算", dstClose);
}

2.4 如何选择开闭运算

2.5 礼帽黑帽运算

礼帽运算

礼帽运算:原图像与"运算"的结果图之差

输入:一张带有白色噪声的黑色背景图像。 ​

输出:仅保留噪声和亮细节(原背景变全黑)

cpp 复制代码
void function1(Mat& src)
{
	Mat gray;
	cvtColor(src, gray, COLOR_BGR2GRAY);
	imshow("灰度图像", gray);
	Mat dstOpen;
	Mat mask = getStructuringElement(MORPH_RECT, Size(5, 5));
	morphologyEx(gray, dstOpen, MORPH_OPEN, mask);

	Mat dstTopHat;
	morphologyEx(gray, dstTopHat, MORPH_TOPHAT, dstOpen);
	imshow("礼帽运算", dstTopHat);
}

黑帽运算

黑帽运算:为"运算"的结果图与原图像之

输入:一张带有黑色小孔的白色物体图像

输出:仅保留孔洞和暗细节(原物体变全白)

第六章 图像噪声和平滑

1. 图像噪声

1.1 椒盐噪声

特征 说明
表现形式 随机分布的**白色(255)黑色(0)**像素点
成因 传感器故障、传输错误、存储介质损坏
影响 破坏图像局部细节,干扰边缘检测、目标识别等任务
典型场景 老式扫描仪、受干扰的无线传输图像、低质量JPEG压缩

如下图所示:

生成椒盐噪声

cpp 复制代码
// 添加椒盐噪声
void addSaltPepperNoise(Mat& image, double noise_ratio = 0.05) {
	Mat dst;
	cvtColor(image, dst, COLOR_BGR2GRAY);
	int num_noise = static_cast<int>(dst.total() * noise_ratio);
	for (int i = 0; i < num_noise; i++) {
		int x = rand() % dst.cols;
		int y = rand() % dst.rows;
		// 随机选择盐(白)或椒(黑)
		dst.at<uchar>(y, x) = (rand() % 2) ? 255 : 0;
	}
	imshow("椒盐噪声", dst);
}

1.2 高斯噪声

在图像上呈现为 细密的颗粒状干扰,类似电视雪花屏效果

高斯噪声效果如图:

给图片生成高斯噪声

cpp 复制代码
void addGaussianNoise(Mat& image, double mean = 0, double stddev = 30) {
	Mat noise = Mat(image.size(), CV_32F);
	randn(noise, mean, stddev);  // 生成高斯噪声矩阵

	if (image.type() == CV_8UC1) {  // 灰度图
		Mat temp;
		image.convertTo(temp, CV_32F);
		temp += noise;
		temp.convertTo(image, CV_8U);
	}
	else if (image.type() == CV_8UC3) {  // 彩色图
		Mat channels[3];
		split(image, channels);
		for (int i = 0; i < 3; i++) {
			channels[i].convertTo(channels[i], CV_32F);
			channels[i] += noise;
			channels[i].convertTo(channels[i], CV_8U);
		}
		merge(channels, 3, image);
	}
	imshow("高斯噪声", image);
}

说明一下:

  • 噪声除了椒盐噪声和高斯噪声外,还有泊松噪声均匀噪声量化噪声等多种噪声

2. 图像平滑

图像平滑的作用:

  • 抑制噪声:消除高斯噪声、椒盐噪声等随机干扰。
  • ​模糊细节:弱化边缘或纹理,用于预处理(如边缘检测前的降噪)。
  • 图像增强:平滑光照不均或背景波动。

常见的图像平滑的处理方式有:

  • 均值滤波
  • 高斯滤波
  • 中值滤波
  • 双边滤波
  • 非局部均值去噪

2.1 均值滤波

均值滤波: 可以抑制高斯噪声等噪声,但是副作用也比较明显,就是边缘和细节也会模糊

cpp 复制代码
void cv::blur(
    InputArray  src,              // 输入图像(支持单通道或多通道)
    OutputArray dst,              // 输出图像(与src尺寸和类型相同)
    Size        ksize,            // 滤波核大小(如Size(3,3))
    Point       anchor = Point(-1,-1),  // 锚点(默认核中心)
    int         borderType = BORDER_DEFAULT  // 边界填充方式
); 
cpp 复制代码
void test(Mat& src)
{
	Mat dst;
	blur(src, dst, Size(3, 3));
	imshow("均值滤波", dst);
}

2.2 高斯滤波

高斯滤波的用途

  • 图像降噪:消除图像中的高频噪声
  • 预处理:在边缘检测等操作前减少图像中的细节
  • 模糊效果:创建艺术效果或保护隐私时模糊部分图像
  • 尺度空间表示:在特征检测(如SIFT)中构建图像金字塔

高斯滤波的大概实现原理:

  • 准备滤镜模板:先制作一个高斯核(比如5×5的网格,中心数字大,边缘数字小)

  • 滑动窗口操作:把这个小网格放在图像的每一个像素点上,就像用这个"加权放大镜"看图像的每一个小区域

  • 计算新像素值:把网格覆盖的像素值乘以对应的网格中的数字(权重)把所有乘积加起来,然后除以权重的总和,这个结果就是当前位置新的像素值

  • 整体效果:因为中心权重高,周围权重低,所以相当于"保留大致模样,但把细节磨平"就像近视眼摘掉眼镜看东西 大致形状还在,但边缘变柔和了

cpp 复制代码
void GaussianBlur(
    InputArray src, 
    OutputArray dst, 
    Size ksize, 
    double sigmaX, 
    double sigmaY = 0, 
    int borderType = BORDER_DEFAULT
)

说明一下:

  • ksize选择
    • 3×3:轻微模糊
    • 5×5:中等模糊
    • 7×7及以上:强模糊效果
    • 必须为正奇数(如3,5,7...)
  • X方向标准差:控制水平方向的模糊程度
  • Y方向标准差:控制垂直方向的模糊程度(通常设为与X相同)
  • 当sigma=0时,OpenCV会根据ksize自动计算sigma,sigma越大,模糊效果越明显
    • 典型值:0.5-2.0(轻度模糊),2.0-5.0(明显模糊)
    • 根据ksize和sigma生成二维高斯核
cpp 复制代码
void test(Mat& src)
{
	Mat dst = Mat::zeros(src.size(), src.type());
	GaussianBlur(src, dst, Size(3, 3), 0.0);
	imshow("3*3的高斯模糊", dst);
	Mat dst5 = Mat::zeros(src.size(), src.type());
	GaussianBlur(src, dst5, Size(5, 5), 0.0);
	imshow("5*5的高斯模糊", dst5);
	Mat dst7 = Mat::zeros(src.size(), src.type());
	GaussianBlur(src, dst7, Size(7, 7), 0.0);
	imshow("7*7的高斯模糊", dst7);
}

2.3 中值滤波

中值滤波的特点:

  • 有效去除椒盐噪声(图像上的黑白噪点)

  • 保护边缘信息(不像均值滤波会使边缘模糊)

  • 计算相对耗时(因为需要排序操作)

中值滤波的实现原理:

  • 定义一个滤波窗口(通常是3×3、5×5等奇数尺寸的正方形)

  • 窗口遍历图像,在每个位置:收集窗口内所有像素值,将这些值按大小排序,取排序后的中间值作为当前像素的新值

  • 边界处理:通常通过复制边缘像素或镜像等方式处理

cpp 复制代码
void medianBlur(InputArray src, OutputArray dst, int ksize); 
cpp 复制代码
void test(Mat& src)
{
	Mat dst;
	medianBlur(src, dst, 3);
	imshow("3*3的中值滤波", dst);

	Mat dst5;
	medianBlur(src, dst5, 5);
	imshow("5*5的中值滤波", dst5);
}

2.4 图像平滑总结

方法 优点 缺点 适用场景
均值滤波 计算快 边缘模糊 实时性要求高的简单去噪
高斯滤波 边缘保留较好 对椒盐噪声无效 通用降噪(如高斯噪声)
中值滤波 消除脉冲噪声 对高斯噪声效果一般 椒盐噪声、医学影像
双边滤波 边缘清晰 计算慢 人像美化、细节保留
非局部均值 去噪效果最佳 极耗资源 高精度静态图像处理

3. 直方图

统计每个区间内像素的数量并绘制成柱状图。其中横轴:像素值(如0-255)纵轴:该像素值出现的频率(像素数量)

直方图的作用:

  • 图像分析:一眼看出图像是偏亮(右侧高)、偏暗(左侧高)还是对比度低(集中在中间)
  • 图像增强:通过直方图均衡化改善图像对比度
  • 目标检测:用于颜色匹配、背景建模等高级应用

术语说明:

  • Bin(区间):将0-255的强度范围划分的区间(如每10个强度值一个bin)

  • 频数(bins):每个bin中像素的数量

  • 范围(range):通常为0-255(8位图像)

cpp 复制代码
void calcHist(
    const Mat* images, 
    int nimages, 
    const int* channels, 
    InputArray mask, 
    OutputArray hist, 
    int dims, 
    const int* histSize, 
    const float** ranges, 
    bool uniform = true, 
    bool accumulate = false
);

参数说明:

  • images:输入图像数组(指针),可以是单幅或多幅图像
  • nimages:输入图像的数量
  • channels:需要计算直方图的通道列表(数组)
  • mask:可选的操作掩码,指定要计算直方图的图像区域
  • hist:输出的直方图
  • dims:直方图的维度(通常为1)
  • histSize:每个维度上直方图的大小(bin的数量)
  • ranges:每个维度上像素值的范围
  • uniform:直方图是否均匀(默认true)
  • accumulate:是否累积计算结果(默认false)

3.1 灰度图像

cpp 复制代码
void test(Mat& src)
{
    // 将图片转为灰度图像
    Mat gray;
    cvtColor(src, gray, COLOR_BGR2GRAY);
    imshow("灰度图像", gray);

    // 计算直方图
    Mat hist;
    int histSize = 256;
    float range[] = { 0,256 };
    const float* histRange = { range };
    calcHist(&gray, 1, 0, Mat(), hist, 1, &histSize, &histRange);

    // 绘制直方图
    // 定义图像的高和宽
    int w = 512, h = 400;
    // 定义每一个bin的宽度  cvRound()做四舍五入的
    int bin_w = cvRound((double)w / histSize);
    Mat dst = Mat(Size(w, h), CV_8UC3, Scalar(0, 0, 0));

    // 归一化
    normalize(hist, hist, 0, dst.rows, NORM_MINMAX, -1, Mat());

    // 绘制
    for (int i = 1; i < histSize; i++) {
        line(dst,
            Point(bin_w * (i - 1), h - cvRound(hist.at<float>(i - 1))),
            Point(bin_w * (i), h - cvRound(hist.at<float>(i))),
            Scalar(255, 0, 0), 2, 8, 0);
    }

    // 显示
    imshow("灰度直方图", dst);
}

补充一下:

  • 绘制直方图的时候一定要做归一化,为了解决数值范围不匹配的问题,同时也为了可视化的友好性
  • 除了这种方法:ploat::**plotHist()**绘制-了解

3.2 彩色直方图

彩色图像的直方图需要分别计算每个颜色通道(通常是B、G、R)的像素分布。与灰度直方图不同,彩色直方图可以:单独显示各通道分布;展示通道间的颜色关系;通过三维直方图表现颜色空间分布

cpp 复制代码
#include <vector>
void test(Mat& src)
{
    //1 分离彩色图像的通道
    vector<Mat> channels;
    split(src, channels);

    //2 设置直方图参数
    int histSize = 256;
    float range[] = { 0,256 };
    const float* histRange = { range };

    //3 分别计算各通道的直方图
    Mat b_hist, g_hist, r_hist;
    calcHist(&channels[0], 1, 0, Mat(), b_hist, 1, &histSize, &histRange);
    calcHist(&channels[1], 1, 0, Mat(), g_hist, 1, &histSize, &histRange);
    calcHist(&channels[2], 1, 0, Mat(), r_hist, 1, &histSize, &histRange);

    //4 绘制直方图
    int w = 512, h = 400;
    int bin_w = cvRound((double)w / histSize);
    Mat dst = Mat(Size(w, h), CV_8UC3, Scalar(0, 0, 0));

    //5 将三个通道都进行归一化
    normalize(b_hist, b_hist, 0, dst.rows, NORM_MINMAX);
    normalize(g_hist, g_hist, 0, dst.rows, NORM_MINMAX);
    normalize(r_hist, r_hist, 0, dst.rows, NORM_MINMAX);

    //6 绘制各通道直方图
    for (int i = 1; i < histSize; i++) {
        // 蓝色通道
        line(dst,
            Point(bin_w * (i - 1), h - cvRound(b_hist.at<float>(i - 1))),
            Point(bin_w * i, h - cvRound(b_hist.at<float>(i))),
            Scalar(255, 0, 0), 2);
        // 绿色通道
        line(dst,
            Point(bin_w * (i - 1), h - cvRound(g_hist.at<float>(i - 1))),
            Point(bin_w * i, h - cvRound(g_hist.at<float>(i))),
            Scalar(0, 255, 0), 2);
        // 红色通道
        line(dst,
            Point(bin_w * (i - 1), h - cvRound(r_hist.at<float>(i - 1))),
            Point(bin_w * i, h - cvRound(r_hist.at<float>(i))),
            Scalar(0, 0, 255), 2);
    }
    imshow("彩色直方图", dst);
}

3.4 掩膜的应用

在 OpenCV 中,掩膜(Mask)是一种用于限定图像处理区域的二进制矩阵,它通过选择性地屏蔽或保留像素来控制操作的范围

掩膜的作用: 提取感兴趣区域:⽤预先制作的感兴趣区掩模与待处理图像进⾏"与"操作,得到感兴趣区图像,感兴趣区内图像值保持不变,⽽区外图像值都为0

cpp 复制代码
void fun3(Mat& src)
{
    //1 创建掩膜
    Mat mask = Mat::zeros(src.size(), CV_8UC1);
    rectangle(mask, Rect(35, 14, 196, 183), Scalar(255), FILLED); // (x,y,width,height)

    //2. 应用掩膜
    Mat masked_img;
    bitwise_and(src, src, masked_img, mask);
    imshow("掩膜", masked_img);

    //3 只统计掩膜部分的直方图
    //1 分离彩色图像的通道
    vector<Mat> channels;
    split(src, channels);

    //2 设置直方图参数
    int histSize = 256;
    float range[] = { 0,256 };
    const float* histRange = { range };

    //3 分别计算各通道的直方图-只计算指定区域
    Mat b_hist, g_hist, r_hist;
    calcHist(&channels[0], 1, 0, mask, b_hist, 1, &histSize, &histRange);
    calcHist(&channels[1], 1, 0, mask, g_hist, 1, &histSize, &histRange);
    calcHist(&channels[2], 1, 0, mask, r_hist, 1, &histSize, &histRange);

    //4 绘制直方图
    int w = 512, h = 400;
    int bin_w = cvRound((double)w / histSize);
    Mat dst = Mat(Size(w, h), CV_8UC3, Scalar(0, 0, 0));

    //5 将三个通道都进行归一化
    normalize(b_hist, b_hist, 0, dst.rows, NORM_MINMAX);
    normalize(g_hist, g_hist, 0, dst.rows, NORM_MINMAX);
    normalize(r_hist, r_hist, 0, dst.rows, NORM_MINMAX);

    //6 绘制各通道直方图
    for (int i = 1; i < histSize; i++) {
        // 蓝色通道
        line(dst,
            Point(bin_w * (i - 1), h - cvRound(b_hist.at<float>(i - 1))),
            Point(bin_w * i, h - cvRound(b_hist.at<float>(i))),
            Scalar(255, 0, 0), 2);
        // 绿色通道
        line(dst,
            Point(bin_w * (i - 1), h - cvRound(g_hist.at<float>(i - 1))),
            Point(bin_w * i, h - cvRound(g_hist.at<float>(i))),
            Scalar(0, 255, 0), 2);
        // 红色通道
        line(dst,
            Point(bin_w * (i - 1), h - cvRound(r_hist.at<float>(i - 1))),
            Point(bin_w * i, h - cvRound(r_hist.at<float>(i))),
            Scalar(0, 0, 255), 2);
    }
    imshow("掩膜直方图", dst);
}

3.4 直方图均衡化

直方图均衡化(Histogram Equalization)是一种用于增强图像对比度的图像处理技术,通过重新分配像素强度值来扩展图像的动态范围。

直方图均衡化的应用场景:

场景 作用
医学影像 增强病灶区域的可见性
卫星图像 改善地表特征识别
人脸识别 提升光照不变性
老旧照片修复 恢复褪色区域的细节

c++ opencv中可以使用equalizeHist()实现直方图均衡化

处理灰度图像:

cpp 复制代码
void test(Mat& src)
{
	Mat gray;
	cvtColor(src, gray, COLOR_BGR2GRAY);
	imshow("灰度图像", gray);
	Mat dst;
	equalizeHist(gray, dst);
	imshow("直方图均衡化", dst);
}

处理彩色图像

cpp 复制代码
Mat color_img = imread("color.jpg");
vector<Mat> channels;
split(color_img, channels);

// 仅对亮度通道处理(YCrCb/YUV色彩空间)
Mat ycrcb;
cvtColor(color_img, ycrcb, COLOR_BGR2YCrCb);
split(ycrcb, channels);
equalizeHist(channels[0], channels[0]); // 处理Y通道
merge(channels, ycrcb);
cvtColor(ycrcb, dst, COLOR_YCrCb2BGR);

3.5 自适应的直方图均衡化

传统的直方图均衡化方法存在一些局限性,特别是在处理局部对比度较低的图像时表现不佳。因此我们需要使用自适应直方图均衡化

灰度图像做一个自适应直方图均衡化

cpp 复制代码
void test(Mat& src)
{
	Mat image;
	cvtColor(src, image, COLOR_BGR2GRAY);
	Ptr<CLAHE> clahe = createCLAHE();
	Mat dst;
	clahe->apply(image, dst);
	imshow("自适应直方图均衡化", dst);
}

3.6 直方图比较

直方图比较(Histogram Comparison)是一种用于衡量两幅图像的直方图相似性的方法

在OpenCV中,提供了专门的函数 cv::compareHist() 来实现直方图比较。该函数支持多种比较方法,每种方法都基于不同的数学公式来计算两个直方图之间的相似性或差异

cv::compareHist() 支持以下几种比较方法:

  • 相关性(cv::HISTCMP_CORREL)衡量两个直方图的线性相关性。值范围:[-1, 1],值越大表示越相似。
  • 卡方距离(cv::HISTCMP_CHISQR)衡量两个直方图的卡方距离。值范围:[0, ∞),值越小表示越相似。
  • 交集(cv::HISTCMP_INTERSECT)计算两个直方图的交集面积。值范围:[0, 1],值越大表示越相似。
  • 巴氏距离(cv::HISTCMP_BHATTACHARYYA)衡量两个直方图的概率分布之间的距离。值范围:[0, 1],值越小表示越相似。
cpp 复制代码
void test(Mat& src1,Mat& src2)
{
    //1 设置直方图参数
    int histSize = 256;
    float range[] = { 0,256 };
    const float* histRange = { range };

    //2 计算直方图
    Mat hist1, hist2;
    calcHist(&src1, 1, 0, Mat(), hist1, 1, &histSize, &histRange);
    calcHist(&src2, 1, 0, Mat(), hist2, 1, &histSize, &histRange);

    //3 归一化直方图
    normalize(hist1, hist1);
    normalize(hist2, hist2);

    //4 直方图比较
    double c1 = compareHist(hist1, hist2, HISTCMP_CORREL);
    double c2 = compareHist(hist1, hist2, HISTCMP_CHISQR);
    double c3 = compareHist(hist1, hist2, HISTCMP_INTERSECT);
    double c4 = compareHist(hist1, hist2, HISTCMP_BHATTACHARYYA);

    cout << "Correlation: " << c1 << " (越大越相似)" << endl;
    cout << "Chi-Square: " << c2 << " (越小越相似)" << endl;
    cout << "Intersection: " << c3 << " (越大越相似)" << endl;
    cout << "Bhattacharyya: " << c4 << " (越小越相似)" << endl;
}

4. 边缘检测

边缘检测(Edge Detection)是图像处理和计算机视觉中的一项基本任务,用于识别图像中亮度、颜色或纹理发生显著变化的区域

4.1 Sobel 算子

Sobel算子是一种离散微分算子,用于图像处理中的边缘检测

cpp 复制代码
void Sobel(
    InputArray src,         // 输入图像
    OutputArray dst,        // 输出图像
    int ddepth,             // 输出图像深度
    int dx,                 // x方向导数阶数
    int dy,                 // y方向导数阶数
    int ksize = 3,          // 核大小
    double scale = 1,       // 缩放因子
    double delta = 0,       // 偏移量
    int borderType = BORDER_DEFAULT  // 边界处理方式
)
cpp 复制代码
void test(Mat& src)
{
	//1 将图像转换为灰度图像
	Mat gray;
	cvtColor(src, gray, COLOR_BGR2GRAY);

	//2 计算x和y方向的梯度
	Mat grad_x, grad_y;
	Sobel(gray, grad_x, CV_16S, 1, 0, 3);
	Sobel(gray, grad_y, CV_16S, 1, 0, 3);

	//3 将图像转为8位无符号整型
	// 数据类型转换(16S → 8U) 取绝对值(处理负梯度值) 比例缩放(自适应归一化)
	Mat abs_grad_x, abs_grad_y;
	convertScaleAbs(grad_x, abs_grad_x);
	convertScaleAbs(grad_y, abs_grad_y);

	//4 合并梯度
	Mat dst;
	addWeighted(abs_grad_x, 0.5, abs_grad_y, 0.5, 0, dst);

	//5 展示结果
	imshow("sobe边缘检测", dst);
}

如果将上述ksize的值改为-1,就是使用Scharr进行边缘检测,它的精度更高

  • Sobel(gray, grad_x, CV_16S,1, 0,-1);
  • Sobel(gray, grad_y, CV_16S, 1, 0, -1);

4.2 Laplacian 算子

Laplacian 算子是一种基于二阶导数的边缘检测算子,用于检测图像中亮度或颜色变化剧烈的区域(即边缘)

在opencv中,我们可以通过Laplacian()实现Laplacian边缘检测

cpp 复制代码
void Laplacian(
    InputArray src,         // 输入图像
    OutputArray dst,        // 输出图像
    int ddepth,             // 输出图像深度(推荐CV_16S)
    int ksize = 1,          // 核大小(1/3/5/7)
    double scale = 1,       // 缩放因子
    double delta = 0,       // 偏移量
    int borderType = BORDER_DEFAULT  // 边界处理方式
)

不带高斯模糊的Laplacian边缘检测

cpp 复制代码
void test(Mat& src)
{
    //1 转为灰度图像
    Mat gray;
    cvtColor(src,gray,COLOR_BGR2GRAY);

    //2 使用Laplacian边缘检测
    Mat dst;
    Laplacian(src, dst, CV_16S,3);

    //3 转换绝对值-将16位转为8位无符号整数
    Mat abs_dst;
    convertScaleAbs(dst, abs_dst);

    //4 展示图像
    imshow("Laplacian边缘检测",abs_dst);
}

带高斯模糊的Laplacian边缘检测

cpp 复制代码
void test(Mat& src)
{
	//1 转为灰度图像
	Mat gray;
	cvtColor(src, gray, COLOR_BGR2GRAY);

	// 高斯模糊-高斯滤波
	Mat blurDst;
	GaussianBlur(gray, blurDst, Size(3, 3), 0);

	//2 使用Laplacian边缘检测
	Mat dst;
	Laplacian(blurDst, dst, CV_16S, 3);

	//3 转换绝对值
	Mat abs_dst;
	convertScaleAbs(dst, abs_dst);

	//4 展示图像
	imshow("Laplacian边缘检测", abs_dst);
}

说明一下:

  • Laplacian算子特别适合需要检测细微特征各向同性边缘的场景,但通常需要配合高斯模糊等预处理来获得更好的效果

4.3 Canny 边缘检测

Canny 边缘检测是一种多阶段的边缘检测算法,由 John F. Canny 于 1986 年提出。它被认为是最优的边缘检测方法之一,能够在保证高精度的同时减少噪声的影响。Canny 算法的目标是找到图像中亮度变化剧烈的区域,并生成清晰、连续且单一像素宽的边缘

cpp 复制代码
void Canny(
    InputArray image,                // 输入图像
    OutputArray edges,               // 输出边缘图
    double threshold1,               // 低阈值
    double threshold2,               // 高阈值
    int apertureSize = 3,            // Sobel核大小(3/5/7)
    bool L2gradient = false          // 梯度计算方式
)

参数说明:

  • threshold1:低阈值,建议取高阈值的1/2~1/3
  • threshold2:高阈值,通常设为低阈值的2~3倍
  • apertureSize:Sobel核尺寸(3/5/7)
  • L2gradient
    • false:使用L1范数(|Gx|+|Gy|)
    • true:使用L2范数(√(Gx²+Gy²)),更精确但计算量更大

基本使用

cpp 复制代码
void test(Mat& src)
{
	//1 转为灰度图像
	Mat gray;
	cvtColor(src, gray, COLOR_BGR2GRAY);

	//2 使用Canny()做边缘检测
	Mat dst;
	Canny(gray, dst, 50, 150);

	//3 展示
	imshow("canny边缘检测", dst);
}

自定义阈值 + 高斯模糊

cpp 复制代码
// 计算图像中值
double calculateMedian(cv::Mat image) {
    // 将图像数据转换为一维数组
    std::vector<uchar> pixelValues;
    pixelValues.assign((uchar*)image.data, (uchar*)image.data + image.total());

    // 排序像素值
    std::sort(pixelValues.begin(), pixelValues.end());

    // 计算中值
    size_t size = pixelValues.size();
    if (size % 2 == 0) {
        return (pixelValues[size / 2 - 1] + pixelValues[size / 2]) / 2.0;
    }
    else {
        return pixelValues[size / 2];
    }
}
void test(Mat& src)
{
    //1 转为灰度图像
    Mat gray;
    cvtColor(src, gray, COLOR_BGR2GRAY);

    //2 高斯模糊
    Mat blur;
    GaussianBlur(gray, blur, Size(3, 3), 0.0);

    //3 自定义阈值
    // 计算图像的中值
    double middle = calculateMedian(blur);
    double lower = max(0.0, (1 - 0.3) * middle);	// 0.7*中值 = 最低阈值
    double upper = min(255.0, (1 + 0.3) * middle);	// 1.3*中值 = 最高阈值

    //4 Canny()边缘检测
    Mat dst;
    Canny(blur,dst,lower,upper);

    //5 展示
    imshow("canny边缘检测",dst);

}

5. 边缘检测算法比较

第七章 图像进阶处理

1. 模板匹配

模板匹配(Template Matching)是一种在大图像中查找小模板图像位置的技术

应用场景:

  • a. 目标检测(如寻找图标、数字、物体)
  • b. 工业检测(如零件定位)
  • c. 视频监控(运动物体跟踪)

原理

模板匹配的核心是通过滑动窗口的方式,在目标图像中逐像素地比较模板图像局部区域的相似度。具体实现步骤如下:

1. 输入

  • 目标图像:需要搜索的大图。

  • 模板图像:需要查找的小图

  1. 相似度计算
  • 模板图像在目标图像上滑动,逐个位置计算模板与目标图像局部区域的相似度。

  • 常见的相似度度量方法包括:

    • 平方差匹配 (TM_SQDIFF):计算模板与目标区域的平方差,值越小表示匹配度越高。

    • 归一化平方差匹配 (TM_SQDIFF_NORMED):对平方差进行归一化处理。

    • 相关匹配 (TM_CCORR):计算模板与目标区域的相关值,值越大表示匹配度越高。

    • 归一化相关匹配 (TM_CCORR_NORMED):对相关值进行归一化处理。

    • 相关系数匹配 (TM_CCOEFF):计算模板与目标区域的相关系数,值越大表示匹配度越高。

    • 归一化相关系数匹配 (TM_CCOEFF_NORMED):对相关系数进行归一化处理。

3.输出:

  • 返回一个结果矩阵,矩阵中每个元素表示模板在对应位置的匹配度。
  • 通过寻找结果矩阵中的最大值或最小值(取决于所选方法),可以确定模板的最佳匹配位置。

1.1 matchTemplate模板匹配

cpp 复制代码
void matchTemplate(
    InputArray image,         // 目标图像(搜索区域)
    InputArray templ,         // 模板图像(要匹配的小图像)
    OutputArray result,       // 匹配结果矩阵(存放相似度)
    int method,               // 匹配方法/相关度计算方法(如 TM_CCOEFF_NORMED)
    InputArray mask = noArray() // 可选掩码(仅对某些方法有效)
);

result:

  • 尺寸计算:
  • 宽度:image.cols - templ.cols + 1
  • 高度:image.rows - templ.rows + 1
  • 数据类型:32位浮点型(CV_32FC1)

**mask:**可选掩码(仅对 TM_SQDIFF 和 TM_CCORR_NORMED 有效),需与 templ 同尺寸。

OpenCV 提供 6 种匹配方法:

展示案例

cpp 复制代码
void test(Mat& targetImage,Mat& templateImage)
{
	//1 创建结果矩阵
	/*宽度:image.cols - templ.cols + 1
	高度:image.rows - templ.rows + 1
	数据类型:32位浮点型(CV_32FC1)*/
	int cols = targetImage.cols - templateImage.cols + 1;
	int rows = targetImage.rows - templateImage.rows + 1;
	Mat result = Mat::zeros(Size(cols, rows), CV_32FC1);


	//2 使用模板匹配
	matchTemplate(targetImage, templateImage, result, TM_CCOEFF_NORMED);

	//3 获取最佳匹配位置
	double minVal, maxVal;
	Point minPoint, maxPoint;
	// 在矩阵中查找最小值和最大值及其对应的位置
	minMaxLoc(result, &minVal, &maxVal, &minPoint, &maxPoint);

	//4 在图像上绘制矩形,标记结果
	rectangle(targetImage,
		maxPoint,
		Point(maxPoint.x + templateImage.cols, maxPoint.y + templateImage.rows),
		Scalar(0, 255, 0),2);

	//5 显示结果
	imshow("模板图像", templateImage);
	imshow("模板匹配", targetImage);
}

2. 轮廓检测

轮廓检测(Contour Detection)是计算机视觉中用于提取图像中物体边界的技术。它通过分析像素的连通性,找到物体的连续边缘点集合

2.1 findContours()提取轮廓 && drawContours()绘制轮廓

cpp 复制代码
void findContours(
    InputArray image,                // 输入二值图像(8-bit 单通道)
    OutputArrayOfArrays contours,     // 输出的轮廓点集
    OutputArray hierarchy,            // 轮廓的层级关系(可选)
    int mode,                        // 轮廓检索模式
    int method,                      // 轮廓近似方法
    Point offset = Point()            // 轮廓点的偏移量(可选)
);

contours:输出的轮廓集合,每个轮廓存储为 vector<Point>,所以轮廓是vector<vector<Point>>
hieraychy:

  • 每个轮廓对应一个 cv::Vec4i,包含以下四个值:
  • hierarchy[i][0]:当前轮廓的下一个同级轮廓索引(如果不存在,则为 -1)。
  • hierarchy[i][1]:当前轮廓的上一个同级轮廓索引(如果不存在,则为 -1)。
  • hierarchy[i][2]:当前轮廓的第一个子轮廓索引(如果不存在,则为 -1)。
  • hierarchy[i][3]:当前轮廓的父轮廓索引(如果不存在,则为 -1)

mode:

  • cv::RETR_EXTERNAL:仅提取最外层轮廓。
  • cv::RETR_LIST:提取所有轮廓,但不建立层次关系。
  • cv::RETR_CCOMP:提取所有轮廓,并将它们分为两层(外部轮廓和内部轮廓)。
  • cv::RETR_TREE:提取所有轮廓,并建立完整的层次关系。

method:

  • cv::CHAIN_APPROX_NONE:保存所有轮廓点,不做任何压缩。
  • cv::CHAIN_APPROX_SIMPLE:压缩水平、垂直和对角线段,仅保留端点。
  • cv::CHAIN_APPROX_TC89_L1 和 cv::CHAIN_APPROX_TC89_KCOS:使用 Teh-Chin 链近似算法。
  • offset:轮廓点的偏移量(默认 Point(0,0))
cpp 复制代码
void drawContours(
    InputOutputArray image,            // 目标图像(绘制在此图上)
    InputArrayOfArrays contours,       // 轮廓点集(来自 `findContours()`)
    int contourIdx,                    // 要绘制的轮廓索引(-1 表示所有)
    const Scalar& color,               // 轮廓颜色
    int thickness = 1,                 // 线宽(-1 表示填充轮廓)
    int lineType = LINE_8,             // 线型(如 `LINE_AA` 抗锯齿)
    InputArray hierarchy = noArray(),  // 层级关系(可选)
    int maxLevel = INT_MAX,            // 最大绘制层级(默认全部)
    Point offset = Point()             // 轮廓点偏移(可选)
);

演示案例

输入图像必须二值化 ,对小物体,可先使用形态学操作(如膨胀/腐蚀)去除噪声。 对复杂场景,建议先边缘检测(如Canny)再轮廓查找

3. 图像分割

图像分割是计算机视觉中的一项基本任务,其目标是将图像划分为多个区域或部分,使得每个区域具有某种特定的属性(如颜色、纹理、形状等),或者属于同一个对象。简单来说,图像分割就是将图像中的像素分组为有意义的区域。将图像划分为语义或实例级区域,每个像素属于唯一类别(如"人""车""天空")

3.1 阈值分割

​ 阈值分割是一种基于像素灰度值颜色值的图像分割方法。它的基本思想是通过设定一个或多个阈值,将图像中的像素分为不同的类别(例如前景和背景)。每个像素根据其灰度值或颜色值与阈值的关系,被分配到某个类别

全局阈值法&固定阈值分割

cpp 复制代码
double cv::threshold(
    InputArray src,      // 输入图像,必须是单通道灰度图像
    OutputArray dst,     // 输出图像,与输入图像大小和类型相同
    double thresh,       // 阈值
    double maxval,       // 当使用 THRESH_BINARY 或 THRESH_BINARY_INV 时的最大值
    int type             // 阈值类型
);

type: 阈值处理的类型,可以是以下几种之一:

  • THRESH_BINARY: 二值化阈值处理。如果像素值大于阈值,则设为 maxval;否则设为0。
  • THRESH_BINARY_INV: 反二值化阈值处理。如果像素值大于阈值,则设为0;否则设为 maxval。
  • THRESH_TRUNC: 截断阈值处理。如果像素值大于阈值,则设为阈值;否则保持原样。
  • THRESH_TOZERO:零阈值处理 将小于阈值的像素设置为0。如果像素值大于阈值,则保持不变;否则设为0。

THRESH_TOZERO_INV : 反向零阈值处理。如果像素值大于阈值,则设为0;否则保持不变。

特殊类型:
THRESH_OTSU : 使用 Otsu 算法自动选择最佳阈值,并结合 THRESH_BINARY 或THRESH_BINARY_INV 使用。
THRESH_TRIANGLE: 使用三角算法自动选择最佳阈值,并结合 THRESH_BINARY 或 THRESH_BINARY_INV 使用。

返回值:返回的是计算得到的最佳阈值(仅当使用 THRESH_OTSU 或 THRESH_TRIANGLE 时有效),对于其他类型的阈值处理,返回的值等于 thresh。

演示案例

cpp 复制代码
void test(Mat& src)
{
	//1 将图像转为灰度图像
	Mat gray;
	cvtColor(src, gray, COLOR_BGR2GRAY);

	//2 固定阈值处理  threshold
	vector<Mat> dsts;
	Mat dst1,dst2,dst3,dst4,dst5;
	// 二值化阈值
	threshold(gray, dst1, 127, 255, THRESH_BINARY);
	dsts.push_back(dst1);
	// 反二值化阈值
	threshold(gray, dst2, 127, 255, THRESH_BINARY_INV);
	dsts.push_back(dst2);
	// 截断阈值处理
	threshold(gray, dst3, 127, 255, THRESH_TRUNC);
	dsts.push_back(dst3);
	// 零阈值处理
	threshold(gray, dst4, 127, 255, THRESH_TOZERO);
	dsts.push_back(dst4);
	// 反向零阈值处理
	threshold(gray, dst5, 127, 255, THRESH_TOZERO_INV);
	dsts.push_back(dst5);

	// 展示
	string titles[5] = {"二值化阈值","反二值化阈值","截断阈值","零阈值","反向零阈值"};
	for (int i = 0;i < 5;i++)
	{
		imshow(titles[i], dsts.at(i));
	}
}

自动阈值分割 - Otsu算法

Otsu算法(大津算法)是一种自动确定图像二值化阈值的方法

cpp 复制代码
void test(Mat& src)
{
	Mat gray;
	cvtColor(src,gray,COLOR_BGR2GRAY);

	Mat dst;
	//THRESH_BINARY | THRESH_OTSU做位或(or)操作 既要执行二值化,又要用Otsu算法自动确定阈值
	threshold(gray, dst, 0, 255, THRESH_BINARY | THRESH_OTSU);

	imshow("otsu算法", dst);
}

自适应阈值分割

⾃适应阈值分割也叫做局部阈值化,它是根据像素的邻域块的像素值分布来确定该像素位置上的阈值

cpp 复制代码
void adaptiveThreshold(
    InputArray src,     // 输入图像(必须为8位单通道)
    OutputArray dst,    // 输出图像
    double maxValue,    // 满足条件的像素赋予的值(通常255)
    int adaptiveMethod, // 自适应方法:ADAPTIVE_THRESH_MEAN_C 或 ADAPTIVE_THRESH_GAUSSIAN_C
    int thresholdType,  // 阈值类型:THRESH_BINARY 或 THRESH_BINARY_INV
    int blockSize,      // 邻域大小(奇数,如3,5,7,...)
    double C           // 从均值/加权和中减去的常数
);

blockSize

  • 定义用于计算局部阈值的邻域大小,必须是奇数(如 3、5、7 等)。
  • 较大的邻域会平滑更多的噪声,但可能会丢失细节。

C:从计算出的局部阈值中减去的常数值。如果 C 太大,可能会导致分割效果过于保守;如果 C 太小,可能会引入噪声。

cv::ADAPTIVE_THRESH_MEAN_C :使用邻域内像素的平均值作为局部阈值。

cv::ADAPTIVE_THRESH_GAUSSIAN_C:使用邻域内像素的高斯加权平均值作为局部阈值。

cv::THRESH_BINARY:如果像素值大于局部阈值,则设为最大值(如 255);否则设为 0。

演示案列

cpp 复制代码
void test(Mat& src)
{
	//1 转为灰度图像
	Mat gray;
	cvtColor(src, gray, COLOR_BGR2GRAY);

	Mat dst1, dst2;
	//2 使用均值法做自适应阈值分割
	adaptiveThreshold(gray, dst1, 255, ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY, 5, 2);

	//3 使用高斯法做自适应阈值分割
	adaptiveThreshold(gray, dst2, 255, ADAPTIVE_THRESH_GAUSSIAN_C, THRESH_BINARY, 5, 2);

	//4 展示
	imshow("均值法", dst1);
	imshow("高斯法", dst2);
}

3.2 边缘分割

边缘分割是一种图像处理技术,其核心思想是通过检测图像中的边缘(即灰度值发生显著变化的区域)来实现对图像的分割

边缘分割的实现步骤:

  • 边缘检测:使用边缘检测算子(如 Sobel、Canny 等)提取图像中的边缘信息。
  • 边缘连接:由于噪声或其他因素,检测到的边缘可能不连续,需要通过一定的算法(如霍夫变换、形态学操作等)将断开的边缘连接起来。
  • 区域划分:根据检测到的边缘,将图像划分为不同的区域。每个区域内部具有相似的特征,而区域之间存在明显的差异。
  • 后处理:对分割结果进行优化,例如去除小的噪声区域或填充空洞。

以下是使用Canny算法实现边缘分割:

cpp 复制代码
void test(Mat& src)
{
	//1 转为灰度图像
	Mat gray;
	cvtColor(src, gray, COLOR_BGR2GRAY);

	//2 高斯模糊
	Mat blur;
	GaussianBlur(gray, blur, Size(3, 3), 1.5);
	//3 canny()边缘检测
	Mat edges;
	Canny(blur, edges, 50, 150);
	imshow("canny边缘检测", edges);

	//4 用边缘检测结果做掩码,分割图像
	Mat dst = Mat::zeros(src.size(), src.type());
	src.copyTo(dst, edges);	// 只保留边缘部分
	imshow("边缘分割", dst);
}

3.3 分水岭算法

分水岭算法是一种基于形态学区域生长的图像分割方法,它把图像看作地形表面,将灰度值视为海拔高度,通过模拟"洪水"从低洼处逐渐淹没整个地形的过程,找到不同区域之间的边界。是通过模拟"水漫过程"来实现图像分割

distanceTransform()函数用于计算二值图像中每个像素到最近的零像素(背景)的距离,并生成距离图

cpp 复制代码
void cv::distanceTransform(
    InputArray src,        // 输入图像(8位单通道二值图像,非零像素视为前景)
    OutputArray dst,       // 输出距离图(32位浮点型或8位无符号整型)
    OutputArray labels,    // 可选:输出标签图(用于DIST_LABEL_*类型)
    int distanceType,      // 距离类型(DIST_L1、DIST_L2、DIST_C等)
    int maskSize,          // 掩模大小(3、5或DIST_MASK_PRECISE)
    int labelType = DIST_LABEL_CCOMP // 标签类型(仅当需要labels时使用)
);

connectedComponents()函数用于对二值图像中的连通区域(Connected Components)进行标记和统计

cpp 复制代码
int cv::connectedComponents(
    InputArray image,          // 输入图像(8位单通道二值图像)
    OutputArray labels,        // 输出标签图(每个像素的连通区域ID)
    int connectivity = 8,      // 连通性(4或8邻域)
    int ltype = CV_32S         // 标签图数据类型(通常为CV_32S)
);
labels(输出标签图)
存储每个像素所属的连通区域ID(从0开始)。
标签规则:
0:背景区域。
1, 2, 3,...:不同的前景连通区域。
connectivity:
选择建议:
需严格区分水平和垂直连接时用4(如文本字符分割)。
需更宽松的连通性时用8(如自然场景中的目标检测)。

演示案例

cpp 复制代码
void test(Mat& src)
{
	//1 转为灰度图像,做模糊处理(可选)
	Mat gray;
	cvtColor(src, gray, COLOR_BGR2GRAY);

	//2 二值化(用otsu算法做自动阈值处理)
	Mat binary;
	threshold(gray, binary, 0, 255, THRESH_BINARY_INV | THRESH_OTSU);

	//3 形态学去除噪声
	Mat mark = getStructuringElement(MORPH_RECT, Size(5, 5));
	morphologyEx(binary, binary, MORPH_OPEN, mark, Point(-1, -1), 2);

	//4 确定背景区域(远离对象的区域) - 膨胀
	Mat screBg;
	dilate(binary, screBg, mark, Point(-1, -1), 3);

	//5 确定前景区域(位置变换+阈值处理)
	Mat distTransform;
	distanceTransform(binary, distTransform, cv::DIST_L2, 5);
	Mat sureForeground;
	threshold(distTransform, sureForeground, 0.7 * distTransform.at<float>(cv::Point(0, 0)), 255, cv::THRESH_BINARY);

	//6 获取未知区域
	sureForeground.convertTo(sureForeground, CV_8U);
	Mat unkown;
	subtract(screBg,sureForeground , unkown);

	//7 创建标记图像(核心图像)其中背景为1  未知区域为0
	Mat marks;
	connectedComponents(sureForeground, marks);
	marks = marks + 1;	// 背景标记为1
	marks.setTo(0, unkown);	// 未知区域标记为0

	//8 应用分水岭算法
	watershed(src, marks);

	//9 可视化分割结果
	Mat markerImage = Mat::zeros(marks.size(), CV_8UC3);
	for (int i = 0; i < marks.rows; ++i) {
		for (int j = 0; j < marks.cols; ++j) {
			int index = marks.at<int>(i, j);
			if (index == -1) {
				markerImage.at<cv::Vec3b>(i, j) = cv::Vec3b(255, 0, 0); // 分水岭线
			}
			else {
				markerImage.at<cv::Vec3b>(i, j) = cv::Vec3b(index * 10 % 255, index * 20 % 255, index * 30 % 255);
			}
		}
	}

	//10 展示分割结果
	imshow("分水岭分割", markerImage);
}

4. 图像特征提取

4.1 cornerHarris角点检测

cpp 复制代码
void cv::cornerHarris(
    InputArray src,   // 输入图像,必须是单通道、8位或浮点型图像。
    OutputArray dst,  // 输出图像,通常是一个与输入图像大小相同的灰度图像(32位浮点)。
    int blockSize,    // 计算导数自相关矩阵时使用的邻域大小。
    int ksize,        // 使用Sobel算子计算偏导数时的孔径大小。
    double k,         // Harris检测器自由参数,用于响应函数的计算。
    int borderType = BORDER_DEFAULT // 边界填充类型。
);
blockSize:计算特征值时考虑的邻域窗口大小(典型值:2-5)
ksize:Sobel算子的孔径大小,用于计算图像的导数(梯度)。常见的取值为3,5等。
k: Harris角点检测器的自由参数,用来调整响应函数的灵敏度。通常取值范围在0.04到0.06之间。
borderType: 边界处理的方式,默认为 BORDER_DEFAULT。其他选项如 BORDER_CONSTANT, BORDER_REPLICATE 等可根据需要选择。
cpp 复制代码
void test(Mat& src)
{
	//1 转换为灰度图像
	Mat gray;
	cvtColor(src, gray, COLOR_BGR2GRAY);

	//2 设置参数
	int blockSize = 2;
	int ksize = 3;
	double k = 0.04;

	//3 计算harris响应
	Mat dst = Mat::zeros(src.size(), CV_32FC1);
	cornerHarris(gray, dst, blockSize, ksize, k);

	//4 归一化
	Mat dst_norm;
	normalize(dst, dst_norm, 0, 255,NORM_MINMAX,CV_32FC1);

	//5 绘制角点
	Mat result = src.clone();
	for (int i = 0; i < dst_norm.rows; i++) {
		for (int j = 0; j < dst_norm.cols; j++) {
			if ((int)dst_norm.at<float>(i, j) > 150) {	// 阈值筛选
				// 绘制红色圆圈标记角点
				circle(result, Point(j, i), 5, Scalar(0, 0, 255), 2);
			}
		}
	}

	imshow("harris角点检测", result);
}

4.2 goodFeaturesToTrack角点检测

cpp 复制代码
void cv::goodFeaturesToTrack(
    InputArray image,           // 输入图像,必须是单通道灰度图像。
    OutputArray corners,        // 输出角点数组。
    int maxCorners,             // 最大角点数量。
    double qualityLevel,        // 质量水平(阈值乘数)。
    double minDistance,         // 角点之间的最小距离。
    InputArray mask = noArray(),// 可选掩码,指定感兴趣区域。
    int blockSize = 3,          // 计算导数自相关矩阵时使用的邻域大小。
    bool useHarrisDetector = false, // 是否使用 Harris 检测器。
    double k = 0.04             // Harris 检测器自由参数(仅当 useHarrisDetector 为 true 时有效)。
);
image:输入图像(必须为单通道)	灰度图
corners:存储检测到的角点(vector<Point2f>
maxCorners:	返回的角点最大数量(≤0表示无限制)	100-500
qualityLevel:角点质量阈值(与最大响应值的比例)	0.01-0.1
minDistance	:角点间最小像素距离	10-20
mask:可选ROI掩码(指定检测区域)	二值图
blockSize:	计算特征值的邻域大小	3-7
useHarrisDetector:是否使用Harris算法(默认false用Shi-Tomasi)	true/false
k:Harris算法的k值(仅useHarris=true时有效)	0.04-0.06

演示案例

cpp 复制代码
void test(Mat& src)
{
	//1 转换为灰度图像
	Mat gray;
	cvtColor(src, gray, COLOR_BGR2GRAY);

	//2 检测角点
	// 2.1 设置角点检测的参数
	vector<Point2f> corners;
	int maxCorners = 100;
	double qualityLevel = 0.01;
	double minDistance = 10;
	goodFeaturesToTrack(gray, corners, maxCorners, qualityLevel, minDistance, Mat());

	//3 绘制角点
	for (Point2f cor : corners)
	{
		circle(src, cor, 5, Scalar(0, 0, 255), 1);
	}
	imshow("Shi-Tomasi角点检测", src);
}

4.3 FastFeatureDetector特征检测

cpp 复制代码
void test(Mat& src)
{
	//1 转为灰度图像
	Mat gray;
	cvtColor(src, gray, COLOR_BGR2GRAY);

	//2 创建Fast特征检测器
	auto fast = FastFeatureDetector::create();

	//3 检测特征点
	vector<KeyPoint> points;
	fast->detect(src, points);

	//4 绘制特征点
	Mat dst;
	drawKeypoints(src, points, dst, Scalar(0, 0, 255));

	//5 显示结果
	imshow("fast特征点", dst);
}

4.4 ORB算法

cpp 复制代码
void test(Mat& src1,Mat& src2)
{
	//1 转为灰度图像
	Mat img1;
	cvtColor(src1, img1, COLOR_BGR2GRAY);
	Mat img2;
	cvtColor(src2, img2, COLOR_BGR2GRAY);

	//2 创建ORB检测器
	auto orb = ORB::create();

	//3 检测特征点和描述子
	vector<KeyPoint> point1, point2;
	Mat des1, des2;
	orb->detectAndCompute(img1, Mat(),point1,des1);
	orb->detectAndCompute(img2, Mat(),point2,des2);

	//4 匹配特征点
	// 创建 BFMatcher(暴力匹配器)
	cv::BFMatcher matcher(cv::NORM_HAMMING);
	std::vector<cv::DMatch> matches;
	matcher.match(des1, des2, matches);
	double max_dist = 0, min_dist = 100;
	for (const auto& match : matches) {
		double dist = match.distance;
		if (dist < min_dist) min_dist = dist;
		if (dist > max_dist) max_dist = dist;
	}
	std::vector<cv::DMatch> good_matches;
	for (const auto& match : matches) {
		if (match.distance <= std::max(2 * min_dist, 30.0)) {
			good_matches.push_back(match);
		}
	}

	//5 绘制匹配结果
	Mat dst;
	drawMatches(img1, point1, img2, point2, good_matches, dst);

	imshow("orb结果", dst);
}

说明一下:

  • cv::BFMatcher:暴力匹配器,适用于小规模特征点匹配。

  • cv::FlannBasedMatcher:基于 FLANN 的近似最近邻匹配器,适用于大规模特征点匹配。

4.5 LBP算法

cpp 复制代码
// 计算 LBP 特征
Mat computeLBP(Mat& src) {
	// 创建一个与输入图像大小相同的矩阵,用于存储 LBP 结果
	Mat dst = Mat::zeros(src.rows, src.cols, CV_8UC1);

	// 遍历图像的每个像素(忽略边界像素)
	for (int i = 1; i < src.rows - 1; i++) {
		for (int j = 1; j < src.cols - 1; j++) {
			// 获取中心像素值
			uchar center = src.at<uchar>(i, j);

			// 计算 3x3 邻域的 LBP 码
			uchar code = 0;
			code |= (src.at<uchar>(i - 1, j - 1) > center) << 7; // 左上角
			code |= (src.at<uchar>(i - 1, j) > center) << 6;     // 正上方
			code |= (src.at<uchar>(i - 1, j + 1) > center) << 5; // 右上角
			code |= (src.at<uchar>(i, j + 1) > center) << 4;     // 右侧
			code |= (src.at<uchar>(i + 1, j + 1) > center) << 3; // 右下角
			code |= (src.at<uchar>(i + 1, j) > center) << 2;     // 正下方
			code |= (src.at<uchar>(i + 1, j - 1) > center) << 1; // 左下角
			code |= (src.at<uchar>(i, j - 1) > center) << 0;     // 左侧

			// 将 LBP 码存储到目标图像中
			dst.at<uchar>(i, j) = code;
		}
	}

	return dst;
}

void test(Mat& src)
{
	Mat dst = computeLBP(src);

	// 归一化 LBP 图像到 [0, 255] 范围
	Mat normalizedDst;
	normalize(dst, normalizedDst, 0, 255, NORM_MINMAX, CV_8UC1);

	imshow("LBP结果", normalizedDst);
	imwrite("C:/Users/Administrator/Desktop/images/test.png", dst);
}

2.6 HOG算法

cpp 复制代码
void test(Mat& src)
{
	//1 转为灰度图像
	Mat gray;
	cvtColor(src, gray, COLOR_BGR2GRAY);

	//2 创建HOG描述符对象
	HOGDescriptor hog;
	hog.setSVMDetector(HOGDescriptor::getDefaultPeopleDetector());	// 使用默认的行人检测器

	//3 检测行人
	vector<Rect> detections;
	// 每个检测框的置信度,置信度越高,说明该检测框越可能是真正的目标(例如行人)。置信度低的结果可能对应于误检或背景区域。
	vector<double> weights;
	//hog.detectMultiScale(gray, detections, weights);
	hog.detectMultiScale(gray, detections, weights, 
		0, Size(8, 8), Size(16, 16), 1.03, 0.7, true);

	vector<Rect> filteredDetections;
	nonMaximumSuppression(detections, weights, filteredDetections, 0.3);

	//4 绘制检测结果
	for (int i = 0;i < filteredDetections.size();i++)
	{
		Rect detec = filteredDetections[i];
		rectangle(src, detec, Scalar(0, 255, 0));
	}

	imshow("行人检测", src);
}

5. 综合练习-车牌识别

5.1 车牌区域检测

cpp 复制代码
#include <opencv2/opencv.hpp>
#include <iostream>

using namespace cv;
using namespace std;

// 1 车牌区域检测
Mat detectLicensePlate(Mat& src)
{
    // 一 对图像做预处理
   // 1. 转换为HSV颜色空间(便于基于颜色筛选)
    Mat hsv;
    cvtColor(src, hsv, COLOR_BGR2HSV);

    // 2. 颜色筛选(蓝色车牌范围,可根据实际调整)
    Mat blue_mask;
    inRange(hsv, Scalar(100, 70, 70), Scalar(140, 255, 255), blue_mask);

    // 3. 形态学操作(连接边缘)
    Mat kernel = getStructuringElement(MORPH_RECT, Size(15, 3));
    morphologyEx(blue_mask, blue_mask, MORPH_CLOSE, kernel);

    //4 查找轮廓
    vector<vector<Point>> contours;
    vector<Vec4i>  hierarchy;
    findContours(blue_mask, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE);

    //5 筛选车牌区域
    vector<Rect> possibleRect;
    for (int i = 0; i < contours.size(); i++)
    {
        // 筛选可能的车牌区域
        double area = contourArea(contours[i]);
        double length = arcLength(contours[i], true);
        if (area > 1000 && length < 500)
        {
            Rect rect = boundingRect(contours[i]);
            double ratio = rect.width / rect.height;
            if (ratio >= 2 && ratio <= 3)
            {
                /*cout << "area:" << area << "    length:" << length << "    ratio:" << ratio << endl;
                rectangle(src, rect, Scalar(0, 255, 0), 2);*/
                possibleRect.push_back(rect);
            }
        }
    }
    if (possibleRect.empty())
    {
        return Mat();
    }
    Rect rect = possibleRect.at(0);
    Mat plate = src(rect);
    return plate;
}

int main() {

    Mat src = imread("C:\\Users\\46285\\Desktop\\qt_code\\images\\licensePlate3.jpeg");
    if (src.empty())
    {
        cout << "image read fail!!!" << endl;
        return -1;
    }
    Mat plate = detectLicensePlate(src);
    imshow("test", plate);
    waitKey(0);
    destroyAllWindows();// 销毁所有窗口
    return 0;
}

第八章 深度学习&目标检测(了解)

深度学习(Deep Learning, DL)是一种特殊的机器学习方法,它模仿人脑的工作机制来处理数据,特别是通过多层的神经网络模型进行学习。这些网络能够自动从大量数据中学习复杂的特征表示。主要依赖于人工神经网络,尤其是深层架构,具有强大的特征提取能力

机器学习(Machine Learning, ML)是实现人工智能的一种方法,它使计算机系统能够通过经验(即数据)自动改进和适应,而无需明确编程来执行特定任务。使用算法解析数据,从中学习,并根据学到的信息对新数据做出决定或预测

1. MobileNet预训练模型+CIFAR-10数据集

cpp 复制代码
#include <opencv2/opencv.hpp>
#include <opencv2/dnn.hpp>
#include <iostream>
#include <vector>
#include <string>
#include <fstream>

using namespace cv;
using namespace cv::dnn;
using namespace std;

// 加载类标签的函数
// 加载图像类别文件,将类别文件中的内容,转成Vector<>返回
vector<string> loadClassLabels(const string& labelPath) {
    vector<string> classes;
    ifstream file(labelPath);
    if (file.is_open()) {
        string line;
        while (getline(file, line)) {
            classes.push_back(line);
        }
        file.close();
    }
    return classes;
}

int fun1()
{
    try {
        // 1. 加载模型和标签
        string modelPath = "C:/software/test/mobilenetv2_cifar10.onnx";
        string labelPath = "C:/software/test/cifar-100-binary/batches.meta.txt";
        string imagePath = "C:/Users/Administrator/Desktop/images/dog1.jpeg";

        // 加载模型文件
        Net net = readNetFromONNX(modelPath);
        if (net.empty()) {
            cerr << "Error: Failed to load ONNX model." << endl;
            return -1;
        }

        // 加载数据集的类别标签
        vector<string> classes = loadClassLabels(labelPath);
        if (classes.empty()) {
            cout << "label path load fail......." << endl;
            return -1;
        }
        else {
            for (string className : classes)
            {
                cout << className << "\t";
            }
            cout << endl;
        }

        // 2. 加载要做图像分裂的图片.并预处理图像
        Mat image = imread(imagePath);
        if (image.empty()) {
            cerr << "Error: Failed to load image." << endl;
            return -1;
        }

        // MobileNet 的预处理:调整大小、归一化、均值减法
        Mat blob = blobFromImage(image,
            1.0 / 255.0,                     // 缩放因子
            Size(32, 32),                    // 输入尺寸
            Scalar(),                        // 不减均值
            false,                           // 不交换RB通道
            false);                          // 不裁剪

        // 调整 blob 形状为 (batch_size, height, width, channels)
        Mat blob_transposed;
        transposeND(blob, { 0, 2, 3, 1 }, blob_transposed);
        net.setInput(blob_transposed);

        // 3. 执行推理
        Mat output = net.forward();

        // 打印输出结果用于调试
        cout << "Model output: " << output << endl;

        // 4. 解析结果
        Point classIdPoint;
        double confidence;
        minMaxLoc(output.reshape(1, 1), 0, &confidence, 0, &classIdPoint);
        int classId = classIdPoint.x;

        cout << "Output shape: " << output.size() << endl;

        // 5. 显示结果
        if (classId < 0 || classId >= classes.size()) {
            cerr << "Error: Invalid class ID: " << classId << endl;
            return -1;
        }
        cout << "classId:" << classId << endl;
        string label = format("%s: %.2f%%", classes[classId].c_str(), confidence * 100);
        cout << "Predicted: " << label << endl;

        // 在图像上绘制结果
        resize(image, image, Size(600, 600));
        putText(image, label, Point(10, 30), FONT_HERSHEY_SIMPLEX, 0.8, Scalar(0, 255, 0), 2);
        imshow("Classification Result", image);
        waitKey(0);
        destroyAllWindows();
    }
    catch (exception& e)
    {
        cout << e.what() << endl;
    }
    return 0;
}

第九章 视频处理&摄像头操作

1. VideoCapture读取视频

在C++中使用OpenCV读取视频文件摄像头实时流,主要通过 cv::VideoCapture 类实现。

cpp 复制代码
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;

int main()
{
	//1 指定视频文件路径创建VideoCapture 
	VideoCapture cap("C:/Users/Administrator/Desktop/video/cat.mp4");

	//2 判断是否成功打开视频
	if (!cap.isOpened())
	{
		cout << "视频打开失败!" << endl;
		return -1;
	}

	//3 获取视频基本信息
	double width = cap.get(CAP_PROP_FRAME_WIDTH);
	double height = cap.get(CAP_PROP_FRAME_HEIGHT);
	double count = cap.get(CAP_PROP_FRAME_COUNT);
	double fps = cap.get(CAP_PROP_FPS);
	cout << "视频的宽=" << width << endl;
	cout << "视频的高=" << height << endl;
	cout << "视频的总帧数=" << count << endl;
    //FPS是视频或游戏画面的帧率,单位为"帧/秒"(frames per second)。例如,24FPS表示每秒显示24张图像。
	cout << "视频的fps=" << fps << endl;


	//4 逐帧读取视频并展示
	Mat frame;
	width = width * 0.2;
	height = height * 0.2;
	while (true)
	{
		// 读取视频的每一帧
		cap >> frame;	//等价于cap.read(frame);
		// 检测当前帧是否为空,一般如果为空,说明已经到达视频末尾
		if (frame.empty())
		{
			break;
		}
		// 如果帧的高和宽过大,导致展示的图片不全,可以调整宽和高
		Mat dst;
		resize(frame, dst, Size(width,height));

		// 对每一帧做处理,比如展示,或者将当前帧转换为灰度图像等等
		Mat gray;
		cvtColor(dst, gray, COLOR_BGR2GRAY);
		imshow("video", dst);
		imshow("gray video", gray);
		// 根据视频的帧率延迟,并按esc键退出循环
		if (waitKey(1000/fps) == 27)
		{
			break;
		}
	}

	//5 释放资源
	cap.release();
	destroyAllWindows();
	return 0;
}

2. 常用VideoCapture视频属性

CAP_PROP_FRAME_WIDTH 视频帧的宽度(像素) double 1920
CAP_PROP_FRAME_HEIGHT 视频帧的高度(像素) double 1080
CAP_PROP_FPS 视频的帧率(Frames Per Second) double 30.0
CAP_PROP_FRAME_COUNT 视频的总帧数 double 2500(需注意:部分摄像头流返回0)
CAP_PROP_POS_FRAMES 当前帧的索引(从0开始) double 100
CAP_PROP_POS_MSEC 当前时间戳(毫秒) double 3500.0
CAP_PROP_FOURCC 视频编解码器的FourCC代码 double 1196444237(对应'MJPG'
CAP_PROP_BRIGHTNESS 摄像头亮度设置(仅摄像头有效) double 0.5(范围依赖设备)
CAP_PROP_CONTRAST 摄像头对比度设置 double 0.3
CAP_PROP_SATURATION 摄像头饱和度设置 double 0.8
CAP_PROP_EXPOSURE 摄像头曝光值 double -4.0(依赖设备)
CAP_PROP_AUTOFOCUS 摄像头自动对焦是否开启 double 1.0(开启)

3. VideoWriter保存视频

在C++中使用OpenCV的VideoWriter类可以轻松实现视频的录制和保存。

FourCC 是一种用于标识视频编码格式的四字符代码。常用的编码格式包括:

  • 'M', 'J', 'P', 'G':Motion-JPEG 编码(常用于 .avi 文件)。
  • 'X', 'V', 'I', 'D':XVID 编码(适用于 .avi 文件)。
  • 'H', '2', '6', '4':H.264 编码(适用于 .mp4 文件)。

使用H.264编码 ,OpenCV 是要加载 OpenH264 动态链接库的,因此要使用这种模式,还需要额外安装OpenH264动态链接库。

.avi:通常与 MJPG 或 XVID 编码配合使用。 .mp4:通常与 H.264 编码配合使用。 确保系统支持所选编码格式,否则可能无法生成视频文件

cpp 复制代码
void writeVideo(VideoCapture& cap)
{
	//1 创建VideoWriter对象,用于写视频文件
	cv::String fileName = "C:/Users/Administrator/Desktop/video/temp.avi";
	// 设置视频编码格式
	int fourcc = VideoWriter::fourcc('M', 'J', 'P', 'G');
	double width = cap.get(CAP_PROP_FRAME_WIDTH);
	double height = cap.get(CAP_PROP_FRAME_HEIGHT);
	double fps = cap.get(CAP_PROP_FPS);
	// isColor:是否为彩色视频(默认为 true)。如果为 false,则生成灰度视频
	VideoWriter writer(fileName,fourcc,fps,Size(width,height),false);

	//2 打开文件
	if (!writer.isOpened())
	{
		cout << "视频文件打开失败!" << endl;
		return;
	}

	//3 向视频文件写入帧
	Mat src;
	while (true)
	{
		// 读取帧
		cap >> src;
		// 读完视频退出
		if (src.empty())
		{
			break;
		}

		// 操作帧
		// 将视频灰度化后重新保存
		Mat gray;
		cvtColor(src, gray, COLOR_BGR2GRAY);

		// 写入帧
		writer.write(gray);

		// 按下Esc键退出循环
		if (waitKey(30) == 27)
		{
			break;
		}
	}
	cout << "已经成功保存视频" << endl;
	//4 释放资源
	writer.release();
}

4. 视频追踪

视频追踪的实现通常包括以下几个步骤:

  1. 目标检测:在初始帧中识别并定位感兴趣的目标。
  2. 特征提取:从目标区域提取可用于区分该目标与其他对象的特征(如颜色、形状、纹理等)
  3. 模型更新:随着视频帧的变化,可能需要更新目标模型以适应外观变化。
  4. 位置预测与校正:根据前一帧中的目标位置和速度,预测当前帧中的位置,并通过搜索或匹配算法进行校正

4.1 Meanshift 算法

Meanshift 是一种基于密度估计的非参数化算法,最初用于聚类分析。在计算机视觉中,它被广泛应用于目标追踪任务。Meanshift 的核心思想是通过迭代地将搜索窗口向目标区域的颜色直方图分布中心移动,从而实现对目标的定位和追踪

cpp 复制代码
void meanshiftTest(VideoCapture& cap)
{
	//1 获取第一帧
	Mat frame;
	cap >> frame;
	if (frame.empty())
	{
		cout << "无法读取视频帧" << endl;
		return;
	}

	//2 定义初始的目标窗口(ROI)
	Rect roi = selectROI("请选择追踪目标",frame,false);	// 手动选择目标
	cout << roi.width << "   " << roi.height << endl;
	if (roi.width <= 0 || roi.height <= 0)
	{
		cout << "已取消选择roi区域" << endl;
		return;
	}
	Mat target = frame(roi).clone();
	if (target.empty())
	{
		cout << "目标区域为空" << endl;
		return;
	}
	imshow("target", target);

	//3 转换目标窗口的颜色空间
	Mat hsv_target;
	cvtColor(target, hsv_target, COLOR_BGR2HSV);

	//4 过滤掉低亮度值的像素,较少噪声
	Mat mask;
	inRange(hsv_target, Scalar(0, 30, 0), Scalar(180, 255, 255), mask);

	//5 计算目标区域的颜色直方图
	int histSize = 180;
	float range[] = { 0,180 };
	const float* histRange = { range };
	Mat hist_target;
	int channels[] = { 0 };
	// 计算直方图
	calcHist(&hsv_target, 1, channels, mask, hist_target, 1, &histSize, &histRange);
	// 归一化
	normalize(hist_target, hist_target, 0,255, NORM_MINMAX);

	//6 利用meanshift逐帧循环追踪
	TermCriteria criteria(TermCriteria::EPS | TermCriteria::COUNT, 10, 1);  // 迭代终止条件
	while (true)
	{
		// 读取帧
		cap >> frame;
		if (frame.empty())
		{
			break;
		}

		// 转换为hsv色彩空间
		Mat hsvFrame;
		cvtColor(frame, hsvFrame, COLOR_BGR2HSV);

		// 计算反向投影
		Mat back;
		calcBackProject(&hsvFrame, 1, channels, hist_target, back,&histRange);

		// 执行MeanShift算法
		meanShift(back, roi, criteria);

		// 绘制结果
		rectangle(frame, roi, Scalar(0, 255, 0));
		imshow("视频追踪", frame);
		if (waitKey(30) == 27)
		{
			break;
		}
	}
}

4.2 Camshift 算法

Camshift(Continuously Adaptive Mean Shift)是 Meanshift 算法的改进版本,专门用于目标追踪。与 Meanshift 不同,Camshift 能够自适应地调整搜索窗口的大小和方向,从而更好地处理目标的尺度变化和旋转

cpp 复制代码
void camshiftTest(VideoCapture& cap)
{
	//1 获取第一帧
	Mat frame;
	cap >> frame;
	if (frame.empty())
	{
		cout << "无法读取视频帧" << endl;
		return;
	}

	//2 定义初始的目标窗口(ROI)
	Rect roi = selectROI("请选择追踪目标", frame, false);	// 手动选择目标
	cout << roi.width << "   " << roi.height << endl;
	if (roi.width <= 0 || roi.height <= 0)
	{
		cout << "已取消选择roi区域" << endl;
		return;
	}
	Mat target = frame(roi).clone();
	if (target.empty())
	{
		cout << "目标区域为空" << endl;
		return;
	}
	imshow("target", target);

	//3 转换目标窗口的颜色空间
	Mat hsv_target;
	cvtColor(target, hsv_target, COLOR_BGR2HSV);

	//4 过滤掉低亮度值的像素,较少噪声
	Mat mask;
	inRange(hsv_target, Scalar(0, 60, 32), Scalar(180, 255, 255), mask);

	//5 计算目标区域的颜色直方图
	int histSize = 180;
	float range[] = { 0,180 };
	const float* histRange = { range };
	Mat hist_target;
	int channels[] = { 0 };
	// 计算直方图
	calcHist(&hsv_target, 1, channels, mask, hist_target, 1, &histSize, &histRange);
	// 归一化
	normalize(hist_target, hist_target, 0, 255, NORM_MINMAX);

	//6 利用meanshift逐帧循环追踪
	TermCriteria criteria(TermCriteria::EPS | TermCriteria::COUNT, 10, 1);  // 迭代终止条件
	while (true)
	{
		// 读取帧
		cap >> frame;
		if (frame.empty())
		{
			break;
		}

		// 转换为hsv色彩空间
		Mat hsvFrame;
		cvtColor(frame, hsvFrame, COLOR_BGR2HSV);

		// 计算反向投影
		Mat back;
		calcBackProject(&hsvFrame, 1, channels, hist_target, back, &histRange);

		// 执行CamShift算法
		RotatedRect trackBox = CamShift(back, roi, criteria);

		// 绘制结果
		rectangle(frame, roi, Scalar(0, 255, 0));
		//ellipse(frame, trackBox, Scalar(0, 0, 255), 2);  // 绘制椭圆表示目标形状
		imshow("视频追踪", frame);
		if (waitKey(30) == 27)
		{
			break;
		}
	}
}

4.3 MeanShift和CamShift算法的比较

优先MeanShift:

  • 目标大小和方向基本不变(如静态摄像头下的人脸追踪)。
  • 对实时性要求极高(>30 FPS)。

优先CamShift:

  • 目标尺度或方向变化明显(如车辆靠近/远离、手势旋转)。
  • 需要输出目标朝向信息(如AR应用)。

5. 摄像头实时处理

5.1 摄像头实时捕获&处理

实现摄像头实时处理的基本步骤

  1. 打开摄像头:使用 OpenCV 的 cv::VideoCapture 打开摄像头设备或读取视频文件。
  2. 逐帧读取视频流:通过循环不断从摄像头读取每一帧图像。
  3. 对每一帧进行处理:应用计算机视觉算法(如目标检测、边缘检测、颜色分割等)对图像进行处理。
  4. 显示处理结果:使用 OpenCV 的 cv::imshow() 函数将处理后的图像实时显示在窗口中。
  5. 释放资源:处理完成后,关闭摄像头并释放相关资源。
cpp 复制代码
#include <opencv2/opencv.hpp>
#include <iostream>

using namespace cv;
using namespace std;

void cameraTest()
{
	//1 打开摄像头
	// 参数 0 表示默认摄像头(通常为内置摄像头)。如果有多台摄像头,可以尝试使用 1, 2 等索引值。
	VideoCapture cap(0);

	//2 设置摄像头分辨率,也就是设置宽和高
	cap.set(CAP_PROP_FRAME_WIDTH, 680);
	cap.set(CAP_PROP_FRAME_HEIGHT, 480);

	//3 存储摄像头传输的每一帧图像
	Mat frame;
	while (true)
	{
		// 读取每一帧
		cap >> frame;
		if (frame.empty())
		{
			cout << "无法读取摄像头帧" << endl;
			break;
		}

		// 对帧做处理,比如灰度处理
		Mat gray;
		cvtColor(frame, gray, COLOR_BGR2GRAY);

		// 显示实时帧和处理后的帧
		imshow("实时帧", frame);
		imshow("灰度帧", gray);

		if (waitKey(30) == 27)
		{
			break;
		}
	}
	// 释放
	cap.release();
	destroyAllWindows();
}

int main() {

	cameraTest();
    return 0;
}

5.2 实时跟踪

使用meanShiftcamShift算法对摄像头捕获到的内容做实时跟踪。整个操作和上面视频跟踪一模一样,只是不提供已存在的视频,而是通过摄像头实时捕获

cpp 复制代码
void camShiftCameraTest()
{
	VideoCapture cap(0);
	//1 获取第一帧
	Mat frame;
	cap >> frame;
	if (frame.empty())
	{
		cout << "无法读取视频帧" << endl;
		return;
	}

	//2 定义初始的目标窗口(ROI)
	Rect roi = selectROI("请选择追踪目标", frame, false);	// 手动选择目标
	cout << roi.width << "   " << roi.height << endl;
	if (roi.width <= 0 || roi.height <= 0)
	{
		cout << "已取消选择roi区域" << endl;
		return;
	}
	Mat target = frame(roi).clone();
	if (target.empty())
	{
		cout << "目标区域为空" << endl;
		return;
	}
	imshow("target", target);

	//3 转换目标窗口的颜色空间
	Mat hsv_target;
	cvtColor(target, hsv_target, COLOR_BGR2HSV);

	//4 过滤掉低亮度值的像素,较少噪声
	Mat mask;
	inRange(hsv_target, Scalar(0, 60, 32), Scalar(180, 255, 255), mask);

	//5 计算目标区域的颜色直方图
	int histSize = 180;
	float range[] = { 0,180 };
	const float* histRange = { range };
	Mat hist_target;
	int channels[] = { 0 };
	// 计算直方图
	calcHist(&hsv_target, 1, channels, mask, hist_target, 1, &histSize, &histRange);
	// 归一化
	normalize(hist_target, hist_target, 0, 255, NORM_MINMAX);

	//6 利用meanshift逐帧循环追踪
	TermCriteria criteria(TermCriteria::EPS | TermCriteria::COUNT, 10, 1);  // 迭代终止条件
	while (true)
	{
		// 读取帧
		cap >> frame;
		if (frame.empty())
		{
			break;
		}

		// 转换为hsv色彩空间
		Mat hsvFrame;
		cvtColor(frame, hsvFrame, COLOR_BGR2HSV);

		// 计算反向投影
		Mat back;
		calcBackProject(&hsvFrame, 1, channels, hist_target, back, &histRange);

		// 执行MeanShift算法
		RotatedRect trackBox = CamShift(back, roi, criteria);

		// 绘制结果
		rectangle(frame, roi, Scalar(0, 255, 0));
		//ellipse(frame, trackBox, Scalar(0, 0, 255), 2);  // 绘制椭圆表示目标形状
		imshow("视频追踪", frame);
		if (waitKey(30) == 27)
		{
			break;
		}
	}
	destroyAllWindows();
}

5.3 实时边缘检测

通过摄像头实时捕获视频,使用边缘检测算法(如 Canny 边缘检测)提取图像中的边缘

cpp 复制代码
void cameraCannyTest()
{
	//1 打开摄像头
	// 参数 0 表示默认摄像头(通常为内置摄像头)。如果有多台摄像头,可以尝试使用 1, 2 等索引值。
	VideoCapture cap(0);

	//2 设置摄像头分辨率,也就是设置宽和高
	cap.set(CAP_PROP_FRAME_WIDTH, 680);
	cap.set(CAP_PROP_FRAME_HEIGHT, 480);

	//3 存储摄像头传输的每一帧图像
	Mat frame;
	while (true)
	{
		// 读取每一帧
		cap >> frame;
		if (frame.empty())
		{
			cout << "无法读取摄像头帧" << endl;
			break;
		}

		// 对帧做处理,比如灰度处理
		Mat gray;
		cvtColor(frame, gray, COLOR_BGR2GRAY);

		// 高斯模糊
		Mat blur;
		GaussianBlur(gray, blur, Size(3, 3),1.5);

		// Canny边缘检测
		Mat dst;
		Canny(blur, dst, 50, 150);

		// 显示实时帧和处理后的帧
		imshow("实时帧", frame);
		imshow("边缘检测", dst);

		if (waitKey(30) == 27)
		{
			break;
		}
	}
	// 释放
	cap.release();
	destroyAllWindows();
}
相关推荐
巷9551 分钟前
常见的卷积神经网络列举
人工智能·神经网络·cnn
每天都要写算法(努力版)16 分钟前
【神经网络与深度学习】VAE 在解码前进行重参数化
人工智能·深度学习·神经网络
蜂耘39 分钟前
国产大模型新突破:小米大语言模型开源,推理性能超越o1-mini
人工智能·语言模型
m0_620607811 小时前
机器学习——逻辑回归ROC练习
人工智能·机器学习·逻辑回归
szxinmai主板定制专家1 小时前
基于RK3568多功能车载定位导航智能信息终端
大数据·arm开发·人工智能·计算机视觉·fpga开发
yxc_inspire1 小时前
基于Qt的app开发第六天
开发语言·c++·qt
qianqianaao1 小时前
实验六 基于Python的数字图像压缩算法
开发语言·图像处理·python·opencv·计算机视觉·自然语言处理·php
摆烂仙君1 小时前
注意力(Attention)机制详解(附代码)
人工智能·机器学习·计算机视觉
数造科技2 小时前
数造科技携 DataBuilder 亮相安徽科交会,展现“DataOps +AI”双引擎魅力
大数据·人工智能·科技·ai·业界资讯·data
老任与码2 小时前
Spring AI(1)—— 基本使用
java·人工智能·spring ai