今年四月份接了一个智慧工地的项目,甲方是某建筑公司的信息化负责人。合同里有一条:人脸识别模块必须在三天内跑通 demo,否则整个项目延期罚款。
我当时觉得三天足够了。百度人脸离线SDK的集成文档我看过,官方示例工程直接能跑,理论上复制粘贴半天就能搞定。
结果第一天下午,我就卡在授权文件上,一直到凌晨都没解决。
第一天下午:授权文件怎么放都不对
百度人脸离线SDK的授权机制是这样的:先在控制台创建一个应用,填写包名和打包签名的MD5值,然后下载一个授权文件,放到项目的assets目录里,代码里初始化的时候引用这个文件名。
听起来简单对吧?我照做了。在build.gradle里配好abiFilters,把.so和.aar丢进libs,assets里放了授权文件,代码里写了初始化:
FaceSDKManager.getInstance().initialize( context, "idl-license", // ← 这里埋了坑 "com.example.myapp", new IInitCallback() { @Override public void onInitSuccess() { Log.d("FaceSDK", "初始化成功"); } @Override public void onInitFailure(int errCode, String errMsg) { Log.e("FaceSDK", "初始化失败:" + errMsg); } } );初始化报了"license校验失败"。
我排查了三个小时。包名对了,签名MD5对了,文件路径对了,文件名也对着呢——idl-license.face-android,在assets目录下,一眼就能看到。
最后发现,控制台下载的授权文件确实是idl-license.face-android,但我在初始化代码里写的是idl-license,少了.face-android后缀。百度SDK的内部逻辑是严格匹配文件名的,差一个字符就过不了。
正确的写法应该是:
FaceSDKManager.getInstance().initialize( context, "idl-license.face-android", // 必须带后缀! "com.example.myapp", new IInitCallback() { ... } );这个坑让我从下午三点折腾到凌晨一点。找到问题的时候,我在办公室里骂了一句脏话,然后给甲方发了一条微信:"明天上午给 demo,今晚我通宵。"
第二天上午:跑通了,但活体检测黑屏
授权文件改对之后,初始化成功,采集界面也能打开。但活体检测的预览画面是黑的——有UI,有提示文字,就是看不到摄像头画面。
我第一反应是权限问题。Android 6.0以上需要动态申请相机权限,我在Activity里加了申请逻辑:
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, 100); }用户授权之后还是黑屏。
然后怀疑是摄像头被占用。工地用的平板是定制品,自带了一个系统级的人脸识别服务,占用了前置摄像头。我停用那个服务之后,画面出来了。
但新问题又来了:活体检测一直采集不到合格的人脸,提示"请正对屏幕",用户明明已经正对屏幕了。
查了百度SDK的质量校验逻辑,才知道它默认的光照阈值比较严格。百度SDK的质量等级分三档:QUALITY_NORMAL(正常)、QUALITY_LOW(低)、QUALITY_CUSTOM(自定义)。默认是QUALITY_NORMAL,光照最小值40、最大值255,模糊度不超过0.6,遮挡不超过0.3。
工地现场的光线不均匀,有的地方逆光,有的地方偏暗,用户站在门口的时候,光照值经常在30左右,直接触发"不合格"。我把质量等级改成了QUALITY_LOW:
FaceConfig config = FaceSDKManager.getInstance().getFaceConfig(); config.setQualityLevel(FaceConfig.QUALITY_LOW); // 放宽质量校验活体检测终于能通过了。但这个改动有代价——低质量等级下,照片攻击的通过率会提高。我跟甲方报备了这个风险,甲方说:"先让工人能用,安全的事后面再说。"
第二天下午:包名改了,授权废了
甲方的测试环境是一个内部签名,正式上线要用另一个签名。我手贱在gradle里把applicationId改了一下,想着只是测试,应该没问题。
结果授权又失败了。
百度人脸SDK的授权是严格绑定包名和签名MD5的,改任何一个都要重新申请授权文件。我在控制台重新申请,提交之后发现授权不是即时生效的,要等几分钟到十几分钟。那十几分钟里,我坐在电脑前刷了无数次页面,感觉自己像个傻子。
这件事给我一个教训:包名和签名一旦确定,打死都不要改。如果项目有多个环境(开发、测试、生产),最好一开始就申请多套授权,而不是用一个授权来回切换。
第三天:甲方来验收,我手心全是汗
第三天上午,甲方派了一个技术员来现场验收。我演示了完整的流程:打开APP、活体检测、人脸比对、结果显示。全流程跑下来大概两三秒,技术员点头说"还行"。
然后他提了一个我没想到的问题:"如果工人戴了安全帽,会不会影响识别?"
我当场愣住。百度的离线采集SDK只负责采集和活体检测,1:1或1:N的比对需要自己实现。我之前的方案是把人脸特征值传到云端做比对,但工地现场的网络信号不稳定,有时候根本没有网。
我跟甲方说:"这个需求得加几天,离线比对我还没做。"
甲方脸沉了一下,然后说:"合同里写的是人脸识别,不是人脸采集。你只做了采集,没做识别,这不算完成。"
我无话可说。确实,我之前的理解是"SDK提供采集+活体,比对走云端API",但甲方要的是完全离线——数据不出设备,网络断了也能用。
第四到第五天:临时补了一个本地比对方案
我紧急调整了方案:用百度SDK采集完人脸特征值之后,本地存一个特征值库,比对的时候直接在本地的float[]数组里做余弦相似度计算。
采集代码是这样的,百度SDK的活体检测回调里返回FaceInfo,里面包含了特征值数组:
@Override public void onLivenessCompletion(FaceStatusEnum status, String message, int hashCode, byte[] feature, FaceInfo faceInfo) { if (status == FaceStatusEnum.OK) { // feature 就是 512 维 float[] 转成的 byte[] float[] faceFeature = bytesToFloatArray(feature); // 存入本地,key 是工人 ID localFaceDB.put(workerId, faceFeature); } }比对的时候,把当前采集的特征值和库里每个特征值算余弦相似度:
public float cosineSimilarity(float[] a, float[] b) { float dot = 0, normA = 0, normB = 0; for (int i = 0; i < a.length; i++) { dot += a[i] * b[i]; normA += a[i] * a[i]; normB += b[i] * b[i]; } return (float) (dot / (Math.sqrt(normA) * Math.sqrt(normB))); }这个方案简单粗暴,但有几个问题:
第一,特征值库大了之后,全量加载到内存会OOM。当时工地注册人数不到一百,暂时没这个问题,但甲方说后续要扩展到全公司几千人。我跟甲方说:"现在这个方案只能撑到几百人,后续要换存储层。"
第二,1:N检索的复杂度是O(N),千人级的时候每次检索要遍历全部特征值。百度官方给出的参考值是RK3288上万级检索不到二十毫秒,但我这个简易实现用的是纯Java循环,没有优化,几百人的时候就要几十到几百毫秒。
第三,特征值没有加密,存在/data/data/包名/files/目录下。我跟甲方说这个风险的时候,甲方说:"工地平板都是我们公司管控的,不会root。"我勉强接受了这个说法,但心里不踏实。
我现在回头看,犯了三个错误
第一个错误:低估了授权机制的严格性。我以为授权文件就是一张"许可证",放进去就行。实际上它是和设备指纹绑定的,包名、签名、文件名、后缀,任何一个不对就过不了。这个坑花了我整整一天。
第二个错误:没提前问清楚"离线"的定义。甲方说的"离线"是"数据不出设备",我理解的是"采集在本地,比对走云端"。这个理解偏差导致第三天差点验收失败,后面两天 frantic 补救。
第三个错误:没在目标设备上提前测试。我在自己的开发机上跑得通,就以为现场设备也没问题。结果目标平板的摄像头被系统服务占用、光照条件不达标、低端处理器跑活体算法掉帧——这些问题在开发机上根本遇不到。
如果重来一次,我会怎么做
第一天上午只做一件事:申请授权文件,核对三遍包名和签名。不要急着写代码,授权文件没搞定,代码写得再漂亮也跑不起来。文件名后缀.face-android一定要写对,最好复制粘贴,不要手打。
第一天下午在目标设备上跑官方示例工程。不要在自己的开发机上跑,直接去现场拿甲方的设备。摄像头、光照、处理器性能、系统服务占用,这些只有目标设备才能暴露。
第二天上午明确"离线"的边界。是采集离线?比对离线?还是全流程离线?甲方嘴里说的"离线"和你脑子里想的"离线"可能完全是两回事。
第二天下午设计存储层。即使当前只有一百人,也要按一千人设计。特征值怎么存、怎么索引、怎么加密、怎么备份,这些在一开始就定好,比后面返工便宜得多。SQLite存元数据,特征值用内存映射文件,这种组合是我后来才学会的,如果当时就用上,能省不少事。
交付之后,甲方给我介绍了第二个项目
虽然中间有波折,但甲方对我最后的处理还算满意。验收的时候,我主动把三个遗留风险写进了验收文档:光照阈值放宽的安全隐患、本地特征值库未加密、千人级之后的性能瓶颈。
甲方说:"别的供应商都是藏着掖着,你直接把问题摊开说,反而让我放心。"
两个月后,这个甲方给我介绍了他们集团另一个分公司,项目规模是这个的三倍。
这次我学乖了,签合同之前先问了三个问题:目标设备型号、网络环境、"离线"的具体定义。
相关文章专栏
专栏一: