前言
光学字符识别(Optical Character Recognition, OCR)是计算机视觉领域的核心应用方向之一,广泛落地于金融身份核验、政务单据录入、工业仪表读数、物流面单识别等场景。当前主流的工业级OCR方案多基于深度学习实现,如PaddleOCR、Tesseract-LSTM等,具备极强的泛化能力,可适配多字体、多场景、多语言的识别需求。
但深度学习方案也存在一定门槛:需要标注数据训练、模型体积大、部署算力要求高、调试逻辑不直观。对于固定字体、固定场景的纯数字识别 任务,传统计算机视觉的模板匹配法反而具备独特优势:原理简单易懂、无需训练、代码轻量、运行速度极快,非常适合CV入门学习者系统掌握图像预处理、形态学操作、轮廓检测、特征匹配等核心基础技能。
本文将通过两个完整实战项目------银行卡号识别 与身份证号识别,从零到一拆解基于模板匹配的数字OCR实现全流程。全文涵盖环境搭建、核心原理详解、逐行代码逻辑讲解、参数调优思路、常见问题排查与进阶优化方案,附完整可运行代码,帮助读者彻底掌握传统OCR的核心方法论,并能举一反三迁移到其他数字识别场景。
一、环境准备与依赖安装
1.1 开发环境说明
本项目基于Python + OpenCV实现,环境配置要求如下:
- Python版本:3.7及以上(本文演示使用Python 3.11)
- OpenCV版本:4.x 系列(兼容3.x系列,代码已做兼容处理)
- 操作系统:Windows/Linux/MacOS 均可运行
1.2 核心库安装
项目依赖两个核心第三方库,通过pip即可快速安装:
bash
pip install opencv-python numpy
opencv-python:核心图像处理库,提供图像读写、滤波、形态学操作、轮廓检测、模板匹配等全套APInumpy:数值计算库,用于图像矩阵运算、匹配分数计算等
1.3 自定义工具模块myutils实现
两段核心代码均引用了自定义工具包myutils,这是很多初学者的高频踩坑点------该模块并非Python官方或OpenCV自带,需要我们自行实现。模块封装了两个高频复用的工具函数:轮廓排序函数、等比例图像缩放函数。
在项目目录下创建myutils.py文件,写入以下代码:
python
import cv2
def sort_contours(cnts, method="left-to-right"):
"""
对轮廓按指定方向排序
:param cnts: 输入的轮廓列表
:param method: 排序方式,支持left-to-right/right-to-left/top-to-bottom/bottom-to-top
:return: 排序后的轮廓列表、对应的外接矩形列表
"""
reverse = False
i = 0 # 0对应x坐标,1对应y坐标
# 根据排序方式设置反转标记与排序维度
if method == "right-to-left" or method == "bottom-to-top":
reverse = True
if method == "top-to-bottom" or method == "bottom-to-top":
i = 1
# 计算每个轮廓的外接矩形,作为排序依据
boundingBoxes = [cv2.boundingRect(c) for c in cnts]
# 按外接矩形的指定坐标排序,同时绑定轮廓和矩形
(cnts, boundingBoxes) = zip(*sorted(zip(cnts, boundingBoxes),
key=lambda b: b[1][i], reverse=reverse))
return cnts, boundingBoxes
def resize(image, width=None, height=None, inter=cv2.INTER_AREA):
"""
图像等比例缩放,避免变形
:param image: 输入图像
:param width: 目标宽度,传None则按高度等比例缩放
:param height: 目标高度,传None则按宽度等比例缩放
:param inter: 插值方法,默认INTER_AREA适合缩小
:return: 缩放后的图像
"""
dim = None
(h, w) = image.shape[:2]
# 宽高都不传则直接返回原图
if width is None and height is None:
return image
# 按高度缩放
if width is None:
r = height / float(h)
dim = (int(w * r), height)
# 按宽度缩放
else:
r = width / float(w)
dim = (width, int(h * r))
resized = cv2.resize(image, dim, interpolation=inter)
return resized
sort_contours:解决轮廓检测结果无序的问题。cv2.findContours返回的轮廓顺序是随机的,而数字识别严格依赖从左到右的顺序,因此必须按x坐标排序。resize:保证图像缩放时宽高比不变,避免数字变形导致匹配准确率下降。
二、核心技术原理详解
2.1 模板匹配OCR的核心思想
模板匹配是最直观的字符识别思路,本质是特征比对,核心流程分为三步:
- 构建模板库:提前准备0-9十个数字的标准模板图像,每个模板对应唯一数字标签;
- 分割待识别字符:对输入图像做预处理,定位数字区域,再分割出单个字符;
- 相似度匹配:将单个字符与所有模板逐一计算相似度,取相似度最高的模板标签作为识别结果。
该方法的核心前提是:待识别数字的字体、风格与模板高度一致。因此它非常适合标准化场景,比如银行卡OCR-A字体、身份证固定印刷字体、工业仪表数码管字体等。
2.2 图像预处理核心技术
数字识别的准确率,80%取决于预处理效果。本项目用到的核心预处理技术如下:
2.2.1 灰度化与二值化
- 灰度化:将三通道BGR彩色图像转换为单通道灰度图像。数字识别只依赖形状特征,颜色信息无价值,灰度化可将数据量降至原来的1/3,大幅提升计算效率。
- 二值化 :将灰度图转换为只有纯黑(0)和纯白(255)两种像素的图像,实现前景(数字)与背景的彻底分离。
- 全局阈值二值化:手动指定一个阈值,大于阈值设为255,小于设为0。
- OTSU自适应阈值:自动计算图像的最优分割阈值,适合直方图呈双峰分布的图像,无需手动调参。
- 反二值化(THRESH_BINARY_INV):阈值反转,即大于阈值设为0,小于设为255。核心目标是统一输出黑底白字的二值图------OpenCV的轮廓检测默认提取白色区域的轮廓。
2.2.2 形态学操作
形态学操作是基于结构元素的图像形状处理,是数字区域增强、连通的核心手段,基础是腐蚀与膨胀。
- 腐蚀(Erosion):用结构元素扫描图像,取邻域内像素最小值作为输出。效果是白色区域缩小,可去除细小的白噪声、分离粘连的物体。
- 膨胀(Dilation):取邻域内像素最大值作为输出。效果是白色区域扩大,可填充物体内部孔洞、连接相邻的碎片区域。
基于腐蚀和膨胀组合出更实用的高级操作:
- 开运算:先腐蚀后膨胀。作用是去除小的亮斑、平滑物体边缘,不改变物体整体大小。
- 闭运算:先膨胀后腐蚀。作用是填充物体内部小黑洞、连接相邻的近邻物体,平滑边缘。
- 顶帽运算(Top Hat) :原图 - 开运算结果。作用是提取图像中比周围背景更亮的细小区域,非常适合在复杂背景下突出数字、文字等亮前景。
- 黑帽运算(Black Hat):闭运算 - 原图。作用是提取比周围背景更暗的细小区域。
本项目中,我们使用矩形结构元素适配数字的长方形形状,通过顶帽增强数字,通过闭运算连通数字笔画与分组区域。
2.2.3 轮廓检测与筛选
轮廓检测是数字区域定位的核心。cv2.findContours从二值图像中提取所有白色前景物体的外轮廓,关键参数:
mode:轮廓检索模式,RETR_EXTERNAL表示只提取最外层轮廓,忽略内部孔洞,适合数字区域定位。method:轮廓逼近方法,CHAIN_APPROX_SIMPLE会压缩轮廓的冗余点,只保留端点,大幅节省内存。
版本兼容技巧:OpenCV 3.x 中
findContours返回3个值(图像、轮廓、层级),OpenCV 4.x 返回2个值(轮廓、层级)。使用cv2.findContours(...)[-2]可同时兼容两个版本,直接获取轮廓列表。
提取轮廓后,通过宽高比(Aspect Ratio, AR)和尺寸范围筛选出目标数字区域,这是排除噪声、定位ROI的核心手段。宽高比 = 轮廓宽度 / 轮廓高度,不同布局的数字区域有稳定的宽高比范围。
2.3 模板匹配算法原理
OpenCV提供cv2.matchTemplate实现模板匹配,原理是将模板图像在待匹配图像上逐像素滑动,计算每个位置的匹配相似度,最终返回一张相似度热力图。
常用的三种匹配方法:
| 匹配方法 | 说明 | 匹配最优值 |
|---|---|---|
TM_SQDIFF |
平方差匹配,计算像素差的平方和 | 值越小越匹配,最小值为最优 |
TM_CCORR |
相关匹配,计算像素点积和 | 值越大越匹配,最大值为最优 |
TM_CCOEFF |
相关系数匹配,对图像均值做归一化 | 值越大越匹配,最大值为最优,抗光照干扰能力最强 |
本项目选用TM_CCOEFF方法,对光照变化的鲁棒性更好。匹配后通过cv2.minMaxLoc获取最大值与对应位置,最大值对应的数字模板即为识别结果。
三、项目一:银行卡号数字识别实现
3.1 项目整体流程
银行卡号识别的完整处理链路如下:
- 读取数字模板图像,预处理后提取0-9单个数字模板,构建模板字典;
- 读取银行卡输入图像,尺寸归一化后转为灰度图;
- 通过顶帽运算增强数字区域,闭运算连通数字,二值化得到前景掩码;
- 提取轮廓,通过宽高比筛选出4组卡号分组区域;
- 对每组区域二次分割,得到单个数字轮廓;
- 单个数字与模板逐一匹配,输出识别结果与银行卡类型。
3.2 步骤1:构建数字模板库
模板库是识别的基准,我们使用OCR-A标准字体的0-9横向排列图像(kahao.png)作为模板源。
3.2.1 模板图像读取与预处理
python
# 读取模板图像
img = cv2.imread(args['template'])
# 转为灰度图
ref = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 反二值化:白底黑字 → 黑底白字,数字为白色前景
ref = cv2.threshold(ref, 10, 255, cv2.THRESH_BINARY_INV)[1]
- 阈值设为10是极低阈值,确保所有数字笔画都被转为白色,避免模板数字断裂。
- 反二值化是关键步骤:确保后续轮廓检测提取的是数字本身,而非背景。
3.2.2 轮廓提取与数字排序
python
# 提取最外层轮廓
refCnts = cv2.findContours(ref, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2]
# 按从左到右排序,对应数字0-9的顺序
refCnts = myutils.sort_contours(refCnts, method='left-to-right')[0]
- 模板图中数字从左到右依次是0、1、2...9,但轮廓检测返回顺序随机,必须排序后才能建立正确的数字-模板映射。
3.2.3 模板ROI裁剪与尺寸归一化
python
digits = {} # 键:数字0-9,值:对应的模板图像
for (i, c) in enumerate(refCnts):
# 获取轮廓的外接矩形
(x, y, w, h) = cv2.boundingRect(c)
# 裁剪出单个数字的ROI
roi = ref[y:y+h, x:x+w]
# 统一缩放到57x88,保证所有模板尺寸一致
roi = cv2.resize(roi, (57, 88))
digits[i] = roi
- 57x88是适配数字宽高比的经验尺寸,宽高比约0.65,符合印刷数字的常规比例。
- 尺寸归一化是模板匹配的前提:模板与待匹配图像尺寸必须完全一致,否则无法计算相似度。
3.3 步骤2:银行卡图像预处理
3.3.1 图像尺寸归一化
python
# 读取银行卡图像
image = cv2.imread(args['image'])
# 统一缩放到宽度300,高度等比例缩放
image = myutils.resize(image, width=300)
# 转为灰度图
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
- 统一宽度为300是为了固定后续所有参数的适配范围,避免不同尺寸输入需要反复调参。
3.3.2 顶帽运算增强数字区域
python
# 定义两个形态学核:长方形核适配数字形状,正方形核用于整体闭运算
rectKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (9, 3))
sqKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
# 顶帽运算:突出比背景亮的数字区域,抑制背景纹理
tophat = cv2.morphologyEx(gray, cv2.MORPH_TOPHAT, rectKernel)
- 选择(9,3)的长方形核:数字是横向长方形,宽大于高,对应尺寸的核可以更好地匹配数字形状,精准增强数字区域。
- 银行卡背景通常有花纹、渐变、浮雕等干扰,顶帽运算可以有效过滤大面积背景,只保留高亮的数字笔画。
3.3.3 闭运算连通数字区域
python
# 闭运算:先膨胀后腐蚀,将断裂的数字笔画连接成整体
closeX = cv2.morphologyEx(tophat, cv2.MORPH_CLOSE, rectKernel)
- 顶帽后的数字可能存在笔画断裂,一次闭运算可以填充缝隙,让数字成为完整的连通域。
3.3.4 OTSU自适应二值化
python
# OTSU自动阈值二值化,得到黑白分明的数字掩码
thresh = cv2.threshold(closeX, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
- 传入阈值0表示不手动指定阈值,由OTSU算法自动计算最优分割阈值,适配不同光照的图像。
3.3.5 二次闭运算填充孔洞
python
# 正方形核做第二次闭运算,填充数字内部小孔洞,让区域更完整
thresh = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, sqKernel)
- 第一次闭运算用长核横向连通,第二次用正方形核做整体填充,确保数字区域没有孔洞,提升轮廓检测的完整性。
3.4 步骤3:卡号分组区域定位
银行卡号通常分为4组,每组4位数字,组间有明显间隔,因此我们先定位4个分组矩形,再在组内分割单个数字。
3.4.1 轮廓提取
python
# 提取二值图的所有外轮廓
cnts = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2]
3.4.2 基于宽高比的轮廓筛选
遍历所有轮廓,通过宽高比和绝对尺寸筛选出符合卡号分组特征的区域:
python
locs = []
for c in cnts:
(x, y, w, h) = cv2.boundingRect(c)
ar = w / float(h) # 计算宽高比
# 筛选条件:宽高比2.5~4.0,宽度40~55,高度10~20
if 2.5 < ar < 4.0:
if (40 < w < 55) and (10 < h < 20):
locs.append((x, y, w, h))
# 按x坐标从左到右排序,对应卡号顺序
locs = sorted(locs, key=lambda x: x[0])
- 宽高比2.5~4.0:每组4个数字,加上间距,整体宽度约为高度的3倍,这是4位数字组的典型特征。
- 尺寸范围是基于宽度300的图像的经验值,可根据实际图像尺寸灵活调整。
- 排序后得到的4个分组,从左到右依次对应卡号的四组数字。
3.5 步骤4:单数字分割与模板匹配识别
对每个分组区域,进一步分割出4个单个数字,逐一与模板匹配。
python
output = [] # 存储最终识别的所有数字
for (gX, gY, gW, gH) in locs:
groupOutput = []
# 裁剪分组区域,上下左右各扩展5像素,避免边缘截断数字
group = gray[gY - 5 : gY + gH + 5, gX - 5 : gX + gW + 5]
# 分组区域二值化,再次得到黑底白字的单组数字
group = cv2.threshold(group, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
# 提取组内数字轮廓,从左到右排序
digitCnts = cv2.findContours(group.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2]
digitCnts = myutils.sort_contours(digitCnts, method='left-to-right')[0]
# 遍历每个数字轮廓
for c in digitCnts:
(x, y, w, h) = cv2.boundingRect(c)
roi = group[y:y+h, x:x+w]
# 缩放到与模板一致的57x88
roi = cv2.resize(roi, [57, 88])
# 与10个模板逐一匹配,计算得分
scores = []
for (digit, digitROI) in digits.items():
result = cv2.matchTemplate(roi, digitROI, cv2.TM_CCOEFF)
(_, score, _, _) = cv2.minMaxLoc(result)
scores.append(score)
# 取最高分对应的数字
groupOutput.append(str(np.argmax(scores)))
# 在原图上绘制分组矩形框与识别结果
cv2.rectangle(image, (gX - 5, gY - 5), (gX + gW + 5, gY + gH + 5), (0, 0, 255), 1)
cv2.putText(image, ''.join(groupOutput), (gX, gY - 15),
cv2.FONT_HERSHEY_SIMPLEX, 0.65, (0, 0, 255), 2)
output.extend(groupOutput)
3.6 银行卡类型判定
根据银行卡号首位的发行者标识(Issuer Identification Number, IIN),可以快速判断卡组织:
python
FIRST_NUMBER = {
'3': 'American Express',
'4': 'Visa',
'5': 'MasterCard',
'6': 'Discover Card'
}
print("Credit Card Type: {}".format(FIRST_NUMBER[output[0]]))
print("Credit Card #: {}".format("".join(output)))
- 银行卡号首位有明确的行业与发行方规则,4开头为Visa、5开头为万事达、6开头为发现卡、3开头为美国运通。
3.7 效果展示与结果输出
运行代码后,终端输出如下:
Credit Card Type:Visa
Credit Card #: 4000123456789010
同时弹出图像窗口,显示银行卡原图,卡号区域被红色框标注,上方显示识别出的数字,实现可视化验证。
四、项目二:身份证号数字识别实现
4.1 身份证识别与银行卡识别的差异
身份证号识别的核心逻辑与银行卡完全一致,都是「模板构建→预处理→区域定位→单字分割→模板匹配」,但数字布局特征不同,因此核心参数有两处关键差异:
- 数字布局不同:身份证号是连续18位数字的长条区域,没有分组间隔,因此宽高比远大于银行卡分组;
- 二值化方向不同:身份证印刷数字为黑色、背景为白色,灰度图中数字像素值更低,因此分组区域二值化需要使用反二值化,才能得到黑底白字的前景。
4.2 步骤1:模板库构建
模板库构建逻辑与银行卡识别完全复用,同样使用OCR-A字体模板,代码无任何修改。若身份证字体与模板差异较大,可替换为对应字体的模板图像。
4.3 步骤2:身份证图像预处理
预处理流程与银行卡完全一致:尺寸归一化→灰度化→顶帽增强→闭运算连通→OTSU二值化→二次闭运算填充。核心参数同样沿用(9,3)长方形核与(5,5)正方形核,适配数字的横向特征。
4.4 步骤3:身份证号整体区域定位
4.4.1 轮廓筛选条件调整
身份证号是18位连续数字,形成一个狭长的矩形区域,因此宽高比阈值大幅提高:
python
locs = []
for c in cnts:
(x, y, w, h) = cv2.boundingRect(c)
ar = w / float(h)
# 筛选条件:宽高比16~20,宽度1~300,高度1~20
if 16.0 < ar < 20.0:
if (1 < w < 300) and (1 < h < 20):
locs.append((x, y, w, h))
locs = sorted(locs, key=lambda x: x[0])
- 18位数字的总宽度约为单个数字宽度的18倍,单个数字宽高比约0.8,因此整体宽高比约为1418,设置1620的范围留有余量。
- 高度范围较窄,因为身份证数字字号统一,高度波动小。
4.4.2 易错点提醒
原代码中存在一处常见笔误:
python
# 错误写法:img是模板图像,画轮廓会错位
cnts_img = img.copy()
cv2.drawContours(cnts_img, cnts, -1, (0, 0, 255), 3)
# 正确写法:在待识别的身份证图像上绘制轮廓
cnts_img = image.copy()
cv2.drawContours(cnts_img, cnts, -1, (0, 0, 255), 3)
很多初学者复制代码时容易混淆模板图像img和待识别图像image,导致轮廓绘制到错误的画布上。
4.5 步骤4:单数字分割与识别
整体区域裁剪后,分割单个数字的逻辑与银行卡一致,唯一区别是分组二值化使用反二值化:
python
# 身份证数字为黑色背景为白色,反二值化后得到黑底白字
group = cv2.threshold(group, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]
二值化方向选择的核心原则:确保输出图像中数字是白色,背景是黑色。如果识别结果全错、轮廓提取为空,优先检查二值化方向是否正确。
后续单数字轮廓提取、尺寸归一化、模板匹配、结果绘制逻辑与银行卡完全相同,最终得到18位身份证号字符串。
4.6 效果展示与结果输出
运行代码后,终端输出身份证号:
ID Card #: 430512198908131367
图像窗口显示身份证原图,号码区域被红色框标注,上方显示完整识别结果。
五、两大项目核心差异对比与参数调优思路
5.1 核心参数对比表
| 对比维度 | 银行卡号识别 | 身份证号识别 |
|---|---|---|
| 数字布局 | 4组,每组4位,组间大间隔 | 连续18位,整体长条状 |
| 目标区域宽高比 | 2.5 ~ 4.0 | 16.0 ~ 20.0 |
| 典型区域宽度 | 40 ~ 55 像素 | 约150 像素 |
| 典型区域高度 | 10 ~ 20 像素 | 约8 像素 |
| 区域二值化方式 | THRESH_BINARY |
THRESH_BINARY_INV |
| 输出附加信息 | 银行卡组织类型 | 无附加信息 |
| 模板复用性 | 完全通用 | 字体一致时可复用 |
5.2 轮廓筛选参数的调优方法
很多初学者的核心困惑是「宽高比和尺寸范围怎么定」,这里给出通用调优步骤:
- 先全量绘制轮廓:注释掉筛选条件,将所有轮廓都画在原图上,直观看到哪个轮廓是目标数字区域;
- 打印目标轮廓参数 :输出目标轮廓的
x, y, w, h, ar数值,记录基准值; - 留余量设置范围:以基准值为中心,上下浮动20%左右作为筛选范围,兼顾准确率与召回率。
- 多图验证:用3~5张不同样本验证参数,避免过拟合单张图像。
5.3 二值化方式选择的依据
判断用正二值化还是反二值化,只需回答一个问题:灰度图中数字和背景哪个更亮?
- 数字亮、背景暗(如顶帽后的银行卡数字)→ 正二值化
THRESH_BINARY,输出白字黑底; - 数字暗、背景亮(如身份证原图裁剪的数字区)→ 反二值化
THRESH_BINARY_INV,输出白字黑底。 - 调试技巧:每步二值化后都调用
cv_show查看效果,确保数字是白色、背景是黑色。
六、完整代码汇总
6.1 myutils.py 完整代码
python
import cv2
def sort_contours(cnts, method="left-to-right"):
reverse = False
i = 0
if method == "right-to-left" or method == "bottom-to-top":
reverse = True
if method == "top-to-bottom" or method == "bottom-to-top":
i = 1
boundingBoxes = [cv2.boundingRect(c) for c in cnts]
(cnts, boundingBoxes) = zip(*sorted(zip(cnts, boundingBoxes),
key=lambda b: b[1][i], reverse=reverse))
return cnts, boundingBoxes
def resize(image, width=None, height=None, inter=cv2.INTER_AREA):
dim = None
(h, w) = image.shape[:2]
if width is None and height is None:
return image
if width is None:
r = height / float(h)
dim = (int(w * r), height)
else:
r = width / float(w)
dim = (width, int(h * r))
resized = cv2.resize(image, dim, interpolation=inter)
return resized
6.2 银行卡识别完整代码(bank_card_ocr.py)
python
import numpy as np
import argparse
import cv2
import myutils
# 命令行参数设置
ap = argparse.ArgumentParser()
ap.add_argument('-i', '--image', required=True, help='path to input bank card image')
ap.add_argument('-t', '--template', required=True, help='path to template OCR-A image')
args = vars(ap.parse_args())
# 银行卡首位对应卡组织
FIRST_NUMBER = {
'3': 'American Express',
'4': 'Visa',
'5': 'MasterCard',
'6': 'Discover Card'
}
def cv_show(name, img):
"""图像显示封装,按任意键关闭"""
cv2.imshow(name, img)
cv2.waitKey(0)
cv2.destroyAllWindows()
# ===================== 1. 构建数字模板库 =====================
# 读取模板并预处理
img = cv2.imread(args['template'])
ref = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ref = cv2.threshold(ref, 10, 255, cv2.THRESH_BINARY_INV)[1]
# 提取轮廓并排序
refCnts = cv2.findContours(ref, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2]
refCnts = myutils.sort_contours(refCnts, method='left-to-right')[0]
# 构建数字模板字典
digits = {}
for (i, c) in enumerate(refCnts):
(x, y, w, h) = cv2.boundingRect(c)
roi = ref[y:y+h, x:x+w]
roi = cv2.resize(roi, (57, 88))
digits[i] = roi
# ===================== 2. 银行卡图像预处理 =====================
# 读取图像并归一化尺寸
image = cv2.imread(args['image'])
image = myutils.resize(image, width=300)
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# 定义形态学核
rectKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (9, 3))
sqKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
# 顶帽运算增强数字
tophat = cv2.morphologyEx(gray, cv2.MORPH_TOPHAT, rectKernel)
# 闭运算连通数字笔画
closeX = cv2.morphologyEx(tophat, cv2.MORPH_CLOSE, rectKernel)
# OTSU二值化
thresh = cv2.threshold(closeX, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
# 二次闭运算填充孔洞
thresh = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, sqKernel)
# ===================== 3. 卡号分组区域定位 =====================
cnts = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2]
locs = []
for c in cnts:
(x, y, w, h) = cv2.boundingRect(c)
ar = w / float(h)
# 筛选4位数字分组
if 2.5 < ar < 4.0:
if (40 < w < 55) and (10 < h < 20):
locs.append((x, y, w, h))
locs = sorted(locs, key=lambda x: x[0])
# ===================== 4. 单数字分割与模板匹配 =====================
output = []
for (gX, gY, gW, gH) in locs:
groupOutput = []
# 裁剪分组区域,扩展边缘避免截断
group = gray[gY - 5 : gY + gH + 5, gX - 5 : gX + gW + 5]
# 分组二值化
group = cv2.threshold(group, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
# 提取组内数字轮廓并排序
digitCnts = cv2.findContours(group.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2]
digitCnts = myutils.sort_contours(digitCnts, method='left-to-right')[0]
for c in digitCnts:
(x, y, w, h) = cv2.boundingRect(c)
roi = group[y:y+h, x:x+w]
roi = cv2.resize(roi, (57, 88))
# 模板匹配计算得分
scores = []
for (digit, digitROI) in digits.items():
result = cv2.matchTemplate(roi, digitROI, cv2.TM_CCOEFF)
(_, score, _, _) = cv2.minMaxLoc(result)
scores.append(score)
groupOutput.append(str(np.argmax(scores)))
# 绘制识别结果
cv2.rectangle(image, (gX - 5, gY - 5), (gX + gW + 5, gY + gH + 5), (0, 0, 255), 1)
cv2.putText(image, ''.join(groupOutput), (gX, gY - 15),
cv2.FONT_HERSHEY_SIMPLEX, 0.65, (0, 0, 255), 2)
output.extend(groupOutput)
# ===================== 5. 输出结果 =====================
print("Credit Card Type: {}".format(FIRST_NUMBER[output[0]]))
print("Credit Card #: {}".format("".join(output)))
cv_show("Recognition Result", image)
8.3 身份证识别完整代码(id_card_ocr.py)
python
import numpy as np
import argparse
import cv2
import myutils
# 命令行参数设置
ap = argparse.ArgumentParser()
ap.add_argument('-i', '--image', required=True, help='path to input ID card image')
ap.add_argument('-t', '--template', required=True, help='path to template OCR-A image')
args = vars(ap.parse_args())
def cv_show(name, img):
cv2.imshow(name, img)
cv2.waitKey(0)
cv2.destroyAllWindows()
# ===================== 1. 构建数字模板库 =====================
img = cv2.imread(args['template'])
ref = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ref = cv2.threshold(ref, 10, 255, cv2.THRESH_BINARY_INV)[1]
refCnts = cv2.findContours(ref, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2]
refCnts = myutils.sort_contours(refCnts, method='left-to-right')[0]
digits = {}
for (i, c) in enumerate(refCnts):
(x, y, w, h) = cv2.boundingRect(c)
roi = ref[y:y+h, x:x+w]
roi = cv2.resize(roi, (57, 88))
digits[i] = roi
# ===================== 2. 身份证图像预处理 =====================
image = cv2.imread(args['image'])
image = myutils.resize(image, width=300)
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
rectKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (9, 3))
sqKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
tophat = cv2.morphologyEx(gray, cv2.MORPH_TOPHAT, rectKernel)
closeX = cv2.morphologyEx(tophat, cv2.MORPH_CLOSE, rectKernel)
thresh = cv2.threshold(closeX, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
thresh = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, sqKernel)
# ===================== 3. 身份证号区域定位 =====================
cnts = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2]
locs = []
for c in cnts:
(x, y, w, h) = cv2.boundingRect(c)
ar = w / float(h)
# 筛选18位数字长条区域
if 16.0 < ar < 20.0:
if (1 < w < 300) and (1 < h < 20):
locs.append((x, y, w, h))
locs = sorted(locs, key=lambda x: x[0])
# ===================== 4. 单数字分割与模板匹配 =====================
output = []
for (gX, gY, gW, gH) in locs:
groupOutput = []
group = gray[gY - 5 : gY + gH + 5, gX - 5 : gX + gW + 5]
# 反二值化:黑字白底 → 白字黑底
group = cv2.threshold(group, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]
digitCnts = cv2.findContours(group.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2]
digitCnts = myutils.sort_contours(digitCnts, method='left-to-right')[0]
for c in digitCnts:
(x, y, w, h) = cv2.boundingRect(c)
roi = group[y:y+h, x:x+w]
roi = cv2.resize(roi, (57, 88))
scores = []
for (digit, digitROI) in digits.items():
result = cv2.matchTemplate(roi, digitROI, cv2.TM_CCOEFF)
(_, score, _, _) = cv2.minMaxLoc(result)
scores.append(score)
groupOutput.append(str(np.argmax(scores)))
cv2.rectangle(image, (gX - 5, gY - 5), (gX + gW + 5, gY + gH + 5), (0, 0, 255), 1)
cv2.putText(image, ''.join(groupOutput), (gX, gY - 15),
cv2.FONT_HERSHEY_SIMPLEX, 0.50, (0, 0, 255), 1)
output.extend(groupOutput)
# ===================== 5. 输出结果 =====================
print("ID Card #: {}".format("".join(output)))
cv_show("Recognition Result", image)
运行方式:
bash
# 银行卡识别
python bank_card_ocr.py -i card1.png -t kahao.png
# 身份证识别
python id_card_ocr.py -i card1.jpg -t kahao.png
总结
本文通过银行卡号识别、身份证号识别两个实战项目,完整拆解了基于OpenCV模板匹配的数字OCR技术体系。从最基础的图像预处理、形态学操作,到轮廓检测筛选、模板匹配原理,再到参数调优、问题排查与进阶优化,覆盖了从入门到实战的全链路知识。
传统计算机视觉是深度学习时代的基石,模板匹配OCR虽然简单,却蕴含了「预处理→定位→分割→识别」的经典OCR范式。掌握这套方法论,不仅能快速解决固定场景的数字识别需求,更能为后续学习深度学习OCR打下扎实的视觉基础。
如果本文对你有帮助,欢迎点赞收藏,也可以基于这套框架拓展到更多场景,比如仪表盘读数、快递单号识别、验证码识别等。