给自家猫主子做一扇“认脸才开门”的猫门这个念头我在脑子里转了很久。最开始想的是买现成的宠物门结果发现市面上大多数是靠 RFID 项圈或者红外感应——项圈猫不爱戴红外是只猫走过就开邻居家的橘猫照样能进来蹭饭。后来琢磨着把摄像头、识别模型和电控锁串起来做成一套只认自家猫的门禁系统。这次我选的路子是用 Android 主板当边缘计算盒子本地跑猫脸识别模型识别通过再驱动门锁整条链路不依赖云端断网也能用。整套东西涉及 Android 相机采集、TFLite 推理、人脸识别那套流水线改造、MQTT 与串口通信、以及 7x24 运行的稳定性打磨工作量不算小但每一步都能拆开单独调试。这篇文章会把我在这个项目里踩过的坑、参数怎么定的、代码怎么写的一次性讲清楚适合有 Android 基础、想入门边缘 AI 落地的开发者也适合想给自己家宠物做点小玩意的硬件爱好者。哪怕你只是想搞明白“猫脸识别到底和人脸识别差在哪”看完也能有个清晰的判断。1. 方案定调为什么把猫脸识别放到 Android 板子上跑1.1 三条技术路线的横向对比做宠物识别门禁摆在面前的路其实就三条纯 MCU 云端 API、自建云服务识别、以及 Android 本地边缘推理。我三条都试过或者评估过最后选 Android 边缘推理不是因为它最便宜而是它在“离线可用”和“开发效率”这两点上综合得分最高。方案算力来源单机物料成本断网可用识别延迟开发周期隐私性MCU 云端 API云端服务器低80~150 元否500ms~2s1~2 周差图像要上传自建云服务识别自建服务器中200~400 元否300ms~1s3~6 周中仍要传图Android 本地推理板载 GPU/NPU中高400~900 元是80~300ms2~4 周好图像不出设备纯 MCU 那条路我一开始很心动一块 ESP32-CAM 才几十块但问题马上就来了图像要传到云端做识别家里的上行带宽和 Wi-Fi 稳定性直接把体验拖垮猫在门口站两秒没反应就走了而且每次识别失败都是一次“猫被关在门外”的糟糕体验。更麻烦的是延迟不可控路由一抖动就是好几秒。Android 板子虽然贵但它自带 Linux 内核、成熟的相机框架、GPU 和现成的推理运行时等于把最难的那部分基础设施白送给你了。还有一点容易被忽略Android 设备天然支持屏幕、扬声器、Wi-Fi、蓝牙、USB 外设你后期想加个“识别成功后播一句主人录的语音”或者“本地屏幕显示今天进出了几次”几乎不用改架构。MCU 要做这些就得再加一堆外设代码量反而上去了。1.2 猫脸和人脸到底差在哪别直接套现成模型这是整个项目里最关键的认知转变。我一开始想得很简单人脸识别那套 MTCNN ArcFace 不是现成的吗直接拿猫脸图喂进去不就行了实测下来准确率惨不忍睹原因有这么几条。第一是几何比例完全不同。人脸是竖长的椭圆形五官分布符合严格的三庭五眼猫脸是宽扁的倒三角眼睛几乎在同一水平线上且间距很大鼻子在正下方耳朵长在头顶两侧。用为人脸设计的 anchor 比例去做检测猫脸框要么框不全要么把整只猫头都框进去。第二是姿态自由度大得多。人会配合镜头猫不会它可能侧躺、仰头、只露半张脸、被自己的爪子挡着。而且猫的瞳孔在暗光下会放大到几乎占满眼球强光下缩成一条缝——同一个特征点在两种光照下长得完全不一样。第三是毛发纹理干扰。人脸的关键点靠皮肤边界和五官轮廓就能定猫脸的关键点周围全是毛发边缘模糊标注一致性很差这也是为什么公开的猫脸关键点数据集质量普遍一般。所以我的做法是关键点定义改成猫脸专用的 5 点——左眼中心、右眼中心、鼻头、左耳根、右耳根。这 5 个点在猫脸上相对稳定受毛发影响小而且足以支撑仿射变换做对齐。检测模型用的是一阶段目标检测器改造只保留“猫脸”一个类别不分类别、不加属性把问题简化到极致。1.3 端到端架构分层整套系统的分层我理成了四层理解了这个分层后面所有代码都能对上号。感知层就是摄像头和补光灯。摄像头负责出图如果要做防伪还得加红外或双目。补光灯很关键晚上猫脸全是噪点模型再强也白搭。边缘推理层是 Android 主板上的 App包含相机采集、图像预处理、检测、对齐、特征提取、比对决策这一整条流水线。这层是整个项目的核心也是最耗性能的地方。决策执行层负责把“识别通过”这个结果变成物理动作。我的做法是双通道主通道走 MQTT 下发到云端记录次通道走串口或蓝牙直接给下位机ESP32发指令开锁。为什么双通道后面第 4 章会详细说简单讲就是网络不可靠的时候本地串口能兜底不能让主子在门口干等。云平台层是可选的用来存进出记录、远程查看抓拍图、远程下发阈值。我个人的原则是识别逻辑必须在本地云端只做记录和配置绝不让云端参与实时决策。2. 硬件选型与开发环境的准备工作2.1 主板、摄像头、执行机构怎么挑主板这块我前后用过三种各自的体感差别很明显。主板类型典型型号推理速度112x112价格区间优点缺点旧安卓手机骁龙 855 级别15~30ms300~600 元便宜、性能强、自带电池散热差、长期插电有鼓包风险国产开发板RK3399 / RK358820~50ms400~1200 元接口丰富、可定制系统系统适配需要折腾高通开发板骁龙 6/8 系10~25ms800~2000 元NPU 支持好贵、开发门槛高如果只是自己家用、图省事旧安卓手机真的是性价比之王性能足够、系统成熟、连屏幕都省了。但要注意两个问题一是散热手机长期插电跑推理背板温度能到 60 度以上得加个小风扇或者铝制散热片二是电池长期满电插着对锂电池不友好理想情况是把电池拆掉直接供 5V不过这需要一点动手能力。摄像头我强烈建议用UVC 免驱 USB 摄像头不要一上来就搞 MIPI CSI。MIPI 的带宽和延迟确实更好但它需要厂商提供匹配的内核驱动换一块板子就可能不认调试周期能从一天变成一周。UVC 摄像头插上就能用720p 30fps 对猫脸识别完全够用——反正模型输入才 112x112你给 4K 也没用。提示选 UVC 摄像头时注意看是否支持免驱的 MJPEG 和 YUY2 两种格式。MJPEG 带宽占用低但解码要 CPU 开销YUY2 带宽高但免解码如果主板 USB 口带宽紧张优先 MJPEG。执行机构分两类。舵机适合轻负载的推拉门闩5V 供电用 PWM 控制角度简单但力气小电磁锁力气大、响应快但要 12V 独立供电得加继电器模块。我最后用的是 12V 电磁锁 继电器Android 只负责发一个高低电平信号具体驱动交给下位机。下位机建议加一块ESP32 或者 STM32 都行成本不到 30 块。它的作用有三个一是隔离电源锁的瞬时电流很大直接挂在 Android 板子的 USB 上容易把设备拉复位二是做 PWM 和限位检测三是在 Android 重启的几十秒里维持门锁的默认状态不至于门开着没人管。2.2 Android Studio 环境准备与踩坑点环境这块我列一下我实际的配置和几个容易卡住的地方。Android Studio 版本用最新的稳定版就行别追预览版。中文语言包可以装但对调试帮助不大命令行输出的日志还是英文我更建议直接用英文环境。SDK 版本compileSdk 用 34minSdk 建议 26 以上。低于 24 的话 Camera2 的一些行为和前台服务限制会让代码写得很别扭。NDK只有用 TensorFlow Lite 的 C API 或者自己写 JNI 预处理时才需要装纯 Java/Kotlin 调 TFLite 的 Java API 不需要。Gradle 版本冲突这是新手最容易卡住的地方。TFLite 的 AAR 对 Gradle 版本有要求如果报Unable to find suitable ...之类的错先把 Gradle Plugin 升到和 Android Studio 匹配的版本再检查repositories里有没有加mavenCentral()。无线调试板子装好之后基本不会再拆下来用 USB 调试很麻烦。Android 11 以上可以用无线调试配对或者用adb tcpip 5555切到网络模式。这一步能省掉大量插拔线的时间。自启动如果是定制板把 App 加到系统白名单避免被后台限制杀掉。普通手机需要在设置里手动允许自启动和后台运行。注意如果主板是定制的 Android 系统很多厂商会默认开启省电策略把后台服务冻住。要么把 App 做成系统应用要么在设置里逐项放开限制否则跑几个小时服务就没了。2.3 摄像头接入Camera2 和 CameraX 怎么选Camera2 是底层 API控制粒度细但代码量巨大光是写一个预览 取帧就要两三百行。CameraX 是 Jetpack 封装几行代码就能跑起来而且它的ImageAnalysis用例天然就是给分析场景设计的。我最终选了 CameraX理由是识别场景不需要手动控制曝光和对焦——反而自动的更好猫脸明暗变化大自动曝光比手动调参省心多了。val analysis ImageAnalysis.Builder() .setTargetResolution(Size(640, 480)) .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888) .build() analysis.setAnalyzer(inferenceExecutor) { imageProxy - try { val bitmap imageProxy.toBitmap() val rotated bitmap.rotate(imageProxy.imageInfo.rotationDegrees) pipeline.submit(rotated) } finally { imageProxy.close() // 这一行如果漏掉相机会直接卡死 } }这里有两个参数必须说清楚。分辨率 640x480是我的实测选择再低猫脸太小检测不到再高预处理开销上去了收益不明显。背压策略KEEP_ONLY_LATEST是核心它保证队列里永远只有最新一帧处理不过来就丢旧帧宁可丢帧也不能让延迟越积越大——门口排队等开门的时候延迟比帧率重要得多。OUTPUT_IMAGE_FORMAT_RGBA_8888这个设置也值得说。默认 CameraX 给你的是 YUV_420_888你得自己做 YUV 转 RGB而转换过程非常容易踩坑后面 5.2 会讲。直接要 RGBA 虽然多一次内部转换但省掉了手动处理 stride 和 pixelStride 的麻烦对稳定性更有利。3. 猫脸识别流水线从像素到身份3.1 检测先把猫脸框出来检测这一步我试过两种架构。一种是两阶段先用 YOLOv8n 检测“猫”这个整体再把猫头区域裁出来送进猫脸检测器。另一种是一阶段直接训练一个只输出猫脸框的检测器。两阶段的优点是整猫检测的预训练权重好找、数据好标缺点是链路长两只猫贴在一起的时候容易出问题。最终我用的是一阶段方案模型基于 YOLOv8n 改的输入 320x320输出一个类别。训练数据我大概标了 3000 张来源是自己拍的 公开猫脸数据集筛出来的。标注规则必须统一框要贴着两只眼睛和鼻头构成的三角形耳朵可以切一半胡须不要框进去。这条规则我一开始没定标了 500 张之后发现前后不一致只能全部返工这个亏吃一次就够了。训练时的 trick 有几个一是马赛克增强开到 0.5猫脸小目标多马赛克增强对小目标帮助很大二是关闭上下翻转猫脸上下翻转后耳朵跑到底下不符合先验三是色彩抖动开大一点因为实际部署时光照变化极大白天阳光直射和晚上补光灯完全是两个分布。实测下来320x320 输入的检测器在 RK3399 上单帧大约 25ms旧手机上 12ms 左右。加上后面的人脸识别模型整条流水线在 100ms 以内猫走到门口站一下就能识别完。3.2 对齐这一步最容易被跳过也最要命检测出框之后很多人会直接把框里的图缩放到 112x112 就送进识别模型。我一开始也这么干结果同一只猫换个姿势相似度能从 0.85 掉到 0.55直接跌破阈值。原因很简单猫的头部姿态变化会让眼睛、鼻子在图像里的相对位置发生剧烈位移模型没有空间不变性它学到的是“像素在哪个位置”而不是“这只猫的鼻子长什么样”。所以对齐这一步不能省。具体做法是用前面提到的 5 个关键点做仿射变换。关键点检测器我是在检测模型上加了一个分支共享主干输出 5 组坐标加置信度。然后定义一个标准模板# 112x112 输入下的猫脸标准模板坐标 TEMPLATE np.array([ [38.3, 51.7], # 左眼中心 [73.7, 51.7], # 右眼中心 [56.0, 71.7], # 鼻头 [25.0, 32.0], # 左耳根 [87.0, 32.0], # 右耳根 ], dtypenp.float32)用cv2.estimateAffinePartial2D求出变换矩阵把实际关键点映射到模板位置再做一次裁剪。之所以用Partial2D而不是完整的Affine2D是因为它只允许旋转、缩放和平移不允许剪切和拉伸能避免猫头侧偏时把脸拉变形。对齐做完之后同猫不同姿态的相似度能稳定在 0.75 以上这个提升非常显著。代价是每帧多了大概 3ms 的关键点检测和 1ms 的仿射变换完全值得。3.3 特征提取MobileFaceNet 微调与数据准备识别模型我用的 MobileFaceNet就是把 MobileNetV2 的瓶颈模块换成更适合人脸的结构输入 112x112输出 128 维特征向量。选它是因为模型只有 1M 出头的参数量在 Android 上跑得飞快而且社区里有大量成熟的训练脚本可以参考。训练数据是这件事成败的关键。每只猫我建议至少准备 200 张理想是 500 张覆盖这些维度正脸、左侧 45 度、右侧 45 度、俯视、仰视白天自然光、晚上开灯、晚上只开补光灯睁眼、眯眼、打哈欠、刚睡醒背景干净和背景杂乱数据不够怎么办我的做法是用同一只猫的视频抽帧抽的时候按相似度去重避免几百张几乎一样的图把数据分布搞偏。另外可以适度做数据增强随机亮度对比度扰动、小角度旋转±15 度以内、随机遮挡模拟被爪子挡脸但不要做水平翻转原因和检测那块一样。损失函数用 ArcFace超参上有个细节人脸识别常用的 m0.5 是在百万级身份数据上调出来的猫脸数据集通常只有几只到几十只身份、每类几百张这个 margin 太大训练很容易不收敛。我实测 m 在 0.3~0.35 比较稳缩放因子 s 保持 64 不变。如果发现 loss 在早期就震荡先把 m 降到 0.25 跑几十个 epoch 再往回升。导出 TFLite 的时候有个取舍converter tf.lite.TFLiteConverter.from_saved_model(catface_saved_model) converter.optimizations [tf.lite.Optimize.DEFAULT] # 方案一float16 量化体积减半精度几乎无损 converter.target_spec.supported_types [tf.float16] tflite_model converter.convert()我对比过三种量化方式float32模型 4.8MB精度最高但速度慢float16模型 2.4MB精度掉不到 0.5%速度提升 20% 左右这是我的首选INT8模型 1.2MB速度快一倍但需要代表性数据集做校准而且猫脸这种细粒度特征对量化误差很敏感我实测精度掉了 3 个百分点误识率上升明显最后没用。3.4 比对决策阈值不是拍脑袋定的特征向量出来后做 L2 归一化然后和库里已注册的向量算余弦相似度取最大值作为匹配分数。这里最关键的问题是阈值定多少。我的方法是准备两组相似度分布5000 对“同一只猫”的样本对5000 对“不同猫”的样本对。分别算相似度画成直方图两个分布的交叠区域就是误差区。选一个阈值让误接受率把别的猫认成自家猫和误拒绝率自家猫认不出来大致相等这就是等错误率点。阈值误接受率异猫放行误拒绝率自家猫被拒适用场景0.45约 3.2%约 1.1%只是记录进出不作为门禁0.50约 1.1%约 2.4%一般门禁可接受偶尔重试0.55约 0.3%约 5.8%安全优先猫要多站两秒0.60约 0.05%约 12%极高安全要求需配合多帧投票我最后定在0.52再加上一个多帧一致性投票连续 3 帧都判定为同一身份且相似度都超过阈值才真正触发开锁。这个机制把单帧的偶发误判压下去一个数量级代价是触发延迟增加大概 100ms猫根本感知不到。另外还有一个细节注册样本数量少的时候阈值要往上调。如果你只给猫录了 10 张图库里的特征分布覆盖不全同一只猫在不同角度下的相似度波动会很大。这种情况下我会把阈值临时提到 0.58并且建议用户补录到 30 张以上。4. Android 端工程落地推理、通信与联动4.1 TFLite 推理封装与加速开关TFLite 的 Java API 用起来很简单但加速配置有几个坑。GPU Delegate 能显著提速但首次加载有几百毫秒的初始化时间绝对不能放在主线程否则直接 ANR。另外 GPU Delegate 对某些算子不支持遇到不支持的算子会静默回退到 CPU你如果不看日志根本不知道自己在跑 CPU。class CatFaceInterpreter(context: Context) { private val interpreter: Interpreter init { val options Interpreter.Options().apply { numThreads 4 // 优先尝试 NNAPI失败再试 GPU try { addDelegate(NnApiDelegate()) } catch (e: Throwable) { Log.w(TAG, NNAPI 不可用回退 GPU, e) addDelegate(GpuDelegate()) } setUseXNNPACK(true) } val model FileUtil.loadMappedFile(context, catface_fp16.tflite) interpreter Interpreter(model, options) } fun infer(input: ByteBuffer, output: ArrayFloatArray) { interpreter.run(input, output) } }我实测下来同一个 float16 模型在 RK3399 上纯 CPU 4 线程 42msGPU Delegate 18msNNAPI 22ms。GPU 最快但兼容性最差NNAPI 居中CPU 最稳。我的策略是启动时按 NNAPI → GPU → CPU 的顺序探测探测失败就往下走并且把最终用的后端打日志出来方便排查。4.2 线程模型别让主线程背锅相机回调线程、推理线程、UI 线程必须是分开的而且推理线程只能有一个。为什么因为 GPU Delegate 不是线程安全的多个线程同时调用同一个 Interpreter 会直接崩溃而且多线程抢 GPU 反而更慢。我的线程模型是这样的相机分析器运行在一个单线程 Executor 上它只负责把 Bitmap 丢进一个有界 Channel推理线程从这个 Channel 取数据容量设成 1 并且策略是丢弃最旧的推理结果通过 Handler 抛回主线程更新 UI。private val frameChannel ChannelBitmap(capacity 1, onBufferOverflow BufferOverflow.DROP_OLDEST) // 推理循环 scope.launch(Dispatchers.Default) { for (frame in frameChannel) { val result pipeline.process(frame) frame.recycle() withContext(Dispatchers.Main) { ui.update(result) } } }这里frame.recycle()很重要Bitmap 不回收的话跑几个小时必 OOM。另外 Channel 的容量千万不要设大设成 5 或者 10你会看到延迟线性增长最后变成“猫走了三秒门才开”。4.3 注册流程怎么让主子配合你采 20 张图注册是最难的环节不是技术难是猫不配合。我的做法是分两步引导先用逗猫棒或者零食把猫引到摄像头前App 界面上放一个进度条显示已采样本数采满自动结束采的过程中实时显示检测框和当前质量评分。这样你能知道系统到底有没有在正常工作而不是傻等。采样的质量控制有三个硬指标不达标直接丢弃并且不计数清晰度用拉普拉斯算子算方差低于 100 判定为模糊通常是猫在动或者对焦没对上框面积猫脸框面积小于整图的 5% 就丢弃太远了特征提取不准关键点置信度5 个关键点平均置信度低于 0.8 丢弃通常是侧脸太偏或者被遮挡保存的时候图片放在私有目录里文件名带上时间戳和哈希。如果想把抓拍图导出给同事看效果用 FileProvider 生成一个content://的 URI 丢给分享组件不要直接给文件路径——Android 7.0 之后直接传file://路径会抛FileUriExposedException这个坑几乎每个人都会踩一次。provider android:nameandroidx.core.content.FileProvider android:authorities${applicationId}.fileprovider android:exportedfalse android:grantUriPermissionstrue meta-data android:nameandroid.support.FILE_PROVIDER_PATHS android:resourcexml/file_paths / /provider!-- res/xml/file_paths.xml -- paths files-path namecaptures pathcaptures/ / cache-path namelogs pathlogs/ / /paths4.4 决策与执行MQTT 加串口的双通道设计识别通过之后要开锁我把指令下发做成了双通道。主通道是 MQTT发给自建的 broker同时把这次开门记录写到云端备通道是串口或者 BLE直接连下位机。为什么非要双通道因为家庭 Wi-Fi 的稳定性远不如你想象。我实测过路由器重启、微波炉工作、手机大量下载的时候MQTT 的心跳都可能断。如果只有 MQTT 一条路网络一抖猫就被关在门外这个体验无法接受。串口是物理连接只要线没掉就一直可用。val payload JSONObject().apply { put(deviceId, deviceId) put(action, UNLOCK) put(identity, matchedCatId) put(score, score) put(ts, System.currentTimeMillis()) put(nonce, UUID.randomUUID().toString()) } // 计算 HMAC 签名防止重放 payload.put(sign, hmacSha256(payload.toString(), deviceSecret)) mqttClient.publish(catdoor/$deviceId/cmd, payload.toString().toByteArray(), 1, false) // 300ms 内没收到回执走串口兜底 handler.postDelayed({ if (!ackReceived) { serialPort.write(OPEN:$nonce\n.toByteArray()) } }, 300)指令里加nonce 和 HMAC 签名是必须的。MQTT 如果没有开 TLS报文在网络里是明文的别人伪造一条UNLOCK就能开门。做法是用设备密钥对关键字段做一次 HMAC-SHA256下位机自己验签验不过就丢弃。nonce 保证同一条指令不能被重放。4.5 7x24 运行的稳定性打磨这是最容易被忽视但最影响体验的部分。我的设备连续跑了一个月中间遇到各种问题总结下来做了这几件事。前台服务 START_STICKYAndroid 8.0 之后后台服务很容易被杀必须用前台服务并挂一个常驻通知。返回标志用START_STICKY被杀之后系统会尝试重建。看门狗启动一个定时任务每 10 秒检查一次最近一次推理的时间戳。如果超过 30 秒没有产出任何结果说明相机或推理线程卡住了直接重建整条流水线。温度控制读取/sys/class/thermal/thermal_zone0/temp超过 70 度把分析帧率从 15fps 降到 5fps低于 60 度再恢复。这一步能显著降低夏天死机概率。存储清理抓拍图存着存着就把空间占满了。我用 FileObserver 监听存储目录同时在每次启动时检查剩余空间低于 500MB 就清理 7 天前的抓拍图。注意别在主线程做遍历几千个文件的 stat 操作能让界面卡死。Wi-Fi 强度检查部署位置定下来之前读一次WifiManager.getConnectionInfo().getRssi()低于 -70dBm 就换个位置或者加个中继。信号弱的时候 MQTT 会频繁重连日志里全是错误排查起来很痛苦。熄屏与唤醒锁屏幕关掉省电但 CPU 不能睡。用PARTIAL_WAKE_LOCK保持 CPU 运行别用FULL_WAKE_LOCK那个会同时点亮屏幕非常耗电。5. 常见问题与排查实录5.1 识别不准按这个顺序排查遇到识别不准不要一上来就怀疑模型按下面的顺序往下查效率最高。现象可能原因验证方法处理方式白天正常晚上全废光照不足导致噪点保存晚上的抓拍图看画质加 850nm 补光灯或加宽动态摄像头正脸准侧脸不准训练数据侧脸太少统计训练集姿态分布补采 45 度、60 度侧脸样本自家猫有时识别不到关键点对齐失败打印关键点置信度置信度低于 0.8 直接丢弃整帧两只猫互相误认毛色花纹接近看两只猫的相似度分布采更多样本阈值上调到 0.58隔着玻璃识别不到玻璃反光用手机拍一张看反光摄像头贴紧玻璃加遮光罩猫刚睡醒识别不到眯眼导致关键点错位看那一帧的关键点可视化多帧投票靠其他帧兜底注意排查的时候一定要把中间结果可视化出来——画出检测框、标出关键点、打印相似度。靠猜是最慢的排查方式。5.2 图像方向和颜色不对这两个问题是 Camera 开发里最经典的坑我第一次做的时候全都踩了一遍。方向不对通常是预览是正的、喂给模型的图是横的。原因是你没有处理ImageInfo.getRotationDegrees()。CameraX 已经帮你算好了你只需要在拿到 Bitmap 之后按这个角度旋转一次。UVC 摄像头的旋转角度可能是 90 也可能是 270取决于它是正装还是倒装所以要允许配置。颜色发绿或者发紫这是 YUV 转 RGB 时的问题。YUV_420_888 有三个平面其中 U 和 V 平面是交错的pixelStride可能是 1 也可能是 2rowStride也可能大于宽度。如果你直接用宽度去遍历边界像素就会错位表现为图像边缘有一条绿色的边或者整体偏色。解决方案有两个一是直接用OUTPUT_IMAGE_FORMAT_RGBA_8888让系统转二是用 LibYUV 库它内部处理了各种 stride 情况比自己写可靠得多。5.3 内存、崩溃和 ANR跑了三天崩一次多半是内存问题。我的经验是三个地方必须盯死。Bitmap 复用绝对不要在每一帧里Bitmap.createBitmap那个开销和 GC 压力都非常大。用BitmapPool或者自己维护一个固定尺寸的缓冲池用完 recycle 归还。ImageProxy 必须 close这个在前面代码里强调了但还是要再说一遍。漏掉 close 的后果不是崩是相机直接停止出帧而且不报错你会以为是推理卡住了查半天查不出来。ANR主线程只做 UI 更新所有图像处理都在后台线程。特别注意 JSON 序列化、文件写入、SharedPreferences 首次加载这几个操作它们都可能耗时超过 100ms 从而触发 ANR。5.4 网络与指令可靠性的坑MQTT 的断线重连要开automaticReconnect但光开这个不够还要处理重连之后的订阅恢复。我用的是 cleanSessionfalse这样 broker 会保留会话重连之后自动恢复订阅省掉手动重订阅的代码。消息重复投递是另一个常见问题。QoS 1 保证至少一次意味着同一条开锁指令可能被下发两次。做法是在下位机侧维护一个最近 10 个 nonce 的缓存重复的直接丢弃。时间同步也值得提一句。如果下位机要做每天几点之后不开门这类策略两边时间必须一致Android 侧定期把时间同步给下位机避免它重启后时间归零。5.5 防伪照片和玩偶能不能骗过系统这是一个在实际使用中会遇到的问题。我做过测试拿手机的猫照片、平板显示的照片、甚至一个毛绒猫玩具放在摄像头前早期的系统确实被骗开过一次。所以如果这套系统真的是当门禁用防伪必须做。方案原理额外成本防伪强度实现难度微动检测连续帧关键点位移照片位移为 0无中低双目测距计算猫脸距离贴脸照片会露馅加一个摄像头高中红外补光屏幕和照片的红外反射特性不同红外灯 无 IR-cut 镜头中低纹理分析照片有摩尔纹和镜面反光无中低中ToF 深度直接获取深度图贵极高高我最后用的是微动检测 双目测距的组合。微动检测成本为零只要连续 5 帧的关键点平均位移超过 2 个像素就认为是活体静态照片直接过不了。双目测距用两个便宜摄像头标定一次之后算视差猫脸距离在 20~60cm 之间才放行。这两个加起来把误开的概率压到了可接受范围。6. 调优经验与后续可扩展的方向6.1 我总结的参数调优速查表调参这件事没有银弹但有几个关键旋钮改对了收益最大。我把它们整理成一张表方便对照调整。参数我的取值调大的影响调小的影响检测输入尺寸320x320小目标更准速度变慢速度快远距离猫脸漏检分析分辨率640x480远处猫脸更清楚CPU 上升CPU 低但小脸检测难相似度阈值0.52更安全误拒增加更宽松误接受增加多帧投票帧数3误判更低延迟增加响应快偶发误判变多分析帧率上限15fps触发更快发热增加省电猫要多站一会儿温度降频阈值70 度更凉快识别变慢性能全开有死机风险特征向量维度128区分度略好内存增加省内存细粒度区分变弱调参的优先级我建议是先把对齐打通再调阈值最后调性能参数。顺序反了会浪费大量时间——对齐没做好的时候你怎么调阈值都是在两个糟糕的分布之间找平衡点。6.2 这套东西还能往哪扩项目跑通之后能扩展的方向其实挺多的我列几个我觉得有意思并且实现成本不高的。多猫管理现在库里存的是单只猫的特征改成一人一档的注册表就行每只猫存多个特征向量不同角度的均值或者聚类中心识别时取最大相似度。要注意的是猫的数量超过 5 只之后误识率会上升需要把阈值相应上调。进食量统计如果同时接了喂食器可以记录每次进食的时间和持续时长设备上显示今天吃了 4 次共 12 分钟。数据攒一段时间之后能看出规律猫突然不吃东西是很重要的健康信号。健康监测雏形通过抓拍图做一些简单的行为分析比如连续多天在同一个位置趴着不动、进食次数下降这些都可以作为提醒。注意这里只是辅助观察任何健康判断都应该交给专业人士不要过度解读。远程查看把抓拍图压缩后同步到自建服务器手机上随时看。这里要注意隐私图片传输必须加密存储也要控制保留期。我个人在实际操作中的体会是这类项目的难点从来不在模型本身而在那些看起来不起眼的工程细节——相机有没有正确关闭、Bitmap 有没有回收、网络断了之后能不能兜底。模型准确率从 95% 提到 97% 要花几周但把泄漏的 ImageProxy 修掉只要一行代码收益却是从跑三天必崩变成稳定跑一个月。如果你是第一次做边缘 AI 落地我建议先把整条链路的稳定性跑通哪怕识别率只有 80%只要能连续跑一周不崩你就已经跨过了最难的那道坎剩下的都是在这个基础上慢慢调优的事。
