简介:面向YOLO系列算法目标检测场景的手持刀行为检测数据集,以4381张图像为基础制作,适用于安全监控、公共区域异常行为识别等任务,可直接用于模型训练、验证与测试。资源压缩包大小171.83MB,共2000个文件,以VOC格式XML标签文件为主要类型,同时附带YOLO格式TXT标注与data.yaml配置文件,数据已完成划分,适配yolov5、yolov7、yolov8、yolov9、yolov10及yolo11等主流算法。YOLO格式使用归一化的目标框中心点坐标与宽高,VOC格式便于通过XML工具读取类别和位置,两类文件分别存放,可降低不同训练框架间的切换成本。目前已有169人学习下载,对需要快速获得手持刀行为标注数据、验证目标检测算法的开发者而言,可省去数据采集与人工标注环节,按目录结构与配置文件即可开展训练和评估。
1. 为什么手持刀具行为检测不能只靠分类网络
在安防和巡检场景里,手持刀具检测和普通的目标分类完全是两回事。分类网络告诉你画面里「有没有刀」,但实际部署时你需要的是「谁在什么位置握着刀」,这就落到了目标检测的任务范畴。4381张图像、带完整标签的数据集,听起来规模不算大,但关键在于它是双格式标注——同时提供YOLO的txt和VOC的xml,并且已经划分好训练集和验证集,放上data.yaml就能直接喂给yolov5到yolo11系列跑训练。这对做行为检测算法验证、毕业设计或者快速搭建安防原型的工程师来说,省掉的是最耗时的人工标注环节,特别是刀具这种反光、细长、容易和被检测者手部重叠的目标,标注质量直接决定模型上限。本文就把这套数据集的格式设计、训练流程和几个容易踩的坑完整拆开讲。
2. 数据集结构与双标签格式的底层设计
2.1 目录布局和文件命名规律
解压后首先面对的是两个标签文件夹和一个图像文件夹。文件名形如img_0524_1518.xml,其中0524是场景编号,1518是帧序号,命名本身没有类别信息,所以你不能靠文件名判断这张图里是折叠刀还是菜刀,必须解析标签内容。常见做法是先跑一段脚本把目录结构摸清楚:
find . -maxdepth 2 -type d | sort ls images/ | head -20 ls labels_yolo/ | head -20 ls labels_voc/ | head -20 wc -l labels_yolo/*.txt # 统计每个txt的标注目标数量执行后你会看到images、labels_yolo、labels_voc三个目录,外加data.yaml。wc -l这个命令在这一步很实用,它能快速筛查出哪些图片没有对应标注文件,以及单张图里目标数量是否异常——比如某个txt里有七八行,通常意味着密集遮挡场景,这类样本在评估时对mAP的影响不能忽视。
2.2 YOLO 格式的归一化坐标机制
YOLO 标签每一行是五个字段:<class> <x_center> <y_center> <width> <height>。这四个数值全部是相对图像的归一化比例,范围在0到1之间。用实际图片尺寸换算回去的公式是:
x_pixel = x_center * img_width y_pixel = y_center * img_height box_width_pixel = width * img_width box_height_pixel = height * img_height这里有一个值得注意的细节:宽度和高度的归一化是分别除以图片宽和图片高,而不是统一除以一个边。也就是说,一个 1920x1080 的图中,width=0.1对应的实际像素宽度是 192 像素,而height=0.1对应的实际高度是 108 像素。如果拿 YOLOv5 的训练脚本直接训练,这个换算在数据加载器内部已经处理了,但你要是自己写数据增强或者做标签可视化,忘记这一点就会画出完全偏移的框。
坐标字段的具体含义如下表:
| 字段 | 含义 | 范围 | 计算方式 |
|---|---|---|---|
| class | 类别索引 | 从0开始 | 与data.yaml中的类别顺序对应 |
| x_center | 目标框中心点x比例 | 0~1 | (xmin + xmax) / 2 / img_width |
| y_center | 目标框中心点y比例 | 0~1 | (ymin + ymax) / 2 / img_height |
| width | 目标框宽比例 | 0~1 | (xmax - xmin) / img_width |
| height | 目标框高比例 | 0~1 | (ymax - ymin) / img_height |
txt 格式的实际内容长这样:
0 0.632812 0.445370 0.101562 0.287037 0 0.480469 0.396296 0.093750 0.268519第一行表示一个类别索引为0的目标,框中心点位于图片横向63.28%、纵向44.54%的位置,框宽约为图片宽度的10.16%,框高约为图片高度的28.70%。
2.3 VOC XML 格式与 YOLO 格式的对应关系
XML 格式保存的是像素绝对坐标,结构上更接近标注工具的原始输出。一个典型的标注文件核心部分如下:
<annotation> <folder>images</folder> <filename>img_0524_1518.jpg</filename> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <object> <name>knife</name> <bndbox> <xmin>1008</xmin> <ymin>352</ymin> <xmax>1203</xmax> <ymax>662</ymax> </bndbox> </object> </annotation>这套结构里的size块很关键。YOLO 格式的归一化换算必须知道原图尺寸,如果 xml 里写的尺寸和实际 jpg 分辨率不一致,转换出来的比例就是错的,训练时会报图片尺寸不匹配或者框全部跑偏。拿到数据集后我建议先做一次全量校验——遍历每个 xml,读size节点的宽高,再和对应图片的实际尺寸对比,不一致的单独挑出来处理。
name节点里的knife是类别名,在转 YOLO 格式时需要映射成整数索引,映射顺序需要和 data.yaml 里的names列表严格一致。常见做法是写一个类别映射表,比如{'knife': 0},这样转出来的 txt 第一列才能正确对应 data.yaml 中的 class 索引。
2.4 data.yaml 配置文件的逐项拆解
data.yaml 是 YOLO 系列训练的统一入口,它的核心字段决定了训练集、验证集和类别数是否正确加载。以这个数据集为例,配置文件至少包含以下内容:
train: images/train val: images/val nc: 1 names: ['knife']train和val可以是相对路径,也可以是绝对路径。但注意,这两个路径指向的是图片目录,不是标签目录,YOLO 训练脚本会自动在同级目录下寻找 labels 文件夹。如果数据集被移动过位置,这个路径最容易踩坑,我一般会改成绝对路径避免权限和相对路径基准问题。nc指类别数量,这里只有刀具一类,所以写 1。如果后续你自己加了新类别数据,同步调整 nc 的同时还要保证所有标签里不会出现大于 nc-1 的索引值。names是类别名列表,索引从 0 开始,和 txt 第一列的数字一一对应。注意检查 names 列表的顺序不要和转换时的映射表错位。
YOLOv5 和 YOLOv8 在读取 yaml 时对路径的处理方式不同:v5 倾向于相对路径基于项目根目录解析,v8 会基于 yaml 文件所在位置解析,所以在不同版本间切换时要确认路径写的是否清晰。
3. 基于 YOLOv8 的训练流程与参数调优
3.1 环境准备和目录摆放
训练这套刀具检测数据集,直接选用 YOLOv8 是效率最高的方案,官方预训练权重在 COCO 上的特征提取能力对刀具这种纹理目标有很好的迁移基础。环境安装不展开说,只需要保证 PyTorch 版本和 Ultralytics 包兼容即可。一个容易忽略的地方是 AMD 显卡环境下跑 yolo,这种方式依赖 DirectML 后端,性能比 CUDA 低一些但能跑通训练流程,适合没有 N 卡的机器。训练前把数据集放成 YOLO 工程默认的布局,注意 images 和 labels 的目录名必须保持对应关系。
项目目录的摆放参考结构如下:
datasets/knife/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ ├── data.yaml即使原始数据集已经划分好了,仍建议自己确认一下 train 和 val 的图片数量比例,并且检查 labels 目录下有没有多余的、不在图片目录中的孤儿标签文件。这类脏数据在训练时不会报错,但会让验证集 mAP 失真,排查起来比训练失败更麻烦。
3.2 训练命令与超参数
目录和配置文件就绪后,直接用以下命令启动训练:
from ultralytics import YOLO if __name__ == '__main__': model = YOLO('yolov8n.pt') results = model.train( data='datasets/knife/data.yaml', epochs=100, imgsz=640, batch=16, lr0=0.01, patience=20, device=0, workers=4, )这段代码启动了一个基于yolov8n.pt预训练权重的迁移学习训练流程。data参数指定数据集配置文件位置,训练脚本会从 yaml 里读取 train、val 和 nc 字段去实例化数据加载器;epochs=100意味着整个训练集被遍历 100 轮;imgsz=640是输入网络的图像尺寸,YOLOv8 在训练阶段会根据这个尺寸做缩放;batch=16由显卡显存决定,如果你用的卡显存不够,优先把 batch 降到 8 或 4,而不是直接换小模型;lr0是初始学习率,迁移学习场景下 0.01 是一个稳妥的起点;patience=20表示连续 20 轮验证指标没有提升就早停,这个机制在数据量不大时很实用,能省掉一大半无效训练时间。
训练过程中的关键观察点是 loss 曲线。yolo 系列的损失函数包含分类损失和边界框回归损失两部分,在训练日志里看到的一串cls_loss、box_loss就是这两项。正常收敛规律是前 20 轮 loss 快速下降,之后进入平台期开始缓慢下降。如果发现 box_loss 持续震荡不下降,通常是学习率过大或者标签坐标本身有问题,需要回查标注质量。
3.3 训练集和验证集的平衡
数据已经划分好了不代表它一定是均衡的,用脚本统计一下各类别的分布:
import os from collections import Counter label_dir = 'datasets/knife/labels/train' counts = Counter() for fname in os.listdir(label_dir): if not fname.endswith('.txt'): continue with open(os.path.join(label_dir, fname), 'r') as f: for line in f.readlines(): cls_id = int(line.strip().split()[0]) counts[cls_id] += 1 print(counts)这个脚本遍历训练集所有 txt 文件,统计每个类别索引出现的总次数。运行后你会看到刀具这一类别的目标总数,以及是否有个别图片里标注了多个目标。这个数量决定了要不要做类别权重调整。对于单类检测来说,如果每张图的平均目标数在 1 到 3 之间,数据是相对充分的,不需要特殊处理。但如果发现大量图片没有目标框——也就是空标签 txt——需要检查这些图片是否被错误放入了训练集,因为它们会让模型学到「输出空检测」的倾向。
3.4 训练后的验证与模型导出
训练结束后,ultralytics 会在runs/detect/train目录下保存权重文件和验证结果。用以下命令在验证集上评估最佳权重:
from ultralytics import YOLO model = YOLO('runs/detect/train/weights/best.pt') metrics = model.val(data='datasets/knife/data.yaml', imgsz=640, batch=16) print(metrics.box.map50) # mAP@0.5 print(metrics.box.map) # mAP@0.5:0.95map50是 IoU 阈值 0.5 下的平均精确率,map是跨 IoU 阈值从 0.5 到 0.95 的平均,后者更严格。刀具检测任务里,如果 map50 在 0.9 以上说明标注质量可靠,而 map 偏低到 0.5 以下通常是边框标注不够紧贴刀身轮廓导致——刀具细长,轻微的标注偏移就会显著拉低高 IoU 阈值下的匹配率。
4. 标签格式互转与数据质量验证
4.1 VOC 转 YOLO 的完整脚本
虽然数据集已经同时提供了两种格式,但实际使用中你大概率需要把 xml 转成 txt——比如你新增了一批自己标注的数据,或者从 CVAT 导出的格式是 VOC 而你的训练框架只吃 YOLO 格式。这一节给出一个可直接套用的转换脚本,关键点在于读取 xml 后必须同步读取图片尺寸:
import os import xml.etree.ElementTree as ET def voc_to_yolo(xml_path, img_width, img_height, class_map): tree = ET.parse(xml_path) root = tree.getroot() lines = [] for obj in root.iter('object'): name = obj.find('name').text if name not in class_map: continue cls_id = class_map[name] bndbox = obj.find('bndbox') xmin = float(bndbox.find('xmin').text) ymin = float(bndbox.find('ymin').text) xmax = float(bndbox.find('xmax').text) ymax = float(bndbox.find('ymax').text) x_center = (xmin + xmax) / 2.0 / img_width y_center = (ymin + ymax) / 2.0 / img_height width = (xmax - xmin) / img_width height = (ymax - ymin) / img_height lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}") return lines class_map = {'knife': 0} lines = voc_to_yolo('img_0524_1518.xml', 1920, 1080, class_map) with open('img_0524_1518.txt', 'w') as f: f.write('\n'.join(lines))脚本的逻辑是:遍历 xml 中每个object节点,提取类别名和边界框像素坐标,然后套用前面讲到的归一化公式换算成比例值,最后按 YOLO 格式写入 txt。class_map字典必须和 data.yaml 的 names 列表顺序一致,否则类别索引会错位。img_width和img_height参数不要从 xml 的size节点直接拿,而要真实读取图片文件的尺寸,避免 xml 元数据和实际图片不一致导致的标签错位。
4.2 标签可视化校验
转换完成后,一个重要步骤是把标签画回原图,肉眼确认每个框是否贴合刀身。这一步能发现坐标偏移、类别错标、框过大过小等问题。用 OpenCV 快速实现:
import cv2 img = cv2.imread('img_0524_1518.jpg') h, w = img.shape[:2] with open('img_0524_1518.txt') as f: for line in f.readlines(): cls_id, x_center, y_center, bw, bh = map(float, line.split()) x1 = int((x_center - bw / 2) * w) y1 = int((y_center - bh / 2) * h) x2 = int((x_center + bw / 2) * w) y2 = int((y_center + bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.imwrite('check_visual.jpg', img)这里的核心操作是把归一化坐标乘以实际图片宽高,还原成像素坐标再做绘制。x1和y1是框左上角,x2和y2是右下角,注意 YOLO 格式存的是中心点和宽高,画矩形时需要先换算成角点坐标。如果画出来的框大面积偏向一侧,大概率是x_center或y_center的归一化除以了错误的基准。
4.3 标签格式中容易忽视的坑
txt 和 xml 同时存在时,最容易出问题的是两者之间的不同步。某些标注工具在保存时会先写 xml 再转 txt,如果中途被中断,就会留下一个没有对应 txt 的 xml 文件,训练脚本会跳过这张图,导致实际参与训练的样本少于预期。一条命令可以快速检查:
for f in labels_voc/*.xml; do base=$(basename "$f" .xml) if [ ! -f "labels_yolo/$base.txt" ]; then echo "missing txt: $f" fi done这个循环遍历所有 xml 文件,用basename去掉扩展名得到文件名主体,再检查同名 txt 是否存在。输出的缺失列表就是需要重新转换的目标。另一个隐蔽问题是 xml 中坐标越界,xmax大于图片宽度,这在从标注工具导出时偶尔发生,建议转换时加一句min(max(x, 0), img_width)的约束。
4.4 类别不均衡的处理策略
虽然这里只有单类目标,但如果你往数据集里补充了其他类别(比如持棍、手机),不均衡问题就会出现。处理不均衡有一个常见误区:直接复制样本文件让两个类别数量接近,这会让模型过拟合复制的那个类别。正确做法是在训练时按 cls_loss 加权,YOLOv8 的model.train()里传入cls=0.7可以调整分类损失的权重系数。另一个思路是在数据增强环节做随机裁剪拼图,把包含少样本目标的小图裁出来和其他图拼接,既增加了少样本数量,又保持了目标的真实性。
5. 针对刀具检测的锚框优化与切片推理技巧
在单类刀具检测任务里,模型的收敛速度和最终精度往往取决于锚框与目标形状的匹配度。刀具和常见的目标(行人、车辆)有一个显著差异:它的长宽比极端,有的折刀展开后宽高比能达到 1:5 以上,而 YOLOv8 的默认锚框是从 COCO 数据集聚类出来的,对细长目标不够友好。解决思路是在训练前用 K-Means 对数据集的标注框做聚类,重新计算锚框参数。这一步只需要运行 ultralytics 的自动锚框计算:
from ultralytics import YOLO model = YOLO('yolov8n.pt') model.model.yaml['anchors'] = [] metrics = model.train( data='datasets/knife/data.yaml', epochs=30, imgsz=640, batch=16, auto_anchor=True, )auto_anchor=True会让训练启动前自动基于当前数据集的标签框重新聚类锚框尺寸,替代默认的 COCO 锚框。注意这个过程只对边框回归的收敛速度有帮助,如果你的标签本身标注质量不好,聚类出的锚框也会失真,所以建议先做上一章的可视化校验再开这个参数。
部署推理时,刀具检测面临一个实际问题是目标太小。4381 张图里如果有一部分是监控摄像头拍摄的远处行人手持刀具,刀身可能只占图像几十个像素,直接以 640 分辨率输入网络,小目标特征在深层特征图中已经被压缩没了。切片推理是解决这个问题的常用方案,把原图分割成若干重叠区域,分别检测再合并结果。以下给出一个简化实现:
import cv2 import numpy as np def slice_inference(model, img_path, slice_size=640, overlap=100): img = cv2.imread(img_path) h, w = img.shape[:2] detections = [] for y in range(0, h, slice_size - overlap): for x in range(0, w, slice_size - overlap): x_end = min(x + slice_size, w) y_end = min(y + slice_size, h) patch = img[y:y_end, x:x_end] results = model.predict(patch, imgsz=640, conf=0.25, verbose=False) for box in results[0].boxes: cx, cy, bw, bh = box.xywh[0].tolist() # 转换回原图坐标 px = cx + x py = cy + y detections.append([px, py, bw, bh, box.conf.item()]) return detections这个函数遍历原图,以slice_size为窗口大小、overlap为重叠量切割图片,对每块执行推理,再把检测框坐标偏移回原图坐标系。x和y是当前切片左上角在原图中的坐标,检测结果中每个框的中心点坐标需要加上这个偏移量才能映射回原图。overlap参数设置重叠区域是为了避免目标正好被切在边界上而断裂,一般取切片尺寸的 15% 到 20% 比较合适。切片尺寸的选择要和你训练时的imgsz保持一致,否则模型看到的尺度分布和训练时不一致,检测效果反而会退化。重叠量过小会导致跨切片的同一目标被重复检测,后处理时需要用 NMS 对合并后的检测列表再做一次去重,NMS 的 IoU 阈值设在 0.5 即可。
本文还有配套的精品资源,点击获取