ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

微信小程序食物识别系统全解析:模型训练、前端落地与部署排坑

微信小程序食物识别系统全解析:模型训练、前端落地与部署排坑 你有没有遇到过这种情况朋友聚餐点了一桌菜想估算一下这顿饭大概多少热量却只能靠猜。或者减脂期想记录饮食打开App还要手动搜索菜品名称搜出来的还不一定是自己做的那道菜。我最近就帮团队开发了一个“基于微信小程序的食物识别系统”——用户拍一张照片小程序自动识别出菜品名称、置信度并给出热量、蛋白质、脂肪、碳水等营养信息。这篇文章我把整个项目的方案选型、模型训练、前端落地、部署排坑全过程拆开讲希望能给准备做类似AI识别类小程序的你一些可参考的经验。这个项目适合三类人一是想快速落地一个AI识别Demo的开发者二是做饮食健康、智慧食堂、社区团购相关产品的团队三是对小程序前端和图像识别服务端联调感兴趣的全栈工程师。看完这篇文章你至少能知道食物识别系统应该怎么搭、模型怎么选、小程序端有哪些坑要避开。1. 项目整体架构与方案选型1.1 核心链路拆解从拍照到营养报告整个系统的核心链路并不复杂但涉及的前端、服务端、模型服务三个环节缺一不可。我第一次设计的时候低估了联调的复杂度后面吃过不少亏这里先把完整链路梳理清楚。用户打开小程序后第一步是登录。这里用微信的wx.login()换取临时 code后端通过 code 调微信接口拿到 openid作为用户的唯一标识。这个流程几乎所有微信小程序都一样但要特别注意 code 是一次性的过期时间很短必须立即使用。登录之后进入识别主页面。用户点击按钮选择拍照或从相册选图小程序端拿到临时文件路径后先做图片压缩再通过wx.uploadFile上传到后端。后端接收到图片后调用识别引擎可以是一台部署了模型的服务器也可以是第三方云API得到一个菜品名称和对应的置信度。识别结果返回后后端还要做一件关键的事把菜品名称映射到营养数据库。比如识别结果是“宫保鸡丁”就查出每100克宫保鸡丁的热量、蛋白质、脂肪、碳水再按照用户拍照的食物估算重量计算出这一餐的营养数据。最后整包返回给小程序端展示。这里有个容易忽略的点小程序端拿到结果后要把这条识别记录保存到数据库历史记录页面需要支持列表加载更多。没有这一步用户刚用完就看不到上次记录留存率会非常差。所以整个系统的模块可以拆成四块小程序端、后端鉴权与数据处理、图像识别服务、营养数据库和用户记录库。1.2 识别引擎选型云API、端侧模型还是自建服务这是整个项目最核心的决策点。市面上可选的方案大致有三类直接用云API、端侧跑轻量模型、自建服务端推理。我做了个对比你在选型时可以参照方案延迟精度成本离线可用适用阶段云API腾讯云/阿里云图像识别中需上传图片较高类别丰富按调用量计费不支持快速验证、MVP端侧TFLite/ONNX轻量模型低本地推理受模型大小限制一次开发成本支持对网络、隐私要求高的场景自建PyTorch/TF服务端推理中低取决服务器可控靠训练数据GPU服务器成本不支持业务稳定后的长期方案我的建议很直接如果你只是想快速跑通Demo或者团队没有专门的算法工程师直接用云API最省事。云API的另一个好处是它们已经内置了大量常见食物的类别比如快餐、水果、蔬菜、饮品识别效果在真实场景下往往好过你自己训练的模型。但缺点是按量计费用户量上来之后成本会不好控制而且接口返回的数据结构不一定满足你的营养库映射需求。端侧模型看起来很美好但微信小程序端跑深度学习模型有个现实问题小程序包体积限制严格主包加分包总共不能超过一定大小而一个能用的图像分类模型压缩后也至少要几兆再者微信小程序不能直接运行Python和标准TensorFlow要用TFLite或ONNX必须封装成原生插件开发成本直接上了一个台阶。所以端侧方案我建议放到后面有专门App团队时再考虑。最终我选择的是自建服务端推理理由有三点一是模型可以自己微调针对中国菜品识别更准二是数据都在自己手里后续可以收集用户纠错数据持续优化三是没有按量计费的后顾之忧。如果你没有GPU服务器从云API起步完全没问题架构上做好接口抽象后续替换成自建服务不影响前端。1.3 为什么选择微信小程序而不是App这个项目选微信小程序不是没有理由的。首先是触达成本扫码即用不用下载安装在餐厅、食堂这种场景下体验非常顺畅。其次是登录体系微信自带wx.login()用户无感授权就能建立用户体系省去了短信验证码那一套。第三是分享能力识别结果可以带参数分享到微信群比如“我今天这顿饭大约600千卡”这对产品传播太有帮助了。小程序也有它的短板我踩过最深的坑是API限制。比如图片上传必须走wx.uploadFile对并发和文件大小有隐性的性能压力比如自定义顶部导航栏需要自己适配胶囊按钮的位置不同机型差异很大再比如H5网页跳小程序有严格的域名绑定限制不是随便一个网页都能唤起小程序的。这些都是你在做技术选型时就要心里有数的地方。2. 数据与模型食物识别的地基2.1 数据集与类别体系怎么定食物识别本质上是图像分类任务。模型的好坏七分靠数据三分靠调参这句话在食物识别上体现得淋漓尽致。公开数据集有几个常用的Food-101101类西餐约10万张图、UECFOOD256256类日料、还有中文食物数据集如ChineseFoodNet208类中国菜。如果你做的是偏中式场景ChineseFoodNet的类别结构可以作为起点但它也覆盖不了所有地方菜系。我最开始直接用了预训练模型在Food-101上finetune结果拿回国内实测识别宫保鸡丁、鱼香肉丝这类菜时经常出错。后来发现原因很简单Food-101里根本没有这些类别模型没见过自然识别不出来。所以建议你一开始就确定好类别体系——是识别几十个高频家常菜还是上百个全品类这个决定直接影响数据采集成本和模型精度。我的经验是前期不要贪多把高频菜品覆盖好就够了。比如你就做食堂或家常菜场景那番茄炒蛋、土豆丝、红烧肉、清蒸鱼、米饭、面条这些类别先做好大概20到30个类别。类别少了每个类别的样本量能撑上去模型精度自然就高。用户用起来发现“识别不准”的次数也会少很多体验远好过100个类别但一半都识别错。自建数据集的方法有三种一是从公开数据集里筛选你需要的类别二是通过爬虫和图像搜索引擎采集但这个要注意版权问题三是你自己去拍或者发动身边人一起拍不同角度、不同光线、不同摆盘。我建议实际场景数据至少要占到总样本的三成因为实验室数据和用户手机拍的照片分布差异真的很大。2.2 模型训练与部署细节模型选型上我推荐用迁移学习。具体做法是拿一个在ImageNet上预训练好的模型比如ResNet50、EfficientNet-B0或者MobileNetV3把最后一层全连接替换成你自己的类别数量然后冻结大部分底层参数只微调后面几层和新的分类头。训练时要注意几个细节输入尺寸统一到224x224训练时做数据增强比如随机裁剪、随机翻转、颜色抖动这能显著提高泛化能力。学习率不要太大我用的是1e-4配合Adam优化器batch size设为32。训练完成后看top1和top5的准确率top5意味着模型输出概率最高的5个类别里包含正确类别这个指标对食物识别很重要因为很多菜长得确实像。模型导出的流程我建议这样走PyTorch训练完转成ONNX格式然后用FastAPI包一个识别接口。也可以直接用PyTorch的TorchServe但FastAPI更轻联调起来更灵活。部署时要注意推理延时CPU部署一张图大概几百毫秒到一秒左右如果用户并发高就需要GPU或多实例。还有一个提升体验的技巧把图片先缩放到小尺寸做初步分类再用原图的局部区域做二次校验这个后面对抗误识别很有用。2.3 营养库设计让识别结果真正有用识别出“宫保鸡丁”只是第一步用户真正关心的是这顿饭能不能吃、吃了多少热量。所以营养库是系统的灵魂设计上不能偷懒。我的数据表结构大致是食物名称、所属分类、每100克热量、蛋白质、脂肪、碳水含量以及一份的估算重量。重量估算是个难点因为照片是二维的没有尺子参考很难判断一份菜到底有多少克。我的做法是预设“一份”的参考值比如一份米饭默认200克一份炒菜默认150克然后在小程序端让用户可以选择分量档位少、正常、多对应乘以0.7、1.0、1.3的系数。这样虽然不精确但比硬邦邦给一个数值要好用得多。同一个菜品不同做法热量差异很大比如同样叫“土豆烧肉”有的偏甜有的偏咸有的油多有的油少。所以我在营养库里同一道菜会存几个版本的热量区间而系统默认返回一个中位值。更重要的一点识别结果页必须允许用户纠正菜名用户纠正后要回传到后端作为一条纠错数据存下来后续喂给模型做增量训练。这套机制是提升产品长期体验的关键。3. 小程序端的功能落地3.1 拍照、选图与上传体验的起点小程序端的第一个核心交互是拍照或选图。这里推荐用wx.chooseMedia它能同时支持拍照和相册选择返回的临时文件路径可以直接用于上传。拿到图片后不要直接传原图一定要先压缩。手机拍出来的原图经常是两三兆甚至更大直接上传不仅慢还浪费流量而且后端处理也吃力。我常用的压缩方案是wx.compressImage压缩quality设到80左右同时把图片尺寸限制在最长边1280像素以内。注意内存峰值问题连续多次选大图会导致内存压力我一般会加上文件大小判断超过2MB就压缩。上传用的是wx.uploadFile后端接口接收multipart/form-data文件同时带上用户身份标识。核心代码可以这样写// 选择图片 wx.chooseMedia({ count: 1, mediaType: [image], sourceType: [camera, album], success(res) { const tempFilePath res.tempFiles[0].tempFilePath compressAndUpload(tempFilePath) } }) function compressAndUpload(filePath) { wx.compressImage({ src: filePath, quality: 80, success(res) { wx.uploadFile({ url: https://api.example.com/recognition, filePath: res.tempFilePath, name: file, formData: { openid: getOpenid(), scene: meal }, success(uploadRes) { // 解析返回的识别结果 const data JSON.parse(uploadRes.data) renderResult(data) } }) } }) }这里要提醒一个坑wx.uploadFile默认返回的数据是字符串不是JSON对象一定要先JSON.parse再使用。另外上传接口要配置在“request合法域名”里否则调试时会直接报url not in domain list这个配置是在微信公众平台的开发管理里设置的本地开发时可以在开发者工具里勾选“不校验合法域名”临时绕开。3.2 识别结果页信息展示与交互识别结果页是产品的门面我建议展示四个核心信息菜品名称、置信度、营养数据汇总、营养结构图。置信度用进度条展示给用户一个直观的“可信程度”感知低于某个阈值时提示“识别结果可能不准确请手动选择菜品”。营养数据汇总用卡片形式热量最显眼其次是蛋白质、脂肪、碳水。营养结构图我用的是小程序版的echarts也就是ec-canvas组件画一个环形图展示三大营养素的占比。这块要注意ec-canvas的引入方式需要复制组件目录到项目中并且每个canvas要设置明确的宽高否则图表不会渲染。还有一个交互细节识别结果页底部放一个“这不对重新选”的入口点击后弹出菜品搜索列表用户可以手动选择正确的菜品。这个设计我强烈建议保留它既是兜底方案也是数据回传的入口。用户纠正后的结果连同原始图片要异步POST到后端的数据积累接口。3.3 登录态与用户体系微信小程序登录的标准流程现在依然是前端wx.login()拿到临时code后端调微信的code2Session接口换取 openid 和 session_key。openid是用户在你小程序里的唯一ID后端用这个ID关联用户的历史记录。这里需要注意一个更新微信已经收紧了用户昵称和头像的获取权限不能再直接通过wx.getUserInfo拿头像昵称而是要用button open-typechooseAvatar和input typenickname让用户主动填写。如果你需要展示用户头像和昵称一定要用新能力。登录态建议在后端返回一个自定义token前端存到storage里后续所有请求带上token。不要每次请求都调wx.login()因为频繁换code不仅慢还可能触发微信的风控机制我实测过连续登录会被短暂限制。3.4 历史记录列表的加载更多历史记录页面是用户复访的关键。列表加载更多应该用onReachBottom触发这是小程序页面自带的分页加载事件当页面滚动到底部时自动触发。我的分页逻辑是维护一个page变量初始是1每次请求成功且返回数据完整时page1同时用一个loading标志防止重复请求。后端接口用page和size两个参数控制分页返回records数组和has_more布尔值。前端拿到数据后追加到列表末尾而不是覆盖。这里容易犯的错是网络慢时用户快速滑动连续触发多次onReachBottom所以我加了if (this.data.loading) return的判断。下拉刷新用enablePullDownRefresh配合onPullDownRefresh刷新时重置 page 为1重新拉第一页数据。这算标准做法但没写过的人容易漏。4. 实操过程从零跑通一个可演示的Demo4.1 账号准备与开发工具初始化动手之前先把环境准备齐注册微信小程序账号个人主体和公司主体都能注册但个人主体很多接口权限受限比如不能开通部分类目和支付食物识别这种工具类目个人主体一般够用。然后下载微信开发者工具新建项目时填上小程序的AppID选择不使用模板直接初始化一个空白项目。后端部分如果你没有现成服务器我建议直接用微信云开发。云开发可以看作一整套腾讯云托管的Serverless环境提供了云函数、云数据库、云存储上手快省去申请域名和配置HTTPS的麻烦。但要注意云函数有冷启动延迟如果你要跑图像识别这种耗时操作冷启动加推理时间叠加起来体验会有点紧张。所以我们的模型服务是部署在独立的云服务器上小程序端通过HTTP调用数据库用的自建MySQL历史记录查询走后端API。云开发能吃下简单的数据存储但扛不住推理。4.2 后端服务搭建与模型封装我用FastAPI搭了后端服务核心接口就一个接收图片返回识别结果。模型推理封装在独立的模块里加载好ONNX模型后用ONNX Runtime跑推理。接口逻辑大致如下from fastapi import FastAPI, UploadFile import onnxruntime as ort import numpy as np from PIL import Image app FastAPI() session ort.InferenceSession(food_model.onnx) food_names [宫保鸡丁, 番茄炒蛋, 红烧肉, 米饭, 面条, ...] app.post(/recognition) async def recognition(file: UploadFile): image_data await file.read() img Image.open(BytesIO(image_data)).resize((224, 224)) arr np.array(img).astype(np.float32) / 255.0 arr arr.transpose(2, 0, 1) arr np.expand_dims(arr, axis0) outputs session.run(None, {input: arr})[0] top_idx np.argsort(outputs[0])[::-1][:5] top5 [{name: food_names[i], confidence: float(outputs[0][i])} for i in top_idx] return {code: 0, data: {top5: top5}}真实项目里还会有鉴权、请求日志、超时控制、图片临时存储等逻辑但核心就这么简单。部署时我会用uvicorn起服务前面再挂一层Nginx做反向代理。有一点要特别提醒一定要设置请求超时wx.uploadFile默认超时是60秒如果模型推理慢前端会先等不住。另外FastAPI的UploadFile接收的是生成器如果文件太大要先限制大小防止有人直接用超大图打爆你的内存。4.3 小程序页面核心代码与联调小程序端的页面主要就三个识别首页、结果页、历史记录页。识别首页放一个大按钮和说明文案点击后走选图和上传流程。结果页通过onLoad接收识别结果参数渲染识别卡片和营养图表。历史记录页用列表组件展示之前说到的分页加载。联调阶段我建议先在后端接口代码里打印完整请求体确认图片有没有传上来再打印模型的原始输出确认前5类别和置信度是否符合预期。小程序端先用开发者工具的模拟器调通再用真机预览调拍照因为模拟器里相机是模拟的选图路径和真机不完全一样。真机上遇到的第一个坑通常是域名没配置第二个坑是图片方向错乱这两个我下面详细说。4.4 上线前必须完成的配置发布之前有几个配置项不做你的小程序基本用不了。第一是request合法域名把后端API的域名加进去。第二是业务域名如果你在小程序里嵌了web-view或者需要跳H5必须配置。第三是隐私协议微信现在强制要求在小程序里展示《用户隐私保护指引》尤其是你要上传用户图片必须在后台申明收集“相册图片信息”。第四是类目选择食物识别建议选“工具 效率”或“生活服务”不要选医疗健康类否则审核会要求提供资质。新版微信还要求代码里调用相关API前先触发隐私授权弹窗代码里要判断是否已经授权过。这一步国内很多教程都没提你第一次提交审核可能会被驳回提前写好在代码里可以少走一次提审流程。5. 常见问题与排坑实录5.1 图片方向错乱与体积膨胀真机调试时最容易被一条问题卡一天用手机竖屏拍照上传后后端识别图片居然横过来了。原因是照片的EXIF信息里有方向标记而OpenCV和很多图像处理库默认不读取这个方向直接按原始像素处理就歪了。解决办法在后端读取图片后用PIL的ImageOps.exif_transpose处理一下再送入模型。这个坑不遇到你是真的想不到。图片体积方面我前面已经说了要走压缩但还要注意wx.chooseMedia返回的本身就是临时文件文件实际路径在iOS和安卓上表现不同不要硬编码后缀名直接用返回的路径就好。5.2 接口超时与并发压测我第一次上线前用压测工具模拟20个并发请求结果有一半请求的耗时超过10秒。查下去发现两个问题一是模型推理是单线程的所有请求排队二是FastAPI默认的线程池不够用。解决办法是把模型加载做成单例常驻内存避免每次请求都重新加载推理部分开线程池设置合理的最大并发数再给接口加上请求排队和超时熔断。最终单张图平均处理时间从2秒降到了不到800毫秒。如果你用的是云API这一步就可以省心很多他们自己会处理并发。但自建服务这个坑必须提前排掉。还有一个小经验给模型服务加一层Redis缓存相同菜品的常见图片直接返回上次的识别结果能大幅降低计算压力。5.3 识别结果不准时怎么处理模型识别结果不准是必然事件不要指望一次训练就100%准。处理策略是三层第一层是置信度阈值低于0.6就强制引导用户手动选择第二层是用户纠正把纠错数据记录下来定期重新训练第三层是前端做菜名联想搜索用户手动选择时支持模糊匹配。我还做过一个策略同一个用户短时间多次识别同一张图片时识别结果优先返回用户之前纠正过的菜名占比较高的那一个。这个逻辑虽然朴素但确实提升了用户感知上的准确率。5.4 自定义顶部导航栏的高度适配小程序页面上如果用了navigationStyle: custom顶部导航栏的高度就要自己算。胶囊按钮的位置是固定的可以这样测用wx.getWindowInfo()拿到状态栏高度和菜单按钮的 boundingClientRect导航栏高度 胶囊按钮的 bottom - 状态栏高度 上下留白。不同机型胶囊按钮的位置差很多一定要用API获取不能写死。我之前在iPhone上调试好的页面换到安卓上标题就偏了。5.5 隐私合规与审核踩坑最后聊聊审核。微信对用户信息的收集管得很严你上传图片的行为必须在隐私弹窗里明确说明而且不能在用户拒绝授权后仍然调用相关接口。另一个容易被驳回的点是医疗暗示比如你不能在介绍里写“精准计算卡路里帮助减肥”这会涉及医疗功效表述审核大概率不通过。我后来把文案改成“参考营养数据”低调很多。注意食物识别涉及营养建议时建议加入免责声明如“营养数据仅供参考不构成医疗建议”既能通过审核也规避法律风险。我个人在这个项目里最大的体会是一个AI识别系统的成败模型精度只占一部分产品细节反而决定用户留不留下来。同样是识别一道菜别人给的是一个冷冰冰的名字你给的是“这道菜大概多少热量、三大营养素分别是什么、比一碗米饭多多少”价值感完全不同。最后分享一个小技巧前期一定不要追求识别类别多先把20到30个高频食物做到90%以上的准确率让第一批用户形成“拍一下就准”的感知远比100个类别但经常翻车更能积累口碑。等用户的纠正数据积累到一定规模再逐步扩充类别你会发现模型越用越准这才是一个食物识别系统该有的长期迭代节奏。
返回列表