Android Google MLKit人脸检测:判断人脸边界框是否在指定视图内
解决人脸检测坐标系转换与圆形区域判断问题
一、核心思路
ImageAnalysis输出的人脸boundingBox基于图像像素坐标系,而圆形提示视图处于Android根视图坐标系下,需完成三步核心操作:
- 将人脸Rect从图像坐标系转换到PreviewView的坐标系
- 将PreviewView内的坐标映射到根视图坐标系
- 判断人脸是否落在圆形提示区域内
二、具体实现步骤
1. 准备全局参数
在Activity/Fragment中保存关键视图与配置参数,方便Analyzer调用:
// CameraX预览视图 private lateinit var previewView: PreviewView // 圆形提示引导视图 private lateinit var circleGuideView: View // ImageAnalysis的输出分辨率(配置CameraX时记录) private var imageAnalysisSize: Size? = null // 摄像头选择器(用于判断前置/后置) private lateinit var cameraSelector: CameraSelector
配置CameraX时记录ImageAnalysis尺寸:
val imageAnalysis = ImageAnalysis.Builder() .setTargetResolution(Size(1280, 720)) // 自定义目标分辨率 .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .build() .also { it.setAnalyzer(cameraExecutor, FaceAnalyzer()) imageAnalysisSize = it.targetResolution }
2. 改造Analyzer的analyze方法
添加坐标转换与区域判断逻辑:
class FaceAnalyzer : ImageAnalysis.Analyzer { override fun analyze(imageProxy: ImageProxy) { val mediaImage = imageProxy.image ?: run { imageProxy.close() return } val inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees) val listFaces = detector.process(inputImage) listFaces.forEach { face -> val faceRect = face.boundingBox // 1. 图像坐标系 → PreviewView坐标系 val previewRect = convertImageToPreviewRect(faceRect, imageProxy) // 2. PreviewView坐标系 → 根视图坐标系 val rootRect = convertPreviewToRootRect(previewRect) // 3. 判断人脸是否在圆形区域内 val isInCircle = checkFaceInCircle(rootRect) // 主线程更新UI(示例:改变圆形提示颜色) (context as Activity).runOnUiThread { circleGuideView.setBackgroundColor(if (isInCircle) Color.GREEN else Color.RED) } } imageProxy.close() } }
3. 实现坐标系转换方法
方法1:图像坐标系转PreviewView坐标系
处理旋转、缩放与前置摄像头镜像:
private fun convertImageToPreviewRect(imageRect: Rect, imageProxy: ImageProxy): RectF { val imageSize = Size(imageProxy.width, imageProxy.height) val previewWidth = previewView.width.toFloat() val previewHeight = previewView.height.toFloat() val rotation = imageProxy.imageInfo.rotationDegrees // 计算缩放比例(保持图像比例,居中显示) val scaleX = previewWidth / imageSize.width.toFloat() val scaleY = previewHeight / imageSize.height.toFloat() val scale = min(scaleX, scaleY) // 旋转后图像的宽高 val rotatedImgWidth = if (rotation % 180 == 0) imageSize.width else imageSize.height val rotatedImgHeight = if (rotation % 180 == 0) imageSize.height else imageSize.width // 图像在PreviewView内的偏移量(居中) val offsetX = (previewWidth - rotatedImgWidth * scale) / 2f val offsetY = (previewHeight - rotatedImgHeight * scale) / 2f // 构建转换矩阵 val matrix = Matrix().apply { postRotate(rotation.toFloat()) // 旋转对齐PreviewView方向 postScale(scale, scale) // 缩放匹配PreviewView postTranslate(offsetX, offsetY) // 平移到居中位置 // 前置摄像头镜像翻转 if (cameraSelector.lensFacing == CameraSelector.LENS_FACING_FRONT) { postScale(-1f, 1f, previewWidth / 2f, previewHeight / 2f) } } // 转换Rect val srcRect = RectF(imageRect) val dstRect = RectF() matrix.mapRect(dstRect, srcRect) return dstRect }
方法2:PreviewView坐标系转根视图坐标系
通过View的位置偏移完成转换:
private fun convertPreviewToRootRect(previewRect: RectF): RectF { val previewLoc = IntArray(2) previewView.getLocationInWindow(previewLoc) val previewX = previewLoc[0].toFloat() val previewY = previewLoc[1].toFloat() return RectF( previewRect.left + previewX, previewRect.top + previewY, previewRect.right + previewX, previewRect.bottom + previewY ) }
4. 判断人脸是否在圆形区域内
提供两种常用判断逻辑,按需选择:
逻辑A:人脸中心在圆形内
private fun checkFaceInCircle(faceRect: RectF): Boolean { val circleLoc = IntArray(2) circleGuideView.getLocationInWindow(circleLoc) val circleCenterX = circleLoc[0] + circleGuideView.width / 2f val circleCenterY = circleLoc[1] + circleGuideView.height / 2f val circleRadius = min(circleGuideView.width, circleGuideView.height) / 2f // 人脸中心坐标 val faceCenterX = faceRect.centerX() val faceCenterY = faceRect.centerY() // 计算中心距离 val distance = sqrt( (faceCenterX - circleCenterX).pow(2) + (faceCenterY - circleCenterY).pow(2) ) return distance <= circleRadius }
逻辑B:人脸区域70%以上在圆形内
private fun checkFaceInCircle(faceRect: RectF): Boolean { val circleLoc = IntArray(2) circleGuideView.getLocationInWindow(circleLoc) val circleRect = RectF( circleLoc[0].toFloat(), circleLoc[1].toFloat(), circleLoc[0] + circleGuideView.width.toFloat(), circleLoc[1] + circleGuideView.height.toFloat() ) // 计算交集区域 val intersection = RectF() if (!intersection.setIntersect(faceRect, circleRect)) return false // 计算交集占比 val faceArea = faceRect.width() * faceRect.height() val intersectionArea = intersection.width() * intersection.height() val ratio = intersectionArea / faceArea return ratio >= 0.7f }
三、注意事项
- 所有UI更新操作必须切换到主线程执行
- 前置摄像头必须处理镜像翻转,否则坐标会左右颠倒
- 需确保在View完成布局后获取宽高与位置(可在
onLayout或view.post中操作)
内容的提问来源于stack exchange,提问作者Lê Khánh Vinh
相关产品推荐
相关产品推荐

