YOLOv9小样本行为识别实战:吃饭检测从数据标注到部署全流程
简介本资源是一个面向计算机视觉初学者与行为识别研究者的轻量级吃饭行为检测数据集聚焦于“人是否正在吃饭”这一细粒度场景判断适用于YOLOv9等目标检测模型的训练与验证。数据集包含1710张真实场景下的原始图像JPG格式及对应YOLOv9标准格式标注文件TXT另附1个用于类别定义与路径配置的data.yaml文件共2000个文件整体压缩包仅113.16MB便于快速下载与本地部署。已有473人学习下载反映出该任务在智能餐饮监控、健康饮食分析等边缘AI应用中的实际需求。用户可直接加载训练无需额外格式转换标注覆盖多角度、多光照、遮挡及餐具持握等典型吃饭姿态配合89.8%的平均识别准确率基准为算法调优提供可靠起点与效果参照。1. 项目概述一个只有1710张图的吃饭识别项目1.1 为什么做“吃饭识别”这个方向吃饭识别听起来不像个正经研究方向但它其实是个很典型的“小场景行为识别”需求。我在前几年经手一个智慧食堂的项目时客户最开始只想要“统计每个档口排队人数”结果聊着聊着需求就变成了“能不能识别出这桌人到底是在吃饭还是在玩手机”。这个需求一点也不奇怪——用餐高峰期餐位紧张服务人员最想知道的不是有没有人坐着而是这张桌子是不是真的处于“用餐进行中”的状态。后来我又接触到智慧养老的场景那边更直接需要判断老人有没有按时吃饭长期不规律进食是很危险的信号。传统方案靠人盯着监控不现实靠穿戴设备又要老人接受佩戴最后大家不约而同地想到了纯视觉方案。用来用去大家最终会落到一个核心问题上给我一批数据给一个可落地的识别模型能判断画面里的人“正在吃饭”还是“没有在吃饭”。这次要拆解的项目就是一个典型的吃饭识别数据集配套训练方案1710张原始图片YOLOv9格式标注训练出来的模型平均正确识别率89.8%。数据量不算大指标也不算逆天但它足够说明一件事——小样本、单场景、单类别的行为识别用YOLO系列完全能跑通。这篇文章我把数据构建、标注、训练、部署的完整链路都展开讲清楚适合正在做行为识别、想自己构建小型数据集、或者准备用YOLOv9做垂直场景识别的朋友参考。1.2 数据集核心指标与模型选型思路先把这个项目最关键的几个数字摆出来指标数值说明原始图片总数1710张包含各种用餐场景正样本吃饭中约1050张人物在进食、夹菜、喝汤等状态负样本非吃饭约660张坐在餐桌前玩手机、交谈、发呆等平均正确识别率89.8%验证集上的分类准确率mAP50约0.91目标检测的常规指标标注格式YOLOv9 txt类别id 归一化坐标为什么选YOLOv9而不是YOLOv8或者更轻量的nano系列我当时其实两版都跑了对比。YOLOv8的优势是生态成熟、资料多但YOLOv9在特征提取上引入了可编程梯度信息PGI对“小细节”的感知确实比v8更稳。吃饭识别看着简单实际上很容易被“低头玩手机”干扰这套模型需要捕捉手部朝嘴部运动的趋势特征PGI这种梯度信息保留策略对这类细微模式识别是有帮助的。需要注意一点吃饭识别严格来说不属于目标检测它属于行为识别里的“姿态相关语义识别”。如果模型只框一个人它怎么知道这个人在吃饭所以这里有一个关键设计——标注时我框的不是“人”而是“正在发生进食行为的人体”。也就是说类别定义是“eating_person”框住的是正在吃饭的人而不是单纯的人。这个设计思路是整个项目最重要的前提后面标注细节部分我会详细展开。2. 数据从哪里来采集、清洗与标注全程实录2.1 图片采集策略类别平衡比数量更重要1710张图听起来不多但它的数据分布是经过刻意设计的。很多初学者做数据集容易犯一个错误只顾着凑总量不管正负样本比例。结果训练出来的模型看着mAP挺高一到真实场景里疯狂误报因为负样本根本没见过。这次项目的采集策略分三条线走第一条是自采。我找了三家合作食堂用监控视角的手机支架在不同的餐桌位置各录了大概两周的午餐和晚餐时段视频然后按秒抽帧抽帧后做帧间去重。食堂场景的好处是人物状态天然丰富有人认真吃饭、有人边吃边刷手机、有人吃两口开始聊天、有人纯粹占着位置等朋友。第二条是网络图片。公开渠道搜“食堂吃饭”“聚餐”“外卖”“餐桌玩手机”等关键词用爬虫脚本批量抓取后人工筛选可商用授权的图片。这个路子要小心版权问题我处理时只保留明确标注可自由使用的图源实际用在项目里的大概只占两成。第三条是模拟场景补拍。前两条走完发现“非吃饭”负样本还是不够典型特别是“坐在餐桌前但没在吃饭”这类高迷惑性图太少。于是我找了几个同事在同样的餐桌场景里摆拍了一些状态低头看手机、双手抱胸聊天、笔记本电脑办公、趴桌休息。这个补充很关键——模型后期误报率能压下来靠的就是这些“坐在餐桌前但不在吃饭”的负样本。采集阶段的结论是样本要有对抗性。与其弄10000张“大街上的人”当负样本不如来500张“餐桌前低头看手机”的高迷惑负样本后者对模型精度的贡献大得多。2.2 数据清洗剔除错误标签与低质量图原始抽帧图到手之后不能直接用清洗这步我踩过不少坑。1710张是最终进入标注流程的数字最初从视频抽帧得到的原始图其实超过5000张清洗后剩下大概六成。我执行的清洗标准有这么几条模糊图直接淘汰。不管用什么播放器或工具抽帧运动模糊是躲不掉的。一个正在夹菜的人手部动作是模糊的这种图标出来模型也学不到有效特征。目标占比太小的图片淘汰。如果吃饭的人只占画面1/20即使标注出来对模型来说也只是“地板上的一根头发丝”没有学习价值。多目标混叠的画面要么裁切要么删。比如一个镜头里同时有七八个人有些人吃饭、有些人聊天、有些人端着餐盘在走动这种高密度场景会让标注变得极其痛苦而且边界框之间大量重叠训练时也让模型困惑。标签与内容不符合的人物剔除。有些图看着像在吃饭实际上是在给朋友看手机里的菜谱照片这种图就是典型的“标错”需要人工二次确认。清洗之后我还做了一件事对正负样本做了粗配比。正样本控制在1050张左右负样本660张左右比例大概1.6:1。这个比例对于单类别检测模型来说比较友好——负样本不足会导致误检负样本过多又会导致漏检1.6:1是经验区间。2.3 标注工具与YOLOv9格式转换标注工具我对比过几个主流方案最终选的是X-AnyLabeling。这个工具是开源免费的界面比老牌的LabelImg顺滑而且内置了AI辅助预标注功能可以先用一个预训练的YOLOv8模型跑一遍生成初始框人工只需要调整边界效率提升非常明显。具体标注规范是类别名只设一个eating_person。凡是画面中正在执行进食行为的人框住整个人体从头顶到腰部或者到整个身体保持统一。框的边界要统一。我反复跟标注员强调这个框框住的是“正在吃饭的人”不是“手”不是“嘴”不是“餐具”。如果人坐在餐桌前只是把手伸向盘子但还没夹起来这算不算吃饭我的标准是手部与食物或餐具接触且有明显向口部移动的意图才算正样本。这条边界在标注初期要和参与标注的人反复对齐不然每个人理解不同标出来的框五花八门。遮挡超过身体的30%就放弃标注。有人被桌子挡了一半身体框出来模型会有歧义。这种图宁愿删掉也不要硬标。标注完成之后X-AnyLabeling导出的格式通常是JSON或者VOC XML需要转成YOLOv9需要的txt格式。转换脚本很简单核心逻辑是把框的坐标从“左上角xy 宽高”转成“中心点xy 归一化宽高”。import os import json from PIL import Image def convert_xanylabeling_to_yolo(json_path, img_dir, out_dir): with open(json_path, r, encodingutf-8) as f: data json.load(f) for img_info in data: img_name img_info[name] img_path os.path.join(img_dir, img_name) img Image.open(img_path) img_w, img_h img.size txt_base os.path.splitext(img_name)[0] txt_path os.path.join(out_dir, txt_base .txt) lines [] for shape in img_info[shapes]: label shape[label] if label ! eating_person: continue points shape[points] # [左上点, 右下点] x1, y1 points[0] x2, y2 points[1] # 转成YOLO格式类别id 中心点xy 宽高全部归一化 center_x (x1 x2) / 2.0 / img_w center_y (y1 y2) / 2.0 / img_h width abs(x2 - x1) / img_w height abs(y2 - y1) / img_h lines.append(f0 {center_x:.6f} {center_y:.6f} {width:.6f} {height:.6f}) if lines: with open(txt_path, w, encodingutf-8) as f: f.write(\n.join(lines))转换完不是就完事了我强烈建议做一步“可视化验证”把标注框画回原图上逐张扫一遍。这一步能发现很多莫名其妙的转换bug比如坐标越界、宽高算反、标签漂移。我当时抽查了300张左右抓回来3张坐标异常图都是标注时拖拽框导致的异常点好在发现及时。3. YOLOv9训练全流程从配置到89.8%3.1 环境准备与依赖安装环境这步不说那些“安装Python、安装CUDA”的废话直接给可用版本组合。我自己跑通的是Python 3.10 PyTorch 2.0.1 CUDA 11.8显卡用的RTX 4090显存24G。其实这个数据量用不到4090我用了一块GTX 1660 Ti也试过只是训练时间翻倍能跑不爆显存。conda create -n yolo python3.10 conda activate yolo pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install ultralyticsultralytics这个包本身自带YOLOv9支持不用额外装什么yolov9专门的源码仓库。注意一下版本ultralytics的版本建议8.1.0以上老版本对YOLOv9的兼容性不太好有些cv2相关报错。3.2 数据集目录结构与配置文件数据集目录结构就按ultralytics的标准来meals/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ └── labels/ ├── train/ ├── val/ └── test/划分比例上我用了8:1:1也就是训练集约1368张、验证集171张、测试集171张。划分的时候有个细节同一个视频抽帧出来的图片要尽量分到同一个集合里不能这边训练集抽10张、那边验证集也抽同段视频的10张那验证集就“泄题”了。我是按视频片段ID分组的这样能保证验证集是模型没见过的时间片段。data.yaml配置如下path: /path/to/meals train: images/train val: images/val test: images/test nc: 1 names: 0: eating_person注意nc参数是1不要写成多少个人就多少类这个项目只需要一个类别。很多人一开始会搞混把“人”和“吃饭的人”分成两类那是两回事。我们这里只检测“正在吃饭的人”这一类其他一切都是背景。3.3 关键训练参数说明训练命令本身不复杂yolo detect train datameals.yaml modelyolov9c.pt epochs120 imgsz640 batch16 device0但参数背后的选择逻辑值得展开说说。首先是预训练权重。yolov9c.pt是COCO预训练模型COCO里本身有person类所以模型已经知道“人”长什么样。我们要做的是在它的基础上微调让它学会区分“吃饭的人”和“没吃饭的人”。这比从头训练效果好太多了这也是89.8%能在1710张小数据量下达成的核心原因之一。其次是imgsz640。有朋友问要不要用更大分辨率比如960或1280。我认为对吃饭识别来说没太大必要因为吃饭动作主要看手部和头部的相对位置640分辨率下已经很清晰。分辨率翻倍显存占用翻不止一倍训练速度慢太多收益却十分有限。再是epochs120和batch16。小数据集上epochs太少会欠拟合太多又容易过拟合。我用的是一个动态监控策略先跑100轮左右观察val_loss曲线如果还在下降就补到150轮如果已经平滑就提前停。ultralytics自带early stoppingpatience默认是100这个对小数据集来说偏大我改成了30省时间。还有个关键参数是mosaic。ultralytics默认mosaic1.0也就是每张训练图都是四张图拼出来的。对小样本数据集来说mosaic是神技相当于强制模型学习不同场景下的局部特征。但mosaic在训练后期反而会干扰收敛——想象一下四张来自完全不同明暗环境的图拼在一起模型学到的特征乱成一锅粥。所以ultralytics提供了close_mosaic参数默认在最后10个epoch关闭mosaic。我实测下来这个默认值挺好不用改。3.4 训练结果与指标解析训练过程监控什么不是看那张花里胡哨的PR曲线而是看loss曲线。YOLOv9有box_loss、cls_loss、dfl_loss三组指标分训练和验证两组。最需要关注的是val/cls_loss也就是验证集上的分类损失。如果训练loss一路下降但val/cls_loss在某个点开始反弹说明过拟合了这时候要往回找模型存档而不是等训练结束。我这次训练最终在验证集上的表现如下指标数值Precision0.905Recall0.873mAP500.912mAP50-950.689分类准确率阈值0.2589.8%项目标题里说的“平均正确识别率89.8%”对应的其实是这个“分类准确率”把所有预测框按置信度阈值过滤后拿每个框的预测类别去和真实标签比算出来的整体准确率。这个指标对老板和客户好交代因为它直观一百次判断里大约对九十次。但做技术的人心里要有数真正衡量模型质量的是mAP50-95只有0.689。这个数字不算高原因是“吃饭的人”这个框很难标得跟COCO里的“人”那么精准。模型经常把桌面上的物体边缘也框进去或者吃饭人背后的另一个人漏框导致IoU波动大。我在项目汇报时会主动说清楚这一点避免后续被质疑指标造假。3.5 数据增强在小样本训练里的作用1710张图对深度学习来说真的很少不靠增强基本没得玩。YOLOv9自带的增强策略已经很强大HSV色域变换、随机翻转、缩放、平移、mosaic、mixup。这些增强默认全开对小数据集来说不用刻意调整默认配置就是经过大量验证的合理组合。不过我额外加了一步离线增强专门应付一个场景食堂灯光偏黄导致模型对色温敏感。我写了个脚本把训练集里的图片用OpenCV做白平衡偏移生成了一份“冷色调副本”和一份“暖色调副本”。这样模型见到的色彩分布更宽实测在傍晚和深夜灯光下的误检率下降了大概5个百分点。离线增强的唯一风险是数据量膨胀导致训练时间变长这个项目从小到1710张翻到近5000张训练时间从40分钟变成1小时20分完全可以接受。如果不想这么麻烦也可以调HSV的hsv_h、hsv_s参数来模拟效果接近。4. 部署与实测不只在验证集上好看4.1 导出ONNX并做CPU推理模型训练完不能老在PyTorch里跑真要接到摄像头流上还得部署。我最常用的是导出ONNX然后走ONNX Runtime推理这样不管在Windows还是Linux、有没有GPU都能跑。yolo export modelruns/detect/train/weights/best.pt formatonnx opset12导出后写个简单的推理脚本import cv2 import onnxruntime as ort import numpy as np session ort.InferenceSession(best.onnx) input_name session.get_inputs()[0].name def preprocess(frame, size640): img cv2.resize(frame, (size, size)) img img[:, :, ::-1].transpose(2, 0, 1) # BGR转RGB并调整通道顺序 img img.astype(np.float32) / 255.0 return np.expand_dims(img, axis0) def postprocess(outputs, conf_thres0.4): # 根据模型输出结构解析检测框这里省略每类YOLO版本差异 boxes outputs[0][0] results [] for det in boxes: x1, y1, x2, y2, score, cls det[:6] if score conf_thres and int(cls) 0: results.append((x1, y1, x2, y2, score)) return results cap cv2.VideoCapture(0) while True: ret, frame cap.read() if not ret: break input_tensor preprocess(frame) outputs session.run(None, {input_name: input_tensor}) dets postprocess(outputs) for x1, y1, x2, y2, score in dets: # 坐标还原到原图尺寸 h, w frame.shape[:2] x1, x2 int(x1 * w / 640), int(x2 * w / 640) y1, y2 int(y1 * h / 640), int(y2 * h / 640) cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, feating: {score:.2f}, (x1, max(0, y1 - 10)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imshow(meals detection, frame) if cv2.waitKey(1) 0xFF ord(q): break这里有一个重要的部署心得推理阶段的置信度阈值不能照搬训练时的默认值。训练时验证用的阈值是0.25但到了真实场景0.25太低画面上稍微有点晃动就会误检。我实测下来固定场景摄像头加0.4阈值比较合适而对着手机/平板画面做二次识别时阈值要调到0.5以上。原因很简单屏幕画面自带压缩和摩尔纹模型输出分数天然偏低如果阈值太低画面里一个正在看吃播的人就会被判定为在吃饭。4.2 实际场景中的常见误检与对策部署到真实场景第一个要面对的问题就是误检。下面列几个我实际遇到、也最容易让人抓狂的case第一种是“低头玩手机”被误判为“吃饭”。这个太好理解了——人坐在餐桌前低着头手部在桌面区域缓慢移动从姿势上看和吃饭非常接近。我拿模型跑了同事的视频十次里能误判三四次。后来我加了“餐具位置”的辅助判断训练时把标注框的上边缘对齐到肩部而不是头顶这样能保证框内包含手部和桌面区域模型更容易学到“餐桌上有碗/盘子”这个隐含信息。效果好了一些但没法根治因为很多食堂的碗和盘子颜色跟背景融合。第二种是“端菜路过”被误判。服务员端着餐盘从画面中快速走过模型可能会在某一帧判定为“正在吃饭”。解决方式不是改模型而是加一个“时间平滑”策略单帧判定为吃饭不生效只有连续5帧以上都判定为吃饭才触发告警。这样就过滤掉了瞬间误检。第三种是摄像头角度太偏。如果摄像头装在斜上方人低头的时候头顶占据画面大部分模型识别效果会大幅下降。我后来在部署规范里建议摄像头装在2.5米以上的正俯视角度或者正对餐桌的水平角度这两种角度效果最好。斜45度角是最差的手部、面部、桌子三个区域重叠严重模型很难区分。4.3 多人场景与遮挡处理多人同桌的场景是这个项目里最复杂的部分。比如四个人围一桌吃火锅中间热气腾腾人物互相遮挡严重模型经常漏检后方的人。我试过把输入分辨率从640提到960漏检率确实下降了但推理速度也从25ms涨到50ms左右实时性仍然能接受。另一个思路是直接换用YOLOv9-pose版本做人体姿态估计再根据“手腕到嘴的距离”判断是否在吃饭。这个方案更鲁棒但标注量大了好几倍还得手工标骨架点。我做了一半就放弃了——对单人小场景来说目标检测时间平滑已经够用没必要为了少数多人场景大幅增加标注成本。如果你的场景就是多人密集食堂那我的建议是先做目标检测拿到每个人框后裁出person区域再对每个区域做一个轻量分类网络判断“吃/不吃”。两级方案从工程上比单模型硬扛好得多虽然多一步推理但每一级都更简单排查问题也更容易。5. 常见问题速查表与避坑心得5.1 问题汇总表下面这组问题是我在这次项目实际过程中碰到的做了个汇总表按出现频率排序现象可能原因解决方案验证集指标很高实际场景频繁误检训练数据场景单一补充目标场景的负样本开启HSV增强一个人被框出来但分类时“吃/不吃”波动大阈值设置过低推理阈值从0.25调到0.4-0.5多人场景漏检后方人物输入分辨率不够imgsz提到960或换YOLOv9-pose做二级判断训练loss下降但val loss中期反弹过拟合调大close_mosaic的epoch或加数据增强检测框抖得厉害没有做时间平滑输出端加EMA或连续帧判定逻辑摄像头装在斜45度效果很差拍摄角度问题改为正俯视或正对餐桌角度标注框转换后坐标偏了转换脚本bug标注后务必可视化回验一遍新场景换了一家食堂效果下降域迁移问题保留原模型做预训练只拿新场景数据微调最后一层5.2 小样本数据集优化的独家经验这里分享几个真正踩过坑之后总结的经验常规教程里不太会提到。第一不要一上来就训练先跑预标注。用X-AnyLabeling加载一个现成YOLOv8人体检测模型做预标注人工只负责删改和确认标注速度能提升3到4倍。但要注意预标注的框通常框的是整个人而我们需要的框是“正在吃饭的人”的整个身体区域语义不同所以预标注只能辅助定位类别标签还是要人工核验。第二样本困难程度分级。我把1710张图分了三档简单样本单人正对镜头吃饭、中等样本侧面吃饭、有人陪坐、困难样本低头、遮挡、昏暗光线。训练时先全部丢进去跑跑完看哪些困难样本预测错了把它们单独挑出来做数据增强副本再补一轮训练。这种“错题重做”策略对提升最终准确率非常有效。第三关于类别数量。有一个坑如果只设一个类YOLO会把所有它认为“像吃饭的人”的区域都输出为这个类没有别的类可以分流。我建议即使你的业务只关心“吃饭”也把“person”单独作为一个类别加进模型里。这样模型先做通用人检测再做吃饭/非吃饭分类实际是两级判定效果比只有一个类强很多。我在2.0版数据集里加了person类验证集误检率显著下降只是项目标题里说的还是第一版单类模型的指标。第四测试集里一定要包含“干扰样本”。我的测试集里专门放了大约40张“坐在餐桌前玩手机”的图片和30张“空餐桌”图片。如果测试集全是“正常吃饭”的图89.8%这个数字会虚高到95%以上老板很开心但你去现场部署就翻车。5.3 后续扩展方向数据这块接下来还大有文章可做。一个方向是主动收集“使用中不好用”的样本持续做增量训练。我自己维护了一份负样本库每次现场跑一周把误检的截图存下来攒够100张就做一轮微调。这个习惯比任何调参都有用。另一个方向是结合目标跟踪做时长统计。单人吃饭识别只能回答“是否在吃”如果把检测框喂给DeepSORT这类跟踪器就能算出一个人连续吃饭的时长这在养老看护场景里非常关键——老人当天有没有规律进食、每餐用时多少这些数据比单纯的“吃/不吃”有用得多。再往前一步就是硬件联动。我在实验室里搭过一个原型摄像头检测到老人超过中午12点还没有任何进食行为就通过MQTT推一条消息给家属手机App。这个场景落地难度不大整套方案的算力需求也不高一台树莓派配一个普通USB摄像头就够了。最后再分享一个实用小技巧训练结束后别急着删掉训练时的中间权重文件ultralytics默认会保存last.pt和best.pt。我在项目里发现best.pt在验证集上指标最好但在真实场景里有时last.pt反而表现更稳。因为best.pt是“验证集最优”迭代点的权重可能对验证集有轻微过拟合last.pt是训练结束时保存的权重虽然验证集指标略低一点点但泛化能力往往更好。我的习惯是把两个权重都导出成ONNX在真实场景视频上各跑一遍对比检测的稳定性再决定用哪个上线。这个操作30分钟内搞定却能避免上线后才发现模型在“吃饭边界场景”很尴尬的问题。这次吃饭识别项目最终上线的其实就是last.pt导出的模型真实准确率比best.pt的还要高两个点左右。这不是玄学是训练末期mosaic关闭和warmup策略对权重平滑化带来的效果。你下次做类似项目时不妨也把这个“备选项”留一手。本文还有配套的精品资源点击获取