简介:面向工业安全场景的安全帽佩戴检测需求,这份基于TensorFlow 1.x实现的YOLO V3模型训练资源包,非常适合有一定深度学习基础、希望快速落地目标检测应用的开发者。压缩包共33个文件,以17个Python脚本为核心,覆盖数据预处理、模型构建、训练、评估、推理等完整流程,同时附带3张网络结构示意图、3张检测效果图、类别标签文件、锚框配置文件及说明文档,整体体积仅828KB,轻量便携。已有212人学习浏览,实用价值初步获得认可。资源内不仅能获取预训练权重转换脚本、K均值锚框聚类工具、单张图片与视频测试脚本,还配有数据集下载指引,可支撑从零复现安全帽识别模型。借助架构图、骨干网络图和聚类结果图,读者还能深入理解YOLO V3的设计原理,便于后续迁移到高空坠物、跌倒检测等类似工业视觉任务。
1. YOLO V3 + TensorFlow 1.x 的安全帽识别:旧组合为何还没被丢进垃圾桶
TensorFlow 1.x 已经停止维护,但网上大批安全帽识别项目仍锁死在 1.15 + Python 3.7 的组合。不是这些项目作者不愿意升级,而是 YOLO V3 的 TensorFlow 1.x 实现里有大量 session、placeholder、frozen graph 代码,迁到 2.x 几乎等于重写。对刚拿到“YOLO V3(Tensorflow 1.x) 安全帽 识别 提供数据集下载和预训练模型.zip”的人来说,这套包的价值在三点:数据集是已经清洗过的施工安全场景图片,预训练模型能直接跑出框,训练脚本把 Darknet 权重到 TensorFlow checkpoint 的路径写得很短。下面按解包、复现、训练、冻结 PB 的完整流程走一遍。
2. 数据集下载与预训练模型的解包检查
拿到 zip 后,我一般第一件事是列出包内目录,不是直接喂给训练脚本。这类压缩包通常包含 JPEGImages、Annotations、cfg 和 weights 四个部分,目录名偶尔是安全帽、hardhat 或 helmet。先列一遍文件结构,确认数据是否完整,再谈训练。
2.1 zip 包内的典型目录结构
unzip "YOLO V3(Tensorflow 1.x) 安全帽 识别 提供数据集下载和预训练模型.zip" -d safehat cd safehat find . -maxdepth 2 -type d | sortunzip 在大多数 Linux 发行版和 Git Bash 里都有;找不到时用 Python 的 zipfile 模块解压,更通用:
python -m zipfile -e "YOLO V3(Tensorflow 1.x) 安全帽 识别 提供数据集下载和预训练模型.zip" safehat解压后,重点确认四类文件在不在,缺哪类补哪类:
| 路径 | 内容 | 训练时是否必需 |
|---|---|---|
| data/safehat/JPEGImages | 安全帽佩戴、未佩戴的现场照片,通常几十到几千张不等 | 是 |
| data/safehat/Annotations | VOC XML 标注,框住头部和帽子区域,类别名通常是 helmet 或 head | 是 |
| cfg/yolov3.cfg | 网络结构、anchors、类别数配置 | 是 |
| weights/darknet53.conv.74 | 预训练骨架权重,用于迁移学习 | 建议有 |
只看到 JPEGImages 而缺少 Annotations,说明压缩包只给了图片,标注要自己补。常见做法是用 LabelImg 重新画,但那样工作量大得多。weights 目录为空则要从 Darknet 官方权重里拿 darknet53.conv.74,训练时只用前 74 层卷积参数。如果连 cfg 都没有,就要根据代码里的配置模板重新补一份,YOLO V3 的 cfg 内容很长,手工补容易漏掉 scale 层后面的 yolo 层配置。
2.2 标注格式与训练集/验证集划分
这类旧项目的数据集标注格式常见有三种:VOC XML、COCO JSON、YOLO txt。标题下这个压缩包大概率给的是 VOC XML,因为 YOLO V3 TensorFlow 1.x 时代的数据加载器主要写的是 VOC。XML 里每个 object 的 name 是安全帽类别名,bndbox 给出 xmin、ymin、xmax、ymax。拿到后先做分布检查:到底有几个类别、每类的框数是否均衡。
用 Python 统计类别和标注框数量:
import xml.etree.ElementTree as ET from pathlib import Path from collections import Counter ann_dir = Path("data/safehat/Annotations") cls_counter = Counter() box_counter = 0 for xml_file in ann_dir.glob("*.xml"): root = ET.parse(xml_file).getroot() for obj in root.findall("object"): name = obj.find("name").text.strip() cls_counter[name] += 1 box_counter += 1 print("类别统计:", dict(cls_counter)) print("总框数:", box_counter)逻辑说明:遍历所有 XML 文件,把每个 object 的 name 取出来计数,顺便数总框数。安全帽类目常见的坑有两个。一是类别名不统一,同一份数据集里 helmet 和安全帽混用,训练时会被当成两个不相干的类别。二是标注框把整个头戴设备都画全了,导致小目标漏检。统计时发现类别超过 2 个就要警觉:安全帽项目通常只有 helmet 和 head 两类,出现第三个类别大多是背景误标或标签写错。
训练脚本一般要求数据清单格式是图片路径 + 对应 XML 路径,可以用下面的脚本生成 train.txt 和 val.txt,并顺手做一次八二切分:
python - <<'PY' import random from pathlib import Path imgs = sorted(Path("data/safehat/JPEGImages").glob("*.jpg")) random.seed(2024) random.shuffle(imgs) split = int(len(imgs) * 0.8) with open("data/safehat/train.txt", "w") as f: for img in imgs[:split]: f.write(str(img.resolve()) + "\n") with open("data/safehat/val.txt", "w") as f: for img in imgs[split:]: f.write(str(img.resolve()) + "\n") PY这里固定随机种子,保证每次切分结果一致;用绝对路径是为了避免后续训练脚本切换工作目录时找不到图片。要注意路径里有中文时,TensorFlow 1.x 的文件读取接口有时会解码失败,建议把数据集挪到纯英文路径下再做切分。
2.3 预训练模型文件确认与哈希校验
zip 里的预训练模型可能是三种格式之一:Darknet 的 .weights、TensorFlow 的 .ckpt / .index / .meta、或者冻结后的 .pb。用 file 命令看文件头最直接:
file weights/* weights/*/* 2>/dev/nullDarknet .weights 开头是版本号和迭代次数等元信息,文件头一般不是 ASCII;TensorFlow checkpoint 是二进制但会伴随 .index 和 .meta;.pb 本身就是 protobuf,可以用 strings 查到可读节点名。判断技巧:如果压缩包里同时有 yolov3-tf.ckpt.data-00000-of-00001 和 checkpoint 文件,说明作者已经把 Darknet 权重转成 TensorFlow 格式,直接加载即可。如果只有 .weights,后面转换步骤少不了一块。
预训练模型版本要和数据集的类别数匹配。安全帽项目通常把 COCO 的 80 类权重截断成两类来用,转换脚本会在最后一个卷积层前砍掉输出维度。下载之后先校验哈希,避免传输损坏:
sha256sum weights/darknet53.conv.74得到的 64 位哈希要和压缩包附带校验文件比对。没有校验文件时,至少观察文件大小:darknet53.conv.74 常见大小在 150MB 到 160MB 之间,差太多就直接换来源。这一步省不得,因为旧项目训练时如果载入的是半截权重,报错会在几个 epoch 之后才出现,排查成本反而更高。
3. 用 Anaconda 复刻 TensorFlow 1.x 环境并加载预训练权重
YOLO V3 的 TensorFlow 1.x 代码对 CUDA 版本很敏感,装错组合最常见的报错是 Could not load dynamic library 'libcudart.so.10.0'。TensorFlow 1.15 需要 CUDA 10.0 与 cuDNN 7.4,网上能找到的多数中文教程也把 Python 锁在 3.6/3.7。在开始转换权重之前,先把环境固定下来。
3.1 anaconda 安装 tensorflow 1.15 的一组固定命令
常见做法是用 conda 新建独立环境,不要动 base 环境。下面的命令组在 Windows 和 Linux 下均适用:
conda create -n tf1 python=3.7 -y conda activate tf1 pip install tensorflow-gpu==1.15.0 numpy==1.17.5 pip install opencv-python==4.1.2.30 pillow lxml tqdm参数说明:numpy 锁在 1.17.5,是为了避免 TensorFlow 1.x 和 numpy 1.19+ 的array_function兼容问题;opencv 用 4.1.x 是因为 3.x 的接口在旧检测脚本里读取 uint8 时行为不同,4.2+ 偶尔会导致画框时字体崩掉。如果机器没有 NVIDIA 卡,把 tensorflow-gpu 换成 tensorflow==1.15.0,CPU 版跑推理可以,训练 416x416 的大尺寸会非常慢。
安装完先验证 GPU 是否可见:
python -c "import tensorflow as tf; print(tf.__version__); print(tf.test.is_gpu_available())"打印出 1.15.0 且 gpu 为 True 才继续。输出 False 时不要急着换安装包,先跑 nvidia-smi 看驱动版本,再检查 cuDNN 是否被放进了 conda env 的 lib 目录。很多“已经安装成功但训练时找不到 GPU”的问题,都出在 conda 环境里没有 libcudnn.so 这个细节上。
3.2 从 Darknet 权重转换到 TensorFlow checkpoint 的常见做法
如果压缩包里只有 darknet53.conv.74 或 yolov3.weights,需要用转换脚本把它变成 TensorFlow 可加载的 ckpt。绝大多数开源项目的转换逻辑是逐层读取 Darknet 的二进制权重,再按 cfg 里的 layer type 顺序写入 tf 变量。
下面是一段简化思路的示意代码,用来帮助理解转换流程:
import tensorflow as tf import numpy as np def load_weights_variable(var, weights_data, start): shape = var.get_shape().as_list() size = np.prod(shape) values = weights_data[start: start + size].reshape(shape) start += size return start, tf.assign(var, tf.constant(values, dtype=tf.float32))逻辑说明:Darknet 的 .weights 文件保存的是一维数组,转换脚本按 cfg 中各层的卷积核大小裁出对应分片,再 reshape 成变量的原始维度。上面的代码没有处理 BN 层的 gamma、beta、mean、var 排布,真正转换时要卷积层和 BN 层成组读取。想手工验证转换是否正确,可以随机抽一个 conv2d 的 kernel 值,和 darknet 权重里对应偏移做逐位对比。
实际项目里一般直接执行转换脚本,调用方式类似:
python convert_weights.py \ --cfg configs/yolov3.cfg \ --weights weights/darknet53.conv.74 \ --output checkpoints/yolov3-tf脚本结束后,目录下会出现 checkpoint、yolov3-tf.ckpt.index、yolov3-tf.ckpt.data-00000-of-00001 三个文件。看到这组文件,说明权重转换成功,后面训练脚本的 restore 路径填 checkpoints/yolov3-tf 即可。注意 Train.py 加载的是路径前缀,不能带 .ckpt.data 后缀。
3.3 网络结构、输入尺寸与三个输出张量
TensorFlow 1.x 版的 YOLO V3 最常把输入占位符定义成 [None, 416, 416, 3],输出是三个尺度的特征图:13x13、26x26、52x52,对应大中小目标。每个网格点会输出 num_anchors/3 乘以(5 + num_classes)个值,其中 5 是 4 个框坐标加 1 个置信度,num_classes 对安全帽识别是 2。
配置里最影响结果的是下面几个参数:
| 参数名 | 常用值 | 影响 |
|---|---|---|
| input_size | 416 | 小于 416 提速但小目标丢失 |
| num_classes | 2 | 对应 helmet 和 head |
| anchors | 9 组从小到大 | 小锚框保留在 52x52 输出层 |
| strides | [8, 16, 32] | 分别对应 52x52、26x26、13x13 的下采样倍率 |
安全帽检测属于典型的小目标密集场景,视频监控里人头可能只占几十像素。常见做法是在第一个 52x52 输出层上增大置信度权重,或者在数据增强阶段对图片做 416 以内的随机裁剪,强制模型学习局部特征。如果直接把 COCO 那套 80 类配置搬过来只改 classes,十有八九小目标全丢掉。
4. 用 YOLO V3 训练自己的安全帽检测模型的参数清单
现在很多教程一上来就是 yolov5 训练自己的数据集,但 YOLO V3 的 TensorFlow 1.x 版训练脚本依然围绕 train.py 展开,核心是 --dataset、--classes、--pretrained_weights 这几个参数。换汤不换药,区别主要在超参数的选值和 cfg 里的隐藏项。
4.1 train.py 的常用参数和推荐取值
用一段命令作为起点:
python train.py \ --dataset data/safehat/train.txt \ --val_dataset data/safehat/val.txt \ --classes data/safehat.names \ --pretrained_weights checkpoints/yolov3-tf \ --input_size 416 \ --epochs 100 \ --batch_size 8 \ --learning_rate 1e-4 \ --gpu 0参数说明:
- pretrained_weights 优先指向第 3 章生成的 ckpt 前缀,而不是直接填 .weights 文件,train.py 里需要的是 TensorFlow checkpoint 路径。
- batch_size 8 是显存不够时的安全值;显存充足时可以上到 16,但 cfg 里的 subdivisions 要同步调。
- learning_rate 1e-4 用于迁移学习。如果从零开始训练,通常是 1e-3 起步。安全帽数据量往往只有几百张,用 1e-3 很快会过拟合,所以我一般建议迁移学习阶段一律从 1e-4 开始。
train.py 训练时会读取 cfg 文件里的超参数,cfg 中与训练强相关的 5 项需要特别注意:
| cfg 字段 | 推荐值 | 坑 |
|---|---|---|
| batch | 64 | 实际显存占用必须再除以 subdivisions |
| subdivisions | 8 | 真实 batch 大小 = batch / subdivisions |
| learning_rate | 0.001 | 自定义小数据集上要降到 0.0001 |
| steps | 40000, 60000 | 小数据要按 epoch 换算成 step 数 |
| classes | 2 | 改完 classes 必须同步改最后一层卷积滤波器数 |
表格里的坑在旧项目里出现频率很高。TensorFlow 代码有时直接读 cfg 里的 batch 为 64,再除以 subdivisions 得到 mini-batch;如果代码走的是 argparse 而不是 cfg,则以命令行参数优先。两者混用会导致显存爆掉或者训练 iter 数对不上。
4.2 锚框聚类:安全帽数据集的 9 组 anchors 要重新算
迁移学习阶段可以沿用 COCO 的原始锚框,但施工安全场景的目标尺寸分布和 COCO 差异很大,帽子在图像中的比例更小且更集中。常见做法是写一个 k-means 脚本对训练集 xml 里的宽高聚类,得到 9 组 anchors。
import numpy as np from pathlib import Path import xml.etree.ElementTree as ET from sklearn.cluster import KMeans boxes = [] for xml_file in Path("data/safehat/Annotations").glob("*.xml"): root = ET.parse(xml_file).getroot() for obj in root.findall("object"): bbox = obj.find("bndbox") w = float(bbox.find("xmax").text) - float(bbox.find("xmin").text) h = float(bbox.find("ymax").text) - float(bbox.find("ymin").text) boxes.append((w, h)) boxes = np.array(boxes) kmeans = KMeans(n_clusters=9, random_state=0).fit(boxes) anchors = kmeans.cluster_centers_ print(np.round(anchors, 1))逻辑说明:sklearn 的 KMeans 用欧氏距离聚类,效果勉强能看;标准 YOLO 锚框聚类应该用 IoU 距离 d = 1 - IOU(box, centroid),这样小目标不会被大目标拉偏。上面的代码适合快速估算,真要用于训练,需要把欧氏距离换成 IoU 距离,并且做 k-means++ 初始化。
算出的 9 组 anchors 按从小到大的顺序分配:1-3 组给 52x52,4-6 组给 26x26,7-9 组给 13x13。安全帽场景常见输出是前几组宽度在 10-30 像素之间。如果聚类结果里最小锚框大于 50,说明数据集里存在大量远距离群体照,此时要做随机裁剪增强,否则小目标会被当成背景。
4.3 Loss 曲线和 mAP 验证
训练过程中保存的 checkpoint 默认在 checkpoints/ 下,train.py 会记录 loss 到日志目录。用 TensorBoard 看曲线:
tensorboard --logdir logs浏览器打开 6006 端口,只关注三个现象。第一,loss 在前 10 个 epoch 有没有从高位降下来;第二,是否在 epoch 60 附近进入平台期;第三,val loss 上升而 train loss 继续下降,就是过拟合。第一种现象通常是学习率太高,第二种要把 steps 提前,第三种要增加数据增强或缩小模型输入尺寸。
评估脚本一般叫 evaluate.py,输出每个类别的 AP 和整体 mAP:
python evaluate.py \ --dataset data/safehat/val.txt \ --classes data/safehat.names \ --weights checkpoints/yolov3-tf \ --nms_thresh 0.45 \ --iou_thresh 0.5参数说明:nms_thresh 是 NMS 的 IoU 阈值,值越大越容易合并重叠框,0.45 是 YOLO V3 常见默认;iou_thresh 是计算 mAP 时判定预测框是否命中真值的阈值,0.5 是标准 PASCAL 指标。对 helmet 和 head 两类且头部遮挡严重的数据集,如果 helmet 的 AP 低于 0.7,先看两类标注框的重叠是不是太高。施工场景里帽子紧贴头部,框的 IoU 经常超过 0.7,评估结果容易两头掉。
5. 冻结 PB 做推理并把三个高频故障拧掉
训练完的 checkpoint 不适合直接丢给线上服务,TensorFlow 1.x 的常见做法是先固化成 .pb 文件,再通过 tf.compat.v1 加载。安全帽识别这类边缘部署场景,PB 体积小、计算图固定,推理时不会因代码版本变动而踩坑。
5.1 单张图片与视频流推理命令
python detect.py \ --image test_images/photo1.jpg \ --weights checkpoints/yolov3-tf \ --classes data/safehat.names \ --score_thresh 0.30 \ --nms_thresh 0.45 \ --output output/photo1.jpgdetect.py 内部流程是加载模型,把图片 resize 到 416x416,跑 session.run 得到 detection_boxes,再把坐标映射回原图。score_thresh 0.30 是安全帽监控常用值,因为误检比漏检好处理;调到 0.5 会漏掉远距离的小帽子。视频流推理把 --image 换成 --video path.mp4,配合 OpenCV 的 VideoCapture 逐帧处理。
5.2 checkpoint 冻结为 PB 的最小步骤
TensorFlow 1.x 的 freeze_graph 工具能直接复用。命令形如:
python freeze_graph.py \ --input_checkpoint checkpoints/yolov3-tf \ --output_graph frozen_model.pb \ --output_node_names detectionsoutput_node_names 是检测输出的张量名,在 train.py 源码里搜索常被命名为 detections、pred_sbbox 等。如果名字填错,会报 KeyError 或输出空张量。冻结后建议用 40-50 张验证图对比 ckpt 和 pb 的 mAP,两者应完全一致;不一致多半是 BN 层在冻结时没有正确融合。
5.3 三个高频故障
显存不足时,把 batch_size 降为 2,或将 cfg 的 subdivisions 调到 16,同时设置显存按需增长:
config = tf.ConfigProto() config.gpu_options.allow_growth = True sess = tf.Session(config=config)检测框在视频里抖动时,把 NMS 阈值压到 0.35,或者对连续帧的框坐标做指数滑动平均。如果抖动只在特定位置出现,多半是输入图片被 VideoCapture 默认宽高改变了,和模型无关。
推理速度远低于预期时,检查是否漏了输出版本校验;更常见的是冻结后的 PB 里 BN 层 epsilon 未融合,导致 CPU 推理变慢。安全帽场景需要提帧率时,把输入尺寸降到 320,mAP 通常只掉 1-2 个点,帧率能提升 40% 左右。最后确认 checkpoint 的前缀三个文件都在,加载时报 missing file 时直接看路径前缀,别只看报错行尾的 .index。
本文还有配套的精品资源,点击获取