Flutter · FastAPI · PyTorch · YOLOE · DeepLabV3+

食材识别与营养分析系统

一次从食材切割、分类模型做到 Flutter 应用的课程项目。真正让我印象最深的,不只是模型本身,还有后期联调、反复测试,以及第一次认真面对团队协作中的问题。

最后更新 2026-08-20

这个项目最初的想法并不复杂:拍一张食材照片,软件自动认出里面有什么,再把营养成分显示出来。真正开始做以后,我才发现,难点不是单独把某个模型跑起来,而是让切割、分类、肉类分析、营养查询和 App 展示形成一条完整而稳定的流程。

这是大三《情報システム総合演習》中的三人小组项目。系统支持水果、蔬菜、蘑菇、肉类和海鲜。用户可以用手机拍照或从相册选择图片,系统负责把不同食材分别切出来、判断类别,再查询对应的营养数据。牛肉和猪肉还会继续分析脂肪与瘦肉的比例。

我主要负责食材图像处理、肉类识别、肉类精细分割、脂瘦比例分析、营养数据接入,以及 Flutter 应用与服务器的整体串联。

从单体分类到完整识别流程

最早讨论题目时,我设想的路线是:准备水果、蔬菜、蘑菇、肉类和海鲜数据集,分别训练分类模型,再做一个界面展示结果。如果输入图片正中间只有一个干净的苹果,这条路线确实可以工作。

但用户真正拍摄的照片不会这么理想。一张图里可能同时出现洋葱、番茄、茄子和土豆,旁边还有盘子、桌面、包装、标签甚至人手。把整张图直接交给分类模型,它并不知道应该关注哪一种食材,结果很容易被背景或旁边的东西影响。

所以在回答“这是什么食材”以前,系统必须先回答两个问题:图里有几样食材,以及每一样食材分别在哪里。项目因此从简单分类变成了下面这条流水线:

  1. 从手机相机或相册取得原图。
  2. 找出图中的食材区域,并把它们分别切出来。
  3. 让水果、蔬菜、蘑菇、肉类和海鲜分类模型判断每张切图。
  4. 对牛肉和猪肉继续进行精细分割与脂瘦分析。
  5. 查询营养数据,把完整结果返回 App。

三路分割模型的分工与互补

这里的“分割”可以简单理解为:模型不只说图片里有一个番茄,还要把番茄所在的像素区域圈出来。这样后面的分类模型看到的主要是番茄,而不是整张桌面。

我最后没有让一个模型承担所有类别,而是让三路结果互相补充:

  1. 肉类与海鲜 YOLOE:使用 meat、seafood、beef、pork、chicken 等提示词,专门寻找肉和海鲜。常见果蔬分割模型并不擅长这些类别,所以需要单独补上。
  2. YOLO26n-seg:负责它已经训练过的常见水果和蔬菜。它的类别范围固定,但在熟悉的类别上通常更稳定,因此同一块果蔬被多路模型找到时会优先保留它的结果。
  3. 果蔬与蘑菇 YOLOE prompts:使用 tomato、mushroom、cabbage、onion 等提示词,补充 YOLO26n-seg 没有覆盖或容易漏掉的类别。它使用更高的 960 像素输入,也更适合捕捉画面中较小的食材。

三路一起使用的原因不是“模型越多越准确”,而是它们的能力范围不同。固定类别模型像一个熟悉常见果蔬的专门人员;YOLOE 可以根据提示词临时扩大搜索范围;肉类分支则专门处理外观和果蔬差别很大的对象。任何一路单独使用都会漏掉一部分目标,组合以后才能覆盖我们计划支持的食材范围。

当然,多路模型会同时圈中同一个番茄,也可能把一整片蔬菜切成一个大区域,或者把盘子、桌面和人手带进结果。因此系统会继续比较 mask 的 IoU 和包含率:重叠过高时判断为重复;果蔬 YOLOE 与 YOLO26n-seg 冲突时优先 YOLO26;明确的 YOLO26 果蔬与肉类 YOLOE 冲突时也保留果蔬结果;相邻碎片会合并,面积过大但有效区域很少的复合候选则被过滤。

被保留和被删除的候选都会写入 CSV。这样我看到错误结果时,不只知道“最后认错了”,还可以追溯是哪一路模型圈出的、置信度是多少,以及它为什么在去重时被保留。

分类结果的筛选与决策规则

