C++与OpenCV实现实时人物轮廓提取:传统图像处理实战指南 1. 项目概述为什么我们需要在C中提取人物轮廓在计算机视觉的日常开发里轮廓提取是个基础但至关重要的环节。无论是做安防监控里的人体闯入检测、互动媒体里的体感游戏还是短视频应用里的背景虚化与替换第一步往往都是把画面里的“人”给精准地“抠”出来。这个“抠”的过程就是人物轮廓提取。你可能会问现在深度学习模型如Mask R-CNN、YOLO-ActSeg那么强大直接用它们不香吗确实在追求极致精度的场景下深度学习是首选。但很多时候我们面对的是资源受限的嵌入式设备比如树莓派、Jetson Nano、对实时性要求极高的应用比如60FPS的视频处理或者项目初期需要一个快速验证的原型。在这些情况下基于传统图像处理库OpenCV的轮廓提取方案以其轻量、高效、不依赖庞大模型和GPU算力的特点依然有着不可替代的价值。用C配合OpenCV来做这件事更是将性能优势发挥到了极致。C的零成本抽象和直接内存操作能力让图像处理这种数据密集型任务的每一分算力都用在刀刃上。OpenCV本身也针对C接口做了大量优化其矩阵运算和算法实现效率极高。这意味着你可以在普通的笔记本电脑上实时处理高清视频流中的人物轮廓而不用担心卡顿。这次我们就来彻底拆解这个流程。我不会只给你一堆代码而是会带你走一遍我从项目实践中总结出来的完整路径从环境搭建、图像预处理到核心算法选型与调参再到轮廓的后处理与优化最后还会分享几个我踩过坑才学到的实战技巧。目标很明确让你看完就能动手做出一个稳定、可用的人物轮廓提取模块。2. 环境准备与项目搭建工欲善其事必先利其器。一个清爽、正确的开发环境是成功的第一步也能避免后续无数莫名其妙的编译错误和运行时崩溃。2.1 OpenCV与C开发环境配置首先你需要一个C编译器和一个集成开发环境IDE。在Windows上Visual Studio是首选社区版免费且功能强大。在Linux或macOS上GCC/Clang配合CMake是标准组合IDE可以选择VSCode或CLion。接下来是OpenCV的安装。我强烈建议从源码编译而不是直接下载预编译的二进制包。原因有三第一你可以根据需求定制编译选项比如只编译核心模块和imgproc图像处理模块减少库文件大小第二可以确保编译器版本、C标准库与你的项目完全匹配避免兼容性问题第三可以启用一些优化如IPPICVIntel集成性能基元来加速。这里以在Ubuntu系统下使用CMake编译OpenCV 4.x为例给出一个经过验证的配置脚本# 1. 安装依赖 sudo apt-get update sudo apt-get install build-essential cmake git pkg-config sudo apt-get install libjpeg-dev libtiff5-dev libpng-dev sudo apt-get install libavcodec-dev libavformat-dev libswscale-dev libv4l-dev sudo apt-get install libxvidcore-dev libx264-dev sudo apt-get install libgtk-3-dev sudo apt-get install libatlas-base-dev gfortran sudo apt-get install python3-dev # 2. 克隆OpenCV和opencv_contrib包含额外模块可选但推荐 cd ~ git clone https://github.com/opencv/opencv.git git clone https://github.com/opencv/opencv_contrib.git # 3. 创建构建目录并进入 cd opencv mkdir build cd build # 4. 使用CMake配置 cmake -D CMAKE_BUILD_TYPERELEASE \ -D CMAKE_INSTALL_PREFIX/usr/local \ -D INSTALL_PYTHON_EXAMPLESOFF \ -D INSTALL_C_EXAMPLESOFF \ -D OPENCV_ENABLE_NONFREEON \ -D OPENCV_EXTRA_MODULES_PATH~/opencv_contrib/modules \ -D BUILD_EXAMPLESOFF \ -D BUILD_opencv_worldOFF \ # 不生成单个大库便于管理 -D WITH_CUDAOFF \ # 根据需求开启开启前需安装CUDA -D WITH_IPPON \ -D WITH_FFMPEGON \ .. # 5. 编译并安装 make -j$(nproc) # -j参数指定并行编译的线程数大幅加快速度 sudo make install sudo ldconfig # 更新动态链接库缓存注意-D OPENCV_ENABLE_NONFREEON这个选项会启用一些专利算法如SIFT、SURF如果你用于商业项目请务必了解相关专利许可。如果只是学习可以开启以获得更完整的功能体验。在Windows上使用Visual Studio过程类似下载源码用CMake-GUI工具生成VS的解决方案.sln文件然后用VS打开并编译ALL_BUILD项目最后编译INSTALL项目。2.2 创建你的第一个轮廓提取项目环境搞定后我们来创建一个最小化的CMake项目确保OpenCV链接正确。项目结构如下PersonContourDemo/ ├── CMakeLists.txt ├── include/ ├── src/ │ └── main.cpp └── data/ └── test_video.mp4CMakeLists.txt是项目的构建蓝图内容如下cmake_minimum_required(VERSION 3.10) project(PersonContourDemo) # 设置C标准 set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 寻找OpenCV包REQUIRED表示必须找到 find_package(OpenCV REQUIRED) # 包含头文件目录 include_directories(${OpenCV_INCLUDE_DIRS}) include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include) # 添加可执行文件并链接OpenCV库 add_executable(${PROJECT_NAME} src/main.cpp) target_link_libraries(${PROJECT_NAME} ${OpenCV_LIBS})在src/main.cpp里我们先写一个简单的程序来测试OpenCV是否工作正常#include opencv2/opencv.hpp #include iostream int main() { // 尝试读取一张图片 cv::Mat image cv::imread(../data/test_image.jpg); if (image.empty()) { std::cerr Could not open or find the image! std::endl; return -1; } std::cout Image loaded successfully. Size: image.size() std::endl; // 尝试打开摄像头 cv::VideoCapture cap(0); // 0代表默认摄像头 if (!cap.isOpened()) { std::cerr Cannot open the camera! std::endl; return -1; } std::cout Camera opened successfully. std::endl; cap.release(); return 0; }编译并运行这个程序如果能看到图片尺寸信息和摄像头打开成功的提示恭喜你环境配置成功如果遇到类似error while loading shared libraries: libopencv_core.so.4.x: cannot open shared object file的错误说明动态库路径没被系统找到。可以尝试将OpenCV的库路径如/usr/local/lib添加到LD_LIBRARY_PATH环境变量中或者运行一下sudo ldconfig。3. 核心原理从图像到轮廓的旅程在动手写代码之前我们必须理解图像是如何一步步变成我们想要的轮廓的。这个过程不是一蹴而就的而是一个精心设计的流水线。3.1 图像预处理为轮廓检测铺平道路原始图像通常包含大量噪声、复杂纹理和光照变化这些都会严重干扰轮廓检测。预处理的目标就是简化图像突出我们关心的前景人物与背景的边界。1. 色彩空间转换从BGR到灰度OpenCV默认读取的图像是BGR格式蓝-绿-红。对于轮廓检测颜色信息通常是冗余的甚至是有害的比如穿着花衣服的人颜色边界会误判为轮廓。第一步就是转换为灰度图将三维的颜色信息压缩为一维的亮度信息。cv::Mat gray; cv::cvtColor(srcImage, gray, cv::COLOR_BGR2GRAY);这里有一个小技巧如果背景是纯色比如绿幕而人物服装颜色与之对比强烈有时保留特定颜色通道如绿色通道反而比灰度图效果更好。你可以通过cv::split分离通道后单独处理。2. 降噪平滑滤波图像传感器噪声和细微纹理会产生大量伪轮廓。高斯模糊是最常用的平滑滤波器。cv::Mat blurred; cv::GaussianBlur(gray, blurred, cv::Size(5, 5), 1.5);cv::Size(5,5)是卷积核大小必须是正奇数。数值越大模糊效果越强但轮廓也会变粗。1.5是高斯分布的标准差如果设为0OpenCV会根据核大小自动计算。我的经验是对于人物轮廓核大小在(3,3)到(7,7)之间比较合适太大容易丢失细节如手指缝隙。3. 背景减除分离运动目标如果我们的场景是静态的比如固定的监控摄像头那么最有效的方法就是背景减除。OpenCV提供了几种背景减除器如cv::createBackgroundSubtractorMOG2。它的原理是建立背景模型然后将当前帧与背景模型对比差异大的区域就是前景运动目标。auto pBackSub cv::createBackgroundSubtractorMOG2(500, 16, true); cv::Mat fgMask; pBackSub-apply(blurred, fgMask);500是历史帧数用于构建背景模型。16是方差阈值决定一个像素与背景差异多大才被认为是前景。true表示检测阴影并将其标记为灰色值127。这个方法对光照缓慢变化和动态背景如摇曳的树叶有一定鲁棒性。3.2 边缘与二值化勾勒出边界经过预处理我们得到了一个相对“干净”的图像。接下来需要将其转化为非黑即白的二值图像其中白色代表前景可能是人物黑色代表背景。1. 边缘检测Canny算子Canny边缘检测是经典且效果出色的方法。它包含多个步骤高斯模糊去噪、计算梯度强度和方向、非极大值抑制细化边缘、双阈值检测和连接边缘。cv::Mat edges; cv::Canny(blurred, edges, lowThreshold, lowThreshold * ratio, kernelSize);lowThreshold: 低阈值。梯度值低于此肯定不是边缘。ratio: 高阈值与低阈值的比例通常为2:1或3:1。kernelSize: Sobel算子的孔径大小通常是3。调参心得lowThreshold的设置非常关键。设得太高会丢失弱边缘如头发丝、衣物褶皱设得太低会引入大量噪声边缘。一个实用的方法是先用一个经验值如50然后根据输出动态调整或者使用自适应阈值方法。2. 阈值化固定阈值与自适应阈值如果背景简单可以直接使用固定阈值。cv::Mat binary; cv::threshold(gray, binary, threshValue, 255, cv::THRESH_BINARY);但对于光照不均的场景比如室内一侧有窗户固定阈值会失效。这时应该用自适应阈值它为图像中的每个小区域计算独立的阈值。cv::Mat adaptiveBinary; cv::adaptiveThreshold(blurred, adaptiveBinary, 255, cv::ADAPTIVE_THRESH_GAUSSIAN_C, cv::THRESH_BINARY, blockSize, C);blockSize: 计算阈值时考虑的邻域大小必须是正奇数。越大光照变化适应能力越强但细节可能丢失。C: 从计算出的均值或加权均值中减去的常数用于微调。一个常见的坑blockSize不能小于3且通常需要根据图像分辨率调整。对于640x480的图像11或15是不错的起点。3.3 轮廓查找与层次结构得到了二值图像白色是前景OpenCV的findContours函数就能大显身手了。这个函数使用Suzuki85的算法来追踪白色区域的边界。std::vectorstd::vectorcv::Point contours; std::vectorcv::Vec4i hierarchy; cv::findContours(binaryImage, contours, hierarchy, cv::RETR_TREE, cv::CHAIN_APPROX_SIMPLE);contours: 输出的轮廓集合。每个轮廓由一系列点cv::Point组成。hierarchy: 轮廓的层次信息是一个cv::Vec4i的向量。每个元素[Next, Previous, First_Child, Parent]描述了轮廓之间的关系。cv::RETR_TREE: 检索模式。TREE会检索所有轮廓并重建完整的嵌套层次。如果只关心最外层轮廓可以用RETR_EXTERNAL。cv::CHAIN_APPROX_SIMPLE: 轮廓近似方法。SIMPLE会压缩水平、垂直和对角线方向的冗余点只保留端点极大减少轮廓点数。NONE则存储所有点。理解层次结构Hierarchy对于人物轮廓提取至关重要。想象一下一个人穿着带有洞眼的毛衣。人的身体是一个外部轮廓父轮廓毛衣的洞是内部轮廓子轮廓。RETR_TREE模式能帮你理清这种“轮廓中的轮廓”关系。在后续筛选时你可以利用层次信息过滤掉那些可能是物体内部空洞的小轮廓。4. 实战演练一步步提取并优化人物轮廓理论说得再多不如一行代码。现在我们把上面的步骤串联起来构建一个完整的、从摄像头读取视频并实时提取人物轮廓的程序。4.1 基础流程实现我们假设一个相对理想的场景人物与背景有一定对比度光照基本均匀。以下是核心代码框架#include opencv2/opencv.hpp #include iostream int main() { cv::VideoCapture cap(0); if (!cap.isOpened()) { std::cerr Error opening camera! std::endl; return -1; } cv::Mat frame, gray, blurred, binary, edges; std::vectorstd::vectorcv::Point contours; std::vectorcv::Vec4i hierarchy; // 创建一个窗口 cv::namedWindow(Contours, cv::WINDOW_AUTOSIZE); while (true) { cap frame; if (frame.empty()) break; // 1. 转换为灰度图 cv::cvtColor(frame, gray, cv::COLOR_BGR2GRAY); // 2. 高斯模糊降噪 cv::GaussianBlur(gray, blurred, cv::Size(5, 5), 1.5); // 3. Canny边缘检测 cv::Canny(blurred, edges, 50, 150, 3); // 4. 膨胀操作连接断开的边缘可选但常用 cv::Mat kernel cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3, 3)); cv::dilate(edges, edges, kernel); // 5. 查找轮廓 cv::findContours(edges, contours, hierarchy, cv::RETR_TREE, cv::CHAIN_APPROX_SIMPLE); // 6. 在原图上绘制轮廓 cv::Mat result frame.clone(); cv::drawContours(result, contours, -1, cv::Scalar(0, 255, 0), 2); // 绿色2像素宽 cv::imshow(Contours, result); // 按ESC退出 if (cv::waitKey(30) 27) break; } cap.release(); cv::destroyAllWindows(); return 0; }运行这个程序你应该能在摄像头画面中看到用绿色线条勾勒出的各种边缘。但很快你会发现问题它画出了所有轮廓包括背景里的桌椅、窗户边框而不仅仅是人。4.2 轮廓筛选找到“那个人”所以我们需要从一大堆轮廓中把属于人物的那个或那几个挑出来。筛选标准通常基于轮廓的几何特征。1. 面积筛选小面积的轮廓很可能是噪声。我们可以计算轮廓的面积cv::contourArea并设定一个最小阈值。double minContourArea 500.0; // 根据图像分辨率调整对于640x480500是个起点 std::vectorstd::vectorcv::Point filteredContours; for (size_t i 0; i contours.size(); i) { double area cv::contourArea(contours[i]); if (area minContourArea) { filteredContours.push_back(contours[i]); } }2. 宽高比和轮廓矩形人的轮廓通常近似于一个较高的矩形。我们可以用cv::boundingRect获取轮廓的外接矩形然后计算其宽高比。float minAspectRatio 0.2; // 最小宽高比瘦高 float maxAspectRatio 1.0; // 最大宽高比差不多是正方形 for (const auto contour : filteredContours) { cv::Rect rect cv::boundingRect(contour); float aspectRatio (float)rect.width / rect.height; if (aspectRatio minAspectRatio aspectRatio maxAspectRatio) { // 符合人体比例的候选轮廓 } }注意这个规则对站立的人有效但如果人坐着、蹲着或姿势奇特就会失效。在实际项目中可能需要结合姿态估计或更复杂的形状描述符。3. 轮廓凸性与缺陷人的轮廓大体上是“凸”的。我们可以用cv::isContourConvex判断或者计算凸包cv::convexHull。凸包是包含轮廓的最小凸多边形。通过比较原轮廓和凸包的面积或周长可以判断轮廓的“凹凸”程度。std::vectorint hull; cv::convexHull(contours[i], hull, false); // 不返回点返回索引 double contourArea cv::contourArea(contours[i]); std::vectorcv::Point hullPoints; for (int idx : hull) hullPoints.push_back(contours[i][idx]); double hullArea cv::contourArea(hullPoints); double solidity contourArea / hullArea; // 凸性系数越接近1越凸 if (solidity 0.8) { // 一个经验阈值 // 轮廓比较凸可能是人体 }4. 轮廓近似与多边形拟合原始轮廓点太多不利于分析和显示。我们可以用cv::approxPolyDP进行多边形近似用更少的点来描绘轮廓形状。这也能用来判断轮廓的复杂程度比如近似后点数很少可能是简单的矩形或圆形而不是复杂的人形。std::vectorcv::Point approx; double epsilon 0.02 * cv::arcLength(contours[i], true); // 精度参数为周长的2% cv::approxPolyDP(contours[i], approx, epsilon, true); if (approx.size() 5) { // 如果近似后点数还比较多形状可能比较复杂 // 可能是人形 }4.3 高级优化应对复杂场景基础方法在简单背景下还行一旦背景杂乱、光照变化效果就急剧下降。下面介绍几个进阶技巧。1. 背景减除器Background Subtractor的深度使用之前简单提过MOG2这里详细说说它的参数调优。创建后通常需要“学习”几十到几百帧来建立稳定的背景模型。auto pMOG2 cv::createBackgroundSubtractorMOG2(500, 25, true); pMOG2-setShadowValue(127); // 阴影标记为灰色 pMOG2-setShadowThreshold(0.5); // 阴影检测阈值默认0.5 // ... 在循环中 cv::Mat fgMask; pMOG2-apply(blurredFrame, fgMask); // 对fgMask进行形态学操作开运算去噪闭运算填充空洞后再找轮廓 cv::morphologyEx(fgMask, fgMask, cv::MORPH_OPEN, kernel); cv::morphologyEx(fgMask, fgMask, cv::MORPH_CLOSE, kernel);关键点apply方法返回的前景掩模fgMask是8位单通道图像前景为白色255背景为黑色0阴影为灰色127。在找轮廓前最好用阈值化把阴影也去掉cv::threshold(fgMask, fgMask, 200, 255, cv::THRESH_BINARY)或者在进行形态学操作时忽略阴影值。2. 结合运动历史图像Motion History Image, MHI对于判断一个区域是静止物体还是运动的人MHI很有用。它记录的是运动发生的时间越新的运动越亮。cv::Mat mhi, mask, orient; // ... 计算光流或使用差分得到运动掩模 cv::updateMotionHistory(motionMask, mhi, currentTime, duration); // 然后从MHI中分割出运动区域 cv::segmentMotion(mhi, mask, orient, currentTime, maxTimeDelta);MHI能帮你过滤掉那些虽然被背景减除器判定为前景但长时间不动的物体比如被人放下的包。3. 多尺度与图像金字塔如果人物距离摄像头远近不同轮廓大小差异会很大。可以在不同尺度的图像上分别进行轮廓检测。使用图像金字塔cv::pyrDown,cv::buildPyramid生成一系列分辨率逐渐降低的图像在每一层进行检测最后将结果融合。这有助于检测远处较小的人物。4. 轮廓分组与合并有时候一个人的轮廓可能因为衣物颜色、阴影或遮挡而被断裂成好几块。我们需要将这些属于同一物体的轮廓合并。一个简单的方法是计算所有轮廓的外接矩形如果两个矩形在水平或垂直方向上有大量重叠且中心距离较近则合并它们。std::vectorcv::Rect rects; // ... 获取所有候选轮廓的外接矩形 for (size_t i 0; i rects.size(); i) { for (size_t j i 1; j rects.size(); j) { cv::Rect intersection rects[i] rects[j]; // 矩形交集 cv::Rect union rects[i] | rects[j]; // 矩形并集 float overlapRatio (float)intersection.area() / union.area(); if (overlapRatio 0.3) { // 重叠度超过30% // 合并矩形或者将两个轮廓的点集合并后重新找轮廓 std::vectorcv::Point combinedPoints; combinedPoints.insert(combinedPoints.end(), contours[i].begin(), contours[i].end()); combinedPoints.insert(combinedPoints.end(), contours[j].begin(), contours[j].end()); cv::Rect newRect cv::boundingRect(combinedPoints); // 更新rects和contours列表... } } }5. 性能调优与实战避坑指南在实时视频流中处理性能是关键。同时一些细节处理不好会导致程序在边缘情况下崩溃或行为异常。5.1 提升处理速度的技巧降低分辨率这是最立竿见影的方法。在调用cvtColor之前先用cv::resize将图像缩小。对于人脸检测可能只需要VGA640x480甚至更低的分辨率。轮廓检测的复杂度与像素数量直接相关。cv::Mat small; cv::resize(frame, small, cv::Size(), 0.5, 0.5, cv::INTER_LINEAR); // 缩小到一半设定ROIRegion of Interest如果你知道人物只可能出现在画面的某些区域如下半部分可以只在这些区域进行处理。cv::Rect roi(0, frame.rows/2, frame.cols, frame.rows/2); // 下半部分 cv::Mat roiImage frame(roi); // 只在roiImage上进行后续处理 // 注意找到的轮廓坐标是相对于roiImage的绘制回原图时需要偏移 for (auto point : contour) { point.x roi.x; point.y roi.y; }选择性处理不是每一帧都需要进行完整的轮廓查找。可以结合运动检测只有当画面中有足够大的变化时才触发昂贵的轮廓计算。可以用帧间差分法快速判断。使用更快的函数和数据类型确保使用OpenCV的优化版本开启了IPP、OpenCL等。对于二值图像的操作使用CV_8UC1类型。避免在循环中频繁创建和销毁大的cv::Mat对象尽量复用。5.2 常见问题与解决方案问题1轮廓断裂不连续。原因Canny阈值过高或者边缘本身对比度低。解决降低Canny的低阈值。在Canny之前或之后使用形态学操作膨胀cv::dilate来连接断开的边缘。cv::morphologyExwithMORPH_CLOSE先膨胀后腐蚀效果更好它能填充小的空洞并连接邻近区域。尝试使用cv::findContours的cv::RETR_LIST模式并配合轮廓近似有时能连接断点。问题2轮廓包含太多背景噪声毛刺。原因预处理降噪不足或阈值化太敏感。解决增大高斯模糊的核大小。使用中值滤波cv::medianBlur代替高斯模糊对椒盐噪声效果更好。在阈值化后、找轮廓前使用开运算cv::MORPH_OPEN去除小的白色噪点。问题3人物内部出现空洞比如衣服的深色区域被当成背景。原因阈值化将暗区归为背景。解决使用自适应阈值代替全局阈值。在找轮廓后利用层次结构hierarchy找到这些空洞它们是子轮廓并将其从前景掩模中填充cv::drawContourswiththickness cv::FILLED。考虑使用更高级的分割方法如GrabCut算法虽然更慢但分割效果更好。问题4程序在cv::findContours或cv::drawContours时崩溃。原因最常见的原因是传入的cv::Mat不是8位单通道二值图像。解决在调用findContours前务必用image.type() CV_8UC1检查图像类型和通道数。确保二值图像中前景是白色255背景是黑色0。可以用cv::threshold明确指定。使用cv::drawContours时确保目标图像result与原图frame尺寸、通道数一致且是深拷贝.clone()或新创建的。问题5轮廓点太多导致后续处理如绘制、计算特征太慢。原因cv::CHAIN_APPROX_NONE存储了所有点或者轮廓本身非常复杂。解决使用cv::CHAIN_APPROX_SIMPLE或cv::CHAIN_APPROX_TC89_L1等近似方法。用cv::approxPolyDP对轮廓进行多边形近似通过epsilon参数控制精度牺牲一点形状精度换取点数大幅减少。5.3 一个鲁棒性更强的完整示例代码结合以上所有要点下面是一个更加健壮和实用的示例。它使用了背景减除并包含了主要的筛选和优化步骤。#include opencv2/opencv.hpp #include iostream #include vector int main() { cv::VideoCapture cap(0); cv::Ptrcv::BackgroundSubtractorMOG2 bgSubtractor cv::createBackgroundSubtractorMOG2(300, 25, true); bgSubtractor-setShadowThreshold(0.5); cv::Mat kernelErode cv::getStructuringElement(cv::MORPH_ELLIPSE, cv::Size(3, 3)); cv::Mat kernelDilate cv::getStructuringElement(cv::MORPH_ELLIPSE, cv::Size(5, 5)); while (true) { cv::Mat frame, fgMask, gray, blurred, binary; cap frame; if (frame.empty()) break; // 1. 降采样提升速度 cv::Mat smallFrame; cv::resize(frame, smallFrame, cv::Size(), 0.5, 0.5); // 2. 转换为灰度图 cv::cvtColor(smallFrame, gray, cv::COLOR_BGR2GRAY); // 3. 背景减除 bgSubtractor-apply(gray, fgMask); // 去除阴影值127和微弱前景 cv::threshold(fgMask, binary, 200, 255, cv::THRESH_BINARY); // 4. 形态学处理先开运算去噪再闭运算填充空洞 cv::morphologyEx(binary, binary, cv::MORPH_OPEN, kernelErode); cv::morphologyEx(binary, binary, cv::MORPH_CLOSE, kernelDilate); // 5. 查找轮廓 std::vectorstd::vectorcv::Point contours; std::vectorcv::Vec4i hierarchy; cv::findContours(binary, contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE); // 6. 轮廓筛选与绘制 cv::Mat result smallFrame.clone(); double minArea smallFrame.rows * smallFrame.cols * 0.005; // 最小面积设为画面的0.5% for (size_t i 0; i contours.size(); i) { double area cv::contourArea(contours[i]); if (area minArea) continue; cv::Rect rect cv::boundingRect(contours[i]); float aspectRatio (float)rect.width / rect.height; if (aspectRatio 0.2 || aspectRatio 1.5) continue; // 宽高比过滤 // 计算凸性 std::vectorcv::Point hull; cv::convexHull(contours[i], hull); double hullArea cv::contourArea(hull); if (hullArea 0) continue; double solidity area / hullArea; if (solidity 0.7) continue; // 凸性过滤 // 通过筛选绘制轮廓和矩形框 cv::drawContours(result, contours, (int)i, cv::Scalar(0, 255, 0), 2); cv::rectangle(result, rect, cv::Scalar(255, 0, 0), 2); cv::putText(result, Person, cv::Point(rect.x, rect.y-5), cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(0, 0, 255), 1); } // 显示结果 cv::imshow(Foreground Mask, binary); cv::imshow(Person Detection, result); if (cv::waitKey(30) 27) break; } cap.release(); cv::destroyAllWindows(); return 0; }这个程序集成了背景建模、形态学处理、多维度轮廓筛选在一般室内环境下应该能比较稳定地框出站立或行走的人物。当然它仍然是一个基于传统视觉的方法对于重度遮挡、快速运动或复杂背景其局限性是固有的。这时你就需要评估是否要引入基于深度学习的分割模型了。但对于很多实时性、资源要求高的应用场景这套OpenCV C方案经过精心调优依然是你工具箱里一把锋利而可靠的手术刀。