一直都觉得粒子滤波是个挺牛的东西,每次试图看文献都被复杂的数学符号搞得看不下去。一个偶然的机会发现了Rob Hess(http://web.engr.oregonstate.edu/~hess/)实现的这个粒子滤波。从代码入手,一下子就明白了粒子滤波的原理。根据维基百科上对粒子滤波的介绍(http://en.wikipedia.org/wiki/Particle_filter),粒子滤波其实有很多变种,Rob Hess实现的这种应该是最基本的一种,Sampling Importance Resampling (SIR),根据重要性重采样。下面是我对粒子滤波实现物体跟踪的算法原理的粗浅理解:
1)初始化阶段-提取跟踪目标特征
该阶段要人工指定跟踪目标,程序计算跟踪目标的特征,比如可以采用目标的颜色特征。具体到Rob Hess的代码,开始时需要人工用鼠标拖动出一个跟踪区域,然后程序自动计算该区域色调(Hue)空间的直方图,即为目标的特征。直方图可以用一个向量来表示,所以目标特征就是一个N*1的向量V。
2)搜索阶段-放狗
好,我们已经掌握了目标的特征,下面放出很多条狗,去搜索目标对象,这里的狗就是粒子particle。狗有很多种放法。比如,a)均匀的放:即在整个图像平面均匀的撒粒子(uniform distribution);b)在上一帧得到的目标附近按照高斯分布来放,可以理解成,靠近目标的地方多放,远离目标的地方少放。Rob Hess的代码用的是后一种方法。狗放出去后,每条狗怎么搜索目标呢?就是按照初始化阶段得到的目标特征(色调直方图,向量V)。每条狗计算它所处的位置处图像的颜色特征,得到一个色调直方图,向量Vi,计算该直方图与目标直方图的相似性。相似性有多种度量,最简单的一种是计算sum(abs(Vi-V)).每条狗算出相似度后再做一次归一化,使得所有的狗得到的相似度加起来等于1.
3)决策阶段
我们放出去的一条条聪明的狗向我们发回报告,“一号狗处图像与目标的相似度是0.3”,“二号狗处图像与目标的相似度是0.02”,“三号狗处图像与目标的相似度是0.0003”,“N号狗处图像与目标的相似度是0.013”...那么目标究竟最可能在哪里呢?我们做次加权平均吧。设N号狗的图像像素坐标是(Xn,Yn),它报告的相似度是Wn,于是目标最可能的像素坐标X = sum(Xn*Wn),Y = sum(Yn*Wn).
4)重采样阶段Resampling
既然我们是在做目标跟踪,一般说来,目标是跑来跑去乱动的。在新的一帧图像里,目标可能在哪里呢?还是让我们放狗搜索吧。但现在应该怎样放狗呢?让我们重温下狗狗们的报告吧。“一号狗处图像与目标的相似度是0.3”,“二号狗处图像与目标的相似度是0.02”,“三号狗处图像与目标的相似度是0.0003”,“N号狗处图像与目标的相似度是0.013”...综合所有狗的报告,一号狗处的相似度最高,三号狗处的相似度最低,于是我们要重新分布警力,正所谓好钢用在刀刃上,我们在相似度最高的狗那里放更多条狗,在相似度最低的狗那里少放狗,甚至把原来那条狗也撤回来。这就是Sampling Importance Resampling,根据重要性重采样(更具重要性重新放狗)。
(2)->(3)->(4)->(2)如是反复循环,即完成了目标的动态跟踪。
根据我的粗浅理解,粒子滤波的核心思想是随机采样+重要性重采样。既然我不知道目标在哪里,那我就随机的撒粒子吧。撒完粒子后,根据特征相似度计算每个粒子的重要性,然后在重要的地方多撒粒子,不重要的地方少撒粒子。所以说粒子滤波较之蒙特卡洛滤波,计算量较小。这个思想和RANSAC算法真是不谋而合。RANSAC的思想也是(比如用在最简单的直线拟合上),既然我不知道直线方程是什么,那我就随机的取两个点先算个直线出来,然后再看有多少点符合我的这条直线。哪条直线能获得最多的点的支持,哪条直线就是目标直线。想法非常简单,但效果很好。
代码和解释如下:
1 // particle_demo.cpp : 定义控制台应用程序的入口点。 2 // 3 4 #include "stdafx.h" 5 /************************************************************************/ 6 /* 7 Description: 基本的粒子滤波目标跟踪 8 Author: Yang Xian 9 Email: [email protected] 10 Version: 2011-11-2 11 History: 12 */ 13 /************************************************************************/ 14 #include// for standard I/O 15 #include <string> // for strings 16 #include // for controlling float print precision 17 #include // string to number conversion 18 19 #include 20 #include // Basic OpenCV structures (cv::Mat, Scalar) 21 #include // OpenCV window I/O 22 23 using namespace cv; 24 using namespace std; 25 26 // 以下这些参数对结果影响很大,而且也会根据视频内容,会对结果有很大的影响 27 const int PARTICLE_NUM = 25; // 粒子个数 28 // 粒子放入的相关区域 29 const double A1 = 2.0; 30 const double A2 = -1.0; 31 const double B0 = 1.0; 32 // 高斯随机数sigma参数 33 const double SIGMA_X = 1.0; 34 const double SIGMA_Y = 0.5; 35 const double SIGMA_SCALE = 0.001; 36 37 // 粒子结构体 38 typedef struct particle { 39 double x; // 当前x坐标 40 double y; // 当前y坐标 41 double scale; // 窗口比例系数 42 double xPre; // x坐标预测位置 43 double yPre; // y坐标预测位置 44 double scalePre; // 窗口预测比例系数 45 double xOri; // 原始x坐标 46 double yOri; // 原始y坐标 47 Rect rect; // 原始区域大小 48 MatND hist; // 粒子区域的特征直方图 49 double weight; // 该粒子的权重 50 } PARTICLE; 51 52 Mat hsv; // hsv色彩空间的输入图像 53 Mat roiImage; // 目标区域 54 MatND roiHist; // 目标区域直方图 55 Mat img; // 输出的目标图像 56 PARTICLE particles[PARTICLE_NUM]; // 粒子 57 58 int nFrameNum = 0; 59 60 bool bSelectObject = false; // 区域选择标志 61 bool bTracking = false; // 开始跟踪标志 62 Point origin; // 鼠标按下时的点位置 63 Rect selection;// 感兴趣的区域大小 64 65 // 直方图相关参数,特征的选取也会对结果影响巨大 66 // Quantize the hue to 30 levels 67 // and the saturation to 32 levels 68 // value to 10 levels 69 int hbins = 180, sbins = 256, vbin = 10; 70 int histSize[] = {hbins, sbins, vbin}; 71 // hue varies from 0 to 179, see cvtColor 72 float hranges[] = { 0, 180 }; 73 // saturation varies from 0 (black-gray-white) to 255 (pure spectrum color) 74 float sranges[] = { 0, 256 }; 75 // value varies from 0 (black-gray-white) to 255 (pure spectrum color) 76 float vranges[] = { 0, 256 }; 77 const float* ranges[] = {hranges, sranges, vranges}; 78 // we compute the histogram from the 0-th and 1-st channels 79 int channels[] = {0, 1, 2}; 80 81 // 鼠标响应函数,得到选择的区域,保存在selection 82 void onMouse(int event, int x, int y, int, void*) 83 { 84 if( bSelectObject ) 85 { 86 selection.x = MIN(x, origin.x); 87 selection.y = MIN(y, origin.y); 88 selection.width = std::abs(x - origin.x); 89 selection.height = std::abs(y - origin.y); 90 91 selection &= Rect(0, 0, img.cols, img.rows); 92 } 93 94 switch (event) 95 { 96 case CV_EVENT_LBUTTONDOWN: 97 origin = Point(x,y); 98 selection = Rect(x,y,0,0); 99 bSelectObject = true; 100 bTracking = false; 101 break; 102 case CV_EVENT_LBUTTONUP: 103 bSelectObject = false; 104 // if( selection.width > 0 && selection.height > 0 ) 105 bTracking = true; 106 nFrameNum = 0; 107 break; 108 } 109 } 110 111 // 快速排序算法排序函数 112 int particle_cmp(const void* p1,const void* p2) 113 { 114 PARTICLE* _p1 = (PARTICLE*)p1; 115 PARTICLE* _p2 = (PARTICLE*)p2; 116 117 if(_p1->weight < _p2->weight) 118 return 1; //按照权重降序排序 119 if(_p1->weight > _p2->weight) 120 return -1; 121 return 0; 122 } 123 124 const char* keys = 125 { 126 "{1| | 0 | camera number}" 127 }; 128 129 int main(int argc, const char **argv)//这里char **argv前必须用const,why? 130 { 131 int delay = 30; // 控制播放速度 132 char c; // 键值 133 134 /*读取avi文件*/ 135 VideoCapture captRefrnc("IndoorGTTest1.avi"); // 视频文件 136 137 /*打开摄像头*/ 138 //VideoCapture captRefrnc; 139 //CommandLineParser parser(argc, argv, keys);//命令解析器函数 140 //int camNum = parser.get ("1"); 141 //captRefrnc.open(camNum); 142 143 if ( !captRefrnc.isOpened()) 144 { 145 return -1; 146 } 147 148 // Windows 149 const char* WIN_RESULT = "Result"; 150 namedWindow(WIN_RESULT, CV_WINDOW_AUTOSIZE); 151 //namedWindow(WIN_RESULT, 0); 152 // namedWindow("Result",0); 153 // 鼠标响应函数 154 //setMouseCallback(WIN_RESULT, onMouse, 0); 155 setMouseCallback("Result", onMouse, 0); 156 157 Mat frame; //视频的每一帧图像 158 159 bool paused = false; 160 PARTICLE * pParticles = particles;//particles为可装PARTICLE_NUM个PARTICLE结构体的数组,所以pParticles为指向其数组的指针 161 162 while(true) //Show the image captured in the window and repeat 163 { 164 if(!paused) 165 { 166 captRefrnc >> frame; 167 if(frame.empty()) 168 break; 169 } 170 171 frame.copyTo(img); // 接下来的操作都是对src的 172 173 174 // 选择目标后进行跟踪 175 if (bTracking == true)//鼠标操作选完后 176 { 177 if(!paused) 178 { 179 nFrameNum++;//帧数计数器 180 cvtColor(img, hsv, CV_BGR2HSV); 181 Mat roiImage(hsv, selection); // 目标区域,这个构造函数第二个参数表示截取selection部分 182 183 if (nFrameNum == 1) //选择目标后的第一帧需要初始化 184 { 185 // step 1: 提取目标区域特征,难道其目标特征就是其色调的直方图分布? 186 calcHist(&roiImage, 1, channels, Mat(), roiHist, 3, histSize, ranges); 187 normalize(roiHist, roiHist); // 归一化L2 188 189 // step 2: 初始化particle 190 pParticles = particles; 191 for (int i=0; i192 { 193 pParticles->x = selection.x + 0.5 * selection.width; 194 pParticles->y = selection.y + 0.5 * selection.height; 195 pParticles->xPre = pParticles->x; 196 pParticles->yPre = pParticles->y; 197 pParticles->xOri = pParticles->x; 198 pParticles->yOri = pParticles->y; 199 pParticles->rect = selection; 200 pParticles->scale = 1.0; 201 pParticles->scalePre = 1.0; 202 pParticles->hist = roiHist; 203 pParticles->weight = 0; 204 pParticles++; 205 } 206 } 207 else //不是第一帧 208 { 209 pParticles = particles; 210 RNG rng;//随机数序列产生器 211 for (int i=0; i 212 { 213 // step 3: 求particle的transition,粒子结构中的参数全部更新过 214 double x, y, s; 215 216 pParticles->xPre = pParticles->x; 217 pParticles->yPre = pParticles->y; 218 pParticles->scalePre = pParticles->scale; 219 220 x = A1 * (pParticles->x - pParticles->xOri) + A2 * (pParticles->xPre - pParticles->xOri) + 221 B0 * rng.gaussian(SIGMA_X) + pParticles->xOri;//以当前点为中心产生高斯分布的粒子 222 pParticles->x = std::max(0.0, std::min(x, img.cols-1.0));//其实就是x,只是考虑了边界在内而已 223 224 225 y = A1 * (pParticles->y - pParticles->yOri) + A2 * (pParticles->yPre - pParticles->yOri) + 226 B0 * rng.gaussian(SIGMA_Y) + pParticles->yOri; 227 pParticles->y = std::max(0.0, std::min(y, img.rows-1.0)); 228 229 s = A1 * (pParticles->scale - 1.0) + A2 * (pParticles->scalePre - 1.0) + 230 B0 * rng.gaussian(SIGMA_SCALE) + 1.0; 231 pParticles->scale = std::max(0.1, std::min(s, 3.0)); 232 // rect参数有待考证 233 pParticles->rect.x = std::max(0, std::min(cvRound(pParticles->x - 0.5 * pParticles->rect.width * pParticles->scale), img.cols-1)); // 0 <= x <= img.rows-1 234 pParticles->rect.y = std::max(0, std::min(cvRound(pParticles->y - 0.5 * pParticles->rect.height * pParticles->scale), img.rows-1)); // 0 <= y <= img.cols-1 235 pParticles->rect.width = std::min(cvRound(pParticles->rect.width * pParticles->scale), img.cols - pParticles->rect.x); 236 pParticles->rect.height = std::min(cvRound(pParticles->rect.height * pParticles->scale), img.rows - pParticles->rect.y); 237 // Ori参数不改变 238 239 // step 4: 求particle区域的特征直方图 240 Mat imgParticle(img, pParticles->rect); 241 calcHist(&imgParticle, 1, channels, Mat(), pParticles->hist, 3, histSize, ranges); 242 normalize(pParticles->hist, pParticles->hist); // 归一化L2 243 244 // step 5: 特征的比对,更新particle权重 245 //compareHist()函数返回2个直方图之间的相似度,因为参数为CV_COMP_INTERSECT,所以返回的是最小直方图值之和 246 pParticles->weight = compareHist(roiHist, pParticles->hist, CV_COMP_INTERSECT);//其差值直接作为其权值 247 248 pParticles++; 249 } 250 251 // step 6: 归一化粒子权重 252 double sum = 0.0; 253 int i; 254 255 pParticles = particles; 256 for(i=0; i 257 { 258 sum += pParticles->weight; 259 pParticles++; 260 } 261 pParticles = particles; 262 for(i=0; i 263 { 264 pParticles->weight /= sum; 265 pParticles++; 266 } 267 268 // step 7: resample根据粒子的权重的后验概率分布重新采样 269 pParticles = particles; 270 // PARTICLE* newParticles = new PARTICLE[sizeof(PARTICLE) * PARTICLE_NUM]; 271 PARTICLE newParticles[PARTICLE_NUM]; 272 int np, k = 0; 273 274 //qsort()函数为对数组进行快速排序,其中第4个参数表示的是排序是升序还是降序 275 qsort(pParticles, PARTICLE_NUM, sizeof(PARTICLE), &particle_cmp);//这里采用的是降序排列 276 for(int i=0; i 277 { 278 np = cvRound(particles[i].weight * PARTICLE_NUM);//权值高的优先重采样 279 for(int j=0; j 280 { 281 newParticles[k++] = particles[i]; 282 if(k == PARTICLE_NUM)//重采样后达到了个数要求则直接跳出 283 goto EXITOUT; 284 } 285 } 286 while(k < PARTICLE_NUM) 287 { 288 newParticles[k++] = particles[0];//个数不够时,将权值最高的粒子重复给 289 } 290 291 EXITOUT: 292 for (int i=0; i 293 { 294 particles[i] = newParticles[i]; 295 } 296 297 }// end else 298 299 qsort(pParticles, PARTICLE_NUM, sizeof(PARTICLE), &particle_cmp); 300 301 // step 8: 计算粒子的期望,作为跟踪结果 302 Rect_<double> rectTrackingTemp(0.0, 0.0, 0.0, 0.0); 303 pParticles = particles; 304 for (int i=0; i 305 { 306 rectTrackingTemp.x += pParticles->rect.x * pParticles->weight;//坐标加上权重的偏移值 307 rectTrackingTemp.y += pParticles->rect.y * pParticles->weight; 308 rectTrackingTemp.width += pParticles->rect.width * pParticles->weight;//宽度也要加上权值的偏移值 309 rectTrackingTemp.height += pParticles->rect.height * pParticles->weight; 310 pParticles++; 311 } 312 Rect rectTracking(rectTrackingTemp); // 跟踪结果 313 314 // 显示各粒子的运动 315 for (int i=0; i 316 { 317 rectangle(img, particles[i].rect, Scalar(255,0,0)); 318 } 319 // 显示跟踪结果 320 rectangle(img, rectTracking, Scalar(0,0,255), 3); 321 322 } 323 }// end Tracking 324 325 // imshow(WIN_SRC, frame); 326 imshow(WIN_RESULT, img); 327 328 c = (char)waitKey(delay); 329 if( c == 27 ) 330 break; 331 switch(c) 332 { 333 case 'p'://暂停键 334 paused = !paused; 335 break; 336 default: 337 ; 338 } 339 }// end while 340 }