Android OpenCV实现人脸检测(一)完成人脸检测功能

环境搭建:

Android Studio 集成OpenCV

本节完整的代码链接:

Android OpenCV Demo 预览黑屏
(下一节会实现预览的功能)
Android OpenCV实现人脸检测(一)完成人脸检测功能_第1张图片

1.创建 assets 文件夹

Android OpenCV实现人脸检测(一)完成人脸检测功能_第2张图片
Android OpenCV实现人脸检测(一)完成人脸检测功能_第3张图片

2.在 “OpenCV-android-sdk\sdk\etc\lbpcascades” 这个目录下找到 lbpcascade_frontalface.xml 这个文件,并将这个文件复制到Android Studio的assets文件夹

Android OpenCV实现人脸检测(一)完成人脸检测功能_第4张图片
Android OpenCV实现人脸检测(一)完成人脸检测功能_第5张图片

3.实现Camera预览功能,预览画面为黑色

import android.Manifest;
import android.app.Activity;
import android.content.pm.PackageManager;
import android.hardware.Camera;
import android.os.Build;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

public class MainActivity extends Activity implements SurfaceHolder.Callback, Camera
        .PreviewCallback {

    static {
        System.loadLibrary("native-lib");
    }

    private CameraHelper cameraHelper;
    int cameraId = Camera.CameraInfo.CAMERA_FACING_FRONT;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        SurfaceView surfaceView = findViewById(R.id.surfaceView);
        checkPermission();
        surfaceView.getHolder().addCallback(this);
        cameraHelper = new CameraHelper(cameraId);
        cameraHelper.setPreviewCallback(this);
    }
    public boolean checkPermission() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && checkSelfPermission(
                Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
            requestPermissions(new String[]{
                    Manifest.permission.READ_EXTERNAL_STORAGE,
                    Manifest.permission.WRITE_EXTERNAL_STORAGE,
                    Manifest.permission.CAMERA
            }, 1);
        }
        return false;
    }
    @Override
    protected void onResume() {
        super.onResume();
        cameraHelper.startPreview();
    }

    @Override
    protected void onStop() {
        super.onStop();
        cameraHelper.stopPreview();
    }


    @Override
    public void surfaceCreated(SurfaceHolder holder) {

    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        //设置surface 用于显示
        setSurface(holder.getSurface());
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {

    }

    @Override
    public void onPreviewFrame(byte[] data, Camera camera) {
        //传输数据
        postData(data, CameraHelper.WIDTH, CameraHelper.HEIGHT, cameraId);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_UP) {
            cameraHelper.switchCamera();
            cameraId = cameraHelper.getCameraId();
        }
        return super.onTouchEvent(event);
    }
}

CameraHelper类的实现如下:

import android.graphics.ImageFormat;
import android.graphics.SurfaceTexture;
import android.hardware.Camera;
import android.opengl.GLSurfaceView;
import android.util.Log;
import android.view.SurfaceHolder;

public class CameraHelper implements Camera.PreviewCallback {

    private static final String TAG = "hugang";
    public static final int WIDTH = 640;
    public static final int HEIGHT = 480;
    private int mCameraId;
    private Camera mCamera;
    private byte[] buffer;
    private Camera.PreviewCallback mPreviewCallback;
    private Camera.Size size;
    public CameraHelper(int cameraId) {
        mCameraId = cameraId;
    }

    public void switchCamera() {
        if (mCameraId == Camera.CameraInfo.CAMERA_FACING_BACK) {
            mCameraId = Camera.CameraInfo.CAMERA_FACING_FRONT;
        } else {
            mCameraId = Camera.CameraInfo.CAMERA_FACING_BACK;
        }
        stopPreview();
        startPreview();
    }

    public int getCameraId() {
        return mCameraId;
    }

    public void stopPreview() {
        if (mCamera != null) {
            //预览数据回调接口
            mCamera.setPreviewCallback(null);
            //停止预览
            mCamera.stopPreview();
            //释放摄像头
            mCamera.release();
            mCamera = null;
        }
    }

