简介:面向初学者的 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 打开命令行 cmd,Mac 打开 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(f"templates/{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:y+h, x:x+w] 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(f"templates/{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:y+h, x:x+w] 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(key=lambda r: r[0]) # 然后再逐个识别 for x, y, w, h in rects: roi = thresh[y:y+h, x:x+w] # ... 模板匹配逻辑这个技巧在实际项目中经常用,比如识别一排数字后要按顺序输出成字符串,排序这步必不可少。
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 的视频其实就是高速连续读取图片,之前的图片处理流程直接搬到循环里就能用。你如果用手机拍一张白纸上的数字,在镜头前晃动,观察程序如何实时反馈,会比静态图片有意思得多。从学模板匹配到现在,我每次拿到新图片,都强制自己先走一遍“灰度、二值、看中间结果”这套流程,不跳过任何一步。这个习惯帮我避开了无数次翻车,希望帮到你。
本文还有配套的精品资源,点击获取