OpenCV数字识别入门:从零搭建环境到模板匹配实战

📅 发布时间:2026/10/16 18:16:21
OpenCV数字识别入门:从零搭建环境到模板匹配实战
简介面向初学者的 OpenCV 数字识别示例包适用于刚接触计算机视觉、希望从实际代码入手理解印刷体数字识别流程的开发者。资源围绕 recognizeOCR.cpp 展开涵盖图像灰度化、二值化、平滑滤波等预处理思路并结合模板匹配与 OCR 识别的基本方法帮助读者在 Linux 环境下快速跑通一个简洁的数字识别 demo。压缩包共 13 个文件以 10 张 jpg 测试图像和 2 张 png 样例图为主另有 1 个 cpp 源码文件包体仅 21KB结构轻量适合直接阅读和调试。已有 357 人学习下载。通过该资源可获得的不仅是可运行的 C 代码还包括配套的输入图像与典型识别流程展示能直观看到预处理、匹配或识别结果之间的对应关系。对于想了解 OpenCV 基础 API 调用、模板匹配原理或初步涉足 OCR 的读者是一份小巧实用的起步材料。1. 先搞懂一件事OpenCV 数字识别到底是什么拿到一张写着数字的图片不管是快递单上的手写号码、答题卡上的填涂标记还是电脑截屏里的一串验证码你想让电脑自己把里面的数字读出来——这个需求看起来有点“玄学”但正是 OpenCV 数字识别要做的事也是最适合零基础新手入门的图像处理项目。先拆开这个词OpenCV 是一个开源的计算机视觉库你可以把它理解成“给电脑装了一双眼睛再配一套处理图像的工具箱”。数字识别就是这套工具箱里最基础、也最能让你快速获得成就感的一个场景。它不需要你懂什么高深的数学只需要一张图、几行代码就能看到电脑真的在“认字”。这篇文章面向完全没接触过 OpenCV 的初学者带你从环境搭建到第一个程序跑通全程不碰复杂的原理只讲你动手需要知道的东西。2. 先想清楚再动手为什么要从数字开始很多零基础的人一上来就想做“人脸识别”“自动驾驶”结果被复杂的安装和报错劝退。数字识别是图像处理里最顺的一条入门线目标明确、反馈直观、出错了也容易排查。你只需要让电脑认 0 到 9 这十个数字做完这一步再去碰车牌识别、答题卡阅卷这些场景会发现流程几乎一模一样。2.1 数字识别为什么是最好的第一课先看数字本身的特点它结构简单总共就 10 个符号形状相对固定不像人脸那样千变万化而且应用场景特别常见。你随便找一张带数字的图片比如超市小票、快递面单、书本页码都可以拿来做实验。数字识别解决的是一个图像处理里的核心问题怎么把“图片里的信息”变成“程序能理解的数据”。这一步想通了后面学什么都快。比如我做模拟项目 X 的时候最开始只让程序识别一张图片里的几个数字后来把同样的流程改一改就做出了一个能统计答题卡选项的小工具。区别只是把“数字轮廓”换成“填涂块轮廓”而已。所以我的建议是不要一上来就追着“深度学习”“目标检测”这些大词跑先把 OpenCV 对图片的基本操作练熟用数字识别作为你的第一个里程碑。它花的时间少产出却能让你真切感受到“我在控制电脑看东西”这种正反馈对新手来说太重要了。2.2 搭建环境一次装好少踩十次坑环境搭建是新手最容易心态炸裂的环节。先明确一点你只需要安装 Python 和 OpenCV 库不需要安装庞大的 IDE用记事本也行。为了少走弯路我第一次学的时候推荐这套流程。打开终端Windows 打开命令行 cmdMac 打开 Terminal先确认 Python 装好了没有输入python --version有版本号出现比如 Python 3.9.7说明环境没问题。如果提示找不到命令去 Python 官网下载安装包安装时记得勾选“Add Python to PATH”选项。这一步没勾选后面会很痛苦。接下来安装 OpenCV一键完成pip install opencv-python如果你还在国内网络环境下下载速度慢得像蜗牛可以加一个镜像源参数速度会立刻快起来pip install opencv-python -i https://pypi.tuna.tsinghua.edu.cn/simple安装完成后打开 Python 交互式命令行输入下面这行做自检import cv2 print(cv2.__version__)会输出一串数字比如 4.5.5就说明 OpenCV 装好了。这里要留意一个常见的坑如果你安装的是 opencv-python那么导入的模块名是 cv2不是 opencv少个 2 就会报 ModuleNotFoundError。很多人第一次都栽在这。2.3 认识图片的三层身份像素、灰度、二值装好环境之后先别急着写代码花五分钟理解一个概念图片在 OpenCV 眼里到底是什么。这个问题搞不明白后面看代码就像看天书。任何图片都可以拆成密密麻麻的小方格每个方格叫一个像素。一张 100x100 像素的图片就是横向 100 个格、纵向 100 个格。每个像素的颜色在电脑里是由三原色组成在 OpenCV 里顺序是 BGR也就是蓝色、绿色、红色每个值在 0 到 255 之间。但做数字识别的时候绝大多数情况下我们不需要颜色信息只关心形状。于是要把彩色图变成灰度图每个像素只剩一个亮度值0 是纯黑255 是纯白。到了更后面一步我们还会把灰度图变成二值图像素要么是 0黑要么是 255白没有中间值。这样数字和背景就被彻底切开了电脑找轮廓时好找得多。我用一个比喻帮你记彩色图是一张高清照片灰度图是把照片印成黑白报纸二值图是把报纸上的内容提炼成纯黑白的剪报。处理步骤越往后信息越纯粹计算量也越小。数字识别要做的第一件事就是完成这三层转变。提示OpenCV 读图时有个小陷阱——它读进来的颜色顺序是 BGR不是常规的 RGB。如果你用别的库把图片存下来再比对颜色值会发现红蓝是反的。但做数字识别只关心轮廓和灰度这个差异影响不大。3. 第一个例子让电脑把图片里的数字认出来从这一章开始进入正题。我们会写一个完整的程序它的任务是读取一张图片把里面所有数字用绿色框框出来再在框上方显示出识别的结果。目标是三步走每一段代码都是可以独立运行、立刻看到效果的。3.1 先拆解识别流程电脑认数字的四步我先带你用大白话梳理一遍电脑“看数字”的完整流程。后面你写代码时就是在照着这个流程一步步执行。第一步找数字在哪。图片里有背景、有文字、可能有各种杂物电脑需要先把颜色相近、连在一起的前景区域揪出来。第二步把数字单独切出来。第二步把数字所在的小区域拆分成一个一个单独的字符。第三步和已知的模板比较。就像查字典一样拿切出来的图案和 0 到 9 的参考图逐一比对看谁最像。第四步输出结果。用图像处理的行话来说这四个步骤对应的是灰度化、二值化、找轮廓、模板匹配。你不需要现在就完全理解每个术语往下看代码就懂了。我第一次学的时候只知道照着例子敲敲完再返回来看流程一下就通透了。3.2 读取图片并转成灰度图先做第一步把彩色图片读进来转成灰度图。这一步的作用是去掉颜色干扰只保留明暗信息为后面的二值化做铺垫。import cv2 # 请把路径换成你自己的图片文件路径 image cv2.imread(numbers.jpg) print(图片尺寸, image.shape) # 转灰度图 gray cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) cv2.imshow(gray, gray) # 按任意键关闭窗口顺便等一下不然窗口一闪就没 cv2.waitKey(0) cv2.destroyAllWindows()代码里的 imread 负责读图片它返回的是一个多维数组你可以把它理解成一个三维矩阵高、宽、颜色通道。如果你 print 出来的 shape 里有 3 个数字说明图片读成功了如果打印出来是 None说明图片没读进来原因基本都是路径写错了。cvtColor 函数起转换作用参数里的 COLOR_BGR2GRAY 就表示“把 BGR 彩色图转成灰度图”。执行完弹出窗口显示的就是一张黑白照片。注意 waitKey(0) 这行是让窗口挂起等待等你按任意键再往下执行。刚学的时候忘了写这行窗口会瞬间消失你会以为自己程序写错了其实只是程序跑完了自动关闭而已。3.3 二值化让数字变成纯黑纯白灰度图虽然去掉了颜色但每个像素还有从 0 到 255 的很多种取值。二值化要让这个区间变成只有两种取值把前景和背景截然分开。import cv2 image cv2.imread(numbers.jpg) gray cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # 使用大津算法自动计算阈值并把前景和背景反转 _, thresh cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU) cv2.imshow(thresh, thresh) cv2.waitKey(0) cv2.destroyAllWindows()这段代码里threshold 函数的参数值得你花两分钟琢磨。第一个参数是输入的灰度图第二个参数 0 表示阈值由后面的大津算法自动计算新手不用手动调第三个参数 255 表示超过阈值的像素会被设为 255第四个参数是关键。THRESH_BINARY_INV 的意思是“反相二值化”它会把原本偏亮的背景变成黑色暗色的数字变成白色。为什么要反相因为 OpenCV 找轮廓时会优先找白色的前景区域数字需要保持白色。THRESH_OTSU 是自动计算最佳阈值的算法它能根据图片的明暗分布自己找到那个“分界线”你不用对着不同图片反复调参数。跑完后你看到的效果应该是背景全黑、数字纯白轮廓清晰。提示如果看到的画面反了背景白数字黑检查一下是不是用了 THRESH_BINARY 而不是 THRESH_BINARY_INV。这两个差一个 INV结果显示完全相反是新手最常遇到的情况。3.4 找轮廓把每个数字单独框出来二值图准备好以后下一步是找出所有连在一起的白色区域这在图像处理里叫轮廓查找。每个数字都是由一段白色像素连在一起的只要找到这些连通区域就能拿到每个数字在图片里的位置。import cv2 image cv2.imread(numbers.jpg) gray cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) _, thresh cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU) # 找出所有轮廓 contours, hierarchy cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # 遍历每个轮廓画矩形框并打印宽度和高度 for cnt in contours: x, y, w, h cv2.boundingRect(cnt) if w 10 and h 10: cv2.rectangle(image, (x, y), (x w, y h), (0, 255, 0), 2) print(发现一个区域位置, x, y, 大小, w, h) cv2.imshow(result, image) cv2.waitKey(0) cv2.destroyAllWindows()这段代码里findContours 返回两个值contours 是轮廓列表hierarchy 是层级关系。注意 OpenCV 的版本差异在 OpenCV 3 及以后的版本里它返回两个值你按两个值接收就行如果你用的是老版本资料它可能返回三个值造成“函数返回的值数量不对”的报错。boundingRect 会把轮廓转成一个矩形框的四个参数x、y 是左上角的坐标w 是宽度h 是高度。后面加了 if w 10 and h 10 这个过滤条件是为了排除那些只有一个点的小噪点。图片里容易有一些零星的白色噪点它们大小远小于真实数字通过尺寸过滤可以避免误框。如果画面里数字很大可以把阈值调大到 30 或 50效果更稳。3.5 模板匹配判断框里是哪个数字前面的步骤已经成功定位了数字但程序还不知道它们分别是什么。模板匹配的思路是准备 10 张参考图每张上面只有一个数字然后把切出来的区域和所有模板一张一张比对得分最高的就是答案。import cv2 image cv2.imread(numbers.jpg) gray cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) _, thresh cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU) contours, hierarchy cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # 加载数字模板模板文件命名0.png, 1.png ... 9.png digit_templates [] for i in range(10): tpl cv2.imread(ftemplates/{i}.png, 0) digit_templates.append(tpl) for cnt in contours: x, y, w, h cv2.boundingRect(cnt) if w 10 or h 10: continue # 从二值图上切出数字区域 roi thresh[y:yh, x:xw] best_score -1 best_digit -1 # 分别和 10 个模板做比对 for i, tpl in enumerate(digit_templates): # 调整切出的区域尺寸让它和模板一样宽高再开始匹配 tpl_resized cv2.resize(tpl, (w, h)) result cv2.matchTemplate(roi, tpl_resized, cv2.TM_CCOEFF_NORMED) _, max_val, _, _ cv2.minMaxLoc(result) if max_val best_score: best_score max_val best_digit i # 分数大于 0.7 才认为可信太低了说明可能切到了噪点 if best_score 0.7: cv2.putText(image, str(best_digit), (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2) cv2.rectangle(image, (x, y), (x w, y h), (0, 255, 0), 2)这段代码是模板匹配的核心。你需要注意几个细节第一模板图和待识别图片要用相同的方式处理。我在加载模板时用了cv2.imread(..., 0)后面的 0 表示直接以灰度模式读入而之前待识别图片也转成了灰度并做了二值化。如果你用直接读入的彩色模板去做匹配分数会很低识别自然不准。第二matchTemplate 要求模板尺寸不大于输入图。我用 resize 把每个模板缩放到和切出的区域一样大确保无论数字大小都能正常比对。3.6 组合完整代码一版能直接跑的识别程序把前面几段代码合起来就是一个完整的数字识别程序。准备一张内容为数字的图片命名为 numbers.jpg存放在和代码同一个目录下再准备一个 templates 文件夹里面放 0.png 到 9.png 共 10 张标准模板图片。模板图片可以用系统自带的字体生成也可以从任意一张数字图片里手动裁剪出来。import cv2 # 1. 读图 image cv2.imread(numbers.jpg) gray cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # 2. 二值化 _, thresh cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU) # 3. 找轮廓 contours, hierarchy cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # 4. 加载模板 digit_templates [] for i in range(10): tpl cv2.imread(ftemplates/{i}.png, 0) digit_templates.append(tpl) # 5. 识别每个数字 for cnt in contours: x, y, w, h cv2.boundingRect(cnt) if w 10 or h 10: continue roi thresh[y:yh, x:xw] best_score -1 best_digit -1 for i, tpl in enumerate(digit_templates): tpl_resized cv2.resize(tpl, (w, h)) result cv2.matchTemplate(roi, tpl_resized, cv2.TM_CCOEFF_NORMED) _, max_val, _, _ cv2.minMaxLoc(result) if max_val best_score: best_score max_val best_digit i if best_score 0.7: cv2.putText(image, str(best_digit), (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2) cv2.rectangle(image, (x, y), (x w, y h), (0, 255, 0), 2) # 6. 展示结果 cv2.imshow(result, image) cv2.waitKey(0) cv2.destroyAllWindows()运行这个程序后你会看到一个弹窗图片上每个数字都被绿框圈住绿框左上角标着红字识别的结果。如果一切顺利你已经做到了“让电脑读图片里的数字”。哪怕识别错了也没关系——你要关注的是整个流程跑通的感觉。修改匹配分数阈值 0.7可以把识别要求调严或调松分数越高程序越保守只认极其相似的图案分数越低越容易把不像的也强行判成一个数字。4. 新手避坑五个翻车现场与自救办法零基础入门 OpenCV绕不开的是各种莫名其妙的报错和匪夷所思的结果。这一章把这些坑集中说透每个都按“现象、原因、解决”的思路写你遇到任何一种情况都能直接回来对照。4.1 图片路径报错读完结果全是 None现象是代码第一行读取图片时print(image.shape) 直接报 AttributeError或者程序跑起来但窗口里一片黑。原因最常见的是路径写错了。imread 读不到文件时不会报错只会默默返回一个空值导致后面访问 shape 时才爆炸。另一个原因是路径里有中文某些版本的 OpenCV 对中文路径支持不友好即使文件明明存在也读不出来。解决方式是先用import os; print(os.path.exists(numbers.jpg))检查文件是否存在。如果存在却读不出来把图片和代码放在同一个目录或者把路径改成绝对路径并确保路径里没有中文字符。把文件名改成纯英文就能绕开大部分路径问题。4.2 二值化结果颜色反了现象是二值化之后数字是黑色、背景是白色整个画面像底片。原因是 threshold 函数的标志位用成了 THRESH_BINARY而 OpenCV 找轮廓时更希望前景是白色。判断哪边是前景取决于你的图片里数字是什么颜色。如果数字是深色的用 THRESH_BINARY_INV 反转如果数字本身就是亮色背景暗色那直接用 THRESH_BINARY 就行。解决方式是两种方式都跑一次观察哪个结果让你能清楚看到数字的轮廓就选哪种。不要背参数以显示效果为准。4.3 findContours 解包报错返回值数量对不上现象是contours, hierarchy cv2.findContours(...)这行报 ValueError: not enough values to unpack。原因是不同版本的 OpenCV 返回值不同。旧版本的 findContours 返回三个值image、contours、hierarchy新版本3.x 之后只返回两个contours、hierarchy。你用的代码是照着旧版教程抄的自然会报错。解决方式很简单如果报错说“需要两个值但收到了三个”就改成contours, hierarchy, _ cv2.findContours(...)不过这种写法在新版本又会报错。更稳的办法是先查一下当前 OpenCV 版本再决定写法。如果嫌麻烦直接用两个值接收这是新版的标准写法。另外一个解决思路是在代码里加一行判断但新手阶段先确保你按自己安装的版本匹配写法。4.4 模板匹配分数低识别结果全错现象是程序能画出框但框上标的数字完全不匹配预览里也看不出逻辑。原因通常是模板图和待识别图片预处理不一致或者字体、大小差异太大。模板匹配对形状非常灵敏你拿一个宋体的数字模板去匹配黑体的数字得分会很低。还有就是模板里有噪点或者没有二值化导致匹配时被背景干扰。解决方式是在模板生成阶段就保证和待识别图片笔画粗细一致、字体接近。可以把模板图片也用同样的灰度、二值化、轮廓切割流程处理一遍再把得到的小图存成模板。这样做出来的模板和待识别图片处于同一个“状态”匹配成功率高出很多。注意模板匹配的适用场景是同一字体、同一风格的印刷体数字。如果你要识别歪歪扭扭的手写数字不要过度指望这套方案那需要模型学习的知识在最后一章里我再给出路线。4.5 识别顺序不对数字从左到右乱序现象是识别出的数字是对的但输出顺序随机比如图片上明明是 123程序可能输出 321。原因是 findContours 返回的轮廓顺序并不保证从左到右。它按图像扫描顺序排列且与连通区域的出现顺序有关。你如果想要按阅读顺序输出结果得自己对轮廓排序。解决方式是在遍历前给轮廓按 x 坐标排序让它们从左到右排列# 先提取所有轮廓的矩形信息 rects [] for cnt in contours: x, y, w, h cv2.boundingRect(cnt) if w 10 and h 10: rects.append((x, y, w, h)) # 按 x 坐标从小到大排序 rects.sort(keylambda r: r[0]) # 然后再逐个识别 for x, y, w, h in rects: roi thresh[y:yh, x:xw] # ... 模板匹配逻辑这个技巧在实际项目中经常用比如识别一排数字后要按顺序输出成字符串排序这步必不可少。5. 下一步往哪走选一条合适的进阶路线跑通了模板匹配你已经熟悉了图像处理的基本流程读图、灰度、二值、轮廓、匹配。下一步不要急着学一堆新库先把这条路线在更多图片上做“压力测试”你会发现它对付印刷体很稳但遇到手写体就崩。这不是你的代码问题而是方案本身的边界。5.1 三条路线怎么选如果你多数场景是印刷体数字比如截图里的验证码、仪表盘读数把模板匹配练熟就够用。你可以试着把模板换个大字体或者加入旋转矫正让程序在更复杂的情况下依然能稳定工作。如果你想识别手写数字下一步建议学 KNN 分类器。OpenCV 自带一个手写数字数据集里面有几万个样本你只要把前面学到的灰度、二值、轮廓切图方法复用一遍把切出的图片转成特征向量丢给 KNN 训练就能得到比模板匹配灵活得多的识别效果。如果你有耐心再往后可以接触深度学习里的卷积神经网络用 MNIST 数据集训练一个手写数字识别模型。那时你会发现前面学的预处理知识依然在发挥作用只是最后的“匹配”步骤换成了模型推理。5.2 一个立刻能练的小技巧摄像头实时识别想从“处理图片”升级到“处理视频流”最简单的做法是把 imread 换成 VideoCapture。第一次跑通实时识别时你会明显感觉自己在掌控一个真实系统。import cv2 # 打开电脑摄像头 cap cv2.VideoCapture(0) while True: ret, frame cap.read() if not ret: break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) _, thresh cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU) # 把 thresh 传入轮廓识别流程 # 画框并显示结果 cv2.imshow(camera, thresh) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()cap.read() 每次从摄像头取一帧画面ret 表示是否成功读取。real-time 的视频其实就是高速连续读取图片之前的图片处理流程直接搬到循环里就能用。你如果用手机拍一张白纸上的数字在镜头前晃动观察程序如何实时反馈会比静态图片有意思得多。从学模板匹配到现在我每次拿到新图片都强制自己先走一遍“灰度、二值、看中间结果”这套流程不跳过任何一步。这个习惯帮我避开了无数次翻车希望帮到你。本文还有配套的精品资源点击获取