在Android设备上落地口罩检测,难点不只是让模型输出一次分类结果,而是保证摄像头连续帧中的每张人脸都能被稳定识别。静态图片测试通常只验证模型对单张图片的分类能力,但实时视频流会引入运动模糊、人脸角度变化、曝光波动以及多人抢占推理资源等问题。因此,口罩检测测试必须围绕完整推理链路展开,而不是孤立地评估模型精度。

一、Android端口罩检测的常见实现路径
Android端实现口罩检测通常有两种路径。第一种是使用云端API或设备端SDK,例如ML Kit的人脸检测配合自定义图像分类,或者直接使用已经封装好的人脸口罩检测能力。这种方式接入快,厂商已经处理了人脸框回归、关键点定位和部分场景适配,适合快速验证产品原型。第二种是训练或转换一个轻量级模型,通过TensorFlow Lite或ONNX Runtime在设备端推理。常见做法是先用人脸检测模型锁定人脸区域,再将该区域输入二分类模型判断是否佩戴口罩。
两种路径对测试的影响不同。使用ML Kit时,人脸检测和口罩分类可能是两个独立步骤,测试需要分别关注人脸召回率与分类准确率。使用端到端模型时,检测框和分类结果同时输出,但模型体积、推理延迟和内存占用会明显上升。无论选择哪种方式,输入图像的分辨率都会显著影响小脸检测。通常把输入限制在224×224或320×320可以降低计算量,但小于一定尺寸后,戴口罩与未戴口罩的纹理差异会被压缩,误报率随之升高。
因此在测试前应先明确设备档位。高端机型可以承受更高分辨率和更密集推理,中低端机型则需要启用跳帧策略。技术选型阶段建议记录模型文件大小、初始化耗时、单帧CPU推理耗时和GPU推理耗时,这些指标会直接影响后续测试用例的通过标准。
二、核心代码:从相机帧到口罩分类结果
下面给出一段基于ML Kit人脸检测和本地二分类模型的Kotlin示例。代码的核心流程是:将CameraX输出的图像转换为可裁剪的人脸区域,对人脸框做适度外扩,再把裁剪结果缩放到分类模型需要的尺寸。外扩的目的是保留额头和下巴区域,因为仅使用人脸检测框裁剪时,口罩上边缘和下巴处的关键纹理可能被截掉。
实时检测时不必对每一帧都执行完整流程。通常采用每隔两帧或三帧推理一次的策略,中间帧直接复用上一次结果,以减少CPU负载。检测结果需要做时序平滑,例如连续三帧出现未佩戴口罩才提示,避免单帧误判造成频繁告警。这种平滑逻辑在测试中也要验证,尤其要确认在用户快速转头或短暂遮挡时不会出现抖动的检测框。
class MaskDetector(private val context: Context) {
private val faceDetector = FaceDetection.getClient(
FaceDetectorOptions.Builder()
.setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_FAST)
.setClassificationMode(FaceDetectorOptions.CLASSIFICATION_MODE_NONE)
.build()
)
private val classifier = MaskClassifier(context)
fun analyze(bitmap: Bitmap, rotationDegrees: Int) {
val image = InputImage.fromBitmap(bitmap, rotationDegrees)
faceDetector.process(image)
.addOnSuccessListener { faces ->
for (face in faces) {
val bounds = face.boundingBox
val expanded = expandBounds(bounds, bitmap.width, bitmap.height)
val safeLeft = expanded.left.coerceAtLeast(0)
val safeTop = expanded.top.coerceAtLeast(0)
val safeRight = expanded.right.coerceAtMost(bitmap.width)
val safeBottom = expanded.bottom.coerceAtMost(bitmap.height)
val cropped = Bitmap.createBitmap(
bitmap,
safeLeft,
safeTop,
(safeRight - safeLeft).coerceAtLeast(1),
(safeBottom - safeTop).coerceAtLeast(1)
)
val label = classifier.predict(cropped)
drawResult(bounds, label)
}
}
.addOnFailureListener {
// 单帧失败时保留上一帧结果,避免检测框闪烁
}
}
private fun expandBounds(bounds: Rect, width: Int, height: Int): Rect {
val marginX = (bounds.width() * 0.18f).toInt()
val marginY = (bounds.height() * 0.22f).toInt()
return Rect(
bounds.left - marginX,
bounds.top - marginY,
bounds.right + marginX,
bounds.bottom + marginY
)
}
}
三、口罩检测测试用例设计与评估指标
口罩检测测试不能只用几张标准人脸图片。真机测试需要覆盖摄像头预览画面中可能出现的姿态、遮挡和光照变化。可以按照表格划分场景,每个场景至少准备20张图片或10段短视频,图片应包含不同性别、年龄、是否佩戴眼镜以及不同口罩颜色和类型。测试时需要同时记录检测框是否贴合人脸、分类结果是否正确以及端到端耗时。
| 测试场景 | 关注点 | 常见问题 |
|---|---|---|
| 正脸无遮挡 | 基础召回率与准确率 | 戴浅色口罩容易被判为未佩戴 |
| 侧脸约45度 | 人脸检测框是否偏移 | 裁剪区域包含过多背景导致误分类 |
| 低头或抬头 | 人脸框外扩是否覆盖下巴 | 只裁剪到嘴部上方导致特征丢失 |
| 手部或纸巾遮挡 | 遮挡后人脸检测是否仍可召回 | 检测框丢失后无法进入分类阶段 |
| 多人同框 | 多人脸并发处理与跳帧策略 | 后排小脸漏检或结果错位 |
| 弱光或逆光 | 曝光波动下的稳定性 | 摄像头自动曝光导致单帧亮度过高 |
指标方面,建议将漏报率和误报率分开统计。漏报率指实际未佩戴口罩但系统未提示的比例,这在防疫或门禁场景中更关键。误报率指实际佩戴口罩但系统误判为未佩戴的比例,过高会带来频繁拦截。除了准确率,还要记录单帧推理延迟的P50和P95,以及连续运行5分钟后的内存占用增长。很多问题只有在长时间运行后才会暴露,例如Bitmap未释放或回调频繁导致内存抖动。
对于实时视频测试,可以使用固定帧率录制的视频回放,以保证每次测试条件一致。同时要关闭系统自适应亮度,避免环境光变化干扰测试。多人场景中,应为每张人脸分配稳定ID,观察检测框是否会随目标移动发生跳变。如果结果框在相邻帧间位置变化超过一定阈值,即使分类正确也应视为不稳定。
四、阈值调优与移动端性能优化
口罩检测模型的输出通常是一个0到1之间的置信度分数。默认采用0.5阈值并不一定适合所有设备和场景。如果漏报偏多,可以降低阈值到0.35或0.4,让模型更容易触发未佩戴口罩提示;如果误报偏多,则需要提高阈值到0.6以上。阈值调整应与业务优先级对应,例如门禁系统为了避免漏放未佩戴口罩者,可以接受稍高的误报,然后通过人工复核兜底。
非极大值抑制的IoU阈值同样重要。人脸检测阶段可能对同一张脸产生多个候选框,IoU阈值过低会保留重叠框,过高则可能合并掉相邻的人脸。一般将IoU设置在0.4到0.5之间,多人场景下可以降低到0.35。输入分辨率方面,如果模型在320×320下小脸漏检严重,可以动态提高分辨率或根据检测框面积调整输入尺寸,但这会增加延迟,需要在真机上实测。
性能优化优先从减少无效计算入手。例如,只有检测到人脸面积大于总画面面积的2%时才执行分类,过小的人脸直接标记为不确定,既节省资源又避免低质量输入导致误判。还可以使用CameraX的分析帧格式YUV,直接在Y平面或灰度图上推理,减少Bitmap转换开销。部分设备支持GPU delegate或NNAPI,能显著降低推理延迟,但测试时要注意不同芯片对算子的兼容性。
Android口罩检测ML_Kit实时推理修改时间:2026-08-13 04:50:07