简介:基于YoloV3+Tensorflow的行人检测系统完整项目,面向人工智能、通信工程、自动化等专业的高校学生与开发者,可支撑毕业设计、课程设计、项目初期演示等场景。资源代码经过测试、功能完整,配套设计文档,方便快速上手。项目共981个文件,压缩包60.28MB,主要包含14个Python源文件、3个cfg模型配置文件(含YOLOv3、Darknet53、Tiny版本)、用于界面展示的HTML/CSS/JS文件、演示动图gif与10段mp4实操视频,以及md/txt说明文档;源码和配置文件支撑算法核心,视频与动图直观展示检测效果,文档辅助理解项目结构与复现流程。已有44人学习。另附docx设计文档和PSD设计稿,便于撰写毕设报告或二次开发;可基于现有代码调整参数、更换数据集,快速扩展至其他目标检测任务。
1. 从“行人检测系统.zip”说起:这套源码要解决什么
做了几年视觉方向的工程,你会发现很多项目交付物不是在线部署的代码仓,而是压缩包里的一整套目录:.zip里既有训练脚本、模型权重,还有作者的注释和一份 README。标题里这串文字描述的就是这样的存在——一个基于 YOLOv3 和 TensorFlow 实现的行人检测系统源码包。它对应的典型应用是园区监控、出入口统计、智慧零售里的人群计数,核心任务是在画面中把“人”这个类别框出来,并输出坐标与置信度。
这套技术栈选得很有代表性:YOLOv3 是 2018 年提出的单阶段检测算法,速度和精度的平衡在工业界验证了多年;TensorFlow 则负责把训练好的网络部署到 Python 侧的推理管线里。拿到这样一个 zip,绝大多数人的第一反应是“先跑通再说”,但源码包往往缺少训练数据、依赖版本不一致、甚至模型文件路径是绝对路径。这篇文章会顺着 YOLOv3 从原理到落地的链路,把环境搭建、源码结构、训练评估和最后的日志留存问题一一拆开,让没碰过检测项目的人和熟手都能找到自己的切入点。
2. YOLOv3+TensorFlow 的设计取舍:网络结构、Anchor 与损失函数
2.1 骨干网 Darknet-53 与多尺度预测
YOLOv3 的骨干网络延续了 ResNet 的短路连接思想,但把卷积层加深到 53 层,称之为 Darknet-53。与 ResNet-152 相比,它在 ImageNet 上的 top-1 准确率接近,但每次前向传播的计算量更少。对行人检测场景来说,推理速度直接决定了监控摄像头单路视频每秒钟能处理几帧,Darknet-53 的残差结构让梯度传播更稳定,训练期间损失函数不会因为层数过深而飞速发散。
多尺度预测是 YOLOv3 区别于 v2 最关键的改动。网络在 32 倍、16 倍和 8 倍下采样处分别引出特征图,形成特征金字塔(FPN)。三个尺度的输出张量形状分别对应[batch, 13, 13, 3*(5+num_classes)]、[batch, 26, 26, 3*(5+num_classes)]和[batch, 52, 52, 3*(5+num_classes)]。大尺度特征图负责小目标,小尺度特征图负责大目标,而行人这类高度大约在 40 到 300 像素的物体,通常是在 26×26 和 52×52 这两层上被检出的。
在 TensorFlow 实现里,这份zip工程通常会把特征提取和三路输出封装成yolov3_tiny或完整版两个类。有的源码直接把三个输出头的卷积结果拼接起来,后续再拆开解码;有的则各自保留。后者更直观,也方便你只取中间层特征做可视化。如果看到代码里出现了route或concat变量名,对应就是特征金字塔的跨层合并逻辑。
2.2 Anchor 的参数化与坐标解码
YOLOv3 在 COCO 数据集上用 k-means 聚类出了 9 组 Anchor,针对行人检测,这些 Anchor 偏大,且宽高比不够贴合人体。常见的处理方式是重新用你自己的训练集做聚类。假设你在源码包的config.py里看到这行配置:
ANCHORS = [ [(10, 13), (16, 30), (33, 23)], [(30, 61), (62, 45), (59, 119)], [(116, 90), (156, 198), (373, 326)] ]这其实是 COCO 原始配置。对于行人数据集,我更推荐把 Anchor 个数保持 9 个,但用下面这段代码重新聚类:
import numpy as np from sklearn.cluster import KMeans boxes = np.load('pedestrian_boxes.npy') # 形状为 (N, 2),每行是[w, h] kmeans = KMeans(n_clusters=9, random_state=0).fit(boxes) anchors = sorted(kmeans.cluster_centers_ / 32, key=lambda x: x[0] * x[1]) print(anchors)注意这里我把聚类得到的 Anchor 除以 32,是因为 YOLOv3 解码时落在原图坐标,而特征图缩放了 32 倍,统一到特征图尺度可以避免数值溢出。聚类完成后,把 Anchor 按面积从小到大分成三组,分别分配给三个检测头。通常小尺度特征图上用小 Anchor,大尺度特征图用大 Anchor,这与 FPN 的设计一致。这一设置极影响小行人的召回率,尤其当你的摄像头离人比较远时。
坐标解码在训练和推理时用的是同一套公式:
box_xy = (tf.sigmoid(pred_xy) + grid) * stride box_wh = tf.exp(pred_wh) * anchors * stridepred_xy是网络输出的中心点偏移,经过sigmoid后限定在 0 到 1 之间,再加上网格偏移乘以步长,还原到原图尺寸。pred_wh是 log 空间的尺度残差,指数化后乘上 Anchor 宽度和高度。对行人框来说,不合理的tf.exp会让宽高在训练初期爆炸,很多源码包因此会在pred_wh外面再乘一个小于 1 的系数,或者使用tf.clip_by_value做限制。
2.3 TensorFlow 实现里的损失函数
行人检测是单类别任务,所以分类损失使用二分类交叉熵(对无行人/有行人两个逻辑),而不是 COCO 的三分类 softmax。代码里常写为:
loss_cls = tf.reduce_mean( tf.nn.sigmoid_cross_entropy_with_logits( logits=pred_cls, labels=tf.cast(object_mask, dtype=pred_cls.dtype)))在 YOLOv3 训练时,object_mask是一个三值张量,1.0表示该 Anchor 负责正样本,0.0表示是否有目标但 IoU 不高,-1表示忽略样本。忽略样本在损失计算时要被过滤掉,否则背景样本数量会爆炸。很多深坑都出在这里:如果源码包里没有对ignore_mask做正确判断,训练曲线会看起来在收敛,但验证集上的 mAP 却一塌糊涂。
边界框回归部分,原版 YOLOv3 用的是二元交叉熵配合 MSE 的思路,但实践里大多数 TensorFlow 工程会引入 GIOU 或 CIOU。如果你手上的源码包还是纯 MSE 损失,针对行人任务可以直接替换成 GIOU 损失,代码只在loss_fn里改几行。原因是行人框的长宽比相对固定,GIOU 会惩罚框位置和形状的综合误差,收敛速度比 MSE 快,而且对目标附近出现的负样本更鲁棒。
在实际调试中,你还需要关注coord_scale和noobj_scale这两个超参数。默认情况下,坐标损失权重是 1.0,但背景区域比例极高,很多实现会额外增加背景框的 loss 权重,比如设置noobj_scale=0.5。这是经验值,你可以根据训练时每个 batch 的正负样本数量动态调整。目标检测的 loss 并不是越低越好,它只反映当前 Anchor 分配的合理性,真正要做的是同时观察分类准确率和召回率。
3. 环境准备与源码解析:把“zip”还原成可运行项目
3.1 Anaconda 环境与 TensorFlow 安装
拿到源码包后,我习惯先新建一个独立的 conda 环境,不直接放在 base 环境里。因为 YOLOv3 相关代码在 TensorFlow 1.x 和 2.x 下运行差异极大,共用环境会污染其他项目。先创建环境并激活:
conda create -n yolov3 python=3.7 conda activate yolov3 pip install tensorflow-gpu==1.15.0如果源码是基于 TensorFlow 2 重写的,那么直接用 2.6 或更新版本更省事:
conda create -n yolov3_tf2 python=3.8 conda activate yolov3_tf2 pip install tensorflow==2.8.0注意 TensorFlow 2 下很多原作者代码仍在使用tf.compat.v1,你需要检查session和placeholder是否能用,必要时在文件顶部添加tf.compat.v1.disable_eager_execution()。如果训练或推理时报AttributeError: module 'tensorflow' has no attribute 'get_default_graph',说明缺少兼容层,通常可以在 import 后面补上:
import tensorflow as tf tf.compat.v1.enable_eager_execution()这里没有把tf_session这类配置写死,是因为源码包里的训练脚本可能声明了GPU显存按需增长。没有这个配置,多个训练进程会互相抢占显存,导致 OOM。常见做法是在脚本顶部加入:
physical_devices = tf.config.list_physical_devices('GPU') for device in physical_devices: tf.config.experimental.set_memory_growth(device, True)3.2 解压后先看 README 和目录结构
把.zip解压后,先不要着急运行python train.py,先扫一遍顶层文件列表。通常这类源码包会包含以下几个目录和文件:
| 文件/目录 | 作用 |
|---|---|
train.py | 训练入口,负责数据读取、损失计算、迭代更新 |
detect.py | 单张图片或视频推理入口 |
utils/ | 数据增强、Anchor 处理、NMS、解码工具 |
model/ | 网络结构定义,可能包含darknet53.py |
config.py | 配置文件,包含锚框、类别数、训练超参数 |
data/ | 原始图像和标签文件,标签格式常见为 YOLO 的 txt |
checkpoints/ | 存放 checkpoint 文件或 pb 模型 |
我见过不少源码包把训练数据和代码放在一起,但标签文件缺失,或者路径写成了作者本机的绝对路径。打开config.py时重点看classes_path、train_path和anchors_path这三个变量。如果它们指向['/home/user/...']这样的绝对路径,你需要改成相对路径:
classes_path = 'data/classes.txt' train_path = 'data/train.txt' anchors_path = 'data/anchors.txt'train.txt里每一行是一张图片的路径,建议统一按os.path.join方式生成,否则 Windows 和 Linux 下路径分隔符不一致会直接报错。可以临时用这个命令重写文件:
find /path/to/dataset/images -name "*.jpg" > data/train.txt然后在后面追加标签文件路径。有的源码包会要求图片和标签名一一对应,即a.jpg对应a.txt,我一般会在数据生成脚本里严格保持这个命名,省得后续检查麻烦。
3.3 权重文件的几种形态与加载方式
源码包里常见的模型文件有三种:.weights(Darknet 原版格式)、.pb(TensorFlow 冻结图)和.ckpt/.index(TensorFlow checkpoint)。很多包只提供了 Darknet 权重,你需要用load_pretrained_weights.py或转换脚本把它读入 TensorFlow 模型。转换脚本的核心逻辑如下:
import numpy as np import tensorflow as tf from model.yolov3 import YOLOv3 def load_darknet_weights(model, weights_file): fp = open(weights_file, 'rb') np.fromfile(fp, dtype=np.int32, count=5) for layer in model.layers: if not layer.name.startswith('conv'): continue bn = layer.next_layer weights = bn.get_weights() shape = weights[0].shape ...这段代码的关键是np.fromfile(fp, dtype=np.int32, count=5)去掉 Darknet 权重文件前 5 个 int32 的元信息。之后按卷积层顺序读取权重,一个卷积层会跟一个 BN 层,所以实际参数分三部分:BN 的 gamma、beta、均值、方差,以及卷积核权重。很多源码包的转换脚本会和自己的模型定义绑定,你直接跑通即可,但一旦在模型里增删了层,转换就会错位,需要检查层匹配。
如果你不想折腾 Darknet 权重,也可以直接用 COCO 预训练的.pb文件,然后微调最后几层,不过这对手头标注数据和训练目标的要求更高。一般来说,行人检测用转换好的 Darknet 预训练权重做迁移学习,在专有场景下效果会好于随机初始化训练,而且收敛更快。
4. 训练、评估与推理:跑通完整流程
4.1 修改配置文件并启动训练
训练前需要明确类别数。行人检测场景只有 1 个类别,因此config.py里的num_classes设置为 1,classes.txt文件里写入person。同时需要确认输入尺寸,常见的配置是 416×416,这决定了训练速度与精度的平衡点。如果监控画面里目标较小,可以改成 544×544,但显存占用和训练时间会大幅增加。
触发训练的命令在不同源码包里不一致,比较典型的入口是:
python train.py --train_file data/train.txt \ --val_file data/val.txt \ --batch_size 8 \ --learning_rate 0.001 \ --epochs 50 \ --classes_file data/classes.txt训练过程中的关键参数说明如下:
| 参数 | 推荐值 | 说明 |
|---|---|---|
batch_size | 4 ~ 16 | 受显存限制,检测网络中的 feature map 极大,调大后先看是否 OOM |
learning_rate | 0.001 起步 | 预训练权重下可设 0.0001 做微调 |
epochs | 50 ~ 100 | 行人数据集若只有几千张,50 个 epoch 足够 |
ignore_thresh | 0.5 | Anchor 与真值 IoU 低于此值且不负责预测的样本会进入 noobj 计算 |
num_classes | 1 | 类别数,若误设成 80,最后输出通道数量会对不上 |
训练脚本里通常会检测checkpoints目录下的最近 checkpoint。如果想从头重新训练,可以加一个--reset参数,或直接把该目录清空。这里有个高频坑:如果你之前训练时改了num_classes,但保留了旧 checkpoint,加载时最后一个卷积层的权重形状不匹配,报错信息会像ValueError: shapes (255, 256, 3, 3) and (18, 256, 3, 3) are incompatible。解决方法是删除对应路径下的 checkpoint,或从预训练权重重新初始化最后一层。
4.2 用 TensorBoard 监控训练曲线
训练跑起来后,不要傻等。TensorFlow 的tf.summary.FileWriter会把 scalar 写入 event 文件,然后你在终端执行:
tensorboard --logdir checkpoints/logs打开浏览器访问http://localhost:6006,主要关注loss、avg_iou、recall三条曲线。正常情况是loss整体震荡下降,avg_iou逐步上扬,recall在 30 个 epoch 后维持高位。如果loss在初期突然冲高后不回落,检查学习率和梯度裁剪。在 YOLOv3 中,我习惯把全局梯度范数裁剪到 10 以内,代码写法是:
optimizer = tf.compat.v1.train.AdamOptimizer(learning_rate) grads_and_vars = optimizer.compute_gradients(loss) grads_and_vars = [(tf.clip_by_norm(g, 10), v) for g, v in grads_and_vars] train_op = optimizer.apply_gradients(grads_and_vars)avg_iou表示预测框和真实框的平均 IoU,如果一直很低,多关注 Anchor 中心和宽高是否匹配数据集,即使在预处理阶段做过letterbox处理,长条形的行人框也可能因为宽高比过小而被错误分配。
4.3 图片和视频推理的最小 Python 实现
推理是整个源码包里最容易被直接复用的部分。以 TensorFlow 2 + Keras 实现为例,推理模块需要完成输入预处理、前向传播、解码、NMS、可视化这几步。下面的代码是从一个常见实现里抽出的核心:
import sys import cv2 import numpy as np import tensorflow as tf from model.yolov3 import YOLOv3 from utils.anchors import generate_anchors from utils.nms import nms model = YOLOv3(num_classes=1, input_shape=(416, 416)) model.load_weights('checkpoints/model.ckpt') def preprocess(img): img = cv2.resize(img, (416, 416)) img = img / 255.0 return img[np.newaxis, ...].astype(np.float32) def decode_predictions(pred, anchors, num_classes): # pred.shape: (1, height, width, 3*(5+1)) grid_h, grid_w = pred.shape[1:3] boxes = [] for i in range(3): mask = i * 3 anchor = anchors[mask:mask+3] ... return boxes代码前半段的model.load_weights会自动加载最近一次保存的权重。在实际的源码包中,generate_anchors会读data/anchors.txt,而不是硬编码在代码里;nms函数可以使用tf.image.non_max_suppression,也可以直接用 numpy 实现。注意推理时输入必须要做归一化,但不要在推理函数里二次缩放,否则框的坐标会偏移。
通过命令行对一段监控视频执行检测:
python detect_video.py --video_path input.mp4 --output_path output.mp4 --conf_thresh 0.4 --nms_iou 0.5conf_thresh是行人框的置信度阈值,监控场景里误报成本较高,我一般会调到 0.5 以上;nms_iou是去重阈值,对行人这种高度相近的框,0.45 到 0.5 表现得比较平稳。如果你发现同一行人被重复打了两个框,说明 NMS 阈值过高或 Anchor 分配的置信度未归一化;反过来,如果两个很近的行人被合并成一个框,则说明阈值过低。
5. 面向监控场景的部署优化与日志防篡改
落到监控场景,源码包通常不会止步于“能跑出检测框”。你需要把模型导出成冻结图,降低前向耗时,同时让下游业务拿到可校验的检测记录。这里给出两个直接可用的技巧。
第一个技巧是导出冻结图并进行 TensorFlow-TRT 或在 CPU 上做算子融合。对任意训练好的 checkpoint,使用freeze_graph时最关键的是确定输出节点名。在模型定义里,输出变量往往叫output_boxes、pred_sbbox这类名字,先把节点名打印出来再导出:
from tensorflow.python.tools import freeze_graph freeze_graph.freeze_graph( input_graph='model.pb', input_saver='', input_binary=True, input_checkpoint='checkpoints/model.ckpt', output_node_names='output_boxes', restore_op_name='save/restore_all', filename_tensor_name='save/Const:0', output_graph='frozen_model.pb', clear_devices=True, initializer_nodes='' )冻结后测试单步推理耗时,若仍满足不了实时性,改用TF-TRT转换或对输入尺寸做缩放。通常 416×416 输入在 1080p 视频上可以跑到 25-40 FPS,如果帧率仍偏低,优先减少视频抽帧而不是降低网络输入尺寸,因为后者对行人这种细节目标影响更明显。
第二个技巧是为检测结果添加日志哈希链。安防场景里经常需要对检测记录做审计,而普通的文本日志是可以被篡改的。我在工程中会用一个简单的链式哈希保存每一条记录的完整性,核心思路是每一条日志的哈希值都包含上一条日志的哈希,任何中间记录被改动都会导致后续哈希全部失配。
import hashlib import json prev_hash = "" log_chain = [] def append_detection(frame_id, boxes, confidences, prev_hash): record = { "frame_id": frame_id, "boxes": [[float(bb[0]), float(bb[1]), float(bb[2]), float(bb[3])] for bb in boxes], "confidences": [float(c) for c in confidences], "prev_hash": prev_hash } record_json = json.dumps(record, sort_keys=True).encode("utf-8") current_hash = hashlib.sha256(record_json).hexdigest() return current_hash, record for i, boxes in enumerate(all_boxes): prev_hash, entry = append_detection(i, boxes, confs, prev_hash) log_chain.append(entry)这个片段把frame_id、检测框、置信度和上一个节点的哈希记录在 JSON 里,并持续性回传新哈希。审计时只需按顺序重放日志文件并比对哈希值即可,任何增删改都能立刻定位到具体的frame_id。若你的源码包本身就带日志模块,可以直接把这段合并进检测主循环,不需要修改 TensorFlow 推理逻辑。
最后再给一个实操验证点:在监控视频的第一帧到第 100 帧做连续检测,统计帧间框的 IoU 变化。若同一行人在连续帧内的 IoU 小于 0.3,往往是模型抖动或后处理参数设置过高,可以通过平滑检测框的位置坐标来修正。把预测框按时间做指数移动平均,即可让输出框更稳定,也不会显著增加延迟。
本文还有配套的精品资源,点击获取