人脸识别项目总结

前言

这篇博客是记录如何将已有人脸识别算法的前提下,一步步搭建人脸识别系统,找人脸识别算法不用往下看了
作为一水硕,研究生两年就做了一个半成品的人脸识别演示项目,突然想写点什么给以后的自己看看。也许未来自己会嘲笑现在写的烂代码,也许不会,也许未来会把这段经历忘掉。人脸识别伴随着我的研究生3年,也许这篇文章写着写着就成了我研究生的总结了。

自己当前状态

2015年,研究生正式入学,我以为我的生活是不停地搞学术,立刻我发现自己对学术没兴趣,我以为我的生活是参与一个精美的项目中进行coding,结果是不断地写demo做演示。我和我本科入学时犯了同样的错误看待问题,不全面,把事物看的太极端了,正如大学不是全是天堂,研究生也不是全是学术和项目。刚入学时,想着卧槽自己怎么也是个高级人才了,得学点高级的知识,什么机器学习,数据挖掘,图像处理都好好涉猎一下,但无奈没有钻研的决心与信心,自己又没啥天赋,到现在也只是知道名词,上网百度百度调用调用开源库。我们导师的人脸识别项目组,我可能是最后一届,整体项目已经过渡到另一个老师下面了。研一时不停地上课,被同学拉着参加两个比赛,顺带出差做演示;研二浑浑噩噩,看论文,出差,写demo,做演示;研三找工作、写论文,写项目;两年半,就这么平平淡淡过来了,并不出彩,但也没混日子。

项目前身

被分到人脸识别组时,人脸识别组已经火过一波了。当时,我们导师从外面搞来了一整套系统,这套系统是一个成熟而完整的产品,有多成熟而完整?这套系统支持sqlserver、 mysql 、oracle等数据库,安装包有近400M,有不错的GUI界面,以windows 服务作为提供后台服务,支持C#和C++调用接口,从1:1人脸相似度比较到1:N的黑名单搜索,从静态图片搜索到视频流实时跟踪都支持,印象中还有一些我看不懂的功能。我所做的项目也是模仿的这套系统。这套系统的缺点也很明显,那就是严格的加密,每次运行时都需要一个加密狗插在电脑上,每两个月需通过专门的人员更新加密狗,它做了什么?如何做的?对于我们几乎是黑盒。当时这套系统就是我们组的命根子,研一研二时的演示我们组就主要就靠它,每次要演示都要担心加密狗是否过期了。

我无法回忆起当时的是什么促成了我想自己动手实现一个人脸识别的项目,也许是厌烦了整天拿着那套系统出去演示还要说是我们自己的技术,也许是当时出现了seetaface这样一个开源的技术,也许是师兄的鼓励,也许是闲的蛋疼自己想找点事做,也许是必然。

在对那整套人脸识别的二次开发中,一次有客户要求能够从远程调用人脸识别接口,我询问师兄 ,师兄说写过。打开师兄封装的代码,封装得也太简单了,不就是WCF吗?本科我也学过。我得做些什么,C#不是可以查看引用dll的所有类吗?好!我把所有的类都自己二次封装一遍,以后远程调用就更加灵活了,然而最终并没有用上。这次封装直加深了我对这套系统的认识,算是对未来自己构建人脸识别系统打下基础。

研一暑假时,导师来了一个行驶证识别项目,手机拍摄的照片,各种角度和光照的都有,导师让我和师兄试试,师兄和我不知道其中的难度,便开始了尝试。那个暑假我在师兄的指导下完成了opencv的配置;尝试opencv各种自带函数:膨胀收缩、放射变换、高斯模糊、霍夫变换、灰度均衡化等;使用tesseract进OCR,最终做出了针对某一张照片的识别,理所当然项目没成功。

