【Android 笔记】记移植OpenCV4.8图像人脸识别

前言


因业务需要,使用大屏端摄像头捕获图像,且要识别图像中人脸的数目以及从中随机抽取一人。

业务流程如下,调用摄像头预览、拍照,使用OpenCV库进行人脸识别,将识别到的人脸使用矩形框绘制出来,从识别的人脸中随机选中一人进行展示。

在这里插入图片描述

实现


一、集成OpenCV4.8版本库

本例中人脸识别功能使用OpenCV4.8.0版本来实现,进入官网,选择OpenCV-4.8.0版本,Android平台下载。

OpenCV官网

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

下载解压,将文件夹中的sdk作为module导入到工程中,以下是OpenCV的检测流程。

在这里插入图片描述

1、加载本地OpenCV

 
	 if (!OpenCVLoader.initDebug()) {
	      Log.d(TAG, "Internal OpenCV library not found. Using OpenCV Manager for initialization")
	      OpenCVLoader.initAsync(OpenCVLoader.OPENCV_VERSION, DemoApplication.context, loaderCallback)
	  } else {
	      Log.d(TAG, "OpenCV library found inside package. Using it!")
	      loaderCallback.onManagerConnected(LoaderCallbackInterface.SUCCESS)
	  }
	
	 private val loaderCallback: BaseLoaderCallback = object : BaseLoaderCallback(DemoApplication.context) {
	         override fun onManagerConnected(status: Int) {
	             when (status) {
	                 SUCCESS -> {
	                     initOnnx()
	                     Log.i(TAG, "OpenCV loaded successfully")
	                 }
	
	                 else -> {
	                     super.onManagerConnected(status)
	                 }
	             }
	         }
	 }

2、初始化检测模型

检测模型使用YuNet架构,YuNet 是一种轻量级的面部检测网络,在移动设备和嵌入式设备上实现高效、实时的面部检测。320_320: 表示模型的输入尺寸是 320x320 像素,模型通常在特定的输入尺寸下进行训练,以优化检测性能和速度。

新建res/raw,将检测模型文件放入raw中。

在这里插入图片描述


   /**
   	* 初始化检测模型
   	*/
  	private fun initOnnx() {
      	try {
	          val yyNet = resources.openRawResource(R.raw.yunet_n_320_320)
	          val cascadeDir = requireContext().getDir("cascade", Activity.MODE_PRIVATE)
	          val cascadeFile = File(cascadeDir, "yunet_n_320_320.onnx")
	          val os = FileOutputStream(cascadeFile)
	          val buffer = ByteArray(4096)
	          var bytesRead: Int
	          while (yyNet.read(buffer).also { bytesRead = it } != -1) {
	              os.write(buffer, 0, bytesRead)
	          }
	          yyNet.close()
	          os.close()
         	 //创建检测器时用到
          	modelPath = cascadeFile.absolutePath
     	 } catch (e: Exception) {
       	   e.printStackTrace()
     	 }
 	 }
    

3、创建检测器

步骤2读取检测模型,在创建检测器时作为参数传入,

 
 	public static FaceDetectorYN create(String model, String config, Size input_size, float score_threshold, float nms_threshold, int top_k, int backend_id, int target_id) {}
 

FaceDetectorYNcreate()方法参数含义如下。


	 private var backendId = 3
	
	 private var targetId = 0
	
	 private var scoreThreshold = 0.70F
	
	 private var nmsThreshold = 0.2F
	
	 private var topK = 5000


	/**
	 * @param modelPath 预训练模型文件的路径,通常是一个 .onnx 文件
	 * @param config 配置文件路径,一般为空字符串
	 * @param inputSize 输入图像的尺寸,以 Size(width, height) 的形式传入
	 * @param scoreThreshold 置信度分数阈值,默认值为 0.9
	 * @param nmsThreshold 非极大值抑制(NMS)阈值,默认值为 0.3
	 * @param topK 留的最高置信度检测结果的数量上限,默认值为 5000
	 * @param backendId 指定使用的计算后端,默认值为 0(DNN_BACKEND_DEFAULT)
	 * @param targetId 指定计算目标设备,默认值为 0(DNN_TARGET_CPU)
	 */
	    
	 private fun detectPicture() {
	     val images = org.opencv.android.Utils.loadResource(DemoApplication.context, R.mipmap.face)
	     val detector = FaceDetectorYN.create(modelPath, "", images.size(), scoreThreshold, nmsThreshold, topK, backendId, targetId)
	     detector.inputSize = images.size()
	     val faces = Mat()
	     detector.detect(images, faces)
	     
	     //自定义View,绘制人脸
	 	 mViewBinding.detectResult.setFacesData(images, faces)
	 }
 

