当前位置:   article > 正文

(学习笔记)第六章 非刚性人脸跟踪——annotation(标记)_非刚性人脸追踪

非刚性人脸追踪

(学习笔记)第六章 非刚性人脸跟踪——annotation(标记)

2013年08月17日  ⁄ 综合 ⁄ 共 24921字 ⁄ 字号  小 中 大  ⁄ 评论关闭
id="iframeu1788635_0" src="http://pos.baidu.com/acom?rdid=1788635&dc=2&di=u1788635&dri=0&dis=0&dai=2&ps=236x888&dcb=BAIDU_UNION_define&dtm=BAIDU_DUP_SETJSONADSLOT&dvi=0.0&dci=-1&dpt=none&tsr=0&tpr=1454375655835&ti=(%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0)%E7%AC%AC%E5%85%AD%E7%AB%A0%20%E9%9D%9E%E5%88%9A%E6%80%A7%E4%BA%BA%E8%84%B8%E8%B7%9F%E8%B8%AA%E2%80%94%E2%80%94annotation(%E6%A0%87%E8%AE%B0)%20%7C%20%E5%AD%A6%E6%AD%A5%E5%9B%AD&ari=1&dbv=2&drs=1&pcs=1325x579&pss=1325x256&cfv=18&cpl=20&chi=1&cce=true&cec=UTF-8&tlm=1454346855&ltu=http%3A%2F%2Fwww.xuebuyuan.com%2F1354389.html&ltr=http%3A%2F%2Fwww.baidu.com%2Flink%3Furl%3D9A2jRnobVD1LeSgb3wCMsLVrPAtSLZqyDeCNRMM-mg7Vu3O3X3gfnRQj-UOwrLGH%26wd%3D%26eqid%3D812c69d2000cd3a40000000256b00418&ecd=1&psr=1366x768&par=1366x728&pis=-1x-1&ccd=32&cja=true&cmi=41&col=zh-CN&cdo=-1&tcn=1454375656&qn=4ffeacfb9a436ed1&tt=1454375655820.31.109.125" width="336" height="280" align="center,center" vspace="0" hspace="0" marginwidth="0" marginheight="0" scrolling="no" frameborder="0" allowtransparency="true" style="margin: 0px; padding: 0px; border-width: 0px; background-color: transparent; vertical-align: bottom;">





一、工具

非刚性人脸跟踪,也就是所谓的asm(active shape model)。

准备的材料,搜先到网站上下载标记用的图片和工具:

http://code.google.com/p/muct/downloads/list

1、下载图像数据文件即: muct-a-jpg-v1.tar.gz到 muct-e-jpg-v1.tar.gz即a,b,c,d,e五个压缩文件。例如直接在D盘根目录下解压会生成一个新的文件夹jpg,5个均解压后共3755张图片。

2、下载muct-landmarks-v1.tar.gz文件,将其加压到和图片同样的目录,这里采用解压到D盘的根目录。

3、下载Mastering OpenCV的随书代码(资料源内有),第六章,Chapter6_NonRigidFaceTracking。

4、新建OpenCV工程,将annotate.cpp做为主程序,并将其用到的头文件和源文件引入到工程。在调试——命令行参数输入格式如下:

./annotate -m $mdir -d $odir 这里的$mdir是存储图像数据所在目录的文件的目录,$odir是要输出的 annotations.yaml文档的路径,该文档包含的数据是以ft_data对象存储的。

例如我输入的命令行参数为:./annotate -m D:/  -d D:/result_landmarks

二、知识普及:.

ASM是基于点分布的模型。点分布模型(Point Distribution Model,PDM)

ASM模型的建立的第一步就是标记图像的特征.

为建立表情完备的模型,训练集中的图像数目应为一百幅以上,且包含不同变化类型的人脸图像:

1)不同性别、年龄、长相尽可能具有代表性。

2)不同表情的图像

3)不同姿态的图像

为了得到最好的效果,图像集应当详细说明环境的类型(即身份,光照,到相机的距离,捕捉设备以及其他)

收集关于形状变化的信息,需要图像中相应的点的位置等信息,在脸部轮廓,以及眉毛,眼睛,鼻子,嘴巴的轮廓和瞳孔等位置的选取一定数目分布均匀的特征点,这样一来,每个形状都可以通过一组标定点来描述。特征点的选取奔着如下的选取原则:

1)一是关键特征点的选取就是那些肉眼直接分辨出的特征点。

2)二是这些特征点之间尽可能的均匀的分布一些特征点。

3)三是特征点的密度要适当使其能够表现形状的全貌和细节。

对于训练集的每幅图像,我们通过目测确定出每个点在形状中的位置,这个过程就叫做训练集的标定。能够准确的指定标定点的位置对于下面的整个过程是非常重要的。在形状标定上,每一幅图像的相应标定点的位置次序必须严格一致。比如,第一幅图像是逆时针顺序标定的,后面的所有图像必须按照一致的顺序进行,绝不能出现顺时针标定,并且每个点在人脸的相对位置也要始终保存一致,否则,也会影响模型的精确性。

幸运地是,对于MUCT数据库,我们不需要对每个图像进行特征点的标定,因为它已经给出了对外使用的接口,即特征点的坐标位置;

在muct-landmarks文件夹下存在四个文件:

1)muct76.shape       shape文件(www.milbo.users.sonic.net/stasm)

2)muct76.rda         R数据文件(www.r-project.org/)

3)muct76.csv       逗号分割值

4)muct76-opencv.csv 逗号分割值,针对opencv的坐标系即原点在左上方。

注意:这些文件的坐标系统和stasm使用的一样.(即(0,0)点在图像的中心,向左移动x增加,向上移动y增加。而一个例外是muct76-opencv.csv,这里的格式是opencv中的格式,即原点在左上方,向左移动x增加,向下移动y增加)

