前阵子接了个挺有意思的需求:某餐饮信息化团队想给食堂的自动回收台加一套视觉方案,能自动识别顾客还回来的餐具到底是什么——碗、餐盘、小碟子、筷子、勺子、杯子,甚至汤煲。最开始以为这就是个普通的图像分类任务,调个模型、标点数据、跑通demo就交差了。真正做下来才发现,餐具种类识别系统踩的坑,真的一点都不比人脸识别少。
先说结论,这套系统的核心价值不在“能分清碗和盘子”这个动作本身,而在于它能把一个高频、低技术含量但极其耗人力的环节自动化。餐厅回收处的工作人员每天弯腰分类几千件餐具,自动结算台需要按餐具类型计价,后厨盘点要统计各类餐具的损耗——这些场景都需要一个能在真实环境下稳定运行的识别器,而不是只在论文数据集上刷精度的模型。
这篇文章不聊虚的,我会把整个项目从需求分析、数据标注、模型选型、训练调参、部署翻车、接口设计到迭代闭环的真实链路完整记录下来。内容偏向工程落地,适合正在做类似视觉识别项目的工程师,也适合拿这类题目做课程设计的学生参考。
1. 为什么餐具识别值得单独做一套系统
先别急着打开标注工具标数据,我们得先搞清楚一个问题:餐具识别和通用物体识别到底有什么不一样?想不清楚这个,后面做出来的系统大概率只能在演示时好用,一换环境就废。
1.1 三个真实场景,三个不同需求
我接触的需求主要来自三个方向,每个方向对系统的约束都不一样。
第一个是食堂回收台分类。顾客吃完饭把托盘往回收台一放,系统自动识别出托盘上有哪些餐具、各是什么种类,然后通过屏幕提示或者机械臂分拣,把碗、盘、筷勺分开。这个场景的特点是:视角固定但环境杂乱,餐具经常堆叠、互相遮挡,而且上面可能残留菜汤、油渍、食物残渣。速度要求高,最好单帧推理在50毫秒以内,准确率要能扛住实际使用中被遮挡的情况。
第二个是自动结算台。按餐具类型计价,三种碗价格不一样,大餐盘和小碟子价格也不一样。这个场景的特点是:餐具摆放相对规整,一般单件或者少量几件放进识别区,环境光可控。但要求的是“细分类”——同样都是碗,得能分清楚是面碗还是米饭碗,这对类别边界的要求就高了。
第三个是后厨餐具盘点。每过一段时间,后厨需要知道库房里各类餐具还剩多少、破损率如何。这个场景通常用手机或者固定摄像头拍一张俯视图,系统输出每种餐具的数量统计。听起来最简单,其实最难,因为库房里的餐具是紧密堆叠的,一个碗只露出碗沿一条边,要靠局部特征猜出整件类别。
这三个场景如果不加区分,塞进同一个模型,效果一定两边都不讨好。所以我建议任何人在做同类项目时,第一步不是选模型,而是先把应用场景钉死。我们的方案是:核心检测模型统一,但在后处理阶段针对场景做不同的规则适配。
1.2 易混淆度远超想象:为什么通用模型在这里会失效
通用的图像分类模型,比如在ImageNet上预训练的那批模型,对猫狗、飞机汽车这类外形差异巨大的物体非常友好。但餐具这个类别有意思的地方在于:不同种类之间,外形差异有时候比同一种类内部差异还小。
举个具体例子。俯视视角下,一个普通的白色圆碗和一个白色圆盘子的边缘轮廓几乎都是圆形,区别只在直径比例和边缘高度。光线不足的时候,模型很容易把碗认成盘子。再比如勺子和叉子,在低分辨率下就是一根带柄的细长条,柄的末端形状稍微模糊一点就会分错。还有透明玻璃杯和白色瓷杯,在黑白边缘特征上高度相似,纯靠RGB图像真的很难区分。
这就是为什么不能直接拿一个现成的分类模型套上去的原因。餐具识别需要定制数据增强策略、定制类别体系、甚至定制后处理规则。后面每一章都会针对这些难点展开。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据集搭建:餐具数据比想象中难搞
很多项目死在第一步不是因为模型不行,而是因为数据没准备好。餐具识别尤其明显——网上现成的餐具数据集几乎没有,就算有,类别体系也对不上国内餐饮的习惯。所以数据这块只能自己动手。
2.1 类别体系怎么定:既要够用,又不能太细
类别设计是整个系统的地基,定错了后面全得返工。我们最终采用了12个类别的方案,按真实使用频率排序如下:
| 类别 | 英文标签 | 典型特征 | 识别难点 |
|---|---|---|---|
| 米饭碗 | rice_bowl | 直径10-12cm,较深 | 与汤碗、面碗界限模糊 |
| 汤碗 | soup_bowl | 直径12-14cm,较浅 | 边缘形状接近饭碗 |
| 面碗 | noodle_bowl | 直径15cm以上,深度大 | 俯视角度下像大盘子 |
| 餐盘 | dinner_plate | 直径25cm以上,浅平 | 与碟子只在尺寸上有差异 |
| 小碟子 | small_plate | 直径14cm以下 | 容易与浅碗混淆 |
| 茶杯 | tea_cup | 带把手,小体积 | 把手容易被遮挡 |
| 玻璃杯 | glass_cup | 透明,高瘦 | 透明材质特征提取困难 |
| 筷子 | chopsticks | 细长,成对出现 | 目标小,容易被忽略 |
| 勺子 | spoon | 柄末端为勺形 | 与叉子外形相似 |
| 叉子 | fork | 柄末端为齿形 | 低分辨率下和勺子难分 |
| 汤勺 | ladle | 长柄大勺头 | 柄长导致检测框细长 |
| 托盘 | tray | 大面积浅色矩形 | 占据画面主体影响其他检测 |
这里有个很重要的设计决策:杯子类没有细分瓷杯和玻璃杯,因为热菜窗口和饮料区用的杯子材质不同,但系统只需要知道“这是个杯子”就能完成计价和分拣任务。细分越多,标注成本越高,模型错误率也越高,对业务价值提升却不大。
2.2 采集和标注:不是拍几张照片就完事
数据采集我们用了大概两周时间,跑了两家食堂、一个中央厨房、一个餐具批发仓库。每类餐具采集了500到800张有效图片,总数接近7000张原始图,扩充后参与训练的图片在25000张左右。采集时特别留意了几个维度:
- 角度:每件餐具至少拍平视、俯视、45度斜视三个角度。
- 状态:干净餐具、带食物残渣的餐具、带水渍的餐具、堆叠状态的餐具都要覆盖。
- 光线:自然光、暖色灯光、白色灯光、背光情况各拍一批。
- 组合:单个餐具和多个餐具混放都要有,模拟真实回收场景。
标注的时候我们用的是通用的目标检测标注工具,标注框统一为矩形框。这里我要强调一个容易被忽视的细节:标注框的紧致度。很多人标注喜欢把整个物体连同周围背景都框进去,这在视觉上省事,但对模型训练是灾难。框太大,会让模型学到背景特征;框太小,又会截断关键边缘。我们的标准是框要贴着餐具外轮廓,最多留5个像素的余量。
还有一类特殊的图:一个碗半藏在另一个碗下面,只露出三分之一边缘。这种情况我建议标注时不要强行标成“未知”,而是直接标成它所属的类别,让模型自己去学习这种极端遮挡下的可用特征。实测下来,模型在这类图上的表现反而会变好。
2.3 数据增强:针对餐具特点做战术级调整
通用增强手段——随机翻转、随机裁剪、颜色抖动——当然要用,但针对餐具识别,还有几个增强策略效果特别明显:
模糊模拟。真实场景里顾客端托盘走过,摄像头拍到的画面多少有点动态模糊。训练时随机对部分图片做高斯模糊和运动模糊,能显著提升模型在真实回收台的鲁棒性。
颜色随机化。食堂灯光偏暖黄色,白瓷碗在画面里会偏黄。训练时有意识地随机调整色温和饱和度,让模型不要太依赖“白色”这个颜色特征,否则换一个灯光环境准确率就会暴跌。
CutMix混合。把一张图里的碗和另一张图里的餐盘拼在一起训练,这种强增强手段在餐具数据集上出奇地好用,因为它能强迫模型关注形状结构而不是整图的背景信息。我们试过不同比例,最终CutMix的概率定在0.3左右,太高反而会让模型学不到稳定的局部特征。
3. 模型选型实测:分类和检测的路线之争
做识别系统,第一个要拍板的技术路线问题就是:到底用纯分类模型配合图像裁剪,还是直接用目标检测模型?
3.1 两条路线的适用边界
如果场景是固定拍摄区域、餐具单件摆放,比如自动结算台,用“传统分类模型加预处理”就够了。先通过背景差分或图像分割找到餐具区域,裁剪出来,送进分类器,输出类别。这条路简单、快、好调,但只适用于受控场景。
食堂回收台那种自由场景就行不通了。托盘上多件餐具互相遮挡,餐具边缘和背景混在一起,你没法定点裁剪,必须先检测出每个餐具的位置和类别边界,再交给分类器。说白了,这种场景必须走检测路线:YOLO、Faster R-CNN或者DETR这些检测框架。
我们最终的方案是“端到端检测模型直接输出类别”,不再单独接一个分类器。原因很直接:在餐具这种同类差异小、异类差异也小的数据上,检测模型在训练时可以同时优化位置和类别,特征学习得比“先检测框再分类”的两段式更充分。实验测下来,两段式的餐具类别准确率大约在91%,而端到端检测模型能做到95%上下,差距基本都来自小目标餐具。
3.2 四组实测对比:轻量级模型的真实表现
在内部测试集上(含各场景约2000张图),我们对比了四个主流轻量级方案:
| 模型 | 输入分辨率 | 参数量 | 平均精度mAP@0.5 | 单帧推理耗时(CPU) | 单帧推理耗时(GPU) |
|---|---|---|---|---|---|
| MobileNetV3-SSD | 320x320 | 5.6M | 78.4% | 88ms | 11ms |
| ShuffleNetV2-SSD | 320x320 | 4.4M | 74.1% | 92ms | 12ms |
| EfficientNet-B0-SSD | 320x320 | 8.1M | 81.2% | 105ms | 13ms |
| YOLOv8n | 640x640 | 3.2M | 84.6% | 210ms | 16ms |
这个表里有几个值得细说的点。
YOLOv8n虽然基础速度不是最快的,但在餐具这个任务上精度领先明显。原因在于它在训练过程中使用了更丰富的增强策略,对小目标的检测能力比传统SSD结构强很多。筷子、勺子这类细长目标,SSD系列的默认锚框很难匹配好,而YOLOv8n的anchor-free设计天然对这类目标友好。
EfficientNet-B0-SSD精度不错,但有个致命问题:它训练时对输入分辨率特别敏感。我们试过把输入从320提到416,精度能涨到83%,可推理耗时也涨了将近50%,在边缘设备上扛不住。
另外我们试过更大的YOLOv8s,精度确实涨了大概2个百分点,但代价是GPU推理时间翻倍。考虑到最终要部署到Jetson系列设备上,我们最终锁定YOLOv8n作为主干模型,输入分辨率设为640x640,在勉强能接受的延迟预算内拿到最好的精度。
3.3 为什么不直接选大模型
有些做项目的人习惯性堆大模型,觉得精度就是一切。但餐具识别这个场景有个特殊性:分类错误带来的损失是“可控”的。一个碗被识别成汤碗,最坏的结果是结算价格差几块钱,或者回收分拣时放错桶,人工纠正成本很低。为这点误差去跑一个几十GB的大模型,在食堂现场根本部署不了,完全没必要。
真要提精度,更划算的路子是用大模型做蒸馏,把知识迁移给小模型。我们用过一版教师模型蒸馏的方案,让YOLOv8n学习大模型在相同输入下的软标签输出。蒸馏在餐具类别上能稳定涨1到1.5个点,这个性价比比换大模型高得多。
4. 训练调参实战:从82%到94%的四个关键改动
模型定下来之后,训练过程才是真正磨人的阶段。我们的初始baseline精度只有82%,在食堂现场实测还会更低。经过几轮针对性调整,最终把测试集精度稳定在94%以上。下面这四个改动,每个都让精度有明显变化。
4.1 数据划分和类别平衡策略
餐具数据天然存在类别不均衡。筷子、盘子数量多到爆炸,而汤勺、叉子每类只有几百张。如果用原始分布训练,模型会对高频类别产生严重偏好,把勺子误判成筷子。
我们采用的策略是按类别重采样。每个epoch里,各类别最多只取统一数量的图片参与训练,多出来的不参与本轮,下轮再随机抽。这种做法比简单的过采样更稳,不会让模型反复看同样几张图导致过拟合。最终每类每轮控制在400张左右,训练集总量约为4800张,验证集按场景分布独立切出300张。
4.2 训练配置细节:一份可以照抄的参数表
下面是最终用于复现的完整训练配置,用YOLOv8框架自带的训练脚本直接就能跑:
python复制# 训练配置摘要
model: yolov8n.pt
datasets: dishes.yaml # 含12个类别,路径指向train/val目录
epochs: 200
batch: 32
imgsz: 640
device: 0
# 优化器与学习率
optimizer: AdamW
lr0: 0.0005
lrf: 0.01
momentum: 0.937
weight_decay: 0.0005
warmup_epochs: 3
# 数据增强开关
hsv_h: 0.015
hsv_s: 0.7
hsv_v: 0.4
degrees: 15
translate: 0.1
scale: 0.5
fliplr: 0.5
mosaic: 1.0
mixup: 0.3
几个参数说明一下。学习率初始值我们没有用默认的0.01,而是降到了0.0005。因为餐具数据集量不大,默认学习率会让loss在前期震荡得很厉害,模型不容易收敛。配合AdamW优化器和小batch,整体训练更稳定。
degrees: 15表示训练时最多随机旋转15度,这个不能调太大。餐具是有方向感的,筷子横着和竖着差别不大,但碗的俯视边缘和斜视边缘如果旋转太狠,模型会被搞得无所适从,旋转超过20度后精度反而下降。
4.3 损失函数和标签平滑:让模型不那么“自信”
检测模型的损失函数默认是分类损失加回归损失。在餐具这个任务上,我们要额外注意分类分支的“过度自信”问题。
具体现象是:模型对碗和盘子这种近邻类别,输出概率往往会烧到0.99以上,哪怕它是错的。这会让后处理的置信度阈值失去意义。解决办法是开启标签平滑,把标签从1变为0.9,同时把其他类别的标签从0变为一个小值。YOLOv8里设置cls_smooth参数,我们最终用0.15。
另一个有效手段是给分类损失加上Focal Loss的权重系数。餐具数据里难点都集中在少数困难样本上——半遮挡的碗、叠在一起的筷子。Focal Loss会让模型把学习重心放在这些“没把握的样本”上,而不是反复学习那些已经识别得很好的大类。在YOLOv8里对应的是cls_focal参数,经验值是0.7到1.0之间。
4.4 过拟合的几道防线
餐具数据量不大,200个epoch跑到后期,验证集loss很容易不降反升。我们用了三道防线:
- 早停:验证集精度连续20个epoch不增长就停止训练,我们把patience设为20。
- EMA指数滑动平均:对模型权重做指数滑动平均,用滑动平均后的权重做推理。这个小技巧通常能稳定提升1个点以上,而且几乎不消耗额外资源。
- DropPath:在模型的深层部分插入轻量随机深度,训练时随机跳过部分模块,让模型不至于太依赖某几个特定的特征通道。
这三道防线加完之后,验证集精度比不加的时候大约高了1.5个点,并且模型在真实场景中的抖动也明显减少。
5. 部署现场:被光照、角度和遮挡轮流教育
训练集精度94%,听起来挺好了对吧?第一次到食堂现场实测,直接被打脸——真实环境下识别准确率掉到了70%出头。这一章记录的,就是模型从实验室到真实世界的各种翻车实录。
5.1 第一杀手:不是模型弱,是光线邪门
食堂的灯光基本是暖黄色,而且是自上而下打的。白色瓷碗、白瓷盘在暖黄光下全部变成淡黄色,模型在训练时学到的“白瓷反光特征”全部失效。玻璃杯更惨,在灯光直射情况下产生强烈反光,透明特征完全被破坏,模型干脆把玻璃杯当成了异物。
这个问题的解法不只在数据增强端,部署端也要配合。我们在摄像头前方加了一层偏振片,很大程度上抑制了瓷器和玻璃的镜面反射。同时把模型推理前的图像做一次白平衡归一化,把画面色温拉回中性,让输入图像尽量接近训练数据的颜色分布。
这一步改造之后,现场准确率直接回升到85%左右。所以说,遇到识别率低,先别急着骂模型,先把图像采集端管好。
5.2 俯视角度下的碗盘之争:轮到了高度信息
第二波翻车发生在回收台俯视视角固定之后。俯视角度下,碗和盘子的轮廓都是圆,直径比例有时也很难判断——碗沿往里收,盘子沿直直往外延,但隔了五六米的摄像头,这些差异在像素层面可能只有一两行像素的区别。
我们用了两个办法解决。
第一个是给模型增加额外的“边缘高度”输入。具体做法是,在推理前用传统图像处理算法提取餐具的梯度轮廓,合成一张边缘高度图,把它作为额外通道拼接到原始图像上。模型能看到RGB之外的第三个维度,碗盘区分一下子就准了很多。
第二个办法更简单粗暴:在硬件层面调整摄像头安装高度和角度,让它不要完全垂直俯拍,而是保持15度左右的倾斜。就这15度,碗的立体感和盘子的平面感就出来了。这两个方案叠加之后,碗和盘子的混淆率从12%降到了3%以内。
5.3 透明玻璃杯和瓷杯:不可见特征的终极难题
透明物体本身就难识别,玻璃杯在图像里大部分像素跟背景融为一体,模型能看到的只有高光和边缘。瓷杯则正好相反,全身都有清晰纹理。在低分辨率下,玻璃杯的高光区域有时会被模型当成瓷杯的白釉。
我们的解法分两步。第一步,把玻璃杯类别的特征权重单独调大,允许模型对它多给一点注意力。第二步,也是在实践中最重要的——后处理规则:当模型对玻璃杯的置信度在0.4以下,但对瓷杯的置信度在0.6以上时,结合杯子的检测框纵横比来做裁决。如果检测框又高又窄,偏向玻璃杯;如果又矮又圆,偏向瓷杯。这类基于物理尺寸的先验规则,在模型犹豫不决时往往是最可靠的兜底。
5.4 重叠和遮挡:用时间换空间的投票机制
最严重的场景是回收台上堆了一大摞餐具,碗叠碗、盘压盘,检测框重叠率达到50%以上。这时候单帧检测的置信度普遍掉到0.3以下,完全不可信。
我们用的是多帧时间序列投票:连续采集5到10帧图像,对同一个检测框(通过IoU和位置追踪关联),统计所有帧的类别输出,取票数最多的类别作为最终结果。在静态摆放场景下,这个方法极其有效,准确率能拉回15到20个百分点。但在动态场景——比如传送带上餐具还在移动——需要把帧之间用光流做对齐,复杂度会高不少。
这里有个工程上的取舍:投票机制需要缓存多帧,输出延迟会增加几百毫秒。对回收分拣这种非实时场景完全没问题,但如果要做实时结算,就得把投票窗口压缩到3帧以内,或者干脆放弃复杂遮挡场景的识别,让系统提示人工处理。
5.5 推理耗时的实测数据
部署到Jetson Orin Nano上之后,实测单帧推理耗时在15到18毫秒之间,加上前后处理和投票机制,整体吞吐能达到每秒20帧以上。这个性能在回收台场景下非常充裕。如果部署到更便宜的树莓派级别设备,只跑单帧不跑投票,也能勉强维持每秒5帧,但实用性就打折扣了。
6. 从Demo到落地:接口设计、误报兜底和迭代闭环
模型精度上来了、部署跑通了,但离“系统能用”还有最后一段路。这段路不写代码的人往往看不到,恰恰又是最容易出问题的地方。
6.1 识别服务的接口设计
我们最终把识别能力封装成了一个独立的HTTP服务,方便对接各种业务系统。接口设计遵循一个原则:识别服务只输出“感知结果”,不掺入任何业务逻辑。
请求很简单,传一张JPEG图像过去,返回检测到的所有目标:
json复制POST /api/detect
{
"image_base64": "/9j/4AAQSkZJRgABAQAAAQ==",
"scenario": "recycle"
}
{
"code": 0,
"results": [
{
"bbox": [120, 80, 260, 310],
"label": "noodle_bowl",
"confidence": 0.92
},
{
"bbox": [300, 100, 420, 210],
"label": "chopsticks",
"confidence": 0.88
}
],
"fps": 18.5
}
scenario字段用于加载不同的后处理规则:recycle模式加载折叠遮挡投票逻辑,checkout模式加载计价优先级规则,inventory模式加载数量统计逻辑。这样一套模型服务,三个场景共用,只在配置层面做区分。
6.2 置信度兜底:识别系统最大的敌人是“半懂不懂”
我特别想强调一个经验:识别系统里,置信度低的错误比置信度高的错误好处理一万倍。真正令人头疼的是模型瞎猜还特别自信——把勺子硬说成叉子,置信度给到0.97。
所以我们在服务层加了硬规则:任何目标置信度低于0.35,直接标记为“unknown”,不参与下游业务。回收台碰到unknown目标就往人工处理通道送,结算台碰到unknown就提醒顾客或者服务员人工确认。你别小看这个“拒绝识别”的开关,它能让系统从“总是给出错误答案”变成“偶尔说不知道”,体验完全不一样。
6.3 难例回流:系统能否越用越聪明,决定项目长期价值
模型上线之后,最怕的是版本一锤子买卖。餐具种类会变、食堂菜品会变、甚至灯光改造了都会导致精度下降。我们的经验是把系统设计成一个持续迭代的闭环:
- 线上每次识别都记录带置信度的原始图片和结果。
- 每周抽取置信度落0.35到0.75区间的样本,交给标注人员快速标注。
- 累计到500张新增图片后,做一轮增量训练,只训练最后一两个层。
- 在验证集上跑回归测试,确认精度不降后发布新版本。
这套闭环跑起来之后,系统会越用越贴合自己现场的环境。我们经历过一次食堂更换全部日光灯,就是靠回流样本在一周内把识别率重新拉回到正常水平的。
6.4 关于整个项目的一些个人体会
项目做到收尾阶段,回头看最初的预期,最大的教训是:识别系统的成败,80%的功夫其实在模型之外。数据集的细致程度、后处理规则的场景适配、置信度阈值的调教、还有持续迭代的工程机制,这些看起来琐碎的事情,每一样都比单纯堆模型架构更能决定最终效果。
餐具识别这个任务本身不会是什么高深的前沿课题,但它把一个非常典型的“看似简单、实际处处是坑”的视觉落地过程完整地暴露出来了。如果你手上正在做类似的识别项目,与其纠结用什么最新的模型结构,不如先把训练和部署链路里那些不性感的细节抠透——收益比高得多。