二、自定义FacialDrawView绘制人脸

前面介绍了OpenCV的配置过程,集成导入、加载模型、初始化检测器以及识别方法。识别的结果参数中有总共识别到的数目、以及每个人脸的的坐标信息。有了信息接下来就比较简单了,自定义FacialDrawView,绘制识别到总人数以及绘制矩形框将人脸标记出来即可,下面是识别标记效果。

在这里插入图片描述
下面贴下FacialDrawView中主要的方法,绘制标记所有人脸、绘制总人数提示框。

 	
	 <!--布局文件中引入-->
   	 <com.ho.csdn.widget.FacialDrawView
        android:id="@+id/detectResult"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:visibility="visible" />


	 /**
	  * 是否是pick模式
	  */
	 private var isPick = false
	 
	 /**
	  * 识别到的人脸数据
	  */
	 private var mFaces: Mat? = null
	 
	 /**
	  * 缩放比
	  */
	 private var scale = 0f
	
	 /**
	  * 随机选人,计数使用
	  */
	 private var count = 0
	
	 /**
	  * 人脸数目
	  */
	 private var rows: Int = 0
	
	 /**
	  * 图片宽
	  */
	 private var imgWidth = 0f
	
	 /**
	  * 图片高
	  */
	 private var imgHeight = 0f
	
	 /**
	  * 设置识别数据
	  * @param[images] 识别的图片
	  * @param[faces] 图片中
	  */
	 fun setFacesData(images: Mat, faces: Mat) {
	     isPick = false
	     mFaces = faces
	     rows = faces.rows()
	     countdown = getCount()
	     imgWidth = images.width().toFloat()
	     imgHeight =  images.height().toFloat()
	     scale = (width / imgWidth).coerceAtMost(height / imgHeight)
	     invalidate()
	 }
	
	 /**
	  * onDraw方法中调用
	  * 绘制所有人脸矩形框
	  */
	 private fun drawAllRect(canvas: Canvas) {
	     paint.style = Paint.Style.STROKE
	     paint.color = ctx.getColor(R.color.color_rect)
	     for (i in 0..rows) {
	         val x1 = mFaces?.get(i, 0)?.get(0)?.times(scale)?.toInt()
	         val y1 = mFaces?.get(i, 1)?.get(0)?.times(scale)?.toInt()
	         val x2 = mFaces?.get(i, 2)?.get(0)?.times(scale)?.toInt()
	         val y2 = mFaces?.get(i, 3)?.get(0)?.times(scale)?.toInt()
	
	         if (x1 != null && x2 != null && y1 != null && y2 != null) {
	             detectRect = Rect(x1, y1, x2 + x1, y2 + y1)
	             canvas.drawRect(detectRect, paint)
	         }
	     }
	 }


 	 /**
	  * 文字的高度
	  */
	 private var textY  = 0f
	
	 /**
	  * 自定义View宽度
	  */
	 private var vWidth = 0
	
	 /**
	  * 自定义View高度
	  */
	 private var vHeight = 0
		
	 /**
	  * 圆角矩形距离顶部距离
	  */
	 private var rectTop = 60f
	
	 /**
	  * 圆角矩形的高度
	  */
	 private var rectH = 120
	  
	 /**
	  * 圆角矩形的宽度
	  */
	 private var rectW = 520

	 /**
	  * 绘制总人数
	  */
	 private fun drawNumber(canvas: Canvas) {
	    if (rows > 0) {
	        paint.apply {
	            style = Paint.Style.FILL
	            color = ctx.getColor(R.color.color_rect)
	        }
	        //绘制蓝色底圆角矩形
	        canvas.drawRoundRect(RectF((width - rectW) /  2f,rectTop,width / 2f + rectW / 2,rectH +  rectTop),20f,20f,paint)
	
	        paint.color = ctx.getColor(R.color.colorWhite)
	        val personTxt = ctx.getString(R.string.detect_student_num,rows)
	        //计算文字的宽度
	        val txtWidth = paint.measureText(personTxt,0,personTxt.length)
	        //绘制文字
	        canvas.drawText(personTxt, (width - txtWidth) /  2, textY +  rectTop + (rectH - textY) /  2, paint)
	    }
	 }


     override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        vWidth = w
        vHeight = h
        calculateTextPos()
     }


	 /**
	  * 计算文字的高度
	  */
	 private fun calculateTextPos(){
	    //获取fontMetrics对象
	    val fontMetrics = paint.fontMetrics
	    //获取文本的高度的一半,取文字垂直中线高度值
	    val textHalfHeight = (abs(fontMetrics.descent) + abs(fontMetrics.ascent)) / 2
	    //将文字的向上移动Descent,再向下移动文字高度一半
	    textY = abs(fontMetrics.descent) + textHalfHeight
	 }

