OpenCV人脸识别

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 的三维地图,等研究出东西再和大家分享吧.

到这里就差不多结束了,希望能帮到你们,多多支持哦!!!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容