用 JavaScript 做司机端证件识别?我差点被产品经理送走
去年双11前两周,我们司机端产品组突然接到一个“紧急需求”:上线司机上传身份证、驾驶证自动识别功能。PM 原话是:“技术上应该不难吧?不就是拍个照,系统读一下文字?”
我当场差点把咖啡喷到显示器上。
作为滴滴干了四年后端的老兵,我主要负责司机端的核心业务逻辑——订单匹配、接单状态机、服务分计算这些“稳如老狗”的模块。平时写 Java、调 Dubbo、怼 MySQL 是日常,但突然让我搞计算机视觉(CV)?还是用 JavaScript?我心里一万个问号。
但没办法,项目 deadline 就在眼前,后端人力紧张,而前端小哥刚好对 TensorFlow.js 有点兴趣。于是我们俩一合计:硬着头皮上!
为啥不用 Python + OCR API?
说实话,第一反应是直接调阿里云或百度的 OCR 接口。稳定、准确、还能开票报销。但 PM 甩出两个现实问题:
- 成本敏感:每天几十万次证件上传,按次计费吃不消;
- 隐私合规:司机证件属于敏感信息,不能随便传第三方。
那就只能自研了。考虑到司机 App 是混合开发(React Native + WebView),最终方案定为:前端用 JavaScript 在浏览器/WebView 内做轻量级 CV 预处理 + 文字区域检测,后端用 Java 调 Tesseract 做高精度识别。这样既能减少网络传输,又能规避隐私风险。
但第一步,得让前端能“看到”证件在哪。
从零搭建:用 JS 实现证件区域检测
我们没打算从头训练一个目标检测模型(那得标注几千张图,我可不想周末加班)。于是盯上了开源社区的轻量级方案 —— TensorFlow.js + MobileNet-SSD。
安装依赖很简单:
npm install @tensorflow/tfjs @tensorflow-models/coco-ssd
但坑马上来了:coco-ssd 默认只支持 COCO 数据集的 80 类物体,根本没有“身份证”、“驾驶证”这种类别!😅
自己造数据,自己打标签
我和前端小哥花了三天,从内部测试账号里扒了 500 多张真实司机上传的证件照片(脱敏后),用 LabelImg 手动标出 bounding box。然后用 Roboflow 做数据增强:旋转、亮度调整、加噪——毕竟司机拍照环境千奇百怪,有的在地下车库,有的对着反光玻璃拍。
接着,用 TensorFlow 的 tfjs-converter 把训练好的模型(我们在 Colab 上用 EfficientDet-D0 微调)转成 Web 可用的 JSON 格式:
tensorflowjs_converter \
--input_format=tf_saved_model \
--output_format=tfjs_graph_model \
saved_model/ \
web_model/
部署到 CDN 后,前端加载模型代码长这样:
import * as tf from '@tensorflow/tfjs';
import * as cocoSsd from '@tensorflow-models/coco-ssd';
// 加载自定义模型(非官方coco)
const model = await tf.loadGraphModel('https://cdn.xxx.com/id-card/model.json');
// 输入图片元素
const img = document.getElementById('upload-img');
const predictions = await model.executeAsync(tf.browser.fromPixels(img));
// 解析输出:[x, y, width, height, classId, score]
const boxes = parseOutput(predictions);
📌 注意:这里没用
cocoSsd.load(),因为那是封装好的 COCO 模型。我们直接loadGraphModel加载自定义权重。
算法选择:为什么不用 YOLO?
其实我私底下试过 YOLOv5 的 JS 版本,但发现:
- 模型体积太大(>15MB),WebView 加载慢;
- 在低端安卓机上推理速度掉到 3fps,司机等得想卸载 App;
- 内存占用高,容易触发 RN 的 OOM。
最后选了 EfficientDet-D0 + TensorFlow.js 的 WebGL 后端,模型压缩后仅 4.2MB,在骁龙 660 机型上也能跑 12fps。关键配置如下:
| 配置项 | 值 | 说明 |
|---|---|---|
| 输入尺寸 | 320x320 | 平衡精度与速度 |
| NMS 阈值 | 0.4 | 避免同一证件被框多次 |
| 置信度阈值 | 0.65 | 低于此值视为无效 |
| 后端 | webgl |
必须开启 GPU 加速 |
开启 WebGL 的代码必须放在最前面:
tf.setBackend('webgl').then(() => {
console.log('Using WebGL backend');
});
否则默认用 CPU,慢到怀疑人生。
踩过的坑:那些让我想砸电脑的瞬间
坑 1:图片 EXIF 方向错乱
司机用 iPhone 拍照,上传后图片在 WebView 里旋转了 90 度。结果模型检测框歪得离谱。
解决方案:用 exif-js 读取 Orientation,前端先矫正再送入模型。
EXIF.getData(file, function() {
const orientation = EXIF.getTag(this, "Orientation");
// 根据 orientation 旋转 canvas...
});
坑 2:模型在 iOS 和 Android 表现不一致
iOS Safari 对 WebGL 支持更好,Android WebView 则要看厂商(华为还行,小米某些机型直接黑屏)。
最后我们加了兜底策略:如果检测失败或耗时 >2s,自动降级为“手动框选区域”,再传给后端识别。
坑 3:算法准确率 vs 用户体验
初期模型召回率只有 78%,经常漏检。PM 急得天天站我工位后面“关怀”。
后来我们加了一个 trick:如果检测不到证件,就用边缘检测(Canny)+ 轮廓分析找矩形区域,作为备选 ROI(Region of Interest)。虽然不准,但至少能引导用户重新拍照。
JS 实现轮廓近似(简化版):
function findRectContours(imageData) {
// 转灰度 → 高斯模糊 → Canny 边缘 → 找轮廓
const edges = cannyEdgeDetection(imageData);
const contours = findContours(edges);
return contours
.filter(c => isRectangle(c)) // 判断是否接近矩形
.sort((a, b) => area(b) - area(a))[0]; // 取最大面积
}
这招让整体成功率从 78% 提升到 92%,PM 终于不再半夜微信轰炸我了。
效果与反思
上线一个月后,数据显示:
- 87% 的证件能被前端准确框出;
- 平均识别耗时从 2.1s 降到 0.8s(减少后端压力);
- 用户重拍率下降 40%。
最重要的是,没再因为这个需求和 PM 对线 😅。
但我也深刻体会到:在移动端做 CV,算法不是唯一瓶颈,工程适配才是地狱。你得考虑机型碎片化、内存限制、用户网络、甚至手机壳反光……
现在回看,如果重来一次,我会更早引入 ONNX Runtime Web 或 WebAssembly 版 OpenCV,可能比 TF.js 更灵活。不过话说回来,在滴滴这种强业务驱动的公司,能快速交付、稳定运行,比追求 SOTA(State-of-the-Art)更重要。
毕竟,司机师傅们不在乎你用了 YOLO 还是 SSD,他们只关心——能不能一次过审,早点接单赚钱。
给想搞 CV 的前端/全栈同学几点建议
- 别迷信端侧 AI:除非场景简单、数据量小,否则优先考虑“端侧预处理 + 云侧精识”;
- 模型越小越好:4MB 是移动端的心理阈值,超过就得有充分理由;
- 兜底策略必须有:AI 不是 100% 可靠,用户体验要靠“人机协作”兜住;
- 别自己造轮子:像证件检测这种通用场景,阿里、腾讯都有 SDK,评估成本后再决定自研。
最后,如果你也在上海租房、加班改需求、被 PM 虐得想转行……欢迎来陆家嘴附近喝杯瑞幸,聊聊怎么用 JS 让世界少一点 Bug,多一点接单成功的提示音 🚗💨

评论 0