    public void startPreview() {
        try {
            //获得camera对象
            mCamera = Camera.open(mCameraId);
            //配置camera的属性
            Camera.Parameters parameters = mCamera.getParameters();
            //设置预览数据格式为nv21
            parameters.setPreviewFormat(ImageFormat.NV21);
            //这是摄像头宽、高
            parameters.setPreviewSize(WIDTH, HEIGHT);
            // 设置摄像头 图像传感器的角度、方向
            mCamera.setParameters(parameters);
            size = parameters.getPreviewSize();
            Log.i(TAG, "----------startPreview  width : " + size.width + "    height: " + size.height);
            buffer = new byte[WIDTH * HEIGHT * 3 / 2];
            //数据缓存区
            mCamera.addCallbackBuffer(buffer);
            mCamera.setPreviewCallbackWithBuffer(this);
            //设置预览画面
            SurfaceTexture surfaceTexture = new SurfaceTexture(11);
            mCamera.setPreviewTexture(surfaceTexture);
            mCamera.startPreview();
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }


    public void setPreviewCallback(Camera.PreviewCallback previewCallback) {
        mPreviewCallback = previewCallback;
    }


    @Override
    public void onPreviewFrame(byte[] data, Camera camera) {
        // data数据依然是倒的
        mPreviewCallback.onPreviewFrame(data, camera);
        camera.addCallbackBuffer(buffer);
    }
}

AndroidManifest.xml增加权限

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

4.将assets目录下的 lbpcascade_frontalface.xml 文件复制到手机的 /data/data/包名/cache/ 目录下面

Utils.copyAssets(this, "lbpcascade_frontalface.xml");

上面这行代码我放在了 MainActivity.onCreate 的最后一行
千万注意:一定不要把 lbpcascade_frontalface.xml 这个文件复制到 /data/data/包名/files/ 这个目录下,OpenCV读取这个目录会有问题,后面程序执行完 tracker->getObjects(faces) 后,你得到的faces永远都是empty,以至于你无法得到识别出的人脸区域。

Utils类的实现如下

import android.content.Context;
import android.os.Environment;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;

public class Utils {

