Camshift算法目的是为了解决meanshift算法中检测窗口大小固定问题,该算法利用零阶距和xy方向的一阶距来实现对窗口大小进行估算。
步骤:
1、 获取目标窗口直方图
2、 利用该直方图获取图像方向投影图
3、 利用目标窗口参数获取其窗口直方图和反向投影图(meanshift)
4、 计算其窗口反向投影图零阶距和xy方向的一阶距
5、 计算新的窗口中心
6、 判断窗口偏移量是否小于阈值,如果是,则更新窗口中心点信息,并进行步骤7,反之,更新窗口信息,在新窗口下重复3、4、5步骤
7、 更新窗口大小信息,至此完成一次迭代。
其中窗口迭代过程与meanshift算法类似,都是通过窗口直方图来获取新图像中目标中心。
其中对零阶距与窗口大小有关可以理解为其某一灰度值在目标窗口和检测窗口中的所占比值,例如,当某一灰度值的检测窗口大于目标窗口,说明这个检测窗口中灰度值数量相当于等比放大,因此图像大小也要放大。
还有一种理解是计算动态窗口大小,当反向投影图的质量大时,目标窗口也大,当质量小时,目标窗口也小,以此实现目标的缩放跟踪。或者跟踪目标的方向投影图密度一定,根据质量=边长边长密度,来得到边长。
其中还可以获取窗口的旋转角度,这里需要计算xy方向的二阶距:
代码:
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;
#define DIST 5 // 偏移向量阈值
#define NUM 1000 // 迭代次数阈值
//全局变量
bool leftButtonDownFlag=false; //左键单击后的标志位
bool leftButtonUpFlag=false; //左键单击后松开的标志位
Point Point_s; //矩形框起点
Point Point_e; //矩形框鼠标左键弹起来的终点
Point processPoint; //矩形框移动的终点
bool tracking = false;
void onMouse( int event, int x, int y, int flags, void *param )
{
if(event==CV_EVENT_LBUTTONDOWN)
{
tracking = false;
leftButtonDownFlag = true; //标志位
leftButtonUpFlag = false;
processPoint=Point(x,y); //设置左键按下点的矩形起点
Point_s=processPoint;
}
else if(event == CV_EVENT_MOUSEMOVE && leftButtonDownFlag)
{
processPoint=Point(x,y);
}
else if(event==CV_EVENT_LBUTTONUP && leftButtonDownFlag)
{
leftButtonDownFlag=false;
processPoint=Point(x,y);
Point_e=processPoint;
leftButtonUpFlag = true;
tracking = true;
}
}
/**
* @brief calHistOfROI 计算直方图
* @param img 输入图像
* @param rect 输入待计算区域
* @param hist 返回待计算区域直方图
*/
void calHistOfROI(Mat img, Rect &rect, double *hist)
{
//初始化权值矩阵和目标直方图
for (int i=0;i<180;i++)
{
hist[i] = 0.0;
}
//计算目标权值直方
for (int i = rect.y;i < rect.y + rect.height; i++)
{
for (int j = rect.x;j < rect.x + rect.width; j++)
{
hist[img.at<uchar>(i, j)]++; // 颜色权重直方图
}
}
double C = rect.height*rect.width;
//归一化直方图
for (int i=0;i<180;i++)
{
hist[i] = (double)hist[i] / C;
}
}
/**
* @brief Projection 获取反投影矩阵
* @param img 输入原始图像
* @param hist 输入直方图
* @param probImage 返回反投影直方图
*/
void Projection(Mat img, double *hist, Mat probImage)
{
double hist_max = 0.0;
for (int i=0;i<180;i++)
{
if(hist_max < hist[i])
{
hist_max = hist[i];
}
}
for(int i=0; i<probImage.rows; i++)
{
for(int j=0; j<probImage.cols; j++)
{
probImage.at<float>(i,j) = hist[img.at<uchar>(i,j)]/hist_max; // 归一化
}
}
}
/**
* @brief Meanshift
* @param probImage 输入反投影矩阵
* @param rect 待检测框参数
* @param wnd_s 输入长宽比
*/
void Meanshift(Mat probImage, double wnd_s, Rect &rect)
{
bool flag = true; // 跳出循环标志位
double M = 0.0, Mx = 0.0, My = 0.0; // 0阶距、xy方向一阶矩
double val = 0.0;
double x =0.0, y = 0.0; // 更新后右上角坐标
int count = 0;
while(flag)
{
val = 0.0;
M = 0.0, Mx = 0.0, My = 0.0;
// 计算各自阶距
for (int i = rect.y;i < rect.y + rect.height; i++)
{
for (int j = rect.x;j < rect.x + rect.width; j++)
{
val = probImage.at<float>(i,j);
M += val;
Mx += i*val;
My += j*val;
}
}
// Mx/M和My/M为更新后中点坐标
x = Mx/M - rect.width/2;
y = My/M - rect.height/2;
// 越界处理
if(x < 0)
{
x = 0;
}
else if(x >= probImage.cols - rect.width)
{
x = probImage.cols-rect.width;
}
if(y < 0)
{
y = 0;
}
else if(y >= probImage.rows-rect.height)
{
y = probImage.rows-rect.height;
}
// 循环跳出判断,1、当新坐标点与老坐标点距离小于阈值,2、循环次数小于阈值
if(pow(rect.x - x, 2) + pow(rect.y - y, 2) < DIST || count > NUM)
{
rect.x = x;
rect.y = y;
flag = false;
continue;
}
count++;
}
double s = 0.0;
s = round(1.2*sqrt(M)); //计算窗口大小
// 更新检测框大小
if(wnd_s > 1)
{
if(rect.width/wnd_s/s < 2 && rect.height/s < 2 && s > 20) // 防止退化
{
rect.width = wnd_s*s;
rect.height = s;
}
}
else
{
if(rect.width/s < 2 && rect.height/s*wnd_s < 2 && s > 20) // 防止退化
{
rect.width = s;
rect.height = s/wnd_s;
}
}
}
int main()
{
double lambda = 0.9; // 直方图更新阈值
Mat frame, hsv, probImage;
vector<Mat> channels;
Rect rect; // 识别矩形框
double wnd_s = 0.0; // 矩形框长宽比
double *m_hist, *hist; // 上一帧直方图、当前直方图
m_hist = (double *)malloc(sizeof(double)*180);
hist = (double *)malloc(sizeof(double)*180);
//打开摄像头或者特定视频
VideoCapture cap;
cap.open(0);
//读入视频是否为空
if (!cap.isOpened())
{
return -1;
}
cap >> frame;
if (frame.empty())
{
return -1;
}
probImage = Mat::zeros(frame.size(), CV_32FC1); // 初始化反投影图像
namedWindow("输出视频", 1);
setMouseCallback("输出视频", onMouse, 0);//鼠标回调函数,响应鼠标以选择跟踪区域
while (1)
{
cap >> frame;
if (frame.empty())
{
return -1;
}
// 对HSV图像中的H通道进行统计
cvtColor(frame, hsv, CV_RGB2HSV);
channels.clear();
split(hsv, channels);
// 选择目标框
if(tracking && leftButtonUpFlag)
{
leftButtonUpFlag = false;
rect.x = Point_s.x;
rect.y = Point_s.y;
rect.width = Point_e.x - Point_s.x;
rect.height = Point_e.y - Point_s.y;
wnd_s = (double)rect.width/(double)rect.height;
//目标初始化
calHistOfROI(channels[0], rect, m_hist); // 获取目标图像直方图
continue;
}
if(leftButtonDownFlag) // 绘制截取目标窗口
{
rect.x = Point_s.x;
rect.y = Point_s.y;
rect.width = processPoint.x - Point_s.x;
rect.height = processPoint.y - Point_s.y;
rectangle(frame, rect, Scalar(0, 255, 0), 3, 8, 0);
}
if(tracking) // 对目标进行跟踪
{
calHistOfROI(channels[0], rect, hist); // 获取当前帧图像直方图
for (int i=0;i<180;i++) // 更新直方图
{
m_hist[i] = lambda*m_hist[i] + (1-lambda)*hist[i];
}
Projection(channels[0], m_hist, probImage); // 获取反投影矩阵
Meanshift(probImage, wnd_s, rect); // 更新检测框参数
rectangle(frame, rect, Scalar(0, 255, 0), 3, 8, 0); // 绘制检测框
}
imshow("输出视频", frame);
waitKey(10);
}
return 0;
}
代码下载链接:https://download.csdn.net/download/OEMT_301/12107286