无效的点用坐标(0,0)标记,"无效的点”是那些被其他面部特征遮挡的点,这指得是鼻子后面或者脸侧面,这些特征点的位置不容易估计。相反的,头发或者眼镜后的特征点位置是通过标记者估计的。

因此任何坐标为(0,0)的点应当被忽视。无效的点只会出现在相机画面b和c上。除非你的程序知道如何处理这些无效的点,否者你应当只使用相机画面a,d,和e

Note that subjects 247 and 248 are identical twins. 注意:247和248是孪生双胞胎。

三、程序

运行程序效果如下:

为了方便下面的理解,我们将76个点的相对位置标记处理,由于点比较靠近,标记的效果不是太好,当然也可以通过人脸检测,人眼检测然后找到人眼矩形区域,然后在计算相对位置,接着放大图像,进行标记,这里为了简单,不再进行上述步骤:

下面我们分析一下代码以及实现:

1、ft_data.cpp

我们数据的导入,输出,以及简单处理和画出都是通过ft_data类来完成的,该类有如下的数据成员,用来存储操作的数据。(这里ft_data是face tracking data)

  1. vector<int> symmetry; //indices of symmetric points//对称点的索引,即vector<int>存储的是对称点的索引
  2. vector<Vec2i> connections; //indices of connected points //存储的为联通点索引
  3. vector<string> imnames; //images//图像的名字
  4. vector<vector<Point2f> > points; //points//图像中的点集

从左到右,第一个是原始图像,第二个是面部特征标记(76个点),第三个为用彩色标记出两边对称的点(symmery),第四个是镜像图像(flip),第五个是面部特征的连通性(connections)

1、symmetry;这里的vector<int> symmery;成员是存储的点值所在的索引,即0,1,2,3,.....75。例如,假如点p1在点序列(76个)中的位置为3,与之相对称的点p2(关于面部对称,类似与左眼和由于是对称的)在序列中的位置为15,那么symmery[2]存储是15,symmery[14]存储的为3.这需要我们在程序运行的时候,手动的点击图像上的点形成对称点。

2、connections;存储连通性,Vec2i类型为Vec<int, 2>存储两个整数对的容器。例如我们将一个正数对(23,15)存储到vector<Vec2i> connections中,利用Vec2i的构造函数构造一个Vec2i类型的对象  Vec2i     v2(23,15);然后利用connectons.pushback(v2);访问采用connections[0]获取Vec2i对象,然后connections[0][0]得到23,connections[0][1]得到15.

3、imnames;是存储的图像名字,(假设图像前几个图像不存在无效点)则:

imnames[0]==i000qa-fn

imnames[1]==i000qb-fn

imnames[2]==i000qc-fn......

4、points;是存储的点集,例如通过points[0]可以访问第一个图像的点集序列,点集中的顺序(容器中的位置顺序)是由加载的muct-landmarks文件夹下的 muct76-opencv.csv中的点的顺序决定的,这个顺序并不影响我们的使用。即一个图像有76个特征点,每个图像的特征点均是按照同样的相对位置进行排序的,例如我们看muct-landmarks文件夹下的muct76-opencv.csv,我们程序读取的时候就是对每个图像的76个点相对应的顺序读取的,如表格中第图像的i000qa-fn的第一个坐标为(x00,y00)=(201,348),然后我们可以通过points访问到该点,首先它是第一个图片(如果想上面提到的,它不存在无效的点,即被存储到了points中,如果为无效的点,则不会被存储到points中),通过points[0]访问到这个图像的点集序列,通过points[0][0],访问到这个(x00,y00),points[0][1]=(x01,y01)等等。

2、ft_data数据的生成

我们要理解ft_data数据成员的含义,最好的方法就是看看这些数据成员的数据是通过什么样的方式生成的。数据生成是通过annotations.cpp来实现的,该类在实现时,直接后跟着示例化了一个annotation对象。

annotation类的数据成员:

  1. int idx; //index of image to annotate//用来标记的图像索引
  2. int pidx; //index of point to manipulate//用来操作的点的索引,用来操作对称性
  3. Mat image; //current image to display //用来显示的当前图像
  4. Mat image_clean; //clean image to display//用来显示的清新图像,原图像的副本
  5. ft_data data; //annotation data//标记数据
  6. const char* wname; //display window name//显示的窗口名字
  7. vector<string> instructions; //annotation instructions//操作说明,窗口左上角的提示

首先从annotation的主函数入手,即看一下ft_data类四个数据成员:symmetry ,connections,imnames,points 的数据形成过程:

1、部分main函数,完成的功能是读入csv文件中的数据,包括图像的名字和这些图像的特征点的位置,也即points点数据和imnames数据的形成的函数

  1. //parse cmd line options
  2. if(parse_help(argc,argv)){//检测命令行参数中是否输入了-h和-help即帮组指令
  3. cout << "usage: ./annotate [-v video] [-m muct_dir] [-d output_dir]"
  4. << endl; return 0;
  5. }
  6. string odir = parse_odir(argc,argv);
  7. string ifile; int type = parse_ifile(argc,argv,ifile);//type==2表示输入的为MUCT数据,type==1表示输入的为视频文件
  8. string fname = odir + "annotations.yaml"; //file to save annotation data to//保存标记数据的文件
  9. //get data
  10. namedWindow(annotation.wname); //annotation这个为类定义时实例化了一个对象
  11. if(type == 2){ //MUCT data
  12. string lmfile = ifile + "muct-landmarks/muct76-opencv.csv";//ifile是muct-landmarks文件夹所在的根目录
  13. ifstream file(lmfile.c_str()); //lmfile表示landmarks文件
  14. if(!file.is_open()){
  15. cerr << "Failed opening " << lmfile << " for reading!" << endl; return 0;
  16. }
  17. //从csv文件中读取图片名和标记的坐标
  18. string str; getline(file,str);//获取文件流到string对象,获取csv文件中的第一行,不用抛弃
  19. while(!file.eof()){ //如果没有遇到文件结束符
  20. getline(file,str); if(str.length() == 0)break; //获取csv中的数据行,没有则break
  21. muct_data d(str,ifile); if(d.name.length() == 0)continue;
  22. annotation.data.imnames.push_back(d.name);//存储图像的名字
  23. annotation.data.points.push_back(d.points);//存储图像上特征点的坐标
  24. }
  25. file.close();
  26. annotation.data.rm_incomplete_samples();//去除不完整的样本
  27. }