研二时,外面的人脸识别技术依旧火热,seetaface出现,听说效果不错,LFW能到97.1%,和现有的这套系统一对比,效果真的不错,好多没有检测到的人脸都检测出来了,如果我能照着写一套支持视频流的演示系统,那就不用受加密狗的约束了,说干就干,花了大概20天一套基于opencv的原始系统被我写出来了。期间人脸组也添加一个师弟,师弟是很厉害,初中学编程,能自己写外挂,ACM拿过奖。我让师弟给我的系统加一个线程并行,他花了半天加了一个线程池任务队列,这个机制我一直到研三才弄懂,但后来师弟到别的项目组了,可能觉得我们这边只有他一个16级的,亦或是在这边没人指导。后来老师又搞了一个人脸识别算法,这次不是一个系统,直接以库的形式给我们,但仍然需要加密狗,因为加密狗的缘故,它不能断点调试,开发很痛苦,不过有了前面seetaface的经验,我很快又完成了一个系统。研二寒假时,导师的合作人有个人脸开闸机的项目,需要做出演示系统来,为这个项目拖到了过年的前一天才回家。同时,我们导师手下的另一个老师L,接手了此前一个与外面合作的动态人脸识别的项目,项目搞得很大,有政府部门和好几家公司,但我们这边写代码的只有三人,我、L老师的一个学生P和公司的一个人X,此时,我带起了我真正意义上的师弟。项目是基于我的系统,几轮测试下来,不太理想,但还是让我们测试了一段时间。期间,我们对代码进行修正,但bug仍不段出现,内存泄漏,程序莫名崩溃,无法断点调试。项目一直持续到17年的7月,最终不了了之。

17年的9月下旬时,此时我已研三了,L老师又有一个项目,要把以前的C/S架构改成B/S架构。我答应下来后端开发的任务,从github上找了一个http服务器魔改一下,前后断断续续花了一个月,完成了任务,后来将项目给了P。时至今日,2018年的元旦,估计P仍在我的基础上改着程序。

人脸识别详情

一、人脸识别SDK

所谓人脸识别接口有三个:

步骤 名称 函数名 输入 输出 说明
1 人脸检测 face detection DetectFace 图像 人脸位置
2 人脸特征抽取 feature extraction Extract 图像,人脸位置 特征数组 将人脸图像变成大小固定的特征数组
3 特征相似度比较feature comparation Compare 特征数组,特征 相似度 比较人脸特征的相似度

有的人脸识别接口会存在第1.5步 :检测人脸关键点,眼睛鼻子嘴之类的,无需在意。

二、人脸识别系统

1基本功能

一个初级人脸识别系统需要的基本功能如下:

编号 名称 函数 输入 输出
1 人员注册 Register 人脸图像,人员信息 ————
2 人员搜索 Search 人脸图像,相似度阈值,最大候选数 可能相似的人员
  • 人员注册
    人员注册会用到如下数据结构,最关键的是face_feature,它由人脸识别SDK的前两步得到:
struct Person{
      int id;
      array<?> face_feature;
      string face_image_path;//(image face_image;)
      string name;
      string gender;//性别
      .
      .
}
  • 人员搜索
    人员搜索会面临输入图像中存在多个人脸的情况,这里首先讨论单个人脸的情况。

首先确定搜索的返回结构Hitrecord,如下:

struct Hitrecord{
      float threshold;//搜索时设定的阈值
      //image search_face_image;//搜索的人脸图片
      string search_face_image_path;//搜索的人脸图片
      //`hit_detials`中的`HitDetial`是按`score`降序排列的
      array<HitDetial> hit_detials;//候选的人员结果
      time occur_time;//发生时间
}
struct HitrecordDetail{
      float score;//相似度
      int person_id;
}

在搜索之前所有的注册人员信息都载入内存了,整个搜索过程如下:

  • 利用SDK的前两步得到搜索的人脸特征feature
  • feature与每个已经注册的人员的face_feature依次进行Compare得到相似度,剔除掉小于相似度阈值的结果,保留最大的几个结果。

对于输入中存在多个人脸的情况,只需分开处理即可

2数据库模块-DataAgine 工程

以上的基本功能并没有考虑数据存储的问题,当然我写过没有数据库的版本,直接将注册的人员库序列化到文件中,而不保留命中的记录,这样做很省事,但只能演示,客户肯定是要查看最终的命中纪录的,也就是以Hitrecord为基础的扩展数据。所以必须考虑存储的问题。
最终我用了mysql数据库,原因很简单,mysql很容易安装,下载解压,输几个命令就行了。

根据基本功能根据基本功能我写了如下几个表:

CREATE DATABASE `frsdb`;
USE `frsdb`;

create table `frsdb`.`person`
(
    `id` int(11) AUTO_INCREMENT,
    `name` nvarchar(50) NULL,
    `gender` char(1) NULL,
    `face_image_path` varchar(200) NOT NULL,
    `feature_data` LongBlob NOT NULL,
    PRIMARY KEY (`id`),
    UNIQUE KEY `id` (`id`)
)ENGINE=InnoDB DEFAULT CHARSET=utf8;