关于文字高度的计算,这里就不详细介绍了,可以参考之前写的一篇文章 Android自定义控件(六) Andriod仿iOS控件Switch开关


三、实现随机选人功能

识别到所有的人脸后,再从其他随机选择一个人脸进行展示就很简单了,开启定时器,在人脸总数范围内生成随机索引,每次绘制对应索引的人脸矩形,就能达到这样的效果。


	/**
     * 是否正在识别
     */
    private var isDetecting = false

  	/**
     * 随机倒计时
     */
    private var countdown = 10

	/**
     * 最终选择学生人脸矩形框对象
     */
    private lateinit var detectRect : Rect
    
    /**
     * 定时器对象
     */
    private var timer: Timer? = null

    /**
     * 定时器任务对象
     */
    private var task: TimerTask? = null

 	/**
     * 随机数
     */
    private var random = 0
  	
  	/**
     * 是否正在检测,主类中使用
     */
    fun isDetecting():Boolean = isDetecting

    /**
     * 随机选人
     */
    fun setRandomPick() {
        isPick = true
        random = 0
        //如果总人数是1,不随机,直接回调
        if(rows == 1){
            isDetecting = false
            mListener?.complete(scale,detectRect)
        }else{
            initTimer()
        }
    }

    /**
     * 开启计时器
     */
    private fun initTimer() {
        isDetecting = true
        timer = Timer()
        task = object : TimerTask() {
            override fun run() {
                //在人脸数目范围内生成随机数
                random = Random.nextInt(rows)
                count += 1
                if (count == countdown) {
                    isDetecting = false
                    //回调到主类显示人脸
                    mListener?.complete(scale,detectRect)
                    cancelTask()
                }else{
                    invalidate()
                }
            }
        }
        timer!!.schedule(task, 0, 200)
    }
    

    /**
     * 根据人脸数目获取pick时间
     */
    private fun getCount():Int{
        var countdown = 10
        if (rows < 10){
            countdown = 10
        }else if(rows in 11..30){
            countdown = 20
        }else if(rows > 30){
            countdown = 40
        }
        return countdown
    }

    /**
     * 识别完成回调
     */
    private var mListener:DetectCompleteListener ? = null

    interface DetectCompleteListener{
        fun complete(scale:Float,rect:Rect)
    }

    fun setCompleteListener(listener:DetectCompleteListener){
        mListener = listener
    }
  

四、展示

FacialDrawView实现随机点名后,将最终的选中的学生人脸坐标范围回调到主类,PickDialog中内容比较简单,实现展示头像、重新点名功能。

在这里插入图片描述

     

    /**
     * 完成随机选人后回调到主类
     */    
    mViewBinding.detectResult.setCompleteListener(object:FacialDrawView.DetectCompleteListener{
            override fun complete(scale:Float,rect: Rect) {
                val left = (rect.left / scale).toInt()
                val top = (rect.top / scale).toInt()
                //裁剪图片中指定的区域
                val cropBitmap = cropBitmap(detectBitmap!!,scale,left ,top ,rect)
                requireActivity().runOnUiThread { showPickDialog(cropBitmap) }
            }
        })
        

    /**
     * 裁剪识别到的头像
     */
    private fun cropBitmap(bitmap: Bitmap, scale:Float,left: Int, top: Int,rect:Rect): Bitmap {
        val matrix = Matrix()
        matrix.setScale(scale,scale)

        val leftX: Int = if(left - 40 < 0){ 0 }else{ left - 40 }
        val topY: Int = if(top - 40 < 0) { 0 }else { top - 40 }

        val totalW = leftX + rect.width() / 2 + 80
        var vWidth = rect.width() / 2 + 80
        if(totalW > bitmap.width){
            vWidth = bitmap.width - leftX
        }

        val totalH = topY + rect.height() / 2 + 80
        var vHeight = rect.height() / 2 + 80
        if(totalH > bitmap.height){
            vHeight = bitmap.height - topY
        }
        return Bitmap.createBitmap(bitmap, leftX, topY, vWidth , vHeight,matrix,false)
    }


    private fun showPickDialog(cropBitmap: Bitmap) {
        PickDialog
            .init(requireContext())
            .setAvatar(cropBitmap)
            .create().setPickListener(object :PickDialog.PickListener{
                override fun pickAgain(isRetry:Boolean) {
                    if(isRetry){
                    	//重新选人
                        mViewBinding.detectResult.setRandomPick()
                    }else{
             			//todo
                    }
                }

            }).show()
    }


五、UVC摄像头预览拍照

