简介:基于Python实现的行人检测系统,配套完整源码与演示视频,适合计算机视觉初学者快速上手。项目以HOG(方向梯度直方图)特征提取与SVM分类器为核心,涵盖图像预处理、多尺度检测、检测框输出和简单目标跟踪等内容,可帮助读者理解经典行人检测流程并迁移到其他物体识别任务。压缩包共有25个文件,包含11个bmp测试图像、5个png图片、2个Python源程序、2个avi演示视频、4个xml配置及1个iml工程文件,整体约155.2MB,文件分类清晰,便于对照学习和运行调试。目前已有8173人学习下载,资源不仅提供可直接运行的检测代码,还配有丰富的行人正负样本和视频素材,适合在OpenCV环境下结合源码边看边练,掌握从特征构造到分类判定的完整链路。
1. 这个项目:一个能直接跑起来的Python行人检测视频Demo
行人检测在计算机视觉里不算新鲜,但多数教程讲到模型原理就停了,真正落到一段视频上、跑出带绿色方框的结果,中间隔了不知道多少个坑。这份源码正是那种“能落地”的项目:用 Python 和 OpenCV 实现一套完整的行人检测视频处理流程,输入任意街道监控视频,输出带检测框的标注视频,压缩包里自带了测试视频和可直接运行的脚本。它没有一上来就上 YOLO、依赖 GPU,而是老老实实选了 HOG + SVM 这条经典路线,让只有 CPU 的笔记本也能把 720p 视频处理流程全部跑通。适合刚接触计算机视觉的开发者用来理解目标检测全链路,也适合需要快速在监控视频上做人流统计原型的场景。
2. 技术栈与目录结构:HOG+SVM如何支撑起整个检测流程
2.1 技术选型:为什么用OpenCV内置方法而不是自己训模型
拿到压缩包我第一件事就是翻 requirements.txt,里面只有 opencv-python 和 numpy 两个依赖,立刻明白作者有意绕开深度学习路线。为什么选 HOG + SVM?因为 OpenCV 的 HOGDescriptor 自带了一套预先训练好的行人检测模型,也就是 HOG 特征加线性 SVM 分类器的组合。这套模型在行人数据集上训练过,权重封装在 C++ 库里,Python 侧一行 setSVMDetector 就能加载,不需要下载权重文件,也不需要安装 PyTorch 或 TensorFlow。对“快速跑通一个 demo”这个目标来说,没有比这更省事的选择了。
具体到原理,HOG 会把图像划分成小单元格,统计每个单元格内的梯度方向直方图,再把多个单元格归一化成块,形成描述人体轮廓的特征向量,随后交给 SVM 分类器输出是行人还是背景的置信度。OpenCV 的 getDefaultPeopleDetector 返回的就是训练好的 SVM 权重,所以这个项目里真正的检测代码只有几行,难点全在特征和权重上。这个项目的视频素材是街道场景,正好和 INRIA 行人数据集的分辨率和视角比较接近,默认权重不需要 fine-tune 也能有可接受的效果。如果你换一个完全不同的场景,比如俯瞰视角的停车场,HOG 的检测率会明显下降,那时才需要考虑重新训练或者换模型。
相比之下,深度学习检测器要配置模型结构、权重、类别标签、输出解码等一系列内容,跑起来 GPU 占用也不低。传统 HOG 方法虽然对遮挡、小目标、复杂背景的误检率偏高,帧率也比不上深度模型,但它能把流程讲清楚,也不会让你卡在环境上。我拆这类项目有个习惯:凡是目标检测 Demo,先跑传统方法拿一个 baseline,再决定要不要上深度学习,否则一上来就 YOLO,出了问题根本不知道是数据集、训练还是部署的锅。这套源码选 HOG + SVM 不是落后,而是对“快速跑通”这个目标最合适的取舍。
2.2 目录结构:源码包里的每个文件都是干什么的
解开压缩包后,目录结构长这样:
pedestrian-detection/ ├── videos/ │ └── street.mp4 ├── outputs/ ├── config.py ├── detector.py ├── nms.py ├── detect.py └── requirements.txt每个文件的职责可以列成一张表:
| 文件 | 作用 |
|---|---|
| detect.py | 主入口,负责读取视频、逐帧调用检测器、绘制框并写输出视频 |
| detector.py | 封装了 HOG 检测器,暴露 detect_pedestrians 函数 |
| nms.py | 非极大值抑制实现,用于消除重叠检测框 |
| config.py | 所有可调参数集中在一起 |
| requirements.txt | 依赖清单 |
detect.py 是流程调度者,它不做特征提取,只调 detector;detector.py 不关心视频怎么读,接收一帧图像返回检测框;nms.py 是通用函数,之后换成深度学习模型时同样可以复用。这种分层结构非常干净:想调检测参数只改 config.py,想换检测算法只重写 detector.py,主循环和视频输出完全不用动。我见过太多项目把所有逻辑塞在一个文件里,改一个参数要搜半天,这个包的划分是值得学习的模板。
config.py 里集中了几个核心参数:
# config.py VIDEO_PATH = "videos/street.mp4" OUTPUT_PATH = "outputs/result.avi" DETECT_SCALE = 1.05 WIN_STRIDE = (4, 4) PADDING = (8, 8) NMS_THRESHOLD = 0.4这里把参数放在 Python 文件里,没有用 yaml 或 json。对这种小的 demo 项目,直接写在 config.py 里反而直观,改完立即生效,不用额外处理解析逻辑。detector.py 的接口设计也值得提一下,它没有暴露 HOGDescriptor 对象,而是提供 detect(frame) 方法,输入一帧输出 boxes。这样做的好处是,未来把内部实现从 HOG 换成 YOLO,只要接口不变,detect.py 里的一行调用都不会改动。这种面向接口的小设计,比抽象基类什么的更实用。
videos 目录里的街道视频是测试样本,outputs 目录默认空着,跑完脚本后结果会生成在里面。输入输出分离的约定虽然简单,但能避免生成文件和源码混在一起,也方便 git 忽略 outputs。如果你要拿自己的视频测试,放到 videos 目录里,或者直接用命令行参数指定外部路径都可以。
2.3 环境准备:两步装好依赖
安装依赖只需要一条命令:
pip install -r requirements.txtrequirements.txt 建议这样写:
opencv-python>=4.5.0 numpy>=1.21.0注意用范围而不是固定版本。OpenCV 4.5 之后的 HOG 接口基本稳定,3.x 的 API 命名和现在有差异,固定老版本反而容易踩坑。装完验证一下:
python -c "import cv2; print(cv2.__version__)"能打印出版本号就说明环境通了。如果你之前装过 opencv-python-headless,需要先卸载再装标准版,两个包共存会互相覆盖,import 行为不可预期。我在 conda 环境下还遇到过 OpenCV 装好后无法加载 DLL 的情况,换一个干净的虚拟环境重装一般就解决了,这种依赖问题有点玄学,但好在不常发生。如果你的 Python 版本是 3.11 以上,部分预编译的 OpenCV 轮子可能安装失败,多试几个版本或者退到 Python 3.9,比折腾源码编译省心得多。
到这里环境就绪,下面开始拆核心代码。
3. 核心模块拆解:一个视频帧从输入到输出检测框的完整链路
3.1 视频读取与预处理:控制帧率与尺寸是性能的第一步
主入口 detect.py 的视频循环是整个项目的地基,先看代码:
import cv2 import config cap = cv2.VideoCapture(config.VIDEO_PATH) fps = cap.get(cv2.CAP_PROP_FPS) width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) writer = cv2.VideoWriter( config.OUTPUT_PATH, cv2.VideoWriter_fourcc(*'XVID'), fps, (width, height) ) while True: ret, frame = cap.read() if not ret: break small = cv2.resize(frame, None, fx=0.8, fy=0.8) boxes, _ = detector.detect(small) frame = draw_boxes(frame, boxes, 1 / 0.8) writer.write(frame)这段代码把整个流程压缩到了几行。cap.get 取到视频的帧率、宽度、高度,写入 VideoWriter 时保持原视频规格,这样输出的视频播放速度和尺寸都不会变。resize 到 0.8 倍是性能优化,后面检测的全部计算量都基于 small 帧,代价是产生的框坐标也在 small 上,所以 draw_boxes 里要用 1/0.8 把坐标还原回原图。如果不还原,输出视频里框会偏小,视觉上就是“没对准”。
这里有几个容易翻车的地方:VideoWriter 的编码参数在不同系统上不一样,Windows 和 Linux 下用 *XVID 基本通用,Mac 上如果输出文件打不开可以换成 *mp4v。另外,输出视频的帧尺寸必须和初始化时一致,如果主循环里画框时的帧尺寸变了,writer.write 会静默失败,文件也能生成但无法播放。建议拿短视频先测一下输出文件,再跑完整视频,否则跑完才发现白费时间。
3.2 检测器初始化:HOG描述子与SVM分类器的组合
detector.py 里封装了检测器,核心代码很短:
import cv2 class PedestrianDetector: def __init__(self): self.hog = cv2.HOGDescriptor() self.hog.setSVMDetector(cv2.HOGDescriptor_getDefaultPeopleDetector()) def detect(self, frame): boxes, weights = self.hog.detectMultiScale( frame, winStride=(4, 4), padding=(8, 8), scale=1.05, finalThreshold=2.0 ) return boxes, weightsHOGDescriptor 是 OpenCV 提供的 HOG 特征提取器,setSVMDetector 把预训练的 SVM 权重注入进去。getDefaultPeopleDetector 返回的是行人检测器的默认系数,窗口大小是 64x128,所以调用者不用关心滑窗怎么设计。detectMultiScale 是核心方法,内部会构建图像金字塔,在每一层上用滑动窗口提取特征并交给 SVM 判断。
参数里,winStride 是窗口滑动的步长,步长越小扫描越密,耗时越长;padding 是窗口周围填充的像素,可以让位于边界的目标也有机会被检测到;scale 控制金字塔的缩放比例;finalThreshold 控制多尺度结果合并的阈值。这些参数直接决定了漏检率和帧率,第 4 章会详细讲。需要注意的是,HOGDescriptor 的默认检测器只针对“行人”这一类,如果你检测猫狗,需要换模型,这也是传统方法的局限。
3.3 NMS:去掉重叠框是显示质量的关键
detectMultiScale 返回的原始结果经常是一堆互相重叠的框,因为同一个行人在不同尺度下都可能被检测到。所以项目里单独写了一个 nms.py,实现经典的非极大值抑制算法:
import numpy as np def nms(boxes, threshold=0.4): if len(boxes) == 0: return [] x1 = boxes[:, 0] y1 = boxes[:, 1] x2 = boxes[:, 2] y2 = boxes[:, 3] if boxes.shape[1] > 4: scores = boxes[:, 4] else: scores = np.ones(len(boxes)) 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 <= threshold)[0] order = order[inds + 1] return boxes[keep]NMS 的思路很直接:按得分从高到低排序,每次保留得分最高的框,然后计算它与其他框的 IoU,把重叠超过阈值的框全部丢弃。IoU 是交集面积除以并集面积,threshold=0.4 时,两个框只要重叠 40% 以上就被认为是同一个目标。boxes 输入格式要注意:detectMultiScale 返回的是 (x, y, w, h),但 NMS 里用的是 (x1, y1, x2, y2),所以在调用 nms 之前要先做转换。代码里对 boxes 列数做了判断,大于 4 说明带了置信度,可以直接作为排序依据;否则用全 1 作为得分,保持顺序稳定。
如果你不想自己实现 NMS,也可以直接用 cv2.dnn.NMSBoxes,但那是深度学习模块的接口,输入输出格式和这个 demo 里自定义的不完全一样。自己实现这段代码可以帮助理解原理,放到其他项目里也能改造成带类别的 NMS,比如类别比较多时按每个类别分别做抑制。
3.4 可视化与输出:在帧上画框并重新编码
检测到目标后,需要在原图上画框。draw_boxes 的实现:
def draw_boxes(frame, boxes, scale=1.0): for (x, y, w, h) in boxes: x, y, w, h = int(x / scale), int(y / scale), int(w / scale), int(h / scale) cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2) cv2.putText(frame, "person", (x, y - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) return frame这段代码首先把检测框的坐标从缩放后的图像坐标还原到原图坐标。cv2.rectangle 的 (0, 255, 0) 是 BGR 颜色,绿色不扎眼;线宽 2 像素。putText 在框上方写了 “person”,字体大小 0.6。坐标还原这个细节非常容易忘,特别是当你把 resize 逻辑写在另一个文件里时,漏掉 scale 参数会导致画出的框整体偏移,看起来就像检测器在随机乱画。调试这类问题,最好把检测坐标和原图坐标的差值打印出来,一眼就能看出问题。
如果要在框上叠加计数,可以在循环里维护一个计数器,比如total_count += len(bboxes),再把数字写到画面左上角。这个项目默认没有这个功能,但结构上很容易加,只需要在 draw_boxes 后面多画一行 putText。后续做二次开发时,计数往往比画框更贴近业务需求。
4. 运行与参数调优:让检测结果既不漏检也不误检
4.1 命令行启动:一条命令跑通整个流程
主脚本支持命令行参数,方便覆盖配置文件:
import argparse import config ap = argparse.ArgumentParser(description="行人检测视频处理") ap.add_argument("--video", default=config.VIDEO_PATH, help="输入视频路径") ap.add_argument("--output", default=config.OUTPUT_PATH, help="输出视频路径") ap.add_argument("--scale", type=float, default=config.DETECT_SCALE, help="检测金字塔缩放比例") args = ap.parse_args()这样启动:
python detect.py --video videos/street.mp4 --output outputs/result.avi如果已经上传了自定义视频,只需把 --video 换成自己的路径。参数没有全覆盖,比如 winStride 和 padding 只留了配置文件,没有暴露到命令行。这种取舍是对的:并不是所有参数都适合频繁变,scale 在测试不同清晰度视频时经常要调,所以放命令行;其他参数更偏“内功”,放 config 就够了。如果你希望在命令行里覆盖所有参数,可以继续往 argparse 里加,但要记得 argparse 的默认值最好从 config 读取,避免两处维护。
4.2 关键参数解读:scale、winStride、padding各自影响什么
上面代码里的核心参数可以总结成一张表:
| 参数 | 默认值 | 影响 |
|---|---|---|
| scale | 1.05 | 金字塔缩放比例,越大每层缩小越多,扫描层越少,速度越快,但是漏检率可能升高 |
| winStride | (4, 4) | 滑动窗口的水平和垂直步长,越小检测越密,耗时成倍增加 |
| padding | (8, 8) | 窗口边缘填充像素,影响贴近图像边缘的行人检测率 |
| finalThreshold | 2.0 | 多尺度合并阈值,越大越容易消除重复框,但可能合并掉邻近的真实目标 |
| hitThreshold | 0.0 | SVM 分类得分阈值,大于 0 可以降低误检,但也会降低召回 |
这几个参数的调整逻辑其实很朴素:scale 越大,金字塔层数越少,计算量越小,但如果在大的缩放步长之间正好跳过了一个行人的最佳尺度,就会漏检。winStride 越小扫描越密集,但 4x4 和 8x8 的计算量相差 4 倍。我经常先用 (8,8) 跑一版确认流程,再把步长调小追求更全的检测结果。
padding 的作用很多人会忽略。行人出现在画面边缘时,窗口只有一部分能覆盖目标,增加 padding 可以让特征计算更完整,从而召回边缘行人。不过 padding 太大也会带来额外计算量,所以默认 8 像素是一个平衡点。hitThreshold 默认是 0,如果你想减少误检,可以试试 0.2 或 0.5,代价是可能丢掉一些置信度低的真目标。这些参数没有绝对最佳,都要根据你的视频场景综合权衡。我一般会把参数组写进 config.py 的 dict 里,用不同命名对应不同场景,测试时切换字典键就行。
4.3 性能瓶颈与优化:从5帧到15帧的调整
跑这个 demo 最常见的抱怨是“太慢”。我自己的机器上 720p 视频用默认参数是每秒 4-5 帧,别说实时,连平滑查看都费劲。优化手段能稳定提到 12-15 帧,顺序如下:
第一步,缩小输入帧。把 resize 的比例从 0.8 改成 0.5,检测时间直接降到原来的 40%,代价是小目标的检出率下降。第二步,把 winStride 从 (4,4) 改成 (8,8),这一步影响最明显,计算量减少一半以上。第三步,把 scale 从 1.05 改成 1.1,金字塔层数减少,也能明显加速。第四步,改为灰度图检测:
gray = cv2.cvtColor(small, cv2.COLOR_BGR2GRAY) boxes, _ = detector.detect(gray)HOG 特征本身基于梯度,对颜色不敏感,所以灰度图完全可行,还能省一部分内存和转换开销。如果还需要更快,可以在视频循环里加一个跳帧逻辑,比如每两帧检测一次,中间帧沿用上一帧的框。但跳帧会丢失运动信息,如果画面里行人移动很快,框就会拖影,所以跳帧一般配合跟踪算法使用,这在最后一章会展开。
5. 避坑与常见问题排查:跑行人检测最容易翻车的四个地方
5.1 检测框重叠严重,同一个行人被框了三四次
现象:输出视频里每个行人身上叠着好几个框,有的框还来回抖动,看起来非常不专业。
原因:detectMultiScale 在不同金字塔尺度下对同一个目标输出了多个检测框,而 NMS 的阈值设置得过高(比如 0.5 以上),导致重叠到 40% 的那批框没有被清理掉。也有一种可能是 finalThreshold 设置过小,多尺度合并这一步就没有起作用。
解决:把 NMS 的 threshold 降到 0.3-0.4,同时把 finalThreshold 调整到 2.0 以上。如果你改过这些参数还是重叠,需要检查 nms.py 里的 boxes 转换是否正确:detectMultiScale 返回的是 (x, y, w, h),而 NMS 内部用的是 (x1, y1, x2, y2),少一步转换会让 IoU 计算完全错误,重叠框永远消不掉。调试时,我习惯把 nms 前后框的数量打印出来,如果数量没变化,说明抑制逻辑没生效。
5.2 检测速度慢到没法用
现象:视频像幻灯片一样一帧一帧跳,CPU 利用率却接近 100%。
原因:winStride 设成了 (2, 2) 或更小,滑动窗口的扫描次数以平方级增长;同时金字塔的 scale 太小,层数过多。720p 图像用 64x128 窗口以 2 像素步长遍历,计算量非常惊人。
解决:winStride 改回 (4, 4) 或 (8, 8),scale 调大到 1.05-1.1,输入帧缩小到 0.5 倍。如果程序里同时开了多个视频流,也要注意 CPU 是否是性能瓶颈。建议先跑单视频确认能达到可接受帧率,再多路并发。我一般会在 config 里放一个 PERFORMANCE_MODE 开关,一键切换快速模式和精确模式,测试时用快速模式,最终演示用精确模式,这样既保证开发效率,又不牺牲演示效果。
5.3 视频路径带中文导致打不开
现象:VideoCapture 成功创建,但 cap.read() 一直接收不到帧,ret 一直为 False,程序没有报错直接就结束了。
原因:OpenCV 在 Windows 上对中文路径的支持一直不稳定,底层文件读取用的编码与系统不一致。源码里的 config.py 如果直接写成 VIDEO_PATH = "D:/测试视频/街道.mp4",就会踩中这个坑。
解决:把视频复制到纯英文路径,或者用 os.path.abspath 和临时文件名中转。常见做法是先复制到当前目录下:
import shutil import os if not os.path.exists("temp_input.mp4"): shutil.copy(config.VIDEO_PATH, "temp_input.mp4")然后用 temp_input.mp4 打开视频,处理完再删除临时文件。虽然多了一次磁盘拷贝,但能彻底绕开编码问题。如果你不想复制,也可以尝试在文件路径前面加上特殊前缀,但那个技巧对某些编译版本无效,所以最稳妥的还是英文路径。
5.4 OpenCV版本差异导致API报错
现象:运行 detect.py 时报错 AttributeError: module 'cv2' has no attribute 'HOGDescriptor_getDefaultPeopleDetector'。
原因:OpenCV 3.x 与 4.x 的命名不同,部分老版本编译出的 cv2 里没有这个函数,或者需要写 cv2.HOGDescriptor().getDefaultPeopleDetector()。
解决:升级到 opencv-python 4.x,先卸载旧版再装新版:
pip uninstall opencv-python pip install opencv-python>=4.5.0如果项目环境不允许升级,可以把 detector.py 里的加载方式改写成兼容写法,先尝试新接口,再回退旧接口。不过这种兼容写法久了没人维护,我自己的经验是直接指定 Python 3.9 + OpenCV 4.8 作为标准环境,比写一堆 try except 更省心。每次接手新环境,我会先跑一个只加载 HOG 的冒烟脚本,确认环境没问题再跑完整流程,这样能把环境问题及早隔离开。
5.5 输出视频播放速度异常,看起来像快进
现象:输出视频的播放速度明显比原视频快或慢,人走路像在加速。
原因:VideoWriter 初始化时 fps 参数没有用原视频的 fps,而是硬编码成了某个固定值,比如 20 或 30。原视频如果是 25fps,你写成 30fps,播放时就会倍速。
解决:用 cap.get(cv2.CAP_PROP_FPS) 获取原始帧率,写入 writer,代码已经在 3.1 里给出。还有一个细节是 VideoWriter 在最后一帧退出循环后要调用 release(),否则文件头可能没有正确写入,导致播放器无法识别时长。虽然 Python 进程退出时会自动释放,但养成显式 release 的习惯,在后续做批量处理时能避免文件占用问题。
6. 二次开发改造:从HOG Demo升级到深度学习检测器
这个项目最好的改造方向是替换检测器,因为 HOG + SVM 在复杂场景下确实不够看。我的做法是保留 detect.py、nms.py、config.py 的框架,只重写 detector.py,把 HOG 换成基于 OpenCV DNN 的 YOLO 模型。这样既能保留原有视频处理主循环,又能大幅提升检测精度。改造后的 detector.py 接口保持不变:
import cv2 class PedestrianDetector: def __init__(self, onnx_path="yolov5s.onnx"): self.net = cv2.dnn.readNet(onnx_path) self.net.setPreferableBackend(cv2.dnn.DNN_BACKEND_OPENCV) self.net.setPreferableTarget(cv2.dnn.DNN_TARGET_CPU) def detect(self, frame): blob = cv2.dnn.blobFromImage(frame, 1/255.0, (640, 640), swapRB=True) self.net.setInput(blob) outs = self.net.forward() boxes = post_process(outs, frame.shape) return boxes, None这样改完后,主循环完全不用动,因为接口仍然是 detect(frame) 返回 boxes。需要注意 YOLO 返回的框格式是 (x1, y1, x2, y2),而项目里画框函数用的是 (x, y, w, h),所以要在 post_process 里转换成统一格式。这种接口对齐是二次开发里最容易漏的一环,漏了会在画框时直接报错。
如果要做更进阶的功能,可以引入跟踪逻辑。最简单的做法是给每个检测框分配一个 ID,下一帧用 IoU 做匹配,行人短暂被遮挡后仍然能保持同一个 ID,不会被重新计数。验证方法很简单:跑完一段视频,抽几帧人工数行人数量和框数量,记录漏检和误检数,与原视频做对比。我的习惯是每次改完检测器都重新跑一遍同一段测试视频,保证召回率不下降。从那以后,我每次接手这类检测 Demo,都会先跑通传统方法拿到 baseline,再决定要不要上深度学习。希望帮到你。
本文还有配套的精品资源,点击获取