0.前言
接着上一篇文章点击打开链接说,opencv中提供的meanshift可以用来实现跟踪,其基本原理是迭代求解概率分布的“局部极值”。这一篇内容,我只讲opencv中的meanshift的用法和源代码分析。因为:(1)具体的数学原理推导,涉及到一些其他方面的知识譬如无参数估计、核函数等方面内容较多(2)opencv中的meanshift严格意义上来说是最简化版本的算法,结合着数学原理讲反而不好讲。。所以后面会再写一篇文章,专门讲meanShift的数学原理推导,和C++的具体实现
1.meanShift原理的直观表现
如下图所示,一堆分布不均匀的桌球,我们想不通过遍历的方式找到最密集的区域。一种做法是,框选一个区域(途中蓝色圆圈),计算该区域的“重心”,将圆环圆心移动到重心处再次计算“重心”,重复上述步骤,直到满足一定条件(迭代次数或者变化范围),最终我们可以到达一个局部密度最集中的区域。这个操作看起来很合乎常识,不过在数学上它是可以被证明的,而且证明过程比想象中复杂一丢丢(所以放在下一篇文章中讲。。)。
2.opencv中使用meanshift
再把上一篇文章中的猴子脸部“概率密度”分布图拿过来,分布图中每一点的灰度值代表其分布概率的高低,我们就可以理解为高概率的地方就是桌球分布更加密集的地方。那后面就好办了,框选一个区域,计算该区域的重心,将区域中心移动至重心处;重复上述过程即可迭代求解“概率密度”分布图的局部极值了呗。事实上opencv的meanShift就是提供了这样的接口和操作。直接把meanshift的调用和执行结果放出来。代码如下:
#include <iostream>
#include <vector>
#include <core/core.hpp>
#include <imgproc/imgproc.hpp>
#include <highgui/highgui.hpp>
#include <opencv2/nonfree/features2d.hpp>
#include <features2d/features2d.hpp>
#include <legacy/legacy.hpp>
#include <opencv2/video/tracking.hpp>
using namespace std;
using namespace cv;
int meanShiftTest(Mat probImage, Rect& window, int maxCount, double epsilon);
int main()
{
/* 第一部分:将输入图像转为灰度图后计算反投影 */
Mat image = imread("baboon1.jpg", 0);
Mat image_2 = imread("baboon3.jpg", 0);
Mat imageROI = image(Rect(110, 260, 35, 40));
Mat image_show;
image.copyTo(image_show);
rectangle(image_show, Rect(110, 260, 35, 40), Scalar(0));
imshow("image_show", image_show);
int histSize[1];
float hranges[2];
const float* ranges[1];
int channels[1];
histSize[0] = 256;
hranges[0] = 0.f;
hranges[1] = 255.f;
ranges[0] = hranges;
channels[0] = 0;
MatND hist;
calcHist(&imageROI, 1, channels, Mat(), hist, 1, histSize, ranges);
normalize(hist, hist, 1.0);
Mat result;
calcBackProject(&image_2, 1, channels, hist, result, ranges, 255.0);
imshow("result", result);
/* 第二部分:转换为HSV后利用颜色信息计算反投影 */
image = imread("baboon1.jpg");
image_2 = imread("baboon3.jpg");
imageROI = image(Rect(110, 260, 35, 40));
image.copyTo(image_show);
rectangle(image_show, Rect(110, 260, 35, 40), Scalar(0, 0, 0));
imshow("image_show_hsv", image_show);
Mat image_hsv;
cvtColor(imageROI, image_hsv, CV_BGR2HSV);
Mat mask;
int minSaturation = 65;
vector<Mat> v;
split(image_hsv, v);
threshold(v[1], mask, minSaturation, 255, THRESH_BINARY);
histSize[0] = 256;
hranges[0] = 0.f;
hranges[1] = 180.f;
ranges[0] = hranges;
channels[0] = 0;
MatND hist_hsv;
calcHist(&image_hsv, 1, channels, mask, hist_hsv, 1, histSize, ranges);
normalize(hist_hsv, hist_hsv, 1.0);
Mat image_2_hsv;
cvtColor(image_2, image_2_hsv, CV_BGR2HSV);
vector<Mat> v_2;
split(image_2_hsv, v_2);
threshold(v_2[1], v_2[1], minSaturation, 255, THRESH_BINARY);
Mat result_hsv;
calcBackProject(&image_2_hsv, 1, channels, hist_hsv, result_hsv, ranges, 255.0);
bitwise_and(result_hsv, v_2[1], result_hsv);
imshow("result_hsv", result_hsv);
/* 第三部分:调用opencv中的 meanShift计算位置 */
Rect rect(110, 260, 35, 40);
TermCriteria criteria(TermCriteria::MAX_ITER, 10, 0.01);
long long t = getTickCount();
meanShift(result_hsv, rect, criteria);
cout<<"time: "<<(getTickCount() - t)/getTickFrequency()<<endl;
rectangle(image_2, rect, Scalar(255, 255, 255));
/* 测试自己写的meanshift */
Rect rect2(110, 260, 35, 40);
t = getTickCount();
meanShiftTest(result_hsv, rect2, 30, 0.01);
cout<<"time: "<<(getTickCount() - t)/getTickFrequency()<<endl;
//rectangle(image_2, rect2, Scalar(255, 255, 255));
rectangle(image_2, Rect(110, 260, 35, 40), Scalar(0, 0, 0));
imshow("image_show_hsv_result", image_2);
cv::waitKey();
return 0;
}
int meanShiftTest(Mat probImage, Rect& window, int maxCount, double epsilon)
{
if(probImage.type() != CV_8UC1)
{
return -1;
}
/* 应该要做window是否合适的判断,譬如参数是否合适,是否位于probImage内部等
* 这里偷懒不写了
*/
Mat imageROI = probImage(window);
int x_o = imageROI.cols / 2;
int y_o = imageROI.rows / 2;
for(int i = 0; i < maxCount; i++)// 迭代次数
{
float x_dst, y_dst;
x_dst = y_dst = 0.f;
float weight_sum = 0;
int sum = 0;
/* 计算meanshift向量(重心相对于中心的偏移) */
for(int m = 0; m < imageROI.rows; m++)
{
for(int n = 0; n< imageROI.cols; n++)
{
if(imageROI.at<unsigned char>(m,n))
{
sum++;
int weight = imageROI.at<unsigned char>(m,n);
weight_sum += weight;
x_dst += (n - x_o)*weight;
y_dst += (m - y_o)*weight;
}
}
}
if(sum > 0)
{
x_dst /= (sum*weight_sum);
y_dst /= (sum*weight_sum);
window.x += x_dst;
window.y += y_dst;
if(fabs(x_dst) + fabs(y_dst) < epsilon)// 位移变化阈值
break;
}
imageROI = probImage(window);
}
return 0;
}
计算结果如下图所示,最右边图像中的黑色方框是初始位置,白色方框是最终计算的位置;中间的概率分布图就是meanShift的输入。结果看来很好的找到了新图像中猴子的脸部。
3.meanShift函数源码分析
把opencv310版本的meanShift()抠出来加上注释如下(函数位于opencv\sources\modules\video\src\camshift.cpp中)
int cv::meanShift( InputArray _probImage, Rect& window, TermCriteria criteria )
{
CV_INSTRUMENT_REGION()
Size size;
int cn;
Mat mat;
UMat umat;
bool isUMat = _probImage.isUMat();
if (isUMat)
umat = _probImage.getUMat(), cn = umat.channels(), size = umat.size();
else
mat = _probImage.getMat(), cn = mat.channels(), size = mat.size();
Rect cur_rect = window;
CV_Assert( cn == 1 );// 单通道图像
if( window.height <= 0 || window.width <= 0 )
CV_Error( Error::StsBadArg, "Input window has non-positive sizes" );
window = window & Rect(0, 0, size.width, size.height);// 选择区域位于图像内部
double eps = (criteria.type & TermCriteria::EPS) ? std::max(criteria.epsilon, 0.) : 1.;
eps = cvRound(eps*eps);
int i, niters = (criteria.type & TermCriteria::MAX_ITER) ? std::max(criteria.maxCount, 1) : 100;
for( i = 0; i < niters; i++ )
{
cur_rect = cur_rect & Rect(0, 0, size.width, size.height);
if( cur_rect == Rect() )
{
cur_rect.x = size.width/2;
cur_rect.y = size.height/2;
}
cur_rect.width = std::max(cur_rect.width, 1);
cur_rect.height = std::max(cur_rect.height, 1);// 选择区域位于图像内部
Moments m = isUMat ? moments(umat(cur_rect)) : moments(mat(cur_rect));// 计算图像的矩
// Calculating center of mass
if( fabs(m.m00) < DBL_EPSILON )
break;
int dx = cvRound( m.m10/m.m00 - window.width*0.5 );// m.m10/m.m00就是图像重心的x坐标
int dy = cvRound( m.m01/m.m00 - window.height*0.5 );// m.m01/m.m00就是图像重心的y坐标
int nx = std::min(std::max(cur_rect.x + dx, 0), size.width - cur_rect.width);
int ny = std::min(std::max(cur_rect.y + dy, 0), size.height - cur_rect.height);
dx = nx - cur_rect.x;
dy = ny - cur_rect.y;
cur_rect.x = nx;
cur_rect.y = ny;// 更新区域中心
// Check for coverage centers mass & window
if( dx*dx + dy*dy < eps )
break;
}
window = cur_rect;
return i;
}
4.其他
(1)代码中的meanShiftTest()是自己写的用于验证meanShift原理的测试程序,通过测试验证对原理的理解没有问题
(2)meanShift得到的是“局部极值”,不能保证一定收敛到全局极值。可以尝试把猴子脸部区域的初始位置修改一下就会发现此时不一定能找到新图像中猴子脸部的正确位置了。
(3)既然meanshift配合calcBackProject可以实现新图像中某区域的定位,那么稍加修改就能实现简单的视频中物体跟踪。唯一区别就在于逐帧读取图像后调用meanshift不断更新区域的位置了。可以参考http://blog.csdn.net/dcrmg/article/details/52694557
(4)使用C++自己实现meanShift跟踪,可参考点击打开链接