引子: 课题需要SURF特征提取算法,在运动中提取摄像头图像中的特征点,并进行跟踪匹配,以此估计运动状态。开始找到了SIFT算法,SIFT特征提取具有极强的适应能力,但运算量稍大,后来就有了SURF特征提取算法,简化了计算量,保持了较高的性能,是性价比很不错的算法。开始并不知道OpenCV的存在,后来的后来发现OpenCV中已经有了SURF算法,感叹于技术发展之快(要知道SIFT是Low在2004年系统的提出的,SURF是在2006年才被Bay等提出的),感谢Low, Bay, et al. 感谢Internet、感谢Google、感谢Intel、感谢OpenCV、感谢Liu Liu(OpenCV中实现SURF算法的作者)、最重要的感谢祖国、感谢全世界最大的局域网。
在庸长的开场白中SURF登台了!!!令各位看官失望的是,引言很长,然内容不多,为什么?我是菜鸟!我怕谁!菜鸟看不懂当然写不多了。
正文开始了,在OpenCV(据说是1.1以后的版本)中包含了SURF算法,并且还有一个使用SURF的例子,这里使用的是OpenCV2.1。在OpenCV的安装目录下/samples/c 文件夹中一个叫 find_obj.cpp 的文件,这是个应用SURF算法寻找一本书的例子。同目录下还有一对于的可执行文件 find_obj.exe,可以先运行一下看看。来看find_obj.cpp
1、这个程序的框架
从入口 main() 开始慢慢道来
const char* object_filename = argc == 3 ? argv[1] : "box.png";
const char* scene_filename = argc == 3 ? argv[2] : "box_in_scene.png";
main函数前两行,判断是否有图片文件名的参数传入,没有则使用同目录下的"box.png"和"box_in_scene.png"
紧接着创建内存块“CvMemStorage* storage = cvCreateMemStorage(0);”坦白地承认:具体的不懂
cvNamedWindow("Object", 1); //建立两个窗口:物体和对比,第一个参数是窗口名称,第二个参数是Flag。
cvNamedWindow("Object Correspond", 1); //据说Flag只支持一个参数 CV_WINDOW_AUTOSIZE,可能CV_WINDOW_AUTOSIZE = 1 吧。
static CvScalar colors[] = // 建立类似调色板的东西,colors[0],表示红色,colors[8],表示白色
{ // 下面的程序也只用到了这两种颜色,红色的圈儿和白色的线儿。
{{0,0,255}}, // 不知道为什么这里弄了九个,吓人吗?
{{0,128,255}},
{{0,255,255}},
{{0,255,0}},
{{255,128,0}},
{{255,255,0}},
{{255,0,0}},
{{255,0,255}},
{{255,255,255}}
};
IplImage* object = cvLoadImage( object_filename, CV_LOAD_IMAGE_GRAYSCALE );
IplImage* image = cvLoadImage( scene_filename, CV_LOAD_IMAGE_GRAYSCALE );
//载入图像,如果为彩色转换为灰度图像,或者说以灰度的模式载入图像。
//后面的if()就是判断是否正确载入了图像,过!
IplImage* object_color = cvCreateImage(cvGetSize(object), 8, 3);
cvCvtColor( object, object_color, CV_GRAY2BGR );
//上面两行依据灰度图像建立彩色图像,虽然目前实际上的数据是黑白的,但是后面可以添加红色圆圈
CvSeq *objectKeypoints = 0, *objectDescriptors = 0;// 表示指向特征点及其描述符的结构体的指针
CvSeq *imageKeypoints = 0, *imageDescriptors = 0;// CvSeq 为可动态增长元素序列,是所有OpenCV动态数据结构的基础
CvSURFParams params = cvSURFParams(500, 1);//SURF参数设置:阈值500,生成128维描述符
// cvSURFParams 函数原型如下:
CvSURFParams cvSURFParams(double threshold, int extended)
{
CvSURFParams params;
params.hessianThreshold = threshold; // 特征点选取的 hessian 阈值
params.extended = extended; // 是否扩展,1 - 生成128维描述符,0 - 64维描述符
params.nOctaves = 4;
params.nOctaveLayers = 2;
return params;
}
往下
double tt = (double)cvGetTickCount(); //计时
cvExtractSURF( object, 0, &objectKeypoints, &objectDescriptors, storage, params );
cvExtractSURF( image, 0, &imageKeypoints, &imageDescriptors, storage, params );
//提取图像中的特征点,函数原型:
CVAPI(void) cvExtractSURF( const CvArr* img, const CvArr* mask,
CvSeq** keypoints, CvSeq** descriptors,
CvMemStorage* storage, CvSURFParams params, int useProvidedKeyPts CV_DEFAULT(0) );
第3、4个参数返回结果:特征点和特征点描述符,数据类型是指针的指针,真麻烦,但是后面的例程中有如何将这些数据从指针的指针中提取出来,所以这里就不管指针的指针是什么东西了。关于对特征点的描述见第二小节,关于特征点描述符见第三小节(三,中国传统文化对三情有独钟,怎么如隔三秋、三日不绝、三月不知菜味<本人菜鸟吗,喜是食白菜的鸟人>,新时代新气象也给“三”赋予新的含义,为发扬传统文化,故本菜鸟喜欢什么都分割为1、2、3)
//后面的因为不太关心没仔细看,大概就是生成匹配对比的图像即:"correspond"
//然后,根据是否定义了 "USE_FLANN" 采用不同的匹配方法,flann 和 非flann
//最后,在object图像中使用红色圆圈画出特征点位置及所处尺度的大小
//在correspond 图像中显示匹配结果,用白色线段相连。
2、特征点的描述,例如 objectKeypoints
定义:CvSeq *objectKeypoints = 0;
对于描述特征点和特征点的描述是基于一种 结构体CvSeq,结构CvSeq是所有OpenCV动态数据结构的基础,内部结构:
int flags; \ //不懂
int header_size; \ // 头大小,具体不懂
struct CvSeq* h_prev; \//类似链表中的东西
struct CvSeq* h_next; \ //同上
struct CvSeq* v_prev; \//同上
struct CvSeq* v_next; \ //同上
int total; \ // 总共有多少个元素,objectKeypoints->total 表有多少个特征点
int elem_size; \ // 每个元素的大小,字节表示
char* block_max; \ //不懂
char* ptr; \ // 当时写指针的位置,可能就是指向实际数据的指针吧
int delta_elems; \
//不懂
CvMemStorage* storage; \//同上
CvSeqBlock* free_blocks; \ //同上
CvSeqBlock* first; //同上
其实这些弄不弄明白对于应用来说没影响,因为一组特征点被提取出来了,通常会关心:
整体的
特征点的个数: objectKeypoints->total
第i个特征点
CvSURFPoint *r = (CvSURFPoint*)cvGetSeqElem(objectKeypoints,i);
第i个特征点的:
X坐标位置(单位像素): r->pt.x
Y坐标位置(单位像素): r->pt.y
特征点所处的尺度大小 : r->size
特征点主方位角 : r->dir (0-360 表示,如何定义未知)
特征点的Hessian值 : r->hessian
还有一个参数: r->laplacian 取值为1 或 -1 不知道是什么意思,什么拉普拉斯,有木有高人给指点一下,望指导
3、特征点描述符,例如:objectDescriptors
定义:CvSeq *objectDescriptors = 0;
特征点的个数: objectDescriptors->total
对于某个特征点描述符,就没有特征点那么简单了,至少这个例程中没有涉及仅使用一个函数和对应的结构体就可以提取出所需数据的简单的方法。
通过观查特征点匹配的代码,菜鸟以为得到特征点描述符的方法为:
CvSetReader reader;
cvStartReadSeq(objectDescriptors, &reader, 0);
for(int i = 0; i < objectDescriptors->total; i++)
{
const float* descriptor = (const float*)reader.ptr; // descriptor 指向第i 特征描述符(float数组)的指针
// 数组长度为:reader.seq->elem_size/sizeof(float) or objectDescriptors->elem_size/sizeof(float)
CV_NEXT_SEQ_ELEM(reader.seq->elem_size, reader); //读取下一个特征点
}
// 同样对于 特征点 objectKeypoints 也有
CvSetReader kreader;
cvStartReadSeq(objectKeypoints , &kreader, 0);
for(int i = 0; i < objectKeypoints ->total; i++)
{
const CvSURFPoint* kp = (const CvSURFPoint*)kreader.ptr;
CV_NEXT_SEQ_ELEM(reader.seq->elem_size, reader); //读取下一个特征点
// 上面等价于 CvSURFPoint *kp = (CvSURFPoint*)cvGetSeqElem(objectKeypoints,i);(第二小节)
}
顺便提一下特征点的匹配,前面第一小节涉及到了两种特征匹配:flann 和 非flann。
flann没看明白,非flann就是计算描述符向量的距离,即每个对应元素差的平方和,对于每个需要匹配的特征点同对应的图片中的各个特征点进行距离的计算并保存最短的两个距离,当最短距离小于次短距离的0.6时认为匹配成功。函数定义如下:
int naiveNearestNeighbor( const float* vec, int laplacian,
const CvSeq* model_keypoints,
const CvSeq* model_descriptors )
vec ---- 待匹配的描述符
laplacian ---- 第二小节中的神秘变量在这里再一次出现了,当两个特征点的 laplacian 值不同时直接pass 不准配对(只有同性才能配对,OMG 太变态了太变态了EMTF怎么存在这么恶心的变量)不匹配也不计算距离
model_keypoints --------- 指向匹配候选特征点们的结构体指针
model_descriptors ------ 指向匹配候选特征点描述符们的结构体指针
如果匹配成功返回 特征点在 model_keypoints 中的索引值
计算两个描述符距离,函数定义
double compareSURFDescriptors( const float* d1, const float* d2, double best, int length )
d1 --------- 描述符1数组
d2 --------- 描述符2数组
best ------ 相当阈值,当计算的过程中距离大于 beat 的值时即刻返回当前计算的距离值,不用再计算了,木有意义,通常将目前的次近距离传递给 best,以便节省匹配时间(其实提取时间远远大于匹配时间)
length --- 描述符的维数,即描述符数组长度
返回计算的距离。