ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

椅子人物桌子目标检测:从数据集清理到YOLO训练部署全攻略

椅子人物桌子目标检测:从数据集清理到YOLO训练部署全攻略 简介面向室内监控、智能家居与机器人等应用场景的椅子、人物、桌子目标检测数据集适配YOLO系列主流框架主要服务计算机视觉研究者、算法工程师及教学实训人员用于解决室内环境中常见物体检测与空间定位问题。资源共854个文件以426张jpg图片和426个配套txt标注文件为核心另含1个yaml配置文件和1份docx说明文档压缩包整体44.55MB结构清晰便于快速接入YOLO训练流程。目前已有105人学习下载。全部图片均按YOLO格式标注边界框与类别标签类别严格聚焦椅子、人物、桌子三类数据一致性较强图片来自多样化真实室内场景覆盖不同角度、光照与布局有助提升模型泛化能力。借助随包的yaml配置文件与说明文档可快速适配主流检测框架直接用于模型训练、精度验证或算法研究也可作为智能视觉系统前期数据集。1. 椅子人物桌子目标检测数据集.zip先想清楚这三类目标到底要解决什么在真实巡检、会议室或门店监控画面里出现频率最高又最难同时兼顾的往往就是椅子、人物、桌子这三类目标。椅子外形规整但经常被桌子和人挡住人物在画面里持续走动和转身桌子的边缘纹路又容易跟地面阴影融在一起。一份按椅子、人物、桌子三类目标整理好的目标检测数据集恰好卡在这个需求缺口上它不像通用目标检测数据集那样铺几百个类别却能用很小的训练成本把室内场景的人员与家具检测跑起来。这篇文章会把这套数据从解压、校验、清洗到搭训练集、跑通模型和部署验证的完整路径讲清楚同时把最容易踩坑的环节提前摆到台面上。适合谁看刚拿到这类数据压缩包想做快速验证的开发者或者在室内巡检项目里被误检率折磨的工程师。下面所有命令都按 YOLO 标注格式和常见开源训练框架来写可以直接复制但每段命令后面我都会点出参数背后的坑避免你按默认值一路跑到底才发现翻车。2. 解压之后先做这三件事目录骨架、标注格式与质量校验拿到 zip 解压的第一反应别急着往训练脚本里塞。这个压缩包里可能是 YOLO 格式也可能是 VOC 或 COCO 风格标注目录结构不同后面的处理方式差很多。常见做法是先围绕三个点做检查目录结构、标注格式、类别编号。这三件事不确认清楚后面每跑一轮训练都在给错误数据交学费。2.1 用三条命令看清目录骨架和样本量先把压缩包解到 dataset 目录再跑一遍树状查看我一般会看顶层目录长什么样而不是直接去看图片长什么样。cd dataset tree -L 2 -d echo --- 统计图片和标注文件数量 --- find . -name *.jpg | wc -l find . -name *.txt | wc -l如果是 YOLO 格式通常能看到 images 和 labels 两个顶层目录下面再按 train 和 val 拆开。如果解压出来是 JPEGImages 加 Annotations 的布局那多半是 VOC 风格后面转换脚本就躲不掉了。先说这条命令的含义tree -L 2 -d只显示两层目录树避免整个包一出来就被密密麻麻的文件刷屏find加wc -l是为了快速确认图片和标注是否一一对应数量差得离谱就说明有损坏或漏标。提示如果压缩包里带中文目录名训练框架大概率会在 Windows 上读路径失败解压完先改成小写英文目录名这一步很多新手会翻车。接下来用脚本对一下每张图片是否都有标注并把缺失列表输出到文件。import os from pathlib import Path base Path(dataset) missing [] for img_path in (base / images / train).glob(*.jpg): # 把图片文件名后缀换成 .txt得到对应的标注路径 label_path base / labels / train / (img_path.stem .txt) if not label_path.exists(): missing.append(str(img_path)) print(f缺标注的图片数: {len(missing)}) for item in missing[:10]: # 只看前 10 个先判断规律 print(item)这段脚本的逻辑很简单但解决的问题很实际训练时一张图没有标注轻则报错重则被当成负样本参与反向传播把本来正确的预测拉偏。glob(*.jpg)只匹配 jpg如果样本里有 png 就要补一个glob(*.png)否则数量统计会漏。缺标注的图片如果只是零星几张可以先筛出去不参与训练如果缺了一大批那说明压缩包在制作时目录搬运出了问题需要回头检查 zip 的完整度。2.2 类别清单文件是数据的地基先核对 classes 顺序很多压缩包会带一个 classes.txt里面每一行就是一个类别名顺序和标注文件里的数字编号一一对应。椅子人物桌子这类数据最常见的问题是类别顺序在不同压缩包版本之间变过比如 A 版本是 chair 0、person 1、table 2B 版本突然变成 person 0、chair 1。如果没仔细核对就用模型会把椅子当成人物去学整个训练过程变成一次白跑。cat dataset/classes.txt wc -l dataset/classes.txt这两条命令不是摆设。第一条看类别名第二条确认类别数量。如果 classes.txt 是三行训练配置里 nc 就必须是 3写 4 或者 2 都会在训练时报索引越界。同时要留意wc -l的结果是不是比实际类别数多一行因为某些文件末尾多了空行会让脚本误判类别数。接着用一个小脚本统计标注文件里的类别编号分布这一步能直接暴露标注错乱问题。import os label_dir dataset/labels/train max_class_id 0 class_count {} for fname in os.listdir(label_dir): if not fname.endswith(.txt): continue with open(os.path.join(label_dir, fname)) as f: for line in f: parts line.strip().split() if len(parts) ! 5: print(f格式异常: {fname}, 行内容{parts}) continue cls_id int(parts[0]) max_class_id max(max_class_id, cls_id) class_count[cls_id] class_count.get(cls_id, 0) 1 print(出现过的类别编号:, sorted(class_count.keys())) print(每个编号对应的目标数:, class_count)这里对标注做了两层校验一是检查每一行是不是五个字段缺少字段说明标注导出时丢了坐标二是统计出现过的类别编号看有没有超出 classes.txt 范围的编号。如果出现编号 3但类别文件只有 chair、person、table 三个那就说明某张图的标注生成环节出过问题这种脏数据必须在训练前清理掉否则模型会去拟合一个不存在的类别。2.3 从 VOC/COCO 转换到 YOLO txt 的坐标映射逻辑若数据包给的是 XML 或 JSON 标注第一步需要先转换成 YOLO 的归一化 txt。转换的核心公式是中心点 x 等于 xmin 加 xmax 除以 2 再除以图片宽度中心点 y 同理宽高也各自做归一化。这里最容易出错的是拿 xmin、ymin 直接当中心点用出来的框会整体偏移到右下角。import xml.etree.ElementTree as ET from pathlib import Path class_index {chair: 0, person: 1, table: 2} def convert_voc(xml_file, img_dir, out_dir, class_index): xml_path Path(xml_file) if not xml_path.exists(): return tree ET.parse(xml_path) root tree.getroot() # size 节点记录图片的真实宽高坐标归一化必须依赖它 img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) img_name root.find(filename).text label_lines [] for obj in root.findall(object): cls_name obj.find(name).text.strip() if cls_name not in class_index: continue box obj.find(bndbox) xmin float(box.find(xmin).text) ymin float(box.find(ymin).text) xmax float(box.find(xmax).text) ymax float(box.find(ymax).text) # 关键转换句所有坐标都要除以真实宽高 x_center (xmin xmax) / 2.0 / img_w y_center (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h label_lines.append(f{class_index[cls_name]} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) out_xml Path(out_dir) / (xml_path.stem .txt) out_xml.write_text(\n.join(label_lines)) convert_voc(Annotations/000001.xml, JPEGImages, labels, class_index)这段代码里class_index必须和训练时的类别顺序完全一致尤其是 chair、person、table 的顺序不能乱否则转换出来的数字没有意义。size/width和size/height是从 XML 里读的如果原图被裁剪过、而 XML 里的 size 没更新归一化结果就会偏。基于这个逻辑转换完成后我习惯随机抽几张图把 txt 里的坐标反推回像素在原图上画框对比验证确认框是不是准确压在物体边缘。这一步虽然费点时间但能堵住最大的一类数据坑。3. 把原始标注变成能直接投喂训练模型的数据集划分、增强与 YAML 配置解压、校验之后常规动作是把数据切成训练集和验证集并在开始训练前想好增强参数和 yaml 配置。这个章节决定了模型最终能不能泛化做得草率后面训练会反复返工。3.1 按场景划分而不是纯随机防止验证集“偷看”很多教程让人用 shuf 随机把文件切到 train 和 val 两个目录但室内数据集往往有个隐藏问题同一张桌子或同一把椅子会在连续帧里反复出现。如果划分不按场景按帧验证集里会出现和训练集几乎同视角的图像mAP 虚高一截实际换一个角度就崩。常见做法是先给图片按目录或文件名前缀分组再按组划分。import os import random import shutil from collections import defaultdict img_root dataset/images label_root dataset/labels train_img dataset/images/train val_img dataset/images/val train_lbl dataset/labels/train val_lbl dataset/labels/val for d in [train_img, val_img, train_lbl, val_lbl]: os.makedirs(d, exist_okTrue) all_imgs [f for f in os.listdir(img_root) if f.endswith((.jpg, .png))] # 用文件名前缀作为场景 id避免连续帧被分进两个集合 scene_groups defaultdict(list) for img in all_imgs: scene_groups[img.split(_)[0]].append(img) scene_ids list(scene_groups.keys()) random.Random(42).shuffle(scene_ids) # 固定随机种子保证每次划分一致 val_scenes set(scene_ids[: max(1, int(len(scene_ids) * 0.2))]) for scene_id, imgs in scene_groups.items(): is_val scene_id in val_scenes for img in imgs: src_img os.path.join(img_root, img) dst_img os.path.join(val_img if is_val else train_img, img) shutil.move(src_img, dst_img) # 标注文件必须跟着图片一起移动 label_file img.rsplit(., 1)[0] .txt src_lbl os.path.join(label_root, label_file) dst_lbl os.path.join(val_lbl if is_val else train_lbl, label_file) if os.path.exists(src_lbl): shutil.move(src_lbl, dst_lbl)这段代码的关键是img.split(_)[0]作为场景分组键只要同一场景的帧有相同前缀就会被整体划到同一集合里。random.Random(42)是固定随机种子重新切分也能得到完全一致的结果。这里有一个细节如果图片目录里混着无标注样本脚本会因找不到对应标签而报错所以前面 2.2 里的校验脚本必须先跑一遍再切分。文件名没有统一规范的需要先用正则把场景 id 提取出来比如从IMG_2024_0715_143001.jpg里取IMG_2024_0715。3.2 增强参数取舍翻转、HSV 和 mosaic 不是全开就好椅子和桌子这类刚性物体占大头增强重点应放在颜色抖动和几何扰动人物形态变化大轻量旋转有帮助。常见做法是开启 mosaic 并小幅度调整 HSV翻转概率设在 0.5 左右。但这里有个反直觉点桌面面积大如果翻转概率太高会让模型过于依赖对称性假设对斜放的桌子反而容易犯错。增强项常见默认值在椅子人物桌子数据上的建议说明mosaic1.00.8拼图增强能制造遮挡场景但桌子大面积交叠时会生成错误遮挡样本flipud0.00.0~0.1室内场景几乎没有上下翻转的桌子开大了会引入异常样本fliplr0.50.5左右翻转对椅子和人物基本无害hsv_h0.0150.02会议室灯光色温变化多轻微色偏增强有助泛化hsv_s0.70.5饱和度增强太强会让深色椅子跟阴影混在一起degrees0.05~10俯拍或斜拍场景适合小角度旋转超过 30 度会产生离谱的桌子框这个表不是让你照抄而是给你一个调整方向。如果你手里的样本本来就是俯拍视角degrees 可以开到 15如果是平视监控视角degrees 保持在 5 以内。验证方法很简单开启增强后随机抽 200 张训练输出人眼扫一遍凡是出现两个桌子框叠在一起但语义完全说不通的情况就把对应增强强度降下来。数据增强是玄学但前提是增强后的样本不能违背场景基本物理规律。3.3 写好 data.yaml 的三条规则路径、类别顺序和名称三位一体任何 YOLO 系列训练框架都靠 yaml 文件描述数据集。路径错了会直接报找不到图片names 顺序错了会导致整个训练在错误映射上白跑nc 写错则会在 loss 计算时报维度不匹配。我一般用脚本自动生成 yaml不用手写让路径和 names 保持一致。# data.yaml path: dataset/ # 相对于 yaml 所在位置的根目录 train: images/train # 训练图片相对路径 val: images/val # 验证图片相对路径 nc: 3 # 类别数量必须等于 names 的长度 names: # 类别名称顺序必须与标注 txt 里的数字一一对应 0: chair 1: person 2: table提示:这里 names 顺序必须和标注 txt 里的数字一一对应和 2.2 里检查的 classes.txt 也应完全一致。如果后面想加新类别不要把新类别插到中间只能追加到末尾否则旧标注的编号全部错位模型等于重新学一遍。一个很实用的自查方式是训练开始后看第一轮输出的类别名称列表如果显示成了 person、chair、table就说明 yaml 里的顺序和标注对不上立刻停掉修正。4. 用 YOLO 训一个椅子人物桌子检测模型参数基准、曲线判读与剪枝数据准备完接下来是把模型真正训出来。这一章重点讲两件事基线参数怎么定、训练曲线怎么读最后补一下导出部署格式时的常见误区。4.1 基线训练从 300 轮开始分辨率别一上来就调大椅子、人物、桌子在室内场景里尺度差异不算极端基线用 640x640 输入、300 轮是稳妥起点。显存不够就把 batch 调小不要优先降分辨率因为桌椅边缘对分辨率敏感降到 416 会让小尺寸椅子的召回明显下降。yolo train datadataset.yaml modelyolov8n.pt epochs300 batch16 imgsz640这里的modelyolov8n.pt指的是轻量级预训练权重如果你从随机初始化开始训三个类别的收敛速度会慢很多。batch16在消费级显卡上比较舒服如果显存吃紧先把 batch 降到 8不要急着把 imgsz 降成 512类别少时 batch 太小会让 BN 统计不稳定。epochs300看起来多但对于目标检测任务前 100 轮基本是在让分类头收敛后 100 轮才是框回归精度提升的关键阶段。4.2 训练曲线要这样读loss 降、mAP 拉过拟合看三条线训练跑起来后别只盯着 loss。检测任务的 loss 包含分类和回归两个分量正常迭代时 train loss 持续下降val loss 应该在拐点附近下行。如果在第 200 轮出现 val loss 反弹而 train loss 继续降就是过拟合信号优先做两件事减小 epochs 或增大增强的 mosaic 概率。训练结束后框架会在权重目录输出 results.png 和 PR 曲线图重点看metrics/Precision(B)、metrics/Recall(B)和val/Box_loss三条线。一个常见误读是只关心最终 mAP忽略了 PR 曲线形态如果 Precision 一路上到 0.9可 Recall 卡在 0.5说明模型偏保守宁可漏检也不误检这在人员密度高的场景里不适用需要降低置信度阈值再做一次验证。看每个类别的 AP 也有价值chair 是小目标AP 通常会略低于 person这是正常的如果 person 的 AP 反而垫底多半是人物姿态样本不够先回数据层面看不要继续调参。4.3 从训练权重到部署格式导出 ONNX 与量化取舍训练结束后输出 best.pt 只是第一步实际部署还得转成 ONNX 或 TensorRT 格式。常见做法是先导出 ONNX再按硬件需求做 half 精度或 int8 量化。yolo export modelbest.pt formatonnx imgsz640 halfTruehalfTrue把模型转成 float16体积缩小一半在推理卡上速度提升明显且精度损失通常很小。如果更想压缩体积可以再走 int8 量化但量化的精度损失往往不是均匀的chair 和 table 这类边缘锐利的目标掉点明显person 相对抗压。我一般会在量化前后拿同一段验证视频对比 Precision 和 Recall如果 float16 和 int8 差了超过两个点就用 float16 放弃 int8别为了体积牺牲可用性。提示:导出时 imgsz 必须和训练时的输入尺寸一致否则输出特征图尺寸变化后处理解析会错位。很多同学训练用 640导出时顺手填了 416然后找半天为什么推理结果对不上。5. 避坑与常见问题排查标注错位、类别失衡和漏检根因这一章把最常见、最容易让人崩溃的四个问题按实际现象、背后原因和处理方式拆开。每一条都是我在真实项目里遇到过的按场景对照排查能省下大半天的时间。5.1 所有框整体偏移标注坐标和图片尺寸不匹配现象训练前画图验证发现所有框整体向右下角偏移框中心和目标中心对不上同一个像素位置。 原因最常见的是转换脚本里把 xmin、ymax 直接当成中心点坐标其次是 XML 里记录的图片尺寸和实际图片不一致比如某次数据增强后图片被裁剪但 XML 里的 size 没跟着变归一化后坐标全部偏差。 解决重新用 2.3 的转换脚本生成标注并加一步自动校验把每张图的宽高和 XML 里的 size 做对比不等就报警。随后随机抽 20 张图画框人眼确认框是否压在目标边缘。这一条属于基础性错误只要画框验证做得认真基本不会留到训练阶段。5.2 人物样本过万椅子只有几百张类别不平衡导致椅子 AP 崩掉现象训练完成后person 的 AP 接近 0.85chair 的 AP 只有 0.4table 在中间整个 mAP 被椅子类别拖累。 原因数据集里类别样本数量差距超过十倍训练时每个 batch 的椅子样本稀少梯度贡献被人物主导模型学不会椅子的形变规律。 解决先做数据层面的均衡常见做法是对椅子样本做复制粘贴增强把同一张椅子的多角度样本复制到不同背景里增加有效样本量同时把 mosaic 增强提高相当于每张训练图覆盖更多密集家具。如果还不行再考虑在训练批次里做 oversample保证每个 batch 都包含足够数量的 chair 标注。这一条不要试图用 loss 权重硬拉数据层面先把样本数量差距缩小才是正路。5.3 桌子底下和人物身边的椅子频繁漏检现象独立放置的椅子检出率很高但桌子底下、人起身带翻的椅子漏检人员走动时干扰特别明显。 原因缺遮挡和截断类样本。独立椅子占训练集比例高模型学到的是完整轮廓现实场景里桌子底下椅子通常只露出一半人物经过时还会构成二次遮挡。 解决把 mosaic 增强概率调到 0.8 以上人为制造遮挡训练样本再单独组织一小批半遮挡椅子的真实样本补到训练集里。注意一个常见误操作有人把完整标注的框切掉一半然后保留切掉后的框作为新标注这样模型学到的是“一半椅子能检测”但另一半椅子的语义信息被丢弃了。正确的做法是保留完整框让遮挡部分纳入回归学习模型才能学会推断被遮住的结构。5.4 改了类别顺序整个模型的输出全部乱套现象调整 yaml 里的 names 顺序后原本能用的验证集 mAP 掉到接近零推理时 chair 框标成 person。 原因标注 txt 里的数字编号没有跟着改。YOLO 标注第一列写的是类别编号模型输出的类别索引要和 yaml 中的 names 对应你只改了 names没改标注编号两边的语义对不上。 解决改顺序前先把 label 文件里的第一列做批量替换或者干脆用脚本读 classes.txt 再把标注编号映射到新编号。我习惯把类别名写进一个独立的 class_mapyaml 和标注转换脚本都从同一份文件读取从源头上杜绝两边不一致。这个问题的隐蔽之处在于数据不会报错训练曲线也不异常只有部署时才发现类别对应关系全乱了。6. 让模型真正落地上线前必做的自查习惯模型训练完不代表能交付上线前我会用一个小流程把“能用”变成“敢用”。第一步是算清楚验证集上的逐类 AP不只盯合成 mAP第二步是准备一段和训练数据不同机位、不同光线条件的现场视频做边采验证只看最终帧率不直观要对着视频暂停逐帧数漏检和误检。我还会干一件看起来有点笨的事情:把推理结果里的“假阳性”截图存下来按场景归档命名成“角落圆凳误检”“窗户反光误检”“桌腿当人误检”这类带触发条件的名字形成一个负样本档案。每次调整模型后先重放这批档案看哪些老问题又出现了。这在检测模型迭代里非常管用因为指标提升往往伴随某类场景的回归而这类回归只看 mAP 根本发现不了。# 推理验证常用脚本保留 5 张最容易出错的画面 yolo predict modelbest.pt sourcetest_scene_01.mp4 conf0.35 save_txtTrue # 后续再按类别统计漏检把所需辅助放到 list这条命令里的conf0.35是置信度阈值我一般不会用默认 0.25因为桌椅密集场景 0.25 会冒出大量低置信度假框调到 0.35 到 0.4 之间能压掉部分背景误检但要注意召回会跟着下降。实际项目里阈值不是拍脑袋定的要看验证集 PR 曲线在 Precision 和 Recall 交接点附近取。我自己现在养成了一个习惯每次训练完第一件事不是看 mAP而是回放旧负样本档案先把上一版模型的错误清单重跑一遍确认没有退步再谈新指标的提升。这个习惯看似耽误时间实际上挡住了一半以上偷偷回归的模型改动比任何训练参数调试都更值钱。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进