切出食材以后,每张切图都会分别交给水果、蔬菜、蘑菇、肉类和海鲜模型。刚开始我直接选择五个模型中置信度最高的结果,后来遇到一个很典型的问题:橙子的图片被海鲜模型认成了扇贝,而且海鲜模型给出的分数还很高。

这并不代表海鲜模型真的比水果模型更了解这张图。分类模型通常只能从自己的类别中选一个最像的答案。海鲜模型即使看到橙子,也可能被迫在虾、螃蟹和扇贝中选一个;不同模型的训练数据和输出尺度又不相同,所以 90% 的海鲜结果不能直接和 80% 的水果结果比较。

我最后没有继续用“谁分数最高就听谁的”,而是加入了三层规则:

  1. 类别范围:先看结果来自普通果蔬模型,还是蘑菇、肉类、海鲜等专门模型。Other、Unknown 等否定结果不会当成有效食材。每个模型只在自己负责的范围内参与选择,避免海鲜模型随意接管一张明显属于果蔬的图片。
  2. 最低阈值:普通水果和蔬菜至少需要 45%;蘑菇需要 75%;肉类需要 70%;海鲜提高到 97%。海鲜阈值最高,是因为它在类别范围以外的图片上也可能给出异常高分。肉类自身低于 70% 时则直接返回 Unknown,不强行判断为鸡肉、猪肉或牛肉。
  3. 模型优先级:如果水果或蔬菜模型已经超过 45%,先在这两类结果中选择;只有普通果蔬模型没有可信答案时,才让超过各自阈值的蘑菇、肉类和海鲜模型参与。也就是说,即使海鲜模型对橙子给出的数字更高,只要水果模型已经达到可信范围,系统仍会优先采用水果结果。

这三条规则分别解决“谁有资格回答”“多确定才接受”和“多个可信答案先听谁的”。把它们拆开以后,模型选择不再是一段突然出现的经验判断,而是可以解释、可以调试,也可以针对错误案例继续调整的规则。

从肉类分类到脂瘦分割

肉类是我投入最多的一部分。猪肉和脂肪较多的牛肉颜色接近,只看整体颜色很容易混淆。我使用 ResNet50 提取整体特征,又加入 Gabor 纹理分支,希望捕捉霜降密度、脂肪纹理方向和分布均匀程度。模型输出 Chicken、Pork、Beef 和 Other;猪肉与牛肉接近时,再参考颜色、亮度和霜降特征做小幅修正。

为了分析脂肪和瘦肉,普通的矩形裁剪还不够。白色托盘、包装和标签一旦留在图里,很容易被算成脂肪。我单独训练了 DeepLabV3+ 与 ResNet-101 的二值分割模型,把每个像素判断为肉或背景。模型使用 BCE 与 Tversky Loss,在已有测试数据上取得 Dice 0.978、IoU 0.958、Precision 0.989。

精细分割以后,程序只在肉的区域内分析 HSV、Lab 和 RGB 特征,再通过 K-means 根据当前图片自己的颜色分布分成五组。明显偏白、亮度较高的区域判断为脂肪,红色和桃色区域判断为瘦肉,阴影、反光和边界保留为 unknown。最后得到的是图像中的面积比例,而不是实际称重比例。

从模型结果到完整应用

模型能在脚本里输出结果以后,项目其实只完成了一半。我用 Flutter 制作手机和 Windows 端界面,用 FastAPI 搭建分析服务器。App 负责选择图片、上传、展示进度和结果;服务器按照顺序执行切割、分类、肉类分析与营养查询。

最初接口需要一直等待,用户只能看到页面卡住,不知道服务器有没有工作。后来我把流程拆成“开始分析”和“查询状态”两个接口:服务器先返回 task_id,再在后台执行任务;App 定期查询 queued、running、complete 或 error,并显示“正在保存图片”“正在切割食材”“正在识别”“正在分析营养”等真实阶段。

网络连接也比想象中麻烦。同一 Wi-Fi 不一定真的能够互相访问,学校或公共网络可能开启客户端隔离,Windows 防火墙、错误的 IPv4 地址和把 /health 误填进服务器根地址都会导致失败。我最后为这些情况分别写了提示,也让 App 在分析前主动检查服务器状态。

我还尝试把模型导出为 ONNX,直接放进 Flutter 本地运行。模型能够离线加载,但服务端的 PyTorch/Ultralytics 处理与 Dart 端自己还原 mask 的结果并不完全一致,后处理规则也一度出现偏差。最终正式界面仍以服务器分析为主,但这次尝试让我真正理解了“模型可以导出”和“端侧结果能够稳定复现”是两件不同的事。

