首页 / 资讯中心 / 文章详情

HOG+SVM行人检测实战:从特征工程到检测器实现全流程解析

HOG+SVM行人检测实战:从特征工程到检测器实现全流程解析 ★ FEATURED ARTICLE
HOGSVM行人检测算得上计算机视觉领域一个绕不开的经典项目了。哪怕现在深度学习检测器满天飞这个老组合依然值得亲手做一遍它帮你理解特征工程、分类器训练、滑窗检测、非极大值抑制这一整套传统视觉流水线是后面学任何检测算法的基础底子。我用C、OpenCV、VS2015这套组合完整做过一版从样本准备、特征提取、SVM训练到检测器落地踩了不少坑也总结了一套流程分享出来给正要下手的朋友参考。不管你是课程作业、毕业设计还是单纯想搞懂传统目标检测的内部原理这篇都能让你少走弯路。1. 项目整体拆解先搞懂这套流水线在干什么1.1 核心需求解析标题里的“一整套”其实包含了从数据到模型再到检测的完整闭环准备正负样本、提取HOG特征、训练SVM分类器、滑动窗口扫描图片、输出检测结果。说直白点就是让程序学会在一张图片里“找行人”——给它看一堆“有行人的图片区域”正样本和“没有行人的图片区域”负样本让它自己总结出一套规律然后拿这套规律去新图片里找目标。HOG特征负责“描述”SVM负责“决策”。HOGHistogram of Oriented Gradients方向梯度直方图统计图像局部区域的梯度方向分布相当于告诉分类器“行人的轮廓长什么样”SVMSupport Vector Machine支持向量机则是拿这些描述信息画一条分类边界把行人和非行人分开。两者组合是2010年前后行人检测的主流方案OpenCV里专门封装了HOGDescriptor配合SVM做检测至今稳定可用。1.2 技术选型为什么还是C、OpenCV、VS2015有人可能问都什么年代了还用VS2015我的看法是不少学校实验室和老项目的代码仓库就停留在VS2015OpenCV 3.x新装OpenCV 4.x配合新编译器当然顺手但接手旧代码时环境不匹配会带来一堆莫名其妙的链接错误。做这个项目环境匹配比版本新旧更重要VS2015配OpenCV 3.4.x是经过大量验证的稳定组合。C的好处是贴近底层、内存控制明确尤其在跑图像金字塔和多尺度滑动窗口时性能优势很明显。Python写起来快但C版本能让你真正体会到“每一帧的计算量是哪来的”。而且OpenCV的C接口资料最多遇到问题基本都能搜到答案。1.3 项目整体流程与关键环节整体流程可以分成四大步样本准备收集正样本行人图片和负样本不含行人的背景图统一尺寸并生成描述文件。特征提取与训练用HOGDescriptor计算每张样本的特征向量送入SVM训练得到分类器模型。检测器集成把训练好的SVM参数封装进HOGDescriptor用detectMultiScale完成多尺度滑窗检测。结果后处理通过重叠度去重NMS思路画出最终检测框。这四步每一步都有细节下面逐一展开。2. 环境搭建与数据准备这两步决定你的项目能走多远2.1 VS2015下配置OpenCV的完整流程安装OpenCV后解压出来通常是一个opencv文件夹里面有build和sources两个目录。build里是编译好的库和头文件sources里是源码和示例我们主要用到build。在VS2015里新建一个空C项目做以下配置项目属性 - VC目录 - 包含目录添加opencv\build\include、opencv\build\include\opencv2。库目录添加opencv\build\x64\vc14\lib注意是x64还是x86要和解决方案平台一致。链接器 - 输入 - 附加依赖项Debug模式填opencv_world340d.libRelease模式填opencv_world340.lib。带d的是调试版不带的发行版混用会链接失败。运行时把opencv\build\x64\vc14\bin里的dll加到系统PATH或者直接拷到exe同目录不然运行时会报“找不到opencv_world340d.dll”。提示Debug和Release混淆是最常见的坑你Debug编译却用了release的lib会报一堆无法解析的外部符号。最好在属性管理器里给Debug和Release分别设置而不是只改全局。OpenCV 3.x以后库文件统一成了opencv_world不再分模块编译省了很多链接配置麻烦。如果你还是OpenCV 2.4.x那需要逐个添加opencv_core、opencv_imgproc、opencv_highgui等一堆lib那是另一个时代的玩法。2.2 正负样本怎么准备最合理训练样本直接影响最终检测效果这个环节值得花时间。正样本包含行人的图片区域需要裁剪成固定尺寸HOG检测窗口一般用64x128。这个尺寸是经验值——行人整体轮廓大致呈2:1的宽高比64x128既够描述细节又不会特征维度过大。可以从公开行人数据集或自己拍摄的视频中截取每张只保留一个行人主体不要有太多遮挡。负样本不含行人的任意背景图比如街道、树木、建筑、车辆等。负样本数量要足够多一般建议是正样本的2到3倍。因为负样本素材容易获取可以从不含行人的风景图、街景图里裁剪出一堆小图块。样本准备好后我习惯把它们整理成OpenCV的trainset格式一个文件夹放正样本一个文件夹放负样本再生成一个txt描述文件每行写图片路径和标签。这样做的好处是训练脚本读起来方便后期加难例也容易。2.3 样本数量与质量的经验值很多初学者在样本数量上纠结我的经验是正样本1000到2000张负样本3000张以上效果就比较可观了。少于500张正样本模型很难学到足够的行人轮廓特征。另外样本质量比数量重要——正样本里混入大量模糊、截断、只有半个人的图片训练出来的模型反而会混乱。我踩过一次坑正样本里有不少背影和弯腰的人结果检测器对背影特别敏感对正面行人反而漏检。后来重新清洗样本保证正样本里包含不同姿态、不同穿着、不同光照条件效果立刻改善。3. HOG特征与SVM训练原理搞清楚参数才会调3.1 HOG特征到底在算什么HOG特征的思路可以用一个类比理解你在画一张素描时第一件事是勾轮廓、描边不需要看颜色和纹理只要抓住物体的边界走向人就能认出这是什么。HOG做的正是这件事——统计图像每个局部区域的梯度方向分布用这些分布模式代表目标的形状特征。具体计算过程分四步灰度化并计算梯度用一维差分算子[-1, 0, 1]分别在x和y方向卷积得到每个像素的梯度幅值和方向。划分cell把图像分成8x8像素的小格子cell每个cell内统计9个方向的梯度直方图。方向范围0到180度分9个bin每20度一个区间。组成block并归一化把相邻的2x2个cell组成一个16x16的blockblock内把4个cell的36维直方图拼接起来做L2归一化。这一步是为了减弱光照和对比度变化的影响。滑动拼接block按步长8像素滑动blockStride8最终整张64x128的图像会被分成7x15个block位置每个block提取36维特征最终得到特征向量维度是7 * 15 * 36 3780。3780维里每一维都对应某一个位置的某个梯度方向区间的强度SVM就是用这3780维去判断“是行人”还是“不是行人”。3.2 训练SVM线性核是首选SVM训练时OpenCV 3.x及以上版本推荐用cv::ml::SVM接口。行人检测场景下线性核SVM已经足够而且检测速度远快于RBF核效果差别不大。核心训练参数设置如下cv::Ptrcv::ml::SVM svm cv::ml::SVM::create(); svm-setType(cv::ml::SVM::C_SVC); svm-setKernel(cv::ml::SVM::LINEAR); svm-setC(0.01); svm-train(trainingData, cv::ml::ROW_SAMPLE, labels);这里的C值控制误分类的惩罚力度。C越大模型越追求把所有训练样本分对容易过拟合C越小边界越宽松泛化性更好。行人检测场景下0.01到0.1之间是比较合理的起点。我实际测试过相同数据下C0.001时漏检率明显上升C1时对训练集完美分类但对测试图片误检增多。训练API有个细节容易搞错trainingData必须是cv::Mat行数为样本数列数为特征维数3780数据类型用CV_32FC1。标签用CV_32SC1正样本标1负样本标-1。3.3 从SVM模型到HOGDescriptor的转换训练完SVM不能直接拿去检测需要把支持向量和偏置转成HOGDescriptor能用的检测子参数。这一步很多教程没讲透其实是关键。OpenCV的HOGDescriptor用setSVMDetector加载一个长度为特征维数1的数组前3780个数是系数向量最后一个是偏置bias。从SVM模型提取的代码如下cv::Mat supportVectors svm-getSupportVectors(); cv::Mat rho svm-getDecisionFunction(0); int featureDim supportVectors.cols; std::vectorfloat detector(featureDim 1); for (int i 0; i featureDim; i) { detector[i] supportVectors.atfloat(0, i); } detector[featureDim] -rho.atfloat(0, 0); cv::HOGDescriptor hog; hog.setSVMDetector(detector);注意最后偏置项取了负号这是因为HOGDescriptor内部判定逻辑是score 0即视为目标而SVM的决策函数是score features * coefficients - rho转换时要把这个关系对齐。提示如果你只用单类SVM训练即只提供正样本让SVM自己找负样本边界OpenCV也提供了HOGDescriptor::getDefaultPeopleDetector()这样的预训练检测子可以直接用来做行人检测效果不错但不一定能适配你特定的应用场景比如俯拍、特定服装所以自己做训练仍然值得。4. 检测器实现多尺度滑窗到底怎么滑4.1 图像金字塔与滑动窗口的配合训练完成后检测阶段的核心就是滑动窗口。窗口大小默认为64x128以一定的步长在图像上移动每移动一次就用HOGDescriptor计算窗口内的特征并交给SVM打分。但行人出现在图片里大小不一你不能假设所有人都是64x128像素。解决办法是图像金字塔把原图按一定比例逐层缩小每个尺度跑一遍滑动窗口小尺度的行人会在某一层正好接近64x128从而被检测出来。detectMultiScale函数封装了这件事关键参数如下hog.detectMultiScale( img, // 输入图像 found, // 输出检测框 weights, // 输出置信度分数 0, // hitThreshold判定阈值越大越难命中 cv::Size(8, 8), // winStride窗口滑动步长 cv::Size(32, 32), // padding图像边缘扩展 1.05, // scale金字塔缩放比例 2, // finalThreshold用于检测框合并 0 // useMeanshiftGrouping一般设0 );参数里最值得关注的是hitThreshold和scale。hitThreshold相当于置信度门槛。默认0也就是SVM打分大于0就认为是行人。实际中建议设置在0到0.5之间——设太低了误检多设太高了漏检多。我一般是先0跑一遍看看误检数量再逐步调高。scale是金字塔缩放倍率。1.05表示每层只缩小5%层数多、检测细腻但速度慢1.1到1.2层数少、速度快但小目标的检测精度会下降。追求速度用1.1追求效果用1.05看你的应用场景。4.2 多尺度检测的重叠框合并滑动窗口天然会产生大量重叠框——同一个行人往往相邻几步的窗口都能把它打出来多个尺度下也会重复框住。这时候就需要做去重合并OpenCV的detectMultiScale的finalThreshold参数就承担了这个职责内部用的是类似NMS的分组策略。如果想更精细控制我推荐拿到found后用一段简单的重叠度IoU判断来合并遍历所有框把重叠面积大于某个阈值的框归为同一组取平均位置或最大置信度的位置作为最终检测框。我自己更常用finalThreshold2它表示两个检测框满足一定重叠条件时归为一组。分值越低合并越激进检测框数量越少。阈值设为2到3之间在大多数场景下平衡性较好。小于这个值会出现同一个行人被画两三个框的情况大于这个值则容易把相邻的两个行人错误合并成一个框。4.3 检测完整代码示例这里给出一段可以直接运行的检测示例读取一张图片用训练好的检测子做多尺度检测绘制框并输出结果统计#include opencv2/opencv.hpp #include iostream int main() { cv::Mat img cv::imread(test.jpg); if (img.empty()) { std::cout load image failed std::endl; return -1; } cv::HOGDescriptor hog; hog.setSVMDetector(cv::HOGDescriptor::getDefaultPeopleDetector()); std::vectorcv::Rect found; std::vectordouble weights; hog.detectMultiScale(img, found, weights, 0, cv::Size(8, 8), cv::Size(32, 32), 1.05, 2, 0); for (size_t i 0; i found.size(); i) { cv::Rect r found[i]; cv::rectangle(img, r, cv::Scalar(0, 0, 255), 2); std::cout detected: r.x , r.y , r.width , r.height score weights[i] std::endl; } cv::imshow(result, img); cv::waitKey(0); return 0; }跑起来之后你会发现直接使用OpenCV的默认行人检测子对标准的直立行人效果不错但遇到一些特殊角度或密集人群检测框会飘。这正是后面用自训练模型替换默认检测子的动力所在。5. 训练流程优化与难例挖掘模型效果翻倍的关键5.1 难例挖掘到底怎么做如果你直接拿初始正负样本训练完就去检测大概率会碰到一个情况误检特别多尤其是树荫、电线杆、空调外机、柱子这类竖长方形的物体特别容易被认成行人。为什么因为这类物体在局部梯度特征上和行人轮廓有相似性仅靠初始负样本不足让SVM认识到这些“不是行人”。解决办法是难例挖掘hard negative mining。流程是用初始训练好的检测器在大批量不含行人的图片上跑一遍。把被误检为行人的区域false positives截取出来作为难负样本加入负样本集。重新训练SVM。重复1到3步一般迭代2到3轮直到误检数量不再明显下降。我第一版模型在测试集上误检率是35%做了两轮难例挖掘之后降到了8%左右效果立竿见影。难例挖掘是传统目标检测最值得投入时间的环节。具体实现时把检测器输出中score大于阈值的box逐个裁剪缩放到64x128存为负样本图片然后重新生成训练描述文件。注意难负样本不要和原负样本完全重复否则模型只是记住了那几个位置。5.2 训练迭代中样本比例与参数调整每次迭代重新训练时负样本数量会越来越多正负样本比会逐渐拉大。理论上SVM对类别不平衡有一定容忍度但比例过悬殊比如1:10以上的时候模型会偏向把所有样本都判为负类。我的经验是控制正负比在1:3到1:5之间可以通过随机下采样部分旧负样本或增加正样本数量来调节。C值在难例挖掘过程中可以微调。如果某一轮训练后发现误检集中在某类区域把C稍微调大比如从0.01调到0.05可以提升对难样本的分类能力但要盯着过拟合风险。5.3 模型评估用准确率和召回率说话项目做完了怎么向别人证明效果好不能光靠肉眼感觉要量化。我习惯在测试集上统计两个指标准确率Precision检测出的框中真正是行人的比例衡量误检情况。召回率Recall所有标注行人中被成功检测出来的比例衡量漏检情况。实际操作中把测试图片的真实行人区域标注成矩形框然后用检测结果和标注框做IoU匹配IoU大于0.5视为正确检测。最后画一条PR曲线曲线的趋势可以直观反映模型在不同阈值下的表现。这套评估流程后来我用于所有目标检测项目是通用方法论。6. 常见问题与排查技巧我把踩过的坑一次说清6.1 训练不收敛或者准确率低的几大原因样本尺寸不统一特征维度和训练样本尺寸强相关正样本和负样本都必须统一到64x128。我试过正样本是64x128负样本忘了resize直接导致SVM训练报维度不一致训练直接崩溃。特征数据未归一化HOG特征本身经过block内的L2归一化但不同样本之间的整体亮度差异仍然存在。SVM对特征尺度敏感训练前把featureMat整体做一遍归一化缩放到-1到1之间效果会更稳定。方法很简单计算所有特征的均值和标准差做标准化后再训练。正样本过于单一如果正样本都是同一角度、同一光线、同一背景的行人模型泛化能力就弱。换个场景图片测试漏检率立刻飙升。多收集不同场景下的行人裁剪图比花时间调参更有效。误把检测阈值设得太高有人训练完模型后为了减少误检把hitThreshold调到1以上结果误检少了漏检却也多了。阈值的选择要结合PR曲线去取平衡点而不是一味追求零误检。6.2 检测速度慢怎么优化检测慢通常有三个原因图像分辨率过大、金字塔层数太多、滑动窗口步长太小。对症下药输入图像先做长边缩放比如限制到640像素以内。把scale从1.05改成1.1或1.15层数直接减少三分之一速度显著提升。winStride从8x8改成16x16窗口数量减少到原来的四分之一检测速度提升明显但小目标可能漏检需要权衡。如果用多线程可以把不同尺度的图像金字塔分配到不同线程处理我实际用OpenMP写了并行版本四线程下速度接近翻倍。6.3 内存崩溃、越界等代码层面的坑ROI越界多尺度检测时图像缩放到较小尺寸后滑动窗口可能会超出图像边界。OpenCV内部做了边界处理但如果你自己写滑窗扫描一定要判断cx和cy加窗口尺寸后是否越界否则读内存直接崩溃。Mat类型不匹配训练SVM时特征矩阵是CV_32FC1标签矩阵是CV_32SC1。用Mat::zeros(rows, cols, CV_32FC1)创建后逐个赋值没问题但如果你用了Mat_float模板类注意访问方式要统一。加载检测子维度对不上自己训练的SVM转出来的detector长度是3781但如果换了一张不同尺寸的训练样本特征维度就变了setSVMDetector时OpenCV会直接抛异常。检查一下训练数据尺寸是否统一为64x128是最快的排查方式。6.4 结果不理想时的排查顺序我给自己定了一个排查顺序遇到模型效果差就按这个来先看正样本质量有没有大量模糊、遮挡、尺寸不统一的情况。再看负样本丰富度有没有包含和你实际应用场景中背景相似的图片。然后看训练日志SVM的迭代次数是否收敛支持向量数量是否过多过多往往是过拟合信号。最后才调参数C值、hitThreshold、scale依次调整。这个顺序能避免很多无意义的调参操作时间要花在真正影响模型质量的地方。7. 扩展思考这个项目还能怎么延伸做完HOGSVM行人检测之后我最大的感受是传统检测方法并不是死路一条它和深度学习框架互补。用HOGSVM训练一个轻量检测器只需要CPU就能实时跑在嵌入式设备上比深度学习模型有天然优势。而且HOG特征本身就是深度学习目标检测中许多底层特征的灵感来源理解它之后再看CNN的梯度计算和卷积操作会有种“原来如此”的感觉。如果你有余力可以在现有项目上做几个方向的扩展用ZCA白化或PCA对HOG特征做降维观察对速度和精度的影响换用DPMDeformable Part Model或者HOG线性SVM的变体来检测其他物体车辆、自行车等把难例挖掘流程自动化写成一个循环脚本让训练过程一次性跑完。动手做一遍这些扩展对你理解整个目标检测领域会很有帮助。最后说个实操体会图像检测项目里数据整理的时间通常远超模型训练和调参的时间。每次迭代前先确认样本怎么来、标签怎么组织设计好文件结构后面跑难例挖掘时才不会手忙脚乱。这套HOGSVM辛苦搭起来的流程练的是你对完整机器学习项目的掌控能力——数据、特征、训练、评估、迭代这一整套循环在任何框架下都不过时。
阅读完成 · 觉得有帮助?
咨询建站