简介:面向计算机视觉初学者与行人检测开发者,这份基于Python的行人检测系统完整演示了HOG特征提取与SVM分类器在视频流中的应用,并配有简单跟踪算法。资源包共25个文件,大小155.2MB,主要包括main.py与detection1.py两套Python源码、行人样本bmp/png图片、两段avi演示视频,以及OpenCV项目配置文件(xml/iml),目录结构清晰,便于直接运行验证。已有8171人学习下载。通过源码与视频,可系统理解行人检测的关键流程:从灰度化与归一化预处理,到多尺度空间构造、8x8单元格划分、梯度方向直方图计算、块归一化,再到SVM分类器输出检测框;同时还能接触多尺度窗口扫描与卡尔曼滤波等简单跟踪思路。配套图像样本覆盖不同姿态和场景的行人,便于调整窗口大小、步长及分类阈值,也可迁移至车辆、骑车人等目标检测任务,是计算机视觉实战入门的扎实参考。
1. 行车记录仪里的AI:为什么OpenCV一行代码就能做行人检测,实战却要完整方案
两个月前有位开发者来找我,说他的监控视频里需要把行人框出来,看到网上一行代码就能调用OpenCV自带的行人检测器,结果放到自己的视频里,白天效果勉强能看,一到晚上满屏都是误检框,CPU占用还飙到90%以上。他问是不是代码写错了。其实代码没写错,是方案选型错了。这份Python行人检测视频资源解决的正是这个问题——不只是一段能跑的检测脚本,而是把视频场景下从检测器选型、参数调节到后处理、落地优化的完整链路打包好。适合刚入门计算机视觉的学生,也适合产品原型阶段需要快速验证的工程师。它能让你在拿到视频素材后,最短时间跑通检测流程,并知道每帧画面背后的计算逻辑。
2. 经典HOG + SVM路线:detectMultiScale参数调优与第一版可跑代码
2.1 为什么先讲HOG而不是直接上深度学习
行人检测这个任务,业界绕不开的第一个里程碑是HOG(方向梯度直方图)特征结合SVM分类器。2005年提出的思路,到今天仍没完全退役。原因有两点:一是HOG对刚性物体、直立行走的人体轮廓有很强的表征能力,行人不像猫狗那样姿态千变万化,躯干和四肢的相对位置相对固定;二是计算量可控,在CPU上跑实时视频不是不可能。
OpenCV把训练好的HOG行人检测模型内置在hog.setSVMDetector()里,真正让新手迷惑的是后续的detectMultiScale——这个函数名里藏着两个概念:多尺度(MultiScale)和检测(Detect)。多尺度意味着它把图像按不同比例缩小后逐层扫描,就好比站在不同距离看同一个行人;检测则是用滑动窗口遍历每一层,判断窗口内是不是人。
初学的人经常以为detectMultiScale传入一张图就会返回所有人的框,其实它的内部做了大量工作。理解不了“尺度金字塔”和“滑动窗口”这两个概念,后面调参就是瞎子摸象。
2.2 完整可跑代码:detectMultiScale四个核心参数
下面这份代码是资源包里第一版脚本的精简形态,也是我每次在新环境验证OpenCV是否正常工作时的固定动作。
import cv2 # 读取视频文件,0 表示摄像头,这里以视频文件为例 cap = cv2.VideoCapture("input_video.mp4") # 初始化 HOG 行人检测器 hog = cv2.HOGDescriptor() hog.setSVMDetector(cv2.HOGDescriptor_getDefaultPeopleDetector()) # 参数:控制多尺度扫描的精细度、误检容忍度、行人最小框尺寸 scale_factor = 1.05 min_neighbors = 5 min_size = (64, 128) max_size = (0, 0) while True: ret, frame = cap.read() if not ret: break # detectMultiScale 返回检测框、权重值 boxes, weights = hog.detectMultiScale( frame, winStride=(4, 4), padding=(8, 8), scale=scale_factor, finalThreshold=min_neighbors, hitThreshold=0, ) # OpenCV 返回的是 (x, y, w, h),注意不是 (x1, y1, x2, y2) for (x, y, w, h) in boxes: cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2) cv2.imshow("HOG Pedestrian Detection", frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()代码逻辑不复杂:循环读帧、检测、画框、显示。真正决定检测质量的是detectMultiScale的四个参数。
scale(缩放步长):金字塔每层图像缩小的比例。1.05表示每层缩小5%,比例越接近1.0,扫描的尺度越精细,越容易命中尺寸刚好的行人,但计算量成倍上涨。实际经验,视频分辨率1080p建议1.05,720p可以大胆用1.03。
finalThreshold(最少相邻检测次数):同一个行人在多个尺度、多个位置可能被多次命中,这个参数要求窗口被至少连续N次检测判定为行人后才输出。设太小人容易漏检——因为行人的中下部在部分尺度下可能被遮挡;设太大容易误检成堆。一般从4起步,调到8基本能压住大多数背景误检。
winStride(窗口步长):滑动窗口每次移动的像素。步长越小、重叠越多,检测越准,但计算量指数级上升。4像素是精度和速度的折中点。
2.3 后处理:为什么detectMultiScale出来的框需要再过滤
很多人在这个阶段直接跑上面的代码,会发现视频里同一个行人被框了两三次。这不是bug,是多尺度检测的必然结果——行人在某个尺度下被命中一次,在相邻尺度下又被命中一次。OpenCV在finalThreshold上做一些抑制,但效果粗糙,大量粘连框还是会出现在输出里。
标准的做法是自己再补一道非极大值抑制(NMS)。NMS的思路:所有检测框按置信度排序,置信度最高的框保留,和它IoU超过阈值的框全部删除,然后继续处理下一个高置信度框。代码实现大概是这样:
def nms(boxes, scores, iou_threshold=0.4): if not boxes: return [] x1 = boxes[:, 0] y1 = boxes[:, 1] x2 = boxes[:, 2] y2 = boxes[:, 3] areas = (x2 - x1 + 1) * (y2 - y1 + 1) order = scores.argsort()[::-1] keep = [] while order.size > 0: i = order[0] keep.append(i) xx1 = np.maximum(x1[i], x1[order[1:]]) yy1 = np.maximum(y1[i], y1[order[1:]]) xx2 = np.minimum(x2[i], x2[order[1:]]) yy2 = np.minimum(y2[i], y2[order[1:]]) w = np.maximum(0.0, xx2 - xx1 + 1) h = np.maximum(0.0, yy2 - yy1 + 1) inter = w * h iou = inter / (areas[i] + areas[order[1:]] - inter) inds = np.where(iou <= iou_threshold)[0] order = order[inds + 1] return keep这段NMS里的iou_threshold是核心参数。设0.3,两个贴得近的行人(比如并排走)会被合并成一个框;设0.6,同一行人的重叠框可能残留两个。监控场景行人稀疏推荐0.4,人流密集的步行街推荐0.3。需要强调一点,OpenCV返回的框是(x, y, w, h)格式,转成(x1, y1, x2, y2)时别忘记x2 = x + w,这个坐标转换错一步,后面的IoU计算全崩。
HOG加NMS这套组合拳下来,白天普通街景视频基本够用。但它有天然天花板:对遮挡、俯视角度、夜间光照三个场景极不友好。如果素材里大量出现这些情况,该切换到深度学习方案了。
3. 深度学习路线:视频场景下的模型选型、训练与评估
3.1 模型选型:从轻量到精度,四个候选方案怎么挑
深度学习检测模型目前主流的两个方向是两阶段(R-CNN家族)和单阶段(YOLO系、SSD)。两阶段模型精度上限高,但视频场景每帧都要做区域提议,速度通常不满足实时要求。单阶段模型把检测当作回归问题一步到位,帧率可观。
在我实际拆过的行人检测项目里,候选方案就四个:YOLO系轻量版、YOLO系标准版、SSD-MobileNet、Faster R-CNN。选型逻辑不复杂——先看视频分辨率,再看部署设备有没有GPU。
| 模型方案 | 输入尺寸 | 推理设备 | 预期帧率 | 精度表现 |
|---|---|---|---|---|
| YOLO轻量版 | 416x416 | CPU | 20-30 FPS | 漏检稍多,小目标弱 |
| YOLO标准版 | 640x640 | GTX系列 | 40-60 FPS | 精度稳定 |
| SSD-MobileNet | 300x300 | CPU | 25-35 FPS | 小目标差 |
| Faster R-CNN | 1000x600 | GPU | 5-8 FPS | 精度最高 |
做视频行人检测不用盲目追求精度。视频有天然的时间冗余——上一帧检测到了,下一帧大概率还在附近。所以我的建议是:CPU部署选YOLO轻量版,有GPU一律上YOLO标准版。Faster R-CNN除非要做学术对比实验,工程场景基本不碰。
这份资源包里的训练代码默认就是YOLO系结构,遵循的是“数据标注 → 数据集组织 → 训练 → 评估”这条标准链路。
3.2 数据标注:VOC格式转YOLO格式的核心映射
行人检测模型不是拿来即用的。公共数据集训练的模型在你自己摄像头视角下,表现会打四折到七折。视角变化是行人检测最大的泛化门槛——俯视45度和水平视角看到的行人特征完全不同。所以需要自采数据、自行标注。
标注工具选择很自由,常见的图形标注工具普遍支持Pascal VOC格式导出。VOC格式的标注文件是XML,里面用<name>标签表示类别名,用<bndbox>记录坐标;YOLO训练需要的格式是一行纯文本——类别id加归一化的中心点坐标和宽高。两者映射关系:
| VOC字段 | YOLO格式 | 计算方式 |
|---|---|---|
| name | class_id | person → 0 |
| xmin, ymin | cx, cy | (xmin+xmax)/2/width |
| xmax, ymax | w, h | (xmax-xmin)/width, (ymax-ymin)/height |
归一化这一步坑很多。很多人直接把像素坐标除以原图尺寸,但当训练时会做随机缩放、裁剪,坐标系的基准必须是“标注时的那张原图”,不能是增强后的图。资源包里有个转换脚本,流程是:解析XML → 按上述公式归一化 → 写入txt → 划分训练验证集。
3.3 训练脚本参数详解与启动方式
训练配置直接决定模型能不能收敛,epochs、batch_size、imgsz三个参数是基础。下面这段是从资源包里抽出来的训练入口配置:
# config.py 训练参数速览 # 图像输入尺寸,必须是32的倍数(下采样五次的约束) IMG_SIZE = 640 # batch size,单卡训练时显存不够就减半 BATCH_SIZE = 16 # 训练轮数 EPOCHS = 200 # 初始学习率,cosine退火会从这里开始衰减 LEARNING_RATE = 0.001 # 类别数,只用person一个类就是1,如果单独做了man/woman拆类则相应增加 NUM_CLASSES = 1启动训练的命令常见做法是:
python train.py --data dataset.yaml --weights yolov8n.pt --img 640 --batch 16 --epochs 200 --device 0--data指向数据集配置文件,里面写清楚train和val集的图片路径、类别数量、类别名称;--weights可以是预训练权重,也可以填""从零训。从零训练CONV层从随机权重起步,收敛慢;加载预训练权重做迁移学习是通用做法,相当于模型已经在千万张图上见过物体长什么样,你只需要让它适应行人的特定姿态分布。
--img 640要重点说。很多人以为输入越大精度越高,在行人检测这个任务上不绝对。行人属于中型目标,一个在1080p视频里高200像素的行人,缩小到640x640输入里还能占一定比例;但缩到416x416就只剩100像素左右,特征开始丢失。另一方面输入越大推理越慢,视频场景帧率优先。综合下来,视频帧率敏感场景选416,精度敏感选640。
3.4 评估指标与帧率测试的正确姿势
训练完不能只看loss下降就完事。行人检测的实际效果要看三个数:mAP、Recall@0.5、FPS。
mAP反映整体检测精度,0.5是IoU阈值。行人类别一般要求mAP@0.5达到85%以上才算产品可用。Recall(召回率)比精度更要紧——监控场景漏检一个行人可能出安全事故,宁可多框几个背景也不能漏人。
FPS测试最容易犯的错是在评估脚本里连着推理几百张图然后算平均时间。这种做法忽视了Warmup机制——GPU在推理前几帧时在做缓存初始化,速度偏慢;线性层和卷积层的cuDNN算法搜索也发生在最前面。标准做法是前30帧丢弃、不计入时间,取稳定后的帧平均值。后面第五部分会专门给一个测试脚本。
4. 落地避坑:视频检测最常见的五个翻车现场与排查套路
4.1 坑一:满屏误检框,背景纹理被当成人
现象:视频里墙角、树木、招牌边缘不断出现细长框,一个画面同时几十个框。
原因:min_size参数没设。detectMultiScale的滑动窗口会从极小尺寸开始扫描,纹理密集区域在缩小后的尺度下看起来和人形轮廓相似。HOG的误检主要来自图像金字塔的底层——小窗口内的梯度方向分布恰好撞上人体模型的统计分布。
解决:把min_size设置成视频里真实行人的最小像素尺寸。假设行人最远出现在画面里高度只有100像素,就把min_size设为(64, 128)。注意,min_size过大会漏掉远处的小目标,过小又失去过滤意义,需要对着视频标注一帧最远行人高度来反推。
4.2 坑二:同一个行人被框三四个框
现象:框在行人身上连环套娃,或者一个行人的头、躯干、腿各出一个框。
原因:多尺度金字塔相邻层同时命中,NMS没生效或IoU阈值设太高。如果你用的是HOG方案但忘了自写NMS,这个问题必现。深度学习方案则多半是NMS阈值调到了0.7,重叠框没被抑制干净。
解决:统一把IoU阈值降下来。行人身材细长,两个框的IoU相对较低,0.3到0.4是合理区间。同时检查坐标转换逻辑——如果x2误写成x + w又除以了错误的分母,IoU会被算偏,NMS形同虚设。
4.3 坑三:夜间视频检测率骤降,白天模型成了瞎子
现象:白天效果尚可,到了晚上画面里行人框断断续续,身体只框半个。
原因:光线不足导致行人轮廓与背景亮度差缩小,梯度方向直方图变得不稳定。HOG本质依赖边缘梯度,夜间行人衣服深色部分与暗部背景混成一片。深度学习模型稍好,但训练数据里夜间样本占比不够时同样会退化。
解决:优先级最高的手段是数据增强——HSV空间的亮度通道随机降级、加高斯噪声模拟传感器暗电流。想根治就采集夜间真实数据重新标一部分。先后关系要摆正:增强只能缓解,真实夜间样本才是解药。
4.4 坑四:视频推理每秒只能跑两三帧
现象:CPU风扇狂转,画面明显卡顿,点击暂停后框位置和画面内容对不上。
原因:输入分辨率设置过大,或者没有做ROI裁剪。很多人的第一个版本是直接把1080p整帧扔进检测器——HOG会在每个尺度上计算特征,YOLO则要对整幅图像做推理。固定摄像头场景下,大量计算浪费在根本不可能出现行人的区域。
解决:两步走。先用视频帧差法或者固定ROI把检测区域裁剪出来,比如只保留画面下半部60%区域做检测;然后控制检测器的输入尺寸上限。资源包里视频处理模块的默认做法就是先对帧做ROI提取,再做缩放,帧率能提升2到3倍。
4.5 坑五:检测框剧烈抖动,同一行人每帧框的位置飘忽不定
现象:行人在画面上保持不动,框却在上下左右摇摆,宽高也在变。
原因:单帧检测天然是独立决策的,相邻帧的检测框没有做时序关联。加上检测器对行人的姿态变化敏感——抬手动作会让框瞬间变高,弯腰动作会让宽高比突变。跟踪模块的缺失让这种抖动直接暴露在最终输出里。
解决:把检测结果过一道平滑滤波器。对每个检测框的中心点和宽高做EMA指数移动平均,平滑系数取0.6到0.8。更省事的做法是引入跟踪器——检测只在关键帧做(每隔5帧),中间帧用卡尔曼滤波预测位置。资源和精度平衡点需要自己试,但我见过的项目里EMA平滑已经能打消大多数抖动。
5. 让视频推理真正可用:帧率测试、Warmup与工程化细节
这一部分收在视频推理的工程化上。训练出模型只是第一步,让它在真实视频上稳定跑起来才是检验方案的地基。我在某图像处理Demo项目中踩过的教训是:模型在验证集上mAP有90%,整段视频跑完却慢到没法看,究其原因就是推理循环写得太粗糙。
一个合格的推理循环至少包含三块:Warmup、时间统计、跳帧策略。Warmup让GPU完成计算图初始化和cuDNN算法选择;时间统计从稳定帧开始,取平均值才是真实的推理耗时;跳帧策略则是视频场景偷帧率的合法手段——行人不会瞬移,每两帧检测一次、中间帧复用上一帧结果,视觉观感几乎没有差异。
import cv2 import time import numpy as np from model import load_detector # 加载模型,这里以通用检测器接口为例 model = load_detector(weights="best.pt", device="cuda:0") # 跳过前30帧做Warmup,不参与计时 warmup_frames = 30 cap = cv2.VideoCapture("test_video.mp4") frame_count = 0 total_time = 0.0 for _ in range(warmup_frames): ret, _ = cap.read() if not ret: break # 空帧跑一次推理,触发GPU初始化 dummy = np.zeros((640, 640, 3), dtype=np.uint8) model.predict(dummy) while True: ret, frame = cap.read() if not ret: break # 推理计时从帧读取后开始 start = time.time() results = model.predict(frame) elapsed = time.time() - start frame_count += 1 if frame_count >= 10: # 跳过前10帧的启动波动 total_time += elapsed # 稳定后平均耗时才有参考意义 avg_fps = frame_count / total_time if total_time > 0 else 0 print(f"稳定推理帧率: {avg_fps:.2f} FPS") cap.release()warmup_frames设置为30是我常用的起点,因为多数GPU在二十帧内能完成所有算子的预热。frame_count >= 10这个阈值同样有讲究——视频解码器刚开始读帧时也有缓冲波动,前几帧的读取时间明显大于均值,强行纳入统计会让帧率虚低。真正该记为有效时间的是解码完成后、推理计算的那一段,所以我把计时起点放在model.predict之前,而不包含cap.read。
如果测出来的帧率还是不理想,第一个动手的位置是model.predict的输入尺寸,而不是模型本身。640降到512,帧率往往涨40%以上,mAP的损失大概率在2%以内。我在模拟项目X里就用这个办法,把一段1080p的行人检测视频从9帧提到了17帧,后来还顺手加了EMA平滑,那个抖动问题也随之缓解了。从那以后,我每次拿到新视频素材都强制走一遍这个流程:Warmup测帧率 → 按帧率调整分辨率 → 加后处理平滑 → 再回放验证。希望帮到你。
本文还有配套的精品资源,点击获取