create table `frsdb`.`hitrecord`
(
    `id` int(11) AUTO_INCREMENT,
    `search_face_image_path` varchar(200) NOT NULL,
    `threshold` float NOT NULL,
    `occur_time` datetime NOT NULL,
    PRIMARY KEY (`id`),
    UNIQUE KEY `id` (`id`)
)ENGINE=InnoDB DEFAULT CHARSET=utf8;

create table `frsdb`.`hitrecord_detail`
(
    `id` int(11) AUTO_INCREMENT,
    `hitrecord_id` int(11) NOT NULL,
    `person_id` int(11) NOT NULL, 
    `rank` int(11) NOT NULL,
    `score` float NOT NULL,
    PRIMARY KEY (`id`),
    UNIQUE KEY `id` (`id`)
)ENGINE=InnoDB DEFAULT CHARSET=utf8;

create view `frsdb`.`hitalert` as 
select 
    hit.id,
    hit.search_face_image_path,
    hit.threshold,
    hit.occur_time,
    detail.id as detail_id,
    detail.rank,
    detail.score,
    person.id as person_id,
    person.name as person_name,
    person.gender as person_gender,
    person.face_image_path as person_face_image_path,
    FROM 
    (`frsdb`.`hitrecord_detail` as detail
    left join `frsdb`.`hitrecord` as hit on detail.hitrecord_id=hit.id) 
    left join `frsdb`.`person` as person on detail.person_id = person.id;

为了最后的方便查看命中记录,我创建了视图hitalert

我用了动软的代码生成器,自动生成了Model,DLL,BLL,所谓的三层架构。动软Model,DLL,BLL三层分布在不同的项目中,我嫌项目太多,直接将他们合为一个DataAgine 项目,只是Model,DLL,BLL放在了三个不同的文件夹中。

3基本实现

完备的人脸识别演示系统,无非就是不断从视频中抽画面帧,进行人员搜索。

现在我手里有一个对外提供C语言接口的windows人脸识别SDK,图片是以char数组的形式传入的,它可以检测人脸 提取人脸的特征(固定大小的数组),比较特征间的相似度。我要实现什么?注册、搜索、与视频流进行对接,实现实时监控。

我的内心活动:图片什么的,用opencv就好了,貌似opencv也支持视频流的读取,等等,得有界面啊,opencv貌似只能用C++啊,C++ windows GUI开发下有qt和MFC,卧槽都不熟,我只会C# GUI,拖拽控件就行了。 C#和C++怎么交互啊。经过一番寻觅,最终我确定了用托管C++来实现,也就是“CLI/C++”,它可以同时使用C++和C#的库,C#也可以直接引用托管C++写的库,perfect! 就是它了,主体实现就用它了。

话外音:其实主体用托管C++,不一定是最好的选择,C#一样可以处理图片和视频流的库,托管C++语法需要慢慢熟悉,有坑踩。托管C++(C#)的数组类型,与C++的数组类型转换就很烦,stackoverflow解决了。托管C++(C#)的Image类型到 opencv 的Mat类型也烦,github 找了个opencvsharp解决了。

基本功能中的Register和Search 就放一个类中,就叫PersonDataset,人脸识别的接口也得托管C++用封装一下,以防C#调用(事实上并没有发生),还得有一个负责视处理频流的类,就参考opencv叫Capture,每个Capture就对应一个PersonDataset对象。

class PersonDataset{
    array<Person> persons;//存放注册的用户
    int LoadAllPerson();//载入人员库
    int Register(Image im, String name,String gender..)//注册
    //搜索,接受图片,相似度阈值,每张人脸返回的最大候选数目,存在数据库操作
    array<Hitrecord> Search(Image im,float thresh,int maxCandinateNum);
}
Class Capture{
    PersonDataset *personDataset;
    Callback OnHit(array<Hitrecord> hits);//每次命中时的回掉函数,C#中用事件
    Callback OnGrab(Image im);//每次获取一帧画面时的回掉函数,用于展示,让一个控件显示im
    //循环搜索线程
    Thread searchThread{
           while(runing){
              获取一帧画面frame;
              hits=personDataset->Search(frame,..);
              OnHit(hits);//调用回调
           }
    }
    //循环显示线程
    Thread showThread{
           while(runing){
             获取一帧画面frame;
             Sleep(30);//休眠30ms
             OnGrab(frame);
           }
    }

    int Start(String videoAddress){
        开启视频流;
        showThread.start();
        searchThread.start();
    }
}

