OpenCV 实战:从可乐标志识别到银行卡、身份证号识别(模板匹配 + 轮廓检测)

板匹配是 OpenCV 中最基础也最实用的目标识别技术之一,小到识别一张图片中的可乐标志,大到提取银行卡、身份证上的数字信息,核心逻辑都可以复用。本文将从可乐标志识别这个简单案例入手,逐步拆解模板匹配的核心原理,再拓展到银行卡号、身份证号识别的实战场景。

一、入门案例:识别图片中的可乐标志

先通过一个极简案例,理解模板匹配的核心流程 ------ 用模板在原图中 "滑动" 比对,找到匹配度最高的区域并标注。

1.1 核心思路

  1. 读取可乐原图和可乐标志图;
  2. 使用cv2.matchTemplate计算原图每个位置与模板的匹配度;
  3. 用cv2.minMaxLoc找到匹配度最高的位置;
  4. 在原图上绘制矩形框,标注出可乐标志的位置。

1.2 完整代码

python 复制代码
import cv2

# 读取包含可乐标志的原图
kele = cv2.imread('kele.png')
# 读取可乐标志的模板图
template = cv2.imread('template.png')

# 显示原图和模板图
cv2.imshow('kele', kele)
cv2.imshow('template', template)

# 获取模板的高、宽
h, w = template.shape[:2]
# 执行模板匹配
res = cv2.matchTemplate(kele, template, cv2.TM_CCOEFF_NORMED)
# 获取匹配结果的极值:最小值、最大值、最小值位置、最大值位置
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)

# 匹配度最高的位置
top_left = max_loc
# 计算匹配区域的右下角坐标
bottom_right = (top_left[0] + w, top_left[1] + h)
# 在原图上绘制绿色矩形框
kele_template = cv2.rectangle(kele, top_left, bottom_right, (0, 255, 0), 2)

cv2.imshow('jieguo', kele_template)
cv2.waitKey(0)
cv2.destroyAllWindows()

1.3结果展示

1.4代码关键解析

cv2.TM_CCOEFF_NORMED是归一化的匹配方法,结果更稳定,推荐优先使用;

max_val越接近1,说明匹配越精准;若max_val低于0.8,可能是模板与原图差异较大;

通过调试模式我们可以看到max_val有0.9990,非常接近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

三、实战 1:银行卡号识别

可乐标志识别是 "单目标匹配",而银行卡号识别是 "多目标 + 数字识别",核心是在模板匹配基础上增加轮廓检测,实现数字区域定位和单个数字分割。

3.1 实现思路

  1. 制作 0-9 数字模板库(从模板图中提取轮廓并排序);
  2. 预处理银行卡图像:顶帽操作突出数字、闭运算连接断裂数字、二值化增强对比度;
  3. 轮廓筛选定位卡号区域,分割单个数字;
  4. 模板匹配识别每个数字,拼接得到完整卡号。

3.2 完整代码

python 复制代码
import numpy as np
import argparse
import cv2
import myutils  # 导入上面的工具函数

# 命令行参数配置(默认路径可根据自己的文件修改)
ap = argparse.ArgumentParser()
ap.add_argument("-i", "--image", default=r"card1.png",
                help="path to input image")
ap.add_argument("-t", "--template", default=r"kahao.png",
                help="path to template 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.destroyWindow(name)

# 读取模板图像并预处理
img = cv2.imread(args["template"])
cv_show("Template Image", img)
ref = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)  # 转灰度
ref = cv2.threshold(ref, 10, 255, cv2.THRESH_BINARY_INV)[1]  # 二值化(反相)
cv_show("Binary Template", ref)

# 提取模板轮廓(仅提取外轮廓)
_, refCnts, hierarchy = cv2.findContours(ref.copy(), cv2.RETR_EXTERNAL,
                                         cv2.CHAIN_APPROX_SIMPLE)
cv2.drawContours(img, refCnts, -1, (0, 255, 0), 3)
cv_show('Template Contours', img)

# 按从左到右排序轮廓,生成0-9数字模板
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  # 存储数字模板