说明:rm_imcomplete_sampes()函数用来去除下列条件的点:

1)图像中点的数目没有达到所有图像的中最多点的数目的图像。这里的用途就是去除点的数目小于76的图像的特征数据(或者说图像)

2)“无效的点”,即x和y至少有一个小于等于0的点。从相机b,c采集来的图像才存在这样的无效点,我们的程序可以处理这样的无效点。

该函数定义在ft_data.cpp文件夹下,是ft_data的成员函数。

  1. //==============================================================================
  2. void
  3. ft_data::
  4. rm_incomplete_samples()//去掉不完整的样本
  5. {
  6. int n = points[0].size(),N = points.size();
  7. for(int i = 1; i < N; i++)n = max(n,int(points[i].size()));//从所有图像中找出最大点集数量,以此为参照
  8. for(int i = 0; i < int(points.size()); i++){
  9. if(int(points[i].size()) != n){//如果大小小于最大的,则移除掉
  10. points.erase(points.begin()+i); imnames.erase(imnames.begin()+i); i--;
  11. }else{//否则,相当于分类,在进行第二次过滤,将点集中坐标(x,y)两个有其一小于等于0的点的,即所谓的“无效点”
  12. int j = 0;
  13. for(; j < n; j++){
  14. if((points[i][j].x <= 0) || (points[i][j].y <= 0))break;
  15. }
  16. if(j < n){//移除“无效点”所在的点集和图片的路径
  17. points.erase(points.begin()+i); imnames.erase(imnames.begin()+i); i--;
  18. }
  19. }
  20. }
  21. }
  22. //==============================================================================

2、main函数中部分代码,connections数据的形成

  1. //annotate connectivity //标记连通性
  2. setMouseCallback(annotation.wname,pc_MouseCallback,0);//设置鼠标回调函数
  3. annotation.set_connectivity_instructions();
  4. annotation.set_current_image(0);
  5. annotation.draw_instructions();
  6. annotation.idx = 0;
  7. while(1){ annotation.draw_connections();
  8. imshow(annotation.wname,annotation.image); if(waitKey(0) == 'q')break;
  9. }
  10. save_ft(fname.c_str(),annotation.data);

connections数据的形成主要是通过我们手动添加的。这里通过一个交互的方式来实现,即通过鼠标回调函数,该函数定义如下:

  1. void pc_MouseCallback(int event, int x, int y, int /*flags*/, void* /*param*/)//pc 表示point connections点的连通
  2. { //这里Vec2i(first,second),first是指当前的点,second是其后的点。
  3. if(event == CV_EVENT_LBUTTONDOWN){//鼠标左键事件
  4. int imin = annotation.find_closest_point(Point2f(x,y));//鼠标点击位置最邻近的点,返回的是该点在点序列中的位置
  5. if(imin >= 0){ //add connection//增加连通
  6. int m = annotation.data.connections.size();//获取 vector<Vec2i> connections的大小
  7. if(m == 0)annotation.data.connections.push_back(Vec2i(imin,-1));//如果是第一个数据,压入两个点的序号,即第一个点与-1关联
  8. else{
  9. if(annotation.data.connections[m-1][1] < 0)//1st connecting point chosen,//选择最后一个连通点
  10. annotation.data.connections[m-1][1] = imin;//如果有一个关联到了,则修改(imin,-1)中的-1为现在的-1
  11. else annotation.data.connections.push_back(Vec2i(imin,-1));//没有关联则还是压入,imin,
  12. }
  13. annotation.draw_connections(); //画出连通性
  14. imshow(annotation.wname,annotation.image); //展示图像
  15. }
  16. }
  17. }

说明:

1)该回调函数的理解,即也就是ft_data数据成员connection的形成过程,可以通过运行工程,手工操作亲自体验,不然不好理解。

2)find_closest_point(Point2f(x,y))是找到鼠标点击位置最临近的特征点,当然这个函数也有阈值约束的当如果发现对临近的距离小于10像素(为默认,当然你可以修改),则表示没有选中。

3)连通性的增加都是获取connections容器的最后一个来处理的。

  1. if (如果存在与鼠标所点位置匹配的特征点)
  2. 则 {
  3. 获取所存连通性容器的最后一个连通关系;
  4. if(如果是第一次增加连通性)
  5. {
  6. 将当前该特征点和一个-1进行匹配——并将其存储到连通性容器中。
  7. }
  8. else //不是第一次
  9. {
  10. if(如果最后容器中存储的最后一个点,匹配的是-1)
  11. {
  12. 将容器中存储的最后一个特征点和当前该特征点匹配;//至少修改
  13. }
  14. else//如果最后一个有了匹配
  15. {
  16. 将该当前特征点和-1匹配——并且将器存储到连通性容器中
  17. }
  18. 画出连通性;
  19. 展示图像;
  20. }//end_else
  21. }//end_if

