opencv3中camshift详解(一)camshiftdemo代码详解

  • Post author:
  • Post category:其他


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时就是黑色顶点。

RGB颜色方块

如果沿着途中颜色方块的对角线方向来看,就可以看到HSV颜色系统。HSV分别是Hue(色调)、Saturation(饱和度)、Value(强度)。图中可以看出Hue的范围是0°-360°,Saturation的范围是0%-100%,Value的范围是0%-100%。camshiftdemo是基于HSV颜色系统的。

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);
  1. 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   //指示直方图是否均匀的标识符
)
  1. 反向投影的原理

    直接使用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的原理。

camshiftdemo流程图



版权声明:本文为qq_37775990原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。