2017-8-13
前言
实习了一个月,搞了一个月的人脸识别,终于研究出结果,就和大家分享一下,虽然感觉不是真正意义上的人脸识别,但还是有很高识别度的,代码我就只贴出了比较重要的代码和逻辑,源码已经在Github上了。完整的项目分为客户端和服务器端,图片的对比和存储以及一些注册信息就存在服务器端,不让客户端处理,但是客户端还是存在人脸对比的代码的(Compare类)。比较基础一点的搭建opencv for android 和 opencv for java 的环境就不说了,说一点干货???(黑人问号.jpg) 讲一讲遇到的问题,分析一下流程和原理。
闲话不多说,我们开始吧。
https://github.com/Hyyzt/FaceRecognition
客户端
1.逻辑
ControlActivity:
用来控制整个程序的流程,进行注册和登录
FaceLoginActivity:
进行人脸注册,根据服务器返回的数据判断是否可以进形注册
InfoActivity:
根据服务器返回的数据进行注册信息的数据,并将数据提交给服务器保存
FaceReconginzedAcitvity:
进行人脸识别,上传数据至服务器,返回人脸识别是否成功的信息
SuccessActivity:
人脸识别成功后从服务器返回注册时的信息并展示
2.重要代码
- 初始化opencv类库
若我们需要使用opencv类库,则必须进行初始化,尽量在Application中的oncreate()中初始化,每次启动的时候只加载一次类库。
System.loadLibrary("opencv_java");
- 剔除opencv manager的关联
使用人脸识别类库的时候,官方规定你必须安装opencv manager才可以使用这些类库,但我们可以通过一些操作来剔除依赖。
首先将opencv目录下的sdk/native/libs下的文件全部拷贝出来,并在自己的程序目录下建立一个与main同级并命名为jniLibs的文件夹,将之前的拷贝文件全被拷贝进去,并在onCreate中加入以下代码
System.loadLibrary("detection_based_tracker");
try {
InputStream is = getResources().openRawResource(R.raw.lbpcascade_frontalface);
File cascadeDir = getDir("cascade", Context.MODE_PRIVATE);
mCascadeFile = new File(cascadeDir, "lbpcascade_frontalface.xml");
FileOutputStream os = new FileOutputStream(mCascadeFile);
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
os.write(buffer, 0, bytesRead);
}
is.close();
os.close();
mJavaDetector = new CascadeClassifier(mCascadeFile.getAbsolutePath());
if (mJavaDetector.empty()) {
Log.e(TAG, "Failed to ControlActivity cascade classifier");
mJavaDetector = null;
} else
Log.i(TAG, "Loaded cascade classifier from " + mCascadeFile.getAbsolutePath());
mNativeDetector = new DetectionBasedTracker(mCascadeFile.getAbsolutePath(), 0);
cascadeDir.delete();
} catch (IOException e) {
e.printStackTrace();
}
mOpenCvCameraView.enableView();
完成之后你就可以在没有opencv manager的情况下进行使用了
- 开始人脸识别
首先,你需要在布局中加入一个opencv自己定义的控件,这个控件就是我们进行人脸检测和识别的控件,这个控件是一个视频流控件,它初始化是后置摄像头,你需要将它前置,但是前置过后每一帧会出现镜像的结果,我们在回调中处理这个问题。
你需要在activity中引入一个接口CvCameraViewListener2,并实现它的方法
<org.opencv.android.JavaCameraView
android:id="@+id/fd_activity_surface_view"
android:layout_width="fill_parent"
android:layout_height="fill_parent" />
//前置摄像头
mOpenCvCameraView.setCameraIndex(CameraBridgeViewBase.CAMERA_ID_FRONT);
实现接口
//视频流开始
//mGray和mRgba分别是每一帧图像的灰度化图像和彩色图像
public void onCameraViewStarted(int width, int height) {
Log.e("TAG", "onCameraViewStarted");
mGray = new Mat();
mRgba = new Mat();
}
//视频流结束
public void onCameraViewStopped() {
Log.e("TAG", "onCameraViewStopped");
mGray.release();
mRgba.release();
}
//使用视频流时每一帧的回调
public Mat onCameraFrame(CvCameraViewFrame inputFrame) {
mRgba = inputFrame.rgba();
mGray = inputFrame.gray();
//处理前置后镜像的摄像头
//倒转镜像的摄像头
Core.flip(mRgba, mRgba, 1);
Core.flip(mGray, mGray, 1);
//将视频流控制住,只在一定区域内可以检测人脸
Point point = new Point(mGray.width() / 2 - 375, mGray.height() / 2 - 375);
Rect rect = new Rect(point, new Size(750, 750));
mGray = new Mat(mGray, rect);
if (mAbsoluteFaceSize == 0) {
int height = mGray.rows();
if (Math.round(height * mRelativeFaceSize) > 0) {
mAbsoluteFaceSize = Math.round(height * mRelativeFaceSize);
}
mNativeDetector.setMinFaceSize(mAbsoluteFaceSize);
}
MatOfRect faces = new MatOfRect();
if (mDetectorType == JAVA_DETECTOR) {
if (mJavaDetector != null)
mJavaDetector.detectMultiScale(mGray, faces, 1.1, 2, 2, // TODO: objdetect.CV_HAAR_SCALE_IMAGE
new Size(mAbsoluteFaceSize, mAbsoluteFaceSize), new Size());
} else if (mDetectorType == NATIVE_DETECTOR) {
if (mNativeDetector != null)
mNativeDetector.detect(mGray, faces);
} else {
Log.e(TAG, "Detection method is not selected!");
}
//这个facesArray数组是每一帧我们提取到的人脸个数,我们需要将它筛选,剔除掉错误的识别情况
Rect[] facesArray = faces.toArray();
if (facesArray.length > 0) {
for (int i = 0; i < facesArray.length; i++){
Point point1 = new Point(facesArray[i].x + point.x, facesArray[i].y + point.y);
facesArray[i] = new Rect(point1, facesArray[i].size());
//遍历数组时,此处剔除,并将正确的脸部在屏幕上显示出来,且返回这个脸部头像bitmap
if(facesArray[i].width > 350) {
Core.rectangle(mRgba, facesArray[i].tl(), facesArray[i].br(), FACE_RECT_COLOR, 3);
//根据矩阵和脸部大小裁剪成图片
bitmap = FaceUtils.cutDownFaceROI(mRgba, facesArray[i]);
}
}
}
return mRgba;
}
在请求服务器时,我们尽量不要在onCameraFrame中请求,在开始识别后,延时消息发送bitmap至服务器,也不要在onCameraFrame中进行复杂的逻辑判断,否则会出现视频流卡死的情况。
在销毁和恢复activity时,要对JavaCameraView进行销毁和恢复
服务器端
1.开发环境
编译工具:Eclipse
服务器:Tomcat
数据库:MySQL
2.相关类库
OpenCV相关Jar包:opencv-2411.jar, opencv-windows-x86_64.jar等
JavaCV相关Jar包:javacv.jar, javacpp.jar等
其他Jar包:gson-2.3.1.jar
3.实现逻辑
客户端请求时,根据不同请求执行不同逻辑并返回结果:
1.客户端注册时对发送的人脸数据进行临时存储,并对比数据库匹配,若匹配结果达到设定阈值(75%),则返回已注册过;反之,则返回未注册过。
2.客户端成功注册后对其发送的用户基本信息和人脸数据进行处理和入库存储,其中人脸数据以图片格式(.png)存储本地。
3.客户端人脸验证登陆时发送的人脸数据进行临时存储,并对比数据库匹配,若匹配结果达到设定阈值(80%)则取出库中对应的用户数据和人脸图片url返回;反之,则返回登陆失败。
4.数据库设计
数据库face-detect-database,表user-info,
主要字段id: int类型, 主键;
name: text类型, 用户名;
age: int类型, 年龄,
sex: int类型, 性别,
birthday: text类型, 出生日期;
face_path: text类型, 人脸图片本地存储路径。
5.接口设计
1.GetData: 注册时判断用户是否注册过
public class GetData extends HttpServlet{
...
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 存储临时图片
ServletInputStream is = request.getInputStream();
String tempPath = ImageUtils.saveImageToLocal(is, "temp");
// 遍历数据库,比较相似度
MySQLDatabaseHelper helper = new MySQLDatabaseHelper();
List<User> userList = helper.query();
helper.close();
PrintWriter writer = response.getWriter();
// FaceRecognizer匹配
User user = MyFaceRecognizer.matchByTrainAndPredict(userList, tempPath);
if (user != null) {
writer.write("Login");
} else {
writer.write("NoLogin");
}
// 灰度匹配
// double similarity = 0;
// for (int i = 0; i < userList.size(); i++) {
// similarity = FaceMatchUtils.
// histogramMatch(userList.get(i).getFace_pic(), tempPath);
// if (similarity > 0.75) {
// writer.write("Login");
// return;
// }
// }
// writer.write("NoLogin");
}
...
}
2.GetJson: 处理和存储用户基本信息和人脸图片
public class GetJson extends HttpServlet {
...
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 获取图片流和Json
ServletInputStream is = request.getInputStream(); // 图片流
// String jsonData = request.getParameter("Info");
// 转码
String str = request.getParameter("Info");
String jsonData = new String(str.getBytes("ISO-8859-1"), "utf-8");
System.out.println(jsonData);
Gson gson = new Gson();
UserInfo userInfo = gson.fromJson(jsonData, UserInfo.class);
// 获取数据后返回Success
response.setContentType("text/html");
PrintWriter writer = response.getWriter();
writer.write("Success");
// 存储图片和更新数据库
MySQLDatabaseHelper helper = new MySQLDatabaseHelper();
int userNum = helper.query().size();
String facePath = ImageUtils.saveImageToLocal(is, "face" + (userNum));
User user = new User();
user.setName(userInfo.name);
user.setAge(Integer.parseInt(userInfo.age));
user.setSex(userInfo.sex);
user.setBirthday(userInfo.birthday);
user.setFace_pic(facePath);
System.out.println(user.toString());
helper.insert(user);
helper.close();
}
...
}
3.VerifyLogin: 人脸验证登陆
public class VerifyLogin extends HttpServlet {
...
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 接收图片并存储
ServletInputStream is = request.getInputStream(); // 图片流
String tempPath = ImageUtils.saveImageToLocal(is, "verify");
// 遍历数据库,验证登陆
MySQLDatabaseHelper helper = new MySQLDatabaseHelper();
List<User> userList = helper.query();
helper.close();
PrintWriter writer = response.getWriter();
// FaceRecognizer匹配
User user = MyFaceRecognizer.matchByTrainAndPredict(userList, tempPath);
if(user != null) {
Gson gson = new Gson();
String json = gson.toJson(new UserInfo(user.getName(),
user.getSex(), user.getAge(), user.getBirthday(),
url + user.getFace_pic().
substring(user.getFace_pic().lastIndexOf("/") + 1)));
System.out.println(json);
response.setContentType("text/html");
writer.write(json);
} else {
writer.write("Fail");
}
}
...
}
6.关键代码
1.基于图像灰度直方图比较的人脸匹配算法
public class FaceMatchUtils {
// 利用灰度直方图计算图像相似度,输要求入人脸图像的均为正方形
public static double histogramMatch(String face, String testFace) {
Mat faceMat = Highgui.imread(face);
Mat testFaceMat = Highgui.imread(testFace);
// 图像灰度化
System.out.println("histogramMatch: 图像灰度化");
Imgproc.cvtColor(faceMat, faceMat, Imgproc.COLOR_RGB2GRAY);
Imgproc.cvtColor(testFaceMat, testFaceMat, Imgproc.COLOR_RGB2GRAY);
// 直方图均衡化,暂时注释
// System.out.println("histogramMatch: 直方图均衡化");
// Imgproc.equalizeHist(faceMat, faceMat);
// Imgproc.equalizeHist(testFaceMat, testFaceMat);
// 把Mat矩阵的type转换为Cv_32F,因为在c++代码中会判断他的类型
faceMat.convertTo(faceMat, CvType.CV_32F);
testFaceMat.convertTo(testFaceMat, CvType.CV_32F);
// 直方图匹配
System.out.println("histogramMatch: 直方图匹配");
double similarity = Imgproc.compareHist(faceMat, testFaceMat, Imgproc.CV_COMP_CORREL);
System.out.println("灰度直方图相似性结果: " + face + " : "+ similarity);
return similarity;
}
}
2.基于FaceRecognizer人脸训练和预测的人脸匹配算法
public static User matchByTrainAndPredict(List<User> userList, String path) {
List<String> pathList = new ArrayList<String>();
for (User user : userList) {
pathList.add(user.getFace_pic());
}
MatVector images = new MatVector(pathList.size());
Mat labels = new Mat(pathList.size(), 1, CV_32SC1);
IntBuffer labelsBuf = labels.createBuffer();
for (int i = 0; i < pathList.size(); i++) {
String p = pathList.get(i);
Mat img = imread(p, CV_LOAD_IMAGE_GRAYSCALE);
images.put(i, img);
labelsBuf.put(i, i);
}
// FaceRecognizer faceRecognizer = createFisherFaceRecognizer();
// FaceRecognizer faceRecognizer = createEigenFaceRecognizer();
FaceRecognizer faceRecognizer = createLBPHFaceRecognizer();
faceRecognizer.train(images, labels);
Mat testImage = imread(path, CV_LOAD_IMAGE_GRAYSCALE);
IntPointer label = new IntPointer(1);
DoublePointer confidence = new DoublePointer(1);
faceRecognizer.predict(testImage, label, confidence);
int predictedLabel = label.get(0);
System.out.println("Predicted label: " + predictedLabel);
System.out.println("Confidence: " + confidence.get(0));
if (confidence.get(0) > 10000) {
System.out.println(userList.get(predictedLabel).toString());
return userList.get(predictedLabel);
} else {
System.out.println("没有匹配");
return null;
}
}
问题
- 在进行图像相似度对比时,要注意对比的图像大小要一致,否则会出现传入非法参数的异常
- 由于对比方法使用的是灰度直方图在归一化之后对比,使得图像对环境的光照强烈十分的敏感,而且对于拍摄条件有一点的限制,注册时的背景和识别时的背景不能相差过大,拍摄距离要控制好,不能太远也不能太近,要保证拍摄的质量,而且拍摄后的照片在经过压缩处理后,会损失一部分的精度,使相似度下降了10个百分点。在上述条件都确保的情况下,对比的结果还是非常精确的,高达80%左右
- 由于opencv类库本身是一个图像处理的库,而不能提取人脸特征,只能通过对比识别人脸后的人脸图像来查看差异,而灰度直方图是在各种方法中最准确的方法
- Opencv高版本提供了一个FaceRecognizer类,对人脸进行特征对比和匹配,但是它没有对应的JAVA API,而JavaCV对这个类进行了封装,提供了对应接口,却没有给出人脸特征点数据,而是通过对比直接返回了匹配结果,且由于源码没开放,无法控制相似度阈值
结束语
下一阶段开始研究Arcgis for android 的三维地图,等研究出东西再和大家分享吧.
到这里就差不多结束了,希望能帮到你们,多多支持哦!!!