4)存在如下的情况:

1、第一次鼠标点击图像中的一个特征点,则也是第一次存储两个点的连通性,则存储一个<当前特征点,-1>的对象;

2、当第二次鼠标点击图像中的另外一个特征点时,算法找到先前的最后一个特征点,发现他没有匹配的,则修改为<上一次的特征点,当前特征点>

3、我们在想:1)连通性在connection容器中的创建没有先后顺序,即连通性对在容器中的位置没有约束。

                          2)如果我们上一次点击的特征点和这一次点击的特征点是同一个特征点如何处理?这里好像没有处理的(现在我还没发现),其实没有相应的处理也是可以理解的,假如我们要连通两个点,即两个点之间有连线,如果还有一个点离着这两个点很近,如果我们不小心本应该连接a点和b点,结果连接成了a点和c点,因此程序无法判别我们的意图,所有在进行手工标记连通性时要格外的细心,如果不小心标错了,则重新运行程序标定。

我们可能会想对于:<特征点,-1>这样的连通性,它是怎么画出的呢?即我们看一下上面annotation.draw_connections()函数的实现:

  1. void
  2. draw_connections(){
  3. int m = data.connections.size();
  4. if(m == 0)this->draw_points();//如果没有连通性,则调用ft_data的draw_points只画出那些特征点
  5. else{
  6. if(data.connections[m-1][1] < 0){//如果该点没有下一个点
  7. int i = data.connections[m-1][0];//获取第一个点的位置。
  8. data.connections[m-1][1] = i;//将第一个点赋给第一个点,即本身,为了直接调用下面的画图函数
  9. data.draw_connect(image,idx); this->draw_points();
  10. circle(image,data.points[idx][i],1,CV_RGB(0,255,0),2,CV_AA);//用绿色标识最后一个点,
  11. data.connections[m-1][1] = -1;//画完后,在修改到起始数据,
  12. }else{data.draw_connect(image,idx); this->draw_points();}
  13. }
  14. }

说明:

其一,从上面的代码我们可以看出,将出现<特征点,-1>的情况时,它做了如下的处理即转换为<特征点,特征点(同样的)>即要画线的两个点是同一个点。

其二,该函数实际只是简单的处理一下输入的数据,相当于打包一下,实际的画图还是调用ft_data类的draw_conncect函数来完成。下面我们看一下ft_data::draw_connect函数:

  1. void
  2. ft_data::
  3. draw_connect(Mat &im,//im画布
  4. const int idx,//图像的索引,也就是图像点集的索引
  5. const bool flipped,//我们在标记annotation.cpp初始化数据时,这里采用默认值,false
  6. const Scalar color,//默认值Scalar(255,0,0)
  7. const vector<int> &con)//默认值vector<int>()一个空对象
  8. {
  9. if((idx < 0) || (idx >= (int)imnames.size()))return;
  10. int n = connections.size();//连通的大小
  11. if(con.size() == 0){ //我们调用时采用的为默认的参数的函数,con.size()==0
  12. for(int i = 0; i < n; i++){
  13. int j = connections[i][0],k = connections[i][1];//获取要连接的两个的位置
  14. if(!flipped)line(im,points[idx][j],points[idx][k],color,1);//如果不翻转,则直接画出,采用默认蓝色
  15. else{
  16. Point2f p(im.cols - 1 - points[idx][symmetry[j]].x,
  17. points[idx][symmetry[j]].y);
  18. Point2f q(im.cols - 1 - points[idx][symmetry[k]].x,
  19. points[idx][symmetry[k]].y);
  20. line(im,p,q,color,1);//如果翻转,则计算翻转后的点在画出
  21. }
  22. }
  23. }else{
  24. int m = con.size();
  25. for(int j = 0; j < m; j++){
  26. int i = con[j]; if((i < 0) || (i >= n))continue;
  27. int k = connections[i][0],l = connections[i][1];
  28. if(!flipped)line(im,points[idx][k],points[idx][l],color,1);
  29. else{
  30. Point2f p(im.cols - 1 - points[idx][symmetry[k]].x,
  31. points[idx][symmetry[k]].y);
  32. Point2f q(im.cols - 1 - points[idx][symmetry[l]].x,
  33. points[idx][symmetry[l]].y);
  34. line(im,p,q,color,1);
  35. }
  36. }
  37. }
  38. }

说明:

我们在annotation.cpp中使用的是带有默认参数的draw_connect函数,即此时的con对象是一个空的,即我们只关心满足if(con.size()==0)的条件内的语句,即上面带有注释的语句。

3、main函数中symmetry 数据的生成


同样的symmetry数据的生成也是通过交互式界面,我们手动标记两个点的的对称型的,我们首先看一下在main函数中的代码:

  1. //annotate symmetry//标记对称性
  2. setMouseCallback(annotation.wname,ps_MouseCallback,0);
  3. annotation.initialise_symmetry(0);
  4. annotation.set_symmetry_instructions();
  5. annotation.set_current_image(0);
  6. annotation.draw_instructions();
  7. annotation.idx = 0; annotation.pidx = -1;
  8. while(1){ annotation.draw_symmetry();
  9. imshow(annotation.wname,annotation.image); if(waitKey(0) == 'q')break;
  10. }
  11. save_ft(fname.c_str(),annotation.data);

