最近又开始学习《MasteringOpenCV系列》之前没看的部分,依旧是英文版。这次主要研究“非刚性人脸跟踪”(non-rigid face tracking),业余时间较少分几次写完吧。
首先谈谈什么是非刚性人脸跟踪。它是对每帧视频图像中人脸特征稠密数据集合的估计。非刚性人脸跟踪侧重于不同脸部表情或不同人物的脸部特征相对距离的变化。它和一般的人脸检测与跟踪算法不同,它不仅仅是找到每一帧中人脸的位置,它还要找到人脸五官的组态关系。【可以作用表情识别】
本次的非刚性人脸跟踪系统是基于数据驱动的,因此该系统的每个模块都涉及两个过程:trianing和testing。“训练”完成了从样本数据构建模块(获得一些关键参数),“测试”利用这些模块在新的未知数据集上检验效果。该系统主要包括以下模块:
- 数据收集及标注工具
由于人脸特征检测算法依赖于人脸特征外观模型的构建和样本点在几何空间中相对位置关系。样本数据集越大,算法健壮性就越好。因此,第一个任务必须开发工具手工指定图像中人脸特征点作为样本点,并采用合适的格式存储和展示。(亦可采用其他算法代替,比如sift/surf特征点等等)
- 几何约束模块
脸部的几何约束关系也是从数据集合中学习到的。它在跟踪阶段将被用来约束和剔除不合理的特征点。该模块将用一个线性形状模型来描述人脸模型。(普氏分析、线性建模)
- 人脸特征提取模块
为了检测下一次被跟踪人脸图像中的外观,首先要学习人脸特征的外观。(基于互联关系的图像块模型)
- 人脸跟踪模块
人脸检测、初始化、跟踪流程。
整个运行流程就是手工获取人脸及标注样本点,训练几何约束模块和人脸特征模块,往后新来一副人脸,这些特征点及互联关系将会自动fit到人脸上。个人感觉小日本:http://v.youku.com/v_show/id_XODY4MjkzMjg0.html,这段酷炫视频的背后,其原理应该这一章内容有关(除去脸部艺术渲染)。有时间的朋友还可以看下斯坦福的课程设计:face swapping,Yifei Feng, Wenxun Huang, Tony Wu,Mentor:HuizhongChen
http://web.stanford.edu/class/ee368/Project_Spring_1314/index.html,
上面都是后话了,下图为作者给出的系统框架:
本次文章只讲解如何开发手工标示特征点的工具。
1.我们要标注哪些特征点呢?即有哪些训练数据类型
- images:涵盖多人,不同光照条件,不同距离,及不同捕获设备。严格限制头的姿势及脸部表情。
- Annotations:在每幅图像中手工标注要跟踪的人脸特征点位置
- Symmetry indices:建立人脸特征点的索引,标记脸部两侧对称的特征点。这样做的好处是对训练图像做镜像时,可以扩大训练集一倍的大小。另外,手工标注特征点的对称关系时只能标注沿着Y轴对称的点。下面两幅图像互为镜像。
- Connectivity indices:建立人脸特征点的索引,定义面部特征的语义解释。【这边已经上升到表情识别的高度】
2.我们将采取何种数据结构存储上述特征呢?
由于模式识别程序都包含算法和数据两部分,而数据往往是线下获取的,即事先人工标定的。所以必须合理设计一种方法更方便地管理这些线下数据。这里根据利用C++操作符重载及泛型编程技术,将类中的数据序列化,保存为OPENCV能处理的XML/YAML文件。(你也可以用TXT保存,格式化字符串存取)。
在介绍完整程序前,先给出一个简单的demo,实现了对类“Test”的序列化存储
#include<iostream> #include <opencv2/opencv.hpp> using namespace cv; using namespace std; class Test { public: int a,b; void write(FileStorage &fs) const; //file storage object to write to void read(const FileNode& node); //file storage node to read from };
#include "ft.hpp" #include "Data.h" int main(){ Test A; A.a=1; A.b=2; save_ft<Test>("foo.xml",A); Test B = load_ft<Test>("foo.xml"); cout<<B.a<<","<<B.b<<endl; return 0; }
#ifndef _FT_FT_HPP_ #define _FT_FT_HPP_ #include <opencv2/opencv.hpp> using namespace cv; using namespace std; //============================================================================== template <class T> T load_ft(const char* fname){ T x; FileStorage f(fname,FileStorage::READ); f["ftobject"] >> x; f.release(); return x; } //============================================================================== template<class T> void save_ft(const char* fname,const T& x){ FileStorage f(fname,FileStorage::WRITE); f << "ftobject" << x; f.release(); } //============================================================================== template<class T> void write(FileStorage& fs, const string&, const T& x) { x.write(fs); } //============================================================================== template<class T> void read(const FileNode& node, T& x, const T& d) { if(node.empty())x = d; else x.read(node); } //============================================================================== #endif
#include <iostream> #include "Data.h" using namespace cv; using namespace std; void Test::write(FileStorage &fs) const { assert(fs.isOpened()); fs << "{" ; fs<< "A1" << a ; fs<< "B1" << b << "}"; } void Test::read(const FileNode& node) { int c,d; assert(node.type() == FileNode::MAP); node["A1"] >> c; node["B1"] >> d; a = c; b = d; }运行结果:
对于上述约定的不同类型的训练数据,分别定义如下vector向量:
vector<int> symmetry; //indices of symmetric points vector<Vec2i> connections; //indices of connected points vector<string> imnames; //images' name vector<vector<Point2f> > points; //annotate points上述向量将保存在类ft_data中,然后同样被序列化保存到xml文件中,供下一步模块调用
3.整个程序使用流程:
原谅我把人脸标注改成动作标注,实在找不到素材了。。。
- 获取数据:
a.获取图像,q-退出,s-保存当前帧,n-进入下一帧
b.标出特征点,q-完成标注
c.标注连接,为了表达人脸结构及语义,选中两个点即为一对连接,q-完成标注
d.标注对称点,为了镜像扩展样本集合
和c差不多,只是选中红点时会变黄点。这边对称点不太好选
(从b到d都只针对第一帧)
e.完善标注,可以任意往前后往后,修改已有标注及连接
(e是一个循环,不断遍历a中的图像,利用第一帧的标注完成剩余图像的修改)
主函数:
int main(int argc,char** argv) { Mat im,img; VideoCapture cam; char str[1024]; string ifile = "test.avi"; string fname = "annotations.xml"; //file to save annotation data to int idx = 0; int flag = 1; int c = 0; //get data namedWindow(annotation.wname); cam.open(ifile); if(!cam.isOpened()){ cout << "Failed opening video file." << endl << "usage: ./annotate [-v video] [-m muct_dir] [-d output_dir]" << endl; return 0; } cam >> im; //get images to annotate annotation.set_capture_instructions(); annotation.image = im.clone(); annotation.draw_instructions(); imshow(annotation.wname,annotation.image); while(im.empty()!= true && flag != 0) { c = waitKey(0); switch((char)c) { case 'q': cout <<"Exiting ...\n"; flag = 0; break; case 's': annotation.image = im.clone(); annotation.draw_instructions(); imshow(annotation.wname,annotation.image); idx = annotation.data.imnames.size(); if(idx < 10) sprintf(str,"00%d.png",idx); else if(idx < 100) sprintf(str,"0%d.png",idx); else sprintf(str,"%d.png",idx); imwrite(str,im); annotation.data.imnames.push_back(str); cam >> im; imshow(annotation.wname,annotation.image); break; case 'n': cam >> im; annotation.image = im.clone(); annotation.draw_instructions(); imshow(annotation.wname,annotation.image); break; } } if(annotation.data.imnames.size() == 0) return 0; annotation.data.points.resize(annotation.data.imnames.size()); //pick points setMouseCallback(annotation.wname,pp_MouseCallback,0); annotation.set_pick_points_instructions(); annotation.set_current_image(0); annotation.draw_instructions(); annotation.idx = 0; while(1) { annotation.draw_points(); imshow(annotation.wname,annotation.image); if(waitKey(0) == 'q') break; } if(annotation.data.points[0].size() == 0) return 0; annotation.replicate_annotations(0); save_ft(fname.c_str(),annotation.data); //annotate connections setMouseCallback(annotation.wname,pc_MouseCallback,0); annotation.set_connectivity_instructions(); annotation.set_current_image(0); annotation.draw_instructions(); annotation.idx = 0; while(1) { annotation.draw_connections(); imshow(annotation.wname,annotation.image); if(waitKey(0) == 'q') break; } save_ft(fname.c_str(),annotation.data); //annotate symmetry setMouseCallback(annotation.wname,ps_MouseCallback,0); annotation.initialise_symmetry(0); annotation.set_symmetry_instructions(); annotation.set_current_image(0); annotation.draw_instructions(); annotation.idx = 0; annotation.pidx = -1; while(1){ annotation.draw_symmetry(); imshow(annotation.wname,annotation.image); if(waitKey(0) == 'q') break; } save_ft(fname.c_str(),annotation.data); //annotate the rest setMouseCallback(annotation.wname,mv_MouseCallback,0); annotation.set_move_points_instructions(); annotation.idx = 1; annotation.pidx = -1; while(1) { annotation.set_current_image(annotation.idx); annotation.draw_instructions(); annotation.set_clean_image(); annotation.draw_connections(); imshow(annotation.wname,annotation.image); c = waitKey(0); if(c == 'q') break; else if(c == 'p') { annotation.idx++; annotation.pidx = -1; } else if(c == 'o') { annotation.idx--; annotation.pidx = -1; } if(annotation.idx < 0) annotation.idx = 0; if(annotation.idx >= int(annotation.data.imnames.size())) annotation.idx = annotation.data.imnames.size()-1; } save_ft(fname.c_str(),annotation.data); destroyWindow("Annotate"); return 0; }
- 展现数据:
int main() { int index = 0; bool flipped = false; Mat image; ft_data data = load_ft<ft_data>("annotations.xml"); if(data.imnames.size() == 0) { cerr << "Data file does not contain any annotations."<< endl; return 0; } data.rm_incomplete_samples(); cout << "n images: " << data.imnames.size() << endl << "n points: " << data.symmetry.size() << endl << "n connections: " << data.connections.size() << endl; //display data namedWindow("Annotations"); while(1) { if(flipped) image = data.get_image(index,3); else image = data.get_image(index,2); data.draw_connect(image,index,flipped); data.draw_sym(image,index,flipped); imshow("Annotations",image); int c = waitKey(0); if(c == 'q') break; else if(c == 'p') index++; else if(c == 'o') index--; else if(c == 'f') flipped = !flipped; if(index < 0) index = 0; else if(index >= int(data.imnames.size())) index = data.imnames.size()-1; } destroyWindow("Annotations"); return 0; return 0; }
其实这个工具对于标注数据者的要求比较高,如果弄错了,后面全白搭。MUCT的数据库目前无法下载,不知道后面的博文是否还能继续写下去。
我的OPENCV版本是2.4.3