YOLOv9成人小孩检测实战:从1738张图到70.9%的调参与标注全流程
2026/9/12 23:26:46 网站建设 项目流程

简介:面向计算机视觉与深度学习入门开发者,这份成人小孩识别数据集包含1738张原始图片及配套的YOLOv9格式标注文件,可用于训练和评估“成人/小孩”检测模型。虽然整体识别率约70.9%,但数据来自真实场景且标注完整,适合作为算法验证、模型调优或课程设计的基准数据。包内共2000个文件,以1738个txt标注文件为主体,另含261张jpg原始图片和1个yaml配置文件,压缩包大小91.41MB,结构简洁,解压后即可按YOLOv9规范读取训练。已有167人学习下载,可作为快速上手YOLO系列数据集的实践样本,帮助理解标注格式、类别配置与训练流程。

1. 1738张成人小孩图片,yolov9标注格式下的70.9%是什么概念

接到一个真实场景的任务:用一份1738张原始图片的成人小孩数据集,训练一个能区分成人和小孩的检测模型,最终识别率卡在70.9%。这个数字如果放在二分类目标检测任务里,说实话并不算好看——如果只是做“图里有没有小孩”的分类,正确率做到90%以上是常见目标;但如果按yolov9的mAP@0.5去算,70.9%意味着有近三成的目标框没被正确识别。问题往往不在模型结构不够新,而在于这份数据集的标注一致性、类别平衡和训练参数。这篇文章就直接从这1738张图和yolov9格式标注出发,把从原始图片到70.9%这条路上每一步的关键操作和调参逻辑拆开讲,适合接手过yolo系列但还没系统调过公开或私有数据集的人,也适合想把二分类检测精度往上拉的工程师。

2. 成人小孩数据集的原始图清理与类别平衡

2.1 1738张图里先做一次人工核对

拿到任何yolov9格式数据集,第一件事不是急着训练,而是把图片本身过一遍。1738张原始图片听起来不多,但人工快速翻阅一遍远比直接训练划算。常见做法是用文件管理器按缩略图浏览,或者写一个简单的OpenCV脚本循环展示图片和对应标签。重点检查三类问题:图片是否损坏、是否重复、是否有明显误标。

很多数据集的图片来自网络爬取,会有少量文件打不开或者解码失败。这类坏图如果混进训练集,轻则训练中断,重则导致loss异常。一张图片一个文件,不需要全部打开,用一段脚本就能全量体检:

import cv2 import os from pathlib import Path img_dir = Path("images") bad_imgs = [] for p in img_dir.glob("*.jpg"): img = cv2.imread(str(p)) if img is None: bad_imgs.append(p.name) print(f"损坏或无法解码: {p.name}") print(f"共 {len(list(img_dir.glob('*.jpg')))} 张图,坏图 {len(bad_imgs)} 张")

这段脚本用cv2.imread读取图像,返回None就代表文件损坏。实际操作中,我会再叠加一个检查:读出来的img.shape里宽高不能为0,并且通道数要是3。有些图片是灰度图或者带透明通道,yolov9训练时虽然能强行读入,但会造成颜色空间的隐性偏差。对成人小孩这种依赖服饰颜色、肤色特征的二分类,以及后续训练稳定性,最好统一转成RGB三通道JPG再喂给模型。

2.2 用脚本统计类别分布和标签异常

yolov9的标签文件是txt,每行格式是class_id x_center y_center width height,其中中心坐标和宽高都是相对于图片宽高的归一化数值。拿到标签后,第一步统计每个类别的目标数量,以及每张图的目标数分布。这一步能直接暴露出数据集的平衡问题。

from pathlib import Path import numpy as np label_dir = Path("labels") stats = {"adult": 0, "child": 0} per_img_obj_count = [] for lbl in label_dir.glob("*.txt"): with open(lbl, "r") as f: lines = [line.strip() for line in f.readlines() if line.strip()] per_img_obj_count.append(len(lines)) for line in lines: parts = line.split() cls_id = int(parts[0]) if cls_id == 0: stats["adult"] += 1 elif cls_id == 1: stats["child"] += 1 # 检查坐标范围 x, y, w, h = map(float, parts[1:]) if not (0 <= x <= 1 and 0 <= y <= 1 and w > 0 and h > 0): print(f"非法坐标: {lbl.name}: {line}") print("类别目标数:", stats) print("每张图目标数分布: 均值 {:.2f}, 最大 {}".format(np.mean(per_img_obj_count), max(per_img_obj_count)))

