FEATURED · 精选文章

Android端侧猫脸识别实战:从模型选型到IoT部署

发布时间 / 2026/9/20 18:17:46
来源 / 创域科博编辑部
栏目 / 资讯中心
Android端侧猫脸识别实战:从模型选型到IoT部署 猫脸识别这个事最早我是被家里那只橘猫逼出来的。它总在凌晨三点挠门要吃的我想做个自动投喂器但问题来了——怎么让机器知道是猫来了而不是狗、人或者风吹动的窗帘摄像头加运动检测的方案太粗糙误触发率高得离谱。后来我琢磨着既然人脸识别在Android上已经跑得很成熟了猫脸识别理论上应该也能走通同一条路。这个项目就是在Android IoT设备上实现猫脸检测与识别跑在边缘端不依赖云端推理适合做智能宠物门禁、自动投喂器、猫砂盆计数这类场景。整条链路涉及Android SDK、Camera2、TFLite、WebRTC推流以及阿里云IoT平台做设备管理和数据上报。如果你也在做端侧视觉项目或者单纯想给自家猫做个好玩的东西这篇内容应该能帮你省下不少试错时间。1. 为什么猫脸识别不能直接套用人脸那套1.1 猫脸和人脸在特征层面的根本差异人脸识别之所以在移动端跑得这么顺是因为人脸有非常稳定的几何结构两眼间距、鼻尖位置、嘴巴轮廓这些关键点在所有人类脸上比例大致固定。你拿MobileFaceNet或者ArcFace这类模型输入112x112的人脸对齐图出来的特征向量余弦相似度就能判断是不是同一个人。但猫脸完全是另一回事。猫的脸型变化极大。扁脸猫比如波斯猫和长脸猫比如暹罗猫的骨骼结构差异比不同人种之间的人脸差异大得多。更麻烦的是猫没有眉毛眼睛和鼻子的相对位置在不同姿态下变化剧烈。你侧躺拍一张、正脸拍一张、俯视拍一张同一个猫的特征向量可能差出0.4以上的余弦距离。我一开始拿人脸检测模型直接跑猫脸MTCNN的候选框几乎全落在猫眼睛上鼻子和嘴巴的关键点回归完全崩掉。所以猫脸识别必须走两条路检测和识别分开处理。检测用目标检测框架YOLO系列或SSD识别用度量学习metric learning训练一个专门针对猫脸的特征提取器。不能指望一个人脸模型改改输出类别就能用。1.2 端侧算力约束下的模型选型逻辑Android IoT设备通常跑在RK3399、高通410或者更低端的芯片上NPU算力有限。我手头这块开发板是RK3399CPU四核A53加双核A72没有独立NPU只能靠CPU和GPU跑推理。这种情况下模型选型要遵循几个硬约束检测模型参数量控制在1M以内输入分辨率不超过320x320识别模型参数量控制在2M以内输入分辨率112x112或128x128单帧推理总耗时不超过80ms否则WebRTC推流会卡顿我最终选了YOLOv5n作为检测骨干输入320x320量化到INT8之后模型大小约1.8MB单帧推理在RK3399上约35ms。识别模型用的是MobileNetV2改的嵌入网络输出128维特征向量量化后约2.3MB单帧约22ms。两个模型串行跑加上前后处理端到端约75ms勉强能跑到12fps左右。这个帧率做实时预览够用但要做高帧率推流就得降分辨率或者跳帧。注意INT8量化对猫脸识别精度的影响比人脸大。人脸特征分布集中量化误差容忍度高猫脸特征分散量化后类间距离可能缩小15%到20%。建议量化后用校准集重新评估阈值不要直接沿用浮点模型的阈值。1.3 数据采集这个坑比模型训练深得多我一开始以为猫脸数据集很好找Kaggle上确实有几个但实际用起来问题很大。公开数据集大多是正面、光照均匀的宠物证件照风格而实际IoT场景里猫是运动的、光照是变化的、角度是随机的。拿这种数据训出来的模型一到真实场景就废。后来我干脆自己搭了个采集装置一个固定角度的摄像头对着猫碗连续拍了三天攒了约4000张原始图。然后手动标注了1200张剩下的用半自动方式——先用初版模型跑一遍把置信度高于0.7的框自动保留低于0.3的丢弃中间区间的人工复核。这样标注效率提升了大概三倍。数据增强方面除了常规的随机裁剪、亮度对比度抖动我额外加了两个针对猫脸的操作一是随机遮挡模拟猫爪挡脸二是随机透视变换模拟俯视和仰视角度。这两个增强对实际场景的召回率提升很明显尤其是遮挡增强让模型在猫半埋着头吃东西时也能检测到。2. Android端Camera2与TFLite的衔接细节2.1 Camera2预览流格式的选择与转换开销Android上拿摄像头数据Camera2 API是绕不开的。预览流可以选YUV_420_888或者PRIVATE格式。PRIVATE格式直接走GPU纹理效率高但CPU读不到数据YUV_420_888能拿到字节数组但格式转换有开销。我的做法是双路输出一路PRIVATE给SurfaceView做实时预览另一路YUV_420_888给推理线程。YUV到RGB的转换用RenderScript或者libyuv我实测libyuv的I420ToARGB比RenderScript快约30%在RK3399上转换一帧640x480大约8ms。转换完再缩放到320x320送进检测模型。这里有个细节容易被忽略Camera2的YUV_420_888实际内存布局可能是I420、NV12或NV21取决于设备厂商。你不能硬编码假设它是I420。我踩过这个坑在一台平板上跑得好好的换到另一台设备上颜色完全错乱。正确做法是用Image.Plane的getBuffer()和getRowStride()动态判断或者直接用libyuv的Android420ToARGB接口它能自动处理各种布局。// 动态处理YUV_420_888的转换 Image image reader.acquireLatestImage(); Image.Plane[] planes image.getPlanes(); ByteBuffer yBuffer planes[0].getBuffer(); ByteBuffer uBuffer planes[1].getBuffer(); ByteBuffer vBuffer planes[2].getBuffer(); int ySize yBuffer.remaining(); int uSize uBuffer.remaining(); int vSize vBuffer.remaining(); byte[] nv21 new byte[ySize uSize vSize]; yBuffer.get(nv21, 0, ySize); vBuffer.get(nv21, ySize, vSize); uBuffer.get(nv21, ySize vSize, uSize); // 再用libyuv做NV21到ARGB的转换2.2 TFLite Interpreter的线程配置与内存复用TFLite在Android上有两种推理后端CPU和GPU Delegate。GPU Delegate在RK3399的Mali-T860上跑速度确实快但有个致命问题——首次推理延迟极高因为要编译shader大概要等2到3秒。对于实时流来说这个卡顿不可接受。所以我最终用了CPU后端配合XNNPACK加速。Interpreter的线程数设置也有讲究。RK3399有6个核4小2大我设了4个线程让推理跑在小核上大核留给Camera和WebRTC编码。如果设成6线程反而会因为大核被占用导致预览掉帧。这个参数没有通用最优值得根据具体芯片的big.LITTLE架构调。内存复用方面TFLite的Interpreter每次run()都会分配输入输出Tensor的内存。如果每帧都新建InterpreterGC压力会很大。正确做法是全局只建一个Interpreter实例用ByteBuffer直接写入输入数据输出也复用同一个ByteBuffer。我实测这样能把GC频率从每秒多次降到几乎为零。// 全局初始化一次 Interpreter.Options options new Interpreter.Options(); options.setNumThreads(4); options.setUseXNNPACK(true); Interpreter interpreter new Interpreter(loadModelFile(context), options); // 每帧复用 ByteBuffer inputBuffer ByteBuffer.allocateDirect(1 * 320 * 320 * 3 * 4); inputBuffer.order(ByteOrder.nativeOrder()); ByteBuffer outputBuffer ByteBuffer.allocateDirect(1 * 25200 * 6 * 4); outputBuffer.order(ByteOrder.nativeOrder()); interpreter.run(inputBuffer, outputBuffer);2.3 前后处理的对齐问题letterbox的坑YOLO系列模型训练时通常用letterbox做图像缩放保持宽高比空白处填灰色。推理时如果直接resize到320x320而不做letterbox检测框坐标映射回原图时会有偏移。这个偏移在小目标上不明显但猫脸占画面比例较大时偏移可能达到十几个像素。我的做法是在预处理阶段记录缩放比例和padding偏移量后处理时反向映射。具体来说如果原图是640x480缩放到320x320时缩放比例是0.5但高度方向会有padding。映射公式是x_original (x_model - pad_x) / scale y_original (y_model - pad_y) / scale其中pad_x和pad_y是letterbox时左右和上下的填充量。这个细节在PC上跑的时候很容易发现因为你可以可视化检测框但在Android上如果只看日志可能很久都发现不了。3. 猫脸特征嵌入网络的设计与训练策略3.1 三元组损失在猫脸场景下的参数调整识别网络用的是三元组损失triplet loss但标准的人脸三元组挖掘策略在猫脸上效果不好。人脸训练时通常用半难样本挖掘semi-hard mining因为人脸类间差异小太难的样本会导致训练不稳定。但猫脸类间差异大半难样本对模型来说太简单了收敛很慢。我改用了难样本挖掘hard mining加一个动态margin。具体来说margin从0.2开始每5个epoch增加0.05直到0.5。这样早期训练稳定后期能拉开类间距离。另外猫脸的三元组采样不能完全随机要保证同一个猫的不同姿态、不同光照的样本能被采到。我的做法是按猫的ID分组每组内随机选anchor和positivenegative从其他组随机选。训练数据量方面我最终用了约8000张标注图覆盖12只猫每只猫大概600到800张包含正面、侧面、俯视、遮挡、不同光照条件。验证集单独留了2只猫不参与训练用来评估泛化能力。最终在验证集上的等错误率EER大约是8.3%对于端侧模型来说可以接受。3.2 特征向量的归一化与距离阈值设定识别网络输出128维特征向量后必须做L2归一化否则距离度量没有意义。归一化之后同一个猫的特征向量余弦相似度通常在0.75以上不同猫之间通常在0.4以下。但阈值不能拍脑袋定要用验证集画ROC曲线找最优工作点。我实际设定的阈值是0.62。这个值是在验证集上让误接受率FAR和误拒绝率FRR大致相等时得到的。但实际部署时还要根据场景调整如果是猫脸门禁宁可误拒绝也不能误接受阈值可以提到0.7如果是自动投喂器误接受只是多喂一次问题不大阈值可以降到0.55。还有一个工程上的技巧维护一个特征库每只猫存多个特征向量比如10个不同姿态的识别时取最小距离。这样比只存一个平均向量鲁棒得多。特征库可以存在本地SQLite里也可以上报到阿里云IoT平台做云端备份。3.3 增量学习新猫加入时怎么不重训整个模型实际使用中肯定会遇到新猫加入的情况。如果每次都要重新训练整个模型那维护成本太高了。我的方案是识别网络本身不重训只更新特征库。新猫来了采集20到30张不同角度的图用现有模型提取特征向量存入特征库即可。检测模型也不需要重训因为YOLO检测的是“猫脸”这个大类不区分具体是哪只猫。只有当识别准确率明显下降比如特征库里的猫超过50只或者出现大量外观相似的猫时才需要考虑重训识别网络。这时候可以用增量学习的方式在原有模型基础上用新数据微调几个epoch学习率设小一点比如1e-4避免灾难性遗忘。4. WebRTC推流与阿里云IoT平台的集成4.1 为什么选WebRTC而不是RTMP做端侧推流RTMP在Android上推流通常用librtmp或者FFmpeg延迟一般在2到5秒。对于猫脸识别这种场景如果只是做监控回看RTMP够用。但我想做的是实时互动比如猫靠近时手机能立刻收到通知并看到画面延迟必须控制在500ms以内。WebRTC的端到端延迟可以做到200ms左右而且原生支持AndroidGoogle的libwebrtc有预编译的AAR包。不过WebRTC在Android IoT设备上跑有个坑libwebrtc默认用OpenGL ES做视频渲染和编码但低端芯片的GPU可能不支持某些扩展。我在RK3399上跑的时候H.264硬件编码器初始化失败最后降级到VP8软件编码才跑通。VP8软件编码在RK3399上720p大概能跑25fps功耗比硬件编码高约30%但至少能用。推流架构是这样的Camera2拿到预览帧一路送TFLite推理一路送WebRTC的VideoCapturer。推理结果猫的ID、置信度、时间戳通过DataChannel发送视频流通过MediaStream发送。接收端可以用任何支持WebRTC的客户端我用的是Android手机上的一个简单Demo。4.2 阿里云IoT平台做设备管理和数据上报设备管理这块我用的是阿里云IoT平台。每台猫脸识别设备作为一个设备接入用MQTT协议上报识别结果和设备状态。阿里云IoT的Android SDK封装了MQTT连接、Topic订阅、设备影子等功能接入比较方便。具体来说设备启动后先连MQTT订阅一个自定义Topic用于接收云端指令比如调整识别阈值。识别到猫之后把猫的ID、置信度、时间戳打包成JSON通过另一个Topic上报。阿里云IoT平台可以配置规则引擎把数据转发到函数计算或者直接存到表格存储里。这里有个细节阿里云IoT的MQTT连接默认是TLS加密的证书需要提前烧录到设备里。如果设备没有安全存储区证书可能被提取。对于家用场景问题不大但如果是商用产品建议用阿里云IoT的安全芯片方案把密钥存在硬件里。{ cat_id: cat_003, confidence: 0.87, timestamp: 1699876543210, device_id: catcam_001 }4.3 弱网环境下的推流稳定性处理家用WiFi的信号覆盖往往不理想猫脸识别设备可能放在阳台或者厨房角落网络抖动很常见。WebRTC本身有拥塞控制GCC但在弱网下码率会降得很低画面糊成一片。我的做法是加一个自适应策略当检测到网络RTT超过300ms或者丢包率超过10%时主动降低视频分辨率和帧率把带宽让给DataChannel的识别结果上报。具体实现上WebRTC的PeerConnection有getStats()接口可以拿到当前的RTT、丢包率、可用带宽估计。我每5秒轮询一次根据结果动态调整VideoCapturer的分辨率。实测下来在信号强度-75dBm的环境下720p降到360p后推流基本能保持稳定识别结果上报的延迟从平均800ms降到200ms左右。另外识别结果上报不能只依赖WebRTC的DataChannel因为DataChannel在极端弱网下可能完全断掉。我加了一个本地缓存队列识别结果先存SQLite后台线程定期尝试上报上报成功后再删除。这样即使网络断了半小时恢复后数据也不会丢。5. 实测中的误报与漏报排查实录5.1 猫脸检测框抖动导致的识别跳变部署第一版之后我发现一个奇怪的现象猫明明没动但识别结果在两只猫之间反复跳变。排查后发现是检测框抖动导致的。YOLO每帧输出的检测框坐标有细微差异如果猫脸刚好处于两个特征库向量的边界区域框的微小偏移就会导致提取的特征向量变化进而导致识别结果跳变。解决方案是加一个跟踪器。我用的是简单的IOU跟踪如果当前帧的检测框和上一帧的IOU大于0.5就认为是同一个目标复用上一帧的识别结果同时把当前帧的特征向量和上一帧做滑动平均。这样识别结果稳定了很多跳变基本消失。跟踪器的代码不复杂核心就是维护一个目标列表每帧做IOU匹配匹配不上的新建目标连续多帧匹配不上的删除目标。5.2 夜间红外模式下的图像偏色问题很多USB摄像头带红外补光夜间会自动切换到黑白模式。但Android的Camera2 API在红外模式下白平衡会偏有时候整个画面偏绿或者偏紫。这会导致检测模型失效因为训练数据里没有这种偏色样本。我的处理方式是在预处理阶段加一个自动白平衡校正。简单来说计算图像的R、G、B通道均值然后按比例调整使三个通道均值接近。这个方法对偏色有不错的校正效果而且计算量很小一帧640x480的图大概1ms就能处理完。校正后再送检测模型夜间召回率从不到30%提升到了75%左右。当然更好的做法是在训练数据里加入红外偏色样本做增强。我后来补采了一些夜间红外图做了色调偏移增强模型在红外模式下的表现更稳了。5.3 多猫同框时的ID分配冲突家里有两只以上猫的时候多猫同框是常态。如果两只猫同时出现在画面里检测框会有两个识别线程需要分别提取特征并匹配。问题在于如果两只猫外观相似比如都是橘猫特征库里的向量距离可能都很近导致ID分配错误。我的解决方案是引入空间约束同一帧内如果两个检测框的IOU大于0.3认为它们不可能属于同一只猫强制分配不同的ID。另外如果两只猫的特征向量距离都低于阈值但其中一只猫的位置和上一帧某只猫的位置更接近就优先分配那个ID。这个逻辑用简单的匈牙利算法做匹配就行不需要太复杂。实测下来这个策略把多猫同框的ID分配准确率从约70%提升到了92%以上。剩下的错误主要出现在两只猫交叉走过的瞬间这个目前还没有特别好的解决办法只能靠提高帧率来减少交叉时间。6. 模型量化与端侧部署的实操细节6.1 从浮点到INT8的量化校准集构建TFLite的INT8量化需要校准集校准集的质量直接决定量化后的精度损失。我一开始随便抽了100张训练图做校准结果量化后识别准确率掉了15个百分点。后来分析发现校准集必须覆盖实际部署场景的数据分布包括不同光照、不同角度、不同猫的姿态。我最终的校准集是300张图按场景分层采样白天正面30%、白天侧面20%、夜间红外20%、遮挡15%、俯视15%。这样量化后的精度损失控制在3个百分点以内。校准集的构建花了我大概半天时间但非常值得。量化命令用的是TFLiteConverter的post-training量化converter tf.lite.TFLiteConverter.from_saved_model(saved_model_dir) converter.optimizations [tf.lite.Optimize.DEFAULT] converter.representative_dataset representative_data_gen converter.target_spec.supported_ops [tf.lite.OpsSet.TFLITE_BUILTINS_INT8] converter.inference_input_type tf.uint8 converter.inference_output_type tf.uint8 tflite_quant_model converter.convert()6.2 量化后阈值重校准的必要性前面提过量化会缩小类间距离但具体缩小多少需要实测。我的做法是量化后重新跑一遍验证集画ROC曲线找新的最优阈值。实测下来浮点模型的最优阈值是0.62量化后变成了0.55。如果沿用0.62误拒绝率会从8%飙升到25%以上。另外量化后的特征向量分布会偏移L2归一化之后虽然还是在单位超球面上但向量之间的角度关系变了。所以特征库里的向量必须用同一个量化模型重新提取不能混用浮点和量化的特征向量。6.3 模型文件在Android assets中的加载优化TFLite模型文件放在assets里首次加载需要拷贝到内部存储或者用MemoryFile映射。如果模型文件超过2MB用AssetManager直接openFd()可能会失败因为某些Android版本对assets文件大小有限制。我的做法是把模型文件放在res/raw或者直接用FileInputStream读取然后写入ByteBuffer。加载时间方面RK3399上加载一个2MB的模型大约需要200ms这个时间在App启动时可以接受。但如果每次推理都重新加载那就完全不可行了。所以Interpreter实例必须全局单例生命周期跟App一致。还有一个内存映射的技巧用MappedByteBuffer加载模型文件可以避免把整个文件读进堆内存减少OOM风险。TFLite的Interpreter构造函数支持MappedByteBuffer参数推荐用这种方式。private MappedByteBuffer loadModelFile(Context context, String modelName) throws IOException { AssetFileDescriptor fileDescriptor context.getAssets().openFd(modelName); FileInputStream inputStream new FileInputStream(fileDescriptor.getFileDescriptor()); FileChannel fileChannel inputStream.getChannel(); long startOffset fileDescriptor.getStartOffset(); long declaredLength fileDescriptor.getDeclaredLength(); return fileChannel.map(FileChannel.MapMode.READ_ONLY, startOffset, declaredLength); }7. 从原型到产品化还需要补哪些课7.1 功耗控制与散热设计原型阶段用开发板跑插着电源不觉得有问题。但要做成产品功耗和散热是绕不开的。RK3399满载跑推理加WebRTC编码功耗大概在5W左右芯片表面温度能到70度以上。如果放在封闭外壳里温度会更高可能导致降频甚至死机。我的优化措施有几个一是把推理帧率从12fps降到8fps检测和识别交替跑奇数帧检测偶数帧识别功耗降了约30%二是加了一个小风扇温度控制在55度以下三是用铝制外壳做散热片外壳本身就能散热。最终整机功耗稳定在3.5W左右对于常电设备来说可以接受。7.2 OTA升级与模型热更新产品化之后模型更新是必须的。总不能每换一次模型就让用户把设备寄回来。我的方案是用阿里云IoT的OTA功能把新模型文件推送到设备设备收到后校验MD5然后替换本地模型文件重启推理服务。这里有个细节模型文件替换时不能直接覆盖正在使用的文件否则Interpreter会崩溃。正确做法是先下载到临时目录校验通过后关闭当前Interpreter替换文件再重新初始化Interpreter。整个过程大概需要500ms期间视频流会中断但可以接受。OTA的固件包大小也要控制。TFLite模型文件加上App本身整个OTA包大概15MB左右。在WiFi环境下下载很快但如果用4G网络建议做差分升级只推送模型文件的差异部分。7.3 隐私与数据安全的底线处理猫脸识别虽然不涉及人脸但视频流本身可能包含家庭环境信息。我的原则是视频流只在局域网内传输不上公网识别结果猫的ID和置信度可以上云但不包含任何图像数据。WebRTC的推流地址用内网IP如果确实需要远程查看通过阿里云IoT的安全隧道做端口转发不直接暴露设备端口。另外特征库数据存在本地SQLite里做了加密处理。加密密钥存在Android Keystore里即使设备被物理接触提取特征库数据也需要破解Keystore成本较高。对于家用场景这个安全级别足够了。最后分享一个我在实际部署中总结的小技巧猫脸识别设备的摄像头角度非常关键。俯视角度摄像头向下倾斜30到45度比平视角度效果好得多因为猫在进食或活动时通常是低头或平躺状态俯视能拍到更正面的猫脸。我一开始把摄像头装在墙上平视检测率只有60%左右改成俯视后直接提升到90%以上。这个调整不需要改任何代码纯粹是物理安装的优化但效果立竿见影。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