ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

OpenCV+HOG+SVM人体识别完整工程:海康摄像头取流、YV12转RGB与检测实践

OpenCV+HOG+SVM人体识别完整工程:海康摄像头取流、YV12转RGB与检测实践 简介面向毕业设计、课程设计与计算机视觉入门人群的完整工程包基于海康威视网络摄像头实时采集画面结合OpenCV与HOGSVM算法实现人体识别与检测。程序采用C编写集成Qt图形界面并划分主窗口、摄像头采集、YV12图像格式转换、人体检测处理与模型训练等模块代码结构清晰可直接编译运行并用于二次开发。压缩包共229个文件大小36.88MB涵盖dll动态库、exe可执行程序、cpp/h源文件、lib库文件、pdb调试符号及vcxproj工程配置等便于快速配置环境、对照源码学习与调试运行。目前已有229人浏览学习适合希望在真实网络摄像头场景下快速搭建人体识别原型、完成课程项目或毕业设计的开发者。1. 海康 OpenCV 人体识别一套能直接跑起来的完整工程而不是教程代码这套资源的核心不是教你如何用 OpenCV 读取一个网络摄像头而是一套能直接在毕业设计或课程设计里跑起来的完整工程。拿到压缩包先别急着编译按文件名把链路理顺mainwindow.cpp 是界面入口camera.cpp 和 Read_Camera02.cpp 负责取流YV12_RGB.cpp 处理海康威视 SDK 回传的原始视频帧processpeople.cpp 和 HOGSVM.cpp 做人体识别train_main.cpp 是独立训练脚本。也就是说它覆盖了从海康威视网络摄像头取流、色彩空间转换、OpenCV 行人检测到 Qt 界面显示的整条链路。适合两类人被摄像头黑屏花屏折腾过的课设党以及想弄懂 HOGSVM 工程化边界、而不是只会在 Jupyter 里跑段样例的入门从业者。2. 取流链路选型RTSP 直连 vs 海康 SDK 裸流稳定性差距不小2.1 RTSP 拉流先跑通再谈稳定性很多教程会直接让你用 OpenCV 的 VideoCapture 拉 RTSP路径通常是rtsp://用户名:密码IP:554/Streaming/Channels/101。这套写法在实验室网络环境下确实能出画面代码量只有三行cv::VideoCapture cap; cap.open(rtsp://admin:123456192.168.1.64:554/Streaming/Channels/101); if (!cap.isOpened()) { qDebug() RTSP open failed; return; } cv::Mat frame; cap frame;这里的101是海康 ISAPI 风格的通道编号第一位 1 表示通道 1后两位 01 表示主码流02 才是子码流。如果你需要低分辨率高帧率来做实时检测优先用102。老版本固件可能不认这个格式要退回rtsp://IP:554/ch1/main/av_stream这种旧路径这是海康威视网络摄像头设置 RTSP 地址时最常见的分叉点。但 VideoCapture 直连有两个现实问题一是断流后cap frame会一直返回空帧程序里必须自己处理重连逻辑二是默认缓冲区设置会导致画面延迟看着像慢动作。我一般会加一个自动重连循环并主动把缓冲区压到最小while (true) { if (!cap.isOpened()) { cap.open(rtspUrl); // 重新初始化 std::this_thread::sleep_for(std::chrono::seconds(2)); continue; } cap.set(cv::CAP_PROP_BUFFERSIZE, 1); cv::Mat frame; if (!cap.read(frame)) { cap.release(); continue; } // 处理 frame }这段代码的思路很简单读不到帧就释放连接并重开避免进程僵在那里。CAP_PROP_BUFFERSIZE设成 1 是为了让 OpenCV 不要积压历史帧否则你看到的永远是几秒前的画面。如果你只是做验证RTSP 直连够用但要做课设演示建议直接走 SDK 裸流也就是这套资源里 Read_Camera02.cpp 实际做的那件事。2.2 海康 SDK 取流与 YV12 转 RGB 的实现海康 SDK 走的是另一条路登录设备后通过NET_DVR_RealPlay拿实时流回调函数里吐出的是 YV12 格式的裸数据不是封装好的视频帧。YV12 是 YUV 4:2:0 平面格式排列顺序是 Y 平面、V 平面、U 平面总大小是width * height * 3 / 2。OpenCV 里默认的 Mat 是 BGR 三通道排列所以中间必须经过一次 YV12 到 RGB 的转换这就是工程里 YV12_RGB.cpp 的来由。转换的核心代码逻辑是这样的void YV12ToRGB(const uint8_t* src, uint8_t* dst, int width, int height) { int ySize width * height; int uvWidth width / 2; int uvSize uvWidth * (height / 2); const uint8_t* yPlane src; const uint8_t* vPlane src ySize; // YV12: Y 后紧跟 V const uint8_t* uPlane src ySize uvSize; // 再后面是 U for (int i 0; i height; i) { const uint8_t* yRow yPlane i * width; const uint8_t* vRow vPlane (i / 2) * uvWidth; const uint8_t* uRow uPlane (i / 2) * uvWidth; uint8_t* outRow dst i * width * 3; for (int j 0; j width; j) { int Y yRow[j]; int U uRow[j / 2] - 128; int V vRow[j / 2] - 128; outRow[j * 3 0] cv::saturate_castuint8_t(Y 1.772 * U); outRow[j * 3 1] cv::saturate_castuint8_t(Y - 0.344 * U - 0.714 * V); outRow[j * 3 2] cv::saturate_castuint8_t(Y 1.402 * V); } } }这段代码每一行都是有讲究的ySize、uvSize用来定位三个平面的起始地址j / 2是因为 UV 平面横向只有 Y 平面一半宽。saturate_cast是 OpenCV 提供的截断函数把超出 0-255 的数值强制收敛到合法范围手写if判断也行但可读性和效率都不如它。如果你用的是较新版本的 OpenCV也可以不手写直接把回调数据包成一个 Mat交给cvtColor处理效果等价且速度更快cv::Mat yuvImg(height * 3 / 2, width, CV_8UC1, srcData); cv::Mat rgbImg; cv::cvtColor(yuvImg, rgbImg, cv::COLOR_YUV2RGB_YV12);需要注意cvtColor要求宽高必须是偶数否则 UV 平面尺寸对不上会直接抛异常。海康主码流一般是 1920x1080 或 1280x720没问题但如果有人把分辨率改成奇数第一件事就是花屏。2.3 Qt 刷新链路与线程模型界面卡死的真正来源资源里是 Qt 工程那么决定能不能稳定跑起来的关键不在 OpenCV而在线程模型。采集回调、YV12 转换、HOG 检测都是耗时的你要是把这些直接塞进 UI 线程窗口会瞬间无响应。这套工程的正确做法是采集线程拿到 RGB 帧转成 QImage通过 signal/slot 投递给主线程刷新检测结果只以数据形式传递绝不在主线程外操作 QWidget。// 采集线程 void CaptureWorker::run() { while (m_running) { cv::Mat rgbFrame; if (takeFrame(rgbFrame)) { QImage img(rgbFrame.data, rgbFrame.cols, rgbFrame.rows, rgbFrame.step, QImage::Format_RGB888); emit frameReady(img.copy()); // 拷贝避免 Mat 生命周期问题 } } } // 主线程 connect(worker, CaptureWorker::frameReady, this, [this](const QImage img) { ui-labelCamera-setPixmap(QPixmap::fromImage(img)); });frameReady用QueuedConnection跨线程投递槽函数只负责刷新界面不做任何计算。img.copy()看起来多余实际是必须的rgbFrame是局部变量函数结束后 Mat 内存释放而QImage默认不持有像素数据不拷贝的话界面上会出现随机花块。这个坑我帮人调过很多次十有八九是忘了拷贝。如果你只想跑通功能不打算深挖 Qt 线程也建议至少把采集和检测拆到两个线程里否则海康摄像头的 25 帧每秒回调会把主循环卡死。3. HOGSVM 检测代码落地从默认行人检测器到参数可调3.1 HOG 为什么够用特征维度与模型行为HOGHistogram of Oriented Gradients的核心思想是把图像分成小格子统计每个格子里的梯度方向分布用这些统计值刻画目标的外形轮廓。OpenCV 默认的行人检测器配置是检测窗口 64x128、cell 8x8、block 16x16、block stride 8x8、梯度方向 9 个 bin。算下来一个窗口的特征维度是 105 个 block 乘以 36 维等于 3780 维特征。为什么这套 2005 年的老方法到现在做课设依然够用因为它本质上是轻量线性分类器CPU 上实时跑完全没问题不需要 GPU也不需要深度学习框架。对毕设场景来说可解释性强反而是加分项答辩时你能说清楚特征怎么提取、SVM 的决策边界是什么这是拿一个黑匣子神经网络比不了的。但 HOG 的边界也很清楚它靠轮廓和梯度区分人体所以非常吃图像质量。夜间噪点大、逆光、目标太小都会让特征退化这就是很多人反映海康威视监控摄像头夜晚不灵敏的根本原因——不是 OpenCV 的问题是 HOG 特征本身的局限。3.2 核心检测代码与参数语义工程里 HOGSVM.cpp 的核心是detectMultiScale这行代码决定了检测效果的上限cv::HOGDescriptor hog; hog.setSVMDetector(cv::HOGDescriptor::getDefaultPeopleDetector()); std::vectorcv::Rect found; std::vectordouble weights; hog.detectMultiScale(img, found, weights, 0.0, // hitThreshold cv::Size(8, 8), // winStride cv::Size(8, 8), // padding 1.05, // scale 2.0, // finalThreshold true); // useMeanshiftGrouping参数对照表参数默认值作用调整方向hitThreshold0.0SVM 分类阈值越大越严格漏检多就调小误检多就调大winStride(8,8)检测窗口滑动步长越小越慢越准卡顿就调大到 (16,16)scale1.05图像金字塔缩放系数越小检测越细太慢就调大到 1.1finalThreshold2.0窗口合并阈值框太碎就调大一个常见误用是忽略 weights 输出。getDefaultPeopleDetector训练出来的模型detectMultiScale返回的 weights 才是每个框的置信度分数不用它过滤的话背景区域的低分框也会全被画出来。正确做法是在后面加一个筛选for (size_t i 0; i found.size(); i) { if (weights[i] -0.5) continue; // 低分框直接丢弃 cv::rectangle(frame, found[i], cv::Scalar(0, 255, 0), 2); }这里hitThreshold设 0.0、weights再卡一道 -0.5是因为 HOGDescriptor 默认检测器的输出分数不是概率负数仍可能是有效行人。具体阈值要看你的场景白天走廊和夜间仓库的最优值能差出一倍。3.3 多尺度重叠框合并NMS 该怎么做用useMeanshiftGroupingtrue时 OpenCV 内置了合并逻辑但实际效果在行人密集场景下依然会输出大量重叠框另一个办法是自己写 NMS。多尺度检测的原理决定了同一个行人会在金字塔的不同层被多次检测到这些框尺寸不同但位置重叠必须按置信度排序后逐个合并void nms(std::vectorcv::Rect boxes, std::vectordouble scores, double iouThreshold) { std::vectorint idx(scores.size()); std::iota(idx.begin(), idx.end(), 0); std::sort(idx.begin(), idx.end(), [](int a, int b) { return scores[a] scores[b]; }); std::vectorcv::Rect keep; for (int i : idx) { bool merged false; for (auto r : keep) { double inter (r boxes[i]).area(); double uni r.area() boxes[i].area() - inter; if (uni 0 inter / uni iouThreshold) { merged true; break; } } if (!merged) keep.push_back(boxes[i]); } boxes keep; }NMS 的注意点有两个。第一IoU阈值一般取 0.4-0.5太大会把同一个人的两个框都保留太小会把相邻的人合并掉。第二boxes[i]在keep循环里不能直接erase因为keep要参与后续所有比较正确做法是先收集再替换上面代码就是这个思路。很多教程只讲groupRectangles但那个函数对低分框的容忍度很差自己写 NMS 反而更好控制。4. 训练自己的模型train_main.cpp 里的样本、SVM 与两个关键坑4.1 正负样本与硬负样本挖掘默认行人检测器是在 INRIA 数据集上训的应对监控场景的视频画面绰绰有余但如果你要检测的是某个特定环境里的人员形态比如工地安全帽下方的半身人像就需要自己训一个模型。工程里的 train_main.cpp 就是为了这个目的准备的。训练的第一步永远是样本不是代码。正样本是包含行人的矩形图统一缩放到 64x128负样本是任意不含行人的背景图同样缩放。数量上建议至少 1000 个正样本、3000 个负样本比例 1:3 左右负样本太少 SVM 会学出大量误检。这里有个课程设计里几乎必讲的技巧叫硬负样本挖掘Hard Negative Mining。第一轮训练完用模型在你准备的负样本图集上跑一遍检测把误检出来的框截下来作为新的负样本加入训练集再训一轮误检率能明显下降。逻辑很简单SVM 只见过你给的负样本凡是你没给的背景它都可能当成正样本那就主动把最难缠的背景喂给它。# 目录结构约定 train/ pos/ # 正样本每个文件一个行人图 neg/ # 负样本背景图 hard_neg/ # 硬负样本挖掘补进来的新样本4.2 特征提取与 SVM 训练参数train_main.cpp 的流程是先遍历所有样本用 HOGDescriptor 把每张图变成 3780 维特征向量组装成特征矩阵然后送给 SVM 训练。核心代码骨架如下cv::HOGDescriptor hog(winSize, blockSize, blockStride, cellSize, nbins); cv::Mat trainData(totalSamples, featureDim, CV_32FC1); cv::Mat labels(totalSamples, 1, CV_32SC1); int row 0; for (auto path : positiveImages) { cv::Mat img cv::imread(path, cv::IMREAD_GRAYSCALE); cv::Mat resized; cv::resize(img, resized, cv::Size(64, 128)); std::vectorfloat feat; hog.compute(resized, feat, cv::Size(8, 8), cv::Size(0, 0)); for (int j 0; j featureDim; j) trainData.atfloat(row, j) feat[j]; labels.atint(row, 0) 1; row; } // 负样本同上labels 置 -1 或 0 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(trainData, cv::ml::ROW_SAMPLE, labels); svm-save(svm_model.xml);hog.compute的第三个参数是窗口步长第四是 padding这里的设置要和检测端完全一致。C是 SVM 的正则化系数0.01-0.1 是 HOGSVM 场景的常用区间C越大越容易过拟合训练集监控画面这种背景复杂的场景建议从小往大试。一个容易翻车的细节不要对feat做额外的手工归一化。hog.compute输出的特征本身已经经过 block 级 L2 归一化如果你再套一层normalize训练时特征分布变了检测端HOGDescriptor内部却不会做同样处理检测效果会直接崩掉。4.3 把 SVM 系数转成 HOG 检测器向量这是整套资源里最容易卡住的一步也是网上大多数教程没讲清楚的地方。svm-save存出来的文件是 OpenCV ML 格式的模型它不能直接喂给HOGDescriptor::setSVMDetector。你从svm_model.xml里读到的支持向量和偏置需要手动转换成一个一维数组格式是支持向量按权重累加数组末尾放偏置项。cv::Ptrcv::ml::SVM svm cv::ml::StatModel::loadcv::ml::SVM(svm_model.xml); std::vectorfloat alpha; double rho svm-getDecisionFunction(0, alpha); cv::Mat supportVectors svm-getSupportVectors(); int dim supportVectors.cols; std::vectorfloat detector(dim 1, 0.f); for (int i 0; i supportVectors.rows; i) { for (int j 0; j dim; j) { detector[j] alpha[i] * supportVectors.atfloat(i, j); } } detector[dim] static_castfloat(-rho); cv::HOGDescriptor hog(winSize, blockSize, blockStride, cellSize, nbins); hog.setSVMDetector(detector);这里有两件事必须说清楚。第一getDecisionFunction(0, alpha)返回的alpha是每个支持向量的组合系数直接用支持向量做加权求和得到的是 SVM 决策超平面的法向量。第二末尾偏置的符号在不同 OpenCV 版本里有可能相反如果你加载模型后检测结果全是框或一个框都没有把detector[dim]的符号取反再试一次就行这不是你的代码写错了是版本差异的玄学。这个转换做完你的自训练模型就和getDefaultPeopleDetector一样可以直接进detectMultiScale跑了。5. 编译、运行与排查安装 OpenCV、配置 Qt 之后还有四个现实问题5.1 构建顺序与 CMake 配置拿到源码先别急着点编译。这套工程依赖的组件有 Qt负责界面、海康 SDK负责取流、OpenCV负责图像处理三者的构建顺序建议是先装 OpenCV再配 Qt最后接海康 SDK因为后两者都要引用 OpenCV 的头文件和库路径。Windows 下最省事的方式是下载 OpenCV 预编译包解压后把opencv_world450.dll这类库所在目录加进环境变量 PATH然后在 Qt 工程里用 CMake 指过去set(OpenCV_DIR D:/opencv/build) find_package(OpenCV REQUIRED COMPONENTS core imgproc objdetect ml videoio highgui) add_executable(people_detector mainwindow.cpp Read_Camera02.cpp camera.cpp YV12_RGB.cpp processpeople.cpp HOGSVM.cpp ) target_link_libraries(people_detector PRIVATE ${OpenCV_LIBS} )注意HOGSVM.cpp这种带加号的文件名在 CMake 里没问题但如果你传到 Linux 服务器上编译文件系统对加号也兼容真正要小心的是源码文件编码。海康 SDK 的头文件经常是 GBK 编码Qt 默认 UTF-8混编时中文注释会变成乱码甚至报错我一般统一转成 UTF-8 再编译。5.2 运行期四个高频问题的血泪经验问题一RTSP 打不开VideoCapture 一直返回 false。现象是cap.isOpened()永远是假。原因有几种RTSP 地址里的密码含特殊字符没有做 URL 编码比如会被解析成账号分隔符通道号不对主码流子码流搞混设备固件太老只支持旧版 RTSP 路径。解决方法是先用 VLC 验证地址能不能直接打开Win10 浏览器加载不了海康插件是常见问题但 VLC 能作为独立验证工具判定地址本身是否可用。然后用rtsp://IP:554/Streaming/Channels/102和rtsp://IP:554/ch1/sub/av_stream两套格式都试一遍。问题二画面出来了但颜色完全错乱或整体发绿发紫。这是 YV12 和 I420 的 UV 平面顺序搞反了。海康部分型号在回调里输出的是 NV12 而不是 YV12两者看起来一样都是 4:2:0但 U 和 V 的排列顺序不同。解决方法是把COLOR_YUV2RGB_YV12换成COLOR_YUV2RGB_I420或COLOR_YUV2RGB_NV12再转一次如果红蓝互换就说明 UV 顺序反了对调 UV 平面指针即可。问题三检测跑起来后 Qt 界面直接白屏无响应。原因基本是采集和检测都在 UI 线程执行阻塞了事件循环。解决思路是照 2.3 节的线程模型改造worker 线程做取流、转换、检测主线程只接收 QImage 刷新。需要注意queuedConnection传大图像时会拷贝多次如果帧率掉得厉害可以把 QImage 改成std::shared_ptrQImage传递。问题四一个人身上叠了十几个框或者一个框时有时无。前者是没做 NMS 或 IoU 阈值太小后者是hitThreshold太高导致低分帧检测不稳定。解决方法是把weights拿出来做一次阈值过滤再按 3.3 节的方法做 NMS 合并。调整参数的顺序建议是先固定scale1.05改hitThreshold找检测率最后再动winStride平衡速度。5.3 性能测量先量化再调优很多同学调了半天参数问起来效果怎么样答案是“还行”“有点卡”这等于没调。工程里我建议用std::chrono把三个耗时点独立测出来单帧取流耗时、YV12 转 RGB 耗时、detectMultiScale 耗时。实现很简单auto t0 std::chrono::steady_clock::now(); // 取流 转换 auto t1 std::chrono::steady_clock::now(); // 检测 auto t2 std::chrono::steady_clock::now(); double captureMs std::chrono::durationdouble, std::milli(t1 - t0).count(); double detectMs std::chrono::durationdouble, std::milli(t2 - t1).count(); qDebug() capture: captureMs ms, detect: detectMs ms;一般课设场景的合理基线是640x360 输入分辨率下取流加转换小于 10msHOG 检测小于 50ms。如果 detect 超过 150ms优先把输入帧resize到 640x360而不是去调金字塔参数。HOG 检测窗口是 64x128输入 1080p 意味着要生成十几层金字塔计算量是指数增长的。6. 用验证脚本量化漏检与误检调整阈值不必靠玄学6.1 一个独立的 Python 验证脚本HOG 参数调优最怕的就是“凭感觉”。我习惯的做法是录一段 10 秒的室内监控视频人工标出其中 20 帧里的行人位置然后用一个独立脚本批量跑不同参数组合把漏检率、误检率、平均 IoU 和 FPS 全部量化出来。验证脚本用 Python 写不污染 C 工程只读取录好的视频文件和标注文件。import cv2 import json import time def compute_iou(a, b): inter max(0, min(a[2], b[2]) - max(a[0], b[0])) * \ max(0, min(a[3], b[3]) - max(a[1], b[1])) union (a[2]-a[0]) * (a[3]-a[1]) (b[2]-b[0]) * (b[3]-b[1]) - inter return inter / union if union 0 else 0 with open(labels.json) as f: labels json.load(f) # {frame_id: [[x1,y1,x2,y2], ...]} for hit_threshold in [0.5, 0.0, -0.5, -1.0]: hog cv2.HOGDescriptor() hog.setSVMDetector(cv2.HOGDescriptor_getDefaultPeopleDetector()) total_hit, total_miss, total_fp, frames, total_time 0, 0, 0, 0, 0.0 cap cv2.VideoCapture(test_clip.mp4) while True: ret, frame cap.read() if not ret: break t0 time.time() rects, weights hog.detectMultiScale( frame, hitThresholdhit_threshold, winStride(8, 8), padding(8, 8), scale1.05) total_time time.time() - t0 frames 1 gt labels.get(str(frames), []) hits [False] * len(gt) for r in rects: if weights[r] -0.5: continue for i, g in enumerate(gt): if compute_iou([r[0], r[1], r[0]r[2], r[1]r[3]], g) 0.5: hits[i] True # 统计漏检、误检 cap.release()脚本里最关键的是IoU 0.5这个验收标准它决定了“检测到了”的判定是否合理。很多课设答辩被追问“你这个框对准了吗”就是因为没有这一道量化全凭肉眼加嘴硬。6.2 用指标反推参数并固化你的调参顺序拿上面的脚本跑一组参数会得到类似下面的记录表hitThreshold漏检率误检数/帧平均 IoU检测耗时0.531%0.20.6142ms0.018%0.60.6344ms-0.511%1.40.6443ms-1.08%3.10.5845ms我的经验是先定一个可接受的漏检率上限比如课设要求“视频里出现的人至少有 80% 被框出来”然后选择满足这个条件的最大hitThreshold因为阈值再低误检数量涨得远比赛率涨得快。之后如果 FPS 不够再调scale从 1.05 升到 1.1重新跑脚本看漏检率变化。这套流程的好处是每个参数都有数据背书答辩时能直接展示“为什么选这个值”。从那以后我每次调 HOG 检测参数都强制自己先录一段视频、写一个验证脚本、把指标量化出来再动手改参数。先定验收指标再谈调参而不是对着画面反复试这个习惯帮我少走了很多弯路。希望帮到你也建议你把这套验证流程写进课程设计报告里作为“模型评估”章节的实践依据。本文还有配套的精品资源点击获取
返回列表