这段代码同时做了两件事:统计类别数量,并检查每个标注框的坐标是否在合法范围。我见过不少数据集的标注框出现中心点大于1或者宽高为负的情况,通常是在转换坐标时没有除以图片实际宽高,或者混入了未归一化的绝对坐标。一旦出现这类问题,跑训练时loss会剧烈震荡,最后mAP卡在60%左右上不去。

2.3 常见误标:小孩脸部露半张、成人背影

成人小孩检测的难点不在大目标,而在小目标、遮挡和模糊。在实际清理中高频出现的误标有三类。第一类是画面中只有一个半身背影,标注为“adult”或“child”时完全靠体型判断,但6岁小孩和偏瘦成年人的背影,在远距离低分辨率下几乎无法区分。第二类是脸部被口罩、帽子、围巾大面积遮挡,仅剩裸露额头或眼睛,这类特征不足以支撑模型学出稳定边界。第三类是人群密集场景,比如游乐场,多个小孩挤在一起,标注框互相重叠,甚至一个框里框住了两个小孩——这种框会让模型在训练时产生梯度冲突。

对这三类情况,我的处理办法很直接:不抱侥幸心理,全部剔除。剔除标准是“如果让我只看这个框,我能确定它是成人还是小孩吗?”。不确定就删。1738张图删掉100到200张完全正常,因为训练目标是把剩余干净样本的识别率拉到更高,而不是用脏数据凑数。如果你需要量化判断,可以统计一下框的宽高比分布,如果出现大量极宽或极高的异常框,多半是标注框没贴住目标。

检查项常见异常处理建议
图片完整性文件损坏、单通道、分辨率异常删除或重编码
标注坐标中心点越界、宽高为负、未归一化统一换算后校验
类别平衡成人2000框、小孩300框收集小孩数据或做重采样
框质量框太小、重叠严重、包含多目标剔除或重新标注

3. 用yolov9格式做成人小孩标注:从VOC或COCO转换成txt

3.1 yolov9标注的内在结构:class cx cy w h

yolov9的标签格式和yolov5、yolov8完全一致,属于Ultralytics系列通用的txt格式。每行五个数值:类别ID(0为成人,1为小孩,或根据你自己的class顺序)、归一化中心点x坐标、归一化中心点y坐标、归一化宽度、归一化高度。注意这里的归一化是除以图片的原始宽度和高度,不是除以边界框的实际像素。很多新手会忽略这点,直接用像素坐标写进去,导致模型训练时预测框几乎不会命中。

如果你手里的原始数据是LabelImg导出的VOC XML格式,或者COCO的JSON格式,都需要转成上述txt。转换过程本身不复杂,但坑在于路径对应关系。yolov9训练时,图片和标签放在两个并列目录下,只要文件名相同(后缀不同),训练脚本会自动匹配。所以转换后一定要确保每张图片都有一个同名的txt文件,哪怕这张图没有目标,也要生成一个空txt。

3.2 用Python脚本批量转换XML到YOLO

下面这段脚本适用于最常见的VOC XML格式转换到yolov9标签。假设你的XML文件都放在xml_dir,对应的图片在img_dir,脚本会读取每个XML里所有的object,把bndbox坐标转换成归一化的cx, cy, w, h。

import xml.etree.ElementTree as ET from pathlib import Path def convert_xml_to_yolo(xml_path, out_dir, img_w, img_h): tree = ET.parse(xml_path) root = tree.getroot() labels = [] for obj in root.findall('object'): name = obj.find('name').text.strip() if name == 'adult': cls_id = 0 elif name == 'child': cls_id = 1 else: print(f"未知类别 {name} 在 {xml_path.name},跳过") continue bnd = obj.find('bndbox') xmin = float(bnd.find('xmin').text) ymin = float(bnd.find('ymin').text) xmax = float(bnd.find('xmax').text) ymax = float(bnd.find('ymax').text) # 裁剪到图片边界 xmin = max(0, xmin); ymin = max(0, ymin) xmax = min(img_w, xmax); ymax = min(img_h, ymax) if xmax <= xmin or ymax <= ymin: continue cx = (xmin + xmax) / 2.0 / img_w cy = (ymin + ymax) / 2.0 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h labels.append(f"{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") out_path = out_dir / (xml_path.stem + ".txt") with open(out_path, "w") as f: f.write("\n".join(labels)) return len(labels) xml_dir = Path("xmls") out_dir = Path("labels") out_dir.mkdir(exist_ok=True) for xml_file in xml_dir.glob("*.xml"): # 实际项目中应读取图片尺寸,这里用常量示例 img_w, img_h = 1920, 1080 n = convert_xml_to_yolo(xml_file, out_dir, img_w, img_h) print(f"{xml_file.name}: {n} 个目标")

