OpenCV学习笔记(五)卡尔曼滤波器
卡尔曼滤波器 – Kalman Filter
1. 什么是卡尔曼滤波器 (What is the Kalman Filter?)
在学习卡尔曼滤波器之前,首先看看为什么叫“卡尔曼”。跟其他著名的理论(例如傅立叶变换,泰勒级数等等)一样,卡尔曼也是一个人的名字,而跟他们不同的是,他是个现代人!
卡尔曼全名Rudolf Emil Kalman,匈牙利数学家,1930年出生于匈牙利首都布达佩斯。1953,1954年于麻省理工学院分别获得电机工程学士及硕士学位。1957年于哥伦比亚大学获得博士学位。我们现在要学习的卡尔曼滤波器,正是源于他的博士论文和1960年发表的论文《A New Approach to Linear Filtering and Prediction Problems》(线性滤波与预测问题的新方法)。如果对这编论文有兴趣,可以到这里的地址下载: http://www.cs./~welch/media/pdf/Kalman1960.pdf。
简单来说,卡尔曼滤波器是一个“optimal recursive data processing algorithm(最优化自回归数据处理算法)”。对于解决很大部分的问题,他是最优,效率最高甚至是最有用的。他的广泛应用已经超过30年,包括机器人导航,控制,传感器数据融合甚至在军事方面的雷达系统以及导弹追踪等等。近年来更被应用于计算机图像处理,例如头脸识别,图像分割,图像边缘检测等等。
2.卡尔曼滤波器的介绍 (Introduction to the Kalman Filter)
为了可以更加容易的理解卡尔曼滤波器,这里会应用形象的描述方法来讲解,而不是像大多数参考书那样罗列一大堆的数学公式和数学符号。但是,他的5条公式是其核心内容。结合现代的计算机,其实卡尔曼的程序相当的简单,只要你理解了他的那5条公式。
在介绍他的5条公式之前,先让我们来根据下面的例子一步一步的探索。
假设我们要研究的对象是一个房间的温度。根据你的经验判断,这个房间的温度是恒定的,也就是下一分钟的温度等于现在这一分钟的温度(假设我们用一分钟来做时间单位)。假设你对你的经验不是100%的相信,可能会有上下偏差几度。我们把这些偏差看成是高斯白噪声(White Gaussian Noise),也就是这些偏差跟前后时间是没有关系的而且符合高斯分配(Gaussian Distribution)。另外,我们在房间里放一个温度计,但是这个温度计也不准确的,测量值会比实际值偏差。我们也把这些偏差看成是高斯白噪声。
好了,现在对于某一分钟我们有两个有关于该房间的温度值:你根据经验的预测值(系统的预测值)和温度计的值(测量值)。下面我们要用这两个值结合他们各自的噪声来估算出房间的实际温度值。
假如我们要估算k时刻的是实际温度值。首先你要根据k-1时刻的温度值,来预测k时刻的温度。因为你相信温度是恒定的,所以你会得到k时刻的温度预测值是跟k-1时刻一样的,假设是23度,同时该值的高斯噪声的偏差是5度(5是这样得到的:如果k-1时刻估算出的最优温度值的偏差是3,你对自己预测的不确定度是4度,他们平方相加再开方,就是5)。然后,你从温度计那里得到了k时刻的温度值,假设是25度,同时该值的偏差是4度。
由于我们用于估算k时刻的实际温度有两个温度值,分别是23度和25度。究竟实际温度是多少呢?相信自己还是相信温度计呢?究竟相信谁多一点,我们可以用他们的covariance来判断。因为Kg^2=5^2/(5^2+4^2),所以Kg=0.78,我们可以估算出k时刻的实际温度值是:23+0.78*(25-23)=24.56度。可以看出,因为温度计的covariance比较小(比较相信温度计),所以估算出的最优温度值偏向温度计的值。
现在我们已经得到k时刻的最优温度值了,下一步就是要进入k+1时刻,进行新的最优估算。到现在为止,好像还没看到什么自回归的东西出现。对了,在进入k+1时刻之前,我们还要算出k时刻那个最优值(24.56度)的偏差。算法如下:((1-Kg)*5^2)^0.5=2.35。这里的5就是上面的k时刻你预测的那个23度温度值的偏差,得出的2.35就是进入k+1时刻以后k时刻估算出的最优温度值的偏差(对应于上面的3)。
就是这样,卡尔曼滤波器就不断的把covariance递归,从而估算出最优的温度值。他运行的很快,而且它只保留了上一时刻的covariance。上面的Kg,就是卡尔曼增益(Kalman Gain)。他可以随不同的时刻而改变他自己的值,是不是很神奇!
下面就要言归正传,讨论真正工程系统上的卡尔曼。
3. 卡尔曼滤波器算法 (The Kalman Filter Algorithm)
在这一部分,我们就来描述源于Dr Kalman 的卡尔曼滤波器。下面的描述,会涉及一些基本的概念知识,包括概率(Probability),随即变量(Random Variable),高斯或正态分配(Gaussian Distribution)还有State-space Model等等。但对于卡尔曼滤波器的详细证明,这里不能一一描述。
首先,我们先要引入一个离散控制过程的系统。该系统可用一个线性随机微分方程(Linear Stochastic Difference equation)来描述: X(k)=A X(k-1)+B U(k)+W(k) 再加上系统的测量值: Z(k)=H X(k)+V(k) 上两式子中,X(k)是k时刻的系统状态,U(k)是k时刻对系统的控制量。A和B是系统参数,对于多模型系统,他们为矩阵。Z(k)是k时刻的测量值,H是测量系统的参数,对于多测量系统,H为矩阵。W(k)和V(k)分别表示过程和测量的噪声。他们被假设成高斯白噪声(White Gaussian Noise),他们的covariance 分别是Q,R(这里我们假设他们不随系统状态变化而变化)。
对于满足上面的条件(线性随机微分系统,过程和测量都是高斯白噪声),卡尔曼滤波器是最优的信息处理器。下面我们来用他们结合他们的covariances 来估算系统的最优化输出(类似上一节那个温度的例子)。
首先我们要利用系统的过程模型,来预测下一状态的系统。假设现在的系统状态是k,根据系统的模型,可以基于系统的上一状态而预测出现在状态: X(k|k-1)=A X(k-1|k-1)+B U(k) ……….. (1) 式(1)中,X(k|k-1)是利用上一状态预测的结果,X(k-1|k-1)是上一状态最优的结果,U(k)为现在状态的控制量,如果没有控制量,它可以为0。
到现在为止,我们的系统结果已经更新了,可是,对应于X(k|k-1)的covariance还没更新。我们用P表示covariance: P(k|k-1)=A P(k-1|k-1) A’+Q ……… (2) 式(2)中,P(k|k-1)是X(k|k-1)对应的covariance,P(k-1|k-1)是X(k-1|k-1)对应的covariance,A’表示A的转置矩阵,Q是系统过程的covariance。式子1,2就是卡尔曼滤波器5个公式当中的前两个,也就是对系统的预测。
现在我们有了现在状态的预测结果,然后我们再收集现在状态的测量值。结合预测值和测量值,我们可以得到现在状态(k)的最优化估算值X(k|k): X(k|k)= X(k|k-1)+Kg(k) (Z(k)-H X(k|k-1)) ……… (3) 其中Kg为卡尔曼增益(Kalman Gain): Kg(k)= P(k|k-1) H’ / (H P(k|k-1) H’ + R) ……… (4)
到现在为止,我们已经得到了k状态下最优的估算值X(k|k)。但是为了要另卡尔曼滤波器不断的运行下去直到系统过程结束,我们还要更新k状态下X(k|k)的covariance: P(k|k)=(I-Kg(k) H)P(k|k-1) ……… (5) 其中I 为1的矩阵,对于单模型单测量,I=1。当系统进入k+1状态时,P(k|k)就是式子(2)的P(k-1|k-1)。这样,算法就可以自回归的运算下去。
卡尔曼滤波器的原理基本描述了,式子1,2,3,4和5就是他的5 个基本公式。根据这5个公式,可以很容易的实现计算机的程序
4月4日
OpenCV学习笔记(四)运动物体跟踪的camshift算法CamShift算法 Back Projection计算。 Mean Shift算法 CamShift算法 1 Back Projection计算 1. 计算被跟踪目标的色彩直方图。在各种色彩空间中,只有HSI空间(或与HSI类似的色彩空间)中的H分量可以表示颜色信息。所以在具体的计算过程中,首先将其他的色彩空间的值转化到HSI空间,然后会其中的H分量做1D直方图计算。 2. 根据获得的色彩直方图将原始图像转化成色彩概率分布图像,这个过程就被称作"Back Projection"。 在OpenCV中的直方图函数中,包含Back Projection的函数,函数原型是: void cvCalcBackProject(IplImage** img, CvArr** backproject, const CvHistogram* hist); 传递给这个函数的参数有三个: 1. IplImage** img:存放原始图像,输入。 2. CvArr** backproject:存放Back Projection结果,输出。 3. CvHistogram* hist:存放直方图,输入
下面就给出计算Back Projection的OpenCV代码。 1.准备一张只包含被跟踪目标的图片,将色彩空间转化到HSI空间,获得其中的H分量: IplImage* target=cvLoadImage("target.bmp",-1); //装载图片 IplImage* target_hsv=cvCreateImage( cvGetSize(target), IPL_DEPTH_8U, 3 ); IplImage* target_hue=cvCreateImage( cvGetSize(target), IPL_DEPTH_8U, 3 ); cvCvtColor(target,target_hsv,CV_BGR2HSV); //转化到HSV空间 cvSplit( target_hsv, target_hue, NULL, NULL, NULL ); //获得H分量 2.计算H分量的直方图,即1D直方图: IplImage* h_plane=cvCreateImage( cvGetSize(target_hsv),IPL_DEPTH_8U,1 ); int hist_size[]={255}; //将H分量的值量化到[0,255] float* ranges[]={ {0,360} }; //H分量的取值范围是[0,360) CvHistogram* hist=cvCreateHist(1, hist_size, ranges, 1); cvCalcHist(&target_hue, hist, 0, NULL); 在这里需要考虑H分量的取值范围的问题,H分量的取值范围是[0,360),这个取值范围的值不能用一个byte来表示,为了能用一个byte表示,需要将H值做适当的量化处理,在这里我们将H分量的范围量化到[0,255]. 4.计算Back Projection: IplImage* rawImage; //---------------------------------------------- //get from video frame,unsigned byte,one channel //---------------------------------------------- IplImage* result=cvCreateImage(cvGetSize(rawImage),IPL_DEPTH_8U,1); cvCalcBackProject(&rawImage,result,hist); 5.结果:result即为我们需要的. 2) Mean Shift算法 这里来到了CamShift算法,OpenCV实现的第二部分,这一次重点讨论Mean Shift算法。 在讨论Mean Shift算法之前,首先讨论在2D概率分布图像中,如何计算某个区域的重心(Mass Center)的问题,重心可以通过以下公式来计算: 1.计算区域内0阶矩 for(int i=0;i<height;i++) for(int j=0;j<width;j++) M00+=I(i,j) 2.区域内1阶矩: for(int i=0;i<height;i++) for(int j=0;j<width;j++) { M10+=i*I(i,j); M01+=j*I(i,j); } 3.则Mass Center为: Xc=M10/M00; Yc=M01/M00 接下来,讨论Mean Shift算法的具体步骤,Mean Shift算法可以分为以下4步: 1.选择窗的大小和初始位置. 2.计算此时窗口内的Mass Center. 3.调整窗口的中心到Mass Center. 4.重复2和3,直到窗口中心"会聚",即每次窗口移动的距离小于一定的阈值。
在OpenCV中,提供Mean Shift算法的函数,函数的原型是: int cvMeanShift(IplImage* imgprob,CvRect windowIn, CvTermCriteria criteria,CvConnectedComp* out);
需要的参数为: 1.IplImage* imgprob:2D概率分布图像,传入; 2.CvRect windowIn:初始的窗口,传入; 3.CvTermCriteria criteria:停止迭代的标准,传入; 4.CvConnectedComp* out:查询结果,传出。 (注:构造CvTermCriteria变量需要三个参数,一个是类型,另一个是迭代的最大次数,最后一个表示特定的阈值。例如可以这样构造criteria:criteria=cvTermCriteria(CV_TERMCRIT_ITER|CV_TERMCRIT_EPS,10,0.1)。)
返回的参数: 1.int:迭代的次数。
实现代码:暂时缺 3) CamShift算法 在了解了MeanShift算法以后,我们将MeanShift算法扩展到连续图像序列(一般都是指视频图像序列),这样就形成了CamShift算法。CamShift算法的全称是"Continuously Apaptive Mean-SHIFT",它的基本思想是视频图像的所有帧作MeanShift运算,并将上一帧的结果(即Search Window的中心和大小)作为下一帧MeanShift算法的Search Window的初始值,如此迭代下去,就可以实现对目标的跟踪。整个算法的具体步骤分5步: Step 1:将整个图像设为搜寻区域。 Step 2:初始话Search Window的大小和位置。 Step 3:计算Search Window内的彩色概率分布,此区域的大小比Search Window要稍微大一点。 Step 4:运行MeanShift。获得Search Window新的位置和大小。 Step 5:在下一帧视频图像中,用Step 3获得的值初始化Search Window的位置和大小。跳转到Step 3继续运行。 2.实现 在OpenCV中,有实现CamShift算法的函数,此函数的原型是: cvCamShift(IplImage* imgprob, CvRect windowIn, CvTermCriteria criteria, CvConnectedComp* out, CvBox2D* box=0); 其中: imgprob:色彩概率分布图像。 windowIn:Search Window的初始值。 Criteria:用来判断搜寻是否停止的一个标准。 out:保存运算结果,包括新的Search Window的位置和面积。 box:包含被跟踪物体的最小矩形。
说明: 1.在OpenCV 4.0 beta的目录中,有CamShift的例子。遗憾的是这个例子目标的跟踪是半自动的,即需要人手工选定一个目标。我正在努力尝试全自动的目标跟踪,希望可以和大家能在这方面与大家交流。 3月31日
OpenCV学习笔记(三)人脸检测的代码分析OpenCV学习笔记(三)人脸检测的代码分析
一、预备知识: 1、动态内存存储及操作函数 CvMemStorage typedef struct CvMemStorage { struct CvMemBlock* bottom;/* first allocated block */ struct CvMemBlock* top; /* the current memory block - top of the stack */ struct CvMemStorage* parent; /* borrows new blocks from */ int block_size; /* block size */ int free_space; /* free space in the top block (in bytes) */ } CvMemStorage; 内存存储器是一个可用来存储诸如序列,轮廓,图形,子划分等动态增长数据结构的底层结构。它是由一系列以同等大小的内存块构成,呈列表型 ---bottom 域指的是列首,top 域指的是当前指向的块但未必是列尾.在bottom和top之间所有的块(包括bottom, 不包括top)被完全占据了空间;在 top和列尾之间所有的块(包括块尾,不包括top)则是空的;而top块本身则被占据了部分空间 -- free_space 指的是top块剩余的空字节数。新分配的内存缓冲区(或显示的通过 cvMemStorageAlloc 函数分配,或隐示的通过 cvSeqPush, cvGraphAddEdge等高级函数分配)总是起始于当前块(即top块)的剩余那部分,如果剩余那部分能满足要求(够分配的大小)。分配后,free_space 就减少了新分配的那部分内存大小,外加一些用来保存适当列型的附加大小。当top块的剩余空间无法满足被分配的块(缓冲区)大小时,top块的下一个存储块被置为当前块(新的top块) -- free_space 被置为先前分配的整个块的大小。如果已经不存在空的存储块(即:top块已是列尾),则必须再分配一个新的块(或从parent那继承,见 cvCreateChildMemStorage)并将该块加到列尾上去。于是,存储器(memory storage)就如同栈(Stack)那样, bottom指向栈底,(top, free_space)对指向栈顶。栈顶可通过 cvSaveMemStoragePos保存,通过 cvRestoreMemStoragePos 恢复指向, 通过 cvClearStorage 重置。
CvMemBlock
内存存储块结构 typedef struct CvMemBlock { struct CvMemBlock* prev; struct CvMemBlock* next; } CvMemBlock; CvMemBlock 代表一个单独的内存存储块结构。 内存存储块中的实际数据存储在 header块 之后(即:存在一个头指针 head 指向的块 header ,该块不存储数据),于是,内存块的第 i 个字节可以通过表达式 ((char*)(mem_block_ptr+1))[i] 获得。然而,通常没必要直接去获得存储结构的域。
CvMemStoragePos 内存存储块地址 typedef struct CvMemStoragePos { CvMemBlock* top; int free_space; } CvMemStoragePos; 该结构(如以下所说)保存栈顶的地址,栈顶可以通过 cvSaveMemStoragePos 保存,也可以通过 cvRestoreMemStoragePos 恢复。
________________________________________ cvCreateMemStorage 创建内存块 CvMemStorage* cvCreateMemStorage( int block_size=0 ); block_size:存储块的大小以字节表示。如果大小是 0 byte, 则将该块设置成默认值 当前默认大小为64k.
函数 cvCreateMemStorage 创建一内存块并返回指向块首的指针。起初,存储块是空的。头部(即:header)的所有域值都为 0,除了 block_size 外. ________________________________________ cvCreateChildMemStorage 创建子内存块 CvMemStorage* cvCreateChildMemStorage( CvMemStorage* parent ); parent 父内存块
函数 cvCreateChildMemStorage 创建一类似于普通内存块的子内存块,除了内存分配/释放机制不同外。当一个子存储块需要一个新的块加入时,它就试图从parent 那得到这样一个块。如果 parent 中 还未被占据空间的那些块中的第一个块是可获得的,就获取第一个块(依此类推),再将该块从 parent 那里去除。如果不存在这样的块,则 parent 要么分配一个,要么从它自己 parent (即:parent 的 parent) 那借个过来。换句话说,完全有可能形成一个链或更为复杂的结构,其中的内存存储块互为 child/ parent 关系(父子关系)。当子存储结构被释放或清除,它就把所有的块还给各自的 parent. 在其他方面,子存储结构同普通存储结构一样。 子存储结构在下列情况中是非常有用的。想象一下,如果用户需要处理存储在某个块中的动态数据,再将处理的结果存放在该块中。在使用了最简单的方法处理后,临时数据作为输入和输出数据被存放在了同一个存储块中,于是该存储块看上去就类似下面处理后的样子: Dynamic data processing without using child storage. 结果,在存储块中,出现了垃圾(临时数据)。然而,如果在开始处理数据前就先建立一个子存储块,将临时数据写入子存储块中并在最后释放子存储块,那么最终在 源/目的存储块 (source / destination storage) 中就不会出现垃圾, 于是该存储块看上去应该是如下形式:Dynamic data processing using a child storage. cvReleaseMemStorage
释放内存块 void cvReleaseMemStorage( CvMemStorage** storage ); storage: 指向被释放了的存储块的指针
函数 cvReleaseMemStorage 释放所有的存储(内存)块 或者 将它们返回给各自的 parent(如果需要的话)。 接下来再释放 header块(即:释放头指针 head 指向的块 = free(head))并清除指向该块的指针(即:head = NULL)。在释放作为 parent 的块之前,先清除各自的 child 块。 cvClearMemStorage
清空内存存储块 void cvClearMemStorage( CvMemStorage* storage ); storage:存储存储块
函数 cvClearMemStorage 将存储块的 top 置到存储块的头部(注:清空存储块中的存储内容)。该函数并不释放内存(仅清空内存)。假使该内存块有一个父内存块(即:存在一内存块与其有父子关系),则函数就将所有的块返回给其 parent. cvMemStorageAlloc
在存储块中分配以内存缓冲区 void* cvMemStorageAlloc( CvMemStorage* storage, size_t size ); storage:内存块. size:缓冲区的大小. 函数 cvMemStorageAlloc 在存储块中分配一内存缓冲区。该缓冲区的大小不能超过内存块的大小,否则就会导致运行时错误。缓冲区的地址被调整为CV_STRUCT_ALIGN 字节 (当前为 sizeof(double)). cvMemStorageAllocString
在存储块中分配一文本字符串 typedef struct CvString { int len; char* ptr; } CvString; CvString cvMemStorageAllocString( CvMemStorage* storage, const char* ptr, int len=-1 );
storage:存储块
ptr:字符串 len:字符串的长度(不计算'/0')。如果参数为负数,函数就计算该字符串的长度。 函数 cvMemStorageAlloString 在存储块中创建了一字符串的拷贝。它返回一结构,该结构包含字符串的长度(该长度或通过用户传递,或通过计算得到)和指向被拷贝了的字符串的指针。 cvSaveMemStoragePos
保存内存块的位置(地址) void cvSaveMemStoragePos( const CvMemStorage* storage, CvMemStoragePos* pos ); storage:内存块.
pos:内存块顶部位置。 函数 cvSaveMemStoragePos 将存储块的当前位置保存到参数 pos 中。 函数 cvRestoreMemStoragePos 可进一步获取该位置(地址)。 cvRestoreMemStoragePos
恢复内存存储块的位置 void cvRestoreMemStoragePos( CvMemStorage* storage, CvMemStoragePos* pos ); storage:内存块. pos:新的存储块的位置 函数 cvRestoreMemStoragePos 通过参数 pos 恢复内存块的位置。该函数和函数 cvClearMemStorage 是释放被占用内存块的唯一方法。注意:没有什么方法可去释放存储块中被占用的部分内存。 2、分类器结构及操作函数: CvHaarFeature #define CV_HAAR_FEATURE_MAX 3 typedef struct CvHaarFeature { int tilted; struct { CvRect r; float weight; } rect[CV_HAAR_FEATURE_MAX]; } CvHaarFeature; 一个 harr 特征由 2-3 个具有相应权重的矩形组成
titled :/* 0 means up-right feature, 1 means 45--rotated feature */ rect[CV_HAAR_FEATURE_MAX]; /* 2-3 rectangles with weights of opposite signs and with absolute values inversely proportional to the areas of the rectangles. if rect[2].weight !=0, then the feature consists of 3 rectangles, otherwise it consists of 2 */ CvHaarClassifier
typedef struct CvHaarClassifier { int count; CvHaarFeature* haar_feature; float* threshold; int* left; int* right; float* alpha; } CvHaarClassifier; /* a single tree classifier (stump in the simplest case) that returns the response for the feature at the particular image location (i.e. pixel sum over subrectangles of the window) and gives out a value depending on the responce */
int count; /* number of nodes in the decision tree */ /* these are "parallel" arrays. Every index i corresponds to a node of the decision tree (root has 0-th index). left[i] - index of the left child (or negated index if the left child is a leaf) right[i] - index of the right child (or negated index if the right child is a leaf) threshold[i] - branch threshold. if feature responce is <= threshold, left branch is chosen, otherwise right branch is chosed. alpha[i] - output value correponding to the leaf. */ CvHaarStageClassifier
typedef struct CvHaarStageClassifier
{ int count; /* number of classifiers in the battery */ float threshold; /* threshold for the boosted classifier */ CvHaarClassifier* classifier; /* array of classifiers */ /* these fields are used for organizing trees of stage classifiers,
rather than just stright cascades */ int next; int child; int parent; } CvHaarStageClassifier; /* a boosted battery of classifiers(=stage classifier): the stage classifier returns 1 if the sum of the classifiers' responces is greater than threshold and 0 otherwise */ int count; /* number of classifiers in the battery */ float threshold; /* threshold for the boosted classifier */ CvHaarClassifier* classifier; /* array of classifiers */ /* these fields are used for organizing trees of stage classifiers, rather than just stright cascades */ CvHaarClassifierCascade
typedef struct CvHidHaarClassifierCascade CvHidHaarClassifierCascade; typedef struct CvHaarClassifierCascade { int flags; int count; CvSize orig_window_size; CvSize real_window_size; double scale; CvHaarStageClassifier* stage_classifier; CvHidHaarClassifierCascade* hid_cascade; } CvHaarClassifierCascade; /* cascade or tree of stage classifiers */ int flags; /* signature */ int count; /* number of stages */ CvSize orig_window_size; /* original object size (the cascade is trained for) */ /* these two parameters are set by cvSetImagesForHaarClassifierCascade */ CvSize real_window_size; /* current object size */ double scale; /* current scale */ CvHaarStageClassifier* stage_classifier; /* array of stage classifiers */ CvHidHaarClassifierCascade* hid_cascade; /* hidden optimized representation of the cascade, created by cvSetImagesForHaarClassifierCascade */ 所有的结构都代表一个级联boosted Haar分类器。级联有下面的等级结构:
Cascade: Stage1: Classifier11: Feature11 Classifier12: Feature12 ... Stage2: Classifier21: Feature21 ... ... 整个等级可以手工构建,也可以利用函数cvLoadHaarClassifierCascade从已有的磁盘文件或嵌入式基中导入。 特征检测用到的函数: cvLoadHaarClassifierCascade 从文件中装载训练好的级联分类器或者从OpenCV中嵌入的分类器数据库中导入 CvHaarClassifierCascade* cvLoadHaarClassifierCascade( const char* directory, CvSize orig_window_size ); directory :训练好的级联分类器的路径 orig_window_size:级联分类器训练中采用的检测目标的尺寸。因为这个信息没有在级联分类器中存储,所有要单独指出。 函数 cvLoadHaarClassifierCascade 用于从文件中装载训练好的利用海尔特征的级联分类器,或者从OpenCV中嵌入的分类器数据库中导入。分类器的训练可以应用函数haartraining(详细察看opencv/apps/haartraining) 函数 已经过时了。现在的目标检测分类器通常存储在 XML 或 YAML 文件中,而不是通过路径导入。从文件中导入分类器,可以使用函数 cvLoad 。 cvReleaseHaarClassifierCascade
释放haar classifier cascade。 void cvReleaseHaarClassifierCascade( CvHaarClassifierCascade** cascade ); cascade :双指针类型指针指向要释放的cascade. 指针由函数声明。 函数 cvReleaseHaarClassifierCascade 释放cascade的动态内存,其中cascade的动态内存或者是手工创建,或者通过函数 cvLoadHaarClassifierCascade 或 cvLoad分配。 cvHaarDetectObjects 检测图像中的目标 typedef struct CvAvgComp { CvRect rect; /* bounding rectangle for the object (average rectangle of a group) */ int neighbors; /* number of neighbor rectangles in the group */ } CvAvgComp; CvSeq* cvHaarDetectObjects( const CvArr* image, CvHaarClassifierCascade* cascade, CvMemStorage* storage, double scale_factor=1.1, int min_neighbors=3, int flags=0, CvSize min_size=cvSize(0,0) ); image 被检图像 cascade harr 分类器级联的内部标识形式 storage 用来存储检测到的一序列候选目标矩形框的内存区域。 scale_factor 在前后两次相继的扫描中,搜索窗口的比例系数。例如1.1指将搜索窗口依次扩大10%。 min_neighbors 构成检测目标的相邻矩形的最小个数(缺省-1)。如果组成检测目标的小矩形的个数和小于min_neighbors-1 都会被排除。如果min_neighbors 为 0, 则函数不做任何操作就返回所有的被检候选矩形框,这种设定值一般用在用户自定义对检测结果的组合程序上。 flags 操作方式。当前唯一可以定义的操作方式是 CV_HAAR_DO_CANNY_PRUNING。如果被设定,函数利用Canny边缘检测器来排除一些边缘很少或者很多的图像区域,因为这样的区域一般不含被检目标。人脸检测中通过设定阈值使用了这种方法,并因此提高了检测速度。 min_size 检测窗口的最小尺寸。缺省的情况下被设为分类器训练时采用的样本尺寸(人脸检测中缺省大小是~20×20)。 函数 cvHaarDetectObjects 使用针对某目标物体训练的级联分类器在图像中找到包含目标物体的矩形区域,并且将这些区域作为一序列的矩形框返回。函数以不同比例大小的扫描窗口对图像进行几次搜索(察看cvSetImagesForHaarClassifierCascade)。 每次都要对图像中的这些重叠区域利用cvRunHaarClassifierCascade进行检测。 有时候也会利用某些继承(heuristics)技术以减少分析的候选区域,例如利用 Canny 裁减 (prunning)方法。 函数在处理和收集到候选的方框(全部通过级联分类器各层的区域)之后,接着对这些区域进行组合并且返回一系列各个足够大的组合中的平均矩形。调节程序中的缺省参数(scale_factor=1.1, min_neighbors=3, flags=0)用于对目标进行更精确同时也是耗时较长的进一步检测。为了能对视频图像进行更快的实时检测,参数设置通常是:scale_factor=1.2, min_neighbors=2, flags=CV_HAAR_DO_CANNY_PRUNING, min_size=<minimum possible face size> (例如, 对于视频会议的图像区域). cvSetImagesForHaarClassifierCascade
为隐藏的cascade(hidden cascade)指定图像 void cvSetImagesForHaarClassifierCascade( CvHaarClassifierCascade* cascade, const CvArr* sum, const CvArr* sqsum, const CvArr* tilted_sum, double scale ); cascade 隐藏 Harr 分类器级联 (Hidden Haar classifier cascade), 由函数 cvCreateHidHaarClassifierCascade生成 sum 32-比特,单通道图像的积分图像(Integral (sum) 单通道 image of 32-比特 integer format). 这幅图像以及随后的两幅用于对快速特征的评价和亮度/对比度的归一化。 它们都可以利用函数 cvIntegral从8-比特或浮点数 单通道的输入图像中得到。 sqsum 单通道64比特图像的平方和图像 tilted_sum 单通道32比特整数格式的图像的倾斜和(Tilted sum) scale cascade的窗口比例. 如果 scale=1, 就只用原始窗口尺寸检测 (只检测同样尺寸大小的目标物体) - 原始窗口尺寸在函数cvLoadHaarClassifierCascade中定义 (在 "<default_face_cascade>"中缺省为24x24), 如果scale=2, 使用的窗口是上面的两倍 (在face cascade中缺省值是48x48 )。 这样尽管可以将检测速度提高四倍,但同时尺寸小于48x48的人脸将不能被检测到。 函数 cvSetImagesForHaarClassifierCascade 为hidden classifier cascade 指定图像 and/or 窗口比例系数。 如果图像指针为空,会继续使用原来的图像(i.e. NULLs 意味这"不改变图像")。比例系数没有 "protection" 值,但是原来的值可以通过函数 cvGetHaarClassifierCascadeScale 重新得到并使用。这个函数用于对特定图像中检测特定目标尺寸的cascade分类器的设定。函数通过cvHaarDetectObjects进行内部调用,但当需要在更低一层的函数cvRunHaarClassifierCascade中使用的时候,用户也可以自行调用。 cvRunHaarClassifierCascade
在给定位置的图像中运行 cascade of boosted classifier int cvRunHaarClassifierCascade( CvHaarClassifierCascade* cascade, CvPoint pt, int start_stage=0 ); cascade Haar 级联分类器
pt 待检测区域的左上角坐标。待检测区域大小为原始窗口尺寸乘以当前设定的比例系数。当前窗口尺寸可以通过cvGetHaarClassifierCascadeWindowSize重新得到。 start_stage 级联层的初始下标值(从0开始计数)。函数假定前面所有每层的分类器都已通过。这个特征通过函数cvHaarDetectObjects内部调用,用于更好的处理器高速缓冲存储器。 函数 cvRunHaarHaarClassifierCascade 用于对单幅图片的检测。在函数调用前首先利用 cvSetImagesForHaarClassifierCascade设定积分图和合适的比例系数 (=> 窗口尺寸)。当分析的矩形框全部通过级联分类器每一层的时返回正值(这是一个候选目标),否则返回0或负值。 二、例程分析:
例子:利用级联的Haar classifiers寻找检测目标(e.g. faces). #include "cv.h" #include "highgui.h" //读取训练好的分类器。
CvHaarClassifierCascade* load_object_detector( const char* cascade_path ) { return (CvHaarClassifierCascade*)cvLoad( cascade_path ); } void detect_and_draw_objects( IplImage* image, CvHaarClassifierCascade* cascade, int do_pyramids ) { IplImage* small_image = image; CvMemStorage* storage = cvCreateMemStorage(0); //创建动态内存 CvSeq* faces; int i, scale = 1; /* if the flag is specified, down-scale the 输入图像 to get a
performance boost w/o loosing quality (perhaps) */ if( do_pyramids ) { small_image = cvCreateImage( cvSize(image->width/2,image->height/2), IPL_DEPTH_8U, 3 ); cvPyrDown( image, small_image, CV_GAUSSIAN_5x5 );//函数 cvPyrDown 使用 Gaussian 金字塔分解对输入图像向下采样。首先它对输入图像用指定滤波器进行卷积,然后通过拒绝偶数的行与列来下采样图像。 scale = 2; } /* use the fastest variant */
faces = cvHaarDetectObjects( small_image, cascade, storage, 1.2, 2, CV_HAAR_DO_CANNY_PRUNING ); /* draw all the rectangles */
for( i = 0; i < faces->total; i++ ) { /* extract the rectanlges only */ CvRect face_rect = *(CvRect*)cvGetSeqElem( faces, i, 0 ); cvRectangle( image, cvPoint(face_rect.x*scale,face_rect.y*scale), cvPoint((face_rect.x+face_rect.width)*scale, (face_rect.y+face_rect.height)*scale), CV_RGB(255,0,0), 3 ); } if( small_image != image )
cvReleaseImage( &small_image ); cvReleaseMemStorage( &storage ); //释放动态内存 } /* takes image filename and cascade path from the command line */
int main( int argc, char** argv ) { IplImage* image; if( argc==3 && (image = cvLoadImage( argv[1], 1 )) != 0 ) { CvHaarClassifierCascade* cascade = load_object_detector(argv[2]); detect_and_draw_objects( image, cascade, 1 ); cvNamedWindow( "test", 0 ); cvShowImage( "test", image ); cvWaitKey(0); cvReleaseHaarClassifierCascade( &cascade ); cvReleaseImage( &image ); } return 0;
} 关键代码很简单,装载分类器,对输入图像进行金字塔采样,然后用cv的函数进行检测目标,最后输出检测到的目标矩形。 OpenCV学习笔记(二)基于Haar-like特征的层叠推进分类器快速目标检测OpenCV学习笔记之二――基于Haar-like特征的层叠推进分类器快速目标检测 一、简介 目标检测方法最初由Paul Viola [Viola01]提出,并由Rainer Lienhart [Lienhart02]对这一方法进行了改善。该方法的基本步骤为: 首先,利用样本(大约几百幅样本图片)的 harr 特征进行分类器训练,得到一个级联的boosted分类器。 分类器中的"级联"是指最终的分类器是由几个简单分类器级联组成。在图像检测中,被检窗口依次通过每一级分类器, 这样在前面几层的检测中大部分的候选区域就被排除了,全部通过每一级分类器检测的区域即为目标区域。 分类器训练完以后,就可以应用于输入图像中的感兴趣区域(与训练样本相同的尺寸)的检测。检测到目标区域(汽车或人脸)分类器输出为1,否则输出为0。为了检测整副图像,可以在图像中移动搜索窗口,检测每一个位置来确定可能的目标。 为了搜索不同大小的目标物体,分类器被设计为可以进行尺寸改变,这样比改变待检图像的尺寸大小更为有效。所以,为了在图像中检测未知大小的目标物体,扫描程序通常需要用不同比例大小的搜索窗口对图片进行几次扫描。 目前支持这种分类器的boosting技术有四种: Discrete Adaboost, Real Adaboost, Gentle Adaboost and Logitboost。 "boosted" 即指级联分类器的每一层都可以从中选取一个boosting算法(权重投票),并利用基础分类器的自我训练得到。 根据上面的分析,目标检测分为三个步骤: 1、 样本的创建 2、 训练分类器 3、 利用训练好的分类器进行目标检测。 二、样本创建 训练样本分为正例样本和反例样本,其中正例样本是指待检目标样本(例如人脸或汽车等),反例样本指其它任意图片,所有的样本图片都被归一化为同样的尺寸大小(例如,20x20)。 负样本 负样本可以来自于任意的图片,但这些图片不能包含目标特征。负样本由背景描述文件来描述。背景描述文件是一个文本文件,每一行包含了一个负样本图片的文件名(基于描述文件的相对路径)。该文件必须手工创建。 e.g: 负样本描述文件的一个例子: 假定目录结构如下: /img img1.jpg img2.jpg bg.txt 则背景描述文件bg.txt的内容为: img/img1.jpg img/img2.jpg 正样本 正样本由程序craatesample程序来创建。该程序的源代码由OpenCV给出,并且在bin目录下包含了这个可执行的程序。 正样本可以由单个的目标图片或者一系列的事先标记好的图片来创建。 Createsamples程序的命令行参数: 命令行参数: -vec <vec_file_name> 训练好的正样本的输出文件名。 -img<image_file_name> 源目标图片(例如:一个公司图标) -bg<background_file_name> 背景描述文件。 -num<number_of_samples> 要产生的正样本的数量,和正样本图片数目相同。 -bgcolor<background_color> 背景色(假定当前图片为灰度图)。背景色制定了透明色。对于压缩图片,颜色方差量由bgthresh参数来指定。则在bgcolor-bgthresh和bgcolor+bgthresh中间的像素被认为是透明的。 -bgthresh<background_color_threshold> -inv 如果指定,颜色会反色 -randinv 如果指定,颜色会任意反色 -maxidev<max_intensity_deviation> 背景色最大的偏离度。 -maxangel<max_x_rotation_angle> -maxangle<max_y_rotation_angle>, -maxzangle<max_x_rotation_angle> 最大旋转角度,以弧度为单位。 -show 如果指定,每个样本会被显示出来,按下"esc"会关闭这一开关,即不显示样本图片,而创建过程继续。这是个有用的debug选项。 -w<sample_width> 输出样本的宽度(以像素为单位) -h《sample_height》 输出样本的高度,以像素为单位。 注:正样本也可以从一个预先标记好的图像集合中获取。这个集合由一个文本文件来描述,类似于背景描述文件。每一个文本行对应一个图片。每行的第一个元素是图片文件名,第二个元素是对象实体的个数。后面紧跟着的是与之匹配的矩形框(x, y, 宽度,高度)。 下面是一个创建样本的例子: 假定我们要进行人脸的检测,有5个正样本图片文件img1.bmp,…img5.bmp;有2个背景图片文件:bg1.bmp,bg2.bmp,文件目录结构如下: positive img1.bmp …… Img5.bmp negative bg1.bmp bg2.bmp info.dat bg.txt 正样本描述文件info.dat的内容如下: Positive/imag1.bmp 1 0 0 24 28 …… Positive/imag5.bmp 1 0 0 24 28 图片img1.bmp包含了单个目标对象实体,矩形为(0,0,24,28)。 注意:要从图片集中创建正样本,要用-info参数而不是用-img参数。 -info <collect_file_name> 标记特征的图片集合的描述文件。 背景(负样本)描述文件的内容如下: nagative/bg1.bmp nagative/bg2.bmp 我们用一个批处理文件run.bat来进行正样本的创建:该文件的内容如下: cd e:/face/bin CreateSamples -vec e:/face/a.vec -info e:/face/info.dat -bg e:/face/bg.txt -num 5 -show -w 24 -h 28 其中e:/face/bin目录包含了createsamples可执行程序,生成的正样本文件a.vec在e:/face目录下。 三、训练分类器 样本创建之后,接下来要训练分类器,这个过程是由haartraining程序来实现的。该程序源码由OpenCV自带,且可执行程序在OpenCV安装目录的bin目录下。 Haartraining的命令行参数如下: -data<dir_name> 存放训练好的分类器的路径名。 -vec<vec_file_name> 正样本文件名(由trainingssamples程序或者由其他的方法创建的) -bg<background_file_name> 背景描述文件。 -npos<number_of_positive_samples>, -nneg<number_of_negative_samples> 用来训练每一个分类器阶段的正/负样本。合理的值是:nPos = 7000;nNeg = 3000 -nstages<number_of_stages> 训练的阶段数。 -nsplits<number_of_splits> 决定用于阶段分类器的弱分类器。如果1,则一个简单的stump classifier被使用。如果是2或者更多,则带有number_of_splits个内部节点的CART分类器被使用。 -mem<memory_in_MB> 预先计算的以MB为单位的可用内存。内存越大则训练的速度越快。 -sym(default) -nonsym 指定训练的目标对象是否垂直对称。垂直对称提高目标的训练速度。例如,正面部是垂直对称的。 -minhitrate《min_hit_rate》 每个阶段分类器需要的最小的命中率。总的命中率为min_hit_rate的number_of_stages次方。 -maxfalsealarm<max_false_alarm_rate> 没有阶段分类器的最大错误报警率。总的错误警告率为max_false_alarm_rate的number_of_stages次方。 -weighttrimming<weight_trimming> 指定是否使用权修正和使用多大的权修正。一个基本的选择是0.9 -eqw -mode<basic(default)|core|all> 选择用来训练的haar特征集的种类。basic仅仅使用垂直特征。all使用垂直和45度角旋转特征。 -w《sample_width》 -h《sample_height》 训练样本的尺寸,(以像素为单位)。必须和训练样本创建的尺寸相同。 一个训练分类器的例子: 同上例,分类器训练的过程用一个批处理文件run2.bat来完成: cd e:/face/bin haartraining -data e:/face/data -vec e:/face/a.vec -bg e:/face/bg.txt -npos 5 -nneg 2 -w 24 -h 28 训练结束后,会在目录data下生成一些子目录,即为训练好的分类器。 注:OpenCv的某些版本可以将这些目录中的分类器直接转换成xml文件。但在实际的操作中,haartraining程序却好像永远不会停止,而且没有生成xml文件,后来在OpenCV的yahoo论坛上找到一个haarconv的程序,才将分类器转换为xml文件,其中的原因尚待研究。 四、目标检测
OpenCV的cvHaarDetectObjects()函数(在haarFaceDetect演示程序中示例)被用来做侦测。关于该检测的详细分析,将在下面的笔记中详细描述。 3月29日
一个合格的程序员该做的事情
程序员每天该做的事 1、总结自己一天任务的完成情况 最好的方式是写工作日志,把自己今天完成了什么事情,遇见了什么问题都记录下来,日后翻看好处多多 2、考虑自己明天应该做的主要工作 把明天要做的事情列出来,并按照优先级排列,第二天应该把自己效率最高的时间分配给最重要的工作 3、考虑自己一天工作中失误的地方,并想出避免下一次再犯的方法 出错不要紧,最重要的是不要重复犯相同的错误,那是愚蠢 4、考虑自己一天工作完成的质量和效率能否还能提高 一天只提高1%,365天你的效率就能提高多少倍你知道吗? (1+0.01)^365 = 37 倍 5、看一个有用的新闻网站或读一张有用的报纸,了解业界动态 闭门造车是不行的,了解一下别人都在做什么,对自己能带来很多启示 6、记住一位同事的名字及其特点 你认识公司的所有同事吗?你了解他们吗? 7、清理自己的代码 今天完成的代码,把中间的调试信息,测试代码清理掉,按照编码风格整理好,注释都写好了吗? 8、清理自己的桌面 当日事当日毕,保持清洁干劲的桌面才能让你工作时不分心,程序员特别要把电脑的桌面清理干净 程序员每月该做的事 1、至少和一个同事一起吃饭或喝茶 2、自我考核一次 3、对你的同事考核一次 3、制定下月的计划,确定下月的工作重点 4、总结自己工作质量改进状况 5、有针对性地对一项工作指标做深入地分析并得出改进的方案 6、与老板沟通一次 程序员每年该做的事 1、年终总结 2、兑现给自己、给家人的承诺 3、下年度工作规划 4、掌握一项新技术 5、推出一种新产品 6、与父母团聚一次
3月28日
OpenCV学习笔记(一)概述和系统配置
一、概述 |
|