opencv3中camshift详解(一)camshiftdemo代码解析
一、准备工作
opencv库是什么如何下载和安装不再赘述。
opencv库直接提供了利用camshift实现目标追踪的代码,位置在..\opencv\sources\samples\cpp\camshiftdemo.cpp,demo中使用的camshift函数的具体实现源码位置在..\opencv\sources\modules\video\src\camshift.cpp。搭建好opencv环境后打开就直接运行就可以看到实际效果了,运行程序前记得把电脑上的摄像头打开。环境搭建可以参考B站上的
这个视频
。
有现成的代码可以学习,就先不要去看论文了,原理看的头昏脑涨说不定还是不得其门而入,实际代码看一遍就能解决一部分问题,剩下的问题再去深究paper也不迟。
二、RGB颜色方块和HSV颜色系统
RGB颜色方块大家很熟悉了,Red、Green、Blue组成颜色方块。RGB的值都为255时就是白色顶点,都为0时就是黑色顶点。
如果沿着途中颜色方块的对角线方向来看,就可以看到HSV颜色系统。HSV分别是Hue(色调)、Saturation(饱和度)、Value(强度)。图中可以看出Hue的范围是0°-360°,Saturation的范围是0%-100%,Value的范围是0%-100%。camshiftdemo是基于HSV颜色系统的。
三、代码分析
下面对camshiftdemo的代码进行分析,理清代码的脉络。
图像的前置操作
所谓视频,分解开来就是一帧帧的图像,创建一个VideoCapture类对象cap接收摄像头传输的视频,再利用>>操作符获取一帧帧图像。在进行目标的追踪之前,需要对图像进行前置操作。
1.将图像从RGB表示转化为HSV表示
使用cvtColor()函数就可以实现颜色空间的转换。一个小知识点是在opencv中颜色方块的排列顺序是BGR,而不是熟知的RGB,因此颜色映射码是COLOR_BGR2HSV。
cvtColor(image, //输入的图像
hsv, //转化后存储的图像
COLOR_BGR2HSV //颜色映射码
);
2.提取Hue分量
获取hsv图像后,需要提取出其中的Hue分量。
int _vmin = vmin, _vmax = vmax;
inRange(hsv, Scalar(0, smin, MIN(_vmin, _vmax)),
Scalar(180, 256, MAX(_vmin, _vmax)), mask);//确保在范围内
int ch[] = { 0, 0 };
hue.create(hsv.size(), hsv.depth());
mixChannels(&hsv, 1, &hue, 1, ch, 1);//将hsv中的h通道放到hue中去,“提取” h(色调)分量
inRange()函数的功能是检查输入数组的每个元素是不是在给定范围内。代码中可以看出,检查的是hsv的像素的Hue分量是否在0-180之间,Saturation分量是否在smin-256之间,Value分量是否在MIN(_vmin, _vmax)和MAX(_vmin, _vmax)之间,返回验证矩阵mask,如果hsv的像素点满足条件,那么mask矩阵中对应位置的点置255,不满足条件的置0。
这边的HSV范围是opencv中规定的,因此Hue的范围是0-180,Saturation和Value的范围是0-255。如果是Photoshop,那么HSV的范围就是第二部分中讲过的0-360°和0-1了。
参考链接
。所以这段demo代码对Hue没有进行限制,限制了Saturation的最小值,对Value的最大值最小值都有限制。
通道复制函数mixChannels(),此函数由输入参数复制某通道到输出参数特定的通道中。函数原型如下:
C++::void mixChannels(
const Mat* src, //输入的数组,所有的数组必须有相同的尺寸和深度
size_t nsrcs, //第一个参数src输入的矩阵数
Mat* dst, //输出的数组,所有矩阵必须被初始化,且大小和深度必须与src[0]相同
size_t ndsts, //第三个参数dst输入的矩阵数
const int* fromTo,//对指定的通道进行复制的数组索引
size_t npairs) //第五个参数fromTo的索引数
从demo的代码中可以看出,输入一个矩阵hsv,输出一个size和depth与hsv完全相同的矩阵hue,复制hsv[0]通道到hue[0]通道,也就是“提取”图像的Hue分量,储存在hue矩阵中。
拿到图像Hue分量的数据后,图像的前置处理就算做完了。
ROI颜色直方图的计算与绘制
获取hue矩阵后,需要计算ROI(Region Of Interest)关于hue的一维颜色直方图。这个直方图除非重新框选,否则在循环中只计算和绘制一次。demo中代码如下:
Mat roi(hue, selection), maskroi(mask, selection);
calcHist(&roi, 1, 0, maskroi, hist, 1, &hsize, &phranges);
normalize(hist, hist, 0, 255, NORM_MINMAX);
可以看出是利用calcHist()函数计算了直方图之后再归一化到0-255。
calcHist()函数原型如下:
calcHist(
const Mat* images, //输入的数组
int nimages, //输入数组的个数
const int* channels, //需要统计的通道索引
InputArray mask, //掩膜
OutputArray hist, //输出的目标直方图
int dims, //需要计算的直方图维度
const int* histSize, //在每一维上直方图的个数。如果是一维直方图,就是竖条(bin)的个数。
const float** ranges, //每一维数值的取值范围数组
bool uniform, //直方图是否均匀的标识符
bool accumulate //是否累加。如果为true,在下次计算的时候不会首先清空hist
)
从demo代码可以看出,输入的数组&roi只有一个,统计的通道是0通道,使用的掩膜是maskroi,输出一维直方图,有16个竖条,取值范围是{0,180}。
计算出hist直方图后,可选的下一个步骤是绘制直方图图案,直方图虽然带“图”字,但其实类型是OutputArray是一个数组,并不是可视化的直方图图案。绘制直方图图案是给人看的,这一步对反向投影和camshift目标追踪并没有影响。demo中绘制直方图的相关代码如下:
histimg = Scalar::all(0);
int binW = histimg.cols / hsize;//一个bin占的宽度
Mat buf(1, hsize, CV_8UC3);//定义一个缓冲单bin矩阵,1行16列
for (int i = 0; i < hsize; i++)
buf.at<Vec3b>(i) = Vec3b(saturate_cast<uchar>(i*180. / hsize), 255, 255);
cvtColor(buf, buf, COLOR_HSV2BGR);
for (int i = 0; i < hsize; i++)
{
int val = saturate_cast<int>(hist.at<float>(i)*histimg.rows / 255);//获取直方图相对高度
rectangle(histimg, Point(i*binW, histimg.rows),
Point((i + 1)*binW, histimg.rows - val),//画出直方图,左上角坐标,右下角坐标,高度,颜色,大小,线型
Scalar(buf.at<Vec3b>(i)), -1, 8);
}
程序并不难,buf是个1行16列的矩阵,用于存放颜色数据,用于直方图hsize个bin的“染色”。
int val = saturate_cast<int>(hist.at<float>(i)*histimg.rows / 255);
val是直方图hist的相对histimg的高度。hist.at(i)获取了第i个bin直方图数据,除以255后得到百分比,再乘以histimg的行数就得到了相对高度,最后进行int的强制类型转换,转换为整数。
之后使用rectangle()函数进行16个bin的绘制。值得注意的是矩阵的坐标系以左上角为原点,y轴是向下的,而需要展示给人看的直方图图案是左下角为原点,y轴向上的。因此rectangle的两个标定点的纵坐标是histimg.rows和(histimg.rows – val)而不是0和val。利用for循环就能完成直方图的绘制了。
反向投影
demo中的反向投影代码如下:
calcBackProject(&hue, 1, 0, hist, backproj, &phranges);
-
calcBackProject()函数原型如下:
demo中的反向投影函数十分简单就不再啰嗦了。
void calcBackProject(
const Mat* images, //输入的数组
int nimages, //输入数组的个数
const int* channels, //需要统计的通道索引
InputArray hist, //输入的直方图
OutputArray backProject,//目标的反向投影
const float** ranges, //每一位数值的取值范围
double scale=1, //输出方向投影的缩放因子
bool uniform=true //指示直方图是否均匀的标识符
)
-
反向投影的原理
直接使用calcBackProject()函数可以得到反向投影backProject,但是这是只知道输入输出的“黑箱操作”,对反向投影的原理和意义还是不清楚,有必要学习一下。
图像的的反向投影实际上是一种概率密度图,根据直方图计算一张图像各个像素点的概率分布。
举个例子,如果将camshift用于人脸追踪,那么框选人脸后作为ROI将会得到脸的Hue直方图。可以看出,ROI内的绝大多数像素拥有的都是人脸的Hue值,因此统计下来这部分像素的数目也是压倒性的多,显示在直方图上就是这个bin很高。
获取ROI的直方图之后,再根据直方图“检测”整个图像,从而得到反向投影,比如图中有像素点A,拥有的Hue是170,查询直方图可以发现170所属区间的bin值,把值填入反向投影的对应位置中。因为直方图已经归一化至0-255,所以反向投影中像素的最大值也就是255。可以看出反向投影确实是一种概率分布图,人脸区域的数值要大于其他区域,反向投影也是灰度图,概率分布体现在直观图像上就是越可能是人脸的部分亮度越高。
这里只是举了个人脸追踪的应用,在其他应用中直方图不一定是ROI的Hue直方图,也不一定是一维直方图。
camshift函数
获取反向投影和追踪框的数据后,就可以进行最关键的camshift目标追踪了。CamShift()函数原型如下:
RotatedRect CamShift(
InputArray _probImage,
Rect& window,
TermCriteria criteria
)
在demo中的代码如下:
RotatedRect trackBox = CamShift(
backproj, //反向投影
trackWindow, //搜索框矩形
TermCriteria(TermCriteria::EPS | TermCriteria::COUNT, 10, 1)//迭代中止条件
);
CamShift函数接受3个参数:反向投影,矩形搜索框,和迭代中止条件。迭代中止条件可以参考
TermCriteria模板类
。
获取CamShift的返回值,是一个旋转矩形,根据旋转矩形绘制一个椭圆形显示在图像上作为追踪结果。
总结
简单总结了一下camshiftdemo流程图。demo中只是使用了camshift函数接口,camshift的原理并没有涉及,在下一篇文章中会主要解释一下camshift的原理。