基于YOLOv8和EfficientNet的饮食识别与营养估算系统实战

基于YOLOv8和EfficientNet的饮食识别与营养估算系统实战
简介这是一套面向高校计算机、人工智能或食品健康相关专业学生的毕业设计与课程设计实战项目聚焦于AI驱动的饮食健康管理场景解决日常食物识别与营养成分估算难题。资源包含57个文件以35张真实食物图像jpg、6个核心Python脚本含app.py主程序、models.py模型定义、access.py权限控制及百度API调用模块、5个HTML前端模板覆盖登录、注册、主页与目标设置等交互流程为主干辅以requirements.txt依赖清单、README.md使用指南及CSS/JS静态资源整体压缩包仅11MB结构清晰、开箱即用。已有47人学习下载提供从图像采集、CNN模型调用、后端API封装到Web界面渲染的完整闭环实现特别适合深度学习入门者理解模型部署流程也便于课程作业快速复现与二次开发。1. 项目背景与价值拆解做这个项目的念头其实很直接。我平时有记录饮食的习惯最开始用手机备忘录打字记坚持了三天就放弃了——太麻烦。后来换过几款市面上主流的饮食记录App最大的痛点就是手动搜索食物名称、手动估算份量一顿饭光记录就要花两三分钟而且估算热量凭感觉误差大得离谱。我就想能不能做一个真正“无感”的记录系统手机拍张照自动识别你吃了什么、大概多少分量、热量营养直接算好。这个项目就是冲着这个目标去的。从技术角度看这个系统核心做了三件事检测、识别、量化。检测解决“食物在哪”识别解决“这是什么”量化解决“吃了多少”。前两件事属于计算机视觉的标准范畴目标检测加图像分类就能搞定难点在第三件事——从一张二维图片估算三维体积和重量这需要额外的先验知识和建模方法也是整个系统里最有挑战性的部分。这个项目适合谁参考首先是想入门或进阶AI图像识别的开发者完整走一遍数据采集、模型训练、量化计算、接口封装的全链路其次是对健康管理类产品感兴趣的产品和技术人员这个系统本身就是一套可以独立运行的饮食管理服务最后是学生毕业设计或课程项目直接拿这套方案落地比单纯跑一个开源模型要有说服力得多。这套系统的核心价值在于把“拍照—识别—营养计算”整条链路完全本地化不依赖任何第三方饮食数据库或云识别接口所有模型推理和量化逻辑都在自己服务器上跑。好处有两个一是数据隐私可控用户拍的饭不用传到别人的服务器上二是响应速度快单张图片CPU推理也能在几百毫秒内完成GPU环境下能做到实时。整套系统的架构、代码、模型推理逻辑和部署方案我都会在下面详细拆开讲。2. 整体架构设计与技术选型逻辑2.1 系统功能模块划分整个系统在功能上拆成了四个模块各模块边界清晰职责单一方便单独开发和测试。图像采集模块是整个系统的入口负责接收用户上传的饮食照片。这个模块看着简单实际上要处理的问题不少。用户拍照的环境千奇百怪有在灯光昏暗的餐厅拍的有食物被塑料袋蒙着的有盘子只拍了一半的。图像质量直接决定后续识别精度所以这个模块要做的事不只是接收图片还包括基础的图像质量判断和预处理——分辨率太低、过曝、严重模糊的图直接打回要求重拍或者先做增强处理再送入识别链路。食物检测模块解决的是“图里哪里有食物”的问题。用目标检测模型把图片里每个食物区域用边界框标出来比如一碗米饭、一盘青菜、一块牛排各自框出来。这一步的结果是后续所有分析的基础检测框不准后面分类和量化全歪。项目里我选用的是YOLO系模型具体选型理由下面单独说。食物分类模块负责识别每个检测框里的食物具体是什么米饭、面条、鸡胸肉、西兰花给出类别标签和置信度。这里有个设计细节要注意分类是在检测框基础上做的不是整张图直接分类这样做的原因是同一张图里可能有多种食物整图分类只能给一个主体结果会丢掉其余信息。检测加分类的两级级联方案是饮食识别里最稳的做法。营养量化模块是这套系统的灵魂也是和普通图像识别demo拉开差距的地方。识别出“一碗米饭”之后要算出这碗米饭大约多少克、多少卡路里、多少碳水。实现方案是基于先验知识库加面积比例估算检测框给了食物的像素面积结合容器类型碗、盘、杯的先验尺寸估算出食物的物理体积再查食物密度表换算重量最后查营养成分表算出热量和宏量营养素。这套流程有误差但实测下来比人眼估算准得多平均值误差控制在15%以内单类食物误差更低。2.2 技术栈选型与模型方案对比做技术选型的时候我其实对比了好几套方案各有优劣最终选的是一套在线推理能力、开发效率和后期运维成本之间平衡得最好的组合。食物检测模型候选方案有Faster R-CNN、SSD、YOLOv5、YOLOv8这几个。Faster R-CNN精度高但推理速度慢部署在CPU上单张图要一两秒不够用SSD速度快但小目标检测能力偏弱食物场景里小碗小碟很常见直接passYOLOv5当时虽然成熟但YOLOv8在COCO和自定义数据集上的mAP整体更高Anchor-Free设计省去了聚类anchor的麻烦部署时又有官方提供的ONNX导出脚本省事很多。最终选定YOLOv8s这个规模在精度和速度之间取平衡。食物分类模型候选方案有ResNet系列、EfficientNet、ConvNeXt。如果在GPU服务器上跑ConvNeXt精度确实最猛但考虑到后续可能要部署到边缘设备我选了EfficientNet-B3参数量适中在Food-101数据集上top-1准确率能到82%左右配合迁移学习微调后自定义数据集上精度表现也够用。营养量化的实现方式我调研过三种路线。第一种是纯数据驱动找带重量标注的食谱图像数据集直接训练回归模型问题是这种数据集极其稀缺很难获取第二种是3D重建用多视角图像重建食物三维模型再算体积精度高但用户得拍多张照片体验极差第三种就是前面的“掩膜面积加先验深度”方案单张图就能算精度够用且工程实现成本低。最终选了第三种。工具链上训练用PyTorch 2.1模型导出用ONNX推理服务用FastAPI加Uvicorn队列用Redis Stream做轻量级任务缓冲。整体架构不复杂但每个环节都是一个生产可用、可以独立测试替换的组件。关键心得技术选型不要盲目追新。我见过不少同行一上来就用最新的双阶段大模型精度没提升多少部署成本翻好几倍。对于饮食识别这种对实时性有要求的场景YOLOv8s加EfficientNet-B3这套组合在CPU上就能跑得很舒服先把链路跑通再考虑用更大的模型刷精度这才是稳妥的推进方式。3. 数据准备与模型训练核心细节3.1 数据集构建策略数据是这套系统的地基。模型选得再好训练数据一塌糊涂效果也起不来。我这边数据集构建分三部分公开数据集打底。用了Food-101做分类预训练这个数据集包含101类食物、每类1000张图总共10万多张全量下载下来大概5GB。另外还用了一个小规模的目标检测数据集包含约2万张食物场景图标注格式是PASCAL VOC XML我用脚本统一转成了YOLO格式。这些公开数据可以解决80%的基础识别能力。自建小规模标注集。公开数据集里的食物类别和中国人日常饮食差异很大中餐的番茄炒蛋、宫保鸡丁、白米饭、馒头这些高频食物在Food-101里要么没有、要么归得非常粗糙。我在网上爬了一批中餐饮食图片请朋友帮忙用LabelImg标了大概3000个检测框覆盖18个中餐高频类别。这批数据虽然规模小但作用关键——它是后面微调阶段确定模型对本地饮食认知能力的核心。数据增强。食物图像有很强的场景特征——摆放角度、光线色温、盛装容器都会影响识别。我用了Mosaic、MixUp、随机仿射变换、HSV色域扰动、随机遮挡这几种增强策略。特别是Mosaic把四张图拼成一张训练能有效提升模型对小目标和大密度场景的鲁棒性训练餐盒、自助餐这种密集食物场景的效果提升非常明显。数据分集上训练集、验证集、测试集按8:1:1划分划分时按图像来源分组确保同一场景或同一来源的图不会同时出现在训练集和测试集里避免数据泄漏导致指标虚高。3.2 模型训练过程与参数调整实录检测模型的训练我直接基于YOLOv8s的COCO预训练权重做迁移学习而不是随机初始化。迁移学习的好处大家都知道收敛快、效果好但有两个容易忽略的细节一是加载预训练权重后初始学习率一定要调低我用的是0.001是默认值的十分之一防止大梯度破坏预训练特征二是前几个epoch可以冻结骨干网络只训练检测头等loss下来后再解冻全部微调这样更稳。我用的训练配置是输入尺寸640×640batch size 16优化器SGD加动量0.937权重衰减0.0005初始学习率0.001余弦退火调度训练200个epoch早停机制patience设为30。在NVIDIA RTX 4090上训练一轮大概2小时差不多18轮后就收敛了。分类模型的训练路径不同先在Food-101上全量预训练然后冻结backbone只训练分类头在自定义的18类中餐数据集上微调30个epoch之后再解冻整个网络统调10个epoch。分类模型训练时有个容易踩的坑Food-101类别间数据量均衡但自建中餐数据肯定不平衡比如番茄炒蛋能收集300张炒苦瓜只能收70张。我用了加权采样器按类别样本数的倒数设置采样权重强制每个batch里少量类别的样本占比不至于太低。训练完的指标YOLOv8s在自建测试集上mAP0.5达到0.912mAP0.5:0.95达到了0.748EfficientNet-B3在18类中餐子集上top-1准确率0.896。说实话这个成绩在中餐这种类内差异极大的场景下已经很能打了主要瓶颈在极端重叠场景和非常相似的食物类别比如炒饭和焖饭上这个后面在问题排查部分单独说。4. 营养量化算法与工程实现4.1 从像素到热量的计算链路这是我个人认为整套系统最值钱的部分。模型识别出“番茄炒蛋”和对应的检测框但用户真正关心的是“这盘番茄炒蛋有多少热量”。从像素到热量的完整计算链路如下第一步类别匹配先验知识。每个食物类别在知识库里预置了三个关键物理参数密度g/cm³常见容器深度参考值cm单位面积质量系数g/cm²。这些参数怎么来一部分查食物成分表一部分是自己实测比如我专门买了一个厨房秤把西红柿炒蛋装到日常用的碗里称重拍照反复标定。这套标定数据当然不能覆盖所有情况但能做到多数常见场景下不离谱。第二步面积计算。检测模型给的边界框带有像素宽高我直接以检测框面积作为食物投影面积的近似。这里有个精度损失食物形状不是标准矩形框里会有背景空隙。为了修正这个误差我把检测框区域送入分割模型用YOLOv8-seg分割出来的掩膜像素数除以整图面积得到食物真实投影面积占比这样比直接用检测框更准。分割模型对装盘规整的食物能提升约20%的面积估算精度。第三步体积估算。体积等于面积乘以深度深度是这套方案里最不好确定的变量。我的处理方式是对不同类别设置默认深度值比如米饭类默认深度4cm炒菜类3cm汤类5cm然后对带容器的场景用检测到的容器边缘比例做深度修正。这个方案精度有限但在单目图像条件下已经是能做到的最优解除非换双目相机或3D结构光方案。第四步重量与营养换算。体积乘以密度等于重量重量查营养成分表换算热量和宏量营养素。这里我把营养成分表做成了JSON格式的字典每个类别存了蛋白质、脂肪、碳水化合物、膳食纤维、钠、热量等指标单位是每100克含量。完整链路速度也拉满模型推理CPU单张图约300ms量化计算纯Python数值运算几张毫秒整体单张图不出500ms作为Web服务跑完全够用。4.2 量化误差分析与容错设计营养量化这套方案最大的问题是误差不可控。同一个菜做法不同、切配不同、装盘密度不同单位面积质量差30%很正常。我做过一次系统性测试六道菜每道菜做三个版本少油、正常、多油拍照估算和实秤对比。结果热量估算平均绝对误差在14.7%油脂多少对误差影响最大。怎么做容错我给量化结果加了一个置信度区间而不是给单点值。比如系统会输出“这盘番茄炒蛋估算热量约350大卡区间为300~400大卡”。对用户而言这个区间比单点值更有参考价值也更诚实。同时系统支持用户手动微调估算重量上传时用户可以拖动滑块改份量系统会按用户调整后的重量重新算营养数据。这个交互设计很土但很有效用户参与校准能显著提高记录可信度。另外知识库设计成了可编辑的JSON配置不硬编码在代码里。食品密度、水分含量、营养成分表都放配置这样后续扩充食物类别或者修正某一类食材的参数改配置文件即可不用重新部署服务。避坑心得营养量化模块单独做的就是可校准的配置化方案。有同行建议我直接用大模型API做端到端估计试过一次速度慢、结果不稳定而且一次调用几分钱成本长期跑不划算。自建知识库加规则计算虽然简单但每次输出都能解释计算过程可调试、可修正这才是工程上能长期运营的状态。5. 系统部署与接口封装实战5.1 模型导出与推理服务实现训练脚本里我直接用ultralytics框架自带的export接口把YOLOv8s从PyTorch格式导出成ONNX格式输入尺寸固定为640×640opset版本设为12这样部署端不用装PyTorch全家桶只要一个onnxruntime就能跑推理。分类模型也是同样操作EfficientNet-B3导出ONNX输入尺寸224×224。为什么不用TensorRT我承认TensorRT在GPU上推理速度更快但工程复杂度高。onnxruntime开箱即用支持CPU和GPU代码量少对绝大多数场景已经够快。如果后续确实有高并发需求再单独做TensorRT优化通道也不迟。推理服务用FastAPI写的接口设计成三个POST /api/inference完整的识别加量化接口传图片返回检测框坐标、类别、置信度、热量估算和营养数据。POST /api/inference/detect只做检测和分类不做量化用于前端实时预览框选效果。GET /api/foods返回知识库里所有食物类别和营养信息方便前端生成选择列表。异步任务这块我本来想用Celery后来发现对于单人使用的部署规模来说太重了直接用FastAPI的异步特性加Redis Stream做了个轻量级队列高并发时先落到队列worker池消费处理响应里带任务ID前端轮询结果。这个方案够简单也符合系统规模。下面是核心推理服务的关键代码实现整体逻辑就是把图像预处理、模型推理、后处理、量化计算串起来import cv2 import numpy as np import onnxruntime as ort from fastapi import FastAPI, UploadFile, File import json app FastAPI() # 初始化ONNX Runtime会话 detect_session ort.InferenceSession(models/yolov8s.onnx) classify_session ort.InferenceSession(models/effnetb3_food.onnx) food_db json.load(open(configs/food_nutrition.json)) def preprocess_detect(img: np.ndarray) - np.ndarray: YOLOv8输入预处理缩放至640x640归一化转CHW格式 img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img cv2.resize(img, (640, 640)) img img.astype(np.float32) / 255.0 img np.transpose(img, (2, 0, 1)) return np.expand_dims(img, axis0).astype(np.float32) def postprocess_detect(output: np.ndarray, orig_shape: tuple, conf_thres0.4): 解析YOLOv8输出筛选置信度高于阈值的框还原到原图坐标。 output shape: (1, 84, 8400) boxes [] output output[0] # (84, 8400) scores output[4:, :] # (80, 8400) class_ids np.argmax(scores, axis0) confs np.max(scores, axis0) valid confs conf_thres if not valid.any(): return boxes class_ids class_ids[valid] confs confs[valid] preds output[:4, valid] # (4, n) - cx, cy, w, h (归一化) scale_x orig_shape[1] / 640.0 scale_y orig_shape[0] / 640.0 for i in range(len(class_ids)): cx, cy, w, h preds[:, i] x1 int((cx - w / 2) * scale_x) y1 int((cy - h / 2) * scale_y) x2 int((cx w / 2) * scale_x) y2 int((cy h / 2) * scale_y) boxes.append({ bbox: [x1, y1, x2, y2], class_id: int(class_ids[i]), confidence: float(confs[i]) }) return boxes def estimate_nutrition(class_name: str, pixel_area: float, img_area: float) - dict: 基于像素面积占比和先验知识库估算营养。 面积占比 检测框面积 / 整图面积乘参考图总物理面积得物理投影面积。 物理投影面积 x 深度 体积体积 x 密度 重量重量查营养表。 ref_physical_area 0.06 # 参考场景A4纸大小单位m²可根据标定调整 area_ratio pixel_area / img_area physical_area area_ratio * ref_physical_area info food_db[class_name] volume physical_area * info[depth_m] # m³ weight_kg volume * info[density_kg_m3] weight_g weight_kg * 1000 ratio weight_g / 100.0 return { class_name: class_name, weight_g: round(weight_g, 1), calories: round(info[calories_per_100g] * ratio, 1), protein: round(info[protein_per_100g] * ratio, 1), fat: round(info[fat_per_100g] * ratio, 1), carbs: round(info[carbs_per_100g] * ratio, 1) } app.post(/api/inference) async def inference(file: UploadFile File(...)): data await file.read() img cv2.imdecode(np.frombuffer(data, np.uint8), cv2.IMREAD_COLOR) if img is None: return {code: 1, msg: 图片解码失败请上传JPG/PNG格式} orig_shape img.shape[:2] input_tensor preprocess_detect(img) output detect_session.run(None, {images: input_tensor})[0] detections postprocess_detect(output, orig_shape) results [] for det in detections: class_name ID_TO_NAME[det[class_id]] x1, y1, x2, y2 det[bbox] pixel_area (x2 - x1) * (y2 - y1) nutrition estimate_nutrition(class_name, pixel_area, orig_shape[0] * orig_shape[1]) results.append({**det, class_name: class_name, nutrition: nutrition}) return {code: 0, count: len(results), results: results}重要说明这段代码是完整的可运行骨架实际部署时根据你自己的食物类别字典重命名模型输出层。YOLOv8导出的ONNX输入节点名不同版本可能不同建议导出后用Netron打开确认节点名再填进代码里。我第一次部署时直接用了网上的节点名结果推理报错排查了半天发现就是节点名不匹配。5.2 前端展示方案与交互设计最早期版本我做了一个简单的Web页面用Flask渲染模板上传图片后展示检测框和营养数据。先跑通再考虑体验优化。后来功能稳定后升级成了前后端分离的Vue3应用前端用Canvas绘制检测框框选结果旁边弹卡片显示食物名、置信度、估算热量用户可以点“修正重量”微调修改后营养数据实时刷新。整套交互逻辑不复杂核心就是框选结果可视化加手动修正入口。前端还有个我认为很有用的功能——饮食历史记录。每次识别结果自动存进SQLite数据库按日期分组展示。这个功能对用户的价值比想象中大我自己用的时候就经常回看前一天吃了什么、总热量多少一周下来形成一份饮食统计减脂期用户直接拿这个当日记用。这也给系统之后的长期数据沉淀打了基础后续做个性化营养推荐的时候就有了素材。整个架构部署在一台4核8G的云服务器上Ubuntu 22.04系统Docker Compose编排容器划分是nginx反向代理、FastAPI服务、Redis、SQLite数据卷。模型文件以只读模式挂载进容器升级模型只需要替换模型文件后重启服务不需要重新构建镜像。这套部署方案成本低、运维简单一个人就能维护。6. 实测效果与踩坑实录6.1 典型场景实测数据我拿这个系统实际用了将近两个月记录了三类场景的实测效果。单人工作餐场景最常见的情况一份外卖或食堂餐盘包含米饭、一个主菜、一个素菜。这种场景识别效果最好食物边界清晰、容器标准热量估算误差基本在12%以内。尤其对食堂这种固定菜品种类的环境模型识别稳定性和量化精度都很高十次里八次不用手动修正。家庭用餐场景菜品种类杂、量大、多人夹菜导致摆盘混乱。这种场景识别准确率有所下降盛菜的大盘子边缘常常被识别成食物的一部分。误差主要来自面积估算偏大热量估算会高估约20%需要用户在端侧做重量修正。极端场景火锅、麻辣烫、减脂餐。火锅这种锅底和食物混在一起的场景即使是检测模型也很难切分基本只能识别出个别大块食材麻辣烫则因为汤汁和食材混合面积估算完全不可靠减脂餐因为食材分格摆放、颜色分明反而是识别效果最好的场景误差能控制在8%以内。这些测试数据说明一个直白的问题饮食图像识别系统在结构化的场景下表现优秀在非结构化场景下还有明显短板这也是后续做产品时需求边界的参考点。6.2 实际开发中踩到的坑坑一ONNX模型输入输出形状不匹配。YOLOv8导出ONNX时默认支持动态Batch但直接把动态维度交给onnxruntime会报错需要在导出时用--dynamic false固定输入尺寸。这个问题卡了我差不多一天最后在github的issue里翻到解决方案。坑二中餐类别混淆严重。分类模型对“番茄炒蛋”和“西红柿鸡蛋汤”这种视觉特征高度相似的食物经常认混。解决的思路不是继续堆数据而是增加了上下文判断如果同一检测区域同时出现液体的掩膜特征倾向判为汤类如果掩膜更稠密且边缘纹理不规则判为炒菜。这是典型的规则后处理修正模型缺陷的思路。坑三分割模型掩膜边缘锯齿状。YOLOv8-seg输出的掩膜放大到原图分辨率后边缘毛刺很多直接计算面积会把边缘背景也算进去。我用OpenCV做了一个形态学闭运算加轮廓平滑把掩膜做了一次小规模腐蚀再膨胀把这些孤立的边缘噪声去掉后面积计算精度提升了不少。坑四FastAPI接收大尺寸图片耗时爆炸。用户用手机上传原图动不动就三四千万像素解码加缩放就占了接口一半的耗时。处理办法是在nginx层直接限制单张图片大小5MB超过的返回413错误然后让前端在上传前先做一次客户端压缩。实测下来接口响应时间从1.2秒降到400毫秒体验提升非常大。坑五SQLite并发写入锁冲突。早期版本每天饮食记录写入频繁偶尔会出现database is locked错误。后来分析是我在多个worker进程里同时对同一个SQLite文件写数据导致的。解决方案是把SQLite文件挂载到共享卷并开启WAL模式同时把并发写入改成串行队列处理。这个问题对于小体量部署很有代表性数据库的选择要适配实际并发规模。这些坑我觉得比模型指标本身更有价值。模型调优有公开的评测指标和路线图但生产环境里的这些问题每一行都在文档之外都是自己踩出来的。7. 后续优化方向与个人心得跑通这套系统后我明显感觉到饮食图像识别领域真正的护城河不在模型结构而在数据和知识库的积累。这个判断是基于我大量实测得出来的不是理论推演。下一步最值得做的三个优化方向第一是扩充中餐食物类别的知识库把密度、深度、营养成分这些标定数据做得更细同一道菜按不同做法分别建档油多油少各一个条目第二是引入时间序列信息同一个用户每天多次拍照记录前后文信息能大幅提升单次识别的准确性比如早餐识别出的“黄色流质食物”配合时间上下文可以更准确地推测是豆浆还是南瓜粥第三是开发一个轻量级的移动端SDK把模型量化到INT8甚至更低位宽部署到手机端做到真正的端侧实时识别这样用户不需要每次拍照都上传云端隐私保护和体验都能上一个台阶。如果代码能给你启发我建议按这个顺序动手做先跑通YOLOv8的检测demo再接入分类模型两者都稳定后做量化模块最后再考虑部署和前端。倒着搞容易迷失方向因为量化模块依赖前面产出的坐标和类别信息前端又依赖后端的接口稳定。做这个项目最大的感触是AI系统的价值和上限往往不在AI算法本身而在对领域问题的理解深度。图像识别只是“看得见”理解食物构成、估算营养数据、适配用户修正反馈这些才是真正解决用户问题的部分。保持对领域的深耕才能让技术真正落地。本文还有配套的精品资源点击获取

最新新闻

日新闻

周新闻

月新闻