image = cv2.imread(args["image"])
cv_show("Original Card", image)
image = myutils.resize(image, width=300)  # 统一缩放尺寸
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
cv_show("Gray Card", gray)

# 顶帽操作:突出亮区域(数字),抑制暗背景
rectKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (9, 3))
sqKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
tophat = cv2.morphologyEx(gray, cv2.MORPH_TOPHAT, rectKernel)
cv_show("Tophat Result", tophat)

# 闭运算:连接数字的断裂部分(先膨胀后腐蚀)
closeX = cv2.morphologyEx(tophat, cv2.MORPH_CLOSE, rectKernel)
cv_show("Close Result 1", closeX)

# 二值化(OTSU自动找阈值)
thresh = cv2.threshold(closeX, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
cv_show("Threshold Result", thresh)

# 再次闭运算:强化数字连接
thresh = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, sqKernel)
cv_show("Close Result 2", thresh)

# 提取轮廓
_, cnts, h = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts_img = image.copy()
cv2.drawContours(cnts_img, cnts, -1, (0, 0, 255), 3)
cv_show("All Contours", cnts_img)

# 筛选卡号轮廓(根据宽高比、尺寸)
locs = []
for c in cnts:
    (x, y, w, h) = cv2.boundingRect(c)
    ar = w / float(h)
    # 卡号区域的宽高比通常在2.5~4之间,尺寸需根据实际调整
    if 2.5 < ar < 4.0 and (40 < w < 55) and (10 < h < 20):
        locs.append((x, y, w, h))

# 按从左到右排序卡号区域
locs = sorted(locs, key=lambda x: x[0])
output = []

for (gX, gY, gW, gH) in locs:
    groupOutput = []
    # 提取数字区域(适当扩大边界)
    group = gray[gY - 5:gY + gH + 5, gX - 5:gX + gW + 5]
    cv_show("Digit Group", group)

    # 二值化处理
    group = cv2.threshold(group, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
    cv_show("Group Threshold", group)

    # 提取单个数字轮廓
    group_, digitCnts, hierarchy = cv2.findContours(group.copy(), cv2.RETR_EXTERNAL,
                                                    cv2.CHAIN_APPROX_SIMPLE)
    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)

print("Credit Card Type: {}".format(FIRST_NUMBER.get(output[0], "Unknown")))
print("Credit Card #: {}".format("".join(output)))
cv2.imshow("Final Result", image)
cv2.waitKey(0)
cv2.destroyAllWindows()

四、实战 2:身份证号识别

身份证号识别的核心逻辑与银行卡号一致,但需针对身份证的特征调整预处理和轮廓筛选规则。

4.1 实现思路

  1. 身份证号是 18 位连续数字,区域更集中,宽高比更大;
  2. 身份证背景复杂有文字、图案,需增加高斯模糊降噪;
  3. 轮廓筛选条件需适配身份证号的尺寸特征(宽高比 5~30)。
python 复制代码
import numpy as np
import argparse
import cv2
import myutils

ap = argparse.ArgumentParser()
ap.add_argument("-i", "--image", default="sfz.jpg",
                help="path to input ID card image")
ap.add_argument("-t", "--template", default="haoma.png",
                help="path to template image (0-9 digits)")
args = vars(ap.parse_args())

template_img = cv2.imread(args["template"])
ref_gray = cv2.cvtColor(template_img, cv2.COLOR_BGR2GRAY)
ref_thresh = cv2.threshold(ref_gray, 10, 255, cv2.THRESH_BINARY_INV)[1]

_, ref_cnts, _ = cv2.findContours(ref_thresh.copy(), cv2.RETR_EXTERNAL,
                                  cv2.CHAIN_APPROX_SIMPLE)
ref_cnts = myutils.sort_contours(ref_cnts, method="left-to-right")[0]

digits = {}
for i, c in enumerate(ref_cnts):
    (x, y, w, h) = cv2.boundingRect(c)
    roi = ref_thresh[y:y + h, x:x + w]
    roi = cv2.resize(roi, (57, 88))
    digits[i] = roi

img = cv2.imread(args["image"])
img = myutils.resize(img, width=600)  # 缩放至宽度600
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
gray = cv2.GaussianBlur(gray, (3, 3), 0)  # 高斯模糊降噪

