从大厂离职后,我在家撸了个计算机视觉产品
上个月还在工位上对着Jira看板发呆,现在坐在自家沙发上,左手咖啡右手Vim,窗外连个催需求的产品经理都没有——是的,我裸辞了。不是因为996(虽然那确实挺折磨人),而是想搞明白:我到底还想不想继续写代码?写什么代码?
结果刚休息两周,手就痒了。正好前同事甩给我一个私活:做个简单的图像识别工具,帮他们团队自动分类用户上传的证件照类型。听起来不难?但一上手才发现,这玩意儿从前端到算法再到后端部署,坑比我家猫掉的毛还多。
这事儿是怎么开始的
起因特别典型:产品经理说“我们要提升用户体验”,于是就想让用户上传身份证、护照、驾驶证时,系统能自动识别类型,不用手动选下拉框。听上去很合理对吧?但等我问清楚需求细节,差点一口老血喷在键盘上:
- 要支持模糊、倾斜、部分遮挡的照片
- 识别速度要快,最好200ms内出结果
- 部署成本不能高,毕竟只是个辅助功能
- 最关键的是:下周就要上线!
看到最后一句,我仿佛又回到了大厂冲刺双11的日子——那种“今天必须上线,不然老板睡不着”的窒息感扑面而来。不过这次没人催我,反而是我自己较上劲了:既然要玩,那就玩透点。
算法选型:轻量级才是王道
一开始我想上YOLOv8,毕竟在大厂时动不动就堆SOTA模型。但冷静下来一想:这是个小工具,不是自动驾驶。用户传张图,你后端跑个500MB的模型,服务器分分钟爆炸。
于是我翻了翻Hugging Face和TensorFlow Hub,最后锁定了 MobileNetV2 + 自定义分类头 的组合。为什么?
- 模型小(<15MB)
- 推理快(CPU上也能跑)
- 在ImageNet上表现不错,迁移学习效果有保障
训练数据嘛,用的是公开的证件照数据集 + 自己爬的一些样本,总共不到3000张。别笑,大厂项目动辄百万级数据,但小项目讲究的是“够用就好”。我还特意加了数据增强:旋转、亮度调整、加噪声——毕竟用户可能在地铁里用手机拍,光线堪忧。
训练过程倒是顺利,PyTorch Lightning搭个脚本,两小时搞定。准确率最终停在92.3%,虽然不算惊艳,但对产品来说够用了。毕竟用户如果传错图,还能手动纠正,又不是医疗诊断。
# 训练脚本核心片段(简化版)
import torch
from torchvision.models import mobilenet_v2
model = mobilenet_v2(pretrained=True)
model.classifier[1] = torch.nn.Linear(model.classifier[1].in_features, 3) # 3类:身份证/护照/驾照
# 数据增强
transform = transforms.Compose([
transforms.RandomRotation(10),
transforms.ColorJitter(brightness=0.2, contrast=0.2),
transforms.ToTensor(),
])
前端:别让算法白跑
很多人以为CV项目就是调模型,其实前端体验才是用户感知最强的部分。我用了Vue3 + TypeScript(别问,问就是前同事技术栈),重点解决了三个问题:
- 图片预处理:用户上传后,前端先压缩到800px宽以内,避免传个4K照片把后端干趴。
- 加载反馈:识别过程加个“正在分析证件…”的提示,而不是干等。
- 错误兜底:如果API返回“无法识别”,前端自动弹出选择框,让用户手动选。
最骚的操作是:我把模型输出的概率也传给了前端。比如 {"type": "passport", "confidence": 0.87}。这样当置信度低于0.7时,前端可以高亮提示“系统不太确定,建议您确认一下”。
// 前端调用示例
const handleUpload = async (file: File) => {
const compressed = await compressImage(file, 800); // 自研压缩函数
const res = await api.detectDocumentType(compressed);
if (res.confidence < 0.7) {
showWarning('识别不太确定,请手动确认');
}
setDetectedType(res.type);
};
后端:别让运维半夜打电话骂你
模型训好了,前端也ready了,该部署了。这时候我突然想起在大厂被线上事故支配的恐惧——有一次因为没设超时,一个CV服务把整个K8s集群拖垮,运维大哥直接冲进会议室:“谁写的代码?出来聊聊人生。”
这次我学乖了。后端用FastAPI(轻量、自动生成文档),部署在Docker里,关键配置如下:
| 配置项 | 值 | 说明 |
|---|---|---|
| Gunicorn workers | 2 | CPU密集型任务,不宜过多 |
| 请求超时 | 3秒 | 超过就fail fast |
| 最大请求体 | 5MB | 防止用户传视频 |
| 模型缓存 | 内存加载一次 | 避免每次推理都读磁盘 |
最关键是加了 熔断机制:如果连续5次识别失败,自动降级为“请手动选择”。宁可功能弱一点,也不能让整个系统雪崩。
# FastAPI 路由片段
@app.post("/detect")
async def detect(file: UploadFile):
if file.size > 5 * 1024 * 1024:
raise HTTPException(400, "文件太大")
try:
img = preprocess(await file.read())
result = model.predict(img) # 已缓存在内存
return {"type": result.label, "confidence": float(result.conf)}
except Exception as e:
logger.error(f"识别失败: {e}")
return {"error": "识别异常,请手动选择", "fallback": True}
产品思维:技术再牛,用户不会用等于零
做完这些,我以为万事大吉。结果前同事试用后第一句话是:“能不能加个‘重新上传’按钮?我现在点错了得刷新页面。”
我:……(内心OS:产品经理果然永远有新需求)
但转念一想,这恰恰说明技术人容易陷入“实现即完成”的误区。用户根本不在乎你用了MobileNet还是ResNet,他们只关心:能不能三秒内搞定,出错了能不能轻松挽回。
于是我又加了:
- 上传区域支持拖拽+点击
- 识别结果旁加“换一张”按钮
- 手动选择后自动记住偏好(localStorage)
这些改动代码量不大,但体验提升巨大。这也让我反思在大厂时做的那些“高大上”项目——有时候为了追新算法,反而忽略了最基本的交互闭环。
总结:小而美,才是自由的味道
折腾一周,这个小工具终于上线了。性能数据如下:
| 指标 | 数值 |
|---|---|
| 平均响应时间 | 180ms |
| 准确率(线上) | 89.5% |
| 服务器成本 | $5/月(一台t3.micro) |
虽然比不上大厂动辄千万DAU的项目,但看着自己从算法到前端再到部署全流程搞定,心里特别踏实。更重要的是,没人催我改需求,没人半夜call我救火,我可以慢慢打磨每一个细节。
裸辞这两个月,我越来越觉得:程序员的价值不在于写了多少行代码,而在于解决了什么真实问题。哪怕只是帮用户少点一次下拉框。
如果你也在思考职业方向,不妨试试接个小项目。不用多复杂,但要完整走通“产品-前端-算法-后端”全链路。你会发现,很多在大厂被分工割裂的能力,其实在小场景里能重新拼成一幅完整的图。
对了,代码我放GitHub了(链接略),欢迎提issue——反正我现在闲着,改bug的速度可能比大厂PR review还快 😏
最后,祝大家都能找到让自己愿意主动敲键盘的事。哪怕是在家,穿着睡衣,用Vim写Python。

评论 0