简介:这份资源面向具备一定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找到牌面的四个角,然后做透视变换fitgeotrans+imwarp,把牌校正成正视图。校正之后,数字区和花色区的坐标就固定了,直接按比例裁剪即可。
% 透视校正后按比例裁剪数字区和花色区 % 假设已经通过角点检测得到校正后的牌面图像 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(方向梯度直方图),分类器用 SVM,MatLab 里都有现成函数。流程是:收集每个数字的样本图各 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 的夜晚。希望帮到你。
本文还有配套的精品资源,点击获取