ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

虹软ArcFace动态人脸识别画框实战:Camera2坐标映射全解析

虹软ArcFace动态人脸识别画框实战:Camera2坐标映射全解析 简介基于虹软ArcFace的人脸识别工程解决方案面向需要在Android等移动端实现摄像头动态人脸检测、追踪与画框识别的开发者覆盖从视频流采集、人脸定位到特征匹配的完整链路。压缩包共137个文件、约65.77MB核心文件包括12个so动态库、9个java源码、6个jar包配合70个xml配置、gradle构建脚本及properties资源可快速接入人脸检测、特征提取与比对流程并适配不同硬件平台。已有1191人学习下载。包内除库文件和示例工程外还包含bin缓存、多类型模块文件与工程历史记录便于开发者对照实际项目结构理解接口调用与集成步骤解决实时识别中的画框跟踪、动态帧处理等问题。适用于门禁签到、安全监控、支付验证、智能零售等实时人脸识别场景的二次开发。1. 先说结论动态人脸识别画框和静态离线识别的思路完全不一样最近在做一个基于虹软人脸识别的Camera动态人脸识别画框项目起初我以为只是把静态图片识别换到视频流里跑一下真正动手才发现这套流程从引擎模式选择、帧数据格式处理到画框坐标映射每一步都有和静态识别完全不同的讲究。这篇文章就围绕这个项目把完整实现链路和踩过的坑梳理一遍给准备接虹软SDK做实时动态识别画框的朋友做个参考。很多第一次接触虹软SDK的人会纠结一个问题既然虹软本身提供了人脸检测接口我是不是只要拿到Camera的预览帧丢给识别引擎返回的FaceInfo里有Rect框直接画到界面上就行了理论上确实只有这几步但实际做下来会发现几个关键细节比如引擎必须用ASF_DETECT_MODE_VIDEO模式才能适应动态场景下的连续检测能力比如Camera2给到的YUV数据不能直接喂给引擎需要转成NV21或BGRA格式再比如画框的手感和检测稳定性需要做坐标变换和帧率控制否则画出来的框要么位置偏移要么剧烈抖动根本没法看。这套逻辑适用于Android平台接入虹软ArcFace SDK做实时预览场景涵盖人脸检测、跟踪和UI画框。无论是做刷脸考勤、客流统计还是做一个带人脸框的相机工具核心链路都是一样的。你需要的核心依赖是虹软开放平台申请到的APP_ID和SDK_KEY以及一个人脸检测可用的Camera采集模块。我后面讲到的所有代码和参数都基于Android Camera2 TextureView ArcFace 3.x这套组合如果你用的是Camera1或其他SDK思路依然可以照搬差异只在取帧方式。1.1 为什么Camera场景必须用视频检测模式虹软的人脸识别引擎提供两种检测模式ASF_DETECT_MODE_VIDEO和ASF_DETECT_MODE_IMAGE。很多刚从图片识别迁移过来的人会习惯性选IMAGE模式因为它单次检测的精度在某些场景下更高但放到视频流里就会出问题。IMAGE模式是为单张静态图设计的引擎会把当前输入当做一个孤立帧去处理不做任何基于时序的优化。在视频流里连续调用时每一帧的人脸检测结果之间没有关联人脸框会出现明显的闪烁因为相邻两帧的检测框在像素层面可能差了十几个甚至几十个像素叠加显示后看起来就是框在脸上乱跳。VIDEO模式则内置了基于连续帧的检测跟踪机制引擎会尝试把当前帧的人脸和历史帧的人脸关联起来输出更稳定的框位置和ID信息。实际测试下来同一段预览流IMAGE模式下画框的抖动幅度能差出一整个额头的高度切换成VIDEO模式后基本稳定。还有一个容易被忽略的点是检测速度。VIDEO模式对单帧的处理耗时通常比IMAGE模式更短因为它在检测时会复用上一帧的上下文信息而不是每次都从零开始。在低端机上这个差异直接决定了预览画面是否掉帧。1.2 技术选型Camera1、Camera2还是外部SDKCamera动态识别面临的第一道选择题是取帧通道。我最初考虑直接用Camera1的PreviewCallback因为它简单粗暴onPreviewFrame回调里直接拿到NV21字节数组完全不需要自己转格式虹软SDK正好原生支持NV21可以说非常省事。但Camera1的问题也很明显它在Android 5.0之后处于维护模式接口过时设备的兼容性问题越来越多部分新机型在Camera1下取不到高分辨率预览帧而且Camera1的预览回调机制是私有的每个设备的行为都有差异。Camera2虽然上手复杂但它是当前Android设备的统一标准能稳定取到YUV_420_888格式的帧数据后续要扩展人脸比对、活体检测等能力也方便。如果你是在做一个快速验证的Demo直接用Camera1也无妨先把人脸画框跑通再说。但如果是正式项目我建议直接用Camera2把YUV_420_888转NV21的逻辑写好。这样后面切到虹软的活体检测、特征比对功能时取帧链路不需要推倒重来。2. 虹软引擎激活与初始化每一步配置都是有讲究的虹软SDK的使用流程分两步先激活再初始化这两步很多人以为只要调用API就行但实际排错时大多数问题都出在这个阶段。2.1 激活流程APP_ID、SDK_KEY和联网校验在虹软开放平台注册应用后你会拿到一组APP_ID和SDK_KEY。需要注意SDK_KEY是分平台的Android版的KEY不能用在iOS上而且申请时填的包名要和APK实际签名保持一致。这个坑很隐蔽有人改了包名后忘了同步申请新的KEY激活时一直报错误码排查了很久才发现是KEY和包名不匹配。激活代码非常简单但有几个隐藏约束FaceEngine faceEngine new FaceEngine(); int activeCode faceEngine.active(context, APP_ID, SDK_KEY);我遇到过activeCode返回非0的情况最常见的原因是网络问题虹软的激活接口需要联网请求服务端。如果你的设备处在内网环境这一步就会失败。另外需要特别注意激活过程要放在子线程执行不要在主线程里调用否则拿到非0错误码之外还可能引发ANR。不同版本SDK的激活错误码含义不同建议先打印出错误码再去SDK文档里查对应含义不要凭空猜测。2.2 初始化参数解读检测模式、检测角度和功能组合引擎初始化里有一堆让人眼花缭乱的参数不少人直接抄网上的代码结果在特定设备上表现很差其实问题就出在这些参数上。下面是我用的初始化配置int initCode faceEngine.init( context, FaceEngine.ASF_DETECT_MODE_VIDEO, FaceEngine.ASF_OP_0_HIGHER_EXT, 16, 10, FaceEngine.ASF_FACE_DETECT );逐项说下这几个参数的意义。第一个参数指定检测模式这里必须用ASF_DETECT_MODE_VIDEO理由在前面已经说过了。第二个参数是检测角度ASF_OP_0_HIGHER_EXT表示只检测0度角度的人脸即手机竖屏时正对镜头的人脸检测速度最快精度也最高。如果你的场景需要支持横屏、倒立、左右侧脸就要用ASF_OP_ALL_OUT但这样会带来额外的性能开销帧率会有明显下降根据实际场景取舍。第三个参数16是最大可检测人脸数第四个参数10是脸部尺寸限制单位是原始图像像素意思是小于10像素的人脸直接忽略。这个值的设定需要结合你的Camera预览分辨率来算如果预览帧宽度是640那么10像素的人脸大约只占画面的1.6%已经很小了。如果你做的是近距离考勤机可以调到32或更高减少误检。最后一个参数是功能组合这里是核心。如果你只做画框只要ASF_FACE_DETECT就够了。但如果还需要年龄、性别、三维角度、活体检测等信息就要用按位或的形式把多个功能组合起来。这里要注意除了ASF_FACE_DETECT必须要有之外其他功能都有对应的模型文件需要在初始化时指定模型路径。不指定或路径错误初始化也会失败。初始化完成后引擎会常驻内存不需要反复创建。我踩过的坑是每次进入预览页面都重新init退出页面又unInit导致页面切换时出现明显的卡顿和内存抖动。正确的做法是做一个全局单例持有FaceEngine页面前后切换只暂停和恢复检测。3. Camera预览帧与识别引擎之间的数据管道初始化完成后真正的麻烦才刚开始。Camera2给到的YUV_420_888帧数据格式和虹软引擎能直接吃的NV21格式不是一回事格式转换这一步做不好后面全白搭。3.1 为什么引擎只能吃NV21/BGRA手机预览却常常给YUV_420_888虹软的人脸检测接口可以接收多种像素格式包括ASF_PIXEL_FMT_NV21、ASF_PIXEL_FMT_BGRA等。但Camera2通过ImageReader拿到的默认格式是YUV_420_888这是一种灵活的YCbCr格式允许Y平面、U平面、V平面各自拥有独立的行对齐和像素步长。换成直白的说法NV21的数据是Y分量连着VU交错分量紧凑排在一个byte数组里而YUV_420_888可能拆成了三个独立的ByteBufferY和UV平面之间还有像素对齐padding。你不做转换就传给引擎拿到的检测结果必然错位。检测框要么偏到脸旁边要么完全检测不到人脸。这里有人会问为什么不直接用ImageReader的YUV_420_888格式据我所知虹软SDK在部分平台上也支持ASF_PIXEL_FMT_YUV420_888但实际兼容性差很多设备上会出现崩溃或识别率低的问题。我建议稳妥起见全部转成NV21。3.2 从ImageReader取帧并转NV21的完整代码在onImageAvailable回调中拿到Image对象后需要先提取各平面数据再合成NV21。这里要注意必须处理完Image之后调用close()否则ImageReader很快就会被占满取帧会停滞。private byte[] imageToNV21(Image image) { Image.Plane[] planes image.getPlanes(); int width image.getWidth(); int height image.getHeight(); Image.Plane yPlane planes[0]; Image.Plane uPlane planes[1]; Image.Plane vPlane planes[2]; byte[] nv21 new byte[width * height * 3 / 2]; byte[] yBuffer new byte[yPlane.getRowStride() * height]; yPlane.getBuffer().get(yBuffer); int ySize width * height; for (int i 0; i height; i) { System.arraycopy(yBuffer, i * yPlane.getRowStride(), nv21, i * width, width); } int uvSize height / 2; byte[] uBuffer new byte[uPlane.getRowStride() * uvSize]; byte[] vBuffer new byte[vPlane.getRowStride() * uvSize]; uPlane.getBuffer().get(uBuffer); vPlane.getBuffer().get(vBuffer); int uvRowStride uPlane.getRowStride(); int uvPixelStride uPlane.getPixelStride(); int uvBufferIndex 0; for (int row 0; row uvSize; row) { for (int col 0; col width / 2; col) { int srcIndex row * uvRowStride col * uvPixelStride; nv21[ySize uvBufferIndex] vBuffer[srcIndex]; nv21[ySize uvBufferIndex] uBuffer[srcIndex]; } } return nv21; }这段代码做了两件关键的事第一件是把Y平面的数据从带行对齐的rowStride压缩成紧凑排列第二件是把U、V平面的数据交错成VU顺序。如果你的设备Y平面没有额外的行对齐这段代码也能正常工作因为它强制按width来拷贝每一行。3.3 帧数据的对齐问题rowStride的坑上面代码里我用了rowStride来做逐行拷贝这个地方千万不能省略。部分设备上Y平面的rowStride等于widthU/V平面的pixelStride等于2但当遇到一些特殊机型时rowStride会多出几十个字节的对齐padding如果直接按行长度去拷贝会发现画面整体移位而且人脸检测结果全偏。我之前在一台测试平板上遇到过这种情况预览看起来正常但画框位置一直往左偏移排查了大半天才发现是Y平面的rowStride比width大了32字节。后来把逐行拷贝逻辑加上问题立刻解决了。UV平面的pixelStride也有讲究。标准NV21要求VU交错排列每个像素占2字节pixelStride就是2。但有些设备上UV平面是分离存储的pixelStride接近1意味着数据不是紧凑的VU交错。上面的代码用pixelStride来定位每个UV分量的位置就是为了兼容这种非标准情况。4. 动态识别与画框坐标映射、平滑处理和帧率控制数据管道打通后接下来是识别调用和画框这个阶段决定最终效果是能用还是好用。4.1 ASFDetectFaces调用与FaceInfo结构虹软检测人脸的调用非常直接把NV21数据、宽高和格式传进去返回一个人脸列表。ListFaceInfo faceInfoList new ArrayList(); int detectCode faceEngine.detectFaces( nv21Data, width, height, FaceEngine.ASF_PIXEL_FMT_NV21, faceInfoList );detectFaces返回0表示成功非0就是各类错误码。有一个容易忽略的点宽高传的是NV21的原始维度也就是ImageReader配置时的宽高。如果相机输出的预览尺寸是1920x1080但TextureView显示区域是1080x1920那么返回的FaceInfo中的Rect框坐标也是基于1920x1080这个原始方向的。你拿到的Rect不能直接画到View上必须先做坐标转换。FaceInfo对象里有几个关键字段Rect是人脸框faceId是同一个人脸的跟踪ID在连续帧中同一张脸倾向于拿到相同的faceIdangle是头部角度。画框时主要用Rect做多人精准识别时可以依赖faceId做目标跟踪。4.2 图片坐标系到View坐标系的转换坐标系转换是画框最简单但最容易错的一步。转换前先明确两个概念图片坐标系的原点在Image的左上角X轴向右Y轴向下单位是预览帧的像素View坐标系的原点在TextureView或自定义View的左上角单位也是像素。如果你的View显示区域和预览帧是等比例的那只要做一个简单的缩放映射Rect faceRect faceInfo.getRect(); float scaleX viewWidth * 1f / previewWidth; float scaleY viewHeight * 1f / previewHeight; float left faceRect.left * scaleX; float top faceRect.top * scaleY; float right faceRect.right * scaleX; float bottom faceRect.bottom * scaleY;但这里必须检查一个隐藏条件预览分辨率的方向和显示方向是否一致。竖屏状态下Sensor输出的原始帧通常是横向的宽大于高而TextureView的显示区域是竖向的宽小于高。这种情况下直接做等比例缩放画出来的框会旋转90度完全错位。正确的做法分两步先把预览帧根据旋转角度调整到和界面一致的方向再做缩放。虹软的FaceInfo返回的Rect是基于图像原始坐标的所以你先要把图像旋转后的坐标换算出来再映射到View。这一步需要根据传感器方向和旋转角手动计算代码如下int rotation getCameraSensorOrientation(); // Camera2中通过SENSOR_ORIENTATION获取 Rect rect faceInfo.getRect(); int previewWidth 1920; int previewHeight 1080; int left, top, right, bottom; switch (rotation) { case 90: left previewHeight - rect.bottom; top rect.left; right previewHeight - rect.top; bottom rect.right; break; case 270: left rect.top; top previewWidth - rect.right; right rect.bottom; bottom previewWidth - rect.left; break; default: left rect.left; top rect.top; right rect.right; bottom rect.bottom; break; }然后再用旋转后的坐标做缩放映射。这个旋转步骤是很多教程里容易漏掉的漏掉之后通常表现为画框位置始终对不上不管怎么调缩放比例都没用。4.3 前置摄像头镜像、旋转角度补偿前置摄像头的画面默认是镜像的也就是你在屏幕上看到的效果经过了一次水平翻转。但引擎拿到的是Sensor原始帧它识别人的位置是真实物理位置。如果直接用识别坐标画框你会看到画框和人脸左右相反。解决方案是在自定义画框View的onDraw里对Canvas做水平镜像变换canvas.scale(-1f, 1f, viewWidth / 2f, viewHeight / 2f);或者更简单的方式在坐标映射时手动翻转X轴坐标。个人建议直接操作Canvas这样不影响View内部其他绘制的坐标逻辑。后置摄像头没有这个问题但要注意部分设备Sensor方向不是90度而是270度或者某些前置摄像头Sensor方向为270度需要针对设备做配置。我通常会在初始化时读取所有可用Camera的SENSOR_ORIENTATION用一个Map缓存下来切换镜头时直接查表。4.4 让画框不抖帧率控制和框位置平滑动态画框最大的痛点不是检测不到人脸而是框一直在抖。虹软SDK的VIDEO模式已经做了很多平滑工作但如果你用最高帧率60fps连续检测每帧结果依然会有小幅度波动。我采用的方式是跳帧检测加插值绘制。具体做法是检测频率降到每3帧检测一次但画框绘制仍然每帧执行。在未检测的那两帧里画框坐标用检测帧的坐标做线性插值。// 简单的线性插值 float alpha 0.35f; smoothLeft lastLeft (targetLeft - lastLeft) * alpha; smoothTop lastTop (targetTop - lastTop) * alpha; smoothRight lastRight (targetRight - lastRight) * alpha; smoothBottom lastBottom (targetBottom - lastBottom) * alpha;alpha取0.3到0.4之间比较合适太小框会拖尾严重跟不上人脸的快速运动太大又起不到平滑效果。如果你想更精细可以记录最近5帧的坐标做滑动平均但实测下来单帧插值法效果已经足够而且代码简单。帧率控制还能直接降低CPU占用。在低端机上我的做法是当连续5秒没有检测到人脸时把检测频率降为每6帧一次检测到人脸后再提升为每3帧一次。这样既省电又保证有人出镜时响应够快。5. 几个实测翻车现场和我的最终处理5.1 低光环境下的人脸检测率骤降虹软SDK在光线充足时识别率相当高但到了昏暗环境检测率会明显下降尤其是侧光和逆光场景。这个问题不是SDK的bug而是传统2D人脸识别对图像质量的天然依赖。我的处理方式是动态调整Camera曝光参数。在Camera2中把CONTROL_AE_MODE设置为CONTROL_AE_MODE_ON并适当调高曝光补偿可以在一定程度上改善低光检测率。如果你用的是Camera2的Recorder模式可以设置CONTROL_AE_TARGET_FPS_RANGE让帧率上限降低换取更长的曝光时间。实测在正常室内光线下能稳定检测在较暗环境下识别率从几乎不可用提升到了勉强可用的水平。如果你的产品形态允许建议直接加补光灯比任何软件调优都有效。5.2 多人场景下的框ID稳定性虹软SDK在多人同时出现在画面里时每个FaceInfo都有一个faceId本意是标识同一物理人脸。但实测下来当两个人距离较近时存在ID互换的可能导致画框颜色或绑定信息在人脸上跳变。如果只是画框问题这个现象影响不大。但如果你要做多目标布控或人脸关联就需要注意这个问题。我的经验是不要只依赖faceId做长期身份追踪每帧还是做一个基于位置的人脸匹配结合faceId和当前帧位置距离综合判断。5.3 引擎资源释放与Activity生命周期虹软引擎非常吃内存初始化后会分配大量native内存如果只做unInit不做destroy或者反过来都会导致内存泄漏。正确顺序是先unInit再destroy。faceEngine.unInit(); faceEngine.destroy();还有一个容易忽略的点是Camera2资源的释放顺序。关闭Camera设备、停止预览和释放ImageReader应该在引擎destroy之前完成否则可能出现还在取帧时引擎已经被销毁的崩溃错误类型通常是底层native抛出的空指针或非法状态。崩溃日志很难看懂经常是SIGSEGV排查成本很高。做一个状态布尔变量在释放引擎前先把取帧回调关闭能有效避免这类问题。最后分享一个小技巧画框View不要直接使用SurfaceView自带的画布而是自定义一个View叠加在预览之上用invalidate刷新。这样可以避免SurfaceView的线程模型干扰代码逻辑清晰很多。上面这套流程是我在多个项目里反复调整后确定的如果你们的产品也需要在动态预览里做识别画框照这条路走能少踩很多不必要的坑。本文还有配套的精品资源点击获取
返回列表