简介:基于YOLOv8的食品图像分割识别系统资源,面向正在深入学习深度学习和计算机视觉的开发者,解决食品图片中食材目标的分割、识别与分类问题。资源共25个文件,压缩包约3.25MB,包含19张示例图像、4个Python源码文件和1份Markdown说明文档,并提供Word版介绍;Python脚本覆盖训练、预测、验证和界面展示等关键环节,示例图可用于效果测试,说明文档帮助理解系统的工作流程与运行环境。已有39人学习下载。借助源码与配图,读者可了解YOLOv8端到端的训练方式、推理输出解析和识别结果可视化方法,也能看到食品外观、颜色、形状等复杂特征在模型中的处理思路。整体适合作为课程设计、入门实践或算法验证的轻量参考,同时YOLOv8的实时检测能力让示例在保持识别精度的同时具备快速响应优势,输出中可见标注框与类别标签;如需二次开发,可直接改写Python脚本并替换数据集,便于继续扩展到食品质量控制、热量评估、智能餐饮管理等应用场景。
1. 基于YOLOv8的食品图像分割识别:像素级理解每一道菜
食品图像分割识别不是普通目标检测加了个滤镜,它要求系统对画面里的每一样食材给出像素级边界——牛排的筋膜、红烧肉里的八角、沙拉碗里的樱桃番茄,都要用掩膜圈住。餐饮后厨的份量估价、食堂的浪费统计、外卖平台的自动菜品标注,这些场景都依赖分割而不是检测。YOLOv8自带的seg分支把实例分割的速度拉到了实时档位,让这类系统有机会从论文落进后厨。这篇笔记只讲落地路径:从图像标注到训练配置,从损失曲线读到端侧部署,每一步都按可复现的标准来写。适合刚拿到标注好的食品图片、准备训练自己数据集的工程师,也适合评估要不要往这个方向投入的团队。
2. 处理食品图像数据集:从labelme标注到YOLOv8-seg能吃的txt格式
2.1 数据从哪来:公开食品数据集与自采集的取舍
食品分割项目里,最初级的误区是以为可以拿Food-101、UECFood-256这类公开图片数据集直接训练。它们是分类数据集,只有整图标签,没有实例掩膜,YOLOv8-seg没法消费这种数据。拿来做预训练权重的前置蒸馏可以,直接作为训练数据不现实。实际项目里,数据只能靠两个来源:一是业内有公司开源过少量标注好的食品分割集,但类别体系通常和你的业务对不上;二是自采集后自己标注,绕不开。
我用得最多的是自采集加半自动标注的路子。先拍1000到2000张后厨和餐桌实拍图,覆盖目标场景里的固定菜品。采集时六个字:多角度、多光照。食品图像和工业零件最大的区别是形态变体极大——同一道番茄炒蛋,今天的鸡蛋碎一点、明天番茄出汁多一点,模型就会在边界上翻车。同一道菜在不同餐厅、不同盘型下的差异,也要在采集阶段尽量覆盖到。
图像采集完成后的第一步是清洗。模糊图、过曝图、别人拍摄的水印图全部删除;一组连拍只留一张。清洗完的图像按8:1:1切成train/val/test三个目录。目录结构后面直接进data.yaml,所以现在就要按YOLO惯例建:
food_dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ └── labels/ ├── train/ ├── val/ └── test/这里有个常见做法值得强调:images和labels分开放置,而不是像VOC那样把jpg和xml放同一个文件夹。YOLO系列默认的目录约定就是分开放,ultralytics在训练时会自动去labels目录下找同名txt。很多从VOC转过来的同学在这里习惯性把标注文件和图片放一起,导致训练时日志里反复出现“WARNING: image not found”之类的告警。
2.2 labelme标注:多边形要紧贴食物视觉边界
标注工具的选择上,labelme依然是社区闭环最完整的方案。它是JSON格式输出,配合后面的转换脚本很好处理。打开labelme后先把“自动保存”勾上,再开始逐张标注复数类别。标准是:每张图里所有可辨识的食物实例都要标注,包括被遮挡的;几个像素大小的装饰物可以跳过,但花生米、葱花这类对业务有意义的必须标。
标注的核心矛盾是食物接触问题。一份饭上盖着两块鸡排,鸡排和米饭有接触边,标注员常常为了“漂亮”把边界模糊成一个大块。我在实际项目中反复要求标注员:掩膜必须画到食物视觉边界上,不要怕重叠。YOLOv8-seg的掩膜本身允许实例间重叠,模型能学到“这是两块挨着的鸡排”;反而是把接触边糊掉,模型会认为是一个实例,推理时输出一团乱麻。
另一个labeledme的坑:shape type。默认多边形(polygon)没问题,但circle和rectangle类型YOLOv8的seg分支不识别,转换时要么跳过,要么做近似展开。我建议直接要求标注员统一用多边形画,不要使用矩形和圆形工具——矩形框对未来的目标检测任务有意义,对分割任务没有。
标注完成后的检查环节不能省。我一般让标注员自检一遍,再让另一个人抽检10%的图。检查点只有两个:类别标签有没有写错;掩膜是否贴合食物边缘。食物图像最容易被贴错的是“番茄”和“圣女果”这种大小维度上的类别,人工标注里这类错误比例能达到3%到5%,不抽检就等于把错误直接喂给了模型。
2.3 labelme JSON转YOLOv8-seg txt:转换脚本与边界处理
YOLOv8-seg的标签格式非常简洁:每张图片对应一个txt文件,每一行代表一个实例,第一列是类别编号,后面是归一化到[0,1]的多边形顶点坐标,按x1 y1 x2 y2的顺序排列。下面是我一直在用的转换脚本,已包含形状过滤和坐标钳位。
import json import os def convert_labelme_to_yolo(json_path, out_dir, class_map): with open(json_path, 'r', encoding='utf-8') as f: data = json.load(f) img_w = data['imageWidth'] img_h = data['imageHeight'] shapes = data['shapes'] lines = [] for shape in shapes: label = shape['label'] if label not in class_map: continue points = shape['points'] if len(points) < 3: continue # 过滤掉退化的多边形 norm_points = [] for pt in points: x = max(0.0, min(1.0, pt[0] / img_w)) y = max(0.0, min(1.0, pt[1] / img_h)) norm_points.append(f"{x:.6f} {y:.6f}") line = f"{class_map[label]} " + " ".join(norm_points) lines.append(line) txt_name = os.path.basename(json_path).replace('.json', '.txt') with open(os.path.join(out_dir, txt_name), 'w') as f: f.write("\n".join(lines)) # 类别映射,按你的业务调整 class_map = { 'steak': 0, 'rice': 1, 'egg': 2, } convert_labelme_to_yolo('path/to/annotation.json', '../labels/train', class_map)这段脚本的逻辑:逐行读取labelme导出的JSON,取出imageWidth和imageHeight做归一化分母;遍历所有标注形状,过滤掉类别不在映射表和点数少于3的形状;对每个顶点做[0,1]钳位,防止标注时手滑画出图像边界外的点。整个脚本跑完后,在labels目录下应该生成与图片同名的txt文件。
这里提醒一个经常翻车的点:labelme导出的points是像素坐标,前提是你在标注时没有对原图做过缩放。如果图像在标注前被resize过,必须用resize后的宽高做归一化分母,否则掩膜会整体错位,训练时loss直接飞到天上。另一个边界是重复多边形。同一个实例被标注了两次,转换脚本不会自动去重,需要靠抽检发现。
对于labelme里误用了rectangle类型标注的,可以在转换前做一次多边形近似。
def rect_to_poly(rect_points, n_points=16): """将labelme的矩形标注展开成近似多边形点序列""" x1, y1 = rect_points[0] x2, y2 = rect_points[1] x_min, x_max = sorted([x1, x2]) y_min, y_max = sorted([y1, y2]) xs = [x_min + (x_max - x_min) * i / n_points for i in range(n_points + 1)] ys = [y_min + (y_max - y_min) * i / n_points for i in range(n_points + 1)] poly = [] for x in xs: poly.append([x, y_min]) for y in ys: poly.append([x_max, y]) for x in reversed(xs): poly.append([x, y_max]) for y in reversed(ys): poly.append([x_min, y]) return poly这段函数在四条边上等距采样,生成一个近似圆的矩形多边形。它对后续训练的作用是让分割分支的监督信号不至于退化成“四角边框内全填充”。
2.4 数据增强与样本平衡:食品分割不能无脑开mosaic
数据增强的常规操作在YOLOv8里有mosaic、随机翻转、HSV扰动等。默认配置下mosaic=1.0即始终开启,但食品分割对mosaic很敏感。四张图拼接后,食物和背景的接缝处会产生伪边缘,模型容易把拼接缝当作食物边界学进去。我实测下来,mosaic一开,验证集分割边界的mAP50-95会掉1到2个点。因此我的做法是前50个epoch关掉mosaic,训练后期再开启,让模型先学会真实边界再接触拼接噪声,这个顺序比反向更稳定。
HSV扰动也要保守。工业零件的颜色通常稳定,HSV给得很激进也不会出大问题;食品恰恰相反,颜色是区分类别的重要线索——生熟、新鲜度、酱汁浓淡都靠颜色判断。我直接用默认值hsv_h=0.01, hsv_s=0.5, hsv_v=0.5不动,改大后会明显掉点。水平翻转可以开,但注意如果业务场景里有菜单上的“左右不对称菜”就别开全图翻转,局部翻转没事。
样本平衡上,餐厅场景下主食类别可能占到80%的图像像素,配菜只占3%。YOLOv8虽然有focal loss做类别均衡,但对这种极端长尾仍不理想。我常用的方法很简单:统计每类的样本图片数,对少类别的图片做3到5次复制,复制时同步复制标签txt。这不算优雅,但实测有效。训练时有随机增强在,复制不会直接引起过拟合,只会让模型看到少类别的频率更高。
3. 用YOLOv8训练自己的食品分割数据集:环境搭建、命令与参数
3.1 ubuntu20.04 CPU版YOLOv8环境搭建:最小可跑配置
在只有CPU的机器上搭建YOLOv8环境,关键是把torch的CPU版和GPU版区分开。很多人在这里踩坑:默认pip install torch下载的是CUDA版,装上也能跑,但每次import torch都会尝试初始化CUDA上下文,在无显卡的机器上报错。准确的做法是显式指定CPU版的PyTorch。
conda create -n yolov8 python=3.10 -y conda activate yolov8 pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu pip install ultralytics第一条命令创建独立环境,避免把系统Python搞乱;第二条是核心,--index-url指向PyTorch官方的CPU wheel仓库,pip会下载无CUDA依赖的版本;第三条安装ultralytics,它自带yolo CLI入口。装完验证一下:
python -c "import torch; print(torch.__version__); print(torch.cuda.is_available())"输出里如果torch.cuda.is_available()为False,且没有CUDA相关报错,CPU环境就正常了。我这里用的Python版本是3.10,ultralytics在3.8到3.11上表现都正常,按自己机器的既有环境来就行。
CPU训练这件事要说透:可以跑通,但很慢。我在GTX1660Ti上跑yolov8n-seg,batch=8,imgsz=640,一个step大概1.5秒;CPU版同样条件要慢10到20倍。所以CPU环境一般用来做三件事:验证数据格式是否正确、跑一个完整的epoch确认代码链路、调标注错误。真正的训练还是放到GPU机器或云服务器上,否则调一次参要等半天,没法干活。
3.2 训练命令与YOLOv8参数含义:一条命令完整解读
数据集目录和data.yaml准备好后,训练命令如下:
yolo segment train \ data=food.yaml \ model=yolov8n-seg.pt \ epochs=100 \ imgsz=640 \ batch=8 \ optimizer=AdamW \ lr0=0.001 \ patience=20 \ project=food_seg \ name=run1每个参数都值得说明。model=yolov8n-seg.pt是nano规模的预训练权重,文件小、速度最快,但精度有限;食品分割这类目标形状不规则的任务,我建议至少从yolov8s-seg.pt起步。优化器选AdamW而不是SGD,因为分割任务loss面更复杂,AdamW对学习率的敏感度低一些。lr0=0.001是AdamW的常见起点,如果损失曲线下降太慢可以提到0.01,但batch小于8时不要这么做。
data/food.yaml的内容如下:
path: ./food_dataset train: images/train val: images/val names: 0: steak 1: rice 2: egg这个yaml有几个硬性要求:path必须指向数据集根目录;train和val是相对path的相对路径;names词典的键必须从0开始连续编号。还有一个我踩过的坑:names里的类别名必须是ASCII字符串,不能包含中文。我在本地写过“番茄炒蛋”作为类别名,训练直接抛Unicode错误,改成拼音或者英文后正常。
训练开始后,终端会逐轮打印box_loss、seg_loss、cls_loss和验证集mAP。这里给两个关注点。第一,观察第一个epoch的输出是否和后的差距过大,如果第一个epoch的box_loss是8,后突然跳到0.5,说明数据加载正常;如果第一个epoch就直接发散(loss数值超过10倍),先检查标签txt的坐标有没有超出[0,1]范围。第二,val/seg_loss这个指标比总loss更值得盯,它直接反映分割分支的收敛状态。
3.3 损失函数曲线图:怎么判断模型真在学,而不是过拟合
YOLOv8训练过程中,ultralytics会在project/name目录下生成results.csv,每一行是一个epoch的汇总数据。把这个CSV画成曲线,能直观看出训练状态。常用的画法是用pandas加matplotlib,我习惯把训练集seg_loss和验证集seg_loss画在一张图里对比。
如果val/seg_loss在某个epoch之后开始缓慢回升,而train/seg_loss还在下降,就是典型的过拟合信号。此时先回调patience或者加大数据增强,不要急着换模型结构。如果val/seg_loss从头到尾都是高频震荡,没有整体下降趋势,多半是batch太小导致BN统计不稳定,或者学习率设高了。改成batch=16、lr0=0.0005再试一次。
另一个信号是loss曲线下降得很快但mAP曲线纹丝不动。这种情况在食品分割里常见,多是因为标注标签本身有错位,模型能从噪声中学到合理的box但学不到精确的掩膜,表现为seg_loss低但mAP50-95低。遇到这种情况,不要盲目调参,去检查标注质量。
3.4 处理类别不均衡:食品长尾分布的针对性调整
餐厅场景下的食品类别分布天然不均衡,主食、肉类样本多,配菜、调料样本少。过采样是第一步;第二步是调整每个类别的损失权重。YOLOv8的配置里没有直接的class_weight参数,但可以通过修改数据集的yaml文件间接实现。我在实际项目中会用另一个做法:把少类别样本的图片做mixup,即把两张少样本图按0.5的透明度叠在一起合成一张新图,这在视觉上模拟了食物相互遮盖的场景,同时增加少类别的训练频次。ultralytics的mixup参数默认是0.0,可以开到0.1到0.2之间进行试验。
所有调参动作都要遵循一个原则:一次只改一个变量。同时改学习率和数据增强方式,出了问题无法定位到底是哪个改动导致的。我这边的血泪经验是,把学习率、mosaic、mixup同时改掉,然后mAP下降了,整整浪费了两天去排查数据格式,最后发现是mixup开太高把食物纹理全搅碎了。
4. 模型评估与推理部署:从mAP到端侧实时分割
4.1 评估指标读法:mAP50和mAP50-95的差距说明什么
训练结束后,results.csv里会给出三组mAP指标:mAP50(B)是目标检测框的mAP,mAP50(M)是分割掩膜的mAP,mAP50-95(M)是跨IoU阈值的综合分割精度。对食品分割来说,mAP50(M)高但mAP50-95(M)低,说明掩膜边界不够精细——模型框对了位置但轮廓毛糙。要提升mAP50-95,最直接的手段是加大imgsz到960,或者把模型从nano换成small,这两步通常能带来3到5个点的提升。
单看mAP还不够,要补一个“业务评估指标”。我做食品分割时,会在验证集上人为统计两个数字:漏检率(该识别到的食物没输出mask)和误检率(非食物区域被输出为mask)。这两个数字能直接换算成后厨场景的经济损失,比mAP更让业务方理解。比如mAP50-95是0.7但漏检率5%,系统对一盘菜估份量时会有5%的情况完全不输出;对食堂计费场景来说,5%的漏检意味着每天要有人工补录几十次。
4.2 导出ONNX:从PyTorch到部署的桥
训练完成后导出ONNX的命令很简短,但后续验证步骤不能省。
yolo export model=best.pt format=onnx opset=12 simplify=Trueformat=onnx指定导出类型,opset=12是一个兼容性较好的算子集版本,在RKNN和OpenVINO上都能转,simplify=True会使用onnx-simplifier对计算图做化简,去掉一些多余的reshape和transpose节点。导出后的ONNX文件需要先在本机用onnxruntime推理一遍,确认输出与PyTorch原模型基本一致。
import onnxruntime as ort import numpy as np from PIL import Image sess = ort.InferenceSession('best.onnx') input_name = sess.get_inputs()[0].name img = Image.open('test.jpg').resize((640, 640)) img_np = np.array(img)[:, :, ::-1].transpose(2, 0, 1).astype(np.float32) img_np = img_np / 255.0 img_np = np.expand_dims(img_np, axis=0) outputs = sess.run(None, {input_name: img_np}) print(outputs[0].shape, outputs[1].shape)这段代码做了三件事:把图片缩放到640x640;把BGR转成RGB再转成CHW排布;归一化到[0,1]区间。输出里第一个是检测头的结果,第二个是掩膜分支的结果。对比PyTorch的推理输出时,注意候选框排序和confidence阈值可能不同,直接对比原始数组会误判为差分巨大。
4.3 端侧部署:RK3588上的YOLOv8食品分割
热词里高频出现的rk3588部署yolov8,是边缘部署很常见的路线。RK3588的NPU跑YOLOv8n-seg,640输入,实测帧率在15到30FPS之间,足够食堂计费台在1秒内完成整盘菜的识别。部署链路是:ONNX先转成RKNN格式,再用RKNN-Toolkit2加载。转换时的常见问题集中在掩膜分支:ONNX的输出节点数量多,RKNN转换器有时会报不支持。我的经验是把模型输出节点精简,只保留必要的候选框和掩膜头,或者直接把模型的seg_head在导出时做一次剪枝,只导出需要的输出。这一步对不熟悉模型结构的同学偏难,但网上RKNN工具链的文档里有YOLOv8的示例转换脚本可以参考。没有提前验证ONNX输出就上RKNN的部署,遇到问题后排查链路会非常长。
5. 避坑与排查:食品分割训练里最常见的5个问题
5.1 现象:loss正常下降,但验证集mAP几乎不涨
原因:训练集和验证集的标签map表不一致。常见于手工修改class_map字典后,只重新转换了训练集,验证集还在用旧的类别顺序。模型学到的类别索引和验证时的标签对不上,mAP自然上不去。
解决:训练前强制对两张分割结果图做可视化验证。我用一段小脚本把训练集和验证集的前几张图连同掩膜标签画出来,肉眼确认类别和位置都正确后再启动训练。这个习惯能挡住至少八成标签类别的翻车问题。
5.2 现象:检测不到盘子边缘的小目标,比如花生米和葱花
原因:imgsz=640时,几个像素宽的小食材在低分辨率下特征几乎消失,尤其是和食物背景颜色相近时。
解决:不要依赖resize放大,对小目标类别单独做裁剪增强。先把原图中包含该小目标区域的图像裁出来,放大到512x512后加入训练集,同时保留原图。测试时把imgsz提高到960,小目标的检出率会有明显改善。代价是推理速度变慢,需要实测能否接受。
5.3 现象:训练出来的掩膜输出全是矩形或正方形
原因:标注文件里大量shape type是rectangle,而不是polygon。YOLOv8-seg的分割分支学习的是多边形顶点,如果监督信号是矩形四角点,模型学到的就是“框内全填充”。
解决:在转换脚本里增加矩形转多边形的近似步骤,把每条边等距采样成8到16个点,将矩形展开成近似多边形。转换后的txt行列数会变多,但分割分支能得到更合理的监督。
5.4 现象:resume训练后loss反弹,精度比训练前还差
原因:yolo train resume使用last.pt恢复训练,其中包括optimizer的动量状态和learning rate的当前值。如果你在resume前调整了数据增强参数或学习率,优化器状态与新超参不匹配,loss出现跳变是正常的。
解决:resume只用于“完全中断后继续原配置”的场景。要改配置就重新训练,不要指望resume换参数。如果代码里自动用了resume,在启动命令前把last.pt重命名或删除,强制从新配置开始。
5.5 现象:CPU训练时内存爆掉,进程被OOM杀
原因:CPU版PyTorch在数据加载阶段,DataLoader先做解码再喂模型,workers开多了CPU内存被图片缓冲占满,机器直接卡死。
解决:把workers降到2甚至1,同时减少DataLoader的预取缓冲。命令里加workers=2 prefetch=2。另一个做法是关闭mosaic增强——mosaic需要同时解码四张图,内存占用和计算量都高,CPU排错时先关掉,跑通后再开。
6. 进阶技巧:用results.csv画损失函数曲线图,精准定位训练问题
训练日志在runs/segment/run1/results.csv旁边还有一个results.png,ultralytics会自动生成。但自动图的信息密度不高,我习惯自己画,把seg_loss、box_loss和mAP50(M)放到同一张图上,从不同时间尺度观察训练状态。
import pandas as pd import matplotlib.pyplot as plt df = pd.read_csv('runs/segment/run1/results.csv') df['epoch'] = df.index plt.figure(figsize=(14, 6)) plt.subplot(1, 2, 1) plt.plot(df['train/seg_loss'], label='train seg_loss', linewidth=1.5) plt.plot(df['val/seg_loss'], label='val seg_loss', linewidth=1.5) plt.xlabel('epoch') plt.ylabel('loss') plt.legend() plt.title('Seg Loss') plt.subplot(1, 2, 2) plt.plot(df['metrics/mAP50(M)'], label='mAP50 mask', linewidth=1.5, color='green') plt.xlabel('epoch') plt.ylabel('mAP') plt.legend() plt.title('Mask mAP50') plt.tight_layout() plt.savefig('food_seg_loss_curve.png', dpi=200)画出来后重点看三组特征。第一组:train/seg_loss持续下降但val/seg_loss在第40轮附近开始回升,过拟合发生,这时应该加大数据增强强度或提前早停。第二组:val/seg_loss从头到尾高频震荡、没有下降趋势,batch太小或lr太高,先用batch=16、lr0=0.0005重跑。第三组:seg_loss明显下降但mAP50(M)停滞在0.6以下,数据标注质量有问题,去检查掩膜是否贴合边界,而不是继续调参数。
我一直保留一个习惯:每个epoch结束后把单张测试图的分割结果和原图叠加保存,每50个epoch亲自看一眼。这不是为了审美,而是为了确认模型在“分割”而不是“抠图”——大量模型mAP很高但掩膜覆盖了盘子边缘或者汤汁部分,这种问题靠数值看不到,看一眼可视化结果就清楚。
基于YOLOv8的食品图像分割识别系统,真正拉开差距的地方不在模型结构,而在数据质量和训练监控的耐心。从labelme标注规范开始,到转换脚本的边界处理,再到用loss曲线判断训练阶段的每一个信号,这些环节扎扎实实做一遍,模型的表现不会太差。希望这篇文章帮你把这条路走通,少走我当年走过的弯路。
本文还有配套的精品资源,点击获取