HOG+SVM行人检测实战:OpenCV与Qt完整实现
1. 项目概述与背景HOGSVM行人检测是计算机视觉领域的经典项目广泛应用于安防监控、自动驾驶等场景。这个项目基于C语言使用OpenCV3.4.10和VS2015开发环境完整实现了从数据集准备到模型训练、测试再到Qt可视化界面搭建的全流程。我在完成这个毕业设计项目时深刻体会到几个关键点首先数据集的质量直接影响模型性能其次HOG特征提取参数必须严格一致最后工程实践中会遇到各种环境配置问题。下面我将详细分享整个项目的实现过程包括那些教科书上不会写的实战经验。2. 开发环境配置2.1 VS2015与OpenCV环境搭建VS2015虽然已经不是最新版本但稳定性很好适合学校实验室环境。配置OpenCV3.4.10时需要注意下载OpenCV Windows版自解压包设置系统环境变量Path添加OpenCV的bin目录路径在VS2015中创建空项目后需要配置以下项目属性// 包含目录添加 $(OPENCV_DIR)\include $(OPENCV_DIR)\include\opencv $(OPENCV_DIR)\include\opencv2 // 库目录添加 $(OPENCV_DIR)\x64\vc14\lib // 链接器输入添加 opencv_world3410.lib (Release模式) opencv_world3410d.lib (Debug模式)注意x64和x86平台要区分清楚建议统一使用x64平台。如果编译时报错找不到std::filesystem需要在项目属性中将C语言标准设置为C17或更高。2.2 Qt5与VS2015集成Qt用于构建可视化界面推荐使用Qt5.9及以上版本。集成步骤如下安装Qt时勾选MSVC 2015 64-bit组件安装Qt Visual Studio Tools扩展在VS2015的Qt选项中设置Qt版本路径创建Qt Widgets Application项目常见问题解决方案如果运行时提示缺少Qt5Core.dll等文件需要将Qt的bin目录如C:\Qt\5.9.9\msvc2015_64\bin添加到系统Path环境变量界面显示乱码时在main函数开头添加QTextCodec::setCodecForLocale(QTextCodec::codecForName(UTF-8));3. 数据集准备与预处理3.1 正负样本收集正样本建议使用INRIA行人数据集作为基础包含1805张64×128像素的行人裁剪图像。负样本可以从MIT场景数据集或自行采集街道、建筑物等场景图片。我的数据集构成正样本2000张INRIA自行标注负样本6000张MIT自行采集验证集正负样本各500张重要经验负样本必须严格检查避免混入任何行人图像。我曾因负样本中混入几个模糊行人导致模型出现严重误检。3.2 样本预处理代码详解正样本预处理的关键是将所有图像统一缩放至64×128像素这是HOG特征的标准输入尺寸。以下是改进后的批量处理代码#include opencv2/opencv.hpp #include filesystem #include vector namespace fs std::filesystem; void preprocessPosSamples(const std::string srcDir, const std::string dstDir) { // 检查并创建目标目录 if(!fs::exists(dstDir)) { if(!fs::create_directory(dstDir)) { std::cerr 无法创建目标目录: dstDir std::endl; return; } } // 支持的文件扩展名 const std::vectorstd::string validExts {.jpg, .jpeg, .png, .bmp}; try { for(const auto entry : fs::directory_iterator(srcDir)) { // 检查文件扩展名 std::string ext entry.path().extension().string(); std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower); if(std::find(validExts.begin(), validExts.end(), ext) validExts.end()) continue; // 读取图像 cv::Mat img cv::imread(entry.path().string(), cv::IMREAD_COLOR); if(img.empty()) { std::cerr 警告: 无法读取图像 entry.path() std::endl; continue; } // 统一缩放尺寸 cv::Mat resizedImg; cv::resize(img, resizedImg, cv::Size(64, 128), 0, 0, cv::INTER_AREA); // 保存处理后的图像 std::string outputPath dstDir / entry.path().filename().string(); if(!cv::imwrite(outputPath, resizedImg)) { std::cerr 警告: 无法保存图像 outputPath std::endl; } } std::cout 正样本预处理完成已保存至: dstDir std::endl; } catch(const fs::filesystem_error e) { std::cerr 文件系统错误: e.what() std::endl; } }改进点增加了错误处理和异常捕获支持更多图像格式bmp等使用INTER_AREA插值方法更适合缩小图像添加了详细的日志输出负样本预处理相对简单只需确保图像尺寸大于64×128即可因为检测时会使用滑动窗口void preprocessNegSamples(const std::string srcDir, const std::string dstDir) { // 与正样本类似的目录检查和创建逻辑... const int minWidth 64; const int minHeight 128; for(const auto entry : fs::directory_iterator(srcDir)) { cv::Mat img cv::imread(entry.path().string()); if(img.empty()) continue; // 检查图像尺寸是否足够大 if(img.cols minWidth || img.rows minHeight) { std::cout 跳过尺寸不足的图像: entry.path() std::endl; continue; } // 负样本不需要resize直接保存 std::string outputPath dstDir / entry.path().filename().string(); cv::imwrite(outputPath, img); } }4. HOG特征提取与模型训练4.1 HOG参数详解与设置HOGDescriptor的参数设置直接影响特征提取效果以下是经过调优的参数配置cv::HOGDescriptor hog( cv::Size(64, 128), // 检测窗口大小 cv::Size(16, 16), // 块大小 cv::Size(8, 8), // 块步长 cv::Size(8, 8), // 胞元大小 9, // 直方图bin数 1, // L2-Hys归一化收缩系数 -1, // gamma校正 0.2, // L2-Hys归一化阈值 true, // 使用高斯加权 cv::HOGDescriptor::DEFAULT_NLEVELS // 检测器级别数 );参数选择依据winSize(64,128)Dalal等人的论文验证这是最佳行人检测尺寸blockSize(16,16)和cellSize(8,8)平衡特征分辨力和计算效率blockStride(8,8)50%重叠提高特征密度nbins9将梯度方向分为9个区间经验值关键点这些参数必须在训练和检测时完全一致我曾在测试时误将blockStride改为(16,16)导致检测率骤降。4.2 SVM模型训练完整实现改进后的训练代码增加了数据增强和交叉验证#include opencv2/opencv.hpp #include opencv2/ml.hpp #include filesystem #include vector #include random namespace fs std::filesystem; // 数据增强随机水平翻转 void augmentData(std::vectorcv::Mat posImgs) { size_t originalSize posImgs.size(); for(size_t i 0; i originalSize; i) { cv::Mat flipped; cv::flip(posImgs[i], flipped, 1); // 水平翻转 posImgs.push_back(flipped); } } // 加载图像数据集 void loadDataset(const std::string posDir, const std::string negDir, std::vectorcv::Mat posImgs, std::vectorcv::Mat negImgs) { // 加载正样本... // 加载负样本... // 数据增强 augmentData(posImgs); } // 主训练函数 void trainHogSvm(const std::string posDir, const std::string negDir, const std::string modelPath) { // 1. 加载数据集 std::vectorcv::Mat posImgs, negImgs; loadDataset(posDir, negDir, posImgs, negImgs); // 2. 提取HOG特征 cv::Mat trainData, trainLabels; cv::HOGDescriptor hog getHogDescriptor(); // 获取配置好的HOG // 提取正样本特征 for(auto img : posImgs) { std::vectorfloat descriptors; hog.compute(img, descriptors); cv::Mat row(descriptors, true); // 转换为单行Mat trainData.push_back(row); trainLabels.push_back(1.0f); // 正样本标签为1 } // 提取负样本特征 for(auto img : negImgs) { std::vectorfloat descriptors; hog.compute(img, descriptors); cv::Mat row(descriptors, true); trainData.push_back(row); trainLabels.push_back(-1.0f); // 负样本标签为-1 } // 3. 转换为SVM需要的格式 trainData.convertTo(trainData, CV_32F); trainLabels.convertTo(trainLabels, CV_32S); // 4. 设置SVM参数 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-setTermCriteria(cv::TermCriteria( cv::TermCriteria::MAX_ITER cv::TermCriteria::EPS, 1000, 1e-6)); // 5. 训练并保存模型 std::cout 开始训练样本数量: trainData.rows std::endl; svm-train(trainData, cv::ml::ROW_SAMPLE, trainLabels); svm-save(modelPath); // 6. 评估模型 cv::Mat predictions; svm-predict(trainData, predictions); int correct 0; for(int i 0; i predictions.rows; i) { if(predictions.atfloat(i) trainLabels.atint(i)) correct; } float accuracy correct * 100.0f / predictions.rows; std::cout 训练集准确率: accuracy % std::endl; }改进点增加了数据增强水平翻转添加了训练集准确率评估优化了SVM参数C0.01增加了详细的训练日志5. 行人检测实现与优化5.1 单张图像检测实现完整版的单张图像检测函数应包含以下优化void detectPedestrians(const cv::Mat img, cv::Mat result, const std::string modelPath, float scale 1.05, int groupThreshold 2, double hitThreshold 0) { // 1. 加载模型 cv::Ptrcv::ml::SVM svm cv::ml::SVM::load(modelPath); if(svm.empty()) { std::cerr 错误: 无法加载模型 modelPath std::endl; return; } // 2. 配置HOG参数必须与训练时一致 cv::HOGDescriptor hog; hog.setSVMDetector(getSvmDetector(svm)); // 3. 多尺度检测 std::vectorcv::Rect found; hog.detectMultiScale(img, found, hitThreshold, cv::Size(8,8), cv::Size(0,0), scale, groupThreshold); // 4. 非极大值抑制 std::vectorcv::Rect foundFiltered; for(size_t i 0; i found.size(); i) { cv::Rect r found[i]; size_t j; // 过滤被包含的矩形 for(j 0; j found.size(); j) { if(j ! i (r found[j]) r) break; } if(j found.size()) foundFiltered.push_back(r); } // 5. 绘制结果 result img.clone(); for(cv::Rect r : foundFiltered) { // 调整矩形大小检测窗口是64x128实际行人可能更小 r.x cvRound(r.width * 0.1); r.width cvRound(r.width * 0.8); r.y cvRound(r.height * 0.07); r.height cvRound(r.height * 0.8); cv::rectangle(result, r, cv::Scalar(0,255,0), 2); } }关键优化点添加了非极大值抑制消除重叠框调整检测框大小更贴合实际行人可调节的scale和groupThreshold参数详细的错误检查5.2 视频流实时检测优化视频检测需要平衡精度和速度以下是优化后的实现void detectVideo(const std::string videoPath, const std::string modelPath) { // 初始化视频捕获 cv::VideoCapture cap(videoPath); if(!cap.isOpened()) { std::cerr 无法打开视频: videoPath std::endl; return; } // 加载模型和HOG cv::Ptrcv::ml::SVM svm cv::ml::SVM::load(modelPath); cv::HOGDescriptor hog; hog.setSVMDetector(getSvmDetector(svm)); // 性能统计变量 int frameCount 0; double totalTime 0; std::vectordouble frameTimes; cv::Mat frame; while(cap.read(frame)) { if(frame.empty()) continue; double t (double)cv::getTickCount(); // 缩小帧提高处理速度 cv::Mat smallFrame; float scaleFactor 0.5f; cv::resize(frame, smallFrame, cv::Size(), scaleFactor, scaleFactor); // 检测行人 std::vectorcv::Rect found; hog.detectMultiScale(smallFrame, found, 0, cv::Size(8,8), cv::Size(0,0), 1.05, 2); // 缩放检测结果回原尺寸 for(auto r : found) { r.x cvRound(r.x / scaleFactor); r.y cvRound(r.y / scaleFactor); r.width cvRound(r.width / scaleFactor); r.height cvRound(r.height / scaleFactor); cv::rectangle(frame, r, cv::Scalar(0,255,0), 2); } // 计算处理时间 t ((double)cv::getTickCount() - t) / cv::getTickFrequency(); frameTimes.push_back(t); totalTime t; frameCount; // 显示FPS std::ostringstream oss; oss FPS: std::fixed std::setprecision(1) (1.0/t); cv::putText(frame, oss.str(), cv::Point(10,30), cv::FONT_HERSHEY_SIMPLEX, 0.8, cv::Scalar(0,0,255), 2); cv::imshow(行人检测, frame); if(cv::waitKey(1) 27) break; // ESC退出 } // 输出性能报告 std::sort(frameTimes.begin(), frameTimes.end()); double medianTime frameTimes[frameTimes.size()/2]; std::cout 处理完成统计信息:\n 总帧数: frameCount \n 总时间: totalTime s\n 平均FPS: frameCount/totalTime \n 中值处理时间: medianTime s/帧\n; }优化策略帧缩放处理提高速度详细的性能统计和FPS显示中值时间计算更准确反映性能结果缩放回原尺寸保持精度6. Qt可视化界面开发6.1 界面设计与功能规划Qt界面应包含以下核心功能区域文件选择区图片/视频选择按钮参数设置区HOG和SVM参数调节结果显示区原图和检测结果对比显示控制区开始/停止检测按钮界面布局建议------------------------------------------- | 菜单栏 | ------------------------------------------ | 文件选择区 | 参数设置区 | | [选择图片] | 缩放因子: [1.05] | | [选择视频] | 分组阈值: [2] | | | 置信阈值: [0] | ------------------------------------------ | 原图显示区 | 结果展示区 | | | | | | | | | | ------------------------------------------ | 控制区 | | [开始检测] [停止] [保存结果] | -------------------------------------------6.2 核心功能代码实现主窗口类定义示例class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent nullptr); ~MainWindow(); private slots: void onSelectImage(); void onSelectVideo(); void onStartDetection(); void onStopDetection(); void onSaveResult(); private: // UI组件 QLabel *originalLabel; QLabel *resultLabel; QDoubleSpinBox *scaleSpinBox; QSpinBox *groupThresholdSpinBox; QDoubleSpinBox *hitThresholdSpinBox; // 处理线程 DetectionThread *detectionThread; // 工具函数 void updateResultDisplay(const cv::Mat result); void enableControls(bool enable); };检测线程类防止界面卡顿class DetectionThread : public QThread { Q_OBJECT public: explicit DetectionThread(QObject *parent nullptr); void setInput(const cv::Mat img); void setParameters(float scale, int groupThresh, double hitThresh); signals: void detectionFinished(cv::Mat result); protected: void run() override; private: cv::Mat inputImage; float scaleFactor; int groupThreshold; double hitThreshold; std::string modelPath; };关键实现细节使用QThread避免界面卡顿OpenCV Mat与QImage的转换QImage MainWindow::matToQImage(const cv::Mat mat) { if(mat.type() CV_8UC3) { return QImage(mat.data, mat.cols, mat.rows, mat.step, QImage::Format_RGB888).rgbSwapped(); } else if(mat.type() CV_8UC1) { return QImage(mat.data, mat.cols, mat.rows, mat.step, QImage::Format_Grayscale8); } return QImage(); }信号槽连接connect(detectionThread, DetectionThread::detectionFinished, this, MainWindow::updateResultDisplay);7. 项目部署与常见问题7.1 应用程序打包发布将项目部署到其他机器需要包含以下文件可执行文件.exeQt运行时库Qt5Core.dll, Qt5Gui.dll, Qt5Widgets.dll等OpenCV DLLopencv_world3410.dll等模型文件hog_svm_model.xml平台插件文件夹platforms/qwindows.dll使用windeployqt工具自动收集依赖windeployqt --release your_app.exe7.2 常见问题解决方案问题1运行时提示缺少MSVCP140.dll等文件解决方案安装Visual C Redistributable for Visual Studio 2015问题2检测结果框过多或过少调整detectMultiScale参数增大hitThreshold减少误检减小scaleFactor提高检测率但降低速度增大groupThreshold减少重叠框问题3Qt界面显示异常确保plugins/platforms目录包含qwindows.dll设置正确的环境变量QT_QPA_PLATFORM_PLUGIN_PATH问题4模型加载失败检查模型文件路径是否正确确认OpenCV版本一致训练和测试使用相同版本问题5视频检测卡顿降低处理帧率如每3帧处理1帧缩小处理图像尺寸使用更简单的HOG参数增大blockStride8. 项目扩展与优化方向8.1 性能优化建议多线程处理// 使用OpenCV的并行框架 cv::setNumThreads(4);ROI区域检测只对图像下部区域行人可能出现的位置进行检测背景减除cv::Ptrcv::BackgroundSubtractor pBackSub cv::createBackgroundSubtractorMOG2(); cv::Mat fgMask; pBackSub-apply(frame, fgMask);检测缓存对连续帧中相同位置的检测结果进行缓存8.2 算法改进方向结合深度学习的改进HOG使用CNN提取特征替代HOG融合HOG和CNN特征多特征融合// 例如结合LBP特征 cv::Mat lbpFeatures; cv::Ptrcv::face::LBPHFaceRecognizer lbp cv::face::LBPHFaceRecognizer::create(); lbp-compute(img, lbpFeatures);使用DPMDeformable Part Model对行人各部位分别建模提高对遮挡情况的鲁棒性集成学习训练多个SVM分类器通过投票机制综合结果9. 实际应用中的经验分享在真实场景部署这个行人检测系统时我总结了以下几点经验光照适应性问题添加gamma校正预处理void gammaCorrection(const cv::Mat src, cv::Mat dst, float gamma) { cv::Mat lookupTable(1, 256, CV_8U); uchar* p lookupTable.ptr(); for(int i 0; i 256; i) p[i] cv::saturate_castuchar(pow(i/255.0, gamma)*255.0); cv::LUT(src, lookupTable, dst); }多尺度检测的权衡在人群密集场景使用较小的scaleFactor(1.01-1.03)在开阔场景使用较大的scaleFactor(1.05-1.1)夜间模式处理切换到红外图像如有或者使用图像增强算法void enhanceLowLight(cv::Mat img) { cv::Mat lab; cv::cvtColor(img, lab, cv::COLOR_BGR2Lab); std::vectorcv::Mat channels; cv::split(lab, channels); cv::equalizeHist(channels[0], channels[0]); cv::merge(channels, lab); cv::cvtColor(lab, img, cv::COLOR_Lab2BGR); }雨天/雾天处理void dehaze(cv::Mat img) { cv::Mat darkChannel, refinedTransmission; // 暗通道先验去雾算法实现... }这个项目从开始到最终完成大约花费了3个月时间其中大部分时间都用在数据集准备和参数调优上。最大的收获是认识到在实际工程中算法只占一部分更多的挑战来自于数据质量、环境适配和性能优化。