
简介这是一套基于YOLO的食物卡路里检测系统毕业设计项目包含完整可运行的Python/C源码、部署教程与配套数据集适用于计算机、电子信息、物联网等专业学生完成毕设、课程设计或快速搭建项目演示。包内共190个文件以Python脚本、C语言源文件、前端JS/CSS/HTML、模型配置JSON及图片素材为主并附带IAR/Keil工程文件、Visual Studio解决方案等涵盖模型检测、单片机数据采集、上位机界面展示等功能模块。整体压缩包约2.58MB结构清晰便于按模块学习。已有116人学习下载资源集成度较高从环境配置到功能扩展均有资料支撑初学者可借助部署教程快速复现具备基础者还可基于源码二次开发适合不同阶段的开发者使用。1. 先从一个实际场景说起外卖时代的“热量焦虑”如果你打开过外卖App里的“营养分析”功能你会发现大多数只能给出一个估算区间的热量范围而且误差经常大得离谱。原因很简单餐饮商家标注的数据参考价值有限真正精确的热量计算必须依赖食材识别和重量估算。这个项目的核心就是用 YOLO 目标检测流程把餐盘里的食物按类别框出来——一块红烧肉、一筷子青菜、半碗米饭——然后根据检测框面积和先验密度换算出可量化的卡路里区间。我需要把话说明白这不算科研级成果但它是个“能跑、能改、能讲清楚”的完整闭环源码包里有训练好的权重、标注好的数据集和部署脚本适合三类人做毕业设计需要演示系统的学生想在智能饮食管理方向做 MVP 的开发者以及刚接触 YOLO 想看看“从数据集到部署”全链路长什么样的从业者。下面我从选型逻辑讲起到训练、部署、排错一条线拆完。2. 模型选型为什么是 YOLO以及为什么是 v8 骨架2.1 在实时检测任务里两阶段模型为什么吃不开很多人一开始会想到 Faster R-CNN 这类两阶段检测器。原理上它先用 RPN 生成候选区域再对每个候选框做二次分类和回归精度天花板确实高但问题也很具体VGG16 或 ResNet 主干提取特征后一张图在 GPU 上要跑 150ms 左右部署到 CPU 服务端直接翻好几倍。食物卡路里这个场景摄像头拍完要立刻出结果用户不会等三秒。SSD 倒是满足实时性但对小目标——比如餐盘边缘的几颗花生米——召回率明显不足。YOLO 把检测统一成单次回归问题v8 版本用 anchor-free 机制进一步省掉了预设锚框的调参负担在精度和速度之间拿到了一个很实用的平衡点。2.2 v8 的核心改动C2f、解耦头与 anchor-freeYOLOv8 相比 v5 有三个感知明显的改动。骨干网络里的 C2f 模块替代了原来的 C3它把梯度流做了更细的拆分每层输出同时汇入后续多个子层特征复用效率提升这直接反映在小目标召回上。检测头从“耦合头”换成“解耦头”分类分支和回归分支各走一个卷积分支避免两个任务在梯度回传时互相干扰。另外 v8 不再需要预设 anchor边界框由模型直接预测中心点和宽高部署时少了一层解码逻辑。对食物数据集这种类别多、形状差异大的场景anchor-free 的上限更高。提示如果你用的是官方 ultralytics 包v8 的训练脚本和 v5 差异不大但迁移学习时优先加载yolov8n.pt或yolov8s.pt预训练权重不要从零开始训。2.3 损失函数构成只理解 BCE 远远不够v8 的损失函数由三部分叠加分类用 BCEBinary Cross Entropy边界框回归用 CIoU 损失另外还包含一个 Distribution Focal LossDFL来计算边界框的分布偏移。DFL 的作用是让模型不只预测一个确定值而是学习边界分布拟合位于真实框边缘附近的概率密度。你在训练日志里看到的cls_loss、iou_loss、dfl_loss三项最终相加得到总损失。不少人调参时只动学习率其实如果训练集里食物被遮挡严重手动增大 DFL 项的权重通过修改 loss 系数比盲目调学习率更有效。常见做法是保持 v8 默认损失配比先跑一轮观察三类损失各自的下降曲线。如果iou_loss降得慢问题多半在标注框不准确而不是模型结构有问题。3. 数据集与标注卡路里检测最容易被卡住的一环3.1 数据从哪来公开数据集合并 自采补缺食物识别没有通用的高质量公开大数据集可以直接用于卡路里估算常见做法是拿 UECFOOD-256、Food-101 这类分类数据集按需截取合并到自采的检测样本里。UECFOOD-256 虽然有标注框但它的框是食物区域的外接圆圆形标注直接喂给 YOLO 会有偏差需要先转换成矩形框。自采部分我用手机拍不同光线和角度下的餐盘每个类别至少收集 500 个实例。类别体系设计上要给卡路里估算留余地不要只标“肉类”“蔬菜”要细化到“红烧肉”“炒青菜”“白米饭”这类可以绑定固定单位热量的类别。同一个大类下面放多个细分类别模型收敛会慢但估算精度高得多。3.2 COCO 格式如何转成 YOLO 训练格式用 LabelImg 或 X-AnyLabeling 标注完导出格式通常是 COCO 的 JSON 或 Pascal VOC 的 XML。YOLO 训练需要的是每张图对应一个同名.txt文件每行内容为类别ID 归一化中心x 归一化中心y 归一化宽 归一化高。转换脚本如下import json import os def coco_to_yolo(coco_json_path, output_dir, img_width, img_height): with open(coco_json_path, r, encodingutf-8) as f: coco_data json.load(f) os.makedirs(output_dir, exist_okTrue) img_id_to_name {img[id]: img[file_name] for img in coco_data[images]} category_id_map {cat[id]: idx for idx, cat in enumerate(coco_data[categories])} annotations_by_image {} for ann in coco_data[annotations]: img_id ann[image_id] annotations_by_image.setdefault(img_id, []).append(ann) for img_id, anns in annotations_by_image.items(): txt_path os.path.join(output_dir, img_id_to_name[img_id].replace(.jpg, .txt)) with open(txt_path, w) as f: for ann in anns: cat_id category_id_map[ann[category_id]] x, y, w, h ann[bbox] cx (x w / 2) / img_width cy (y h / 2) / img_height nw w / img_width nh h / img_height f.write(f{cat_id} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}\n) if __name__ __main__: coco_to_yolo(annotations.json, labels, 640, 640)这个脚本的逻辑是先把每个 COCO 标注的bbox从左上角坐标转为 YOLO 需要的中心点坐标再分别除以图片宽高做归一化。category_id_map是类别映射表注意 COCO 的类别 ID 往往不连续一定要显式映射到0, 1, 2...否则训练时类别索引会错位。3.3 标注框的颗粒度按“可食用区域”框还是按“餐盘占比”框这是直接影响卡路里模型精度的关键点。你拍一碗面条如果按碗的外轮廓标注检测框会包含大量汤水和碗壁面积虚高热量自然虚高。正确粒度是标注所有可食用食材的区域鸡腿要把骨头区域也包含进去但必须减去不属于鸡腿的汤汁背景。实际操作中标注越贴边模型推理时对食材边界越敏感。4. 模型训练与后处理从检测框到卡路里数字4.1 数据集目录结构与训练命令YOLOv8 训练需要的数据集目录标准结构如下food_data/ ├── images/ │ ├── train/ # 约 3200 张 │ └── val/ # 约 800 张 ├── labels/ │ ├── train/ │ └── val/ └── food.yamlfood.yaml内容path: /path/to/food_data train: images/train val: images/val names: 0: baicai 1: hongshaorou 2: baifan 3: jidan训练命令yolo detect train \ modelyolov8s.pt \ datafood.yaml \ epochs100 \ imgsz640 \ batch16 \ patience15 \ optimizerAdamW \ lr00.001 \ projectfood_train \ nameexp01参数说明modelyolov8s.pt是加载官方在 COCO 上的预训练权重用它做初始化而不是随机初始化收敛速度快很多patience15表示验证集指标连续 15 轮不提升就早停optimizerAdamW适合中小规模数据集SGD 在这种细分类别多但单类样本量小的场景下容易震荡。4.2 数据增强让模型对“摆盘方式”不敏感食物检测有个特殊问题同一道菜外卖盒里是一坨餐厅摆盘是另一坨。模型很容易过拟合到标注框的形状上。ultralytics 默认开启的马赛克增强mosaic对这个问题有奇效它把四张图拼在一起训练让模型在框得不准的情况下也能学到本质特征。如果显存紧张导致 mosaic 无法开启我一般会手动加大 hsv_h 和 hsv_s 这两个颜色扰动参数hsv_h: 0.02 hsv_s: 0.6 hsv_v: 0.5hsv_h控制色相偏移设置太小对麻辣烫这种颜色接近的食材无效设置太大会让肉类的颜色失真。我调下来的经验值是 0.02~0.03 之间hsv_s饱和度可以给到 0.6因为各个手机摄像头拍出来的食物颜色饱和度差异本来就巨大。4.3 卡路里估算不是分类任务是面积乘密度的回归检测框输出后要换算热量不能查表直接给值因为同样 100g 米饭在不同含水量的烹饪方式下热量能差出 30%。我实现的逻辑分两步检测框面积作为像素级参考再用一个回归系数把面积映射到估算重量最后乘以该类别每克热量。这里的回归系数不是模型学出来的而是用标注数据里的先验密度表预先算好。举例白米饭按每平方厘米像素面积约 0.85g 估算红烧肉由于脂肪密度低约 0.6g。数据集中每个类别的密度系数整理如下类别像素密度 (g/cm²)每 100g 热量 (kcal)备注白米饭0.85116含水率高红烧肉0.62478脂肪占比高炒青菜0.3885水分大误差也大煎鸡蛋0.74199形状规整推理时拿到检测框直接计算框内面积像素数转换为物理面积的公式是物理面积 像素面积 / (imgsz / 真实图像尺寸)^2再乘以密度系数得到估算重量最后查表得热量def estimate_calories(detection, class_density_map, class_kcal_map, pixel_area_ratio): class_id int(detection.cls) box detection.boxes.xyxy[0].cpu().numpy() pixel_area (box[2] - box[0]) * (box[3] - box[1]) physical_area pixel_area * pixel_area_ratio weight physical_area * class_density_map[class_id] calories weight / 100 * class_kcal_map[class_id] return caloriespixel_area_ratio这个值是整个换算链路里最关键的中间参数实质是“每像素对应真实物理面积”的缩放系数。你需要固定住摄像头到餐盘的距离拍一张已知尺寸的 A4 纸来标定。不标定的话拿不同距离拍出来的照热量可能差出一整顿饭的热量。5. 三种最常问到的部署形态Python Web、小程序 ARM 端、嵌入式5.1 Python WebFlask 快速封装成 HTTP 接口最直接、也是项目里部署教程主推的方案就是把模型封装为 Flask 服务拍照后前端上传图片后端返回 JSON。核心路由如下from flask import Flask, request, jsonify from ultralytics import YOLO app Flask(__name__) model YOLO(best.pt) app.route(/detect, methods[POST]) def detect(): file request.files[image] file.save(tmp.jpg) results model.predict(tmp.jpg, conf0.35, iou0.5, imgsz640) detections [] for r in results: for box in r.boxes: class_id int(box.cls) conf float(box.conf) xyxy box.xyxy[0].tolist() detections.append({ class: model.names[class_id], confidence: conf, bbox: xyxy }) return jsonify({success: True, detections: detections}) if __name__ __main__: app.run(host0.0.0.0, port8000)conf0.35是置信度阈值食物遮挡或模糊时该调低到 0.25iou0.5是 NMS 的 IoU 阈值餐盘里食物互相堆叠紧密时降到 0.3 可以减少框被合并掉的风险。生产环境别用 Flask 自带的开发服务器前面套一层 Gunicorn 或者直接用 FastAPI Uvicorn。5.2 小程序/APP 端导出 ONNX 配合 NCNN 或 Core ML如果需要部署到手机端不能直接跑 PyTorch 权重。标准流程是把训练好的 best.pt 导出为 ONNX 格式再转成 NCNN 参数文件Android或 Core ML 模型iOS。yolo export modelbest.pt formatonnx dynamicTrue imgsz640dynamicTrue允许输入尺寸动态变化但要小心NCNN 转换工具链对动态维度的支持不够好实际开发时我建议固定 640 输入尺寸导出时去掉 dynamic。转换后的.param和.bin文件可以直接拿到 Android Studio 里通过 NCNN 推理框架加载。提示如果目标是微信小程序不要去碰任何需要自己搭代理的网络请求方案直接把 ONNX 模型放进小程序本地用 WASM 版推理引擎跑推理然后把结果缓存为 JSON 存入本地数据缓存这是最稳的离线方案。5.3 嵌入式内核源码视角边缘设备上的工程取舍放在树莓派或 Jetson Nano 这种资源受限设备上得先看看手上的板子能不能跑得动 v8s。yolov8s权重约 22MB在树莓派 4B 上纯 CPU 推理一张图大约要 2.5 秒体验很差。这里有一种组合策略模型剪枝加半精度推理。剪枝建议按通道维度做只对 C2f 模块中贡献度低的通道做裁剪保留检测头不动。剪枝后模型体积能降到 12MB 左右精度损失 1~2 个点但单帧延迟降到 1.2 秒。对实时性要求再高的场合就得切到 TensorRT FP16 推理或者上 int8 量化。6. 从训练到交付你一定会踩的几个坑6.1 数据集类别不均衡几类食物的样本量差出 10 倍食物数据集天然长尾分布米饭几乎每张图都有但某些汤羹类样本很少。开始训练前用yolo自带的类别分布统计看一眼。如果最少的类别不足最多的 20%不要急着加样本先把已有样本做复制粘贴增强把尾部类别权重抬高yolo train ... class_weights{0:1.0, 1:1.0, 2:0.8, 3:2.5}class_weights 权重乘到对应类别的分类损失上尾部类别权重给到 2.5 可以有效对抗类别失衡但会出现尾部类别误检率升高的副作用所以训练完必须在验证集上单独统计每个类别的 PR 曲线。6.2 在验证集上做一次“距离盲测”训练时图集里的食物都是近距离拍摄标注框占比大。但用户实际使用是从 30cm 左右拍的俯视餐盘食物在整体画面里占比可能不到 40%。我一般会在训练完成后额外拍 50 张没有进过训练集的、不同距离的餐盘照逐张跑推理看检测框偏移程度。如果距离一远就漏检第一优先调整的是输入尺寸从 640 改成 800小目标的特征图分辨率会显著提高。6.3 重叠食物堆叠确认NMS 调参的边界在哪餐盘里菜和饭堆叠是常态模型容易把两个类别输出到同一个区域。遇到这类情况把iou调低能保留更多框但代价是同一个物体可能出现双重检测框。我一般会先观察模型输出置信度如果两个重叠框的类别不同且置信度都超过 0.5就额外加一条规则面积重叠比超过 30% 时取置信度更高那个。这个逻辑放后处理里比在 NMS 阶段强行调参要可控得多。6.4 验证部署效果时要量化的三个指标不要只拿两张图看效果。量化验证很简单把验证集 800 张图全部跑通推理统计每个类别的平均检测置信度和检测框面积分布然后把模型估算的总热量和人工标注的真实热量放在同一张表里对比误差。如果误差持续偏高超过 25%问题大概率出在 4.3 节里的密度系数而不是模型本身。这是整个系统唯一不需要重新训练模型就能大幅降低误差的调优点。本文还有配套的精品资源点击获取