引言
在图像处理中,模板匹配是一项基础且重要的技术。通过模板匹配,我们可以在目标图像中寻找与给定模板最相似的区域。身份证号码识别是计算机视觉中一个经典的应用场景,常用于自动化信息录入、身份验证等系统。由于身份证号码区域具有固定的位置、字体和大小,我们可以通过图像处理技术定位该区域,并利用模板匹配方法识别每个数字字符。
本文将基于OpenCV实现一个完整的身份证号码识别系统,主要包含以下模块:
-
数字模板的构建(从包含0-9数字的模板图片
kahao.png中提取数字) -
身份证图像的预处理与号码区域定位
-
基于模板匹配的数字识别
-
结果可视化与输出
代码将逐步解释每个环节,并重点剖析轮廓排序 和模板匹配这两个关键用法。
环境与依赖
-
Python 3.x
-
OpenCV(
cv2) -
NumPy(
np)
任务描述
给定一张包含0-9数字的模板图片kahao.png和一张待识别的身份证图片sfz.jpg,要求完成以下任务:
-
从
kahao.png中提取每个数字的轮廓,并按照从左到右的顺序构建数字模板库; -
对待识别图片
sfz.jpg进行预处理,定位身份证号码区域; -
使用模板匹配方法识别每个数字字符;
-
在原图上绘制识别结果,并输出完整的身份证号码。
模板图片 (sfz_tp.png) 示意:

待识别图片 (sfz.jpg) 内容:

最终效果示意:

实现步骤详解
1. 工具函数定义
首先定义两个辅助函数,方便后续调用。
1.1 图像显示函数 cv_show
def cv_show(name, image):
if image is None or image.size == 0:
print(f"[{name}] 图像为空,无法显示!")
return
cv2.imshow(name, image)
cv2.waitKey(0)
该函数封装了图像显示,并增加了空值检查,避免程序崩溃。
1.2 轮廓排序函数 sort_contours
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
该函数根据指定的方向(如从左到右)对轮廓进行排序。在识别身份证号码时,我们需要按字符从左到右的顺序输出结果,因此这个函数非常实用。
2. 数字模板构建(从sfz_tp.png提取)
模板图片sfz_tp.png包含0-9十个数字,我们需要从中提取每个数字的ROI,并将其缩放到统一尺寸(57×88),作为后续匹配的模板库。
2.1 读取模板图像并预处理
img = cv2.imread('sfz_tp.png')
cv_show('img', img)
gray = cv2.imread('sfz_tp.png', 0) # 灰度读取
ref = cv2.threshold(gray, 150, 255, cv2.THRESH_BINARY_INV)[1] # 反色二值化
cv_show('ref', ref)
说明 :使用阈值150进行二值化,并采用 THRESH_BINARY_INV 反色处理,使数字变为白色(255),背景为黑色(0)。这有利于轮廓检测。
结果展示:

