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

Posted 胡刚2021

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android OpenCV实现人脸检测完成人脸检测功能相关的知识,希望对你有一定的参考价值。

环境搭建:

Android Studio 集成OpenCV

本节完整的代码链接:

Android OpenCV Demo 预览黑屏
(下一节会实现预览的功能)

1.创建 assets 文件夹


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


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 <opencv2/opencv.hpp>
#include <android/native_window_jni.h>
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 <sys/stat.h>
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/ 这个目录

同步后即可看到生成的图片

打开图片发现问题:

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


第一个问题的解决方法:
在 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);
... ...
// 原来的imwrite(picture_name, src); 要改成下面这行
imwrite(picture_name, gray_img);


为了增强灰度图的对比度,我们还要在刚才的 cvtColor(src, gray_img, COLOR_RGBA2GRAY) 后面增加一行代码

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

运行后,发现灰度图的对比度确实增强了

接下来,我们需要将灰度图传给 跟踪器,让它帮我们识别人脸

extern "C"
JNIEXPORT void JNICALL
Java_com_example_myopencv_MainActivity_postData(

以上是关于Android OpenCV实现人脸检测完成人脸检测功能的主要内容,如果未能解决你的问题,请参考以下文章

OpenCV人脸检測(完整源代码+思路)

Android基于opencv4.6.0实现人脸识别功能

使用OpenCV实现Android人脸检测APP

Android OpenCV实现人脸检测JNI层添加打印时间

请教,jni调用,类型转换.用opencv进行静态人脸检测

如何在 android 中提高 OpenCV 人脸检测性能?