团队协作:共识还需要可验证的进度

从第一节课开始,我就在小组里强调要尽量赶进度。每个人同时都有不少课程,我不希望这个项目一直拖到期末,最后影响考试和其他作业。两位组员当时都表达了赞同,我们在目标上其实没有分歧。

但后来我发现,大家都同意“要早点完成”,不等于项目每周真的在按计划前进。我一直在推进自己的部分,也因为相信组员,没有要求每周展示代码、实验结果或可以运行的阶段成果。微信消息有时没有回复,线下讨论过的事情,我也默认它会继续完成。

期中小发表时,对方展示的是 ResNet 相关实验,所以我认为那部分正在正常推进。到了后期,最终拿到的内容却换成了 EfficientNet-B0,完成度和效果与整体需求还有距离。我和另一位组员随后重新接手并补完了不少内容,原本可以用于整体测试的时间也被压缩了。

这段经历没有改变我对最终成果的评价,但它让我分清了两件事:团队成员可以互相信任,项目进度仍然需要通过代码、实验结果和文档来确认。口头共识解决的是方向问题,可验证的阶段成果才解决进度问题。

项目管理:把共同目标拆成阶段成果

如果以后再做同样规模的小组项目,我会继续在一开始就把完成时间往前放,但会把“尽量早点做完”进一步拆成可以检查的节点:数据什么时候准备完成,模型什么时候交出第一次结果,什么时候接入服务器,什么时候用真实手机图片测试。

每周需要确认的也不是“做了吗”,而是这周新增了哪些数据、代码能运行到哪一步、指标和错误案例是什么。这样做不是不信任成员,而是让每个人都能更早发现依赖关系和风险,也避免一个人的工作到了最后才第一次与整个系统连接。

测试同样不应该只是最后确认能不能运行。这个项目中很有价值的改动——多模型去重、复合区域过滤、不同模型阈值、分类优先级、Unknown 机制和异步进度——全部来自真实图片和完整流程暴露出的问题。越早让系统端到端运行,越早能看见真正值得解决的地方。

一套完整运行、让我自豪的系统

我对这个项目最后的感受不是后悔,也不是惋惜,而是自豪。我们没有只完成一份模型实验或演示 PPT,而是做出了一套能够实际运行的系统:手机选择图片,服务器切出多种食材,多个模型完成分类,牛肉和猪肉继续分析脂瘦比例,最后把营养结果和处理过程完整返回 App。

课程项目中,有些小组直到发表时也没能把自己的设想完整做出来。我们的系统不仅能跑,而且从数据、模型、后处理、接口到界面都真正连通了。它支持复杂图片和多种食材,也包含肉类精细分割、营养查询、异步任务与错误提示,这个完成度是我认为非常不错的地方。

它当然还有可以继续提高的部分,例如复杂背景、小目标、遮挡和训练集以外的食材仍可能识别错误,脂瘦比例也只是像素面积估算。但这些边界不影响我对成果的判断。我们选择了一个不简单的题目,最后把它完整地做了出来;而我也第一次把图像分割、分类模型、传统图像处理、服务端和移动端放进同一个可以使用的产品里。对我来说,这就是这次课程最有分量的结果。

主要技术

  • Flutter
  • Dart
  • FastAPI
  • Python
  • PyTorch
  • YOLOE
  • YOLO26n-seg
  • ResNet50
  • Gabor
  • DeepLabV3+
  • OpenCV
  • K-means
  • ONNX

Flutter Dart
制作手机和 Windows 端界面,负责选择图片、上传任务、显示分析进度和整理最终结果。

FastAPI Python
搭建分析服务器,把图片切割、分类、肉类分析和营养查询串联起来,并通过任务状态接口向 App 返回进度。

YOLOE YOLO26n-seg
从原图中寻找水果、蔬菜、蘑菇、肉类和海鲜。固定类别模型负责稳定识别常见果蔬,两套 YOLOE 提示词分别补充肉类、海鲜和容易漏掉的果蔬。

PyTorch ResNet50 Gabor
训练和运行分类模型;肉类模型同时参考整体特征与纹理特征,区分鸡肉、猪肉和牛肉。

DeepLabV3+ OpenCV K-means
去除托盘和包装背景,再根据颜色与亮度把肉区域分成脂肪、瘦肉和不确定区域。

ONNX
用于尝试把模型迁到 Flutter 本地运行,并验证端侧 mask 还原与服务端后处理的一致性。