Android 使用 OpenCV JavaCV 实现人脸检测和识别

在安卓手机上实现人脸检测和人脸识别功能。

# 安装

花了一整天才发现的正确安装姿势:

开发环境:macOS 10.12 + Android Studio 2.3.1

compile 'org.bytedeco:javacv:1.3.1'
compile 'org.bytedeco.javacpp-presets:opencv:3.1.0-1.3:android-arm'
compile 'org.bytedeco.javacpp-presets:opencv:3.1.0-1.3:android-x86'
compile 'org.bytedeco.javacpp-presets:ffmpeg:3.2.1-1.3:android-arm'
compile 'org.bytedeco.javacpp-presets:ffmpeg:3.2.1-1.3:android-x86'

// 为了能够直接在电脑上进行单元测试
testCompile 'org.bytedeco.javacpp-presets:opencv:3.1.0-1.3:macosx-x86_64'
testCompile 'org.bytedeco.javacpp-presets:ffmpeg:3.2.1-1.3:macosx-x86_64'

其他的各种抄来抄去的文章、文档(包括官方文档)、什么手动拷包添加依赖啊、解压出 so 文件的啊都是浮云🙄️

# 人脸检测

private void faceDetect(String imageFilePath, String cascadeFilePath) {
    // 图片预处理
    Mat image = imread(imageFilePath);
    Mat grayImage = new Mat();
    cvtColor(image, grayImage, COLOR_BGR2GRAY); // 变成灰度图像
    equalizeHist(grayImage, grayImage); // 直方图均衡,增加对比度以提高识别率

    // 使用默认配置检测
    CascadeClassifier faceDetector = new CascadeClassifier(cascadeFilePath);
    RectVector faceDetections = new RectVector();
    faceDetector.detectMultiScale(grayImage, faceDetections);

    System.out.println(String.format("Detected %s faces", faceDetections.size()));
    File file = new File(xmlPaths[0]);
    String dirPath = file.getParentFile().getAbsolutePath();

    // 给每张脸画框,并保存脸文件
    for (int i = 0; i < faceDetections.size(); i++) {
        Rect rect = faceDetections.get(i);
        saveFace(image, rect, dirPath + "/face-" + i + "-" + file.getName());
        opencv_core.Point point = new opencv_core.Point(rect.x(), rect.y());
        opencv_core.Point point1 = new opencv_core.Point(
          (rect.x() + rect.width()), rect.y() + rect.height());
        rectangle(image, point, point1, new opencv_core.Scalar(0, 255, 0, 0));
    }

    String newFile = dirPath + "/faces-" + file.getName();
    System.out.println(String.format("Writing %s", newFile));
    imwrite(newFile, image);
}

private void saveFace(Mat image, Rect rect, String filePath) {
    Mat sub = image.rowRange(rect.y(), rect.y() + rect.height())
      .colRange(rect.x(), rect.x() + rect.width());
    Mat mat = new Mat();
    opencv_core.Size size = new opencv_core.Size(100, 100);
    resize(sub, mat, size);
    return imwrite(filePath, mat);
}

# 人脸识别

/**
 * 训练人脸识别器
 * @param dirPath 待训练的文件夹,文件命名按 数字分组-文件名 规则。
 *                比如:1-alan.jpg 1-alan2.jpg 2-tom.jpg
 * @param recognizerFilePath 备份训练结果,避免每次都重新训练
 * @return 人脸识别器
 */
private FaceRecognizer trainRecognizer(String dirPath, String recognizerFilePath) {
    // 获取所有训练文件
    File root = new File(dirPath);
    FilenameFilter imgFilter = new FilenameFilter() {
        public boolean accept(File dir, String name) {
            name = name.toLowerCase();
            return name.endsWith(".jpg") || 
              name.endsWith(".pgm") || name.endsWith(".png");
        }
    };
    File[] imageFiles = root.listFiles(imgFilter);

    MatVector images = new MatVector(imageFiles.length);

    Mat labels = new Mat(imageFiles.length, 1, CV_32SC1);
    IntBuffer labelsBuf = labels.createBuffer();

    int counter = 0;

    for (File image : imageFiles) {
        Mat img = imread(image.getAbsolutePath(), CV_LOAD_IMAGE_GRAYSCALE);
        int label = Integer.parseInt(image.getName().split("-")[0]);
        images.put(counter, img);
        labelsBuf.put(counter, label);
        counter++;
    }

    // createFisherFaceRecognizer
    // createEigenFaceRecognizer
    // createLBPHFaceRecognizer 不要求图片尺寸一样
    FaceRecognizer faceRecognizer = createEigenFaceRecognizer();
    faceRecognizer.train(images, labels);
    faceRecognizer.save(recognizerFilePath);

    // faceRecognizer.load(recognizerFilePath);
    // faceRecognizer.update(images, labels);

    return faceRecognizer;
}

/**
 * 开始识别
 * @param faceRecognizer 人脸识别器
 * @param filePath 待识别文件
 */
void faceRecognize(FaceRecognizer faceRecognizer, String filePath) {
    Mat testImage = imread(filePath, 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);
}

# 其他问题

# 加载图片或文件、文件夹的时候可能报错
OpenCV Error: Assertion failed (scn == 3 || scn == 4) in cvtColor

文件路径不对,试试用绝对路径。

# 特征分类器文件 cascade.xml 加载失败

两种方式能完成加载

  1. new CascadeClassifier(xmlPath);
  2. 使用 2.4.9 版本的文件 OpenCV 自带的特征分类器文件下载
# 安装完成之后构建失败
java.lang.UnsatisfiedLinkError: 又臭又长 couldn't find "libjniopencv_core.so"

虽然不知道为什么,但按照👆的正确姿势重新安装就解决了。

# cvLoadImage(filename) 永远为空

给存储权限!!!给存储权限!!!给存储权限!!!

Android 6.0 以后有些权限是要在代码里请求的,单单在配置文件里写是不够的!!!