上述鼠标回调函数调用ps_MouseCallback()函数,该函数定义如下:

  1. void ps_MouseCallback(int event, int x, int y, int /*flags*/, void* /*param*/)//ps 表示point symmetry点的对称
  2. {
  3. if(event == CV_EVENT_LBUTTONDOWN){//监听左键事件
  4. int imin = annotation.find_closest_point(Point2f(x,y));//找到鼠标点击位置临近的点
  5. if(imin >= 0){//如果找到了
  6. if(annotation.pidx < 0)annotation.pidx = imin;//如果标记点的索引小于0,则把当前点的给标记点
  7. else{
  8. annotation.data.symmetry[annotation.pidx] = imin;//否则把当前点作为标记点的对称点
  9. annotation.data.symmetry[imin] = annotation.pidx;//并且将当前点的对称点设置为上一次标记的点
  10. annotation.pidx = -1;//完成一次点的匹配,回复到初始状态
  11. }
  12. annotation.draw_symmetry();
  13. imshow(annotation.wname,annotation.image);
  14. }//if
  15. }//if
  16. }

说明:

上面程序完成的功能是存储与之对称的点的位置。例如vector<int> symmetry中,symmetry[0]的值是一点在序列中的相对位置,这里假设值为5,则(0,5)即为一对对称点,0和5是点在序列,或者说是容器中以0开始的相对位置。那么,symmetry[5]的结果自然就是0了。

