从大厂离职后,我在家撸了个计算机视觉产品

郑明
2025-12-24 02:40
阅读 1893

上个月还在工位上对着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(别问,问就是前同事技术栈),重点解决了三个问题:

  1. 图片预处理:用户上传后,前端先压缩到800px宽以内,避免传个4K照片把后端干趴。
  2. 加载反馈:识别过程加个“正在分析证件…”的提示,而不是干等。
  3. 错误兜底:如果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

最热最新
暂无评论
郑明Lv.1
0
影响力
0
文章
0
粉丝