0. 前言
直方图反投影的结果是一个概率图,表示在特定图像位置找到给定图像内容的概率。假设我们现在知道一个物体在图像中的大概位置;概率图可用于找到对象的确切位置。目标对象最有可能的位置是在给定窗口内概率最大化的像素。因此,如果我们从初始位置开始并迭代移动,应该可以找到确切的对象位置,这就是均值漂移算法的核心思想。均值偏移算法被广泛应用于视觉跟踪相关应用中。
1. 均值漂移算法
均值漂移算法是一种迭代算法,用于定位概率函数的局部最大值,通过在预定义窗口内查找数据点的质心或加权平均值进行定位。然后算法将窗口中心移动到质心位置并重复该过程直到窗口中心收敛到一个稳定点。OpenCV
可以两种迭代停止标准:最大迭代次数和窗口中心位移值(低于给定值则认为位置已收敛到稳定点),这两个条件存储在 cv::TermCriteria
实例中。cv::meanShift
函数返回已执行的迭代次数,显然,算法执行的结果质量取决于在给定初始位置上提供的概率图的质量。在本节中,我们使用颜色直方图来表示图像,但我们也可以使用其他特征直方图来表示对象,例如,边缘方向直方图等。
2. 检测图像内容
(1) 假设我们已经确定了一个感兴趣的对象——人物面部,如下图所示:
(2) 使用 HSV
颜色空间的色调通道来检测目标对象。这意味着我们需要将图像转换为 HSV
图像,然后提取色调通道并计算定义的 (Region of Interest
, ROI
) 的一维色调直方图:
// 读取参考图像
cv::Mat image = cv::imread("4.png");
// 初始位置
cv::Rect rect(340, 260, 35, 40);
cv::rectangle(image, rect, cv::Scalar(0, 0, 255));
// 面部 ROI
cv::Mat imageROI = image(rect);
// 获取面部 ROI 的 Hue 直方图
int minSat = 65;
ColorHistogram hc;
cv::Mat colorhist = hc.getHueHistogram(imageROI, minSat);
(3) 色调直方图是使用我们添加到 ColorHistogram
类中的 getHueHistogram
方法获得的:
// 计算 1D Hue 直方图
cv::Mat getHueHistogram(const cv::Mat& image, int minSaturation=0) {
cv::Mat hist;
// 转换为 HSV 色彩空间
cv::Mat hsv;
cv::cvtColor(image, hsv, cv::COLOR_BGR2HSV);
cv::Mat mask;
if (minSaturation>0) {
std::vector<cv::Mat> v;
cv::split(hsv, v);
cv::threshold(v[1], mask, minSaturation, 255, cv::THRESH_BINARY);
}
hranges[0] = 0.0;
hranges[1] = 180.0;
channels[0] = 0;
// 计算直方图
cv::calcHist(
&hsv,
1,
channels,
mask,
hist,
1,
histSize,
ranges
);
return hist;
}
(4) 然后将生成的直方图传递给我们的 ContentFinder
类实例:
ContentFinder finder;
finder.setHistogram(colorhist);
(5) 打开第二张图片,在新的图像中定位人脸的新位置。图像同样需要首先转换到 HSV
空间,然后反向投影第一张图像的直方图:
// 第二张图像
image = cv::imread("5.png");
// 转化到 HSV 色彩空间
cv::Mat hsv;
cv::cvtColor(image, hsv, cv::COLOR_BGR2HSV);
// 获取反向投影
int ch[1] = {0};
finder.setThreshold(-1.0f);
cv::Mat result = finder.find(hsv, 0.0f, 180.0f, ch);
(6) 现在,从初始矩形区域(即第一张图像中人脸的位置)开始,OpenCV
的 cv::meanShift
算法将人脸新位置处的 rect
对象:
// 初始化窗口位置
cv::rectangle(image, rect, cv::Scalar(0, 0, 255));
// 使用均值漂移检索对象
cv::TermCriteria criteria(cv::TermCriteria::MAX_ITER | cv::TermCriteria::EPS,
10, // 最大迭代次数
1); // 或质心位置的变化小于1px
cout << "meanshift= " << cv::meanShift(result,rect,criteria) << endl;
初始(红色)和新(绿色)人脸位置如下图所示:
在以上代码中,使用 HSV
颜色空间的色调通道来检测我们的目标对象。使用色调通道是因为人脸具有独特的色彩,因此,使用像素的色调能够使人脸易于识别。基于以上考虑,首先将图像转换为 HSV
颜色空间,当使用 COLOR_BGR2HSV
标志时,色调分量是结果图像的第一个通道,该分量的取值范围为 0
到 180
(使用 cv::cvtColor
,转换后的图像与源图像的类型相同)。为了提取色调分量,使用 cv::split
函数将三通道 HSV
图像拆分为三个单通道图像,并被放入 std::vector
数组中,色调图像在其中的索引为 0
。
当使用颜色的色调分量时,考虑图像的饱和度(向量中的第 2
个元素)同样重要。事实上,当颜色的饱和度过低时,色调信息将变得不再可靠,这是因为对于低饱和度的颜色,B
、G
和 R
分量几乎相等,这将很难确定确切颜色。因此,我们需要忽略低饱和度颜色的色调分量,也就是说,它们不计入直方图中,可以通过在 getHueHistogram
方法中使用 minSat
参数忽略饱和度低于此阈值的像素来做到这一点。
3. 完整代码
完整代码包含两个头文件和一个主函数文件,头文件 colorhistogram.h
和 contentFinder.h
可以参考反向投影直方图,主函数文件 (finder.cpp
) 如下所示:
#include <iostream>
#include <vector>
using namespace std;
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/video/tracking.hpp>
#include "contentFinder.h"
#include "colorhistogram.h"
int main() {
// 读取参考图像
cv::Mat image = cv::imread("4.png");
if (!image.data) return 0;
// 初始位置
cv::Rect rect(340, 260, 35, 40);
cv::rectangle(image, rect, cv::Scalar(0, 0, 255));
// 面部 ROI
cv::Mat imageROI = image(rect);
cv::namedWindow("Image 1");
cv::imshow("Image 1", image);
// 获取面部 ROI 的 Hue 直方图
int minSat = 65;
ColorHistogram hc;
cv::Mat colorhist = hc.getHueHistogram(imageROI, minSat);
ContentFinder finder;
finder.setHistogram(colorhist);
finder.setThreshold(0.2f);
// 转化到 HSV 色彩空间
cv::Mat hsv;
cv::cvtColor(image, hsv, cv::COLOR_BGR2HSV);
// 分割图像通道
vector<cv::Mat> v;
cv::split(hsv,v);
// 消除饱和度低的像素
cv::threshold(v[1], v[1], minSat, 255, cv::THRESH_BINARY);
cv::namedWindow("Saturation mask");
cv::imshow("Saturation mask",v[1]);
// 第二张图像
image = cv::imread("5.png");
cv::namedWindow("Image 2");
cv::imshow("Image 2", image);
cv::cvtColor(image, hsv, cv::COLOR_BGR2HSV);
// 获取反向投影
int ch[1] = {0};
finder.setThreshold(-1.0f);
cv::Mat result = finder.find(hsv, 0.0f, 180.0f, ch);
// 显示反向投影结果
cv::namedWindow("Backprojection on second image");
cv::imshow("Backprojection on second image",result);
// 初始化窗口位置
cv::rectangle(image, rect, cv::Scalar(0, 0, 255));
// 使用均值漂移检索对象
cv::TermCriteria criteria(cv::TermCriteria::MAX_ITER | cv::TermCriteria::EPS,
10, // 最大迭代次数
1); // 或质心位置的变化小于1px
cout << "meanshift= " << cv::meanShift(result,rect,criteria) << endl;
// 绘制结果窗口
cv::rectangle(image, rect, cv::Scalar(0, 255, 0));
cv::namedWindow("Image 2 result");
cv::imshow("Image 2 result", image);
cv::waitKey();
return 0;
}
相关链接
OpenCV实战(1)——OpenCV与图像处理基础
OpenCV实战(2)——OpenCV核心数据结构
OpenCV实战(3)——图像感兴趣区域
OpenCV实战(4)——像素操作
OpenCV实战(5)——图像运算详解
OpenCV实战(6)——OpenCV策略设计模式
OpenCV实战(7)——OpenCV色彩空间转换
OpenCV实战(8)——直方图详解