上述使用本地资源文件进行人脸识别过程,和摄像头拍照识别过程是一致的,只需要将预览、拍照、将照片文件转为为OpenCV能识别的图像格式即可。因大屏端使用的是USB Camera,尝试使用Android原生Camera1Camera2 API来调用相机,预览十分卡顿,无法正常使用,这里就借用了Github上大神的AndroidUSBCamera工程库来实现,下面介绍下识别拍照中人脸的一些重要内容。

首先是覆写getCameraRequest()方法,指定根据自身需求预览的尺寸等。


    override fun getCameraRequest(): CameraRequest {
        return CameraRequest.Builder()
            .setPreviewWidth(1920) // camera preview width
            .setPreviewHeight(1080) // camera preview height
            .setRenderMode(CameraRequest.RenderMode.OPENGL) // camera render mode
            .setDefaultRotateType(RotateType.ANGLE_0) // rotate camera image when opengl mode
            .setAudioSource(CameraRequest.AudioSource.SOURCE_AUTO) // set audio source
            .setAspectRatioShow(true) // aspect render,default is true
            .setCaptureRawImage(false) // capture raw image picture when opengl mode
            .setRawPreviewData(false)  // preview raw image when opengl mode
            .create()
    }

savePath是拍照图片保存的路径,个将拍照的图片转化成Bitmap。其他后续流程和上述识别本地资源图片过程一致。

 
     /**
     * 识别拍照图片图片
     */
    private fun detectPicture() {
        detectBitmap = ImageProcessor.resizeBitmap(ImageProcessor.compressBitmap(savePath,50),1920,1080)
        mViewBinding.facesPic.setImageBitmap(detectBitmap)
        val images = ImageProcessor.convertToMat(detectBitmap)
        val detector = FaceDetectorYN.create(modelPath, "", images.size(), scoreThreshold, nmsThreshold, topK, backendId, targetId)
        detector.inputSize = images.size()
        val faces = Mat()
        detector.detect(images, faces)
        mViewBinding.detectResult.setFacesData(images, faces)
        mViewBinding.loading.visibility = View.GONE
    }
        

转化的过程需要注意一点的是,如果直接将生成的位图交给OpenCV识别,会提示一下错误信息。Number of input channels should be multiple of 3 but got 4:表示在卷积层中期望输入的通道数是3的倍数,但实际上输入的通道数为4,这导致了错误。

输入数据格式不正确:通常,图像数据在RGB模式下有3个通道(红、绿、蓝),而在RGBA模式下有4个通道(红、绿、蓝、透明度)。如果模型或网络期望的是RGB数据,提供的是RGBA数据,就会出现这个错误。

将生成的Bitmap通过Imgproc.cvtColor 转化为3通道的数据,这样OpenCV才能正常识别。


   fun convertToMat(bitmap: Bitmap?): Mat {
        val mat = Mat()
        Utils.bitmapToMat(bitmap, mat)
        Imgproc.cvtColor(mat, mat, Imgproc.COLOR_BGR2RGB)
        return mat
    }

摄像头拍照识别效果如下:

在这里插入图片描述


结尾

~~

  • 9
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Android OPenCV 4.8是一个强大的人脸识别工具,可在Android设备上进行人脸识别的开发。它基于OpenCV库,提供了许多功能强大的特征和算法来检测和识别人脸。 使用Android OPenCV 4.8进行人脸识别需要进行以下步骤: 1. 导入OpenCV库:首先,需要将OpenCV库导入到Android项目中。可以下载OpenCV Android SDK并将其导入到项目中,然后在gradle文件中配置相关依赖。 2. 初始化OpenCV:在应用程序启动时,需要初始化OpenCV库,以便能够使用其功能。这可以通过在应用程序的主活动中添加一些代码来实现。 3. 图像预处理:在进行人脸识别之前,需要对图像进行预处理。这可能包括降噪、增强对比度和明暗等操作,以提高识别的准确性。 4. 人脸检测:使用OpenCV提供的人脸检测器,可以在图像中检测出人脸区域。可以使用Haar特征分类器来实现人脸检测,该分类器训练了大量的正负样本来识别人脸。 5. 特征提取:一旦检测到人脸区域,就可以从中提取出一些关键特征,例如眼睛的位置、嘴巴的位置等。这些特征将用于后续的人脸识别过程。 6. 人脸识别:使用提取的特征,可以将其与预先存储的已知人脸特征进行比较。可以使用各种算法,如人脸识别一对一、一对多等。 7. 结果展示:根据识别的结果,可以将结果展示给用户。可以显示识别的人脸名称或其他相关信息。 总之,Android OPenCV 4.8是一种实现人脸识别的强大工具,它提供了一系列功能和算法来实现人脸检测、特征提取和人脸识别等任务。使用这个工具,我们可以开发出各种应用,如人脸解锁、人脸支付等。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值