目录
[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())
[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. 主循环)
[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(稠密光流,代码可以参考以前的博文)
直接用光流做完整对象跟踪在工业中已基本淘汰(易漂移、无重检测);但光流作为跟踪系统的辅助模块(帧间预测、降算力)仍广泛使用,光流算法本身也在持续发展。
[① 稀疏光流 Sparse Optical Flow](#① 稀疏光流 Sparse Optical Flow)
[② 稠密光流 Dense Optical Flow](#② 稠密光流 Dense Optical Flow)
[120-CAMShift对象跟踪-04(CAMShift 作为独立的完整对象跟踪方案在工业中已基本淘汰;但在教学实验和颜色固定、背景可控的简单场景中仍在使用)](#120-CAMShift对象跟踪-04(CAMShift 作为独立的完整对象跟踪方案在工业中已基本淘汰;但在教学实验和颜色固定、背景可控的简单场景中仍在使用))
[1. 全局参数](#1. 全局参数)
[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 对比)
[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 背景建模原理简述)
[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 协方差矩阵计算)
[1. 构造输入矩阵](#1. 构造输入矩阵)
[2.eigen 函数原型](#2.eigen 函数原型)
[3. 打印特征值](#3. 打印特征值)
[4. 输出特征向量矩阵](#4. 输出特征向量矩阵)
[和 PCA 的关系](#和 PCA 的关系)
[main 函数部分](#main 函数部分)
[calcPCAOrientation () 核心函数](#calcPCAOrientation () 核心函数)
[⚠️OpenCV4 编译报错点](#⚠️OpenCV4 编译报错点)
[和 minAreaRect 对比](#和 minAreaRect 对比)
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);
detectMultiScale只接收灰度图像输入。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));
- 输入灰度图;输出
vector<Rect> faces所有检测到人脸矩形框。 1.1:图像金字塔缩放系数,每一层图像缩小 1.1 倍;数值越大速度越快,精度下降。3:组合最小邻域检测结果,候选框至少被 3 次检测命中才判定为人脸。Size(30,30):最小人脸尺寸,小于这个大小直接跳过不检测。
6. 绘制人脸框
rectangle(src, faces[t], Scalar(255,0,0),2,8,0);
蓝色绘制人脸外接矩形。
窗口输出
input image:原始图片demo‑win:绘制蓝色人脸框之后结果图 控制台输出:检测消耗的 tick 计数。
Haar 与 LBP 对比(本代码可以直接做对照实验)
表格
| Haar | LBP | |
|---|---|---|
| 特征 | Haar‑like 矩形特征 | LBP 局部二值纹理特征 |
| 运算 | 大量浮点运算 | 全部整数运算,速度更快 |
| xml 文件 | haarcascade_xxx.xml | lbpcascade_xxx.xml |
| 适用场景 | PC 追求检测效果 | 嵌入式、追求速度 |
实验操作:
- 加载 haarfile,运行,记录 tick;
- 修改 load 加载 lbpfile,其他代码不变,再次运行; 观察控制台时间,LBP 消耗 tick 明显更小,速度更快。
代码小问题
- 打印提示字符串写的
could not load image...,实际是加载模型失败,文字提示容易误导。 - 只输出 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 算法)。 工作逻辑:
- 摄像头读取画面,水平镜像翻转;灰度化 + 直方图均衡增强;
CascadeClassifierHaar 人脸检测器检测人脸;- 在人脸 ROI 上半部分划分左眼、右眼感兴趣区域;
- 第一次:Haar 眼睛检测器检测到眼睛,截取眼睛图像作为模板 tpl;
- 后续帧:不再调用 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);
flip( ,1):左右镜像,摄像头画面镜像,符合人眼看摄像头习惯。- Haar 检测器只能输入灰度图像。
equalizeHist直方图均衡:提升图像对比度,暗光环境改善检测效果。
4.detectMultiScale 多尺度检测
cpp
face_detector.detectMultiScale(gray, faces, 1.1, 3, 0, Size(30, 30));
参数含义:
1.1:每次图像金字塔缩放比例;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:摄像头实时画面
- 蓝色框:人脸;
- 红色框:左眼;
- 黄框:右眼。
关键知识点(面试)
-
Haar 级联(Viola‑Jones)
- 属于传统机器学习 (AdaBoost),不是深度学习;
- 依赖灰度图;光照变化、侧脸、遮挡容易失效;
detectMultiScale多尺度金字塔检测。
-
检测 vs 跟踪
表格
模式 实现 特点 检测 detectMultiScale Haar 分类器 精度高,速度慢;每一帧都检测开销大 跟踪 matchTemplate 模板匹配 模板匹配 速度快;模板容易漂移,光照变化容易跟丢
本代码策略:检测器做初始化,模板匹配做跟踪;缺点:一旦跟丢,模板不会自动重新初始化,眼睛框会消失,需要人脸重新进出画面才会再次初始化模板。
matchTemplate特性:
- 模板大小固定;物体缩放、旋转之后,匹配效果急剧下降;
- 适合短时间小范围运动跟踪。
代码现存缺陷
- 模板一旦初始化成功后,跟丢之后不会自动重新调用 Haar 重新检测眼睛,必须人脸消失再重新出现才会重置模板。
- 模板匹配对光照变化、眼睛缩放、旋转鲁棒差,容易漂移丢失。
- 左右眼睛共用同一个
haarcascade_eye.xml,没有区分左右眼,容易误检测眉毛等。 - Haar 检测器对暗光、侧脸、戴眼镜效果差。
简答背诵
该代码基于 Haar 级联分类器实现摄像头人脸检测,结合模板匹配实现眼睛跟踪。摄像头读取图像,镜像翻转、灰度化、直方图均衡预处理;detectMultiScale 检测人脸;在人脸的上半部分划定左右眼 ROI;当眼睛模板为空时,使用 Haar 眼睛检测器检测眼睛,截取眼睛图像作为跟踪模板;后续帧不再调用 Haar 眼睛检测,调用 matchTemplate 模板匹配在 ROI 区域完成眼睛跟踪;达到阈值则绘制眼睛框,ESC 键退出。采用 "检测初始化 + 模板匹配跟踪" 的思路,兼顾初始化精度与运行速度,但模板匹配容易受光照影响发生漂移。
拓展对比:现在工程上更多使用 dlib、OpenCV‑DNN 深度学习人脸关键点,替代老旧 Haar。
注意:(视频截图是后续涂抹的)

94-高斯混合模型(GMM)方法-原理与数据聚类(GMM-EM(高斯混合模型 + 期望最大化)作为经典无监督聚类和概率建模算法不会淘汰,在图像分割、背景建模、数据聚类等场景仍在使用;但作为复杂语义分割方案已被深度学习取代。)
代码整体概述
该代码演示 GMM‑EM 高斯混合模型 + 期望最大化算法 (OpenCV cv::ml::EM)。
- 在 500×500 画布上,生成多组服从正态高斯分布的二维随机样本点;
- 使用 EM 算法训练高斯混合模型,对样本做聚类;
- 遍历画布每一个像素,用训练好的 GMM 模型预测每个像素属于哪一类,绘制分类背景;
- 再把原始采样点绘制到画布上,观察聚类划分效果。
⚠️重点区分:
- 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);
rowRange:截取矩阵连续行;每一个簇分配一部分行;RNG::NORMAL:正态高斯分布填充;每一簇有自己的中心点 center;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());
setClustersNumber(numCluster):设置 K 个高斯分量;COV_MAT_SPHERICAL:球形协方差,每个高斯分布各个方向方差相同;还有 DIAGONAL、FULL;TermCriteria:迭代终止条件:最多迭代 100 次,或者变化小于 0.1 停止;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 计数换算为毫秒,打印预测阶段消耗时间。
窗口输出
input image:原始输入图片EM‑Segmentation:EM 颜色聚类分割结果,图像被简化为 3 种纯色; 控制台打印预测阶段耗时(ms)。
EM (GMM) 图像分割 vs KMeans 图像分割
表格
| KMeans | EM(GMM) | |
|---|---|---|
| 聚类性质 | 硬聚类,距离判断 | 软聚类,概率模型 |
| 输入样本 | 像素 BGR 特征 | 像素 BGR 特征 |
| OpenCV 接口 | kmeans() |
cv::ml::EM |
| 输出 | 每个样本类别标签 | 每个样本属于各个高斯的概率 + 类别标签 |
共同点:只使用颜色信息,没有利用空间位置,相同颜色像素归为一类。
代码存在问题
- 全部像素调用
predict2()做预测,计算量大,运行慢;直接使用labels数组效率高很多。 - 拼写错误:
could not load iamge拼写错误。 - 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 交互式图像分割(前景背景抠图)。 交互流程:
- 鼠标在图片上画矩形框,框住想要抠出的目标物体;
- 按下键盘
n执行 GrabCut 算法迭代分割; - 矩形内部标记为可能前景 GC_PR_FGD,外部是确定背景 GC_BGD;
- 算法迭代估算前景、背景高斯混合模型,输出 mask 掩码,把前景抠出来;
- 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 =1,3 &1 =1→ 前景(确定前景、可能前景都保留)0 &1 =0,2 &1 =0→ 背景屏蔽。
逐段解析
1. 全局变量
mask:单通道掩码图,存储每个像素属于上面 4 种状态;bgModel、fgModel:内部 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);
参数说明:
src:输入原图;mask:输入输出掩码,算法会修改 mask;rect:用户框选矩形;bgModel/fgModel:前景背景 GMM 模型缓冲区;iterCount:迭代次数,一般 1~5;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 颜色聚类的简易背景替换。
- 将图像全部 BGR 像素转为样本矩阵,做 KMeans 聚类(K=4);
- 拿图像左上角位置的类别当作背景类别,生成二值 mask 掩码;
- 对 mask 做腐蚀形态学操作 + 高斯模糊,得到软边缘(0‑255 过渡,不是只有 0 和 255);
- 遍历图像:前景保留原图像素;背景替换为指定紫色;模糊过渡区域做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 × 3,CV_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);
erode腐蚀:收缩前景区域,消除小噪点孔洞;- 高斯模糊:把原本只有 0/255 二值 mask 变成 0~255 连续灰度,得到软边缘,用于 alpha 混合。
mask 不再只有 0 和 255,中间会出现 1‑254 的过渡灰度,这是做抗锯齿的关键。
5. 三分支像素融合逻辑
cpp
int m = mask.at<uchar>(row, col);
m == 255:完全前景,直接复制原图 BGR;m == 0:完全背景,替换成自定义紫色;0 < m < 255:边缘过渡区域,做线性 alpha 混合: \(out = src \cdot w + color \cdot (1-w),\quad w=\frac{m}{255.0}\) w 为原图权重;1‑w为替换背景颜色权重,实现边缘柔和过渡,没有生硬锯齿。
窗口输出
input image:原始输入图片背景替换:替换背景之后输出图,物体边缘柔和。
核心优缺点
✅优点:
- 全自动,不需要用户交互框选;
- 通过高斯模糊 + alpha 混合,得到柔和物体边缘。
❌缺点:
- 强假设:左上角像素属于背景。如果左上角是前景物体,分割完全失效;
- 只依靠颜色聚类,前景和背景颜色相近时分割失败;
- 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 颜色阈值绿幕抠图在简单场景、实时应用、教学实验中仍广泛使用;但专业影视 / 直播场景中,已被更精细的算法和深度学习抠图方案补充或替代。
还在用的场景
- 教学实验:OpenCV 入门经典案例,理解 HSV 颜色空间、inRange 阈值、掩码生成、图像合成的完整流程。
- 简单实时应用:直播软件的简易抠图、视频会议虚拟背景(部分实现)、嵌入式实时抠图。
- 绿幕条件好的场景:绿幕均匀、光照稳定、无阴影、主体不含绿色,HSV 阈值法效果好且速度极快。
- 快速原型验证:项目初期快速验证抠图思路,不需要训练模型。
- 资源受限设备:纯 CPU、微控制器,几行代码就能跑。
HSV 绿幕抠图的局限性
- 对绿幕质量要求高:绿幕颜色不均、有褶皱、光照阴影都会导致抠图不干净。
- 主体含绿色时失效:人物穿绿色衣服、物体有绿色部分,会被一起抠掉。
- 边缘处理粗糙:头发丝、半透明区域(婚纱、玻璃杯)处理不好,有锯齿或绿边。
- 需要手动调阈值:不同绿幕、不同光照都要重新调 HSV 上下限。
- 光照变化敏感:光照变化导致绿色偏移,阈值失效。
专业场景的更优方案
- 专业软件算法:OBS、Premiere、After Effects 的色度键(Chroma Key),支持溢出抑制、边缘羽化、细节处理。
- 深度学习抠图:MODNet、BiRefNet、RMBG 等模型,不需要绿幕,自然场景也能抠图,边缘精细。
- 传统进阶方法:颜色差异法、泊松克隆、alpha matting(闭形式抠图),处理半透明边缘。
HSV 绿幕抠图典型流程(必背)
BGR图像 → 转HSV → inRange绿色阈值 → 形态学去噪(开运算/闭运算)
→ 生成掩码 → 反掩码 → 主体与新背景按掩码合成
对比表
表格
| 抠图方法 | 适用场景 | 效果 | 算力 |
|---|---|---|---|
| HSV 颜色阈值 | 绿幕均匀、简单场景 | 中(边缘粗糙) | 极低 |
| 专业 Chroma Key | 影视 / 直播 | 高(边缘精细) | 中 |
| 深度学习抠图 (MODNet/BiRefNet) | 任意背景 | 极高(头发丝级) | 高 |
| Alpha Matting | 半透明边缘 | 极高 | 中高 |
简答背诵
HSV 颜色空间绿幕抠图通过 inRange 阈值分割绿色背景、生成掩码、与新背景合成实现。在绿幕均匀、光照稳定的简单场景和教学实验、实时应用中仍广泛使用,优点是简单、速度快、无需训练;但对绿幕质量要求高,主体含绿色、光照变化、头发丝等半透明边缘处理效果有限。专业影视 / 直播场景中,已被更精细的色度键算法和深度学习抠图方案(如 MODNet、BiRefNet)补充或替代。
核心考点
- HSV 优于 RGB 做颜色分割:Hue(色调)通道对光照变化更鲁棒,单独控制颜色类别。
- 绿色选择原因:人皮肤中蓝色 / 红色成分多,绿色最少,且绿色传感器在 CMOS 中占比最高,信噪比好。
- 溢出抑制(Spill Suppression):去除主体边缘的绿色反光,专业抠图必备步骤。
- 形态学操作:开运算去除小噪点,闭运算填充内部空洞。
代码整体概述
该代码实现HSV 颜色空间绿幕抠图(视频版),属于色度键抠图。
- 读取视频帧,BGR 转 HSV;使用
inRange把绿色背景提取为 mask 掩码; - 形态学闭运算、腐蚀、高斯模糊,修复 mask 孔洞,生成带 0‑255 过渡的软掩码;
replace_and_blend函数:mask=255 使用替换背景图;mask=0 保留视频前景人物;中间灰度做 Alpha 加权混合,消除硬锯齿;- 逐帧处理视频,实时输出抠图结果,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_01、background_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);
- MORPH_CLOSE 闭运算:填充 mask 内部细小孔洞,消除人物内部误识别为背景的小黑点;
- erode 腐蚀:收缩背景掩码,防止人物边缘残留绿色;
- 高斯模糊:mask 不再只有 0/255,产生 0‑255 灰度过渡,用于边缘 alpha 混合,消除锯齿。
4.replace_and_blend 函数
- 尺寸自适应:
background_02图片尺寸不一定和视频帧一样,resize自动对齐分辨率; - 使用
ptr<uchar>指针遍历像素,相比at<>()速度更快,视频处理优先用指针; - 三分支逻辑:
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 退出。
窗口输出
input video:原始输入视频;result video:绿幕抠图替换背景后的输出视频。
关键知识点对比
表格
| 项目 | 本绿幕抠图 | Kmeans 背景替换 | GrabCut 分割 |
|---|---|---|---|
| 实现原理 | HSV 颜色阈值(色度键) | 像素颜色聚类 | 图割 + GMM |
| 交互 | 全自动 | 全自动 | 需要鼠标框选 |
| 适用场景 | 纯色绿幕 / 蓝幕视频 | 静态图片,背景颜色统一 | 任意物体抠图 |
| 局限 | 物体不能含有和背景相近的绿色 | 前景背景颜色接近会出错 | 需要人工交互 |
代码存在问题
- HSV 阈值是写死的;实际不同光照下绿幕 H/S/V 会偏移,固定阈值效果不稳定;
- 全局变量
background_02在函数内部被 resize 修改,会改变原始图片; - 没有处理绿幕反光、物体半透明区域(头发)的高级去绿溢出处理。
简答背诵
该代码实现基于 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 的一种。
还在用的场景
- 静态摄像头运动检测:固定摄像头下检测移动物体(人、车、异物),是最经典的方案。
- 资源受限设备:纯 CPU、嵌入式,不需要深度学习推理,速度快、开销小。
- 简单监控 / 工业场景:室内固定摄像头、流水线异物检测、入侵检测、越界检测。
- 作为预处理模块:先用 BSM 快速提取运动候选区域,再用 YOLO 做精细分类,兼顾速度和精度。
- 教学实验:背景减除是计算机视觉运动分析的基础内容。
BSM 的优势
- 无需训练数据:自动学习背景,不需要标注。
- 计算量小:CPU 实时,适合嵌入式。
- 对任意类别有效:只要在动就能检测,不局限于训练过的物体类别。
- OpenCV 内置多种算法 :
createBackgroundSubtractorMOG2()、createBackgroundSubtractorKNN()等直接调用。
局限性
- 摄像头必须静止:摄像头晃动直接失效。
- 光照突变敏感:突然开灯、阳光变化产生大量误检。
- 动态背景干扰:树叶晃动、水波、雨体会被误判为前景。
- 目标静止后消失:物体静止一段时间会被学习成背景。
- 无类别识别:只知道 "有东西在动",不知道是什么。
常见 BSM 算法对比
表格
| 算法 | OpenCV 支持 | 特点 |
|---|---|---|
| MOG2 | ✅ | 混合高斯,支持阴影检测,最常用 |
| KNN | ✅ | K 近邻,对复杂背景抗干扰更强 |
| GMG | ✅ | 像素级贝叶斯,适合静态背景 |
| LSBP | contrib | 局部纹理特征,对光照变化更鲁棒 |
工业项目中的实际用法
- 简单场景直接用 BSM:固定摄像头、背景稳定,足够用。
- BSM + 深度学习:BSM 提取运动区域 → YOLO 分类,减少计算量。
- 纯深度学习:复杂场景直接 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) 退出循环。
窗口输出
track mask:二值掩码图,白色是检测到的绿色小球;input video:原始视频画面,叠加红色跟踪框。
算法优缺点
✅优点
- 实现简单,计算速度快;
- 适合颜色特征非常突出的目标。
❌缺点
- 在 BGR 直接做 inRange,对光照敏感;光照一变,mask 就失效;
- 画面中只要出现其他大块绿色物体,就会误跟踪;
- 没有目标预测,物体短暂遮挡后直接丢失;
- 只能矩形框,不能得到物体实际轮廓。
区分:这是颜色 + 轮廓检测,不是真正的跟踪算法(MeanShift、CamShift、KCF、CSRT)。本算法每一帧都重新检测,没有利用上一帧位置做预测。
简答背诵
该代码实现基于颜色阈值 + 轮廓分析的绿色小球视频检测跟踪。读取视频帧,BGR 空间使用 inRange 筛选绿色目标生成二值 mask;使用开运算去除噪点,膨胀操作补全目标掩码。调用 findContours 查找全部最外层轮廓,遍历计算每个轮廓面积,选出面积最大轮廓,通过 boundingRect 求取外接矩形;在原图绘制红色矩形框标记小球位置;没有轮廓时矩形置零。按 ESC 退出。该方法每帧独立检测,没有目标运动预测,依赖目标颜色与背景差异,BGR 空间阈值易受光照干扰,不属于专用目标跟踪算法。
拓展对比
表格
| 方法 | 特点 |
|---|---|
| 本代码 | 颜色 + 轮廓,逐帧检测,无预测 |
| CamShift | 基于 HSV 直方图反向投影,具备简单目标跟踪,支持目标大小变化 |
| KCF/CSRT | 相关滤波跟踪,初始化后跟踪,抗部分遮挡 |
视频11111111111111111
115-光流的对象跟踪-03(稀疏光流,代码可以参考以前的博文)
116-光流的对象跟踪-04(稠密光流,代码可以参考以前的博文)
直接用光流做完整对象跟踪在工业中已基本淘汰(易漂移、无重检测);但光流作为跟踪系统的辅助模块(帧间预测、降算力)仍广泛使用,光流算法本身也在持续发展。
一句话结论
光流(Optical Flow)本身作为运动估计工具仍广泛使用;但直接用光流做完整对象跟踪在工业中已较少单独使用,更多作为跟踪 / 检测系统的辅助模块。
光流算法本身还在用的场景
- 运动估计与分析:视频稳定、动作识别、视频插帧、SLAM 视觉里程计。
- 跟踪辅助:KCF/CSRT 等跟踪器内部可能用光流辅助;检测 + 跟踪系统中用光流做帧间预测。
- 无监督 / 自监督学习:光流是视频理解任务的重要监督信号。
- 简单运动检测:Farneback 稠密光流做整体运动分析。
- 教学实验:Lucas-Kanade 稀疏光流是经典 CV 课程内容。
为什么直接用光流做对象跟踪已少用
- 漂移累积:光流是帧间增量估计,长时间跟踪误差累积,目标容易跑偏。
- 无重检测能力:目标被遮挡后再出现,无法自动恢复。
- 对光照、快速运动敏感:光照变化、大位移时光流估计不准。
- 稀疏光流依赖角点:目标纹理少、角点丢失时跟踪失败。
- 被更优方案取代 :
- 单目标:KCF/CSRT(相关滤波)更稳定
- 多目标:YOLO + ByteTrack/OC-SORT(检测驱动,ID 保持,遮挡恢复)
光流的两类核心方法
表格
| 类型 | 代表算法 | 特点 |
|---|---|---|
| 稀疏光流 | Lucas-Kanade (LK) | 只跟踪角点等特征点,速度快,OpenCV calcOpticalFlowPyrLK |
| 稠密光流 | Farneback、DIS、RAFT | 计算每个像素的运动,精度高,计算量大 |
工业项目中的实际用法
- 光流 + 检测器:用光流做帧间预测,减少检测器调用频率(如每 5 帧检测一次,中间用光流跟踪),降低算力。
- 光流 + 跟踪器:辅助相关滤波跟踪器处理快速运动。
- 运动分析任务:视频稳定、动作识别、异常行为检测,光流是核心特征。
- 简单场景:固定摄像头、目标运动平缓、纹理丰富,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、按照求解方法划分
- 基于梯度的方法(LK、Farneback)
核心假设:亮度恒定、小运动、空间一致;依靠图像灰度梯度求解。 大位移时配合图像金字塔,解决大运动问题。
- 基于匹配的方法 块匹配,把图像分小块,在下一帧搜索相似块,得到位移。
3、按照图像金字塔
- 单层光流:只在原图计算,只能处理很小位移
- 金字塔光流 (LK 金字塔 / Farneback 自带金字塔):先在缩小图算粗略运动,逐层回推原图,可以处理物体大位移。
4、另外两个概念
- 时间光流:相邻两帧之间的运动(我们代码里用的)
- 还有深度学习光流:RAFT‑Flow,不属于 OpenCV 传统光流。
对比速记表
表格
| 类型 | 算法 | 计算对象 | 速度 |
|---|---|---|---|
| 稀疏光流 | LK 金字塔光流 | 少量角点 | 快 |
| 稠密光流 | Farneback | 全部像素 | 慢 |
考试简答: 光流分为稀疏光流 和稠密光流。稀疏光流以 LK 光流为代表,只跟踪部分特征角点,计算速度快;稠密光流以 Farneback 算法为代表,为图像每个像素计算运动偏移,计算量大,可以得到完整运动场。二者都可以配合图像金字塔,解决物体大位移运动。
光流三个基本假设(简答题)
- 亮度恒定:同一个物体点在相邻帧灰度不变
- 时间连续(小运动):帧间物体移动距离不能过大
- 空间一致性:邻近的像素运动基本相同
当物体运动位移很大,破坏小运动假设 → 使用图像金字塔,逐层缩小图像,把大位移变成小位移。
120-CAMShift对象跟踪-04(CAMShift 作为独立的完整对象跟踪方案在工业中已基本淘汰;但在教学实验和颜色固定、背景可控的简单场景中仍在使用)
代码整体概述
该代码实现 CamShift 目标跟踪算法(Continuously Adaptive Mean‑Shift)。 流程:
- 第一帧使用
selectROI手动框选要跟踪的目标; - 提取 ROI 区域的 Hue(色相)一维直方图,做归一化,作为目标颜色模板;
- 对后续每一帧,计算直方图反向投影 backprojection,得到概率图,像素值越大代表越像目标颜色;
- CamShift 算法在反向投影图上迭代搜索目标,输出旋转外接矩形
RotatedRect; - 使用
ellipse绘制旋转跟踪框,可以适应目标旋转、大小缩放; - 同时可视化目标颜色直方图,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));
参数说明:
backprojection:反向投影概率图;selection:输入输出;输入上一帧搜索窗口,输出更新后的窗口位置;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、计算目标直方图、绘制直方图画布;后续帧不再重新计算直方图,一直复用第一帧的颜色模板。
窗口输出
CAMShift Tracking:视频画面,红色旋转椭圆跟踪框;ROI Histogram:目标颜色色相直方图。
CamShift / MeanShift 对比
表格
| 算法 | 窗口 | 输出 | 特点 |
|---|---|---|---|
| MeanShift | 窗口大小固定 | Rect 普通矩形 | 不能适应目标缩放旋转 |
| CamShift | 自适应窗口大小 | RotatedRect 旋转矩形 | 支持目标放大、缩小、旋转 |
优缺点
✅优点
- 算法简单,速度快;
- 可以适应目标旋转、尺寸变化;
- 不需要特征点,依靠颜色跟踪。
❌缺点
- 完全依赖颜色;画面出现和目标颜色相近物体,容易漂移丢失;
- 目标颜色发生变化(光照变化)跟踪失效;
- 没有模板更新,全程使用第一帧直方图;目标颜色改变无法自适应;
- 物体完全遮挡之后跟踪框会飘走。
简答背诵
该代码实现 CamShift 目标跟踪。第一帧手动框选跟踪目标;图像转为 HSV 空间,提取色相 H 通道,计算目标 ROI 区域的色相归一化直方图作为颜色模板。后续每一帧计算直方图反向投影,生成目标颜色概率图;CamShift 算法在概率图迭代搜索目标,自动调整搜索窗口大小,返回旋转外接矩形 RotatedRect;使用 ellipse 绘制旋转椭圆跟踪框。CamShift 在 MeanShift 基础上增加窗口自适应,可以处理目标缩放与旋转;缺点是依赖颜色特征,相似颜色物体容易造成跟踪漂移。
补充:反向投影:根据目标直方图,把图像每个像素映射为属于目标的概率,值越大越像目标。
视频11111111111111111111111
121-视频中移动对象统计(MOG2 背景建模:仍在使用,静态摄像头场景的经典选择)
MOG2 背景建模:仍在使用,静态摄像头场景的经典选择
一句话结论
MOG2 没有被淘汰,在静态摄像头、简单运动目标检测场景中仍是常用的传统方法;但在复杂场景下,工业项目常结合或转向深度学习检测方案。
还在用的场景
- 静态摄像头运动检测:固定摄像头下检测移动物体(人、车、球等),是最经典的方案。
- 资源受限设备:纯 CPU、嵌入式设备,不需要深度学习推理,速度快、开销小。
- 简单监控场景:室内固定摄像头、工业流水线异物检测、简单入侵检测。
- 教学实验:背景减除、运动目标检测的入门必学算法。
- 作为预处理模块:先 MOG2 提取运动区域,再做后续识别 / 跟踪,减少计算量。
MOG2 的优势
- 自动学习背景:不需要手动标注,自动适应光照缓慢变化。
- 支持阴影检测:可区分前景和阴影(阴影输出灰色掩码)。
- 计算量小:CPU 实时,适合嵌入式。
- OpenCV 内置 :
createBackgroundSubtractorMOG2()直接调用,无需额外模型。
局限性(复杂场景下的问题)
- 摄像头必须静止:摄像头晃动直接失效。
- 光照突变:突然开灯、闪光灯、阳光变化会产生大量误检。
- 动态背景:树叶晃动、水波、喷泉、雨体会被误判为前景。
- 目标静止后消失:物体静止一段时间会被学习成背景,不再被检测。
- 无类别识别:只知道 "有东西在动",不知道是什么物体。
工业项目中的实际用法
- 简单场景直接用 MOG2:固定摄像头、背景稳定,足够用。
- MOG2 + 深度学习:先用 MOG2 快速提取运动候选区域,再用 YOLO 做精细分类,兼顾速度和精度。
- 纯深度学习:复杂场景直接用 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. 窗口输出
input video:原始视频;motion objects:绘制框 + 编号之后的视频画面。
waitKey(50)每帧等待 50ms,控制播放速度;ESC 退出。
MOG2 背景建模原理简述
- 维护多个高斯分布,用来描述背景每个像素;
- 当前像素和背景模型匹配,判定背景;不匹配判定前景(运动物体);
- 模型会慢慢更新,适应光照缓慢变化;
- 缺点:容易产生阴影,MOG2 可以开启阴影检测,阴影输出灰色掩码。
优缺点
✅优点
- 静态摄像头下检测运动物体效果好;
- 自动学习背景,不需要手动设置阈值;
- 速度较快。
❌缺点
- 摄像头晃动直接失效;
- 树叶晃动、水波、光照突变会产生大量噪点;
- 物体静止一段时间后,会被学习成背景,目标消失;
- 会产生阴影干扰,需要额外处理。
简答背诵
该代码使用 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 维持,抗遮挡,算力要求高 |
简答背诵
- MultiTracker 是 OpenCV3 提供的多目标跟踪封装类,OpenCV4 已经移除,现在需要手动使用
vector<Ptr<Tracker>>管理多个跟踪器实例。- KCF、CSRT 单目标跟踪器仍然保留在 opencv‑contrib 扩展模块;优点是 CPU 速度快,不需要深度学习;缺点是没有重检测机制,目标被遮挡后容易漂移丢失,适合简单场景与教学实验。
- 工业实际多目标跟踪,一般采用 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 统计函数:
meanStdDev():计算图像每个通道的均值、标准差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);
参数说明:
samples:输入样本数据cov:输出协方差矩阵(注意:OpenCV 输出的是未除以样本数的求和结果)mu:输出样本均值向量- 标志位:
COVAR_ROWS:矩阵每一行是一个样本COVAR_NORMAL:计算普通协方差模式
⚠️重要坑:
calcCovarMatrix输出的cov是求和协方差矩阵,没有做归一化除以样本数目 。 想要标准协方差矩阵,需要手动除以样本数量,示例中:cov /5。
输出:
mu:1×3 向量,5 个样本各个维度的平均值cov/5:3×3 标准协方差矩阵。
数学关系考点
PCA 算法完整流程:
- 求样本均值 mu
- calcCovarMatrix 得到协方差矩阵
- 对协方差矩阵做特征值分解
eigen() - 取前 k 个最大特征值对应的特征向量作为投影基。
OpenCV 的
cv::PCA类内部就是封装了以上三步。
标志位补充
表格
| 标志 | 含义 |
|---|---|
| COVAR_ROWS | 每一行是一个样本 |
| COVAR_COLS | 每一列是一个样本 |
| COVAR_NORMAL | 标准协方差计算 |
简答背诵
meanStdDev()计算图像每个通道像素均值与标准差;彩色图像输出 3 行 1 列的 mean、stddev 矩阵,分别对应 B/G/R 通道。标准差反映像素灰度离散程度。calcCovarMatrix()用于计算样本集协方差矩阵与样本均值;COVAR_ROWS代表一行是一个样本;该函数输出 cov 为求和形式,需要手动除以样本个数得到标准协方差矩阵。- 协方差矩阵是 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)
防止控制台窗口直接一闪关闭。
⚠️关键坑点(考试常考)
cv::eigen()只接受实对称矩阵,普通非对称方阵不能用这个函数求特征值。- 输入必须是
double(CV_64F),float会报错。 - 输出
eigenvectors:行是特征向量,不是列向量,和数学课本写法不一样。 - 特征值输出降序排列,最大的放第 0 行,PCA 算法就是利用这个性质,取前面大特征值对应的向量做主成分。
和 PCA 的关系
PCA 算法内部核心就是做特征值分解:
- 计算数据协方差矩阵(实对称)
- 调用 eigen 求特征值、特征向量
- 选取前 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 函数部分
- 读取图片,转灰度,OTSU 自动阈值二值化,得到黑白二值图。
findContours查找全部轮廓,RETR_LIST取出所有层级轮廓,CHAIN_APPROX_NONE保存轮廓全部点。- 轮廓面积过滤:
100 < area <100000,过滤微小噪点、超大区域。 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));
核心考点
- PCA 求取物体姿态,输入是轮廓全部点坐标 (x,y);
pca.mean得到轮廓质心;- eigenvectors每一行是特征向量;最大特征值对应的特征向量就是物体主轴;
atan2(y,x)计算向量角度,输出弧度,需要转角度;- 适用:不规则物体姿态估计;只对物体轮廓点集做统计,不受轮廓旋转影响。
和 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 逻辑一模一样:算法活着,老的人脸应用淘汰。