逻辑说明:脚本先解析XML里的每个目标,根据类别名映射到0或1,然后对bndbox坐标做边界裁剪,再转换为YOLO要求的格式。边界裁剪这一步不是可有可无,有些标注框会稍微超出图片范围,如果不裁剪,归一化坐标会大于1,训练时容易出现边界框溢出。

参数说明:img_wimg_h必须与每张图片的实际尺寸一致,但在批量转换时,很多XML里其实自带了<size>节点。我更推荐从XML里读取,而不是写死常量:

size = root.find('size') img_w = int(size.find('width').text) img_h = int(size.find('height').text)

用XML里记录的尺寸最稳妥,因为某些XML文件头里的尺寸和实际图片可能有一两个像素的偏差,但归一化计算只需要相对比例,所以允许微小误差。如果转换后发现标注框整体偏移,优先去核对XML里的尺寸信息是否是图的原始尺寸。

3.3 转换后必须做的标注可视化

转换完成不等于标注正确。最有效的检查办法是把标注框直接画到图片上,人眼扫一遍,而不是去读txt里的数字。用下面这段代码快速生成可视化结果:

import cv2 from pathlib import Path img_dir = Path("images") label_dir = Path("labels") vis_dir = Path("visualized") vis_dir.mkdir(exist_ok=True) colors = {0: (0, 255, 0), 1: (0, 0, 255)} # adult绿色,child红色 for img_path in list(img_dir.glob("*.jpg"))[:50]: # 抽查前50张 img = cv2.imread(str(img_path)) h, w = img.shape[:2] lab_path = label_dir / (img_path.stem + ".txt") if not lab_path.exists(): print(f"缺少标签: {img_path.name}") continue with open(lab_path) as f: lines = [line.strip() for line in f if line.strip()] for line in lines: cls, cx, cy, bw, bh = line.split() cls = int(cls); cx = float(cx)*w; cy = float(cy)*h bw = float(bw)*w; bh = float(bh)*h x1 = int(cx - bw/2); y1 = int(cy - bh/2) x2 = int(cx + bw/2); y2 = int(cy + bh/2) cv2.rectangle(img, (x1, y1), (x2, y2), colors[cls], 2) cv2.putText(img, "child" if cls==1 else "adult", (x1, max(0, y1-5)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, colors[cls], 2) cv2.imwrite(str(vis_dir / img_path.name), img)

这段代码读入图片后,把归一化的坐标还原成了像素坐标,然后用不同颜色绘制成人框和小孩框。我会把可视化结果按图片文件名排序后,在文件管理器里快速滚动查看,重点看三点:框是否紧贴目标上下边缘、成人小孩的颜色分配是否符合预期、是否存在两个不同类别框大面积重叠。如果某个类别的框频繁画偏,说明转换脚本里那个类别的ID映射错了;如果所有框都扩大了一圈,说明归一化时除以的尺寸和实际图片尺寸不一致。

4. 在Ultralytics YOLOv9上训练成人小孩检测器

4.1 准备数据目录与data.yaml

yolov9在Ultralytics框架下训练,数据目录结构遵循固定约定。两种方式:一种是图片和标签分别放在images/labels/下,每个集合下再分train/val/;另一种是全部放在一起,用txt文件列表指定划分。我推荐前者,因为目录结构清晰,不容易在数据集变动时漏掉某个文件。

datasets/ ├── adult_child_yolo/ │ ├── images/ │ │ ├── train/ │ │ └── val/ │ └── labels/ │ ├── train/ │ └── val/

划分比例一般按80/20做随机切分,但要注意先对图片做随机打乱,避免暗光照片全部落进验证集。对于1738张图的规模,验证集大概在300到400张之间,足够稳定评估。如果训练样本总数过少,可以改成85/15,但验证集不能少于200张。

data.yaml是训练时最重要的配置文件,内容如下:

path: datasets/adult_child_yolo train: images/train val: images/val nc: 2 names: 0: adult 1: child

参数说明:path指向数据集根目录,trainval是相对根目录的子目录。注意不要写成绝对路径,否则换机器跑需要改文件。nc是类别数量,这里是2。names字典里的顺序必须与标签txt里的class_id一致,如果这里写反了,模型会把成人当小孩、小孩当成人,训练出来的loss照样会收敛,但预测结果完全错误。

