
简介基于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, encodingutf-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_points16): 将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扰动等。默认配置下mosaic1.0即始终开启但食品分割对mosaic很敏感。四张图拼接后食物和背景的接缝处会产生伪边缘模型容易把拼接缝当作食物边界学进去。我实测下来mosaic一开验证集分割边界的mAP50-95会掉1到2个点。因此我的做法是前50个epoch关掉mosaic训练后期再开启让模型先学会真实边界再接触拼接噪声这个顺序比反向更稳定。HSV扰动也要保守。工业零件的颜色通常稳定HSV给得很激进也不会出大问题食品恰恰相反颜色是区分类别的重要线索——生熟、新鲜度、酱汁浓淡都靠颜色判断。我直接用默认值hsv_h0.01, hsv_s0.5, hsv_v0.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 python3.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.10ultralytics在3.8到3.11上表现都正常按自己机器的既有环境来就行。CPU训练这件事要说透可以跑通但很慢。我在GTX1660Ti上跑yolov8n-segbatch8imgsz640一个step大概1.5秒CPU版同样条件要慢10到20倍。所以CPU环境一般用来做三件事验证数据格式是否正确、跑一个完整的epoch确认代码链路、调标注错误。真正的训练还是放到GPU机器或云服务器上否则调一次参要等半天没法干活。3.2 训练命令与YOLOv8参数含义一条命令完整解读数据集目录和data.yaml准备好后训练命令如下yolo segment train \ datafood.yaml \ modelyolov8n-seg.pt \ epochs100 \ imgsz640 \ batch8 \ optimizerAdamW \ lr00.001 \ patience20 \ projectfood_seg \ namerun1每个参数都值得说明。modelyolov8n-seg.pt是nano规模的预训练权重文件小、速度最快但精度有限食品分割这类目标形状不规则的任务我建议至少从yolov8s-seg.pt起步。优化器选AdamW而不是SGD因为分割任务loss面更复杂AdamW对学习率的敏感度低一些。lr00.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统计不稳定或者学习率设高了。改成batch16、lr00.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)是目标检测框的mAPmAP50(M)是分割掩膜的mAPmAP50-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 modelbest.pt formatonnx opset12 simplifyTrueformatonnx指定导出类型opset12是一个兼容性较好的算子集版本在RKNN和OpenVINO上都能转simplifyTrue会使用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, axis0) 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-seg640输入实测帧率在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 现象检测不到盘子边缘的小目标比如花生米和葱花原因imgsz640时几个像素宽的小食材在低分辨率下特征几乎消失尤其是和食物背景颜色相近时。解决不要依赖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的预取缓冲。命令里加workers2 prefetch2。另一个做法是关闭mosaic增强——mosaic需要同时解码四张图内存占用和计算量都高CPU排错时先关掉跑通后再开。6. 进阶技巧用results.csv画损失函数曲线图精准定位训练问题训练日志在runs/segment/run1/results.csv旁边还有一个results.pngultralytics会自动生成。但自动图的信息密度不高我习惯自己画把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], labeltrain seg_loss, linewidth1.5) plt.plot(df[val/seg_loss], labelval seg_loss, linewidth1.5) plt.xlabel(epoch) plt.ylabel(loss) plt.legend() plt.title(Seg Loss) plt.subplot(1, 2, 2) plt.plot(df[metrics/mAP50(M)], labelmAP50 mask, linewidth1.5, colorgreen) plt.xlabel(epoch) plt.ylabel(mAP) plt.legend() plt.title(Mask mAP50) plt.tight_layout() plt.savefig(food_seg_loss_curve.png, dpi200)画出来后重点看三组特征。第一组train/seg_loss持续下降但val/seg_loss在第40轮附近开始回升过拟合发生这时应该加大数据增强强度或提前早停。第二组val/seg_loss从头到尾高频震荡、没有下降趋势batch太小或lr太高先用batch16、lr00.0005重跑。第三组seg_loss明显下降但mAP50(M)停滞在0.6以下数据标注质量有问题去检查掩膜是否贴合边界而不是继续调参数。我一直保留一个习惯每个epoch结束后把单张测试图的分割结果和原图叠加保存每50个epoch亲自看一眼。这不是为了审美而是为了确认模型在“分割”而不是“抠图”——大量模型mAP很高但掩膜覆盖了盘子边缘或者汤汁部分这种问题靠数值看不到看一眼可视化结果就清楚。基于YOLOv8的食品图像分割识别系统真正拉开差距的地方不在模型结构而在数据质量和训练监控的耐心。从labelme标注规范开始到转换脚本的边界处理再到用loss曲线判断训练阶段的每一个信号这些环节扎扎实实做一遍模型的表现不会太差。希望这篇文章帮你把这条路走通少走我当年走过的弯路。本文还有配套的精品资源点击获取