OpenCV 实战:身份证号码识别系统(基于模板匹配)

引言

在图像处理中,模板匹配是一项基础且重要的技术。通过模板匹配,我们可以在目标图像中寻找与给定模板最相似的区域。身份证号码识别是计算机视觉中一个经典的应用场景,常用于自动化信息录入、身份验证等系统。由于身份证号码区域具有固定的位置、字体和大小,我们可以通过图像处理技术定位该区域,并利用模板匹配方法识别每个数字字符。

本文将基于OpenCV实现一个完整的身份证号码识别系统,主要包含以下模块:

  • 数字模板的构建(从包含0-9数字的模板图片kahao.png中提取数字)

  • 身份证图像的预处理与号码区域定位

  • 基于模板匹配的数字识别

  • 结果可视化与输出

代码将逐步解释每个环节,并重点剖析轮廓排序模板匹配这两个关键用法。

环境与依赖

  • Python 3.x

  • OpenCV(cv2

  • NumPy(np

任务描述

给定一张包含0-9数字的模板图片kahao.png和一张待识别的身份证图片sfz.jpg,要求完成以下任务:

  1. kahao.png中提取每个数字的轮廓,并按照从左到右的顺序构建数字模板库;

  2. 对待识别图片sfz.jpg进行预处理,定位身份证号码区域;

  3. 使用模板匹配方法识别每个数字字符;

  4. 在原图上绘制识别结果,并输出完整的身份证号码。

模板图片 (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]) :将轮廓和对应的边界框打包在一起,根据边界框的 xi=0)或 yi=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。

运行结果与讨论

运行代码后,将依次弹出多个窗口,展示中间步骤的图像:

  1. 模板原图及二值化结果

  2. 模板轮廓绘制结果

  3. 每个提取出的数字模板

  4. 身份证原图及预处理结果

  5. 身份证轮廓绘制结果

  6. 每个待识别字符区域

  7. 最终带有红色矩形框和数字标签的身份证图片

控制台将输出识别结果:

复制代码
Card ID #: 007204039379000094

可能遇到的问题及改进

  1. 模板图片质量 :如果kahao.png中数字不清晰或排列不齐,可能导致模板提取失败。建议使用打印体数字的标准图片,确保数字完整且间隔足够。

  2. 光照影响:身份证照片可能因光照不均导致二值化效果差。可使用形态学操作(如闭运算)填充断裂字符,或采用动态阈值。

  3. 字符粘连:如果身份证号码字符之间粘连,可能无法分离出单个轮廓。可尝试基于垂直投影的分割方法。

  4. 通用性:此方法依赖于号码区域的固定位置,适用于扫描仪或固定布局的身份证图像。对于手机拍照的图像,需要先进行透视校正,将身份证区域矫正为标准矩形。

总结

本文通过一个完整的身份证号码识别示例,展示了OpenCV在图像处理中的典型应用流程:

  1. 模板构建:从模板图像中提取数字轮廓,排序并统一尺寸

  2. 图像预处理:灰度化、二值化、轮廓检测

  3. 区域筛选:基于坐标或特征筛选目标区域

  4. 模板匹配 :利用 matchTemplate 进行数字识别

  5. 结果可视化:绘制矩形框并标注识别结果

重点剖析了轮廓排序函数 的设计思想和模板匹配的实现细节,帮助读者理解如何利用OpenCV和Python内置函数实现高效的字符识别系统。

该方案简单高效,适合作为入门级OCR实践项目。读者可根据实际应用场景优化预处理流程,提升识别鲁棒性。

希望这篇文章对你在图像处理和字符识别方面有所帮助!如果有任何问题或建议,欢迎留言讨论。

注意 :实际运行代码时,请确保 kahao.pngsfz.jpg 存在于当前目录,并根据实际图像调整阈值和坐标范围。

相关推荐
咚咚王者1 小时前
人工智能之语言领域 自然语言处理 第十六章 生成式预训练模型
人工智能·自然语言处理
万里沧海寄云帆1 小时前
pytorch+cpu版本对Intel Ultra 9 275HX性能的影响
人工智能·pytorch·python
阿里云大数据AI技术2 小时前
阿里云荣获 2025–2026 年度 Elastic中国最佳合作伙伴奖
人工智能·elasticsearch
yrwang_xd2 小时前
人工智能基础-常用Nvidia Tesla及RTX显卡算力大全-2026版
人工智能
用户4815930195912 小时前
MCP 终极指南(进阶篇):手写一个 MCP Server,再用抓包拆解协议底层
人工智能
用户4815930195912 小时前
我抓包了 Cline 与模型的通信,发现了一件有趣的事
人工智能
1941s2 小时前
Google Agent Development Kit (ADK) 指南 第二章:环境搭建与快速开始
人工智能·python·adk·google agent
抓个马尾女孩2 小时前
位置编码:绝对位置编码、相对位置编码、旋转位置编码
人工智能·深度学习·算法·transformer
风酥糖2 小时前
AI时代的技术焦虑与自我救赎
人工智能