【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: Intwhile (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 = 3private var targetId = 0private var scoreThreshold = 0.70Fprivate var nmsThreshold = 0.2Fprivate 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.FacialDrawViewandroid: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 = falsemFaces = facesrows = 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.STROKEpaint.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.FILLcolor = 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 = wvHeight = hcalculateTextPos()}/*** 计算文字的高度*/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 = truerandom = 0//如果总人数是1,不随机,直接回调if(rows == 1){isDetecting = falsemListener?.complete(scale,detectRect)}else{initTimer()}}/*** 开启计时器*/private fun initTimer() {isDetecting = truetimer = Timer()task = object : TimerTask() {override fun run() {//在人脸数目范围内生成随机数random = Random.nextInt(rows)count += 1if (count == countdown) {isDetecting = false//回调到主类显示人脸mListener?.complete(scale,detectRect)cancelTask()}else{invalidate()}}}timer!!.schedule(task, 0, 200)}/*** 根据人脸数目获取pick时间*/private fun getCount():Int{var countdown = 10if (rows < 10){countdown = 10}else if(rows in 11..30){countdown = 20}else if(rows > 30){countdown = 40}return countdown}/*** 识别完成回调*/private var mListener:DetectCompleteListener ? = nullinterface 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 + 80var vWidth = rect.width() / 2 + 80if(totalW > bitmap.width){vWidth = bitmap.width - leftX}val totalH = topY + rect.height() / 2 + 80var vHeight = rect.height() / 2 + 80if(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}

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

在这里插入图片描述


结尾

~~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/404936.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【秋招笔试】8.18大疆秋招(第三套)-三语言题解

🍭 大家好这里是 春秋招笔试突围,一起备战大厂笔试 💻 ACM金牌团队🏅️ | 多次AK大厂笔试 | 编程一对一辅导 ✨ 本系列打算持续跟新 春秋招笔试题 👏 感谢大家的订阅➕ 和 喜欢💗 和 手里的小花花🌸 ✨ 笔试合集传送们 -> 🧷春秋招笔试合集 🍒 本专栏已收…

STM32CubeMX 配置串口通信 HAL库

一、STM32CubeMX 配置串口 每个外设生成独立的 ’.c/.h’ 文件 不勾&#xff1a;所有初始化代码都生成在 main.c 勾选&#xff1a;初始化代码生成在对应的外设文件。 如 GPIO 初始化代码生成在 gpio.c 中。 二、重写fputc函数 ​ #include <stdio.h>#ifdef __GNUC__#def…

无人机之固定翼无人机的组成

固定翼无人机是根据空气动力学原理设计机翼的形状&#xff0c;靠动力装置产生推力或者拉力&#xff0c;使无人机获得一定速度后&#xff0c;会导致空气在飞机上下表面的压力不同&#xff0c;进而产生升力&#xff0c;其升力主要来源于固定的机翼。大多数都是由机翼、机身、尾翼…

FastHTML:使用 Python 彻底改变 Web 开发

什么是 FastHTML&#xff1f;&#x1f310; FastHTML 是一个现代 Python Web 应用程序框架&#xff0c;其真正目的是让 Python 开发人员轻松进行 Web 开发。它大大减少了对 JavaScript 和 CSS 构建交互式和可扩展 Web 应用程序的依赖。FastHTML 通过使用 Python 对象来表示 HTM…

python 捕获异常

捕获指定异常 e 是保存的异常信息 捕获多个异常

全国大学生数学建模比赛——时间序列(详细解读)

全国大学生数学建模比赛中&#xff0c;时间序列分析是一种重要的方法。以下是对时间序列在该比赛中的详细解读&#xff1a; 一、时间序列的概念 时间序列是按时间顺序排列的一组数据。在数学建模中&#xff0c;时间序列数据通常反映了某个现象随时间的变化情况。例如&#xf…

使用vs配置opencv环境(属性表方法)

opencv官网&#xff1a;https://opencv.org/releases/ 老手回忆&#xff08;新建属性表&#xff09; Step1: 安装VS&#xff0c;安装openCV Step2: 新建项目&#xff0c;新建项目属性表&#xff0c;debug|x64新建属性&#xff0c;命好名字 Step3: VC目录-包含目录中添加: 安装…

经典游戏,用java实现的坦克大战小游戏

今天给大家分享一个使用java编写的坦克大战小游戏&#xff0c;整体还是挺好玩的&#xff0c;通过对这款游戏的简单实现&#xff0c;加深对java基础的深刻理解。 一、设计思路 1.坦克大战小游戏通过java实现&#xff0c;其第一步需要先绘制每一关对应的地图&#xff0c;地图包括…

WPF中RenderTransform,LayoutTransform区别