    public static void copyAssets(Context context, String path) {
        File model = new File(path);
        File file = new File("/data/data/com.example.myopencv/cache/", model.getName());
        if (file.exists()) {
            return;
        }
        try {
            FileOutputStream fos = new FileOutputStream(file);
            InputStream is = context.getAssets().open(path);
            int len;
            byte[] b = new byte[2048];
            while ((len = is.read(b)) != -1) {
                fos.write(b, 0, len);
            }
            fos.close();
            is.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

5.初始化OpenCV

    native void init(String model);
#include 
#include 
using namespace cv;

DetectionBasedTracker *tracker = 0;


// 适配器
class CascadeDetectorAdapter : public DetectionBasedTracker::IDetector{

public:
    CascadeDetectorAdapter(cv::Ptr<cv::CascadeClassifier> detector): IDetector(), Detector(detector){

    }

    void detect(const cv::Mat &image, std::vector<cv::Rect> &object){
        Detector->detectMultiScale(image, object, scaleFactor, minNeighbours, 0, minObjSize, maxObjSize);
    }

private:
    CascadeDetectorAdapter();
    cv::Ptr<cv::CascadeClassifier> Detector;
};

extern "C"
JNIEXPORT void JNICALL
Java_com_example_myopencv_MainActivity_init(JNIEnv *env, jobject thiz, jstring model_) {
    pthread_mutex_init(&mutex, 0);
    if (tracker) {
        tracker->stop();
        delete tracker;
        tracker = 0;
    }
    const char *model = env->GetStringUTFChars(model_, 0);
    LOGE("============ model: %s ============", model);
//    new 一个分类器
//    CascadeClassifier *cascadeClassifier= new CascadeClassifier();

    // 创建级联分类器
    Ptr<CascadeClassifier> classifier = makePtr<CascadeClassifier>(model);
    // 创建检测器,上面创建的级联分类器用于初始化检测器
    Ptr<CascadeDetectorAdapter> mainDetector = makePtr<CascadeDetectorAdapter>(classifier);

    // 创建级联分类器
    Ptr<CascadeClassifier> classifier_1 = makePtr<CascadeClassifier>(model);
    // 创建跟踪器,上面创建的级联分类器用于初始化跟踪器
    Ptr<CascadeDetectorAdapter> trackingDetector = makePtr<CascadeDetectorAdapter>(classifier_1);

    DetectionBasedTracker::Parameters params;
    // 创建跟踪器,由DetectionBasedTracker的构造函数可知:第一个参数为检测器,第二个参数为分类器
    tracker = new DetectionBasedTracker(mainDetector, trackingDetector, params);

    tracker->run();
    env->ReleaseStringUTFChars(model_, model);
}

OpenCV初始化好之后,OpenCV 处于运行状态,接下来我们需要给它图像数据

6.编写 Java 层传 Bitmap 对象到 JNI 层的代码

// MainActvivty.java

native void postData(byte[] data, int w, int h, int cameraId);

@Override
public void onPreviewFrame(byte[] data, Camera camera) {
    //传输数据
    postData(data, CameraHelper.WIDTH, CameraHelper.HEIGHT, cameraId);
}
#include 
int index = 0;

extern "C"
JNIEXPORT void JNICALL
Java_com_example_myopencv_MainActivity_postData(JNIEnv *env, jobject thiz, jbyteArray data, jint w,
                                                jint h, jint camera_id) {
    
    jbyte *data = env->GetByteArrayElements(data_, NULL);

    // 创建一张图片
    // 图片高度为 h + h / 2 原因是摄像头的数据为NV21,是YVU格式的,YUV格式的图像的高度就是 h + h / 2
    Mat src(h + h / 2, w, CV_8UC1, data);

    // 将图像格式从 NV21 转换为 RGBA
    // 这里利用 OpenCV 进行格式转换的效率可能比较低,可以尝试使用 libYUV ,libYUV 使用了 neon 汇编,处理像素会很快
    cvtColor(src, src, COLOR_YUV2RGBA_NV21);

    char picture_name[100];

    // 创建文件夹,用于存放图片数据
    mkdir("/data/data/com.example.myopencv/cache/test/", 0777);

    // 将 "/data/data/com.example.myopencv/cache/test/%d.png" 赋值给 picture_name
    sprintf(picture_name, "/data/data/com.example.myopencv/cache/test/%d.png", index++);
    // 将 转换格式后的图片写入到 /sdcard/hugang/ 这个路径下
    imwrite(picture_name, src);

    env->ReleaseByteArrayElements(data_, data, 0);
}

运行代码,然后手动同步 /data/data/com.example.myopencv/cache/ 这个目录
Android OpenCV实现人脸检测(一)完成人脸检测功能_第6张图片
同步后即可看到生成的图片
Android OpenCV实现人脸检测(一)完成人脸检测功能_第7张图片

打开图片发现问题:

1.图片的方向不对
2.图片的颜色不对

Android OpenCV实现人脸检测(一)完成人脸检测功能_第8张图片
第一个问题的解决方法:
在 cvtColor(src, src, COLOR_YUV2RGBA_NV21) 之后,加入下面的代码

if (camera_id == 1) {
   // camera_id == 1 说明是前置摄像头
   // 需要将图片旋转 90° , 然后再进行 镜像 处理
   // 可以将下面两个函数随便注释掉一个或者两个,对比观察最后生成的图片有什么区别

   // 将图片旋转 90°
   rotate(src, src, ROTATE_90_COUNTERCLOCKWISE);
   // 镜像处理
   flip(src, src, 1);
} else {
   // 后置摄像头,需要顺时针旋转 90°
   rotate(src, src, ROTATE_90_CLOCKWISE);
}

运行代码后,发现图片的角度确实改正了
Android OpenCV实现人脸检测(一)完成人脸检测功能_第9张图片
对于第二个问题,因为我们做图像检测时,都是需要将彩色的图片变为灰度图进行处理,因此引入下面的代码:

// 将图像转换为灰度图
Mat gray_img;
cvtColor(src, gray_img, COLOR_RGBA2GRAY);
... ...
// 原来的imwrite(picture_name, src); 要改成下面这行
imwrite(picture_name, gray_img);

Android OpenCV实现人脸检测(一)完成人脸检测功能_第10张图片
为了增强灰度图的对比度,我们还要在刚才的 cvtColor(src, gray_img, COLOR_RGBA2GRAY) 后面增加一行代码

// 用于增强灰度图的对比度(内部采用了直方图均衡化的方式)
equalizeHist(gray_img, gray_img);

运行后,发现灰度图的对比度确实增强了
Android OpenCV实现人脸检测(一)完成人脸检测功能_第11张图片
接下来,我们需要将灰度图传给 跟踪器,让它帮我们识别人脸

extern "C"
JNIEXPORT void JNICALL
Java_com_example_myopencv_MainActivity_postData(JNIEnv *env, jobject thiz, jbyteArray data_, jint w,
                                                jint h, jint camera_id) {

    jbyte *data = env->GetByteArrayElements(data_, NULL);

    // 创建一张图片
    // 图片高度为 h + h / 2 原因是摄像头的数据为NV21,是YVU格式的,YUV格式的图像的高度就是 h + h / 2
    Mat src(h + h / 2, w, CV_8UC1, data);

    // 将图像格式从 NV21 转换为 RGBA
    // 这里利用 OpenCV 进行格式转换的效率可能比较低,可以尝试使用 libYUV ,libYUV 使用了 neon 汇编,处理像素会很快
    cvtColor(src, src, COLOR_YUV2RGBA_NV21);

    if (camera_id == 1) {
        // camera_id == 1 说明是前置摄像头
        // 需要将图片旋转 90° , 然后再进行 镜像 处理
        // 可以将下面两个函数随便注释掉一个或者两个,对比观察最后生成的图片有什么区别

        // 将图片旋转 90°
        rotate(src, src, ROTATE_90_COUNTERCLOCKWISE);
        // 镜像处理
        flip(src, src, 1);
    } else {
        // 后置摄像头,需要顺时针旋转 90°
        rotate(src, src, ROTATE_90_CLOCKWISE);
    }

    // 将图像转换为灰度图
    Mat gray_img;
    cvtColor(src, gray_img, COLOR_RGBA2GRAY);
    // 用于增强灰度图的对比度(内部采用了直方图均衡化的方式)
    equalizeHist(gray_img, gray_img);

    char picture_name[100];

    // 创建文件夹,用于存放图片数据
    mkdir("/data/data/com.example.myopencv/cache/test/", 0777);

    // 将 "/data/data/com.example.myopencv/cache/test/%d.png" 赋值给 picture_name
    sprintf(picture_name, "/data/data/com.example.myopencv/cache/test/%d.png", index++);
    // 将 转换格式后的图片写入到 /sdcard/hugang/ 这个路径下
    imwrite(picture_name, gray_img);

    // 将灰度图传给跟踪器
    tracker->process(gray_img);
    std::vector<Rect> faces;
    tracker->getObjects(faces);
    LOGE("========= faces size : %d =========", faces.size());
    // 创建文件夹,用于存放识别出的人脸的图片
    mkdir("/data/data/com.example.myopencv/cache/face_img/", 0777);
    for (Rect face : faces) {
        sprintf(picture_name, "/data/data/com.example.myopencv/cache/face_img/%d.png", index++);
        Mat face_rect;
        face_rect = gray_img(face).clone();
        imwrite(picture_name, face_rect);
    }

    env->ReleaseByteArrayElements(data_, data, 0);
}

运行后得到 face_img 目录下识别出的人脸的照片
注意:建议不要戴眼镜,否则很难识别出人脸
Android OpenCV实现人脸检测(一)完成人脸检测功能_第12张图片

为了后期处理方便,我们需要将生成的人脸的照片缩小到 24*24 的尺寸
resize(face_rect, face_rect, Size(24, 24));

Android OpenCV实现人脸检测(一)完成人脸检测功能_第13张图片
本节的代码就讲到这里,实际上我们的 app 打开后,预览画面一直都是黑色的,下节介绍如何显示预览画面

你可能感兴趣的:(Android,OpenCV,opencv,android,android,studio)