ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

MATLAB手势识别实战:基于肤色分割与几何特征的石头剪刀布分类

MATLAB手势识别实战:基于肤色分割与几何特征的石头剪刀布分类 简介一份基于 MATLAB 的手势识别项目围绕“石头剪刀布”三种手势展开面向 MATLAB 学习者与计算机视觉入门人群可帮助学习者理解图像采集、预处理、特征提取到机器学习分类的完整实现流程。压缩包共 18 个文件以 .m 源代码为主线覆盖图像读取、裁剪、灰度转换、缩放等预处理环节同时配有 .jpg/.bmp 样本图像、.fig 图形界面文件及 .asv 备份文件整体仅 347KB便于下载后直接运行调试。资源已有1862人学习下载虽然体积小巧但功能模块完整包括多种手势样本、滤波与裁剪函数、识别结果界面等。借助这些文件可对照代码学习手势分割、背景去除、形状特征提取与 SVM 等分类器的应用适合用于课程设计、毕业设计或入门练手由于代码结构清晰也可替换样本或算法扩展为其他手势识别项目。1. 用 MATLAB 做石头剪刀布手势识别先别急着上深度学习在固定摄像头前把石头、剪刀、布三个手势分开MATLAB 手势识别往往是最快的路径之一不需要先积累上万张标注图片也不需要 GPU 训练图像分割、形态学处理、区域统计这些基础函数都内置在手头可用的工具箱里几十行脚本就能让一个简单手势识别器跑起来识别结果在命令行或界面上立刻可见。这篇文章面向想把“基于 MATLAB 的石头剪刀布识别”从纸面方案落成可运行代码的人包括课程设计、竞赛 demo 和快速原型验证。你会看到一套以肤色分割和几何特征为核心的方案以及参数怎么设、问题出在哪里时该怎么定位。2. 手势分割从肤色区域到最大连通域的 MATLAB 实现任何“简单手势识别”的第一步都是把“手”从画面里抠出来。这一步做不好后面算再多特征也是白算。MATLAB 里最常用的做法是 YCbCr 肤色模型加最大连通域整套处理只需要图像处理工具箱就能完成。2.1 用 YCbCr 肤色模型把皮肤从背景中分离肤色分割虽然听上去简单但选错颜色空间会立刻翻车。RGB 空间下皮肤颜色受亮度干扰很大同一个人的手在阴影和强光下的 RGB 值差异非常大而 YCbCr 的 Cb、Cr 两个分量专门保存色度信息Y 分量保留亮度。所以常规做法是只用 Cb 和 Cr 做区间判断刻意不把 Y 放进条件里。I imread(gesture.jpg); % 输入单张手势图片 YCbCr rgb2ycbcr(I); % 转 YCbCr 色彩空间 cb double(YCbCr(:, :, 2)); cr double(YCbCr(:, :, 3)); skinMask (cb 77 cb 127) ... (cr 133 cr 173); % 经典肤色范围这段代码的意图很直接先转色彩空间再对色度通道做阈值截断得到逻辑矩阵skinMask其中 1 表示“像皮肤”的像素。这里用的Cb 77~127、Cr 133~173是从大量室内样本统计出来的经验范围实际效果与摄像头、白平衡、手部肤色都相关。如果你在调试时发现指尖大量丢失可以用imtool打开图片用像素拾取器对准手背和指缝观察 Cb/Cr 的分布再手动收窄或放宽区间。光线偏暖时 Cr 下限要往上调光线偏冷时 Cb 下限要往上调这是这套方案里最先需要动的参数。2.2 形态学清理与最大连通区域提取肤色掩膜在刚开始时往往是“碎”的指缝因为光照不均匀被切成几块背景噪声形成小孔洞衣服领口或手臂也可能被误检成皮肤。常见的处理顺序是“闭运算 小面积过滤 最大连通域”。skinMask imclose(skinMask, strel(disk, 5)); % 闭运算填平指缝间的暗区 skinMask imopen(skinMask, strel(disk, 3)); % 去除细小毛刺 skinMask bwareaopen(skinMask, 500); % 删除少于500像素的孤立小块 labeled bwlabel(skinMask); % 给每个连通区域编号 stats regionprops(labeled, Area); % 计算各区域面积 [~, idx] max([stats.Area]); % 找面积最大的一块 handMask labeled idx; % 取这一块作为手势区域imclose的 disk 半径为 5表示用半径 5 像素的圆形结构元素做膨胀再腐蚀。半径太小填不住指缝太大则会把张开的手指“糊”在一起后续波峰检测会失效。bwareaopen的 500 是绝对像素面积如果你的输入是 1920×1080这个值没问题但如果输入是手机缩略图手部面积可能不足 500会被整块删掉。更稳妥的写法是round(numel(skinMask) * 0.0005)让阈值跟随图像尺寸缩放。regionprops返回结构数组数组长度等于连通域个数用max找到面积最大的区域编号然后取出那一块作为handMask。这个“最大连通域”假设是整套方案成立的前提画面里最显眼的皮肤色块就是手。因此背景里不能有大面积裸露皮肤、肤色的纸张或木制家具否则分割阶段就会选错目标。2.3 分割结果验证掩膜质量决定识别率上限分割做得好不好不用等识别完再判断。我一般会立刻把掩膜和原图做一次叠加显示figure; imshow(I); hold on; visboundaries(handMask, Color, g, LineWidth, 2); title(Hand mask overlay);visboundaries能快速画出掩膜边界绿色边界必须紧密贴住手指轮廓。如果边界明显小于手指说明闭运算半径不够或肤色阈值过窄如果边界包含大片背景说明阈值过宽或最大连通域选错了目标。现象原因调整参数手指尖被切掉指尖肤色偏暗或阈值过窄扩大 Cb/Cr 范围调大 imclose 半径手和背景粘连背景中有类似肤色区域收窄阈值或改用第 5 章的背景差分手指之间连成一块结构元素过大disk 半径降到 3 或 2mask 上有很多洞光照不均肤色局部出界加一次imfill(handMask,holes)这里有一个原则宁可让 mask 稍微偏大也不要切掉手指。手指的轮廓信息全部依赖掩膜边界切掉一个指尖就少一个波峰后面的分类结果会直接跳变。这个阶段多花两分钟肉眼检查比后面调半天特征阈值都管用。3. 手部几何特征区分石头、剪刀、布的关键指标分割完成后要回答的问题是丢掉彩色信息后怎么只用黑白掩膜判断石头、剪刀、布这章介绍的方法不依赖深度学习利用的是手部轮廓的几何结构参数直观也容易在 MATLAB 里复现。3.1 从轮廓到极坐标距离曲线一个直观的指尖计数法最直接的想法是数凸缺陷OpenCV 里有convexityDefects但 MATLAB 没有完全对应的内置函数自己实现时还要处理缺陷合并阈值比较麻烦。我一般用“极坐标距离曲线”替代先取手部轮廓以质心为原点把每个轮廓点转换为距离 r 和角度 θ再按角度重采样。手指张开时指尖距离质心远指缝距离质心近于是“r 随 θ 变化”的曲线会出现波峰一个波峰基本对应一根手指。boundary bwboundaries(handMask); b boundary{1}; % 外轮廓点N x 2列顺序为 (y, x) cenInfo regionprops(handMask, Centroid); cx cenInfo.Centroid(1); % Centroid 返回的是 (x, y) cy cenInfo.Centroid(2); dist sqrt((b(:,1) - cy).^2 (b(:,2) - cx).^2); ang atan2d(b(:,1) - cy, b(:,2) - cx); [angSort, idx] sort(ang); distSort dist(idx); angleAxis linspace(-180, 180, 360); distResample interp1(angSort, distSort, angleAxis, linear, extrap); distResample smoothdata(distResample, movmean, 7);这里有一个非常容易踩的坑bwboundaries返回的轮廓坐标是(row, col)即(y, x)而regionprops的Centroid返回的是(x, y)两者顺序相反。很多人在这一步把 xy 搞反得到的距离曲线完全错位还以为是分割问题。拿到轮廓后先min(b)、max(b)看一眼坐标范围确认和handMask尺寸对应再往下写。interp1的作用是把不均匀的角度采样变成均匀的 360 个点。轮廓点数可能上千按角度排序后相邻点之间的角度差并不均匀直接做峰值检测会产生大量假峰重采样成等间隔信号后后续findpeaks的参数才有意义。smoothdata的窗口选 7窗口太小保留毛刺太大则会把两指并拢时的小波峰直接抹平。3.2 用 findpeaks 统计手指个数与辅助几何特征有了平滑后的距离曲线直接提取局部极大值。参数设计是这一节的关键minPeakH mean(distResample) 0.45 * (max(distResample) - min(distResample)); [~, locs] findpeaks(distResample, ... MinPeakHeight, minPeakH, ... MinPeakDistance, 20); numFingers numel(locs);MinPeakHeight没有写死成 80 像素之类的绝对数值而是用“峰值至少要高出整体曲线区间的 45%”作为条件。这样当手离摄像头远近不同、掩膜总尺寸不同时识别结果不会剧烈漂移。0.45 是我在室内光照样本上调出的经验值握拳时曲线较平少数误检峰高度不足 0.35剪刀两指并拢时两个峰通常一高一低0.45 能稳定保留较高那个。MinPeakDistance设为 20表示两个峰之间至少间隔 20 个采样点对应约 20 度。它的作用是过滤同一根手指边缘的毛刺峰。五根手指张开时波峰平均间隔约 60 度取 20 度既不会把两根手指的峰合并又能有效抑制噪声。除了峰数还有两个几何特征值得同时提取handArea bwarea(handMask); % 掩膜面积 prop regionprops(handMask, ConvexArea, Perimeter); solidity handArea / prop.ConvexArea; % 实心度 maxR max(distResample); % 最长半径solidity描述掩膜面积占其凸包面积的比例。握拳时掩膜接近凸包solidity 接近 1五指张开时指缝凹陷明显solidity 会降到 0.85 以下。它是区分“石头”和“布”的重要辅助特征。3.3 三分类规则先数手指面积和实心度兜底根据numFingers和solidity可以构造不需要训练分类器的规则手势期望指尖峰数solidity备注石头0~10.92~1.00握拳时轮廓崎岖偶尔出现一个假峰剪刀1~20.82~0.95两指并拢时可能只有一个峰需辅助判断布3~50.70~0.90五指张开越彻底峰越清晰纯按numFingers分类有一个问题剪刀两指并拢时预处理后可能只检测到一个峰此时石头和剪刀都表现为 0~1 个峰需要再用solidity或面积做二次区分。剪刀虽然峰数少但掩膜面积比石头大不少实心度也更低这一差异足以把它们分开。这里顺便回答标题里“为什么是简单手势识别”它只解决固定背景下、手部占画面主体时的三分类问题不处理多手、遮挡、任意背景。如果场景变复杂几何特征会快速失效那就需要进入深度学习或 MediaPipe 路线第 5 章会给出取舍依据。4. 完整可运行的 MATLAB 手势识别器代码与实时改造把前两章的内容串起来就是一个完整可运行的手势识别器。这一章给出主函数、参数表和摄像头实时运行的改造方式。4.1 主脚本分割、提特征、输出三分类function label recognizeGesture(img) % 输入: 任意尺寸彩色图像 % 输出: rock | scissors | paper % ---------- 1. 肤色分割 ---------- YCbCr rgb2ycbcr(img); cb double(YCbCr(:,:,2)); cr double(YCbCr(:,:,3)); skin (cb 77 cb 127) (cr 133 cr 173); skin imclose(skin, strel(disk, 5)); skin bwareaopen(skin, round(numel(skin) * 0.0005)); labeled bwlabel(skin); stats regionprops(labeled, Area); [~, idx] max([stats.Area]); hand labeled idx; % 把手掌中心的小洞填掉避免轮廓断裂 hand imfill(hand, holes); % ---------- 2. 极坐标距离曲线 ---------- bnd bwboundaries(hand, noholes); if isempty(bnd) label none; return; end b bnd{1}; c regionprops(hand, Centroid); cx c.Centroid(1); cy c.Centroid(2); dist sqrt((b(:,1) - cy).^2 (b(:,2) - cx).^2); ang atan2d(b(:,1) - cy, b(:,2) - cx); [angSort, si] sort(ang); distSort dist(si); angleAxis linspace(-180, 180, 360); distRes interp1(angSort, distSort, angleAxis, linear, extrap); distRes smoothdata(distRes, movmean, 7); % ---------- 3. 特征提取 ---------- minH mean(distRes) 0.45 * (max(distRes) - min(distRes)); [~, locs] findpeaks(distRes, ... MinPeakHeight, minH, ... MinPeakDistance, 20); nPeaks numel(locs); handArea bwarea(hand); convArea regionprops(hand, ConvexArea).ConvexArea; solidity handArea / convArea; % ---------- 4. 分类 ---------- if nPeaks 4 label paper; % 四根以上完整手指明显张开 elseif nPeaks 2 || nPeaks 3 label scissors; % 两根或三根手指峰常见于剪刀 elseif nPeaks 1 if solidity 0.92 label rock; % 拳头轮廓饱满接近凸包 else label scissors; % 两指并拢时峰少但实心度低 end else label unknown; end endbwboundaries里用了noholes选项这很重要如果手掌内部有孔洞bwboundaries会返回多个边界第一个不一定是外轮廓noholes保证只取最外层边界。imfill的作用是把肤色阈值在掌心留下的零星空洞先补上避免后续solidity虚低。分类决策里把nPeaks 3也归为剪刀是因为很多人在做剪刀时会把中指、无名指、小指一起伸出形成三个峰。规则设计优先保证石头和布不错剪刀即使偶尔和石头混淆也能通过solidity兜底。4.2 阈值参数表换相机、换人时改哪里参数位置当前取值影响Cb/Cr 范围分割阶段77~127 / 133~173皮肤漏检或背景误检disk 半径imclose / imopen3~5手指粘连或轮廓缺失bwareaopen 比例分割阶段0.0005是否误删手部区域MinPeakHeight 系数特征阶段0.35~0.50峰数量偏多或偏少MinPeakDistance特征阶段15~25同手指毛刺峰是否被过滤4.3 改造成摄像头实时识别的脚本固定相机场景下用webcam对象逐帧读取画面调用上面的函数cam webcam(); % 自动选择默认摄像头 cam.Resolution 1280x720; % 限制分辨率保证实时性 preview(cam); % 先看取景范围 for i 1:200 frame snapshot(cam); frame imresize(frame, [360, NaN]); % 缩小到 360 高度 label recognizeGesture(frame); imshow(frame); text(10, 10, label, Color, g, FontSize, 40); drawnow; end clear cam;实时模式对分割参数更敏感因为摄像头自动白平衡会让肤色范围在运行过程中缓慢漂移。固定相机曝光是最好的解决办法做不到的话就需要像第 5 章那样引入背景差分或周期性阈值校正。webcam函数需要 MATLAB Support Package for USB Webcams没装支持包的老版本常见替代方案是imaqhwinfo配合videoinput但配置复杂度明显上升。5. 参数怎么调把识别坑一个个填平5.1 用一张失败图定位瓶颈当你发现某一个手势总识别错先别急着改分类阈值。常规做法是按顺序打印三个中间量掩膜边界是否正确、nPeaks数量和实际手指数是否一致、solidity是否在合理区间。fprintf(peaks%d solidity%.3f maxR%.1f\n, nPeaks, solidity, maxR);一次误判就能看出是特征错了还是规则错了。特征错了回到分割和MinPeakHeight去调特征正确但分类错才去改决策逻辑。这个顺序能省掉大量盲调时间。5.2 光照漂移与背景干扰的治理肤色阈值是这套方案里最脆弱的一点。光照一变同一只手在画面里的 Cb/Cr 会整体偏移。固定场景下更好的方案是采集一张没有手的背景图用背景差分先把前景抠出来再和肤色掩膜做与运算fgMask imabsdiff(rgb2gray(frame), rgb2gray(bgImg)) 25; hand hand fgMask;背景差分能滤掉大量肤色背景比如桌面、纸箱、木质家具。背景图必须保证没有人入镜时采集否则差分结果会把整个人体全部保留最大连通域也随之失效。5.3 什么时候放弃这套规则识别器如果换了三个以上环境每次都是“调好 A 环境、B 环境又崩”问题不在参数而在方案本身。几何特征方案适合固定场景、固定相机、肤色相对一致的场景一旦要处理任意背景或多人更常见的做法是转到 MediaPipe Hands 或训练轻量 CNN。那一步的工程代价是数据标注和模型部署不再只是调五个参数的问题。方案适应场景维护成本YCbCr 几何特征固定相机、固定背景调 5 个参数MediaPipe任意背景、多人手部引入深度学习模型CNN 分类大规模多样数据需要数据集和训练流程最后的调试技巧是把每次误判的图片、当时的nPeaks、solidity存成一个表调整参数后批量回放而不是对着摄像头反复试。对一张 360p 图片整套流程每次耗时不到 100ms批量回放 100 张图两分钟内就能看出参数变化对识别率的影响。把分割、特征、分类三段各自独立验证任何一个环节出问题都能快速定位到具体函数和阈值上。本文还有配套的精品资源点击获取
返回列表