ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

基于OpenCV的C++手掌图像测量:肤色分割与凸包分析实战

基于OpenCV的C++手掌图像测量:肤色分割与凸包分析实战 简介面向计算机视觉初学者与课程设计人员这是一份基于C与OpenCV的手掌测量完整项目。它利用摄像头获取手掌图像通过滤波、边缘检测、角点检测、霍夫变换等算法精确计算五根手指的长度与宽度、虎口角度、手掌宽度及手腕宽度可直接运行验证。压缩包共47个文件包含9个cpp源码、10个头文件、1份设计报告docx及多张测试图片整体仅1.18MB结构清晰、便于学习。已有655人学习下载适合作为图像处理与OpenCV实践课程的参考方案。资源附带了详细设计文档与全部源码读者可对照报告理解算法流程也可在此基础上扩展手势识别等应用。1. 手掌图像测量一个OpenCV课程设计能拆出多少东西人手的图像测量听起来就是“拍个照量一下”真正用C和OpenCV跑起来第一步就能卡半个月为什么颜色分割之后手指之间总是粘连为什么凸包检测到的“指尖”比实际少两个这些问题的答案都藏在这份基于C利用OpenCV视觉库进行手掌图像计算机视觉分析的项目里。它是一套完整的手掌图像分析工程通过肤色分割、边缘检测、角点检测、凸包分析、距离变换等传统视觉手段从单目图像中量出五根手指的长度与宽度、虎口角度、手掌宽度和手腕宽度并输出可视化结果。资源里带着设计报告word、完整VS源码和一批手掌样本图既能当OpenCV图像处理项目练手也能直接作为计算机视觉大作业的底子。适合正在做图像测量类作业的人也适合想了解传统视觉方案边界的人。2. 测量方案选型为什么是肤色分割加凸包分析而不是直接上深度学习2.1 方案对比传统视觉手段在这个场景里的优势做手掌测量最容易想到的方案其实是深度学习——用关键点检测网络标出21个手部关键点长度宽度自然就出来了。但这份课程设计选择了纯OpenCV传统视觉路线在实际场景里这个选择是合理的。对比维度OpenCV传统视觉方案深度学习关键点方案本场景适用性数据依赖不需要标注数据需要几千张标注手图传统方案完胜计算资源CPU实时跑通常需要GPU课程设计无GPU场景可解释性每个参数能追溯来源黑匣子答辩时传统方案更稳开发周期1-2周数周起步传统方案更适合精度稳定性受光照和背景影响受数据集分布影响半斤八两在背景可控、手掌摊开、光照相对固定的桌面场景里传统图像处理完全够用。这类测量任务本质上是“找点、连线、算距离”不涉及复杂语义理解用凸包和几何关系就能解决。常见的误区是一上来就上神经网络结果数据标注、训练调参的时间比写测量逻辑还长最后还不一定收敛。2.2 测量流程总览从原始帧到五组参数的完整链路整个测量流程是一条清晰的五步链路每一环的输出都是下一环的输入流程环节关键OpenCV函数输出数据常用参数图像采集与预处理VideoCapture、flip、resizeRGB帧640x480或720p肤色分割cvtColor、inRange二值掩码YCrCb阈值形态学净化morphologyEx干净的二值掩码5x5椭圆核轮廓与关键点findContours、convexHull、convexityDefects指尖、谷点、掌心凸缺陷深度阈值几何测量pointPolygonTest、norm、余弦定理长度、宽度、角度像素与毫米比例边缘检测在这个流程里不是主角但它负责一个关键任务定位手掌与手腕的分界。用Canny边缘检测配合霍夫直线变换可以找到腕部的两条侧边线从而确定腕部位置。角点检测则用于给轮廓上高曲率点打分作为指尖候选的补充验证。这套组合拳比单纯依赖某一个算法要稳得多。2.3 理解工程代码的组织方式先读主循环再读算法文件打开这个工程会发现一个特点源码文件非常多而且是以“人名.cpp / 人名.h”的方式组织的。这是典型的多人协作课程设计结构每个人负责一根手指的测量模块。这种组织方式的优点是并行开发效率高缺点是主流程不清晰第一次读代码的人容易一头扎进某个cpp里出不来。// Computer vision course design.cpp 主入口的典型骨架 #include opencv2/opencv.hpp #include Head.h // 公共头文件结构体定义都在这里 #include ConnectList.h // 手指参数的链表组织 int main() { // 1. 打开摄像头或者读取图片文件 cv::VideoCapture cap(0); cv::Mat frame; cap frame; // 2. 图像预处理与肤色分割 cv::Mat handMask segmentSkin(frame); // 具体实现分散在各自模块中 // 3. 调用各手指测量模块结果汇总到链表中 ConnectList results; // 公共数据结构任意模块都能读写 results.append(measureThumb(frame, handMask)); // 拇指测量模块 results.append(measureIndex(frame, handMask)); // 食指测量模块 // ... 其余手指同理 // 4. 绘制并输出 drawResults(frame, results); cv::imshow(hand measurement, frame); cv::waitKey(0); return 0; }读这份代码的正确顺序先打开Head.h看公共数据结构理解手指参数链表的定义方式然后看主cpp里的调用顺序最后才进单个手指的测量模块。我一般会把main函数里每个手指模块的调用行后面加上断点跑一遍看看每个模块的输入输出整个测量流程就清楚了。需要注意每个人写的模块接口风格可能不完全一致公共头文件里声明的结构体就是“对齐标准”遇到编译报错先检查是否有人改了公共头文件没同步。3. 手部分割与预处理把“手掌”从背景里干净地抠出来3.1 肤色检测YCrCb颜色空间里的阈值选取肤色检测是整条流水线的第一关这一关过不好后面全是白费。RGB空间里肤色受光照影响太剧烈直接对RGB设阈值极不稳定。常见做法是把图像转换到YCrCb颜色空间其中Y是亮度分量Cr和Cb是色度分量。肤色在CrCb平面上的聚类特性非常好把Y分量丢掉之后亮度的干扰就小了很多。// 肤色检测转换颜色空间 阈值分割 cv::Mat segmentSkin(const cv::Mat src) { cv::Mat ycrcb, mask; cv::cvtColor(src, ycrcb, cv::COLOR_BGR2YCrCb); // 常见经验阈值Cr 133~173Cb 77~127 // 这个范围覆盖亚洲人肤色较好但环境偏色时需要微调 cv::inRange(ycrcb, cv::Scalar(0, 133, 77), cv::Scalar(255, 173, 127), mask); // 光照补偿做一次直方图均衡能提升极端光照下的鲁棒性 cv::Mat yuv, equalized; cv::cvtColor(src, yuv, cv::COLOR_BGR2YUV); std::vectorcv::Mat channels; cv::split(yuv, channels); cv::equalizeHist(channels[0], channels[0]); cv::merge(channels, yuv); cv::Mat corrected; cv::cvtColor(yuv, corrected, cv::COLOR_YUV2BGR); // 用均衡后的图像重新做肤色分割 cv::cvtColor(corrected, ycrcb, cv::COLOR_BGR2YCrCb); cv::inRange(ycrcb, cv::Scalar(0, 133, 77), cv::Scalar(255, 173, 127), mask); return mask; }这段代码做了两件事第一把帧从BGR转到YCrCb用inRange直接卡阈值得到初步掩码第二对Y通道做直方图均衡后再分割一遍。均衡的作用是拉伸亮度分布在背光或过曝场景下能显著改善分割质量。阈值参数133和173不是随意写的这是肤色样本在Cr通道上聚类统计出来的经验值不同摄像头会有小幅偏差调试时先把这两个值打印出来看直方图再定。3.2 滤波与形态学操作去噪、补洞、断开粘连阈值分割出来的掩码通常很脏背景噪点、手指间的微小粘连、指甲区域内部的空洞都需要清理。滤波用中值滤波比高斯滤波更合适因为中值滤波在去除椒盐噪声的同时能更好地保留边缘锐利度而高斯滤波会模糊边缘导致后面找指尖时坐标偏移。// 形态学净化先开运算去噪再闭运算补洞 cv::Mat cleanMask(const cv::Mat rawMask) { cv::Mat kernel cv::getStructuringElement(cv::MORPH_ELLIPSE, cv::Size(5, 5)); cv::Mat opened, closed; // 开运算先腐蚀后膨胀去掉孤立的小噪点 cv::morphologyEx(rawMask, opened, cv::MORPH_OPEN, kernel); // 闭运算先膨胀后腐蚀补上手指内部的空洞和断口 cv::morphologyEx(opened, closed, cv::MORPH_CLOSE, kernel); return closed; }开运算和闭运算的顺序不能反过来。如果先闭运算背景噪点会被膨胀放大本来不相连的噪点可能连成一片先开运算把小噪点腐蚀掉再做闭运算补洞才安全。kernel大小用5x5的椭圆核在640x480下效果好如果图像是1080p要放大到7x7。指甲区域在肤色检测中经常被误判为非肤色闭运算能把指甲区域填回来这一步不做的话后面距离变换找掌心时会把指甲空洞误当成手背边界。3.3 轮廓提取与最大连通域筛选拿到手的轮廓分割结果里除了手掌还可能有手臂、桌面上的肤色物体。取最大连通域是最粗暴也最有效的过滤方式前提是手掌在画面中确实占主导地位。// 提取最大连通域拿到手掌的外轮廓 cv::Mat extractLargestContour(cv::Mat binary) { std::vectorstd::vectorcv::Point contours; std::vectorcv::Vec4i hierarchy; cv::findContours(binary.clone(), contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE); // 按面积降序排序取最大的一项 int maxIdx -1; double maxArea 0.0; for (int i 0; i contours.size(); i) { double area cv::contourArea(contours[i]); if (area maxArea) { maxArea area; maxIdx i; } } if (maxIdx -1) return cv::Mat::zeros(binary.size(), CV_8UC1); // 把最大轮廓画到空白掩码上 cv::Mat result cv::Mat::zeros(binary.size(), CV_8UC1); cv::drawContours(result, contours, maxIdx, cv::Scalar(255), cv::FILLED); return result; }注意findContours会修改传入的二值图所以传入前要clone一份。RETR_EXTERNAL只提取最外层轮廓这对手掌场景足够不需要内部轮廓。CHAIN_APPROX_SIMPLE会压缩轮廓点数量保留端点这样后面凸包计算的输入量更小。如果画面里手臂露出来太多最大连通域会把整条手臂包进去手掌区域反而只占一小部分这时候需要结合画面位置先验做ROI裁剪或者让用户在采集时把手腕贴近画面下边缘。3.4 腕部与掌心定位为后续关键点测量做几何约束腕部定位决定了“手掌”和“手臂”的分界线在哪。常见做法是对掩码逐行扫描统计每行白色像素的数量。手掌区域像素数多手腕区域像素数少找到白色像素数量最少的行就近似得到了腕部位置的基准线。// 腕部定位逐行扫描掩码找白色像素最少的行 int locateWrist(const cv::Mat mask, int startRow, int endRow) { int wristRow startRow; int minCount INT_MAX; for (int r startRow; r endRow; r) { int count cv::countNonZero(mask.row(r)); if (count minCount) { minCount count; wristRow r; } } return wristRow; }扫描范围不要从第0行开始因为指尖区域也可能出现像素数少的行尤其是五指张开时指尖缝隙处会有几行像素很少会误导定位。我一般从图像高度30%的位置开始扫描到这个位置以下的手掌主体区域结束这样找到的最小值才是真正接近手腕的位置。定位到腕部后把腕部以下的掩码区域全部置零后续所有几何计算都在这个“净手掌”掩码上进行能避免手臂对后续凸包分析和宽度测量产生干扰。4. 关键点提取与参数计算指尖、谷点和掌心圆心的几何关系4.1 凸包与凸性缺陷指尖与指根谷点的来源手掌掩码的轮廓是一圈点直接在这圈点上找指尖并不可靠因为轮廓在高曲率区域受离散化影响很大。凸包是包含整个手掌的最小凸多边形在五指张开时指尖天然落在凸包顶点上手指之间的缝隙则对应凸包与轮廓之间的凹陷区域这些凹陷在OpenCV里叫凸性缺陷。// 凸包与凸性缺陷指尖与谷点的检测核心 void findFingerTips(const std::vectorcv::Point contour, std::vectorcv::Point tips, std::vectorcv::Point valleys) { // 凸包计算必须返回索引形式 std::vectorint hullIndices; cv::convexHull(contour, hullIndices, false, false); // 凸性缺陷检测输入必须是有向轮廓 std::vectorcv::Vec4i defects; cv::convexityDefects(contour, hullIndices, defects); double depthThreshold 20.0; // 缺陷深度阈值过滤浅层凹陷 for (const auto d : defects) { int startIdx d[0]; // 凸包上的起始点 int endIdx d[1]; // 凸包上的结束点 int farIdx d[2]; // 缺陷最深点即谷点 float depth d[3] / 256.0f; // 缺陷深度注意单位要换算 if (depth depthThreshold) { valleys.push_back(contour[farIdx]); tips.push_back(contour[startIdx]); tips.push_back(contour[endIdx]); } } }这里有一个非常容易踩的坑convexityDefects的输入轮廓必须是顺时针或者逆时针的有向轮廓且必须是整型坐标。如果直接用findContours结果传入通常没问题但如果轮廓经过approxPolyDP简化方向可能变化导致函数返回空集。深度阈值d[3]的单位不是像素而是纵深方向偏差的256倍所以必须除以256才是真实像素深度。深度阈值20像素在640x480下能过滤掉大多数噪声凹陷但手指并拢时缝隙太浅谷点检测不出来所以这个算法要求被测者五指分开。4.2 距离变换与掌心圆心找到整个手掌最“厚”的位置掌心的定位对后续手指宽度测量很关键因为它提供了指根区域的参考。距离变换计算掩码中每个白色像素到最近黑色像素的距离距离最大的那个点就是手掌内部“最厚”的位置也就是掌心。// 距离变换找掌心最大距离点就是内切圆圆心 cv::Point locatePalmCenter(const cv::Mat handMask) { cv::Mat dist; cv::distanceTransform(handMask, dist, cv::DIST_L2, 5); double maxVal 0.0; cv::Point maxPt; cv::minMaxLoc(dist, nullptr, maxVal, nullptr, maxPt); return maxPt; }DIST_L2是欧氏距离这是最精确的距离度量maskSize参数5表示使用5x5的掩码进行近似计算精度足够。掌心位置对后面旋转校正很有用以掌心为圆心找出指尖方向可以把手掌旋转到竖直方向再做测量减少手指方向不一致带来的误差。距离变换对掩码内部的空洞非常敏感如果形态学闭合没做好空洞位置也可能变成“最厚”的点。4.3 手指长度与宽度的测量公式拿到指尖和谷点后测长度宽度就是纯几何计算。每根手指有1个指尖和2个谷点大拇指除外它只有一个主谷点。手指长度取指尖到两个谷点连线中点的欧氏距离。注意不是到谷点连线的垂足而是到中点这个细微差别在小拇指上能差出好几个像素。// 单根手指的长度与宽度计算 float measureFinger(const cv::Point tip, const cv::Point valley1, const cv::Point valley2, const cv::Mat handMask) { // 谷点连线中点作为指根基准 cv::Point root((valley1.x valley2.x) / 2, (valley1.y valley2.y) / 2); float length cv::norm(tip - root); // 手指轴线方向从指根指向指尖 cv::Point axisDir tip - root; float axisLen cv::norm(axisDir); if (axisLen 1e-6) return 0.0f; cv::Point unitAxis(axisDir.x / axisLen, axisDir.y / axisLen); // 垂直方向 cv::Point unitPerp(-unitAxis.y, unitAxis.x); // 在中段30%~70%位置采样多个截面平均宽度 float sumWidth 0.0f; int sampleCount 0; for (float t 0.3f; t 0.7f; t 0.05f) { // 当前截面的中心点 cv::Point center(root.x t * axisDir.x, root.y t * axisDir.y); // 从中心往两边延伸统计掩码上的像素 float halfWidth 0.0f; for (int step 1; step 200; step) { cv::Point pLeft(center.x step * unitPerp.x, center.y step * unitPerp.y); // 超出图像范围则停止 if (pLeft.x 0 || pLeft.y 0 || pLeft.x handMask.cols || pLeft.y handMask.rows) break; if (handMask.atuchar(pLeft.y, pLeft.x) 0) break; halfWidth step; } sumWidth 2 * halfWidth; sampleCount; } float width sampleCount 0 ? sumWidth / sampleCount : 0.0f; return width; }宽度测量必不能直接横向数像素因为手指可能是斜的。这里的做法是先把手指看作一条从指根到指尖的线段在线段上取30%到70%的中间段每隔5%采样一个截面在截面的垂直方向上向两侧扫描掩码直到遇到黑像素统计宽度并取平均。取中段而不是全段的原因是指尖和指根处轮廓曲率大宽度波动剧烈中间段最接近手指的真实平均宽度。4.4 虎口角度、手掌宽度与手腕宽度的计算虎口角度就是大拇指指尖、虎口谷点、食指指尖三点构成的夹角。用余弦定理即可已知三角形三边长度求夹角。// 虎口角度计算三点余弦定理 float calcAngle(const cv::Point p1, const cv::Point p2, const cv::Point p3) { // 以 p2 为顶点求 p1-p2-p3 的夹角 float a cv::norm(p2 - p3); // p2p3 边长 float b cv::norm(p1 - p2); // p1p2 边长 float c cv::norm(p1 - p3); // p1p3 边长 float cosAngle (a * a b * b - c * c) / (2 * a * b); cosAngle std::max(-1.0f, std::min(1.0f, cosAngle)); // 浮点溢出保护 return std::acos(cosAngle) * 180.0f / CV_PI; }手掌宽度取四指根部那条横截线的宽度也就是摊开手掌后最宽的位置。做法是在掌心位置的上下范围内扫描掩码各行统计白色像素数量取最大值。手腕宽度则在之前定位的腕部基准线附近扫描取最小值。这两个宽度的测量都依赖腕部定位和掌心定位的准确性所以前面预处理环节不能偷懒。4.5 像素尺度到真实尺度的标定所有测量结果目前都是像素值要变成毫米或者厘米必须做标定。最常见也最方便的做法拍摄时在画面里放一枚一元硬币直径25毫米程序识别硬币轮廓用其直径像素数作为比例尺。// 标定已知物体直径毫米 / 对应像素直径 float calibrationScale(const cv::Mat frame) { // 假设通过圆形检测拿到了硬币直径的像素值 float coinDiameterPixels detectCoinDiameter(frame); float coinDiameterMm 25.0f; // 一元硬币直径 float scale coinDiameterMm / coinDiameterPixels; // mm per pixel return scale; }标定物离手掌越近越好因为如果标定物在手掌前而镜头有透视效果测出来的比例会偏差。最稳的做法是把硬币贴在手掌背面或者放在手掌旁边同一平面。检测硬币可以用HoughCircles或者先做颜色分割再找圆形轮廓如果只是从图片上离线测量也可以直接手动点选硬币边缘的两个点计算像素距离。记住标定值要在同一场景下沿用切换摄像头或者改变物距后必须重新标定。5. 避坑与常见问题排查光线、镜像、凸包误检的五条血泪经验5.1 现象背景里的肤色物体抢走了最大连通域背景里出现另一只手、人脸、或者肤色接近的物体时最大连通域筛选会直接选出面积最大的那个物体手掌测量全盘崩溃。原因extractLargestContour只按面积排序它对“最大等于手掌”这件事有强假设这个假设在很多实际拍摄场景并不成立。解决加一个位置先验手掌通常位于画面中心区域。在取最大连通域时对每个连通域计算质心到图像中心的距离综合面积和距离给分或者直接先截取画面中心80%区域作为ROI再做肤色检测和连通域筛选。另外可以要求拍摄时手掌尽量靠近镜头让手掌占据画面主要面积。5.2 现象摄像头镜像导致左右手被认反打开摄像头默认画面是镜像的相当于把右手拍成了左手的样子。如果程序里有“根据拇指位置判断左右手”的逻辑就会全盘认反。原因镜头采集的原始画面左右颠倒而算法按几何特征排序指尖时默认了画面坐标和真实手型的对应关系。解决在读取每帧图像后先做cv::flip(frame, frame, 1)水平翻转把画面恢复成“照镜子”的直观视角再做后续处理。更稳妥的做法是不要让算法去判断左右手而是让用户指定左手或右手程序只负责按指定顺序编排指尖和谷点。5.3 现象凸性缺陷把指根谷点当成指尖五指并拢或者手指间距太小时凸包会跨过好几根手指原本属于指节的凸起点被包在凸包内部凸性缺陷的起点和终点根本不是期望的指尖位置。原因凸性缺陷的原理决定了它的起始点和结束点必须是凸包顶点。手指并拢时只有最外侧的两根手指拇指和小指的指尖还保持在凸包上中间手指的指尖全部成了“凹陷”的一部分。解决第一嘱咐被测者张开五指这是物理层面的约束第二算法上不能只依赖凸包顶点还要结合轮廓曲率检测。在轮廓上取指尖候选点附近的若干点计算前后向量的夹角夹角接近180度时说明轮廓近似直线不是指尖夹角突变且朝外凸时才算指尖。这个曲率过滤能找回凸包漏掉的指尖。// 曲率过滤补充凸包漏检的指尖 bool isTipBylocalCurvature(const std::vectorcv::Point contour, int idx, float angleThresh) { int n contour.size(); cv::Point prev contour[(idx - 10 n) % n]; // 前方第10个点 cv::Point curr contour[idx]; cv::Point next contour[(idx 10) % n]; // 后方第10个点 cv::Point v1 prev - curr; cv::Point v2 next - curr; float cosAngle (v1.x * v2.x v1.y * v2.y) / (cv::norm(v1) * cv::norm(v2) 1e-6); // 夹角越小越尖指尖处夹角通常小于80度 return cosAngle cos(angleThresh * CV_PI / 180.0); }这段代码的窗口大小取10在640x480下轮廓点间距大约3-5像素前后取10个点相当于覆盖了手指两侧约30-50像素的区间能体现指尖的局部曲率特性。角度阈值80度需要根据实际轮廓平滑度调整轮廓经过平滑处理后夹角值会偏大阈值要适当放宽。5.4 现象手指宽度忽大忽小边缘跳动同一只手静止放在桌上连续采几帧测出来的宽度值却波动十几个像素。尤其是手指侧边的毛发、边缘锯齿会让宽度值像心电图一样跳。原因肤色检测的阈值在边缘区域是“硬切”的一个像素可能因为噪声从255突然变成0导致截面扫描时提前遇到黑色像素宽度突变。另外手指边缘的阴影让肤色检测结果在边缘处非常不稳定。解决测量前对掩码做一次腐蚀操作把边缘的毛刺削掉宽度序列再做一次中值滤波在多个采样截面结果中取中位数而不是平均数中位数对异常值有天然的抵抗力。腐蚀量控制在1像素即可腐蚀太多会让宽度系统性偏小。5.5 现象VS报一堆OpenCV链接错误症状是编译能过链接时报一堆LNK2019、LNK2001未解析的外部符号或者运行时找不到opencv_world4100.dll。原因多半是Debug和Release模式下的库配置不一致或者OpenCV运行时库没有放进系统PATH。OpenCV的库分vc14、vc15版本分别对应VS2015和VS2017把Debug模式的库配成了Release版本也会报错。解决把库配置做成一个属性表文件新建项目时导入属性表而不是每次手动在vcxproj里改。属性表里统一写好include目录、lib目录、附加依赖项这样换机器、换VS版本时只需改属性表里的一处路径。运行时若报缺少dll把OpenCV的bin目录加到系统PATH或者直接把dll复制到exe同目录。注意Debug模式要链接带d后缀的库Release模式链接不带d的库混用必炸。6. 进阶用法离线验证与实时测量的一体化调试技巧6.1 用一组标准照片验证测量逻辑这个资源里带了一批编号的手掌样本图这就是天然的验证集。我拿到工程后的第一个动作不是接摄像头而是写一个离线遍历脚本把每张样本图依次送入测量pipeline打印每根手指的长度宽度测量结果再和设计报告里记录的真实数值对比。这一步能快速暴露算法在某个特定手指姿势下的崩溃点而不需要靠摄像头实时调试时一帧一帧去抓。// 离线批量验证遍历样本图并导出测量结果 std::vectorstd::string files { 1.png, 21.png, 32.png, 42.png, 51.png, 61.png }; for (const auto name : files) { cv::Mat img cv::imread(img/ name); if (img.empty()) continue; auto results measureHand(img); std::cout name : thumb results.thumbLength mm index results.indexLength mm\n; }不要小看这一步它会直接告诉你哪些图片是“凑数拍照”哪些是真正的算法边界样本。比如有一张图上五根手指没有完全张开那这张图应该是允许算法输出“检测失败”的而不是强行给一个错误数值。6.2 实时模式下该保留什么、可以砍掉什么实时测量时帧率往往掉到10帧以下这时候要在流程上做减法。我一般砍掉三个东西一是把输入帧缩小到400像素宽再处理宽度测量精度损失极小二是距离变换每10帧算一次因为手掌不动或者微动时掌心位置变化不大三是滤波和形态学在实时模式下各做一次就够不要再补第二次。保留的部分是肤色检测和凸包计算这两个环节直接影响关键点质量省不得。6.3 结果绘制与报告输出测量完成后把数据直接画到原图上比看命令行数字直观得多。用putText在每根手指尖旁边标注长度在虎口位置标注角度在掌根位置标注手掌宽度。输出报告建议用CSV格式方便直接拉到Excel里做误差分析。从那以后但凡做图像测量类任务我拿到工程的第一件事永远是先跑离线验证再上摄像头这个习惯帮我拦下了至少五次“算法写着没问题但实测全错”的翻车现场希望帮到你。本文还有配套的精品资源点击获取
返回列表