RenderTransform RenderTransform 是在渲染阶段应用的变换。它不会影响控件的布局&#xff0c;只会影响控件的外观。常用于动画和视觉效果。 • 应用时机&#xff1a;在控件已经完成布局之后。 • 影响范围&#xff1a;仅影响控件的外观&#xff0c;不影响布局。 • 常见用途&…

探索 HarmonyOS 的层叠布局:灵活的 Stack 容器

在应用开发中&#xff0c;灵活的布局设计是提高用户体验的关键之一。HarmonyOS 提供了丰富的布局组件&#xff0c;其中层叠布局&#xff08;Stack Layout&#xff09;是一个强大的工具&#xff0c;可以帮助开发者轻松实现元素的重叠显示。本文将深入探讨 Stack 容器的功能和应用…

【设计模式】六大原则-下

❓首先什么是设计模式&#xff1f; &#x1f635;相信刚上大学的你和我一样&#xff0c;在学习这门课的时候根本不了解这些设计原则和模式有什么用处&#xff0c;反而不如隔壁的C更有意思&#xff0c;至少还能弹出一个小黑框&#xff0c;给我个hello world。 ✨ 如何你和我一样…

ArcGIS Pro基础:状态栏显示栏的比例尺设置和经纬度位置

上图所示&#xff0c;界面下方最左侧是显示的比例尺&#xff0c;可以进行选择设置&#xff0c;也可以进行自定义设置 上图所示&#xff0c;可以手动录入比例尺&#xff0c;同时也可以对比例尺设置别名&#xff0c;比如【实验1】作为特定比例尺的标记 如上图所示&#xff0c;可以…

KEEPALIVED高可用集群最详解

目录 一、高可用集群 1.1 集群的类型 1.2 实现高可用 1.3 VRRP&#xff1a;Virtual Router Redundancy Protocol 1.3.1 VRRP相关术语 1.5.2 VRRP 相关技术 二、部署KEEPALIVED 2.1 keepalived 简介 2.2 Keepalived 架构 2.3 Keepalived 环境准备 2.3.1 实验环境 2…

嵌入式初学-C语言-二七

文件操作 概述&#xff1a; 什么是文件&#xff1a; 是保存在外存储器&#xff08;磁盘&#xff0c;u盘&#xff0c;移动硬盘等等&#xff09;上的数据的集合。 文件操作体现在哪几个方面&#xff1a; 文件内容的读取文件内容的写入 数据的读取和写入可被视为针对文件进行…

Day42 | 739. 每日温度 496.下一个更大元素 I 503.下一个更大元素II

语言 Java 739. 每日温度 每日温度 题目 给定一个整数数组 temperatures &#xff0c;表示每天的温度&#xff0c;返回一个数组 answer &#xff0c;其中 answer[i] 是指对于第 i 天&#xff0c;下一个更高温度出现在几天后。如果气温在这之后都不会升高&#xff0c;请在该…

关于windows环境使用nginx的一些性能问题

遇到的问题 最近在一个windows环境中部署nginx&#xff0c;遇到了以下问题&#xff1a; 1. nginx启动了九个线程&#xff08;1master8woekr&#xff09;&#xff0c;但是所有链接都被1个woker接收&#xff0c;其余worker不工作 2. 用户端访问web很慢&#xff0c;登录服务器使…

k8s搭建

环境&#xff1a; 操作系统&#xff1a;win10 虚拟机&#xff1a;virtual box linux发行版&#xff1a;CentOS7.9 linux内核(使用uname -r查看)&#xff1a;3.10.0-957.el7.x86_64 master和node节点通信的ip(master)&#xff1a; 10.0.0.198 0.检查配置 本次搭建的集群共三个节…

deepspeed的并行模式介绍笔记

1.整体框架 2.并行模式 1.数据并行DDP 数据切分以后&#xff0c;分开单张卡训练得到参数&#xff0c;然后综合在单卡计算。 要点&#xff1a;前向计算和反向计算两步骤走并汇总。 1.前向计算 需要留一块主卡一定空间用于综合。 2.反向传播 利用前向传播的汇总参数得到各个…

Leetcode Java学习记录——动态规划基础

概念 首先想到递归、分治。动态规划本质也一样。 共性&#xff1a;找到重复子问题 差异性&#xff1a;有最优子结构&#xff0c;中途可以淘汰次优解。 动态规划是分治最优子结构。 例题 斐波那契数列 递归实现&#xff0c;时间复杂度是指数级。 最基础的写法为 int fib(i…

Linux CentOS java JDK17

1. 下载 cd /usr/local/ wget https://download.oracle.com/java/17/latest/jdk-17_linux-x64_bin.tar.gz 2. 解压 tar -zxf jdk-17_linux-x64_bin.tar.gz 3.配置环境变量 vim /etc/profile // 在末尾处添加 export JAVA_HOME/usr/local/jdk-17.0.12 #你安装jdk的路径&…