附函数annotation.cpp中代码,带有部分注释:

  1. /*****************************************************************************
  2. * Non-Rigid Face Tracking
  3. ******************************************************************************
  4. * by Jason Saragih, 5th Dec 2012
  5. * http://jsaragih.org/
  6. ******************************************************************************
  7. * Ch6 of the book "Mastering OpenCV with Practical Computer Vision Projects"
  8. * Copyright Packt Publishing 2012.
  9. * http://www.packtpub.com/cool-projects-with-opencv/book
  10. *****************************************************************************/
  11. /*
  12. annotate: annotation tool
  13. Jason Saragih (2012)
  14. */
  15. #include "opencv_hotshots/ft/ft.hpp"
  16. #include <opencv2/highgui/highgui.hpp>
  17. #include <iostream>
  18. #include <fstream>
  19. //==============================================================================
  20. class annotate{
  21. public:
  22. int idx; //index of image to annotate//用来标记的图像索引
  23. int pidx; //index of point to manipulate//用来操作的点的索引,用来操作对称性
  24. Mat image; //current image to display //用来显示的当前图像
  25. Mat image_clean; //clean image to display//用来显示的清新图像,原图像的副本
  26. ft_data data; //annotation data//标记数据
  27. const char* wname; //display window name//显示的窗口名字
  28. vector<string> instructions; //annotation instructions//操作说明,窗口左上角的提示
  29. annotate(){wname = "Annotate"; idx = 0; pidx = -1;}//默认构造函数
  30. int
  31. set_current_image(const int idx = 0){
  32. if((idx < 0) || (idx > int(data.imnames.size())))return 0;
  33. image = data.get_image(idx,2); return 1;
  34. }
  35. void
  36. set_clean_image(){
  37. image_clean = image.clone();
  38. }
  39. void
  40. copy_clean_image(){
  41. image_clean.copyTo(image);
  42. }
  43. void
  44. draw_instructions(){
  45. if(image.empty())return;
  46. this->draw_strings(image,instructions);
  47. }
  48. void
  49. draw_points(){
  50. data.draw_points(image,idx);
  51. }
  52. void
  53. draw_chosen_point(){
  54. if(pidx >= 0)circle(image,data.points[idx][pidx],1,CV_RGB(0,255,0),2,CV_AA);
  55. }
  56. void
  57. draw_connections(){
  58. int m = data.connections.size();
  59. if(m == 0)this->draw_points();//如果没有连通性,则调用ft_data的draw_points只画出那些特征点
  60. else{
  61. if(data.connections[m-1][1] < 0){//如果该点没有下一个点
  62. int i = data.connections[m-1][0];//获取第一个点的位置。
  63. data.connections[m-1][1] = i;//将第一个点赋给第一个点,即本身,为了直接调用下面的画图函数
  64. data.draw_connect(image,idx); this->draw_points();
  65. circle(image,data.points[idx][i],1,CV_RGB(0,255,0),2,CV_AA);//用绿色标识最后一个点,
  66. data.connections[m-1][1] = -1;//画完后,在修改到起始数据,
  67. }else{data.draw_connect(image,idx); this->draw_points();}
  68. }
  69. }
  70. void
  71. draw_symmetry(){
  72. this->draw_points(); this->draw_connections();
  73. for(int i = 0; i < int(data.symmetry.size()); i++){
  74. int j = data.symmetry[i];
  75. if(j != i){
  76. circle(image,data.points[idx][i],1,CV_RGB(255,255,0),2,CV_AA);
  77. circle(image,data.points[idx][j],1,CV_RGB(255,255,0),2,CV_AA);
  78. }
  79. }
  80. if(pidx >= 0)circle(image,data.points[idx][pidx],1,CV_RGB(0,255,0),2,CV_AA);
  81. }
  82. void
  83. set_capture_instructions(){
  84. instructions.clear();
  85. instructions.push_back(string("Select expressive frames."));
  86. instructions.push_back(string("s - use this frame"));
  87. instructions.push_back(string("q - done"));
  88. }
  89. void
  90. set_pick_points_instructions(){
  91. instructions.clear();
  92. instructions.push_back(string("Pick Points"));
  93. instructions.push_back(string("q - done"));
  94. }
  95. void
  96. set_connectivity_instructions(){
  97. instructions.clear();
  98. instructions.push_back(string("Pick Connections"));
  99. instructions.push_back(string("q - done"));
  100. }
  101. void
  102. set_symmetry_instructions(){
  103. instructions.clear();
  104. instructions.push_back(string("Pick Symmetric Points"));
  105. instructions.push_back(string("q - done"));
  106. }
  107. void
  108. set_move_points_instructions(){
  109. instructions.clear();
  110. instructions.push_back(string("Move Points"));
  111. instructions.push_back(string("p - next image"));
  112. instructions.push_back(string("o - previous image"));
  113. instructions.push_back(string("q - done"));
  114. }
  115. void
  116. initialise_symmetry(const int index){
  117. int n = data.points[index].size(); data.symmetry.resize(n);
  118. for(int i = 0; i < n; i++)data.symmetry[i] = i;
  119. }
  120. void
  121. replicate_annotations(const int index){
  122. if((index < 0) || (index >= int(data.points.size())))return;
  123. for(int i = 0; i < int(data.points.size()); i++){
  124. if(i == index)continue;
  125. data.points[i] = data.points[index];
  126. }
  127. }
  128. int
  129. find_closest_point(const Point2f p,
  130. const double thresh = 10.0){
  131. int n = data.points[idx].size(),imin = -1; double dmin = -1;
  132. for(int i = 0; i < n; i++){
  133. double d = norm(p-data.points[idx][i]);
  134. if((imin < 0) || (d < dmin)){imin = i; dmin = d;}
  135. }
  136. if((dmin >= 0) && (dmin < thresh))return imin; else return -1;
  137. }
  138. protected:
  139. void
  140. draw_strings(Mat img,
  141. const vector<string> &text){
  142. for(int i = 0; i < int(text.size()); i++)this->draw_string(img,text[i],i+1);
  143. }
  144. void
  145. draw_string(Mat img,
  146. const string text,
  147. const int level)
  148. {
  149. Size size = getTextSize(text,FONT_HERSHEY_COMPLEX,0.6f,1,NULL);
  150. putText(img,text,Point(0,level*size.height),FONT_HERSHEY_COMPLEX,0.6f,
  151. Scalar::all(0),1,CV_AA);
  152. putText(img,text,Point(1,level*size.height+1),FONT_HERSHEY_COMPLEX,0.6f,
  153. Scalar::all(255),1,CV_AA);
  154. }
  155. }annotation;
  156. //==============================================================================
  157. void pp_MouseCallback(int event, int x, int y, int /*flags*/, void* /*param*/)
  158. {
  159. if(event == CV_EVENT_LBUTTONDOWN){
  160. annotation.data.points[0].push_back(Point2f(x,y));
  161. annotation.draw_points(); imshow(annotation.wname,annotation.image);
  162. }
  163. }
  164. //==============================================================================
  165. void pc_MouseCallback(int event, int x, int y, int /*flags*/, void* /*param*/)//pc 表示point connections点的连通
  166. { //这里Vec2i(first,second),first是指当前的点,second是其后的点。
  167. if(event == CV_EVENT_LBUTTONDOWN){//鼠标左键事件
  168. int imin = annotation.find_closest_point(Point2f(x,y));//找到点击的最临近的点在
  169. if(imin >= 0){ //add connection
  170. int m = annotation.data.connections.size();//获取 vector<Vec2i> connections的大小
  171. if(m == 0)annotation.data.connections.push_back(Vec2i(imin,-1));//如果是第一个数据,压入两个点的序号,即第一个点与-1关联
  172. else{
  173. if(annotation.data.connections[m-1][1] < 0)//1st connecting point chosen,//选择最后一个连通点
  174. annotation.data.connections[m-1][1] = imin;//如果有一个关联到了,则修改(imin,-1)中的-1为现在的-1
  175. else annotation.data.connections.push_back(Vec2i(imin,-1));//没有关联则还是压入,imin,
  176. }
  177. annotation.draw_connections(); //画出连通性
  178. imshow(annotation.wname,annotation.image); //展示图像
  179. }
  180. }
  181. }
  182. //==============================================================================
  183. void ps_MouseCallback(int event, int x, int y, int /*flags*/, void* /*param*/)//ps 表示point symmetry点的对称
  184. {
  185. if(event == CV_EVENT_LBUTTONDOWN){//监听左键事件
  186. int imin = annotation.find_closest_point(Point2f(x,y));//找到鼠标点击位置临近的点
  187. if(imin >= 0){//如果找到了
  188. if(annotation.pidx < 0)annotation.pidx = imin;//如果标记点的索引小于0,则把当前点的给标记点
  189. else{
  190. annotation.data.symmetry[annotation.pidx] = imin;//否则把当前点作为标记点的对称点
  191. annotation.data.symmetry[imin] = annotation.pidx;//并且将当前点的对称点设置为上一次标记的点
  192. annotation.pidx = -1;//完成一次点的匹配,回复到初始状态
  193. }
  194. annotation.draw_symmetry();
  195. imshow(annotation.wname,annotation.image);
  196. }//if
  197. }//if
  198. }
  199. //==============================================================================
  200. void mv_MouseCallback(int event, int x, int y, int /*flags*/, void* /*param*/)
  201. {
  202. if(event == CV_EVENT_LBUTTONDOWN){
  203. if(annotation.pidx < 0){
  204. annotation.pidx = annotation.find_closest_point(Point2f(x,y));
  205. }else annotation.pidx = -1;
  206. annotation.copy_clean_image();
  207. annotation.draw_connections();
  208. annotation.draw_chosen_point();
  209. imshow(annotation.wname,annotation.image);
  210. }else if(event == CV_EVENT_MOUSEMOVE){
  211. if(annotation.pidx >= 0){
  212. annotation.data.points[annotation.idx][annotation.pidx] = Point2f(x,y);
  213. annotation.copy_clean_image();
  214. annotation.draw_connections();
  215. annotation.draw_chosen_point();
  216. imshow(annotation.wname,annotation.image);
  217. }
  218. }
  219. }
  220. //==============================================================================
  221. class muct_data{ //从来读取csv中数据的类,存储图片名字和索引,以及图片对应的点集
  222. public:
  223. string name; //图片的名字
  224. string index; //图片的索引
  225. vector<Point2f> points; //图片中特征点的坐标
  226. muct_data(string str,//str存储的为从流中读取的每一行
  227. string muct_dir){ //构造函数,这里muct_dir传入的是muct-landmarks文件夹所在的根目录,我这里为"D:/"
  228. size_t p1 = 0,p2;
  229. //set image directory//设置图像路径
  230. string idir = muct_dir; if(idir[idir.length()-1] != '/')idir += "/";//如果输入的路径后面没有"/",则加上"/"
  231. idir += "jpg/";//即这就是为什么要求注释工具文件夹muct-landmarks和jpg文件夹在同一目录。
  232. //get image name//获取图像的名字
  233. p2 = str.find(",");//寻找逗号的位置
  234. if(p2 == string::npos){cerr << "Invalid MUCT file" << endl; exit(0);}//如果没有找到则输出无效 muct file
  235. name = str.substr(p1,p2-p1);//从起始位置开始的到逗号之前的str的部分,即从0到','
  236. if((strcmp(name.c_str(),"i434xe-fn") == 0) || //corrupted data//损坏的数据
  237. (name[1] == 'r'))name = ""; //ignore flipped images//忽略翻转的图像
  238. else{
  239. name = idir + str.substr(p1,p2-p1) + ".jpg"; p1 = p2+1;//获取图像路径,并且获取图像名字的下一个位置
  240. //get index
  241. p2 = str.find(",",p1);//找到从p1开始的下一个逗号
  242. if(p2 == string::npos){cerr << "Invalid MUCT file" << endl; exit(0);}//如果没有找到,则说明是无效的数据,即没有坐标信息
  243. index = str.substr(p1,p2-p1); p1 = p2+1;
  244. //get points//获取点的坐标信息
  245. for(int i = 0; i < 75; i++){//0-74共75个
  246. p2 = str.find(",",p1);//p2用来存储','逗号的位置,p1用来存储逗号的下一个位置
  247. if(p2 == string::npos){cerr << "Invalid MUCT file" << endl; exit(0);}
  248. string x = str.substr(p1,p2-p1); p1 = p2+1;//获取x坐标
  249. p2 = str.find(",",p1);
  250. if(p2 == string::npos){cerr << "Invalid MUCT file" << endl; exit(0);}
  251. string y = str.substr(p1,p2-p1); p1 = p2+1;//获取y坐标
  252. points.push_back(Point2f(atoi(x.c_str()),atoi(y.c_str())));
  253. }
  254. p2 = str.find(",",p1);//最后一个单独处理
  255. if(p2 == string::npos){cerr << "Invalid MUCT file" << endl; exit(0);}
  256. string x = str.substr(p1,p2-p1); p1 = p2+1;
  257. string y = str.substr(p1,str.length()-p1);//最后一个不需要find逗号,
  258. points.push_back(Point2f(atoi(x.c_str()),atoi(y.c_str())));
  259. }
  260. }
  261. };
  262. //==============================================================================
  263. bool
  264. parse_help(int argc,char** argv)//检测是否输入了-h和-help指令
  265. {
  266. for(int i = 1; i < argc; i++){
  267. string str = argv[i];
  268. if(str.length() == 2){if(strcmp(str.c_str(),"-h") == 0)return true;}
  269. if(str.length() == 6){if(strcmp(str.c_str(),"--help") == 0)return true;}
  270. }return false;
  271. }
  272. //==============================================================================
  273. string
  274. parse_odir(int argc,char** argv)//parse从语法上分析,函数功能是找到命令行输入目的输出目录
  275. {
  276. string odir = "data/";
  277. for(int i = 1; i < argc; i++){
  278. string str = argv[i];
  279. if(str.length() != 2)continue;
  280. if(strcmp(str.c_str(),"-d") == 0){
  281. if(argc > i+1){odir = argv[i+1]; break;}//寻找目的目录
  282. }
  283. }
  284. if(odir[odir.length()-1] != '/')odir += "/";//如果你输入的参数目录中的最后没有'/'则我们在后面追加一个,更安全
  285. return odir;
  286. }
  287. //==============================================================================
  288. int
  289. parse_ifile(int argc,//从参数中寻找输入的文件名
  290. char** argv,
  291. string& ifile)
  292. {
  293. for(int i = 1; i < argc; i++){
  294. string str = argv[i];
  295. if(str.length() != 2)continue;
  296. if(strcmp(str.c_str(),"-m") == 0){ //MUCT data//寻找明命令行中的-m,进而寻找到输入的命令行参数中要输入的muct数据的地址
  297. if(argc > i+1){ifile = argv[i+1]; return 2;}//找到了返回结束
  298. }
  299. if(strcmp(str.c_str(),"-v") == 0){ //video file 这里我们不用视频文件,所以略去
  300. if(argc > i+1){ifile = argv[i+1]; return 1;}
  301. }
  302. }
  303. ifile = ""; return 0;
  304. }
  305. //==============================================================================
  306. int main(int argc,char** argv)
  307. {
  308. //parse cmd line options
  309. if(parse_help(argc,argv)){//检测命令行参数中是否输入了-h和-help即帮组指令
  310. cout << "usage: ./annotate [-v video] [-m muct_dir] [-d output_dir]"
  311. << endl; return 0;
  312. }
  313. string odir = parse_odir(argc,argv);
  314. string ifile; int type = parse_ifile(argc,argv,ifile);//type==2表示输入的为MUCT数据,type==1表示输入的为视频文件
  315. string fname = odir + "annotations.yaml"; //file to save annotation data to//保存标记数据的文件
  316. //get data
  317. namedWindow(annotation.wname); //annotation这个为类定义时实例化了一个对象
  318. if(type == 2){ //MUCT data
  319. string lmfile = ifile + "muct-landmarks/muct76-opencv.csv";//ifile是muct-landmarks文件夹所在的根目录
  320. ifstream file(lmfile.c_str()); //lmfile表示landmarks文件
  321. if(!file.is_open()){
  322. cerr << "Failed opening " << lmfile << " for reading!" << endl; return 0;
  323. }
  324. //从csv文件中读取图片名和标记的坐标
  325. string str; getline(file,str);//获取文件流到string对象,获取csv文件中的第一行,不用抛弃
  326. while(!file.eof()){ //如果没有遇到文件结束符
  327. getline(file,str); if(str.length() == 0)break; //获取csv中的数据行,没有则break
  328. muct_data d(str,ifile); if(d.name.length() == 0)continue;
  329. annotation.data.imnames.push_back(d.name);//存储图像的名字
  330. annotation.data.points.push_back(d.points);//存储图像上特征点的坐标
  331. }
  332. file.close();
  333. annotation.data.rm_incomplete_samples();//去除不完整的样本
  334. }else{
  335. //open video stream//我们的type==2,是处理的图片库不是视频文件,故这里略过
  336. VideoCapture cam;
  337. if(type == 1)cam.open(ifile); else cam.open(0);
  338. if(!cam.isOpened()){
  339. cout << "Failed opening video file." << endl
  340. << "usage: ./annotate [-v video] [-m muct_dir] [-d output_dir]"
  341. << endl; return 0;
  342. }
  343. //get images to annotate//获取图像用来标记
  344. annotation.set_capture_instructions();//初始化标记的instructions对象
  345. while(cam.get(CV_CAP_PROP_POS_AVI_RATIO) < 0.999999){
  346. Mat im,img; cam >> im; annotation.image = im.clone();
  347. annotation.draw_instructions();
  348. imshow(annotation.wname,annotation.image); int c = waitKey(10);
  349. if(c == 'q')break;
  350. else if(c == 's'){
  351. int idx = annotation.data.imnames.size(); char str[1024];
  352. if (idx < 10)sprintf(str,"%s00%d.png",odir.c_str(),idx);
  353. else if(idx < 100)sprintf(str,"%s0%d.png",odir.c_str(),idx);
  354. else sprintf(str,"%s%d.png",odir.c_str(),idx);
  355. imwrite(str,im); annotation.data.imnames.push_back(str);
  356. im = Scalar::all(255); imshow(annotation.wname,im); waitKey(10);
  357. }
  358. }
  359. if(annotation.data.imnames.size() == 0)return 0;
  360. annotation.data.points.resize(annotation.data.imnames.size());
  361. //annotate first image
  362. setMouseCallback(annotation.wname,pp_MouseCallback,0);
  363. annotation.set_pick_points_instructions();
  364. annotation.set_current_image(0);
  365. annotation.draw_instructions();
  366. annotation.idx = 0;
  367. while(1){ annotation.draw_points();
  368. imshow(annotation.wname,annotation.image); if(waitKey(0) == 'q')break;
  369. }
  370. if(annotation.data.points[0].size() == 0)return 0;
  371. annotation.replicate_annotations(0);
  372. }
  373. //从这里开始,上面的属于处理视频图像的
  374. save_ft(fname.c_str(),annotation.data);
  375. //annotate connectivity //标记连通性
  376. setMouseCallback(annotation.wname,pc_MouseCallback,0);//设置鼠标回调函数
  377. annotation.set_connectivity_instructions();
  378. annotation.set_current_image(0);
  379. annotation.draw_instructions();
  380. annotation.idx = 0;
  381. while(1){ annotation.draw_connections();
  382. imshow(annotation.wname,annotation.image); if(waitKey(0) == 'q')break;
  383. }
  384. save_ft(fname.c_str(),annotation.data);
  385. //annotate symmetry//标记连通性
  386. setMouseCallback(annotation.wname,ps_MouseCallback,0);
  387. annotation.initialise_symmetry(0);
  388. annotation.set_symmetry_instructions();
  389. annotation.set_current_image(0);
  390. annotation.draw_instructions();
  391. annotation.idx = 0; annotation.pidx = -1;
  392. while(1){ annotation.draw_symmetry();
  393. imshow(annotation.wname,annotation.image); if(waitKey(0) == 'q')break;
  394. }
  395. save_ft(fname.c_str(),annotation.data);
  396. //annotate the rest
  397. if(type != 2){
  398. setMouseCallback(annotation.wname,mv_MouseCallback,0);
  399. annotation.set_move_points_instructions();
  400. annotation.idx = 1; annotation.pidx = -1;
  401. while(1){
  402. annotation.set_current_image(annotation.idx);
  403. annotation.draw_instructions();
  404. annotation.set_clean_image();
  405. annotation.draw_connections();
  406. imshow(annotation.wname,annotation.image);
  407. int c = waitKey(0);
  408. if (c == 'q')break;
  409. else if(c == 'p'){annotation.idx++; annotation.pidx = -1;}
  410. else if(c == 'o'){annotation.idx--; annotation.pidx = -1;}
  411. if(annotation.idx < 0)annotation.idx = 0;
  412. if(annotation.idx >= int(annotation.data.imnames.size()))
  413. annotation.idx = annotation.data.imnames.size()-1;
  414. }
  415. }
  416. save_ft(fname.c_str(),annotation.data); destroyWindow("Annotate"); return 0;
  417. }
  418. //==============================================================================
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/菜鸟追梦旅行/article/detail/186576
推荐阅读
相关标签
  

闽ICP备14008679号