4.2 选择预训练权重与训练命令

Ultralytics官方提供yolov9的预训练权重,包括yolov9-c.pt、yolov9-e.pt等。对于成人小孩这种二分类任务,不需要用最大的模型,yolov9c足够,推理速度也更快。如果算力有限,可以退一步用yolov9s或直接使用yolov9-t,但精度会下降。训练命令是:

yolo detect train \ model=yolov9c.pt \ data=adult_child.yaml \ epochs=100 \ imgsz=640 \ batch=16 \ lr0=0.01 \ patience=20 \ project=runs \ name=adult_child

参数说明:epochs=100是最大训练轮数,配合patience=20,意思是如果连续20轮mAP没有提升就提前停止。imgsz=640是训练输入尺寸,对于较小目标多的场景,改成imgsz=768imgsz=832能提升小目标召回率,但会显著增加显存占用。lr0=0.01是初始学习率,这个值对batch size敏感;如果batch只有8,建议把lr0降到0.005,否则loss容易发散。batch=16需要根据显卡显存调整,6GB以下就设8,12GB可以设16。

训练过程中,终端会实时打印每个epoch的loss、precision、recall和mAP50。关注点在于:训练前几个epoch的loss是否在下降,如果loss一开始就乱跳,需要按Ctrl+C停止,降低lr0或者检查数据标注。训练结束时,runs/adult_child/weights/best.ptlast.pt两个权重文件是核心产物,后续推理、评估、部署都用best.pt

4.3 关键训练参数与70.9%的关系

70.9%这个识别率,如果指mAP@0.5,意味着模型在验证集上平均只有七成的预测框和真实框重叠度超过0.5。这个结果受几个参数直接影响。

参数对成人小孩检测的影响建议值
modelyolo规格越小,参数越少,训练快但上限低yolo9c或yolo9s
imgsz太低会让小目标消失,太高增加显存压力640~768
epochs不足会欠拟合,过大配合early stop即可100~200
batch过小导致BN统计不稳定,过大容易内存溢出显存允许时尽量大
lr0过大震荡,过小收敛慢0.005~0.01
mosaic增强会混合4张图,对多目标位置推理有帮助默认1.0,可调为0.5

这几个参数里,imgsz是最容易被忽略却对结果影响最明显的。成人小孩的身高差异在图像中体现为框的尺寸差异,如果imgsz太小,比如416,那么一个在画面中只有30像素高的小孩,缩放后只剩十几个像素,卷积特征图上可能只剩下2x2区域,完全无法表达轮廓信息。把imgsz调大到768,往往能把mAP从70%拉到75%以上。

5. 识别率70.9%的验证与失败拆解

5.1 从results.csv和混淆矩阵看成人小孩各自的精度

训练完成后,Ultralytics会在runs/adult_child/results.csv里记录每个epoch的所有指标。用pandas直接读取,可以快速找到训练过程中最佳epoch和对应精度:

import pandas as pd df = pd.read_csv("runs/adult_child/results.csv") best = df.loc[df["metrics/mAP_0.5"].idxmax()] print("最佳epoch:", int(best["epoch"])) print("mAP@0.5:", round(float(best["metrics/mAP_0.5"]), 4)) print("mAP@0.5:0.95:", round(float(best["metrics/mAP_0.5:0.95"]), 4)) print("精确率:", round(float(best["metrics/precision"]), 4)) print("召回率:", round(float(best["metrics/recall"]), 4))

这里metrics/mAP_0.5就是我们常说的70.9%这种数字的来源。只看总数还不够,要分解到每个类别。在runs/adult_child/目录下有一个confusion_matrix.png,打开它就能看到成人、小孩、背景三类之间的混淆情况。

confusion_matrix.png是一个二维矩阵,行代表真实类别,列代表预测类别。如果中间表示“child”的真实框被误判成“adult”的比例很高,矩阵里对应位置的颜色就会很亮。通常二分类检测里出现高混淆,集中在两类目标尺寸相近的场景,以及非正面姿态下。

5.2 检视val_batch0_prediction.jpg里的漏检与错检

训练完成后的runs/adult_child/目录下会生成几张验证集预测结果图,比如val_batch0_prediction.jpg。它会画出模型预测的框以及置信度。这时候不要只看最终数字,要把这些图放大逐张看,记录漏检和错检的规律。

