ARTICLE DETAIL

资讯详情

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

基于MatLab的扑克牌数字与花色识别:从预处理到分类的完整流程

基于MatLab的扑克牌数字与花色识别:从预处理到分类的完整流程 简介这份资源面向具备一定MATLAB基础、希望入门计算机视觉与模式识别的开发者与高校学生提供了一套完整的扑克牌数字与花色自动识别项目源码。项目围绕图像预处理、特征提取、模式识别与分类器设计展开涵盖灰度化、二值化、边缘检测、字符分割及机器学习模型训练等关键环节可用于课程设计、毕业设计或人工智能实践练习。压缩包共43个文件以35张jpg图像样本为主配合5个m脚本文件、1个txt说明文档及少量png、asv文件整体约1.8MB目录结构清晰便于按模块查阅与调试。目前已有148人学习下载。读者可从中获取可运行的MATLAB识别流程、分类脚本与配套样本图像理解从图像输入到数字花色判定的完整实现思路并在此基础上调整算法参数、替换数据集或扩展实时识别功能适合作为计算机视觉入门与项目复现的参考素材。1. 从一张随手拍的牌桌照片说起MatLab 扑克牌数字与花色识别到底在做什么很多人第一次听到「基于 MatLab 实现扑克牌的数字、花色识别」脑子里浮现的是摄像头对着牌桌、屏幕上实时跳出「红桃 A」的画面。真实情况没那么玄也没那么简单。它本质上是把一张扑克牌图像经过预处理、定位、分割、特征提取、分类五个环节最终输出两个标签一个是点数A、2 到 K一个是花色黑桃、红桃、梅花、方块。听起来像是一个普通的目标分类任务但扑克牌有几个天然难点牌面是白色底、图案细线条、数字和花色位置固定但受拍摄角度影响、不同牌之间差异极小比如红桃和方块都是红色光照一变颜色阈值就崩。这套方案适合谁适合正在做 matlab 图像处理大作业的学生、需要快速验证 OCR 或模板匹配思路的工程师、以及想用 MatLab 把「图像处理 分类器」串成完整 pipeline 的从业者。它不需要深度学习框架不需要 GPU一台装了 MatLab 的普通笔记本就能跑通。核心思路是先用几何特征把牌从背景里抠出来再用模板匹配或简单分类器识别数字和花色。这条路子不追求 SOTA 精度但胜在可解释、可调试、每一步都能看到中间结果。如果你手头正好有 matlab 安装包或者刚查完 matlab 安装教程把环境搭好接下来这套流程可以直接复现。2. 图像预处理与牌面定位把牌从背景里「捞」出来2.1 为什么预处理决定了后面所有环节的上限扑克牌识别翻车十次有八次不是分类器不行而是预处理没做好。牌桌背景、手部阴影、反光、牌与牌之间的遮挡都会让后续的轮廓检测和分割直接跑偏。我一般把预处理拆成四步灰度化、去噪、边缘增强、二值化。灰度化用rgb2gray但注意如果花色识别依赖颜色红桃/方块 vs 黑桃/梅花灰度图只能用于定位颜色信息要单独保留。去噪用中值滤波medfilt2窗口大小 3×3 或 5×5太大把数字笔画也磨没了。边缘增强用imadjust做对比度拉伸或者用adapthisteq做自适应直方图均衡后者在光照不均时更稳。二值化用imbinarize配合 Otsu 方法但扑克牌白色区域占比大Otsu 有时会把整张牌判成前景这时候要改用局部自适应阈值imbinarize(I,adaptive)。% 读取图像并做基础预处理 img imread(poker_table.jpg); gray rgb2gray(img); % 灰度化用于定位 denoised medfilt2(gray, [5 5]); % 中值滤波去噪 enhanced adapthisteq(denoised); % 自适应直方图均衡 bw imbinarize(enhanced, adaptive, ... Sensitivity, 0.5, ForegroundPolarity, dark); bw bwareaopen(bw, 500); % 去掉小面积噪点这段代码的逻辑是灰度图负责找牌的位置自适应二值化负责把牌面和背景分开。Sensitivity参数控制阈值敏感度0.5 是中性值调高会保留更多细节但也引入噪点。bwareaopen去掉小于 500 像素的连通域这个值根据你的图像分辨率调整一般取牌面积的十分之一左右。跑完这一步用imshowpair对比原图和二值图如果牌面内部出现大量空洞说明阈值太激进把Sensitivity降到 0.4 试试。2.2 用连通域和最小外接矩形锁定单张牌二值图出来之后下一步是找到每张牌的轮廓。常见做法是bwconncomp找连通域再用regionprops拿每个连通域的BoundingBox和Area。扑克牌是矩形长宽比大约 1.4:1面积在整张图里占前几位。过滤条件可以设成面积大于图像总面积的 5%长宽比在 1.2 到 1.6 之间。这样能把大部分背景干扰滤掉。如果牌是斜的BoundingBox是轴对齐的会包含多余背景这时候要用regionprops的Orientation属性做旋转校正或者直接用minAreaRect的思路——MatLab 里没有现成的minAreaRect但可以用bwboundaries拿轮廓点再自己算最小外接矩形。% 连通域分析并筛选扑克牌区域 cc bwconncomp(bw); stats regionprops(cc, Area, BoundingBox, Orientation); imgArea numel(bw); validIdx []; for k 1:length(stats) areaRatio stats(k).Area / imgArea; bbox stats(k).BoundingBox; aspectRatio max(bbox(3),bbox(4)) / min(bbox(3),bbox(4)); if areaRatio 0.05 aspectRatio 1.2 aspectRatio 1.6 validIdx [validIdx, k]; end end % 提取第一张有效牌的 ROI bbox stats(validIdx(1)).BoundingBox; roi imcrop(img, bbox);这里的关键参数是areaRatio和aspectRatio。面积比 0.05 是经验值如果你的图里牌很小降到 0.02长宽比范围 1.2 到 1.6 覆盖了标准扑克牌的比例但如果拍摄角度倾斜严重这个范围要放宽到 1.1 到 1.8。Orientation属性返回的是区域主轴与水平方向的夹角如果绝对值大于 10 度建议先imrotate校正再裁剪否则后面的模板匹配会对不齐。裁剪出来的roi就是单张牌的图像接下来要在这张牌上找数字和花色的位置。2.3 牌面区域分割数字区和花色区的坐标关系标准扑克牌的版面是固定的左上角有数字和花色右下角有镜像的数字和花色中间是图案区。识别时通常只取左上角区域因为右下角是旋转 180 度的处理起来多一步翻转。左上角区域大致占整张牌的左上四分之一具体坐标可以用比例定位数字在牌宽的 5% 到 25%、牌高的 5% 到 30% 之间花色在数字正下方或右侧。更稳的做法是先做角点检测用detectHarrisFeatures找到牌面的四个角然后做透视变换fitgeotransimwarp把牌校正成正视图。校正之后数字区和花色区的坐标就固定了直接按比例裁剪即可。% 透视校正后按比例裁剪数字区和花色区 % 假设已经通过角点检测得到校正后的牌面图像 cardImg [H, W, ~] size(cardImg); numRegion cardImg(round(0.05*H):round(0.30*H), ... round(0.05*W):round(0.25*W), :); suitRegion cardImg(round(0.30*H):round(0.50*H), ... round(0.05*W):round(0.25*W), :);比例参数不是拍脑袋定的是我拿标准 Bicycle 扑克牌量出来的。不同品牌的牌比例略有差异如果识别率低先把numRegion和suitRegion用imshow显示出来肉眼确认有没有裁偏。裁偏了调比例别急着改分类器。这一步做完你手里就有了两张小图一张只有数字一张只有花色。接下来分别对它们做识别。3. 数字识别模板匹配、OCR 与轻量分类器怎么选3.1 模板匹配的适用边界与归一化操作数字识别最直接的方法是模板匹配。把 A、2 到 K 共 13 个数字做成标准模板每个模板归一化到固定大小比如 40×60然后拿待识别数字区域和每个模板算相似度。MatLab 里用normxcorr2做归一化互相关取响应最大的模板作为结果。这个方法在牌面清晰、字体标准的情况下准确率能到 90% 以上但有两个硬伤一是模板必须和待识别图像字体一致换个牌厂就崩二是对旋转和缩放敏感虽然做了透视校正但数字本身可能有轻微倾斜。% 模板匹配识别数字 templates cell(1, 13); % 预加载 13 个数字模板 labels {A,2,3,4,5,6,7,8,9,10,J,Q,K}; numGray rgb2gray(numRegion); numNorm imresize(numGray, [60, 40]); % 统一尺寸 scores zeros(1, 13); for i 1:13 tmpl imresize(templates{i}, [60, 40]); corrMap normxcorr2(tmpl, numNorm); scores(i) max(corrMap(:)); end [~, bestIdx] max(scores); recognizedNum labels{bestIdx};normxcorr2的输出是相关系数图最大值越接近 1 说明越匹配。imresize到 [60, 40] 是经验尺寸太小丢失笔画细节太大增加计算量。如果 10 和 1 经常混淆是因为 10 的模板里包含两个字符而 1 只有一个归一化后 10 被压扁了。解决办法是把 10 单独处理先判断数字区域宽度如果宽高比大于 0.8大概率是 10直接跳过模板匹配。这个 trick 能解决大部分 10 的误判。3.2 用 OCR 做数字识别的配置与中文注释乱码问题MatLab 自带ocr函数底层是 Tesseract。对印刷体数字OCR 的准确率比模板匹配高而且不需要自己做模板。用法很简单results ocr(numRegion, TextLayout, Block)然后取results.Text。但有几个坑一是 OCR 对图像二值化质量要求高直接拿 RGB 图进去效果很差要先转灰度再imbinarize二是 OCR 可能把数字识别成字母比如 0 识别成 O、1 识别成 I需要做后处理映射三是如果你在中文 Windows 上跑 MatLab 2023 或更早版本ocr函数的中文注释可能乱码这是编码问题在脚本开头加feature(DefaultCharacterSet, UTF-8)能缓解。% 用 OCR 识别数字区域 numGray rgb2gray(numRegion); numBw imbinarize(numGray, adaptive); numBw ~numBw; % OCR 期望白底黑字如果当前是黑底白字要反转 results ocr(numBw, TextLayout, Block, ... CharacterSet, 0123456789AJQK); rawText strtrim(results.Text); % 后处理修正常见误识别 rawText strrep(rawText, O, 0); rawText strrep(rawText, I, 1); rawText strrep(rawText, l, 1);CharacterSet参数限定只识别数字和字母能显著降低误识别率。TextLayout设为Block表示把整个区域当一个文本块处理适合单个数字或短字符。如果 OCR 返回空字符串检查二值图是不是全黑或全白用imshow看一眼。OCR 的优点是泛化好缺点是慢单张牌识别大概 0.3 到 0.5 秒如果要做实时视频流建议还是用模板匹配或训练一个简单分类器。3.3 用 HOG SVM 做数字分类的完整流程模板匹配和 OCR 都是「不用训练」的方法但如果你想在自定义牌面上达到更高精度训练一个轻量分类器是值得的。特征用 HOG方向梯度直方图分类器用 SVMMatLab 里都有现成函数。流程是收集每个数字的样本图各 50 到 100 张统一尺寸提取 HOG 特征训练多类 SVM然后用predict做识别。HOG 对光照变化和轻微形变比模板匹配鲁棒SVM 在小样本下表现稳定。% HOG SVM 数字分类器训练与预测 % 假设 X_train 是 M×N 的特征矩阵Y_train 是对应标签 % 特征提取阶段 cellSize [8 8]; hogFeature extractHOGFeatures(numNorm, CellSize, cellSize); % 训练阶段离线做一次 svmModel fitcecoc(X_train, Y_train, ... Learners, svm, Coding, onevsall); % 预测阶段 predictedLabel predict(svmModel, hogFeature);extractHOGFeatures的CellSize参数控制局部感受野8×8 是经典配置对 40×60 的小图合适。fitcecoc是 MatLab 的多类分类框架onevsall编码适合类别数不多的情况13 类。训练数据要覆盖不同光照、不同角度、不同牌厂否则模型过拟合。如果训练集只有一种牌测试时换一副牌准确率会掉 20% 以上。我一般会留 20% 数据做验证用confusionmat看混淆矩阵重点看 6 和 9、3 和 8 有没有互相误判。4. 花色识别颜色特征与形状特征的组合策略4.1 红色与黑色花色的颜色空间选择花色识别第一步是分红色和黑色。红桃和方块是红色黑桃和梅花是黑色。听起来简单但 RGB 空间里红色和黑色的区分受光照影响很大暗光下红色会偏黑。更稳的做法是转到 HSV 空间用色调Hue和饱和度Saturation联合判断。红色的 Hue 在 0 到 10 或 170 到 180 之间Saturation 大于 0.3黑色的 Saturation 很低Value 也低。MatLab 里用rgb2hsv转换然后按阈值分割。% HSV 空间分离红色和黑色花色 suitHsv rgb2hsv(suitRegion); H suitHsv(:,:,1); S suitHsv(:,:,2); V suitHsv(:,:,3); redMask ((H 0.05 | H 0.94) S 0.3 V 0.2); blackMask (V 0.3 S 0.4); redRatio sum(redMask(:)) / numel(redMask); if redRatio 0.15 colorLabel red; else colorLabel black; endredRatio阈值 0.15 是经验值因为花色区域里红色像素占比不会太高图案有留白。如果整张牌偏红比如红色牌背这个阈值要调高。HSV 转换前确保图像是uint8或double且范围正确rgb2hsv对uint8输入会自动归一化到 0 到 1。如果红色和黑色都检测不到检查suitRegion是不是裁到了空白区域。4.2 形状特征区分红桃/方块与黑桃/梅花分出红黑之后还要区分红桃和方块、黑桃和梅花。这两组内部的区别在形状红桃是心形上方有凹口方块是菱形四个角尖黑桃是倒心形加柄梅花是三个圆瓣加柄。形状特征可以用regionprops的Solidity、Eccentricity、Extent来区分。红桃的 Solidity 较高接近 1因为心形比较「实」方块的 Eccentricity 较高因为菱形细长黑桃和梅花的区别在瓣数可以用bwlabel数连通域个数——梅花通常是三个瓣连在一起但二值化后可能分成多个区域。% 形状特征区分四种花色 suitBw imbinarize(rgb2gray(suitRegion), adaptive); suitBw bwareaopen(suitBw, 50); props regionprops(suitBw, Solidity, Eccentricity, Extent); if strcmp(colorLabel, red) if props(1).Solidity 0.85 suitLabel heart; else suitLabel diamond; end else if props(1).Eccentricity 0.9 suitLabel spade; else suitLabel club; end endSolidity是区域面积与凸包面积之比心形因为上方凹口Solidity 会比菱形低这里要实测。我拿标准牌测的结果是红桃 Solidity 约 0.88方块约 0.82所以用 0.85 做阈值。但这个值随牌面字体变化建议先跑一批样本统计一下。Eccentricity对黑桃和梅花区分度不高更稳的方法是数瓣数梅花二值化后通常有 3 个连通域如果瓣之间断开黑桃只有 1 个。用bwconncomp的NumObjects判断大于 2 就是梅花。4.3 颜色与形状冲突时的决策逻辑实际跑的时候会遇到颜色和形状判断矛盾的情况比如红色区域形状像黑桃或者黑色区域形状像红桃。这通常是预处理出了问题不是分类器的问题。我的处理逻辑是颜色判断优先因为颜色特征比形状特征稳定如果颜色置信度低redRatio 在 0.1 到 0.2 之间再参考形状。另外如果regionprops返回空二值化后没有连通域说明花色区域裁偏了或者图像太模糊这时候直接返回unknown不要硬猜。硬猜的代价是后面整条 pipeline 的输出都不可信。% 颜色与形状冲突时的降级策略 if isempty(props) suitLabel unknown; elseif redRatio 0.1 redRatio 0.2 % 颜色不确定用形状兜底 if props(1).Solidity 0.85 suitLabel heart; else suitLabel diamond; end else % 正常路径 suitLabel decideByColorAndShape(colorLabel, props); end这段逻辑的核心是「不确定时降级不硬猜」。unknown标签在后续统计里可以单独处理比如人工复核。很多教程不写降级逻辑导致遇到模糊牌时输出随机结果反而更难排查。5. 避坑与排查扑克牌识别里最容易翻车的五个地方5.1 现象二值化后牌面全是黑色轮廓检测找不到牌原因通常是 Otsu 全局阈值把白色牌面判成了背景因为牌面白色区域占比太大Otsu 认为白色是「多数派」自动把阈值推到极端。解决方法是改用自适应阈值imbinarize(I,adaptive)或者手动指定阈值imbinarize(I, 0.6)。如果图像本身偏暗先做imadjust拉伸对比度再二值化。判断标准用imshowpair看二值图和原图如果牌面区域和背景混在一起就是阈值问题。5.2 现象数字识别把 6 认成 9把 3 认成 8这是模板匹配和 OCR 的共同痛点。6 和 9 是旋转关系3 和 8 是闭合区域数量不同。模板匹配对旋转敏感OCR 对笔画粘连敏感。解决办法对 6 和 9先判断数字区域的上下半部分像素密度6 的下半部分更密9 的上半部分更密对 3 和 8数闭合区域个数8 有两个孔3 没有。这些后处理规则比换分类器更有效。如果误判率超过 10%检查模板是不是从同一副牌上采集的不同牌厂的字体差异会导致模板匹配全面失效。5.3 现象花色识别在红色牌上准确黑色牌上全错原因是 HSV 阈值只调了红色没调黑色。黑色在 HSV 里 S 和 V 都低但如果图像有阴影阴影区域的 V 也低会被误判成黑色花色。解决办法黑色判断加一个 S 的上限S 0.4同时用bwareaopen去掉小面积阴影区域。另外黑色花色的二值化要用ForegroundPolarity设为dark因为黑桃和梅花是暗色前景。如果黑色牌识别率突然下降先看suitRegion的imshow确认裁剪区域没有偏移。5.4 现象MatLab 2023 中文注释乱码导致脚本报错这是编码问题不是代码问题。MatLab 2023 及更早版本在中文 Windows 上默认用 GBK 编码如果脚本文件是 UTF-8注释里的中文会变成乱码严重时导致%后面的内容被解析成代码。解决办法在脚本第一行加feature(DefaultCharacterSet, UTF-8)或者把脚本另存为 GBK 编码。如果已经乱码用matlab.editor的设置里改默认编码为 UTF-8。这个问题在 MatLab 2025 之后版本有所改善但如果你还在用 2023b建议养成加编码声明的习惯。5.5 现象换一副牌后识别率从 90% 掉到 50%这是泛化问题根源是模板和阈值都是针对特定牌面调的。解决办法模板匹配改成 HOG SVM训练数据覆盖至少三种不同牌厂颜色阈值不要写死用graythresh或multithresh自适应计算形状特征的阈值用统计方法确定比如跑 100 张样本取均值和标准差阈值设成均值加减两倍标准差。如果不想重新训练至少把模板匹配的相似度阈值从 0.7 降到 0.5让更多候选进入后处理再用规则筛选。6. 把识别结果串成完整 pipeline从单张图到批量测试的工程化技巧前面几章拆开讲了预处理、数字识别、花色识别和避坑这一章讲怎么把它们串成一个可复用的函数以及怎么验证整套系统的真实准确率。我一般会写一个主函数recognizePokerCard(img)输入是原始图像输出是一个结构体包含number、suit、confidence三个字段。内部按顺序调用预处理、定位、分割、识别四个模块每个模块的输出都存到结构体里方便出错时回溯。这样做的好处是当识别错误时你可以直接看中间结果判断是定位偏了还是分类器错了而不是面对一个黑匣子干瞪眼。function result recognizePokerCard(img) % 主流程预处理 - 定位 - 分割 - 识别 result struct(number, , suit, , confidence, 0); % 第一步预处理 [bw, gray] preprocessImage(img); % 第二步定位单张牌 bbox locateCard(bw); if isempty(bbox) result.number unknown; result.suit unknown; return; end % 第三步透视校正与区域分割 cardImg imcrop(img, bbox); [numRegion, suitRegion] splitCardRegions(cardImg); % 第四步数字识别 [result.number, numConf] recognizeNumber(numRegion); % 第五步花色识别 [result.suit, suitConf] recognizeSuit(suitRegion); % 综合置信度 result.confidence min(numConf, suitConf); end这个主函数的逻辑是「每一步都可单独测试」。preprocessImage返回二值图和灰度图locateCard返回边界框splitCardRegions返回两个小图recognizeNumber和recognizeSuit各自返回标签和置信度。置信度用相似度或分类器概率取两者较小值作为整体置信度。如果置信度低于 0.6建议人工复核。批量测试时把所有测试图放在一个文件夹里用imageDatastore遍历统计每个类别的准确率和混淆矩阵。% 批量测试与准确率统计 imds imageDatastore(test_images, IncludeSubfolders, true, ... LabelSource, foldernames); numCorrect 0; confMat zeros(13, 13); % 数字混淆矩阵 for i 1:numel(imds.Files) img readimage(imds, i); result recognizePokerCard(img); trueLabel imds.Labels(i); % 解析真实标签和预测标签更新混淆矩阵 % ...标签映射代码略 end accuracy numCorrect / numel(imds.Files); fprintf(整体准确率%.2f%%\n, accuracy * 100);批量测试的关键是标签格式要统一。我一般把测试图按「数字_花色」命名比如A_heart.jpg、10_spade.jpg然后用imageDatastore的LabelSource自动提取标签。混淆矩阵用confusionmat生成重点看哪些类别互相误判。如果某个类别准确率特别低回到对应章节查参数。这套流程跑通之后你可以把recognizePokerCard封装成 App Designer 界面或者用videoinput接摄像头做实时识别。实时场景下模板匹配比 OCR 快HOG SVM 居中OCR 最慢但泛化最好。选哪个取决于你的帧率和精度要求。最后说一个我踩过的坑不要试图用一套参数通吃所有场景。室内暖光和室外冷光下的白平衡差异能让 HSV 阈值完全失效。我的习惯是每换一个拍摄环境先拍 10 张样本跑一遍preprocessImage用imshow看二值化效果确认没问题再跑全量。这个习惯帮我省了无数个 debug 的夜晚。希望帮到你。本文还有配套的精品资源点击获取
返回列表