OPENCV学习(补充1)

目录

[80-Haar与LBP级联分类器使用-01(了解就行,现在工程上更多使用 dlib、OpenCV‑DNN 深度学习人脸关键点,替代老旧 Haar,LBP 级联人脸检测,工业已基本淘汰工业已基本淘汰)](#80-Haar与LBP级联分类器使用-01(了解就行,现在工程上更多使用 dlib、OpenCV‑DNN 深度学习人脸关键点,替代老旧 Haar,LBP 级联人脸检测,工业已基本淘汰工业已基本淘汰))

代码整体概述

逐段详细解释

[1. 模型对象与文件路径](#1. 模型对象与文件路径)

[2.load () 加载模型](#2.load () 加载模型)

[3. 图像预处理](#3. 图像预处理)

[4.OpenCV 高精度计时](#4.OpenCV 高精度计时)

[5.detectMultiScale 参数](#5.detectMultiScale 参数)

[6. 绘制人脸框](#6. 绘制人脸框)

窗口输出

[Haar 与 LBP 对比(本代码可以直接做对照实验)](#Haar 与 LBP 对比(本代码可以直接做对照实验))

代码小问题

简答背诵

​编辑

[85-视频中人脸检测与眼睛跟踪-03(已淘汰,现在工程上更多使用 dlib、OpenCV‑DNN 深度学习人脸关键点,替代老旧 Haar。)](#85-视频中人脸检测与眼睛跟踪-03(已淘汰,现在工程上更多使用 dlib、OpenCV‑DNN 深度学习人脸关键点,替代老旧 Haar。))

[代码整体概述(现在工程上更多使用 dlib、OpenCV‑DNN 深度学习人脸关键点,替代老旧 Haar。)](#代码整体概述(现在工程上更多使用 dlib、OpenCV‑DNN 深度学习人脸关键点,替代老旧 Haar。))

逐段详细解析

[1. 模型与检测器对象](#1. 模型与检测器对象)

[2.trackEye () 模板匹配跟踪函数](#2.trackEye () 模板匹配跟踪函数)

[3. 摄像头主循环预处理](#3. 摄像头主循环预处理)

[4.detectMultiScale 多尺度检测](#4.detectMultiScale 多尺度检测)

[5. 划定眼睛 ROI](#5. 划定眼睛 ROI)

[6. 双模式逻辑:初始化 + 跟踪](#6. 双模式逻辑:初始化 + 跟踪)

[7. 坐标转换关键点](#7. 坐标转换关键点)

[8. 退出逻辑](#8. 退出逻辑)

窗口输出

关键知识点(面试)

代码现存缺陷

简答背诵

[94-高斯混合模型(GMM)方法-原理与数据聚类(GMM-EM(高斯混合模型 + 期望最大化)作为经典无监督聚类和概率建模算法不会淘汰,在图像分割、背景建模、数据聚类等场景仍在使用;但作为复杂语义分割方案已被深度学习取代。)](#94-高斯混合模型(GMM)方法-原理与数据聚类(GMM-EM(高斯混合模型 + 期望最大化)作为经典无监督聚类和概率建模算法不会淘汰,在图像分割、背景建模、数据聚类等场景仍在使用;但作为复杂语义分割方案已被深度学习取代。))

代码整体概述

逐段详细解析

[1. 随机数与画布](#1. 随机数与画布)

[2. 样本矩阵 points](#2. 样本矩阵 points)

[3. 生成多簇正态分布样本](#3. 生成多簇正态分布样本)

[4.EM 模型参数](#4.EM 模型参数)

[5. 逐像素预测,绘制背景分类区域](#5. 逐像素预测,绘制背景分类区域)

[6. 绘制原始样本点](#6. 绘制原始样本点)

窗口输出

[GMM‑EM 对比 KMeans(高频面试)](#GMM‑EM 对比 KMeans(高频面试))

简答背诵

[95-高斯混合模型(GMM)方法-图像分割(GMM-EM(高斯混合模型 + 期望最大化)作为经典无监督颜色分割方法仍在广泛使用,尤其在简单颜色分割、教学、特定工业场景;复杂语义分割已被深度学习取代。)](#95-高斯混合模型(GMM)方法-图像分割(GMM-EM(高斯混合模型 + 期望最大化)作为经典无监督颜色分割方法仍在广泛使用,尤其在简单颜色分割、教学、特定工业场景;复杂语义分割已被深度学习取代。))

代码整体概述

逐段详细解析

[1. 样本矩阵 points](#1. 样本矩阵 points)

[2. 像素循环,填充 points](#2. 像素循环,填充 points)

[3.EM 模型参数训练](#3.EM 模型参数训练)

[4. 两段实现方式对比(重点)](#4. 两段实现方式对比(重点))

[5.predict2 返回值](#5.predict2 返回值)

[6. 计时](#6. 计时)

窗口输出

[EM (GMM) 图像分割 vs KMeans 图像分割](#EM (GMM) 图像分割 vs KMeans 图像分割)

代码存在问题

简答背诵

[101-Grabcut原理与演示应用-代码演示(GrabCut 作为交互式图像分割工具没有淘汰,仍在抠图、医学图像、无训练数据场景使用;但批量自动分割已被 SAM、U-Net 等深度学习方法取代。)](#101-Grabcut原理与演示应用-代码演示(GrabCut 作为交互式图像分割工具没有淘汰,仍在抠图、医学图像、无训练数据场景使用;但批量自动分割已被 SAM、U-Net 等深度学习方法取代。))

代码整体概述

[Grab‑Cut mask 四个值(必背)](#Grab‑Cut mask 四个值(必背))

逐段解析

[1. 全局变量](#1. 全局变量)

[2. 鼠标回调 onMouse()](#2. 鼠标回调 onMouse())

3.setROIMask()

4.showImage()

[5.runGrabCut () 核心函数 grabCut()](#5.runGrabCut () 核心函数 grabCut())

[6.main 主循环](#6.main 主循环)

窗口输出

[GrabCut 对比 KMeans / EM 分割](#GrabCut 对比 KMeans / EM 分割)

简答背诵

[103-案例实战一证件照背景替换(K-Means 颜色聚类没有被淘汰,在颜色量化、图像压缩、简单分割、主色提取等场景中仍是高效常用的方法;复杂语义分割已被深度学习取代。)](#103-案例实战一证件照背景替换(K-Means 颜色聚类没有被淘汰,在颜色量化、图像压缩、简单分割、主色提取等场景中仍是高效常用的方法;复杂语义分割已被深度学习取代。))

代码整体概述

逐段详细解析

[1. mat_to_samples()函数](#1. mat_to_samples()函数)

[2.KMeans 聚类调用](#2.KMeans 聚类调用)

[3. 生成 mask 掩码](#3. 生成 mask 掩码)

[4. 形态学腐蚀 + 高斯模糊](#4. 形态学腐蚀 + 高斯模糊)

[5. 三分支像素融合逻辑](#5. 三分支像素融合逻辑)

窗口输出

核心优缺点

[对比 GrabCut](#对比 GrabCut)

简答背诵

[105-案例实战一绿幕背景视频抠图(HSV 颜色阈值绿幕抠图在简单场景、实时应用、教学实验中仍广泛使用;但专业影视 / 直播场景中,已被更精细的算法和深度学习抠图方案补充或替代。)](#105-案例实战一绿幕背景视频抠图(HSV 颜色阈值绿幕抠图在简单场景、实时应用、教学实验中仍广泛使用;但专业影视 / 直播场景中,已被更精细的算法和深度学习抠图方案补充或替代。))

[HSV 颜色空间绿幕抠图:仍在使用,专业场景有更优方案](#HSV 颜色空间绿幕抠图:仍在使用,专业场景有更优方案)

一句话结论

还在用的场景

[HSV 绿幕抠图的局限性](#HSV 绿幕抠图的局限性)

专业场景的更优方案

[HSV 绿幕抠图典型流程(必背)](#HSV 绿幕抠图典型流程(必背))

对比表

简答背诵

核心考点

代码整体概述

逐段详细解析

[1. 全局变量](#1. 全局变量)

[2. 视频读取与颜色阈值抠图](#2. 视频读取与颜色阈值抠图)

[3. 形态学处理 + 高斯模糊](#3. 形态学处理 + 高斯模糊)

[4.replace_and_blend 函数](#4.replace_and_blend 函数)

[5. 主循环](#5. 主循环)

窗口输出

关键知识点对比

代码存在问题

简答背诵

110-背景消除建模(BSM)-02(重要补充代码)

[BSM(Background Subtraction Method,背景减除方法):仍在广泛使用](#BSM(Background Subtraction Method,背景减除方法):仍在广泛使用)

一句话结论

还在用的场景

[BSM 的优势](#BSM 的优势)

局限性

[常见 BSM 算法对比](#常见 BSM 算法对比)

工业项目中的实际用法

简答背诵

核心考点

[112-对象检测与跟踪(基于颜色)-02(基于颜色阈值的方法在特定场景(教学、机器人竞赛、简单工业检测)仍广泛使用;但在通用、复杂场景下已被更鲁棒的检测 + 跟踪方案取代)](#112-对象检测与跟踪(基于颜色)-02(基于颜色阈值的方法在特定场景(教学、机器人竞赛、简单工业检测)仍广泛使用;但在通用、复杂场景下已被更鲁棒的检测 + 跟踪方案取代))

工业项目中的实际选择

对比表

简答背诵

代码整体概述

逐段详细解析

[1. 全局变量](#1. 全局变量)

[2. 形态学核](#2. 形态学核)

[3.inRange 颜色阈值](#3.inRange 颜色阈值)

[4. 形态学操作](#4. 形态学操作)

[5.processFrame () 轮廓处理函数](#5.processFrame () 轮廓处理函数)

[6. 绘制跟踪框](#6. 绘制跟踪框)

[7. 视频循环](#7. 视频循环)

窗口输出

算法优缺点

简答背诵

拓展对比

视频11111111111111111115-光流的对象跟踪-03(稀疏光流,代码可以参考以前的博文)116-光流的对象跟踪-04(稠密光流,代码可以参考以前的博文)

直接用光流做完整对象跟踪在工业中已基本淘汰(易漂移、无重检测);但光流作为跟踪系统的辅助模块(帧间预测、降算力)仍广泛使用,光流算法本身也在持续发展。

一句话结论

光流算法本身还在用的场景

为什么直接用光流做对象跟踪已少用

光流的两类核心方法

工业项目中的实际用法

对比表

简答背诵

核心考点

光流分类

1、按照计算像素范围划分(最常考)

[① 稀疏光流 Sparse Optical Flow](#① 稀疏光流 Sparse Optical Flow)

[② 稠密光流 Dense Optical Flow](#② 稠密光流 Dense Optical Flow)

2、按照求解方法划分

3、按照图像金字塔

4、另外两个概念

对比速记表

光流三个基本假设(简答题)

[120-CAMShift对象跟踪-04(CAMShift 作为独立的完整对象跟踪方案在工业中已基本淘汰;但在教学实验和颜色固定、背景可控的简单场景中仍在使用)](#120-CAMShift对象跟踪-04(CAMShift 作为独立的完整对象跟踪方案在工业中已基本淘汰;但在教学实验和颜色固定、背景可控的简单场景中仍在使用))

代码整体概述

逐段详细解析

[1. 全局参数](#1. 全局参数)

2.selectROI

[3.HSV 转换 + mask 过滤](#3.HSV 转换 + mask 过滤)

[4.mixChannels 提取 Hue 通道](#4.mixChannels 提取 Hue 通道)

[5.calcHist 计算目标直方图(第一帧)](#5.calcHist 计算目标直方图(第一帧))

[6.calcBackProject 直方图反向投影](#6.calcBackProject 直方图反向投影)

[7.CamShift 核心函数](#7.CamShift 核心函数)

[8.ellipse 绘制旋转框](#8.ellipse 绘制旋转框)

[9.firstRead 标志](#9.firstRead 标志)

窗口输出

[CamShift / MeanShift 对比](#CamShift / MeanShift 对比)

优缺点

简答背诵

视频11111111111111111111111

[121-视频中移动对象统计(MOG2 背景建模:仍在使用,静态摄像头场景的经典选择)](#121-视频中移动对象统计(MOG2 背景建模:仍在使用,静态摄像头场景的经典选择))

[MOG2 背景建模:仍在使用,静态摄像头场景的经典选择](#MOG2 背景建模:仍在使用,静态摄像头场景的经典选择)

一句话结论

还在用的场景

[MOG2 的优势](#MOG2 的优势)

局限性(复杂场景下的问题)

工业项目中的实际用法

对比表

简答背诵

代码整体说明

逐段解析

[1. 创建 MOG2 背景模型](#1. 创建 MOG2 背景模型)

[2. 预处理掩码](#2. 预处理掩码)

[3.findContours 轮廓查找](#3.findContours 轮廓查找)

[4. 轮廓筛选逻辑](#4. 轮廓筛选逻辑)

[⚠️代码 BUG:putText 参数写错!](#⚠️代码 BUG:putText 参数写错!)

[5. 窗口输出](#5. 窗口输出)

[MOG2 背景建模原理简述](#MOG2 背景建模原理简述)

优缺点

简答背诵

拓展对比

视频1111111111111111111

122-扩展模块中的跟踪方法介绍(已经废弃)

123-扩展模块中的多对象跟踪(已经废弃)

对比总结表

简答背诵

[126-使用GoogleNet模型实现图像分类-02(GoogLeNet(Inception v1)作为图像分类的工业首选方案已被淘汰;但在教学实验、论文基线、简单分类任务和遗留项目中仍在使用。)](#126-使用GoogleNet模型实现图像分类-02(GoogLeNet(Inception v1)作为图像分类的工业首选方案已被淘汰;但在教学实验、论文基线、简单分类任务和遗留项目中仍在使用。))

[128-使用SSD模型实现对象检测-02(SSD 作为对象检测的工业首选方案已被 YOLO 系列(v5/v8)取代;但作为经典基线、教学实验、MobileNet-SSD 轻量部署和遗留项目仍在使用。)](#128-使用SSD模型实现对象检测-02(SSD 作为对象检测的工业首选方案已被 YOLO 系列(v5/v8)取代;但作为经典基线、教学实验、MobileNet-SSD 轻量部署和遗留项目仍在使用。))

129-MobileNet模型实时对象检测(MobileNet 作为轻量级骨干网络没有淘汰,仍在移动端 / 嵌入式实时检测中广泛使用;但 MobileNet-SSD 这个具体检测组合在工业新项目中已被 YOLOv8n/s 等轻量检测模型取代

133-GOTURN模型实现视频对象跟踪(GOTURN 作为视频对象跟踪方案在工业中已基本淘汰,OpenCV-contrib 的TrackerGOTURN接口仍保留但工程不再使用,仅用于教学和理解早期深度学习跟踪范式

[135-均值方差与协方差 协方差矩阵](#135-均值方差与协方差 协方差矩阵)

代码整体说明

逐段解析

[① meanStdDev 图像均值与标准差](#① meanStdDev 图像均值与标准差)

[② calcCovarMatrix 协方差矩阵计算](#② calcCovarMatrix 协方差矩阵计算)

数学关系考点

标志位补充

简答背诵

记忆坑

136-特征值与特征向量

代码整体说明

逐段解析

[1. 构造输入矩阵](#1. 构造输入矩阵)

[2.eigen 函数原型](#2.eigen 函数原型)

[3. 打印特征值](#3. 打印特征值)

[4. 输出特征向量矩阵](#4. 输出特征向量矩阵)

5.waitKey(0)

⚠️关键坑点(考试常考)

[和 PCA 的关系](#和 PCA 的关系)

简答背诵

138-PCA原理与应用-02

代码整体说明

逐段解析

[main 函数部分](#main 函数部分)

[calcPCAOrientation () 核心函数](#calcPCAOrientation () 核心函数)

[⚠️OpenCV4 编译报错点](#⚠️OpenCV4 编译报错点)

核心考点

[和 minAreaRect 对比](#和 minAreaRect 对比)

简答背诵

工程用途

补充坑

140-人脸识别算法之EigenFace-02(已淘汰)

141-人脸识别算法之FisherFace(已淘汰)

142-人脸识别算法之LBPH(已淘汰)

三个传统人脸识别对比(必背)


80-Haar与LBP级联分类器使用-01(了解就行,现在工程上更多使用 dlib、OpenCV‑DNN 深度学习人脸关键点,替代老旧 Haar,LBP 级联人脸检测,工业已基本淘汰工业已基本淘汰)

代码整体概述

该代码用来对比测试 Haar / LBP 级联人脸检测速度 ,读取一张图片,使用CascadeClassifier人脸检测,利用getTickCount()统计检测耗时。

同一个CascadeClassifier类,加载不同 xml,就切换 Haar 或者 LBP 模型。当前代码加载的是 haar 模型;把加载路径换成lbpfile就切换 LBP。

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

using namespace cv;
using namespace std;

CascadeClassifier face_cascader;
//Haar人脸模型路径
String haarfile = "C:/opencv/opencv_3.2_win764_install/install/etc/haarcascades/haarcascade_frontalface_alt.xml";
//LBP人脸模型路径
String lbpfile = "C:/opencv/opencv_3.2_win764_install/install/etc/lbpcascades/lbpcascade_frontalface.xml";

int main(int argc, char** argv) {
	//加载Haar级联模型,这里可以改成lbpfile切换为LBP
	if (!face_cascader.load(haarfile)) {
		printf("could not load image...\n");
		return -1;
	}
	namedWindow("demo-win", WINDOW_AUTOSIZE);

	Mat src = imread("C:/opencv/images/test.jpg");
	if (src.empty()) {
		printf("could not load image...\n");
		return -1;
	}
	imshow("input image", src);

	Mat gray;
	vector<Rect> faces;
	cvtColor(src, gray, COLOR_BGR2GRAY);
	equalizeHist(gray, gray);  //直方图均衡,提升对比度,改善检测效果

	//计时开始
	int st = getTickCount();
	//多尺度人脸检测
	face_cascader.detectMultiScale(gray, faces, 1.1, 3, 0, Size(30, 30));
	//计算消耗tick数
	int et = (getTickCount() - st);
	printf("time consume : %d", et);

	//绘制人脸框
	for (size_t t = 0; t < faces.size(); t++) {
		rectangle(src, faces[t], Scalar(255, 0, 0), 2, 8, 0);
	}
	imshow("demo-win", src);

	waitKey(0);
	return 0;
}

逐段详细解释

1. 模型对象与文件路径

复制代码
CascadeClassifier face_cascader;
String haarfile = "...haarcascade_frontalface_alt.xml";
String lbpfile = "...lbpcascade_frontalface.xml";
  • CascadeClassifier:级联分类器类,同时支持 Haar、LBP 两种模型,只取决于加载的 xml 文件
  • haarfile:Haar 特征人脸模型;lbpfile:LBP 特征人脸模型。

2.load () 加载模型

复制代码
if (!face_cascader.load(haarfile))

把 xml 文件读到检测器对象;加载失败返回 false。

想要测试 LBP,直接改为 face_cascader.load(lbpfile),其余代码完全不用改。

3. 图像预处理

复制代码
cvtColor(src, gray, COLOR_BGR2GRAY);
equalizeHist(gray, gray);
  1. detectMultiScale只接收灰度图像输入
  2. equalizeHist()直方图均衡:拉伸灰度动态范围,暗光图像增强对比度,降低漏检。

4.OpenCV 高精度计时

复制代码
int st = getTickCount();
face_cascader.detectMultiScale(gray, faces, 1.1, 3, 0, Size(30, 30));
int et = (getTickCount() - st);
printf("time consume : %d", et);
  • getTickCount():获取系统时钟滴答数,用于性能计时。
  • getTickFrequency():每秒多少个 tick,真实耗时秒 = (end‑start)/(double)getTickFrequency()

本代码只打印 tick 差值,不是毫秒;想要得到毫秒:

复制代码
double time_ms = (et * 1000.0) / getTickFrequency();
printf("time consume: %.2f ms\n", time_ms);

5.detectMultiScale 参数

复制代码
face_cascader.detectMultiScale(gray, faces, 1.1, 3, 0, Size(30, 30));
  1. 输入灰度图;输出vector<Rect> faces所有检测到人脸矩形框。
  2. 1.1:图像金字塔缩放系数,每一层图像缩小 1.1 倍;数值越大速度越快,精度下降。
  3. 3:组合最小邻域检测结果,候选框至少被 3 次检测命中才判定为人脸。
  4. Size(30,30):最小人脸尺寸,小于这个大小直接跳过不检测。

6. 绘制人脸框

复制代码
rectangle(src, faces[t], Scalar(255,0,0),2,8,0);

蓝色绘制人脸外接矩形。

窗口输出

  1. input image:原始图片
  2. demo‑win:绘制蓝色人脸框之后结果图 控制台输出:检测消耗的 tick 计数。

Haar 与 LBP 对比(本代码可以直接做对照实验)

表格

Haar LBP
特征 Haar‑like 矩形特征 LBP 局部二值纹理特征
运算 大量浮点运算 全部整数运算,速度更快
xml 文件 haarcascade_xxx.xml lbpcascade_xxx.xml
适用场景 PC 追求检测效果 嵌入式、追求速度

实验操作:

  1. 加载 haarfile,运行,记录 tick;
  2. 修改 load 加载 lbpfile,其他代码不变,再次运行; 观察控制台时间,LBP 消耗 tick 明显更小,速度更快

代码小问题

  1. 打印提示字符串写的could not load image...,实际是加载模型失败,文字提示容易误导。
  2. 只输出 tick 计数,没有换算成毫秒,可读性差。

简答背诵

该代码演示 OpenCV 级联分类器人脸检测,支持 Haar、LBP 两种模型。读取图片转为灰度图,直方图均衡做图像增强;使用getTickCount()统计 detectMultiScale 人脸检测耗时;检测得到人脸矩形集合,绘制蓝色人脸框。CascadeClassifier类可以加载 Haar 或者 LBP 模型,只需要更换 xml 文件路径;LBP 为整数运算,运行速度优于 Haar。
注意:Haar/LBP 都是 AdaBoost 训练的传统机器学习算法,不是深度学习,对大角度侧脸、遮挡、强光暗光鲁棒性有限。

85-视频中人脸检测与眼睛跟踪-03(已淘汰,现在工程上更多使用 dlib、OpenCV‑DNN 深度学习人脸关键点,替代老旧 Haar。)

代码整体概述(现在工程上更多使用 dlib、OpenCV‑DNN 深度学习人脸关键点,替代老旧 Haar。)

该代码实现摄像头实时人脸 + 眼睛检测 + 眼睛模板匹配跟踪,基于 OpenCV 旧版 Haar 级联分类器(Viola‑Jones 算法)。 工作逻辑:

  1. 摄像头读取画面,水平镜像翻转;灰度化 + 直方图均衡增强;
  2. CascadeClassifier Haar 人脸检测器检测人脸;
  3. 在人脸 ROI 上半部分划分左眼、右眼感兴趣区域;
  4. 第一次:Haar 眼睛检测器检测到眼睛,截取眼睛图像作为模板 tpl
  5. 后续帧:不再调用 Haar 检测,改用matchTemplate模板匹配做眼睛跟踪

思路:检测器做初始化,模板匹配做快速跟踪;避免每一帧都跑 Haar 眼睛检测,提升速度。
注意:haar 级联是传统机器学习,不是深度学习,对光照、姿态比较敏感。

cpp

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

using namespace cv;
using namespace std;

// Haar级联xml模型路径
String facefile = "C:/opencv/opencv_3.2_win764_install/install/etc/haarcascades/haarcascade_frontalface_alt.xml";
String lefteyefile = "C:/opencv/opencv_3.2_win764_install/install/etc/haarcascades/haarcascade_eye.xml";
String righteyefile = "C:/opencv/opencv_3.2_win764_install/install/etc/haarcascades/haarcascade_eye.xml";

//三个级联检测器对象
CascadeClassifier face_detector;
CascadeClassifier leftyeye_detector;
CascadeClassifier righteye_detector;

Rect leftEye , rightEye;

/**
 * @brief trackEye:模板匹配实现眼睛跟踪
 * @param im 待搜索的图像ROI(人脸上面的眼睛搜索区域)
 * @param tpl 眼睛模板(第一帧由haar检测截取得到)
 * @param rect 输入输出:搜索区域左上角;输出匹配到的眼睛外接矩形
 */
void trackEye(Mat&  im, Mat& tpl, Rect& rect) {
	Mat result;
	int result_cols = im.cols - tpl.cols + 1;
	int result_rows = im.rows - tpl.rows + 1;

	result.create(result_rows, result_cols, CV_32FC1);
	//模板匹配,归一化相关系数
	matchTemplate(im, tpl, result, TM_CCORR_NORMED);

	double minval, maxval;
	Point minloc, maxloc;
	minMaxLoc(result, &minval, &maxval, &minloc, &maxloc);

	//匹配度大于0.75认为匹配成功
	if (maxval > 0.75) {
		rect.x = rect.x + maxloc.x;
		rect.y = rect.y + maxloc.y;
	} else {
		//匹配失败,矩形全部置0
		rect.x = rect.y = rect.width = rect.height = 0;
	}
}

int main(int argc, char** argv) {
	//加载haar xml模型
	if (!face_detector.load(facefile)) {
		printf("could not load data file...\n");
		return -1;
	}
	if (!leftyeye_detector.load(lefteyefile)) {
		printf("could not load data file...\n");
		return -1;
	}
	if (!righteye_detector.load(righteyefile)) {
		printf("could not load data file...\n");
		return -1;
	}

	Mat frame;
	VideoCapture capture(0); //打开默认摄像头
	namedWindow("demo-win", WINDOW_AUTOSIZE);

	Mat gray;
	vector<Rect> faces;
	vector<Rect> eyes;
	Mat lefttpl, righttpl; //眼睛模板,初始为空

	while (capture.read(frame))
	{
		flip(frame, frame, 1);   //镜像,左右翻转,符合人照镜子视觉
		cvtColor(frame, gray, COLOR_BGR2GRAY);
		equalizeHist(gray, gray); //直方图均衡,提升暗区域对比度,改善haar检测效果

		// Haar人脸多尺度检测
		face_detector.detectMultiScale(gray, faces, 1.1, 3, 0, Size(30, 30));

		for (size_t t = 0; t < faces.size(); t++)
		{
			rectangle(frame, faces[t], Scalar(255, 0, 0), 2, 8, 0); //蓝色绘制人脸框

			// 人脸内部,划定眼睛搜索的ROI区域(人脸上半部分)
			int offsety = faces[t].height / 4;
			int offsetx = faces[t].width / 8;
			int eyeheight = faces[t].height/2 - offsety;
			int eyewidth = faces[t].width/2 - offsetx;

			//=========左眼处理=========
			Rect leftRect;
			leftRect.x = faces[t].x + offsetx;
			leftRect.y = faces[t].y + offsety;
			leftRect.width = eyewidth;
			leftRect.height = eyeheight;
			Mat leftRoi = gray(leftRect); //左眼搜索ROI

			if (lefttpl.empty())
			{
				//模板为空:代表还没有初始化,使用Haar检测器找眼睛
				leftyeye_detector.detectMultiScale(leftRoi, eyes, 1.1, 1, 0, Size(20, 20));
				if (eyes.size())
				{
					//把ROI内局部坐标,换算成全图坐标
					leftRect = eyes[0] + Point(leftRect.x, leftRect.y);
					lefttpl = gray(leftRect); //截取眼睛作为后续跟踪模板
					rectangle(frame, leftRect, Scalar(0, 0, 255), 2, 8, 0);
				}
			}
			else
			{
				//模板已经存在,使用模板匹配跟踪,不再调用haar检测
				leftEye.x = leftRect.x;
				leftEye.y = leftRect.y;
				trackEye(leftRoi, lefttpl, leftEye);
				if (leftEye.x > 0 && leftEye.y > 0)
				{
					leftEye.width = lefttpl.cols;
					leftEye.height = lefttpl.rows;
					rectangle(frame, leftEye, Scalar(0, 0, 255), 2, 8, 0);
				}
			}

			//=========右眼处理=========
			Rect rightRect;
			rightRect.x = faces[t].x + faces[t].width/2;
			rightRect.y = faces[t].y + offsety;
			rightRect.width = eyewidth;
			rightRect.height = eyeheight;
			Mat rightRoi = gray(rightRect);

			if (righttpl.empty())
			{
				righteye_detector.detectMultiScale(rightRoi, eyes, 1.1, 1, 0, Size(20, 20));
				if (eyes.size())
				{
					rightRect = eyes[0] + Point(rightRect.x, rightRect.y);
					righttpl = gray(rightRect); //保存右眼模板
					rectangle(frame, rightRect, Scalar(0, 255, 255), 2, 8, 0);
				}
			}
			else
			{
				rightEye.x = rightRect.x;
				rightEye.y = rightRect.y;
				trackEye(rightRoi, righttpl, rightEye);
				if (rightEye.x > 0 && rightEye.y > 0)
				{
					rightEye.width = righttpl.cols;
					rightEye.height = righttpl.rows;
					rectangle(frame, rightEye, Scalar(0, 255, 255), 2, 8, 0);
				}
			}
		}
		imshow("demo-win", frame);
		char c = waitKey(100);
		if (c == 27) { //ESC退出
			break;
		}
	}

	capture.release();
	waitKey(0);
	return 0;
}

逐段详细解析

1. 模型与检测器对象

cpp

复制代码
CascadeClassifier face_detector;

CascadeClassifier:OpenCV Haar 级联分类器,Viola‑Jones 算法,传统 AdaBoost 机器学习,非深度学习。 需要加载 xml 训练好的模型文件。

2.trackEye () 模板匹配跟踪函数

cpp

复制代码
matchTemplate(im, tpl, result, TM_CCORR_NORMED);
  • TM_CCORR_NORMED归一化相关系数;输出值0~1,越接近 1 代表相似度越高。
  • minMaxLoc(result, &minval, &maxval, &minloc, &maxloc)找到最大相似度位置。
  • maxval>0.75阈值:大于阈值代表匹配成功;否则跟踪失效,矩形置 0。

注意:模板匹配只在传入的 ROI 小区域内搜索,不是全图搜索,速度快。

3. 摄像头主循环预处理

cpp

复制代码
flip(frame, frame, 1);
cvtColor(frame, gray, COLOR_BGR2GRAY);
equalizeHist(gray, gray);
  1. flip( ,1):左右镜像,摄像头画面镜像,符合人眼看摄像头习惯。
  2. Haar 检测器只能输入灰度图像
  3. equalizeHist直方图均衡:提升图像对比度,暗光环境改善检测效果。

4.detectMultiScale 多尺度检测

cpp

复制代码
face_detector.detectMultiScale(gray, faces, 1.1, 3, 0, Size(30, 30));

参数含义:

  1. 1.1:每次图像金字塔缩放比例;
  2. 3:至少需要 3 次邻域检测命中才判定为人脸;
  3. Size(30,30)最小检测人脸尺寸,小于该大小直接忽略。

5. 划定眼睛 ROI

人脸框内部,人脸上半部分划分左右眼搜索区域。

技巧:不在整张图找眼睛,只在人脸的上半部分小 ROI 搜索,减少误检测,加快速度。

6. 双模式逻辑:初始化 + 跟踪

cpp

复制代码
if(lefttpl.empty())
{
    //模板为空:Haar检测器检测眼睛,截取眼睛图像作为模板
}
else
{
    //模板存在:调用trackEye模板匹配跟踪,不跑Haar眼睛检测
}
  • 初始化阶段 :使用 Haar 眼睛检测器找到眼睛,截取该块灰度图像作为模板lefttpl / righttpl
  • 跟踪阶段:后续帧不再调用眼睛 Haar 检测,直接模板匹配在小 ROI 内寻找眼睛位置。

核心思想:检测做初始化,模板匹配做快速跟踪。检测慢,跟踪快。

7. 坐标转换关键点

cpp

复制代码
leftRect = eyes[0] + Point(leftRect.x, leftRect.y);

eyes[0]ROI 局部坐标系,需要加上 ROI 左上角偏移,换算成整张图像的全局像素坐标。

8. 退出逻辑

waitKey(100)每一帧等待 100ms;按下 ESC (27) 跳出循环。

窗口输出

demo‑win:摄像头实时画面

  • 蓝色框:人脸;
  • 红色框:左眼;
  • 黄框:右眼。

关键知识点(面试)

  1. Haar 级联(Viola‑Jones)

    • 属于传统机器学习 (AdaBoost),不是深度学习;
    • 依赖灰度图;光照变化、侧脸、遮挡容易失效;
    • detectMultiScale多尺度金字塔检测。
  2. 检测 vs 跟踪

    表格

    模式 实现 特点
    检测 detectMultiScale Haar 分类器 精度高,速度慢;每一帧都检测开销大
    跟踪 matchTemplate 模板匹配 模板匹配 速度快;模板容易漂移,光照变化容易跟丢

本代码策略:检测器做初始化,模板匹配做跟踪;缺点:一旦跟丢,模板不会自动重新初始化,眼睛框会消失,需要人脸重新进出画面才会再次初始化模板

  1. matchTemplate特性:
  • 模板大小固定;物体缩放、旋转之后,匹配效果急剧下降;
  • 适合短时间小范围运动跟踪。

代码现存缺陷

  1. 模板一旦初始化成功后,跟丢之后不会自动重新调用 Haar 重新检测眼睛,必须人脸消失再重新出现才会重置模板。
  2. 模板匹配对光照变化、眼睛缩放、旋转鲁棒差,容易漂移丢失。
  3. 左右眼睛共用同一个haarcascade_eye.xml,没有区分左右眼,容易误检测眉毛等。
  4. Haar 检测器对暗光、侧脸、戴眼镜效果差。

简答背诵

该代码基于 Haar 级联分类器实现摄像头人脸检测,结合模板匹配实现眼睛跟踪。摄像头读取图像,镜像翻转、灰度化、直方图均衡预处理;detectMultiScale 检测人脸;在人脸的上半部分划定左右眼 ROI;当眼睛模板为空时,使用 Haar 眼睛检测器检测眼睛,截取眼睛图像作为跟踪模板;后续帧不再调用 Haar 眼睛检测,调用 matchTemplate 模板匹配在 ROI 区域完成眼睛跟踪;达到阈值则绘制眼睛框,ESC 键退出。采用 "检测初始化 + 模板匹配跟踪" 的思路,兼顾初始化精度与运行速度,但模板匹配容易受光照影响发生漂移。
拓展对比:现在工程上更多使用 dlib、OpenCV‑DNN 深度学习人脸关键点,替代老旧 Haar。

注意:(视频截图是后续涂抹的)

94-高斯混合模型(GMM)方法-原理与数据聚类(GMM-EM(高斯混合模型 + 期望最大化)作为经典无监督聚类和概率建模算法不会淘汰,在图像分割、背景建模、数据聚类等场景仍在使用;但作为复杂语义分割方案已被深度学习取代。)

代码整体概述

该代码演示 GMM‑EM 高斯混合模型 + 期望最大化算法 (OpenCV cv::ml::EM)。

  1. 在 500×500 画布上,生成多组服从正态高斯分布的二维随机样本点;
  2. 使用 EM 算法训练高斯混合模型,对样本做聚类;
  3. 遍历画布每一个像素,用训练好的 GMM 模型预测每个像素属于哪一类,绘制分类背景;
  4. 再把原始采样点绘制到画布上,观察聚类划分效果。

⚠️重点区分:

  • KMeans:硬聚类,每个点严格归属于某一类;
  • GMM‑EM:软聚类,基于概率,样本可以属于多个高斯分布,取概率最大作为类别

cpp

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

using namespace cv;
using namespace cv::ml;
using namespace std;

int main(int argc, char** argv) {
	// 创建500×500黑色画布
	Mat img = Mat::zeros(500, 500, CV_8UC3);
	RNG rng(12345); //固定随机种子,每次运行结果复现

	//5套颜色,用于不同聚类类别渲染
	Scalar colorTab[] = {
		Scalar(0, 0, 255),
		Scalar(0, 255, 0),
		Scalar(255, 0, 0),
		Scalar(0, 255, 255),
		Scalar(255, 0, 255)
	};

	//随机生成聚类数目:2~4
	int numCluster = rng.uniform(2, 5);
	printf("number of clusters : %d\n", numCluster);

	//随机总样本点数量5~1000
	int sampleCount = rng.uniform(5, 1000);
	//样本矩阵:sampleCount行,2列,CV_32FC1;每一行 [x,y]二维坐标
	Mat points(sampleCount, 2, CV_32FC1);
	Mat labels; //输出每个样本对应的类别标签

	// ==========生成多组高斯分布随机样本点==========
	for (int k = 0; k < numCluster; k++) {
		Point center;
		center.x = rng.uniform(0, img.cols);
		center.y = rng.uniform(0, img.rows);

		//取出points矩阵的一块行区域,作为当前这一簇的数据存储位置
		Mat pointChunk = points.rowRange(k*sampleCount / numCluster,
			k == numCluster - 1 ? sampleCount : (k + 1)*sampleCount / numCluster);

		//用正态分布填充这一块,均值center,方差img.cols*0.05
		rng.fill(pointChunk, RNG::NORMAL, Scalar(center.x, center.y), Scalar(img.cols*0.05, img.rows*0.05));
	}
	randShuffle(points, 1, &rng); //打乱全部样本点顺序

	// ========== 创建EM(GMM)模型 ==========
	Ptr<EM> em_model = EM::create();
	em_model->setClustersNumber(numCluster);	//设置高斯分量数目(聚类K)
	em_model->setCovarianceMatrixType(EM::COV_MAT_SPHERICAL); //球形协方差
	em_model->setTermCriteria(TermCriteria(TermCriteria::EPS + TermCriteria::COUNT, 100, 0.1));
	em_model->trainEM(points, noArray(), labels, noArray()); //EM训练

	// =========遍历图像每一个像素,用训练好的GMM做预测,画出分类背景色=========
	Mat sample(1, 2, CV_32FC1); //单个待预测样本 [x,y]
	for (int row = 0; row < img.rows; row++) {
		for (int col = 0; col < img.cols; col++) {
			sample.at<float>(0) = (float)col;
			sample.at<float>(1) = (float)row;
			//predict2返回数组:[0]概率,[1]类别编号
			int response = cvRound(em_model->predict2(sample, noArray())[1]);
			Scalar c = colorTab[response];
			circle(img, Point(col, row), 1, c*0.75, -1);
		}
	}

	// =========绘制原始输入样本点=========
	for (int i = 0; i < sampleCount; i++) {
		Point p(cvRound(points.at<float>(i, 0)), cvRound(points.at<float>(i, 1)));
		circle(img, p, 1, colorTab[labels.at<int>(i)], -1);
	}

	imshow("GMM‑EM Demo", img);
	waitKey(0);
	return 0;
}

逐段详细解析

1. 随机数与画布

cpp

复制代码
RNG rng(12345);
int numCluster = rng.uniform(2,5);
  • 种子固定,每次运行生成的随机簇数目、样本点是一样的;
  • numCluster:高斯混合模型分量个数,等价于聚类 K 值,取值 2、3、4。

2. 样本矩阵 points

cpp

复制代码
Mat points(sampleCount,2,CV_32FC1);

每一行代表一个二维点 (x,y)EM 输入要求浮点矩阵,N 行 D 列,D=2 维

3. 生成多簇正态分布样本

cpp

复制代码
Mat pointChunk = points.rowRange(...);
rng.fill(pointChunk, RNG::NORMAL, Scalar(center.x,center.y), Scalar(img.cols*0.05, img.rows*0.05));
randShuffle(points,1,&rng);
  1. rowRange:截取矩阵连续行;每一个簇分配一部分行;
  2. RNG::NORMAL:正态高斯分布填充;每一簇有自己的中心点 center;
  3. randShuffle:打乱全部样本顺序,消除输入顺序带来的影响。

4.EM 模型参数

cpp

复制代码
Ptr<EM> em_model = EM::create();
em_model->setClustersNumber(numCluster);
em_model->setCovarianceMatrixType(EM::COV_MAT_SPHERICAL);
em_model->setTermCriteria(TermCriteria(TermCriteria::EPS + TermCriteria::COUNT,100,0.1));
em_model->trainEM(points, noArray(), labels, noArray());
  1. setClustersNumber(numCluster):设置 K 个高斯分量;
  2. COV_MAT_SPHERICAL:球形协方差,每个高斯分布各个方向方差相同;还有 DIAGONAL、FULL;
  3. TermCriteria:迭代终止条件:最多迭代 100 次,或者变化小于 0.1 停止;
  4. trainEM()执行 EM 训练;输出labels,每个输入样本的类别标签; noArray()代表该参数不需要输出。

EM 算法分两步交替迭代:

  • E 步:估计每个样本属于各个高斯分量的后验概率;
  • M 步:利用概率更新每个高斯的均值、协方差、权重。

5. 逐像素预测,绘制背景分类区域

cpp

复制代码
int response = cvRound(em_model->predict2(sample, noArray())[1]);

predict2()返回一个向量:

  • [0]:该样本最大的概率值
  • [1]:概率最大的类别编号 遍历画布所有像素,每个像素当成一个二维点交给 GMM 预测,用颜色填充,画出决策边界

6. 绘制原始样本点

cpp

复制代码
Point p(cvRound(points.at<float>(i,0)), cvRound(points.at<float>(i,1)));
circle(img, p,1, colorTab[labels.at<int>(i)],‑1);

把训练用的样本点画在画布上,点颜色等于 EM 给出的类别标签。

窗口输出

GMM‑EM Demo

  • 大面积半透明彩色背景:整个平面被 GMM 划分出来的决策区域;
  • 彩色小圆点:生成的原始样本点。 控制台打印:number of clusters实际生成的簇数量。

GMM‑EM 对比 KMeans(高频面试)

表格

KMeans GMM‑EM
聚类类型 硬聚类,点只能属于一类 软聚类,输出属于每一类的概率,取最大作为类别
假设 每个簇是圆形,半径相同 每个簇服从高斯分布,可以椭圆分布(协方差控制形状)
原理 最小化样本到簇中心距离 最大似然估计,高斯混合模型
OpenCV 函数 kmeans() cv::ml::EM
对重叠样本 效果差 重叠样本表现更好

注意:OpenCV 的 EM 模块,新版本已经不推荐继续使用。

简答背诵

该代码演示 GMM 高斯混合模型 EM 期望最大化聚类。生成多组二维正态分布随机样本点,打乱样本;创建 EM 模型,设置高斯分量数量、协方差类型、迭代终止条件,调用 trainEM 完成训练,得到每个样本的类别标签。遍历画布全部像素,使用 predict2 对每个像素点做预测,绘制 GMM 模型的分类决策背景;再绘制原始样本点。GMM‑EM 属于软聚类,输出样本属于每个高斯分布的概率,相比 KMeans 更适合处理存在重叠的数据。
补充:KMeans 只看距离;GMM 看概率分布。

95-高斯混合模型(GMM)方法-图像分割(GMM(高斯混合模型)作为经典概率统计和机器学习工具没有淘汰,在背景建模、数据聚类、GrabCut 底层、图像压缩等场景仍广泛使用;但作为复杂图像语义分割方案已被深度学习取代。)

代码整体概述

该代码实现 基于 GMM‑EM 高斯混合模型的图像颜色分割 。 把图像每一个 RGB 像素当作三维样本 (B,G,R),送入 EM 模型做聚类;聚类完成后,对每一个像素预测所属类别,用预设纯色填充,实现图像分割。

和前面二维点聚类 demo 区别:样本不再是平面坐标 (x,y),而是像素的 BGR 颜色三维特征 。 代码里有一段被注释掉的分支:使用训练输出的 labels 数组;实际运行代码走的是predict2()逐像素重新预测。

cpp

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

using namespace cv;
using namespace cv::ml;
using namespace std;

int main(int argc, char** argv) {
	Mat src = imread("C:/opencv/images/toux.jpg");
	if (src.empty()) {
		printf("could not load iamge...\n");
		return -1;
	}
	char* inputWinTitle = "input image";
	namedWindow(inputWinTitle, WINDOW_AUTOSIZE);
	imshow(inputWinTitle, src);

	// 聚类数目K=3,分成3类颜色区域
	int numCluster = 3;
	//每一类对应的显示颜色
	const Scalar colors[] = {
		Scalar(255, 0, 0),
		Scalar(0, 255, 0),
		Scalar(0, 0, 255),
		Scalar(255, 255, 0)
	};

	int width = src.cols;
	int height = src.rows;
	int dims = src.channels();	//维度=3,BGR三个通道
	int nsamples = width*height; //总样本数 = 图像全部像素数量
	Mat points(nsamples, dims, CV_64FC1); //样本矩阵:N行3列,double浮点
	Mat labels;	//训练输出:每个样本的类别标签
	Mat result = Mat::zeros(src.size(), CV_8UC3); //分割输出图,和原图大小一致

	// =====图像BGR像素转为训练样本points矩阵=====
	int index = 0;
	for (int row = 0; row < height; row++) {
		for (int col = 0; col < width; col++) {
			index = row*width + col;
			Vec3b rgb = src.at<Vec3b>(row, col);
			//把B、G、R三个通道值存入points矩阵一行
			points.at<double>(index, 0) = static_cast<int>(rgb[0]);
			points.at<double>(index, 1) = static_cast<int>(rgb[1]);
			points.at<double>(index, 2) = static_cast<int>(rgb[2]);
		}
	}

	// =====EM(GMM)模型训练=====
	Ptr<EM> em_model = EM::create();
	em_model->setClustersNumber(numCluster);	//设置高斯分量K=3
	em_model->setCovarianceMatrixType(EM::COV_MAT_SPHERICAL); //球形协方差
	em_model->setTermCriteria(TermCriteria(TermCriteria::EPS + TermCriteria::COUNT, 100, 0.1));
	em_model->trainEM(points, noArray(), labels, noArray());

	// =====逐像素预测,生成分割结果=====
	Mat sample(dims, 1, CV_64FC1); //单个待预测样本:3行1列 [B;G;R]
	double time = getTickCount();
	int r = 0, g = 0, b = 0;
	for (int row = 0; row < height; row++) {
		for (int col = 0; col < width; col++) {
			index = row*width + col;

			/* 【注释掉的代码】直接使用训练得到labels数组,不需要再predict2
			int label = labels.at<int>(index, 0);
			Scalar c = colors[label];
			result.at<Vec3b>(row, col)[0] = c[0];
			result.at<Vec3b>(row, col)[1] = c[1];
			result.at<Vec3b>(row, col)[2] = c[2];*/

			//实际运行代码:取出原图BGR,构造sample,调用predict2预测类别
			b = src.at<Vec3b>(row, col)[0];
			g = src.at<Vec3b>(row, col)[1];
			r = src.at<Vec3b>(row, col)[2];
			sample.at<double>(0) = b;
			sample.at<double>(1) = g;
			sample.at<double>(2) = r;
			int response = cvRound(em_model->predict2(sample, noArray())[1]);
			Scalar c = colors[response];

			result.at<Vec3b>(row, col)[0] = c[0];
			result.at<Vec3b>(row, col)[1] = c[1];
			result.at<Vec3b>(row, col)[2] = c[2];
		}
	}
	//计算耗时,毫秒
	printf("execution time(ms) : %.2f\n", (getTickCount() - time)/getTickFrequency()*1000);
	imshow("EM‑Segmentation", result);

	waitKey(0);
	return 0;
}

逐段详细解析

1. 样本矩阵 points

cpp

复制代码
Mat points(nsamples, dims, CV_64FC1);
  • nsamples = width*height:每一个像素是 1 个样本;
  • dims=3:特征维度是 BGR 三个颜色通道;
  • 格式:N 行 × 3 列,CV_64FC1 double 类型,EM 训练输入要求。

每一行:[B值, G值, R值]

2. 像素循环,填充 points

cpp

复制代码
index = row*width + col;
Vec3b rgb = src.at<Vec3b>(row, col);
points.at<double>(index,0)=rgb[0];
points.at<double>(index,1)=rgb[1];
points.at<double>(index,2)=rgb[2];

把二维图像(row,col)展平为一维索引,把 BGR 像素值存入样本矩阵。

3.EM 模型参数训练

cpp

复制代码
Ptr<EM> em_model = EM::create();
em_model->setClustersNumber(numCluster);
em_model->setCovarianceMatrixType(EM::COV_MAT_SPHERICAL);
em_model->setTermCriteria(TermCriteria(TermCriteria::EPS + TermCriteria::COUNT,100,0.1));
em_model->trainEM(points, noArray(), labels, noArray());
  • setClustersNumber(3):设置 3 个高斯分量,图像被分成 3 类颜色;
  • COV_MAT_SPHERICAL球形协方差;
  • trainEM()执行 EM 迭代训练;输出labels:每个输入样本对应的类别编号。

4. 两段实现方式对比(重点考点)

方式 A(被注释):直接拿训练输出 labels 数组

cpp

复制代码
int label = labels.at<int>(index,0);

训练的时候已经全部样本算好标签,直接读取即可,速度快

方式 B(代码实际运行):循环每一个像素调用predict2()重新预测

cpp

复制代码
int response = cvRound(em_model->predict2(sample, noArray())[1]);

把每个像素颜色重新送入模型做预测。

⚠️缺点:整张图全部像素重新预测,速度很慢 ;优点:这套写法可以用于新的未知样本推理。 本场景完全可以打开注释,删掉 predict2 那套循环,提升运行速度。

5.predict2 返回值

predict2(sample, noArray())返回向量:

  • [0]最大后验概率
  • [1]最大概率对应的类别编号

6. 计时

cpp

复制代码
(getTickCount()‑time)/getTickFrequency()*1000

把 tick 计数换算为毫秒,打印预测阶段消耗时间。

窗口输出

  1. input image:原始输入图片
  2. EM‑Segmentation:EM 颜色聚类分割结果,图像被简化为 3 种纯色; 控制台打印预测阶段耗时(ms)。

EM (GMM) 图像分割 vs KMeans 图像分割

表格

KMeans EM(GMM)
聚类性质 硬聚类,距离判断 软聚类,概率模型
输入样本 像素 BGR 特征 像素 BGR 特征
OpenCV 接口 kmeans() cv::ml::EM
输出 每个样本类别标签 每个样本属于各个高斯的概率 + 类别标签

共同点:只使用颜色信息,没有利用空间位置,相同颜色像素归为一类。

代码存在问题

  1. 全部像素调用predict2()做预测,计算量大,运行慢;直接使用labels数组效率高很多。
  2. 拼写错误:could not load iamge拼写错误。
  3. EM 在新版 OpenCV 已经标记废弃,新项目不推荐使用。

简答背诵

该代码实现基于 EM 高斯混合模型 GMM 的图像颜色分割。读取图像,把图像每一个 BGR 像素当作三维样本,将全部像素展平构造成 N 行 3 列 double 样本矩阵 points;设置聚类数目 K=3,配置 EM 模型参数并执行 trainEM 训练。代码提供两种获取类别标签的方式:一种直接读取训练输出 labels 数组(注释);另一种遍历每个像素,构造 BGR 样本调用 predict2 做预测,使用类别对应的纯色填充输出图像,完成颜色聚类分割,打印预测耗时。GMM‑EM 属于概率软聚类;该算法只利用颜色特征,不利用像素空间位置。
拓展:Kmeans、EM 都属于无监督聚类图像分割;不是语义分割,没有物体含义,只做颜色归并。

101-Grabcut原理与演示应用-代码演示(GrabCut 作为交互式图像分割工具没有淘汰,仍在抠图、医学图像、无训练数据场景使用;但批量自动分割已被 SAM、U-Net 等深度学习方法取代。)

代码整体概述

该代码实现 OpenCV GrabCut 交互式图像分割(前景背景抠图)。 交互流程:

  1. 鼠标在图片上画矩形框,框住想要抠出的目标物体;
  2. 按下键盘 n 执行 GrabCut 算法迭代分割;
  3. 矩形内部标记为可能前景 GC_PR_FGD,外部是确定背景 GC_BGD;
  4. 算法迭代估算前景、背景高斯混合模型,输出 mask 掩码,把前景抠出来;
  5. ESC 退出。

GrabCut:结合颜色 GMM + 边界能量,属于图割算法,专门做物体抠图;相比 Kmeans/EM 只靠颜色,GrabCut 会考虑空间邻接关系,分割物体轮廓效果更好。

cpp

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

using namespace cv;
using namespace std;

int numRun = 0;       //记录GrabCut迭代执行次数
Rect rect;            //鼠标绘制的ROI矩形框
bool init = false;    //是否已经完成GrabCut初始化
Mat src, image;
Mat mask, bgModel, fgModel; //mask掩码;bgModel背景模型、fgModel前景模型(内部GMM)
const char* winTitle = "input image";

void onMouse(int event, int x, int y, int flags, void* param);
void setROIMask();
void showImage();
void runGrabCut();

int main(int argc, char** argv) {
	src = imread("C:/opencv/images/flower.png", 1);
	if (src.empty()) {
		printf("could not load image...\n");
		return -1;
	}
	//mask掩码图,全部初始化为确定背景 GC_BGD(0)
	mask.create(src.size(), CV_8UC1);
	mask.setTo(Scalar::all(GC_BGD));

	namedWindow(winTitle, WINDOW_AUTOSIZE);
	setMouseCallback(winTitle, onMouse, 0); //注册鼠标回调函数
	imshow(winTitle, src);

	while (true) {
		char c = (char)waitKey(0);
		if (c == 'n') {
			runGrabCut();
			numRun++;
			showImage();
			printf("current iteative times : %d\n", numRun);
		}
		if ((int)c == 27) { //ESC退出
			break;
		}
	}

	waitKey(0);
	return 0;
}

// 显示结果:根据mask把前景拷贝出来,绘制红色矩形框
void showImage() {
	Mat result, binMask;
	binMask.create(mask.size(), CV_8UC1);
	// mask & 1:GC_FGD(1)、GC_PR_FGD(3)都会变成1;GC_BGD(0)、GC_PR_BGD(2)变成0
	binMask = mask & 1;
	if (init) {
		//使用binMask作为掩码,只拷贝前景像素
		src.copyTo(result, binMask);
	} else {
		src.copyTo(result);
	}
	rectangle(result, rect, Scalar(0, 0, 255), 2, 8); //画红色选择框
	imshow(winTitle, result);
}

// 根据鼠标矩形设置mask:框外=确定背景;框内=可能前景 GC_PR_FGD
void setROIMask() {
	// GrabCut mask四个常量
	// GC_BGD    = 0  确定背景
	// GC_FGD    = 1  确定前景
	// GC_PR_BGD = 2  可能背景
	// GC_PR_FGD = 3  可能前景

	mask.setTo(GC_BGD); //整张图全部置确定背景
	//边界保护,防止矩形越出图片范围
	rect.x = max(0, rect.x);
	rect.y = max(0, rect.y);
	rect.width = min(rect.width, src.cols - rect.x);
	rect.height = min(rect.height, src.rows - rect.y);
	mask(rect).setTo(Scalar(GC_PR_FGD)); //矩形区域设置为【可能前景】
}

//鼠标回调:鼠标拖拽画矩形框
void onMouse(int event, int x, int y, int flags, void* param) {
	switch (event)
	{
	case EVENT_LBUTTONDOWN: //鼠标左键按下,记录起点
		rect.x = x;
		rect.y = y;
		rect.width = 1;
		rect.height = 1;
		init = false;
		numRun = 0;
		break;
	case EVENT_MOUSEMOVE: //鼠标拖动,动态更新矩形
		if (flags & EVENT_FLAG_LBUTTON) {
			rect = Rect(Point(rect.x, rect.y), Point(x, y));
			showImage();
		}
		break;
	case EVENT_LBUTTONUP: //左键松开,完成选框,设置mask
		if (rect.width > 1 && rect.height > 1) {
			setROIMask();
			showImage();
		}
		break;
	default:
		break;
	}
}

//执行grabCut分割
void runGrabCut() {
	if (rect.width < 2 || rect.height < 2) {
		return;
	}
	//代码小bug:此处少else,语法等价 if(init){...} ; { ... }
	if (init) {
		//已经初始化过,直接迭代优化,不重新初始化GMM
		grabCut(src, mask, rect, bgModel, fgModel, 1);
	} 
    {
		//第一次运行:GC_INIT_WITH_RECT 使用矩形初始化GrabCut
		grabCut(src, mask, rect, bgModel, fgModel, 1, GC_INIT_WITH_RECT);
		init = true;
	}
}

Grab‑Cut mask 四个值(必背)

表格

常量 数值 含义
GC_BGD 0 确定背景
GC_FGD 1 确定前景
GC_PR_BGD 2 可能背景
GC_PR_FGD 3 可能前景

binMask = mask & 1

  • 1 & 1 =13 &1 =1 → 前景(确定前景、可能前景都保留)
  • 0 &1 =02 &1 =0 → 背景屏蔽。

逐段解析

1. 全局变量

  • mask:单通道掩码图,存储每个像素属于上面 4 种状态;
  • bgModelfgModel内部 GMM 模型缓冲区,不要手动读写,grabCut 内部维护
  • init标记是否已经完成 GrabCut 初始化。

2. 鼠标回调 onMouse()

  • 左键按下:记录矩形起点;重置 init、迭代次数;
  • 鼠标移动 + 左键按住:动态拖拽绘制矩形,实时刷新画面;
  • 左键松开:调用setROIMask()给 mask 赋值。

3.setROIMask()

整张 mask 全部设置为确定背景GC_BGD; 鼠标框选的矩形 ROI 区域设置为GC_PR_FGD(可能前景); 同时做边界保护,防止矩形超出图片边界。

4.showImage()

cpp

复制代码
binMask = mask & 1;
src.copyTo(result, binMask);

copyTo(输出图, 掩码):掩码为 1 的像素才拷贝原图像素,掩码 0 像素置黑,实现抠图。 绘制红色矩形框,刷新窗口显示。

5.runGrabCut () 核心函数 grabCut()

cpp

复制代码
grabCut(src, mask, rect, bgModel, fgModel, iterCount, mode);

参数说明:

  1. src:输入原图;
  2. mask:输入输出掩码,算法会修改 mask;
  3. rect:用户框选矩形;
  4. bgModel/fgModel:前景背景 GMM 模型缓冲区;
  5. iterCount:迭代次数,一般 1~5;
  6. mode
    • GC_INIT_WITH_RECT用矩形初始化模型,只第一次调用使用
    • 不传 mode:直接在现有 mask、model 基础上迭代优化。

⚠️本代码语法 bug:

cpp

复制代码
if (init) {
	grabCut(src, mask, rect, bgModel, fgModel, 1);
} {
	grabCut(src, mask, rect, bgModel, fgModel, 1, GC_INIT_WITH_RECT);
	init = true;
}

第二个大括号没有else无论 init 是 true 还是 false,后面一段代码永远执行 。属于代码笔误,正确应该是 else { ... }

6.main 主循环

等待按键:

  • n:执行一次 GrabCut 迭代,更新分割结果;
  • ESC(27):退出程序。

窗口输出

input image窗口:

  • 红色矩形框:鼠标拖拽选择的目标区域;
  • 抠图效果:背景变黑,保留框选物体前景。 控制台打印当前迭代次数。

GrabCut 对比 KMeans / EM 分割

表格

KMeans / EM 颜色聚类 GrabCut
信息 只使用像素 BGR 颜色 颜色 GMM + 图像空间邻接、边界信息(图割)
类型 无监督聚类 交互式分割,需要用户给框 / 标记
输出 全部像素归为几类纯色 抠出物体前景,保留物体原始纹理颜色
用途 颜色简化、聚类 物体抠图、前景背景分割

关键点:Kmeans、EM 把像素改成纯色;GrabCut 保留物体原本像素颜色,把背景去除。

简答背诵

该代码演示 OpenCV GrabCut 交互式前景分割抠图。注册鼠标回调,鼠标拖拽绘制目标矩形框;框外 mask 标记为确定背景 GC_BGD,矩形内部标记为可能前景 GC_PR_FGD。按下键盘 n 调用 grabCut 执行分割;第一次使用 GC_INIT_WITH_RECT 根据矩形初始化前景背景 GMM 模型;后续迭代在已有模型基础上优化。利用mask & 1生成二值掩码,copyTo根据掩码把前景拷贝出来,实现抠图效果,ESC 退出。GrabCut 结合高斯混合模型与图割,同时利用颜色和空间邻域信息,适合物体抠图;注意代码存在语法 bug,if 后面缺少 else。
拓展:GrabCut 需要用户交互框选;属于交互式分割,不是全自动分割。

103-案例实战一证件照背景替换(K-Means 颜色聚类没有被淘汰,在颜色量化、图像压缩、简单分割、主色提取等场景中仍是高效常用的方法;复杂语义分割已被深度学习取代。)

代码整体概述

该示例实现 基于 K‑Means 颜色聚类的简易背景替换

  1. 将图像全部 BGR 像素转为样本矩阵,做 KMeans 聚类(K=4);
  2. 拿图像左上角位置的类别当作背景类别,生成二值 mask 掩码;
  3. 对 mask 做腐蚀形态学操作 + 高斯模糊,得到软边缘(0‑255 过渡,不是只有 0 和 255);
  4. 遍历图像:前景保留原图像素;背景替换为指定紫色;模糊过渡区域做alpha 线性混合,消除硬锯齿边缘,输出背景替换效果图。

对比 GrabCut:本方案全自动,不需要鼠标画框;只依靠颜色聚类;缺点:如果前景和背景颜色接近,分割会出错。

cpp

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

using namespace cv;
using namespace std;

Mat mat_to_samples(Mat &image);
int main(int argc, char** argv) {
	Mat src = imread("C:/opencv/images/toux.jpg");
	if (src.empty()) {
		printf("could not load image...\n");
		return -1;
	}
	namedWindow("input image", WINDOW_AUTOSIZE);
	imshow("input image", src);

	// 把图像像素组装成KMeans输入样本矩阵
	Mat points = mat_to_samples(src);

	// 运行KMeans聚类 K=4
	int numCluster = 4;
	Mat labels;     //每个样本(像素)对应的类别编号
	Mat centers;    //4个聚类中心颜色
	TermCriteria criteria = TermCriteria(TermCriteria::EPS + TermCriteria::COUNT, 10, 0.1);
	kmeans(points, numCluster, labels, criteria, 3, KMEANS_PP_CENTERS, centers);

	// 生成mask掩码:选取图像左上角点的类别作为背景类别
	Mat mask=Mat::zeros(src.size(), CV_8UC1);
	int index = src.rows*2 + 2;
	int cindex = labels.at<int>(index, 0); //背景类别编号
	int height = src.rows;
	int width = src.cols;

	for (int row = 0; row < height; row++) {
		for (int col = 0; col < width; col++) {
			index = row*width + col;
			int label = labels.at<int>(index, 0);
			if (label == cindex) { 
				mask.at<uchar>(row, col) = 0;  //背景置0
			} else {
				mask.at<uchar>(row, col) = 255; //前景置255
			}
		}
	}

	// 腐蚀 + 高斯模糊,生成软边缘mask
	Mat k = getStructuringElement(MORPH_RECT, Size(3, 3), Point(-1, -1));
	erode(mask, mask, k);
	GaussianBlur(mask, mask, Size(3, 3), 0, 0);

	//设置替换的背景颜色(固定紫色)
	RNG rng(12345);
	Vec3b color;
	color[0] = 217;
	color[1] = 60;
	color[2] = 160;
	Mat result(src.size(), src.type());

	double w = 0.0;
	int b = 0, g = 0, r = 0;
	int b1 = 0, g1 = 0, r1 = 0;
	int b2 = 0, g2 = 0, r2 = 0;

	// 根据mask做融合:前景、背景、过渡混合
	for (int row = 0; row < height; row++) {
		for (int col = 0; col < width; col++) {
			int m = mask.at<uchar>(row, col);
			if (m == 255) {
				result.at<Vec3b>(row, col) = src.at<Vec3b>(row, col); //完全前景,原图
			}
			else if (m == 0) {
				result.at<Vec3b>(row, col) = color; //完全背景,替换紫色
			} 
			else {
				//边缘过渡区域 alpha混合
				w = m / 255.0;
				b1 = src.at<Vec3b>(row, col)[0];
				g1 = src.at<Vec3b>(row, col)[1];
				r1 = src.at<Vec3b>(row, col)[2];

				b2 = color[0];
				g2 = color[1];
				r2 = color[2];

				b = b1*w + b2*(1.0 - w);
				g = g1*w + g2*(1.0 - w);
				r = r1*w + r2*(1.0 - w);

				result.at<Vec3b>(row, col)[0] = b;
				result.at<Vec3b>(row, col)[1] = g;
				result.at<Vec3b>(row, col)[2] = r;
			}
		}
	}
	imshow("背景替换", result);

	waitKey(0);
	return 0;
}

/**
 * mat_to_samples:把BGR图像转为KMeans输入样本矩阵
 * 输出:N行×3列 CV_32FC1,每一行 [B,G,R]
 */
Mat mat_to_samples(Mat &image) {
	int w = image.cols;
	int h = image.rows;
	int samplecount = w*h;
	int dims = image.channels();
	Mat points(samplecount, dims, CV_32F, Scalar(10));

	int index = 0;
	for (int row = 0; row < h; row++) {
		for (int col = 0; col < w; col++) {
			index = row*w + col;
			Vec3b bgr = image.at<Vec3b>(row, col);
			points.at<float>(index, 0) = static_cast<int>(bgr[0]);
			points.at<float>(index, 1) = static_cast<int>(bgr[1]);
			points.at<float>(index, 2) = static_cast<int>(bgr[2]);
		}
	}
	return points;
}

逐段详细解析

1. mat_to_samples()函数

把二维图像展开成样本矩阵

  • 样本总数 = 图像总像素 w*h
  • 维度 dims=3,BGR 三个通道;
  • 输出矩阵:samplecount × 3CV_32F浮点,KMeans 输入要求。 每一行:[B值, G值, R值]

2.KMeans 聚类调用

cpp

复制代码
kmeans(points, numCluster, labels, criteria, 3, KMEANS_PP_CENTERS, centers);
  • numCluster=4:把全部像素颜色分成 4 类;
  • labels:一维数组,每个像素对应类别编号 0‑3;
  • KMEANS_PP_CENTERS:Kmeans++ 初始化聚类中心,效果优于随机初始化。

3. 生成 mask 掩码

cpp

复制代码
int index = src.rows*2 + 2;
int cindex = labels.at<int>(index, 0);

假设图像左上角附近是背景,取该像素的类别作为背景类别 cindex

  • 像素 label 等于 cindex → mask=0(背景)
  • 其他 label → mask=255(前景)

⚠️缺陷:如果左上角不是背景,整个 mask 就完全错误。

4. 形态学腐蚀 + 高斯模糊

cpp

复制代码
erode(mask, mask, k);
GaussianBlur(mask, mask, Size(3,3),0,0);
  1. erode腐蚀:收缩前景区域,消除小噪点孔洞;
  2. 高斯模糊:把原本只有 0/255 二值 mask 变成 0~255 连续灰度,得到软边缘,用于 alpha 混合。

mask 不再只有 0 和 255,中间会出现 1‑254 的过渡灰度,这是做抗锯齿的关键。

5. 三分支像素融合逻辑

cpp

复制代码
int m = mask.at<uchar>(row, col);
  1. m == 255:完全前景,直接复制原图 BGR;
  2. m == 0:完全背景,替换成自定义紫色;
  3. 0 < m < 255:边缘过渡区域,做线性 alpha 混合: \(out = src \cdot w + color \cdot (1-w),\quad w=\frac{m}{255.0}\) w 为原图权重;1‑w为替换背景颜色权重,实现边缘柔和过渡,没有生硬锯齿。

窗口输出

  1. input image:原始输入图片
  2. 背景替换:替换背景之后输出图,物体边缘柔和。

核心优缺点

✅优点:

  • 全自动,不需要用户交互框选;
  • 通过高斯模糊 + alpha 混合,得到柔和物体边缘。

❌缺点:

  1. 强假设:左上角像素属于背景。如果左上角是前景物体,分割完全失效;
  2. 只依靠颜色聚类,前景和背景颜色相近时分割失败
  3. K 值需要手动设置 (numCluster=4)。

对比 GrabCut

表格

项目 KMeans 实现背景替换 GrabCut
交互 全自动,无需鼠标 交互式,需要画框
依赖 只依靠像素颜色 颜色 GMM + 空间图割邻域信息
边缘 依靠模糊做软边缘 算法内部输出边缘
缺点 前景背景颜色接近就失效 需要用户交互操作

简答背诵

该代码实现基于 K‑Means 颜色聚类的全自动背景替换。自定义函数mat_to_samples将图像 BGR 像素展开为 N 行 3 列浮点样本矩阵;执行 KMeans 聚类 K=4,得到每个像素类别标签 labels。选取图像左上角像素类别作为背景类别,生成 mask 掩码;对 mask 执行腐蚀去除噪点,再高斯模糊得到带有过渡灰度的软掩码。遍历图像,mask=255 保留原图前景;mask=0 替换为自定义背景色;中间过渡灰度做 alpha 线性混合消除锯齿,输出背景替换结果。该算法完全基于颜色,无需交互,但要求背景颜色和前景差异明显,且图像角落区域必须是背景。

105-案例实战一绿幕背景视频抠图(HSV 颜色阈值绿幕抠图在简单场景、实时应用、教学实验中仍广泛使用;但专业影视 / 直播场景中,已被更精细的算法和深度学习抠图方案补充或替代。)


HSV 颜色空间绿幕抠图:仍在使用,专业场景有更优方案

一句话结论

HSV 颜色阈值绿幕抠图在简单场景、实时应用、教学实验中仍广泛使用;但专业影视 / 直播场景中,已被更精细的算法和深度学习抠图方案补充或替代

还在用的场景

  1. 教学实验:OpenCV 入门经典案例,理解 HSV 颜色空间、inRange 阈值、掩码生成、图像合成的完整流程。
  2. 简单实时应用:直播软件的简易抠图、视频会议虚拟背景(部分实现)、嵌入式实时抠图。
  3. 绿幕条件好的场景:绿幕均匀、光照稳定、无阴影、主体不含绿色,HSV 阈值法效果好且速度极快。
  4. 快速原型验证:项目初期快速验证抠图思路,不需要训练模型。
  5. 资源受限设备:纯 CPU、微控制器,几行代码就能跑。

HSV 绿幕抠图的局限性

  1. 对绿幕质量要求高:绿幕颜色不均、有褶皱、光照阴影都会导致抠图不干净。
  2. 主体含绿色时失效:人物穿绿色衣服、物体有绿色部分,会被一起抠掉。
  3. 边缘处理粗糙:头发丝、半透明区域(婚纱、玻璃杯)处理不好,有锯齿或绿边。
  4. 需要手动调阈值:不同绿幕、不同光照都要重新调 HSV 上下限。
  5. 光照变化敏感:光照变化导致绿色偏移,阈值失效。

专业场景的更优方案

  1. 专业软件算法:OBS、Premiere、After Effects 的色度键(Chroma Key),支持溢出抑制、边缘羽化、细节处理。
  2. 深度学习抠图:MODNet、BiRefNet、RMBG 等模型,不需要绿幕,自然场景也能抠图,边缘精细。
  3. 传统进阶方法:颜色差异法、泊松克隆、alpha matting(闭形式抠图),处理半透明边缘。

HSV 绿幕抠图典型流程(必背)

复制代码
BGR图像 → 转HSV → inRange绿色阈值 → 形态学去噪(开运算/闭运算) 
→ 生成掩码 → 反掩码 → 主体与新背景按掩码合成

对比表

表格

抠图方法 适用场景 效果 算力
HSV 颜色阈值 绿幕均匀、简单场景 中(边缘粗糙) 极低
专业 Chroma Key 影视 / 直播 高(边缘精细)
深度学习抠图 (MODNet/BiRefNet) 任意背景 极高(头发丝级)
Alpha Matting 半透明边缘 极高 中高

简答背诵

HSV 颜色空间绿幕抠图通过 inRange 阈值分割绿色背景、生成掩码、与新背景合成实现。在绿幕均匀、光照稳定的简单场景和教学实验、实时应用中仍广泛使用,优点是简单、速度快、无需训练;但对绿幕质量要求高,主体含绿色、光照变化、头发丝等半透明边缘处理效果有限。专业影视 / 直播场景中,已被更精细的色度键算法和深度学习抠图方案(如 MODNet、BiRefNet)补充或替代。

核心考点

  • HSV 优于 RGB 做颜色分割:Hue(色调)通道对光照变化更鲁棒,单独控制颜色类别。
  • 绿色选择原因:人皮肤中蓝色 / 红色成分多,绿色最少,且绿色传感器在 CMOS 中占比最高,信噪比好。
  • 溢出抑制(Spill Suppression):去除主体边缘的绿色反光,专业抠图必备步骤。
  • 形态学操作:开运算去除小噪点,闭运算填充内部空洞。

代码整体概述

该代码实现HSV 颜色空间绿幕抠图(视频版),属于色度键抠图。

  1. 读取视频帧,BGR 转 HSV;使用inRange把绿色背景提取为 mask 掩码;
  2. 形态学闭运算、腐蚀、高斯模糊,修复 mask 孔洞,生成带 0‑255 过渡的软掩码;
  3. replace_and_blend函数:mask=255 使用替换背景图;mask=0 保留视频前景人物;中间灰度做 Alpha 加权混合,消除硬锯齿;
  4. 逐帧处理视频,实时输出抠图结果,ESC 退出。

核心原理:绿幕是特定绿色,在 HSV 空间筛选绿色得到背景掩码,完成背景替换。

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

using namespace cv;
using namespace std;

Mat replace_and_blend(Mat &frame, Mat &mask);
Mat background_01;
Mat background_02;

int main(int argc, char** argv) {
	// 读取两张备选背景图片
	background_01 = imread("C:/opencv/images/bg_01.jpg");
	if (background_01.empty()) {
		printf("could not load background_01 image...\n");
		return -1;
	}
	background_02 = imread("C:/opencv/images/bg_02.jpg");
	if (background_02.empty()) {
		printf("could not load background_02 image...\n");
		return -1;
	}

	// 打开视频文件
	VideoCapture capture;
	capture.open("C:/opencv/images/01.mp4");
	if (!capture.isOpened()) {
		printf("could not find the video file...\n");
		return -1;
	}

	char* title = "input video";
	char* resultWin = "result video";
	namedWindow(title, WINDOW_AUTOSIZE);
	namedWindow(resultWin, WINDOW_AUTOSIZE);

	Mat frame, hsv, mask;
	while (capture.read(frame))
	{
		// BGR转HSV颜色空间
		cvtColor(frame, hsv, COLOR_BGR2HSV);
		// 绿幕颜色阈值筛选,生成mask:绿色区域=255(背景),其余=0(前景)
		inRange(hsv, Scalar(35, 43, 46), Scalar(155, 255, 255), mask);

		// 形态学操作,修复mask噪声、孔洞
		Mat k = getStructuringElement(MORPH_RECT, Size(3, 3), Point(-1, -1));
		morphologyEx(mask, mask, MORPH_CLOSE, k); //闭运算:先膨胀后腐蚀,填充小孔洞
		erode(mask, mask, k);                     //腐蚀,收缩背景掩码
		GaussianBlur(mask, mask, Size(3, 3), 0, 0);//高斯模糊,生成软边缘过渡

		// 执行替换与alpha混合
		Mat result = replace_and_blend(frame, mask);

		char c = waitKey(1);
		if (c == 27) { //ESC退出
			break;
		}
		imshow(resultWin, result);
		imshow(title, frame);
	}

	waitKey(0);
	return 0;
}

/**
 * @brief 根据mask完成前景背景替换与alpha融合
 * mask=255:使用新背景;mask=0:保留原视频前景;中间灰度做加权混合
 */
Mat replace_and_blend(Mat &frame, Mat &mask) {
	if (frame.empty() || mask.empty()) {
		printf("frame or mask is empty in replace_and_blend\n");
		return Mat();
	}
	if (background_02.empty()) {
		printf("background_02 is empty, cannot blend\n");
		return Mat::zeros(frame.size(), frame.type());
	}

	// 如果背景图尺寸和视频帧不一致,自动缩放背景图和帧对齐
	if (background_02.size() != frame.size()) {
		Mat resizedBg;
		resize(background_02, resizedBg, frame.size());
		background_02 = resizedBg;
	}

	Mat result = Mat::zeros(frame.size(), frame.type());
	int h = frame.rows;
	int w = frame.cols;

	int m = 0;
	double wt = 0;
	int r = 0, g = 0, b = 0;
	int r1 = 0, g1 = 0, b1 = 0;
	int r2 = 0, g2 = 0, b2 = 0;

	// 使用ptr指针访问,速度优于at<>
	for (int row = 0; row < h; row++) {
		uchar* current = frame.ptr<uchar>(row);     //原视频帧行指针
		uchar* bgrow = background_02.ptr<uchar>(row); //替换背景行指针
		uchar* maskrow = mask.ptr<uchar>(row);      //掩码行指针
		uchar* targetrow = result.ptr<uchar>(row); //输出结果行指针

		for (int col = 0; col < w; col++) {
			m = *maskrow++;
			if (m == 255) {
				// mask=255:背景,使用替换背景图片
				*targetrow++ = *bgrow++;
				*targetrow++ = *bgrow++;
				*targetrow++ = *bgrow++;
				current += 3;
			}
			else if (m == 0) {
				// mask=0:前景,保留原始视频像素
				*targetrow++ = *current++;
				*targetrow++ = *current++;
				*targetrow++ = *current++;
				bgrow += 3;
			}
			else {
				// 边缘过渡区域,alpha加权混合
				b1 = *bgrow++;
				g1 = *bgrow++;
				r1 = *bgrow++;

				b2 = *current++;
				g2 = *current++;
				r2 = *current++;

				wt = m / 255.0;
				// 混合公式:输出 = bg*wt + frame*(1‑wt)
				b = b1*wt + b2*(1.0 - wt);
				g = g1*wt + g2*(1.0 - wt);
				r = r1*wt + r2*(1.0 - wt);

				*targetrow++ = b;
				*targetrow++ = g;
				*targetrow++ = r;
			}
		}
	}
	return result;
}

逐段详细解析

1. 全局变量

  • background_01background_02:两张备选替换背景图,本代码实际使用background_02

2. 视频读取与颜色阈值抠图

cpp

复制代码
cvtColor(frame, hsv, COLOR_BGR2HSV);
inRange(hsv, Scalar(35, 43, 46), Scalar(155, 255, 255), mask);
  • BGR 转 HSV:做颜色筛选优先使用 HSV,色相 H 独立于亮度,抗光照变化比 BGR 好;
  • inRange在这个 HSV 区间内的像素置 255(代表绿幕背景),其余置 0(前景人物)

⚠️关键点:mask含义和前面 Kmeans 示例相反: 本代码:mask=255 → 背景(要替换)mask=0 → 前景(保留)

3. 形态学处理 + 高斯模糊

cpp

复制代码
morphologyEx(mask, mask, MORPH_CLOSE, k);
erode(mask, mask, k);
GaussianBlur(mask, mask, Size(3,3),0,0);
  1. MORPH_CLOSE 闭运算:填充 mask 内部细小孔洞,消除人物内部误识别为背景的小黑点;
  2. erode 腐蚀:收缩背景掩码,防止人物边缘残留绿色;
  3. 高斯模糊:mask 不再只有 0/255,产生 0‑255 灰度过渡,用于边缘 alpha 混合,消除锯齿。

4.replace_and_blend 函数

  1. 尺寸自适应:background_02图片尺寸不一定和视频帧一样,resize自动对齐分辨率;
  2. 使用ptr<uchar>指针遍历像素,相比at<>()速度更快,视频处理优先用指针;
  3. 三分支逻辑:
    • m == 255:背景,直接取新背景图像素;
    • m == 0:前景,保留原视频帧像素;
    • 0<m<255:边缘过渡区域,加权混合: \(pixel_{out}=bg\cdot wt + frame\cdot(1-wt),\quad wt=\frac{m}{255.0}\)

5. 主循环

capture.read(frame)循环读取视频每一帧;waitKey(1)视频流式等待,间隔 1ms;按 ESC 退出。

窗口输出

  1. input video:原始输入视频;
  2. result video:绿幕抠图替换背景后的输出视频。

关键知识点对比

表格

项目 本绿幕抠图 Kmeans 背景替换 GrabCut 分割
实现原理 HSV 颜色阈值(色度键) 像素颜色聚类 图割 + GMM
交互 全自动 全自动 需要鼠标框选
适用场景 纯色绿幕 / 蓝幕视频 静态图片,背景颜色统一 任意物体抠图
局限 物体不能含有和背景相近的绿色 前景背景颜色接近会出错 需要人工交互

代码存在问题

  1. HSV 阈值是写死的;实际不同光照下绿幕 H/S/V 会偏移,固定阈值效果不稳定;
  2. 全局变量background_02在函数内部被 resize 修改,会改变原始图片;
  3. 没有处理绿幕反光、物体半透明区域(头发)的高级去绿溢出处理。

简答背诵

该代码实现基于 HSV 颜色阈值的视频绿幕抠图。读取视频帧,BGR 图像转换 HSV 空间;使用inRange筛选绿色背景生成 mask 掩码;通过闭运算填充孔洞、腐蚀消除噪点,高斯模糊生成软边缘掩码。自定义replace_and_blend函数,使用指针快速遍历像素:mask=255 使用替换背景图片;mask=0 保留原视频前景;过渡灰度区域执行 alpha 加权混合,消除锯齿。自动将背景图缩放至和视频帧相同尺寸,逐帧输出抠图结果,ESC 按键退出。该算法属于色度键抠图,适合纯色幕布;缺点是阈值固定,光照变化会影响抠图质量。
补充:mask 含义注意区分:本代码 mask=255 代表背景,和之前 GrabCut、Kmeans 示例 mask 语义相反。

2231

110-背景消除建模(BSM)-02(重要补充代码)

BSM(Background Subtraction Method,背景减除方法):仍在广泛使用

一句话结论

BSM(背景减除 / 背景建模)没有被淘汰,在静态摄像头运动检测场景中仍是基础且常用的方法;复杂场景下常与深度学习结合使用

BSM = Background Subtraction Model/Method,是一类方法的统称,包括 MOG、MOG2、KNN、GMG、LSBP 等具体算法。你之前问的 MOG2 就是 BSM 的一种。

还在用的场景

  1. 静态摄像头运动检测:固定摄像头下检测移动物体(人、车、异物),是最经典的方案。
  2. 资源受限设备:纯 CPU、嵌入式,不需要深度学习推理,速度快、开销小。
  3. 简单监控 / 工业场景:室内固定摄像头、流水线异物检测、入侵检测、越界检测。
  4. 作为预处理模块:先用 BSM 快速提取运动候选区域,再用 YOLO 做精细分类,兼顾速度和精度。
  5. 教学实验:背景减除是计算机视觉运动分析的基础内容。

BSM 的优势

  1. 无需训练数据:自动学习背景,不需要标注。
  2. 计算量小:CPU 实时,适合嵌入式。
  3. 对任意类别有效:只要在动就能检测,不局限于训练过的物体类别。
  4. OpenCV 内置多种算法createBackgroundSubtractorMOG2()createBackgroundSubtractorKNN() 等直接调用。

局限性

  1. 摄像头必须静止:摄像头晃动直接失效。
  2. 光照突变敏感:突然开灯、阳光变化产生大量误检。
  3. 动态背景干扰:树叶晃动、水波、雨体会被误判为前景。
  4. 目标静止后消失:物体静止一段时间会被学习成背景。
  5. 无类别识别:只知道 "有东西在动",不知道是什么。

常见 BSM 算法对比

表格

算法 OpenCV 支持 特点
MOG2 混合高斯,支持阴影检测,最常用
KNN K 近邻,对复杂背景抗干扰更强
GMG 像素级贝叶斯,适合静态背景
LSBP contrib 局部纹理特征,对光照变化更鲁棒

工业项目中的实际用法

  1. 简单场景直接用 BSM:固定摄像头、背景稳定,足够用。
  2. BSM + 深度学习:BSM 提取运动区域 → YOLO 分类,减少计算量。
  3. 纯深度学习:复杂场景直接 YOLO 检测,不依赖背景建模。

简答背诵

BSM(背景减除方法)是一类基于背景建模的运动目标检测算法,包括 MOG2、KNN 等具体实现。它在静态摄像头、资源受限、简单监控场景中仍广泛使用,优点是无需训练、计算量小、能检测任意运动物体;但要求摄像头静止,对光照突变和动态背景敏感,且无类别识别能力。复杂工业场景中常与 YOLO 等深度学习检测结合,或直接转向纯检测方案。

核心考点

  • BSM 核心思想:建立背景模型 → 当前帧与背景比较 → 差异大的像素判定为前景
  • 背景模型会在线更新,适应缓慢的光照和场景变化。
  • 输出是二值前景掩码(255 = 前景,0 = 背景)。

112-对象检测与跟踪(基于颜色)-02(基于颜色阈值的方法在特定场景(教学、机器人竞赛、简单工业检测)仍广泛使用;但在通用、复杂场景下已被更鲁棒的检测 + 跟踪方案取代)


工业项目中的实际选择

表格

场景 推荐方案
颜色固定、背景可控、光照稳定 颜色阈值法(简单、快、可靠)
光照变化、背景复杂 YOLO 检测 + ByteTrack 跟踪
多目标、需要 ID 保持 YOLO + ByteTrack/OC-SORT
嵌入式、算力极弱 颜色阈值 或 轻量化 YOLOv8n

对比表

表格

方法 适用场景 鲁棒性 算力
颜色阈值跟踪 颜色固定、简单场景 低(光照 / 背景敏感) 极低
MOG2 背景建模 静态摄像头、运动检测
KCF/CSRT 单目标跟踪
YOLO+ByteTrack 通用多目标跟踪

简答背诵

基于颜色阈值的运动小球跟踪通过 HSV 颜色空间分割、轮廓检测和质心计算实现目标定位。在颜色固定、背景可控、光照稳定的场景(如机器人竞赛、简单工业检测、教学实验)仍广泛使用,优点是简单、速度快、无需训练;但在通用复杂场景下,因对光照敏感、易受相似颜色干扰、无多目标区分能力,已被 YOLO 检测 + ByteTrack 跟踪等更鲁棒的方案取代。


代码整体概述

实现基于颜色阈值的运动小球目标跟踪 。 流程:读取视频,用inRange筛选绿色小球得到二值 mask;开运算去噪、膨胀扩大目标区域;findContours查找全部外轮廓,筛选面积最大轮廓,求取外接矩形;在原图绘制红色矩形框跟踪小球;无轮廓时矩形置 0,ESC 退出。

属于基于颜色的简单目标跟踪,不是 MeanShift、KCF 这类跟踪算法;依赖目标颜色与背景差异大。

cpp

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

using namespace std;
using namespace cv;

Rect roi;
void processFrame(Mat &binary, Rect &rect);

int main(int argc, char* argv) {
	// 打开视频
	VideoCapture capture;
	capture.open("C:/opencv/images/balltest.mp4");
	if (!capture.isOpened()) {
		printf("could not find video file");
		return -1;
	}

	Mat frame, mask;
	// 两个形态学核
	Mat kernel1 = getStructuringElement(MORPH_RECT, Size(3, 3), Point(-1, -1));
	Mat kernel2 = getStructuringElement(MORPH_RECT, Size(5, 5), Point(-1, -1));

	namedWindow("input video", WINDOW_AUTOSIZE);
	namedWindow("track mask", WINDOW_AUTOSIZE);

	while (capture.read(frame)) {
		// BGR空间颜色阈值筛选,提取绿色小球,生成二值mask
		inRange(frame, Scalar(0, 127, 0), Scalar(120, 255, 120), mask);

		// 开运算:先腐蚀后膨胀,去除小噪点
		morphologyEx(mask, mask, MORPH_OPEN, kernel1, Point(-1, -1), 1);
		// 膨胀4次,把小球掩码区域扩大,填补内部细小空洞
		dilate(mask, mask, kernel2, Point(-1, -1), 4);
		imshow("track mask", mask);

		// 轮廓处理,得到最大物体外接矩形roi
		processFrame(mask, roi);
		// 在原图绘制红色跟踪框
		rectangle(frame, roi, Scalar(0, 0, 255), 3, 8, 0);
		imshow("input video", frame);

		char c = waitKey(100); //每一帧等待100ms,控制播放速度
		if (c == 27) {
			break;
		}
	}

	capture.release();
	waitKey(0);
	return 0;
}

/**
 * @brief 轮廓查找,找出面积最大的外部轮廓,输出外接矩形rect
 * @param binary 输入二值图像(白色目标,黑色背景)
 * @param rect 输出最大轮廓的boundingRect
 */
void processFrame(Mat &binary, Rect &rect) {
	vector<vector<Point>> contours;
	vector<Vec4i> hireachy;
	// 查找轮廓:只取最外层轮廓,压缩轮廓点
	findContours(binary, contours, hireachy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE, Point(0, 0));

	if (contours.size() > 0) {
		double maxArea = 0.0;
		for (size_t t = 0; t < contours.size(); t++) {
			// 计算每个轮廓面积
			double area = contourArea(contours[static_cast<int>(t)]);
			if (area > maxArea) {
				maxArea = area;
				// 获取轮廓最小外接矩形
				rect = boundingRect(contours[static_cast<int>(t)]);
			}
		}
	}
	else {
		// 没有找到轮廓,矩形全部置0
		rect.x = rect.y = rect.width = rect.height = 0;
	}
}

逐段详细解析

1. 全局变量

Rect roi:保存目标物体的外接矩形(x,y,w,h),用于画跟踪框。

2. 形态学核

cpp

复制代码
Mat kernel1 = getStructuringElement(MORPH_RECT, Size(3,3), Point(-1,-1));
Mat kernel2 = getStructuringElement(MORPH_RECT, Size(5,5), Point(-1,-1));
  • kernel1:3×3 矩形核,用于开运算消除微小噪点;
  • kernel2:5×5 矩形核,用于膨胀,放大目标白色区域。

3.inRange 颜色阈值

cpp

复制代码
inRange(frame, Scalar(0, 127, 0), Scalar(120, 255, 120), mask);

⚠️注意:这里直接在BGR 空间做阈值,不是 HSV。 B 通道 0‑120,G 通道 127‑255,R 通道 0‑120; 满足条件像素 mask=255 白色(小球),不满足 = 0 黑色背景。
缺点:BGR 受亮度影响大,光照变化阈值容易失效,工程一般转 HSV 再 inRange。

4. 形态学操作

cpp

复制代码
morphologyEx(mask, mask, MORPH_OPEN, kernel1, Point(-1,-1),1);

MORPH_OPEN 开运算 = 腐蚀 → 膨胀 作用:消除图像中小的白色噪点,目标物体本身大小基本不变

cpp

复制代码
dilate(mask, mask, kernel2, Point(-1,-1),4);

膨胀,迭代 4 次:把小球白色区域向外扩张,填补小球内部的小孔洞,让轮廓更加完整。

5.processFrame () 轮廓处理函数

cpp

复制代码
findContours(binary, contours, hireachy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE, Point(0,0));
  • RETR_EXTERNAL只提取最外层轮廓,忽略内部嵌套轮廓
  • CHAIN_APPROX_SIMPLE:压缩轮廓点,只保留拐点,减少点数量;
  • contours:全部轮廓集合,每个轮廓是vector<Point>
  • hireachy:轮廓层级,本代码 RETR_EXTERNAL,层级基本不用。

cpp

复制代码
double area = contourArea(contours[t]);

contourArea()计算轮廓包围区域的面积(像素个数)。 遍历全部轮廓,记录面积最大的那一个,代表小球。

cpp

复制代码
rect = boundingRect(contours[t]);

boundingRect():计算轮廓的最小外接正矩形 ,返回Rect(x,y,width,height)

如果没有任何轮廓,将 rect 全部置 0,跟踪框消失。

6. 绘制跟踪框

cpp

复制代码
rectangle(frame, roi, Scalar(0,0,255),3,8,0);

在原图上绘制红色粗框,标记小球位置。

7. 视频循环

waitKey(100):每帧等待 100ms,控制视频播放速度;ESC (27) 退出循环。

窗口输出

  1. track mask:二值掩码图,白色是检测到的绿色小球;
  2. input video:原始视频画面,叠加红色跟踪框。

算法优缺点

✅优点

  1. 实现简单,计算速度快;
  2. 适合颜色特征非常突出的目标。

❌缺点

  1. 在 BGR 直接做 inRange,对光照敏感;光照一变,mask 就失效;
  2. 画面中只要出现其他大块绿色物体,就会误跟踪;
  3. 没有目标预测,物体短暂遮挡后直接丢失;
  4. 只能矩形框,不能得到物体实际轮廓。

区分:这是颜色 + 轮廓检测,不是真正的跟踪算法(MeanShift、CamShift、KCF、CSRT)。本算法每一帧都重新检测,没有利用上一帧位置做预测。

简答背诵

该代码实现基于颜色阈值 + 轮廓分析的绿色小球视频检测跟踪。读取视频帧,BGR 空间使用 inRange 筛选绿色目标生成二值 mask;使用开运算去除噪点,膨胀操作补全目标掩码。调用 findContours 查找全部最外层轮廓,遍历计算每个轮廓面积,选出面积最大轮廓,通过 boundingRect 求取外接矩形;在原图绘制红色矩形框标记小球位置;没有轮廓时矩形置零。按 ESC 退出。该方法每帧独立检测,没有目标运动预测,依赖目标颜色与背景差异,BGR 空间阈值易受光照干扰,不属于专用目标跟踪算法。

拓展对比

表格

方法 特点
本代码 颜色 + 轮廓,逐帧检测,无预测
CamShift 基于 HSV 直方图反向投影,具备简单目标跟踪,支持目标大小变化
KCF/CSRT 相关滤波跟踪,初始化后跟踪,抗部分遮挡

视频11111111111111111

115-光流的对象跟踪-03(稀疏光流,代码可以参考以前的博文)
116-光流的对象跟踪-04(稠密光流,代码可以参考以前的博文)


直接用光流做完整对象跟踪在工业中已基本淘汰(易漂移、无重检测);但光流作为跟踪系统的辅助模块(帧间预测、降算力)仍广泛使用,光流算法本身也在持续发展

一句话结论

光流(Optical Flow)本身作为运动估计工具仍广泛使用;但直接用光流做完整对象跟踪在工业中已较少单独使用,更多作为跟踪 / 检测系统的辅助模块

光流算法本身还在用的场景

  1. 运动估计与分析:视频稳定、动作识别、视频插帧、SLAM 视觉里程计。
  2. 跟踪辅助:KCF/CSRT 等跟踪器内部可能用光流辅助;检测 + 跟踪系统中用光流做帧间预测。
  3. 无监督 / 自监督学习:光流是视频理解任务的重要监督信号。
  4. 简单运动检测:Farneback 稠密光流做整体运动分析。
  5. 教学实验:Lucas-Kanade 稀疏光流是经典 CV 课程内容。

为什么直接用光流做对象跟踪已少用

  1. 漂移累积:光流是帧间增量估计,长时间跟踪误差累积,目标容易跑偏。
  2. 无重检测能力:目标被遮挡后再出现,无法自动恢复。
  3. 对光照、快速运动敏感:光照变化、大位移时光流估计不准。
  4. 稀疏光流依赖角点:目标纹理少、角点丢失时跟踪失败。
  5. 被更优方案取代
    • 单目标:KCF/CSRT(相关滤波)更稳定
    • 多目标:YOLO + ByteTrack/OC-SORT(检测驱动,ID 保持,遮挡恢复)

光流的两类核心方法

表格

类型 代表算法 特点
稀疏光流 Lucas-Kanade (LK) 只跟踪角点等特征点,速度快,OpenCV calcOpticalFlowPyrLK
稠密光流 Farneback、DIS、RAFT 计算每个像素的运动,精度高,计算量大

工业项目中的实际用法

  1. 光流 + 检测器:用光流做帧间预测,减少检测器调用频率(如每 5 帧检测一次,中间用光流跟踪),降低算力。
  2. 光流 + 跟踪器:辅助相关滤波跟踪器处理快速运动。
  3. 运动分析任务:视频稳定、动作识别、异常行为检测,光流是核心特征。
  4. 简单场景:固定摄像头、目标运动平缓、纹理丰富,LK 光流可做轻量跟踪。

对比表

表格

跟踪方案 工业地位 特点
Lucas-Kanade 光流 教学 / 辅助模块 特征点跟踪,速度快,易漂移
KCF/CSRT 传统跟踪常用 相关滤波,稳定,无重检测
YOLO+ByteTrack 工业主流 检测驱动,多目标,ID 保持
光流 + 检测混合 特定场景优化 降算力,帧间预测

简答背诵

光流是经典的运动估计算法,分为稀疏光流(如 Lucas-Kanade)和稠密光流(如 Farneback、RAFT)。光流本身仍广泛用于视频稳定、动作识别、SLAM 和跟踪辅助;但直接用光流做完整对象跟踪在工业中已较少单独使用,因为存在漂移累积、无重检测能力、对光照和快速运动敏感等问题,已被 KCF/CSRT 和 YOLO+ByteTrack 等方案取代。实际项目中常将光流作为辅助模块,与检测器结合以降低算力开销。

核心考点

  • Lucas-Kanade 光流假设:亮度恒定、小运动、空间一致性
  • 金字塔 LK 光流(calcOpticalFlowPyrLK):解决大位移问题,从顶层到底层逐层细化。
  • 光流跟踪流程:检测角点 → 计算下一帧光流 → 过滤好的跟踪点 → 绘制轨迹。

光流分类

1、按照计算像素范围划分(最常考)

① 稀疏光流 Sparse Optical Flow

代表:LK (Lucas‑Kanade) 光流

  • 只对少数特征角点计算运动矢量,不是全部像素
  • 需要先检测角点(goodFeaturesToTrack),再跟踪这些点
  • 计算速度快,资源消耗小
  • 输出:前后两帧对应的特征点坐标集合
  • 用途:目标跟踪、SLAM、特征点追踪

② 稠密光流 Dense Optical Flow

代表:Farneback 光流 calcOpticalFlowFarneback

  • 图像每一个像素都计算运动向量 (dx,dy)
  • 不需要提取角点,对整张图所有像素计算
  • 计算量大,速度慢
  • 输出:CV_32FC2的 Mat,每个像素 Point2f (dx, dy)
  • 用途:运动区域分割、运动检测、视频稳像

2、按照求解方法划分

  1. 基于梯度的方法(LK、Farneback)

核心假设:亮度恒定、小运动、空间一致;依靠图像灰度梯度求解。 大位移时配合图像金字塔,解决大运动问题。

  1. 基于匹配的方法 块匹配,把图像分小块,在下一帧搜索相似块,得到位移。

3、按照图像金字塔

  • 单层光流:只在原图计算,只能处理很小位移
  • 金字塔光流 (LK 金字塔 / Farneback 自带金字塔):先在缩小图算粗略运动,逐层回推原图,可以处理物体大位移

4、另外两个概念

  • 时间光流:相邻两帧之间的运动(我们代码里用的)
  • 还有深度学习光流:RAFT‑Flow,不属于 OpenCV 传统光流。

对比速记表

表格

类型 算法 计算对象 速度
稀疏光流 LK 金字塔光流 少量角点
稠密光流 Farneback 全部像素

考试简答: 光流分为稀疏光流稠密光流。稀疏光流以 LK 光流为代表,只跟踪部分特征角点,计算速度快;稠密光流以 Farneback 算法为代表,为图像每个像素计算运动偏移,计算量大,可以得到完整运动场。二者都可以配合图像金字塔,解决物体大位移运动。

光流三个基本假设(简答题)

  1. 亮度恒定:同一个物体点在相邻帧灰度不变
  2. 时间连续(小运动):帧间物体移动距离不能过大
  3. 空间一致性:邻近的像素运动基本相同

当物体运动位移很大,破坏小运动假设 → 使用图像金字塔,逐层缩小图像,把大位移变成小位移。

120-CAMShift对象跟踪-04(CAMShift 作为独立的完整对象跟踪方案在工业中已基本淘汰;但在教学实验和颜色固定、背景可控的简单场景中仍在使用)

代码整体概述

该代码实现 CamShift 目标跟踪算法(Continuously Adaptive Mean‑Shift)。 流程:

  1. 第一帧使用selectROI手动框选要跟踪的目标;
  2. 提取 ROI 区域的 Hue(色相)一维直方图,做归一化,作为目标颜色模板;
  3. 对后续每一帧,计算直方图反向投影 backprojection,得到概率图,像素值越大代表越像目标颜色;
  4. CamShift 算法在反向投影图上迭代搜索目标,输出旋转外接矩形RotatedRect
  5. 使用ellipse绘制旋转跟踪框,可以适应目标旋转、大小缩放;
  6. 同时可视化目标颜色直方图,ESC 退出。

CamShift = MeanShift + 自适应窗口大小;MeanShift 窗口固定,CamShift 会自动改变窗口尺寸,支持目标放大缩小、旋转。 核心依靠颜色直方图做跟踪,对颜色特征依赖强。

cpp

复制代码
#include <opencv2/opencv.hpp>
#include <opencv2/video/tracking.hpp>
#include <iostream>
#include <math.h>

using namespace cv;
using namespace std;

int smin = 15;
int vmin = 40;
int vmax = 256;
int bins = 16;

int main(int argc, char** argv) {
	VideoCapture capture;
	capture.open("C:/opencv/images/balltest.mp4");
	if (!capture.isOpened()) {
		printf("could not find video data file...\n");
		return -1;
	}
	namedWindow("CAMShift Tracking", WINDOW_AUTOSIZE);
	namedWindow("ROI Histogram", WINDOW_AUTOSIZE);

	bool firstRead = true;
	float hrange[] = { 0, 180 };    // H通道范围0~180
	const float* hranges = hrange;

	Rect selection;                 //初始选择ROI
	Mat frame, hsv, hue, mask, hist, backprojection;
	Mat drawImg = Mat::zeros(300, 300, CV_8UC3); //直方图绘制画布

	while (capture.read(frame)) {
		if (firstRead) {
			//手动框选跟踪目标
			Rect2d first = selectROI("CAMShift Tracking", frame);
			selection.x = first.x;
			selection.y = first.y;
			selection.width = first.width;
			selection.height = first.height;
			printf("ROI.x= %d, ROI.y= %d, width = %d, height= %d", selection.x, selection.y, selection.width, selection.height);
		}

		// BGR转HSV颜色空间
		cvtColor(frame, hsv, COLOR_BGR2HSV);
		// mask:过滤低饱和度、低亮度像素,去除阴影、噪声
		inRange(hsv, Scalar(0, smin, vmin), Scalar(180, vmax, vmax), mask);

		// 提取hue色相通道,单独拿出来做直方图
		hue = Mat(hsv.size(), hsv.depth());
		int channels[] = { 0, 0 };
		mixChannels(&hsv, 1, &hue, 1, channels, 1);

		if (firstRead) {
			//截取ROI区域的hue和对应的mask
			Mat roi(hue, selection);
			Mat maskroi(mask, selection);
			//计算目标ROI的色相直方图
			calcHist(&roi, 1, 0, maskroi, hist, 1, &bins, &hranges);
			normalize(hist, hist, 0, 255, NORM_MINMAX);

			//========绘制直方图可视化========
			int binw = drawImg.cols / bins;
			Mat colorIndex = Mat(1, bins, CV_8UC3);
			for (int i = 0; i < bins; i++) {
				colorIndex.at<Vec3b>(0, i) = Vec3b(saturate_cast<uchar>(i * 180 / bins), 255, 255);
			}
			cvtColor(colorIndex, colorIndex, COLOR_HSV2BGR);
			for (int i = 0; i < bins; i++) {
				int  val = saturate_cast<int>(hist.at<float>(i)*drawImg.rows / 255);
				rectangle(drawImg, Point(i*binw, drawImg.rows), Point((i + 1)*binw, drawImg.rows - val), Scalar(colorIndex.at<Vec3b>(0, i)), -1, 8, 0);
			}
		}

		// 计算直方图反向投影
		calcBackProject(&hue, 1, 0, hist, backprojection, &hranges);
		//和mask做按位与,过滤低饱和低亮度噪声
		backprojection &= mask;

		// CamShift跟踪算法
		RotatedRect trackBox = CamShift(backprojection, selection, TermCriteria((TermCriteria::COUNT | TermCriteria::EPS), 10, 1));

		//绘制旋转椭圆跟踪框,支持目标旋转、缩放
		ellipse(frame, trackBox, Scalar(0, 0, 255), 3, 8);

		if (firstRead) {
			firstRead = false;
		}
		imshow("CAMShift Tracking", frame);
		imshow("ROI Histogram", drawImg);

		char c = waitKey(50);
		if (c == 27) {
			break;
		}
	}

	capture.release();
	waitKey(0);
	return 0;
}

逐段详细解析

1. 全局参数

  • smin=15:饱和度下限;vmin=40亮度下限;过滤灰暗、阴影像素;
  • bins=16:直方图 bin 数量,把 H (0‑180) 划分为 16 组;
  • hrange[]={0,180}:HSV 中 H 通道取值范围。

2.selectROI

cpp

复制代码
Rect2d first = selectROI("CAMShift Tracking", frame);

弹窗,手动鼠标框选跟踪目标,只在第一帧执行。

3.HSV 转换 + mask 过滤

cpp

复制代码
cvtColor(frame, hsv, COLOR_BGR2HSV);
inRange(hsv, Scalar(0, smin, vmin), Scalar(180, vmax, vmax), mask);

mask 作用:过滤饱和度很低、亮度很低的像素,避免阴影、灰度背景干扰颜色直方图。

4.mixChannels 提取 Hue 通道

cpp

复制代码
mixChannels(&hsv,1,&hue,1,channels,1);

把 HSV 图像的第 0 通道 (H 色相) 复制到 hue 矩阵,只使用色相 H 做直方图,消除亮度影响。

5.calcHist 计算目标直方图(第一帧)

cpp

复制代码
calcHist(&roi, 1, 0, maskroi, hist, 1, &bins, &hranges);
normalize(hist, hist,0,255,NORM_MINMAX);
  • 截取框选 ROI 区域,计算一维色相直方图,作为目标颜色模板;
  • normalize归一化到 0‑255,消除目标明暗影响。

直方图可视化:每一个 bin 对应一种色相,柱子高度代表该颜色在目标中占比。

6.calcBackProject 直方图反向投影

cpp

复制代码
calcBackProject(&hue,1,0,hist,backprojection,&hranges);
backprojection &= mask;

反向投影:把输入图像每个像素,去查目标直方图;输出一张概率图

  • 像素越接近目标颜色 → 值越接近 255(白色);
  • 不像目标颜色 → 值接近 0(黑色)。

CamShift 算法就是在这张概率图上搜索目标位置。

7.CamShift 核心函数

cpp

复制代码
RotatedRect trackBox = CamShift(backprojection, selection, TermCriteria((TermCriteria::COUNT | TermCriteria::EPS),10,1));

参数说明:

  1. backprojection:反向投影概率图;
  2. selection:输入输出;输入上一帧搜索窗口,输出更新后的窗口位置;
  3. TermCriteria迭代终止条件:最多迭代 10 次,或者中心移动小于 1 像素停止迭代。

返回值RotatedRect trackBox旋转矩形,包含中心、宽高、旋转角度。

区别于 MeanShift:MeanShift 只返回窗口位置,窗口大小固定;CamShift 会自动调整窗口大小,支持物体放大缩小、旋转。

8.ellipse 绘制旋转框

cpp

复制代码
ellipse(frame, trackBox, Scalar(0,0,255),3,8);

RotatedRect 可以直接交给 ellipse 绘制椭圆旋转跟踪框,不是普通正矩形。

9.firstRead 标志

只在第一帧:框选 ROI、计算目标直方图、绘制直方图画布;后续帧不再重新计算直方图,一直复用第一帧的颜色模板。

窗口输出

  1. CAMShift Tracking:视频画面,红色旋转椭圆跟踪框;
  2. ROI Histogram:目标颜色色相直方图。

CamShift / MeanShift 对比

表格

算法 窗口 输出 特点
MeanShift 窗口大小固定 Rect 普通矩形 不能适应目标缩放旋转
CamShift 自适应窗口大小 RotatedRect 旋转矩形 支持目标放大、缩小、旋转

优缺点

✅优点

  1. 算法简单,速度快;
  2. 可以适应目标旋转、尺寸变化;
  3. 不需要特征点,依靠颜色跟踪。

❌缺点

  1. 完全依赖颜色;画面出现和目标颜色相近物体,容易漂移丢失;
  2. 目标颜色发生变化(光照变化)跟踪失效;
  3. 没有模板更新,全程使用第一帧直方图;目标颜色改变无法自适应;
  4. 物体完全遮挡之后跟踪框会飘走。

简答背诵

该代码实现 CamShift 目标跟踪。第一帧手动框选跟踪目标;图像转为 HSV 空间,提取色相 H 通道,计算目标 ROI 区域的色相归一化直方图作为颜色模板。后续每一帧计算直方图反向投影,生成目标颜色概率图;CamShift 算法在概率图迭代搜索目标,自动调整搜索窗口大小,返回旋转外接矩形 RotatedRect;使用 ellipse 绘制旋转椭圆跟踪框。CamShift 在 MeanShift 基础上增加窗口自适应,可以处理目标缩放与旋转;缺点是依赖颜色特征,相似颜色物体容易造成跟踪漂移。
补充:反向投影:根据目标直方图,把图像每个像素映射为属于目标的概率,值越大越像目标。

视频11111111111111111111111

121-视频中移动对象统计(MOG2 背景建模:仍在使用,静态摄像头场景的经典选择)


MOG2 背景建模:仍在使用,静态摄像头场景的经典选择

一句话结论

MOG2 没有被淘汰,在静态摄像头、简单运动目标检测场景中仍是常用的传统方法;但在复杂场景下,工业项目常结合或转向深度学习检测方案

还在用的场景

  1. 静态摄像头运动检测:固定摄像头下检测移动物体(人、车、球等),是最经典的方案。
  2. 资源受限设备:纯 CPU、嵌入式设备,不需要深度学习推理,速度快、开销小。
  3. 简单监控场景:室内固定摄像头、工业流水线异物检测、简单入侵检测。
  4. 教学实验:背景减除、运动目标检测的入门必学算法。
  5. 作为预处理模块:先 MOG2 提取运动区域,再做后续识别 / 跟踪,减少计算量。

MOG2 的优势

  1. 自动学习背景:不需要手动标注,自动适应光照缓慢变化。
  2. 支持阴影检测:可区分前景和阴影(阴影输出灰色掩码)。
  3. 计算量小:CPU 实时,适合嵌入式。
  4. OpenCV 内置createBackgroundSubtractorMOG2() 直接调用,无需额外模型。

局限性(复杂场景下的问题)

  1. 摄像头必须静止:摄像头晃动直接失效。
  2. 光照突变:突然开灯、闪光灯、阳光变化会产生大量误检。
  3. 动态背景:树叶晃动、水波、喷泉、雨体会被误判为前景。
  4. 目标静止后消失:物体静止一段时间会被学习成背景,不再被检测。
  5. 无类别识别:只知道 "有东西在动",不知道是什么物体。

工业项目中的实际用法

  1. 简单场景直接用 MOG2:固定摄像头、背景稳定,足够用。
  2. MOG2 + 深度学习:先用 MOG2 快速提取运动候选区域,再用 YOLO 做精细分类,兼顾速度和精度。
  3. 纯深度学习:复杂场景直接用 YOLO 检测,不依赖背景建模。

对比表

表格

背景建模算法 现状 特点
MOG2 仍广泛使用 混合高斯,支持阴影,自动学习背景
KNN 可用 K 近邻背景建模,对复杂背景抗干扰更强
MOG (v1) 基本淘汰 初代混合高斯,被 MOG2 取代
YOLO 检测 复杂场景主流 不依赖背景,直接检测物体类别

简答背诵

MOG2 是基于混合高斯模型的背景减除算法,在静态摄像头运动目标检测场景中仍广泛使用,尤其适合资源受限设备和简单监控场景。它能自动学习背景、支持阴影检测、计算量小;但要求摄像头静止,对光照突变、动态背景和静止目标处理有限。复杂工业场景中,常与 YOLO 等深度学习检测结合使用,或直接转向纯检测方案。


代码整体说明

该代码实现 MOG2 背景建模(背景减除)运动目标检测 。 原理:createBackgroundSubtractorMOG2 混合高斯模型,区分背景与前景运动物体;得到前景掩码,经过阈值、开运算去噪;轮廓检测筛选面积足够大的运动目标,绘制矩形框并且给目标编号;统计画面中运动物体数量;ESC 退出。

MOG2:混合高斯背景建模,OpenCV 内置背景差分算法,适合视频静态摄像头场景;摄像头不能晃动。

cpp

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

using namespace cv;
using namespace std;

int main(int argc, char**) {
	VideoCapture capture;
	capture.open("C:/opencv/images/mulballs.mp4");
	if (!capture.isOpened()) {
		printf("could not load video data...\n");
		return -1;
	}

	namedWindow("input video", WINDOW_AUTOSIZE);
	namedWindow("motion objects", WINDOW_AUTOSIZE);

	// 创建MOG2混合高斯背景减除器
	Ptr<BackgroundSubtractor> pMOG2 = createBackgroundSubtractorMOG2();
	Mat kernel = getStructuringElement(MORPH_RECT, Size(3, 3), Point(-1, -1));

	vector<vector<Point>> contours;
	vector<Vec4i> hireachy;
	int count = 0;

	Mat frame, gray, mogMask;
	while (capture.read(frame)) {
		imshow("input video", frame);
		// 背景建模,输出前景掩码mogMask
		pMOG2->apply(frame, mogMask);
		// 二值化,过滤微弱噪声
		threshold(mogMask, mogMask, 100, 255, THRESH_BINARY);
		// 开运算:腐蚀+膨胀,去除小噪点
		morphologyEx(mogMask, mogMask, MORPH_OPEN, kernel, Point(-1, -1));

		// 查找最外层轮廓
		findContours(mogMask, contours, hireachy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE, Point(0, 0));
		count = 0;
		char numText[8];
		for (size_t t = 0; t < contours.size(); t++) {
			double area = contourArea(contours[t]);
			// 过滤面积过小的噪声轮廓
			if (area < 1000) continue;
			Rect selection = boundingRect(contours[t]);
			// 过滤宽高太小的目标
			if (selection.width < 30 || selection.height < 30) continue;

			count++;
			// 绘制红色外接矩形框
			rectangle(frame, selection, Scalar(0, 0, 255), 2, 8);

			sprintf(numText, "%d", count);
			// ⚠️这里代码有BUG!!!
			putText(frame, numText, Point(selection.x, selection.y), cv::FONT_HERSHEY_SIMPLEX, FONT_HERSHEY_PLAIN, Scalar(255, 0, 0), 1, 8);
		}
		
		imshow("motion objects", frame);
		char c = waitKey(50);
		if (c == 27) {// ESC 
			break;
		}
	}

	capture.release();
	waitKey(0);
	return 0;
}

逐段解析

1. 创建 MOG2 背景模型

cpp

复制代码
Ptr<BackgroundSubtractor> pMOG2 = createBackgroundSubtractorMOG2();
  • MOG2:混合高斯背景建模算法;
  • apply(frame, mogMask):输入当前帧,输出mogMask前景掩码;
  • 掩码规则:白色 255 代表运动前景,黑色 0 代表背景;会自动学习背景,光照缓慢变化可以适应。

注意:刚启动前若干帧模型在学习背景,mask 会有很多噪点,需要等待模型收敛。

2. 预处理掩码

cpp

复制代码
threshold(mogMask, mogMask, 100, 255, THRESH_BINARY);

MOG2 输出 mask 是 0‑255 浮点 / 灰度图;二值化,大于 100 置 255,过滤微弱阴影噪声。

cpp

复制代码
morphologyEx(mogMask, mogMask, MORPH_OPEN, kernel, Point(-1, -1));

开运算:先腐蚀,后膨胀;消除图像中小白色噪点,物体整体大小基本不变。

3.findContours 轮廓查找

cpp

复制代码
findContours(mogMask, contours, hireachy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE, Point(0, 0));
  • RETR_EXTERNAL:只获取最外层轮廓,忽略内部嵌套轮廓;
  • contours:存放所有检测到轮廓集合。

4. 轮廓筛选逻辑

cpp

复制代码
double area = contourArea(contours[t]);
if (area < 1000) continue;
Rect selection = boundingRect(contours[t]);
if (selection.width < 30 || selection.height < 30) continue;
  • contourArea()计算轮廓像素面积;过滤面积小于 1000 的噪声;
  • boundingRect()获取轮廓正外接矩形;过滤宽高小于 30 像素过小目标;
  • count++:有效运动目标计数。

cpp

复制代码
rectangle(frame, selection, Scalar(0,0,255),2,8);

绘制红色矩形框标记运动物体。

⚠️代码 BUG:putText 参数写错!

cpp

复制代码
//错误原代码
putText(frame, numText, Point(selection.x, selection.y), cv::FONT_HERSHEY_SIMPLEX, FONT_HERSHEY_PLAIN, Scalar(255, 0, 0), 1, 8);

putText 函数原型:

cpp

复制代码
putText(图像,文字,坐标,字体类型,字体缩放系数,颜色,线宽,线型)

第 4 参数:字体;第 5 参数:缩放大小(double),不能再传字体!

✅修复后正确行:

cpp

复制代码
putText(frame, numText, Point(selection.x, selection.y), cv::FONT_HERSHEY_SIMPLEX, 1.0, Scalar(255, 0, 0), 1, 8);

5. 窗口输出

  1. input video:原始视频;
  2. motion objects:绘制框 + 编号之后的视频画面。

waitKey(50)每帧等待 50ms,控制播放速度;ESC 退出。

MOG2 背景建模原理简述

  1. 维护多个高斯分布,用来描述背景每个像素;
  2. 当前像素和背景模型匹配,判定背景;不匹配判定前景(运动物体);
  3. 模型会慢慢更新,适应光照缓慢变化;
  4. 缺点:容易产生阴影,MOG2 可以开启阴影检测,阴影输出灰色掩码。

优缺点

✅优点

  1. 静态摄像头下检测运动物体效果好;
  2. 自动学习背景,不需要手动设置阈值;
  3. 速度较快。

❌缺点

  1. 摄像头晃动直接失效
  2. 树叶晃动、水波、光照突变会产生大量噪点;
  3. 物体静止一段时间后,会被学习成背景,目标消失;
  4. 会产生阴影干扰,需要额外处理。

简答背诵

该代码使用 MOG2 混合高斯背景建模实现运动目标检测。读取视频,调用 BackgroundSubtractorMOG2 的 apply 得到前景掩码;经过二值化、开运算去除噪声;findContours 查找前景轮廓,通过面积、轮廓外接矩形宽高过滤虚假目标;对有效运动目标绘制红色外接矩形并编号计数。MOG2 算法适合静态摄像机场景,可以自动学习背景;物体静止过久会被当作背景,光照突变、树叶抖动容易产生误检测。

拓展对比

  • MOG2:混合高斯,支持阴影检测;
  • KNN 背景减除:K 近邻背景建模,对复杂背景抗干扰更强。

补充:如果想要去掉阴影,创建模型时:

cpp

复制代码
Ptr<BackgroundSubtractor> pMOG2 = createBackgroundSubtractorMOG2(500,16,false);

第三个参数detectShadows=false关闭阴影检测。

视频1111111111111111111

122-扩展模块中的跟踪方法介绍(已经废弃)

123-扩展模块中的多对象跟踪(已经废弃)

对比总结表

方案 现状 特点
MultiTracker 废弃(OpenCV4 删除) 只是外壳,不要用
KCF / CSRT 单目标跟踪 仍可用,contrib 模块 轻量 CPU;无重检测,跟丢无法恢复;适合简单场景、课程实验
YOLO+ByteTrack 工业主流 深度学习,自动检测 + ID 维持,抗遮挡,算力要求高

简答背诵

  1. MultiTracker 是 OpenCV3 提供的多目标跟踪封装类,OpenCV4 已经移除,现在需要手动使用vector<Ptr<Tracker>>管理多个跟踪器实例。
  2. KCF、CSRT 单目标跟踪器仍然保留在 opencv‑contrib 扩展模块;优点是 CPU 速度快,不需要深度学习;缺点是没有重检测机制,目标被遮挡后容易漂移丢失,适合简单场景与教学实验。
  3. 工业实际多目标跟踪,一般采用 YOLO 检测配合 ByteTrack/OC‑SORT 等检测驱动式跟踪算法。

126-使用GoogleNet模型实现图像分类-02(GoogLeNet(Inception v1)作为图像分类的工业首选方案已被淘汰;但在教学实验、论文基线、简单分类任务和遗留项目中仍在使用。)

128-使用SSD模型实现对象检测-02(SSD 作为对象检测的工业首选方案已被 YOLO 系列(v5/v8)取代;但作为经典基线、教学实验、MobileNet-SSD 轻量部署和遗留项目仍在使用。)

129-MobileNet模型实时对象检测(MobileNet 作为轻量级骨干网络没有淘汰,仍在移动端 / 嵌入式实时检测中广泛使用;但 MobileNet-SSD 这个具体检测组合在工业新项目中已被 YOLOv8n/s 等轻量检测模型取代。)

133-GOTURN模型实现视频对象跟踪(GOTURN 作为视频对象跟踪方案在工业中已基本淘汰,OpenCV-contrib 的TrackerGOTURN接口仍保留但工程不再使用,仅用于教学和理解早期深度学习跟踪范式。)

135-均值方差与协方差 协方差矩阵

代码整体说明

这份代码演示两个 OpenCV 统计函数:

  1. meanStdDev():计算图像每个通道的均值、标准差
  2. calcCovarMatrix():计算样本集的协方差矩阵、样本均值

协方差矩阵是 PCA 主成分分析的前置基础,PCA 就是对协方差矩阵做特征值分解。

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

using namespace cv;
using namespace std;

int main(int argc, char** argv) {
	Mat image = imread("C:/opencv/images/blank.png");
	if (image.empty()) {
		printf("could not load image...\n");
		return -1;
	}
	imshow("input image", image);

	// 计算图像各通道均值、标准差
	Mat means, stddev;
	meanStdDev(image, means, stddev);
	printf("means rows : %d, means cols %d\n", means.rows, means.cols);
	printf("stddev rows : %d, stddev cols %d\n", stddev.rows, stddev.cols);
	for (int row = 0; row < means.rows; row++) {
		printf("mean %d = %.3f\n", row, means.at<double>(row));
		printf("stddev %d = %.3f\n", row, stddev.at<double>(row));
	}

	// 构造5个样本,每个样本3维;一行是一个样本
	Mat samples = (Mat_<double>(5, 3) <<
		90, 60, 90,
		90, 90, 30,
		60, 60, 60,
		60, 60, 90,
		30, 30, 30);
	Mat cov, mu;
	// 计算协方差矩阵 + 样本均值
	calcCovarMatrix(samples, cov, mu, COVAR_NORMAL | COVAR_ROWS);

	cout << "=============================" << endl;
	cout << "cov : " << endl;
	cout << cov/5 << endl;

	cout << "means : "<< endl;
	cout << mu << endl;

	waitKey(0);
	return 0;
}

逐段解析

① meanStdDev 图像均值与标准差

复制代码
Mat means, stddev;
meanStdDev(image, means, stddev);

函数原型:

复制代码
void meanStdDev(InputArray src, OutputArray mean, OutputArray stddev);
  • src:输入图像,可以单通道 / 三通道 BGR;
  • means:输出,N 行 1 列列矩阵,N 等于通道数;每个值对应一个通道像素均值;
  • stddev:输出,同样 N 行 1 列,每个通道像素标准差。

如果是 BGR 彩色图:means.rows =3,顺序 B、G、R。 标准差:反映像素灰度波动大小;画面越杂乱,stddev 数值越大;纯色图 stddev≈0。

循环打印每个通道均值、标准差。


② calcCovarMatrix 协方差矩阵计算

复制代码
Mat samples = (Mat_<double>(5, 3) <<
	90, 60, 90,
	90, 90, 30,
	60, 60, 60,
	60, 60, 90,
	30, 30, 30);

构造样本矩阵:5 个样本,每个样本 3 个特征维度,一行代表一个样本

复制代码
calcCovarMatrix(samples, cov, mu, COVAR_NORMAL | COVAR_ROWS);

参数说明:

  1. samples:输入样本数据
  2. cov:输出协方差矩阵(注意:OpenCV 输出的是未除以样本数的求和结果
  3. mu:输出样本均值向量
  4. 标志位:
  • COVAR_ROWS矩阵每一行是一个样本
  • COVAR_NORMAL:计算普通协方差模式

⚠️重要坑: calcCovarMatrix输出的cov求和协方差矩阵,没有做归一化除以样本数目 。 想要标准协方差矩阵,需要手动除以样本数量,示例中:cov /5

输出:

  • mu:1×3 向量,5 个样本各个维度的平均值
  • cov/5:3×3 标准协方差矩阵。

数学关系考点

PCA 算法完整流程:

  1. 求样本均值 mu
  2. calcCovarMatrix 得到协方差矩阵
  3. 对协方差矩阵做特征值分解 eigen()
  4. 取前 k 个最大特征值对应的特征向量作为投影基。

OpenCV 的cv::PCA类内部就是封装了以上三步。

标志位补充

表格

标志 含义
COVAR_ROWS 每一行是一个样本
COVAR_COLS 每一列是一个样本
COVAR_NORMAL 标准协方差计算

简答背诵

  1. meanStdDev()计算图像每个通道像素均值与标准差;彩色图像输出 3 行 1 列的 mean、stddev 矩阵,分别对应 B/G/R 通道。标准差反映像素灰度离散程度。
  2. calcCovarMatrix()用于计算样本集协方差矩阵与样本均值;COVAR_ROWS代表一行是一个样本;该函数输出 cov 为求和形式,需要手动除以样本个数得到标准协方差矩阵
  3. 协方差矩阵是 PCA 降维的数学基础,协方差矩阵描述各个特征之间的相关性。

记忆坑

  • meanStdDev输出是列向量(通道数行,1 列)。
  • calcCovarMatrix返回 cov 没有归一化,必须自己除以样本数。
  • 协方差矩阵一定是实对称方阵 ,所以可以直接交给cv::eigen()做特征分解。

136-特征值与特征向量

代码整体说明

该代码调用 OpenCV 的 eigen() 函数,求解实对称矩阵的特征值与特征向量

数学:对于方阵A,满足 \(A\boldsymbol{v}=\lambda \boldsymbol{v}\);\(\lambda\) 是特征值,\(\boldsymbol{v}\) 是对应的特征向量。 注意:cv::eigen() 只支持实对称矩阵,输入非对称矩阵会得到错误结果。

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

using namespace cv;
using namespace std;

int main(int argc, char** argv) {
	// 构造2×2 实对称矩阵
	Mat data = (Mat_<double>(2, 2) <<
		2, 4,
		4, 2);

	Mat eigenvalues, eigenvector;
	// 计算特征值、特征向量
	eigen(data, eigenvalues, eigenvector);

	// 循环打印每一个特征值
	for (int i = 0; i < eigenvalues.rows; i++) {
		printf("eigen value %d  :  %.3f \n", i, eigenvalues.at<double>(i));
	}

	//输出特征向量矩阵
	cout << " eigen vector : " << endl;
	cout << eigenvector << endl;

	waitKey(0);
	return 0;
}

逐段解析

1. 构造输入矩阵

cpp

复制代码
Mat data = (Mat_<double>(2, 2) <<
	2, 4,
	4, 2);

创建2行2列 double类型矩阵:

\(data= \begin{bmatrix} 2 & 4 \\ 4 & 2 \end{bmatrix}\)

这是实对称矩阵 (\(A^T=A\)),满足cv::eigen()输入要求。

2.eigen 函数原型

cpp

复制代码
bool eigen(InputArray src, OutputArray eigenvalues, OutputArray eigenvectors=noArray());
  • src:输入,实对称方阵,只能 CV_64F (double)
  • eigenvalues:输出,列向量 ,特征值,按从大到小降序排列
  • eigenvectors:输出,特征向量矩阵;每一行是一个归一化的特征向量

⚠️重要记忆: OpenCV eigen()每一行 = 一条特征向量,不是列! 很多教材数学定义特征向量为列向量,这里要注意存储格式差异。

3. 打印特征值

cpp

复制代码
for (int i = 0; i < eigenvalues.rows; i++) {
	printf("eigen value %d  :  %.3f \n", i, eigenvalues.at<double>(i));
}

eigenvalues是 N×1 列矩阵,存放各个特征值,从大到小排序。

本样例矩阵数学求解:

\(\begin{vmatrix} 2-\lambda &4\\ 4 &2-\lambda \end{vmatrix}=0 \Rightarrow \lambda_1=6,\ \lambda_2=-2\)

程序输出:

plaintext

复制代码
eigen value 0  :  6.000
eigen value 1  :  -2.000

4. 输出特征向量矩阵

cpp

复制代码
cout << " eigen vector : " << endl;
cout << eigenvector << endl;

输出矩阵两行分别对应 λ=6、λ=-2 的归一化特征向量。

理论结果: \(\lambda_1=6\) 对应向量 \(0.7071,\\ 0.7071\) \(\lambda_2=-2\) 对应向量 \(-0.7071,\\ 0.7071\)

输出:

plaintext

复制代码
[0.7071067811865476, 0.7071067811865476;
 -0.7071067811865476, 0.7071067811865476]

每一行是单位长度(归一化)特征向量。

5.waitKey(0)

防止控制台窗口直接一闪关闭。

⚠️关键坑点(考试常考)

  1. cv::eigen() 只接受实对称矩阵,普通非对称方阵不能用这个函数求特征值。
  2. 输入必须是double(CV_64F)float会报错。
  3. 输出eigenvectors行是特征向量,不是列向量,和数学课本写法不一样。
  4. 特征值输出降序排列,最大的放第 0 行,PCA 算法就是利用这个性质,取前面大特征值对应的向量做主成分。

和 PCA 的关系

PCA 算法内部核心就是做特征值分解:

  1. 计算数据协方差矩阵(实对称)
  2. 调用 eigen 求特征值、特征向量
  3. 选取前 k 个最大特征值对应的特征向量作为投影基,完成降维。

OpenCV 也封装好cv::PCA类,不需要自己手动调用 eigen。

简答背诵

cv::eigen () 用于对实对称方阵做特征值分解;输入必须为 double 类型实对称矩阵;输出 eigenvalues 为列向量,特征值按从大到小排序;eigenvectors 矩阵每一行代表一个归一化后的特征向量。特征值分解是 PCA 降维算法的数学基础。

138-PCA原理与应用-02

代码整体说明

该代码使用 PCA 主成分分析求取轮廓物体的主轴方向(物体姿态角度)

应用场景:不规则工件、目标物体,通过轮廓点集做 PCA,得到物体的中心、长短轴、旋转角度。 原理:把轮廓所有像素点坐标作为样本,PCA 协方差分解;最大特征值对应的特征向量就是物体主轴方向,计算出旋转角度。
注意:OpenCV4.5.1,代码里有一处旧宏CV_PI会编译报错。

cpp

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

using namespace cv;
using namespace std;

double calcPCAOrientation(vector<Point> &pts, Mat &image);
int main(int argc, char** argv) {
	Mat src = imread("C:/opencv/images/pca.png");
	if (src.empty()) {
		printf("could not load image...\n");
		return -1;
	}
	namedWindow("input image", WINDOW_AUTOSIZE);
	imshow("input image", src);

	Mat gray, binary;
	cvtColor(src, gray, COLOR_BGR2GRAY);
	threshold(gray, binary, 0, 255, THRESH_BINARY | THRESH_OTSU);

	vector<Vec4i> hireachy;
	vector<vector<Point>> contours;
	findContours(binary, contours, hireachy, RETR_LIST, CHAIN_APPROX_NONE);
	Mat result = src.clone();
	for (int i = 0; i < contours.size(); i++) {
		double area = contourArea(contours[i]);
		//过滤轮廓:面积大于100、小于100000
		if (area > 1e5 || area < 1e2) continue;
		drawContours(result, contours, i, Scalar(0, 0, 255), 2, 8);
		//调用PCA计算物体角度
		double theta = calcPCAOrientation(contours[i], result);
	}
	imshow("contours result", result);

	waitKey(0);
	return 0;
}

//输入物体轮廓点集合,绘图,返回物体主轴弧度角
double calcPCAOrientation(vector<Point> &pts, Mat &image) {
	int size = static_cast<int>(pts.size());
	//构造样本矩阵:每一行一个样本 [x,y],double类型
	Mat data_pts = Mat(size, 2, CV_64FC1);
	for (int i = 0; i < size; i++) {
		data_pts.at<double>(i, 0) = pts[i].x;
		data_pts.at<double>(i, 1) = pts[i].y;
	}

	// PCA构造函数直接执行PCA分析;DATA_AS_ROW:一行一个样本
	PCA pca_analysis(data_pts, Mat(), PCA::DATA_AS_ROW);

	// 获取轮廓点集的均值(物体几何中心)
	Point cnt = Point(static_cast<int>(pca_analysis.mean.at<double>(0, 0)),
					static_cast<int>(pca_analysis.mean.at<double>(0, 1)));
	circle(image, cnt, 2, Scalar(0, 255, 0), 2, 8, 0);

	vector<Point2d> vecs(2);
	vector<double> vals(2);
	//取出前两组特征值、特征向量
	for (int i = 0; i < 2; i++) {
		vals[i] = pca_analysis.eigenvalues.at<double>(i, 0);
		printf("Th %d eigen value : %.2f\n", i, vals[i]);
		//❗pca_analysis.eigenvectors:每一行是特征向量
		vecs[i] = Point2d(pca_analysis.eigenvectors.at<double>(i, 0),
			pca_analysis.eigenvectors.at<double>(i, 1));
	}
	//绘制主轴、次轴线段
	Point p1 = cnt + 0.02*Point(static_cast<int>(vecs[0].x*vals[0]), static_cast<int>(vecs[0].y*vals[0]));
	Point p2 = cnt - 0.05*Point(static_cast<int>(vecs[1].x*vals[1]), static_cast<int>(vecs[1].y*vals[1]));

	line(image, cnt, p1, Scalar(255, 0, 0), 2, 8, 0);
	line(image, cnt, p2, Scalar(255, 255, 0), 2, 8, 0);

	//根据主特征向量计算角度(弧度)
	double angle = atan2(vecs[0].y, vecs[0].x);
	// ❗OpenCV4 错误点 CV_PI,替换成 CV_PI
	printf("angle : %.2f\n", 180 * (angle / CV_PI));
	return angle;
}

逐段解析

main 函数部分

  1. 读取图片,转灰度,OTSU 自动阈值二值化,得到黑白二值图。
  2. findContours查找全部轮廓,RETR_LIST取出所有层级轮廓,CHAIN_APPROX_NONE保存轮廓全部点。
  3. 轮廓面积过滤:100 < area <100000,过滤微小噪点、超大区域。
  4. drawContours绘制红色轮廓;调用calcPCAOrientation()计算物体方向角度并绘图。

calcPCAOrientation () 核心函数

功能:对轮廓点集做 PCA,求物体中心、长短主轴、旋转角度

cpp

复制代码
Mat data_pts = Mat(size, 2, CV_64FC1);

把轮廓每个点(x,y),存入矩阵,一行 = 一个样本点 (x,y)

cpp

复制代码
PCA pca_analysis(data_pts, Mat(), PCA::DATA_AS_ROW);

PCA 构造函数直接完成计算;第二个参数Mat()无掩码;DATA_AS_ROW:每行是样本。

PCA 输出成员:

  • pca_analysis.mean:样本均值,就是物体轮廓点集的质心(几何中心)
  • pca_analysis.eigenvalues:特征值,降序排列;第 0 个最大,代表主轴方向能量
  • pca_analysis.eigenvectors每一行对应一个归一化特征向量
    • 第 0 行:最大特征值 → 物体长主轴方向向量
    • 第 1 行:次大特征值 → 物体短次轴方向向量

cpp

复制代码
circle(image, cnt, 2, Scalar(0, 255, 0), 2, 8, 0);

绿色小圆绘制物体质心。

cpp

复制代码
vecs[0] 主方向向量;vecs[1]次方向向量

利用特征值大小缩放向量,从中心点向外画出两条轴线:

  • 蓝色线:主轴(最大特征值方向,物体主要朝向)
  • 黄线:次轴

cpp

复制代码
double angle = atan2(vecs[0].y, vecs[0].x);

atan2(y,x),根据主轴方向向量,返回弧度值 ;弧度转角度:180*angle/CV_PI。 函数返回弧度角度。

⚠️OpenCV4 编译报错点

cpp

复制代码
180 * (angle / CV_PI)

OpenCV4 不再有CV_PI,替换为:

cpp

复制代码
#include <cmath>
180 * (angle / M_PI)

修改后那一行完整:

cpp

复制代码
printf("angle : %.2f\n", 180 * (angle / M_PI));

核心考点

  1. PCA 求取物体姿态,输入是轮廓全部点坐标 (x,y);
  2. pca.mean得到轮廓质心;
  3. eigenvectors每一行是特征向量;最大特征值对应的特征向量就是物体主轴;
  4. atan2(y,x)计算向量角度,输出弧度,需要转角度;
  5. 适用:不规则物体姿态估计;只对物体轮廓点集做统计,不受轮廓旋转影响

和 minAreaRect 对比

  • minAreaRect最小外接矩形:得到旋转角度,依赖外包矩形。
  • PCA 求角度:基于点集统计分布,不受外形边界影响,更反映物体内部点分布主轴

简答背诵

该代码利用 PCA 主成分分析计算轮廓物体的主轴方向。提取轮廓全部坐标点,构造样本矩阵,执行 PCA;mean 得到物体质心;最大特征值对应的特征向量代表物体主轴方向;使用 atan2 计算旋转角度,并在图像绘制质心、主轴、次轴。PCA 角度是基于点集统计分布,和 minAreaRect 最小外接矩形角度原理不同。

工程用途

工件姿态检测、零件角度定位、目标物体方向估计。

补充坑

  • 轮廓点必须全部输入,不能压缩点,所以 findContours 使用CHAIN_APPROX_NONE
  • 特征值大小代表该方向点分布离散程度;特征值越大,该方向伸展越长。

140-人脸识别算法之EigenFace-02(已淘汰)

141-人脸识别算法之FisherFace(已淘汰)

142-人脸识别算法之LBPH(已淘汰)

三个传统人脸识别对比(必背)

表格

算法 原理 现状 关键点
EigenFace PCA 主成分分析 教学使用 只关心数据方差,对光照很敏感
FisherFace PCA+LDA 线性判别 教学使用 放大类别之间差异;比 Eigen 好,但依旧传统算法局限
LBPH 局部 LBP 纹理直方图 教学使用 不需要人脸统一尺寸;三者里面工程遗留相对最多
  • LDA 算法本身没有淘汰:机器学习、数据分析还在用;
  • FisherFace(拿 LDA 做人脸识别)这个应用淘汰。 和 PCA 逻辑一模一样:算法活着,老的人脸应用淘汰。
相关推荐
9i编程1 小时前
四大准则也还是不靠谱啊:定好了铁律,AI 照样偷懒给你看
人工智能·openai·ai编程
狂僧1 小时前
从 Markdown、Wiki 到 AI 知识库:本体与知识图谱到底处在哪一层?
人工智能
lf13210271 小时前
用 JSON Schema 管装修节点记录:从照片台账到可校验工程数据
网络·数据库·人工智能·经验分享·物联网·json·智能家居
watersink1 小时前
机器学习XGBoost
人工智能·机器学习
新知图书2 小时前
7.2 上下文记忆的优化与重整合(智能体工程)
人工智能·agent·ai agent·智能体
watersink2 小时前
机器学习极大似然估计与EM算法
人工智能·算法·机器学习
叠层归一研究院2 小时前
如何用程序搭建一个 AGI 种子系统(一):从向量种子到无限生长引擎
人工智能·python·算法·机器学习·agi
名不经传的养虾人2 小时前
从0到1:企业级AI项目迭代日记 Vol.87|记忆链路切换了,系统接管有了质量门
大数据·人工智能·ai编程·企业ai·多agent协作
GlobalInfo2 小时前
2026年显微外科手术机器人系统市场报告:市场规模、产业研究、十五五规划与发展趋势预测
大数据·人工智能·机器人