2.2 查找轮廓并绘制
_, refCnts, hierarchy = cv2.findContours(ref.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cv2.drawContours(img, refCnts, -1, (0, 255, 0), 2)
cv_show('img', img)
使用 RETR_EXTERNAL 只检测最外层轮廓,避免内部空洞干扰。绘制轮廓可以验证检测效果。
结果展示:

2.3 对轮廓排序并提取每个数字
refCnts = sort_contours(refCnts, method='left-to-right')[0]
digits = []
for c in refCnts:
(x, y, w, h) = cv2.boundingRect(c)
# 稍微扩大一点范围,防止切到数字边缘
roi = gray[y-2:y+h+2, x-2:x+w+2]
roi = cv2.resize(roi, (57, 88)) # 统一缩放
cv_show('roi', roi)
digits.append(roi)
cv2.destroyAllWindows()
排序后,轮廓顺序与数字顺序一致(从左到右)。对每个轮廓计算外接矩形,并向外扩展2个像素后截取ROI,然后缩放到固定尺寸(57×88)。digits 列表存储了所有数字模板。
结果展示:

3. 身份证号码识别(从sfz.jpg识别)
3.1 读取待识别图像并预处理
img = cv2.imread('sfz.jpg')
img_copy = img.copy()
cv_show('img', img)
gray = cv2.imread('sfz.jpg', 0)
cv_show('gray', gray)
ref = cv2.threshold(gray, 120, 255, cv2.THRESH_BINARY_INV)[1]
cv_show('ref', ref)
同样进行灰度化和反色二值化。阈值120可根据图像质量微调。

3.2 查找轮廓并筛选号码区域
_, refCnts, hierarchy = cv2.findContours(ref.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
a = cv2.drawContours(img.copy(), refCnts, -1, (0, 255, 0), 2)
cv_show('img', a)
locs = []
for c in refCnts:
(x, y, w, h) = cv2.boundingRect(c)
# 根据身份证号码区域的实际坐标范围进行筛选
if (330 < y < 360) and (x > 220):
locs.append((x, y, w, h))
locs = sorted(locs, key=lambda x: x[0]) # 按x坐标从左到右排序
由于身份证号码通常位于固定位置,我们可以通过观察图像确定坐标范围。本例中,号码区域的y坐标大致在330~360之间,且x>220。筛选后得到每个字符的外接矩形,并按x排序。
结果展示:

3.3 模板匹配识别数字
output = []
for (gX, gY, gW, gH) in locs:
# 截取单个字符区域,并稍微扩大边界
group = ref[gY-2:gY+gH+2, gX-2:gX+gW+2]
roi = cv2.resize(group, (57, 88))
cv_show('roi', roi)
scores = []
for digitROI in digits:
result = cv2.matchTemplate(roi, digitROI, cv2.TM_CCOEFF)
(_, score, _, _) = cv2.minMaxLoc(result)
scores.append(score)
jiegou = str(np.argmax(scores))
output.append(jiegou)
# 在原图上绘制矩形和识别结果
cv2.rectangle(img, (gX-5, gY-5), (gX+gW+5, gY+gH+5), (0, 0, 255), 1)
cv2.putText(img, jiegou, (gX, gY-15), cv2.FONT_HERSHEY_SIMPLEX, 0.65, (0, 0, 255), 2)
匹配过程 :对每个待识别字符ROI,与 digits 列表中的10个模板分别进行模板匹配(使用相关系数法 TM_CCOEFF),得分最高的模板对应的数字即为识别结果。np.argmax(scores) 返回得分最高的索引,该索引即为数字值。
3.4 输出与显示
print("Card ID #: {}".format("".join(output)))
cv2.imshow("Image", img)
cv2.waitKey(0)
cv2.destroyAllWindows()
最终打印识别出的身份证号码,并显示带有标记的图片。
结果展示:

完整代码
整合以上所有步骤,得到完整的识别程序。
import cv2
import numpy as np
# ===================== 通用工具函数定义 =====================
def cv_show(name, image):
"""
图片展示函数
:param name: 窗口名称
:param image: 图像对象
"""
# 防止传入空图像导致报错
if image is None or image.size == 0:
print(f"[{name}] 图像为空,无法显示!")
return
cv2.imshow(name, image)
cv2.waitKey(0)
# cv2.destroyWindow(name)
def sort_contours(cnts, method="left-to-right"):
"""
轮廓排序函数(用于数字识别时按从左到右顺序排列)
:param cnts: 找到的轮廓列表
:param method: 排序方式,默认从左到右
:return: 排序后的轮廓、对应的边界框坐标
"""
# 初始化排序索引
i = 0
reverse = False
# 根据排序方向调整
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
# ===================== 模板图像中的数字定位处理 =====================
# 1. 读取模板图片(身份证模板)
img = cv2.imread('sfz_tp.png')
cv_show('img', img)
# 2. 灰度化处理
# 注意:此处图片路径重复,且flags=0 代表以灰度模式读取,下面重新规范读取
gray = cv2.imread('sfz_tp.png', 0)
# 3. 二值化处理 (THRESH_BINARY_INV 表示反色二值化,白色背景变黑,数字变白)
ref = cv2.threshold(gray, 150, 255, cv2.THRESH_BINARY_INV)[1]
cv_show('ref', ref)
# 4. 查找轮廓 (只检测外轮廓,压缩轮廓点)
_,refCnts, hierarchy = cv2.findContours(ref.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# 5. 在原图上绘制轮廓 (绿色线条,厚度2)
cv2.drawContours(img, refCnts, -1, color=(0, 255, 0), thickness=2)
cv_show('img', img)
# 6. 对检测到的轮廓进行从左到右排序
refCnts = sort_contours(refCnts, method='left-to-right')[0]
# 7. 遍历每一个数字轮廓,提取ROI并标准化大小
digits = []
for c in refCnts:
# 计算外接矩形
(x, y, w, h) = cv2.boundingRect(c)
# 截取ROI(区域),并稍微扩大一点范围(x-2: x+w+2, y-2: y+h+2)
# 防止切到数字边缘
roi = gray[y - 2: y + h + 2, x - 2: x + w + 2]
# 统一缩放为 57x88 的标准尺寸(适配模型输入)
roi = cv2.resize(roi, dsize=(57, 88))
cv_show('roi', roi)
digits.append(roi)
# 8. 销毁所有OpenCV窗口
cv2.destroyAllWindows()
# ===================== 身份证号码识别 =====================
# 1. 读取待识别的身份证照片
img = cv2.imread('sfz.jpg')
img_copy = img.copy() # 备份原图
cv_show('img', img)
# 2. 灰度化处理
gray = cv2.imread('sfz.jpg', 0)
cv_show('gray', gray)
# 3. 二值化处理 (阈值120,反色)
ref = cv2.threshold(gray, 120, 255, cv2.THRESH_BINARY_INV)[1]
cv_show('ref', ref)
# 查找轮廓(外轮廓,压缩轮廓点)
_, refCnts, hierarchy = cv2.findContours(ref.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# 在副本上绘制轮廓(绿色,厚度2)
a = cv2.drawContours(img.copy(), refCnts, -1, color=(0, 255, 0), thickness=2)
cv_show(name='img', image=a)
locs = []
# 遍历轮廓,筛选符合坐标范围的区域
for c in refCnts:
(x, y, w, h) = cv2.boundingRect(c) # 外接矩形
# 选择合适的区域,根据实际任务来
if (330 < y < 360) and (x > 220): # 符合的留下来
locs.append((x, y, w, h))
# 按x坐标从左到右排序
locs = sorted(locs, key=lambda x: x[0])
import numpy as np
output = []
# 遍历每个定位到的字符区域
for (gX, gY, gW, gH) in locs:
# 截取区域并加一点边界
group = ref[gY - 2:gY + gH + 2, gX - 2:gX + gW + 2]
# cv_show('group', group)
# 缩放到和模板一致的尺寸
roi = cv2.resize(group, (57, 88))
cv_show('roi', roi)
'''------使用模板匹配,计算匹配得分------'''
scores = []
# 在模板中计算每一个得分
for digitROI in digits:
# 模板匹配(相关系数法)
result = cv2.matchTemplate(roi, digitROI, cv2.TM_CCOEFF)
(_, score, _, _) = cv2.minMaxLoc(result)
scores.append(score)
# 得到最合适的数字(得分最大的索引)
jiegou = str(np.argmax(scores))
output.append(jiegou)
# 在图像上绘制矩形和识别结果
cv2.rectangle(img, pt1=(gX - 5, gY - 5), pt2=(gX + gW + 5, gY + gH + 5), color=(0, 0, 255), thickness=1)
cv2.putText(img, jiegou, org=(gX, gY - 15), fontFace=cv2.FONT_HERSHEY_SIMPLEX, fontScale=0.65, color=(0, 0, 255),
thickness=2)
# 打印结果
print("Card ID #: {}".format("".join(output)))
# 显示最终结果
cv2.imshow(winname="Image", mat=img)
cv2.waitKey(0)
cv2.destroyAllWindows()
关键点解析
1. 轮廓排序函数 sort_contours 的设计
-
cv2.boundingRect(c):计算每个轮廓的最小外接矩形,返回(x, y, w, h)。 -
sorted(zip(cnts, boundingBoxes), key=lambda b: b[1][i]):将轮廓和对应的边界框打包在一起,根据边界框的x(i=0)或y(i=1)坐标进行排序。 -
参数
method:支持四种排序方向:从左到右、从右到左、从上到下、从下到上。在身份证号码识别中,我们使用默认的left-to-right。
为什么需要排序?cv2.findContours 返回的轮廓顺序是随机的,而身份证号码的字符顺序必须从左到右,因此排序是必不可少的一步。
2. 二值化阈值的选择
-
模板图片
kahao.png:使用阈值150。由于模板图片通常背景简单、数字清晰,固定阈值即可良好分割。 -
身份证图片
sfz.jpg:使用阈值120。实际身份证照片可能存在光照不均,阈值需要根据具体图像调整。若效果不佳,可考虑自适应阈值(如cv2.adaptiveThreshold)或Otsu大津法。
3. 字符区域筛选策略
本例中,身份证号码区域的y坐标大致在330~360之间,且x>220。这种基于坐标的筛选简单有效,但依赖于身份证在图像中的固定位置。更通用的方法是:
-
根据轮廓的宽高比(例如数字通常宽高比在0.3~0.8之间)筛选
-
根据轮廓面积筛选
-
利用轮廓的排列规律(连续多个轮廓且间距均匀)进行组合筛选
4. 模板匹配方法选择
-
cv2.TM_CCOEFF:相关系数匹配法,计算模板与图像区域的相关系数,值越大表示越匹配。cv2.minMaxLoc(result)返回最大值及其位置。 -
为什么使用这种方法?因为它对线性光照变化有一定鲁棒性,且结果范围不固定,便于取最大值进行比较。
-
注意:模板和待识别字符已经缩放到相同尺寸(57×88),因此可以直接匹配。
5. ROI扩展
在截取字符区域时,向外扩展2个像素(y-2:y+h+2 等),可以避免因轮廓检测不精确而切掉字符边缘,提高匹配精度。这个技巧在处理边缘不清晰的字符时尤其有效。
6. np.argmax(scores) 的用法
scores 是一个长度为10的列表,分别对应0-9十个模板的匹配得分。np.argmax(scores) 返回得分最高的索引,该索引正好等于识别出的数字值。例如,如果索引3得分最高,则识别结果为数字3。
运行结果与讨论
运行代码后,将依次弹出多个窗口,展示中间步骤的图像:
-
模板原图及二值化结果
-
模板轮廓绘制结果
-
每个提取出的数字模板
-
身份证原图及预处理结果
-
身份证轮廓绘制结果
-
每个待识别字符区域
-
最终带有红色矩形框和数字标签的身份证图片
控制台将输出识别结果:
Card ID #: 007204039379000094
可能遇到的问题及改进
-
模板图片质量 :如果
kahao.png中数字不清晰或排列不齐,可能导致模板提取失败。建议使用打印体数字的标准图片,确保数字完整且间隔足够。 -
光照影响:身份证照片可能因光照不均导致二值化效果差。可使用形态学操作(如闭运算)填充断裂字符,或采用动态阈值。
-
字符粘连:如果身份证号码字符之间粘连,可能无法分离出单个轮廓。可尝试基于垂直投影的分割方法。
-
通用性:此方法依赖于号码区域的固定位置,适用于扫描仪或固定布局的身份证图像。对于手机拍照的图像,需要先进行透视校正,将身份证区域矫正为标准矩形。
总结
本文通过一个完整的身份证号码识别示例,展示了OpenCV在图像处理中的典型应用流程:
-
模板构建:从模板图像中提取数字轮廓,排序并统一尺寸
-
图像预处理:灰度化、二值化、轮廓检测
-
区域筛选:基于坐标或特征筛选目标区域
-
模板匹配 :利用
matchTemplate进行数字识别 -
结果可视化:绘制矩形框并标注识别结果
重点剖析了轮廓排序函数 的设计思想和模板匹配的实现细节,帮助读者理解如何利用OpenCV和Python内置函数实现高效的字符识别系统。
该方案简单高效,适合作为入门级OCR实践项目。读者可根据实际应用场景优化预处理流程,提升识别鲁棒性。
希望这篇文章对你在图像处理和字符识别方面有所帮助!如果有任何问题或建议,欢迎留言讨论。
注意 :实际运行代码时,请确保 kahao.png 和 sfz.jpg 存在于当前目录,并根据实际图像调整阈值和坐标范围。