常见错检有几种:模型把远处的成年人错标成小孩,因为远处人体像素少,体型特征模糊;模型把推车里的小孩漏掉了,因为被部分遮挡;还有逆光条件下整个目标成为剪影,模型只能给出很低的置信度。这些错检模式如果集中在某个时间段或某种拍摄场景,说明训练集里这种负样本或难例不够,需要考虑额外收集数据。

另一个值得看的是置信度分布。从results.csv里提取val/box_p,观察最终轮次的平均置信度。如果所有预测框的置信度都集中在0.3到0.5之间,说明模型对特征不够自信,这时候唯一有效的手段是增加数据多样性和训练轮次,调置信度阈值只能改变输出框数量,不能提升特征质量。

5.3 哪些原因会把准确率压在70%附近

根据我折腾yolo系列数据集的经验,mAP@0.5死活卡在70%左右,通常不是模型能力问题,而是三个环节出了状况。

第一个是标签噪声。人工标注时主观性很强,同一个目标,一个标注员认为框要含住手臂,另一个只框躯干;成年人瘦小、小孩壮实,这些主观差异相当于在训练集中注入了不一致的监督信号。核对方法是用清洗后的标签重训一次,如果mAP上涨超过3个百分点,说明标签噪声确实是主要瓶颈。

第二个是类别不平衡。如果成人目标有4000个,小孩目标只有400个,模型会为了优化整体loss而偏向成人。此时不应该简单复制小孩样本(会导致过拟合),而应该用采样策略或损失函数调整。Ultralytics提供了class_weights,但在yolov9里更直接的做法是在数据增强上偏向小孩,比如使用mosaiccopy_paste增强小目标数据量的效果,或者从大图中多裁剪含有小孩的子图加入训练。

第三个是验证集划分不当。如果验证集和训练集来自同一个场景序列,比如同一段监控视频抽帧,那么相邻帧极其相似,验证集精度虚高;反之,如果验证集包含大量不同场景,精度就会被打回原形。对于1738张图,正确的划分方式是按场景分组,先按采集时间或拍摄地点分组,再把整个组划入训练或验证,而不是随机从所有图片里抽样。

6. 把70.9%往上推的3个可落地方向

6.1 数据增强比换模型更立竿见影

70.9%如果想升到80%以上,先别急着换yolov9的变体,试试调整Ultralytics自带的增强参数。在训练命令里追加hsv_h=0.015 hsv_s=0.7 hsv_v=0.4 degrees=10 translate=0.1 scale=0.5 fliplr=0.5,这些参数分别控制色调、饱和度、亮度、旋转、平移、缩放和水平翻转的振幅。对于成人小孩检测,scaletranslate是提升明显的两个参数——缩放模拟了不同距离下的人体大小变化,平移模拟了人体偏离画面中心的情况。设置后,且训练轮次相应增加。

6.2 调整锚点和类别权重

如果模型预测的框偏大或偏小,可以考虑手动调整锚点。yolov9在训练时会根据验证集自动计算锚点,但在目标尺寸分布极不均匀时,自动计算的锚点可能集中在大多数目标的尺寸上。你可以通过yolo detect valplots=Truesave_json=True,输出sensitivities.json,查看每个尺寸区间内的目标数量,然后针对性地在data.yaml里自定义anchors。不过新手常用且更稳妥的方式是直接用yolo detect train加上auto_anchor=False,再手动指定锚点表,这需要先跑一次k-means聚类统计目标框宽高比。

另一个有效操作是监督类别不平衡。如果你统计出小孩目标只有成人目标的1/5,可以在损失函数上给小孩类别更高的权重。Ultralytics的loss_gsloss_oi没有暴露直接的class_weight,但你可以通过cls参数增加分类损失的权重,比如cls=0.8,同时配合数据层级的小目标增强。

6.3 标签清洗后重训一轮

很多人在70.9%的瓶颈上反复调参,其实忘了回头重看标注。我最后建议的动作是:把训练结果中预测置信度低于0.4的所有框打印出来,和对应的真实框画在一起,列出IoU低于0.5的样本。这些样本往往是原始标注框手抖画大、画偏的地方。人工重新修正100到200个框,再在同样参数下重训20轮,大概率能看到mAP提升。

把重训后的best.pt在验证集上重新评估,输出置信度阈值调优曲线,找到F1分数最高的置信度阈值。在部署阶段,用yolo predict带上conf=0.5 iou=0.45参数再跑一次,生成的预测图和验证图进行比对,确认70.9%已经变成过去式,新的数字才是你在这个数据集上的实绩。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询