You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android Google MLKit人脸检测:判断人脸边界框是否在指定视图内

解决人脸检测坐标系转换与圆形区域判断问题

一、核心思路

ImageAnalysis输出的人脸boundingBox基于图像像素坐标系,而圆形提示视图处于Android根视图坐标系下,需完成三步核心操作:

  1. 将人脸Rect从图像坐标系转换到PreviewView的坐标系
  2. 将PreviewView内的坐标映射到根视图坐标系
  3. 判断人脸是否落在圆形提示区域内

二、具体实现步骤

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.08 03:50:41