# 形态学操作突出数字
rectKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (15, 6))  # 调整卷积核尺寸
sqKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
tophat = cv2.morphologyEx(gray, cv2.MORPH_TOPHAT, rectKernel)
close1 = cv2.morphologyEx(tophat, cv2.MORPH_CLOSE, rectKernel)

# 二值化(调整阈值适配身份证)
thresh = cv2.threshold(close1, 60, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
close2 = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, sqKernel)

_, cnts, _ = cv2.findContours(close2.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

# 筛选身份证号轮廓
locs = []
for c in cnts:
    (x, y, w, h) = cv2.boundingRect(c)
    ar = w / float(h)
    if 5 < ar < 30 and (100 < w < 600) and (10 < h < 50):
        locs.append((x, y, w, h))

# 按y轴反向排序(身份证号通常在下方),取最下方的区域
locs = sorted(locs, key=lambda x: x[1], reverse=True)
(gX, gY, gW, gH) = locs[0]

groupOutput = []
# 提取身份证号区域(扩大边界,避免截断)
group = gray[max(0, gY - 10):min(gray.shape[0], gY + gH + 10),
             max(0, gX - 10):min(gray.shape[1], gX + gW + 10)]

# 预处理数字区域
group_thresh = cv2.threshold(group, 80, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (2, 2))
group_thresh = cv2.dilate(group_thresh, kernel, iterations=1)  # 膨胀增强数字

# 提取单个数字轮廓
_, digit_cnts, _ = cv2.findContours(group_thresh.copy(), cv2.RETR_EXTERNAL,
                                    cv2.CHAIN_APPROX_SIMPLE)
# 过滤小轮廓(噪声)
digit_cnts = [c for c in digit_cnts if cv2.boundingRect(c)[2] > 5 and cv2.boundingRect(c)[3] > 10]
digit_cnts = myutils.sort_contours(digit_cnts, method="left-to-right")[0]

# 模板匹配识别每个数字
for c in digit_cnts:
    (x, y, w, h) = cv2.boundingRect(c)
    if w < 5 or h < 10:
        continue
    # 计算绝对坐标,绘制矩形框
    abs_x = gX - 10 + x
    abs_y = gY - 10 + y
    cv2.rectangle(img, (abs_x, abs_y), (abs_x + w, abs_y + h), (0, 0, 255), 1)

    # 匹配模板
    roi = group_thresh[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)))

id_number = "".join(groupOutput)
cv2.putText(img, id_number, (gX, gY - 25),
            cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 0, 255), 2)

print("身份证号: " + id_number)
cv2.imshow("ID Card Result", img)
cv2.waitKey(0)
cv2.destroyAllWindows()
相关推荐
sell-cloud8 分钟前
Prompt Engineering要凉了?Fable 5.5能“猜“意图,但银行正在封杀AI Agent——企业AI的真正护城河是什么
人工智能·prompt
Navicat中国2 小时前
Navicat 18 内置 AI 助手让数据库开发从繁琐操作迈向智能协作
大数据·人工智能·数据库开发
4SAPI2 小时前
多模型请求路由怎么设计?成本、速度和能力如何平衡
人工智能·python·gateway·php
魔乐社区2 小时前
翼芯・智算测试与适配优化专区,让国产算力“选得准、跑得通、用得好“
人工智能·开源·大模型
XCDoris4 小时前
郴州logo设计机构
大数据·人工智能·python
Goboy4 小时前
一个人带三个Agent,我用全新TRAE从0到1搭了个资讯周刊站
人工智能·trae
俊哥V4 小时前
每日 AI 研究简报 · 2026-10-19
人工智能·ai
AI_AGENT_DEV_AI4 小时前
AI英语教培智能体的开发
人工智能
物联网软硬件开发-轨物科技5 小时前
【技术白皮书】光伏清检一体化机器人系统解决方案技术白皮书
大数据·运维·网络·人工智能·机器人
海宇数据5 小时前
零信任架构实战:基于海宇网上踪迹欺诈行为识别构建自动化信贷资信评估网关
运维·人工智能·架构·自动化