由于使用了多线程Capture,在读取画面时,互斥必不可少。Capture需要与一个PersonDataset相关联。Capture 靠回调函数返回结果。开始时,Start开启了两个线程一个用来显示画面,一个用来返回命中结果。

以上都是一个后台逻辑的实现,采用的都是托管C++,还需要一个界面。界面用的是winform,C#直接引用DataAngine的所编译的库,其他就是接收视频地址的TextBox,开始停止,查询用的Button,显示查询结果的DatagridView,显示监控画面的PictureBox,做的很是丑陋。

4系统改进

opencv 处理的视频流用的是VideoCapture类有bug,会花屏,每次开启关闭视频流会内存泄漏,后来改用了vlc,在github上搜vlc opencv windows 找到个例子直接用了 。

为了提高精度,L老师想出了利用海康摄像头自带人脸跟踪的功能,一个人从出现到消失在镜头中海康摄像头会记录这个人的几张照片。提高精度就靠这几张照片,假设有5张,首先对这5张照片都搜索,返回:

{{a1,b1,c1,d1,e1},{a2,b2,c2,d2,e2},{a3,b3,c3,d3,e3},{a4,b4,c4,d4,e4},{a5,b5,c5,d5,e5}}

a b c d e 的分数为降序。

  • 返回这五张照片中分数最高的那个
  • 返回ab 相差最大的那个a,这个貌似结果好一点
  • 返回a 中多数的那个

三、从C/S到B/S

从C/S到B/S,客户需要自己添加设备信息(device),人员库类别也有多种(person 表添加一个type字段就可以),还需要一个表将设备与人员库象关联(surveillance_task

create table `frsdb`.`device`
(
    `id` int(11) AUTO_INCREMENT,    
    `name` nvarchar(50) NOT NULL,
    `video_address` nvarchar(200) NOT NULL
    PRIMARY KEY (`id`),
    UNIQUE KEY `id` (`id`)
)ENGINE=InnoDB DEFAULT CHARSET=utf8;

create table `frsdb`.`surveillance_task`
(
    `id` int(11) AUTO_INCREMENT,
    `name` nvarchar(50) NOT NULL,
    `person_type` int(11) NOT NULL,
    `device_id` int(11) NOT NULL,
    PRIMARY KEY (`id`),
    UNIQUE KEY `id` (`id`)
)ENGINE=InnoDB DEFAULT CHARSET=utf8;

每次开始需要指定一个surveillance_taskid来载入指定的人员,监控指定的设备。

不得不说程序员都是乐观的,当时我从来没有做过这样的项目,没写过网站项目。完全不知道怎么返回数据,思考了半天,我觉得和http服务器有点像,客户端(浏览器)请求一个地址,我给它返回一个数据,于是在gitihub找了个C#的http服务器,直接魔改了。当然其中过程有些曲折。原来的服务器只能返回静态文件,后来改成了可以根据不同的地址和参数接受返回不同的(json)数据,期间我接触了restful 风格的设计,于是就有了以下的设计:

获得id1的设备
GET http://localhost:8080/v1/device/1/update 

添加设备
GET http://localhost:8080/v1/device/

更新id1的设备
POST http://localhost:8080/v1/device/1/update 

删除id1的设备
POST http://localhost:8080/v1/device/1/delete 

至此传统的 服务–客户端 已经走通,还有个难题,那就是如何实时更新命中的数据(hitrecord)到客户端?我查了一下可以用websocket,没错又是在github上找了一个C#的websocket 库– fleck,但我不是直接用,而是将其最主要的通信功能抠出来,fleck原来是通过回掉函数进行通信,一旦有连接到达就发送数据,通信完毕就直接关闭websocket了。现在我需要改成:只要客户端不关闭,我就一直向客户端发送数据的逻辑,同时如果有别的客户端连接了,我就主动关闭。没错这只是一个伪B/S,因为实时更新的websocket只能有一个客户端。

FaceAngine的改动并不大,只需要在OnHit中使用websocket向客户端发送数据就可以,OnGrab没有作用了。

总结

自学。

猜你喜欢

转载自blog.csdn.net/guiqulaxi920/article/details/78947269