ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

基于OpenCV轮廓检测与TensorFlow CNN的银行卡号识别实践

2026/9/12 9:07:04 拓冰建站 浏览量
基于OpenCV轮廓检测与TensorFlow CNN的银行卡号识别实践 简介一套面向银行卡号识别的完整工程资源基于Python3.6、OpenCV3与TensorFlow的卷积神经网络方案实现适用于计算机、人工智能、自动化、电子信息等相关专业的在校学生和教师可用于毕业设计、课程设计、作业提交或项目初期演示也适合希望入门深度学习图像识别的小白作为案例研习。压缩包共88个文件约47.99MB文件类型覆盖训练与推理全流程Jupyter Notebook负责模型训练与识别演示Java与Vue构建可交互的前后端界面JS、JSON和BMP等提供配置与测试图像model.pb为预训练模型另有详细的Word/PDF文档辅助阅读。整个工程已通过导师指导答辩评审分达95分代码经测试运行成功可完整体验从样本准备、CNN模型训练、字符切分到界面部署的识别流程压缩包内还附带一份OpenCV与TensorFlow子项目便于对照不同实现思路。目前已有56人学习下载适合作为课程设计、毕业设计参考也可作为深度学习实践的有力起点。1. 银行卡识别为什么不只靠 CNN还需要 OpenCV 的轮廓检测把一个银行卡号从照片里安全识别出来最反直觉的一点是真正的好坏不由后面的 CNN 深度决定而由前端 OpenCV 定位决定。这套基于 Python3.6 OpenCV3 TensorFlow 的银行卡识别资料故意把卡号定位与数字识别拆成两段OpenCV3 负责找区域、切字符TensorFlow 里跑一个很小的 CNN 只做最后分类。拆开资源会发现它既有 train.ipynb/test.ipynb 的 Python 训练侧也有带 pom.xml 的 Maven server 端和 model.pb还包含前端 client相当于给你一组可复现的“训练—导出—部署”样例。对正在做课设、毕设的在校学生或者想快速搭建 OCR 演示的工程师这份材料有直接参考价值。2. 先在 Python 3.6 里把 OpenCV3 环境拉起来再定位卡号区域2.1 环境依赖里最容易被忽略的版本约束资源标题直接锁定了 Python3.6 和 OpenCV3这不是拍脑袋。OpenCV3 的cv2.Sobel、cv2.findContours返回形式与 OpenCV4 略有差异很多网上代码直接用 OpenCV4 跑这套工程会撞上返回值个数不一致或排序行为变化真正更麻烦的是 TensorFlow在 Python 3.6 环境下装过高版本的 tensorflow 会直接提示找不到匹配版本。更稳的组合是先用虚拟环境固定版本再逐个装依赖。我这里用一个和项目兼容的固定组合python -m venv bankcard_env source bankcard_env/bin/activate pip install opencv-python3.4.2.17 pip install tensorflow1.15.0 pip install jupyter提示如果本机已经装过高版本 numpy装 tf1.x 时需同步降到numpy1.20否则 import tensorflow 会报 illegal instruction 错误。上面的组合把 OpenCV 锁在 3.4.x把 TensorFlow 锁在 1.15这样 Jupyter notebook 里四个核心文件train.ipynb、test.ipynb、recognition.ipynb、trainTest.ipynb的接口与经典 CNN 教程对齐不需要大改模型读取逻辑。如果执意用 TensorFlow 2.x 也能跑但model.pb导出和import_graph_def加载路径会多一层兼容代码起步阶段没必要引入。环境跑通后打开压缩包里的test_images目录找一张卡面干净的正面图来验证预处理后续所有步骤都按“单张图先跑通再上批量”推进避免定位参数还没调好就进入模型训练。2.2 灰度化、直方图均衡和高斯模糊先让卡面变成易切的状态银行卡一旦拍照光照不均是常态。直接对彩色图做轮廓检测会受到卡面图案干扰整张卡的颜色渐变和 logo 纹理都会变成噪声所以第一步先做色彩降维import cv2 import numpy as np img cv2.imread(test_images/card_sample.jpg) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) gray cv2.equalizeHist(gray) gray cv2.GaussianBlur(gray, (5, 5), 0)cvtColor把三通道压成单通道后续 Sobel 和形态学操作的计算量会小一倍以上equalizeHist对直方图做全局均衡把高光和阴影拉回同一亮度区间这一步在卡号压花有反光时尤其重要GaussianBlur的核取 5×5能压掉传感器噪声又不会把数字笔画粘在一起。这个阶段不急着做二值化因为光照不均时固定阈值很容易把部分数字和背景融在一起保持灰度图做梯度计算更稳妥等锁定 ROI 后再在小区域里做二值化。2.3 基于 Sobel 边缘和形态学闭运算的卡号区域定位卡号通常是一段凸起的打印数字在水平方向上有明显梯度响应。用 Sobel 水平梯度比 Canny 好控制因为 Canny 会把商标和姓名压花也检测成边缘候选区域会多出一倍Sobel 只保留横向亮度变化卡号带的横边最连续。grad_x cv2.Sobel(gray, cv2.CV_16S, 1, 0, ksize3) abs_grad_x cv2.convertScaleAbs(grad_x) # 闭运算把分散的数字笔画连成完整字块 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (17, 5)) closed cv2.morphologyEx(abs_grad_x, cv2.MORPH_CLOSE, kernel)这里ksize3的 Sobel 对细笔画敏感MORPH_RECT核宽 17、高 5是按照银行卡号笔画宽度和数字间距估算出来的。如果测试图分辨率更高或卡片离镜头更近这个核要同步放大通常以“数字之间能粘连、整段号码不被拆散”为准。做完闭运算后卡号区域会变成一个高亮的矩形条再用轮廓提取收拢候选框。用findContours提取候选区域时没有在第一步去找“数字”而是依据几何形状筛contours, hierarchy cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) for cnt in contours: x, y, w, h cv2.boundingRect(cnt) if w 80 and h 20: aspect w / float(h) if 4.0 aspect 8.0: roi gray[y:y h, x:x w]卡号区的宽高比一般落在 4 到 8 之间卡面 Logo 和芯片要么太小要么宽高比不满足筛选后基本只剩卡号带。cv2.boundingRect返回的x, y, w, h四元组可以理解为矩形的坐标和尺寸后边画框、切图、排序都会用到这个结果。2.4 字符级分割垂直投影把一连串数字切分成单字拿到整条卡号 ROI 后先做局部二值化再做垂直投影binary cv2.threshold(roi, 0, 255, cv2.THRESH_BINARY_INV cv2.THRESH_OTSU)[1] col_sum np.sum(binary, axis0) start None char_regions [] for i, v in enumerate(col_sum): if start is None and v 8: start i elif start is not None and v 8: char_regions.append((start, i)) start None if start is not None: char_regions.append((start, len(col_sum)))THRESH_OTSU会在小块 ROI 上自动算阈值避免全局阈值对暗部失效。col_sum表示每列像素中亮像素的总和数字之间会产生明显低谷连续非零区间就是单个字符。判断阈值 8 表示允许两像素的噪声列干扰如果有烫金浮雕反光可把 8 调到 15但太高会把数字 1 和 7 粘在一起。切出来后的每个 char 还要统一 resize 到模型输入尺寸再做归一化后送给 CNN。到这里OpenCV 侧的「定位与分割」已经闭环剩下的活交给 TensorFlow。3. 用 TensorFlow CNN 训练银行卡数字模型并导出 model.pb3.1 CNN 结构图背后的两个直觉局部感受野和参数共享很多初学者直接对着 cnn 结构图去背层名却忽略 CNN 适合小尺寸字符分类的关键原因。数字识别和普通图像分类不同字符没有复杂的空间布局只是局部笔画组合。卷积层用 3×3 或 5×5 的滑动核捕捉局部边缘池化层压缩位置信息全连接层把所有局部特征汇总成类别概率。参数共享让 28×28 输入不用展开成 784 个独立特征而是用同一组卷积核在整张图上复用这样训练参数量大幅下降少量样本也能拟合。银行卡数字集比 MNIST 更干净但也更单一存在字体一致、数字位置固定的特点。如果模型结构照搬 50 层 ResNet反而容易在几百张自定义样本上过拟合把背景纹理当特征学进去。这个小项目里的 CNN 做得比较克制通常是两层卷积加一层全连接已经足够。理解结构图里的Conv - MaxPool - Flatten - Dense主链再去看 notebook 里的实际代码会顺畅很多。3.2 适合银行卡数字识别的卷积网络设计用 Keras 搭一个可直接替换的分类头import tensorflow as tf from tensorflow.keras import layers, models model models.Sequential([ layers.Input(shape(28, 28, 1)), layers.Conv2D(32, (3, 3), activationrelu, paddingsame), layers.MaxPooling2D((2, 2)), layers.Conv2D(64, (3, 3), activationrelu, paddingsame), layers.MaxPooling2D((2, 2)), layers.Flatten(), layers.Dense(128, activationrelu), layers.Dropout(0.5), layers.Dense(10, activationsoftmax) ]) model.compile(optimizeradam, losssparse_categorical_crossentropy, metrics[accuracy]) model.summary()网络尺寸对照如下层输出尺寸作用Conv2D 3×3, 3228×28×32提取笔画边缘和局部纹理MaxPooling 2×214×14×32降低分辨率增强平移容忍Conv2D 3×3, 6414×14×64在降维后的图上提取组合特征MaxPooling 2×27×7×64压缩空间信息Dense 128 Dropout128特征映射到类别防过拟合Dense 1010输出 0~9 概率分布Dropout(0.5)在样本量只有几百张时很关键训练时随机丢弃一半神经元能显著减少对训练集的死记硬背。paddingsame保证卷积不缩小特征图尺寸让池化层统一负责降采样。3.3 训练与验证train.ipynb / test.ipynb 的常用跑法notebook 里的训练流程通常把分割好的字符喂给model.fit。如果直接从 MNIST 接口读数据则要保持输入尺寸和灰度范围一致history model.fit( train_images, train_labels, batch_size32, epochs10, validation_split0.2 )batch_size32对小数据集稳定epochs10对这个规模基本够如果验证准确率还在上升把 epochs 加到 1520。看test.ipynb时注意它很可能直接加载model.pb做推理所以训练完成后必须把权重保存和导出两步做扎实否则下次重启 kernel 一切归零。训练侧可以用model.save_weights(card_digits.ckpt)保存权重方便中断后续训。提示不要去改数据集里图片的灰度范围训练时图片值域是 0~255预测时也必须先做相同倍数的归一化否则 CNN 输出概率几乎全集中在一类上。3.4 把训练好的模型冻结成 model.pb 给 Java 或 Python 推理用资源里的 server 端是 Maven 工程pom.xml和mvnw说明部署侧是 Java Spring 一类服务为了方便 Java 加载TensorFlow 模型通常要冻结成单个图文件。在 TensorFlow 1.x 里常见做法是from tensorflow.python.framework import convert_variables_to_constants sess tf.compat.v1.Session() # 假设已有训练好的 saver saver.restore(sess, ./card_digits.ckpt) frozen_graph convert_variables_to_constants(sess, sess.graph_def, [dense_2/Softmax]) with open(model.pb, wb) as f: f.write(frozen_graph.SerializeToString())冻结的本质是把变量节点替换成常量让推理不再依赖 checkpoint 文件位置。output_names必须对应最后一层 Softmax 的节点名写错时 Java 端会找不到输出张量。如果看到testWeight16.bin、npclass.bin这类文件通常是旧版本工程里直接保存的权重矩阵或类别映射server 启动时会按特定路径读取复制到类路径下时要保持相对路径不变避免FileNotFoundException。4. 把 OpenCV 分割和 CNN 推理拼成一条端到端识别流水线4.1 从 test_images 到输出号码的完整脚本前两章的处理都是片段真正交作业时要能一条命令跑完。打包识别函数import cv2 import numpy as np def predict_card_number(img_path): img cv2.imread(img_path) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) gray cv2.equalizeHist(gray) blur cv2.GaussianBlur(gray, (5, 5), 0) grad_x cv2.Sobel(blur, cv2.CV_16S, 1, 0, ksize3) abs_grad_x cv2.convertScaleAbs(grad_x) kernel cv2.getStructuringElement(cv2.MORPH_RECT, (17, 5)) closed cv2.morphologyEx(abs_grad_x, cv2.MORPH_CLOSE, kernel) contours, _ cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) candidates [] for cnt in contours: x, y, w, h cv2.boundingRect(cnt) if w 80 and h 20 and 4.0 w / h 8.0: candidates.append((x, y, w, h)) candidates.sort(keylambda r: r[0]) if not candidates: return x, y, w, h candidates[0] roi blur[y:y h, x:x w] binary cv2.threshold(roi, 0, 255, cv2.THRESH_BINARY_INV cv2.THRESH_OTSU)[1] col_sum np.sum(binary, axis0) digits [] start None for i, v in enumerate(col_sum): if start is None and v 8: start i elif start is not None and v 8: digits.append(binary[:, start:i]) start None return digits这个函数把 2.2 到 2.4 的所有步骤收拢成单一入口。返回的digits列表里每个元素都是一张字符图后续循环调用模型预测即可。排序逻辑candidates.sort(keylambda r: r[0])必不可少findContours返回的轮廓顺序和图像命中区域没有必然关联不按 x 坐标排序的话卡号顺序会乱掉。4.2 server 端和 client 端的分工model.pb、testWeight16.bin 怎么放项目目录里有server和client两套代码说明这不是单纯的 Python 脚本。常见做法是 server 用 Java 封装模型推理接口client 负责上传卡片照片并展示结果。Maven 工程里model.pb和各类.bin权重文件放在src/main/resources下Java 类通过ClassPathResource读取而不是写绝对路径。如果你只想做毕设演示也可以不碰 Java直接用 Flask 在 Python 里加载同一份model.pbwith tf.compat.v1.Graph().as_default() as graph: graph_def tf.compat.v1.GraphDef() with open(model.pb, rb) as f: graph_def.ParseFromString(f.read()) tf.import_graph_def(graph_def, name) input_tensor graph.get_tensor_by_name(dense_input:0) output_tensor graph.get_tensor_by_name(dense_2/Softmax:0)dense_input:0要和各 notebook 里的输入节点名一致。如果在导入时改名后续get_tensor_by_name会报错最简单的方式是导入时name保持命名空间干净否则所有节点名都要加import/前缀。4.3 光照不匀和倾斜卡片的处理窗口银行卡识别失败通常发生在两个场景卡片倾斜角度超过 30 度以及卡号区域反光导致数字中间有高光斑块。对前者先做透视矫正再进定位流程pts_src np.float32([[x0, y0], [x1, y0], [x1, y1], [x0, y1]]) pts_dst np.float32([[0, 0], [width, 0], [width, height], [0, height]]) M cv2.getPerspectiveTransform(pts_src, pts_dst) card_rect cv2.warpPerspective(img, M, (width, height))对后者局部直方图均衡比全局效果好cv2.createCLAHE(clipLimit2.0, tileGridSize(8,8))能限制对比度放大强度避免高光区域完全过曝。这些都属于参数微调窗口当定位框出现上下漂移时优先检查equalizeHist是否让暗色卡面上的卡号更清晰而不是急着改 CNN 结构。5. 旧模型、新图像用 test_images 验证识别结果的三个实用技巧5.1 先跑单张图而不是整个测试集拿到资源先跑recognition.ipynb里的单图演示确认预处理输出每一阶段的图像都正常再跑test.ipynb批量测试。批量测试一旦失败排查顺序是定位框为空、分割顺序错乱、相似数字混淆。给每张中间图加cv2.imwrite是最快的定位手段比如把closed、binary、每个字符都落盘就能看出是定位失败还是分类失败。5.2 用 OpenCV 的矩形画出卡号区域并检查识别结果for i, (x, y, w, h) in enumerate(candidate_rects): cv2.rectangle(img, (x, y), (x w, y h), (0, 255, 0), 2) cv2.putText(img, str(results[i]), (x, y - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2)cv2.rectangle的第二个参数是左上角坐标第三个是右下角如果误把(xw, yh)写成(w, h)画框会偏移到图像边缘。把识别数字直接写在框上方可以一眼看出哪一位判错常见错误是 3 和 8、4 和 9 混淆出现这种情况时增加训练样本中对应字符的旋转和模糊增强即可。5.3 环境报错的快速处理表报错信息原因处理方式ModuleNotFoundError: No module named cv2OpenCV 未安装或环境串了在虚拟环境执行pip install opencv-pythonTypeError: NoneType object is not subscriptable图片路径错误或imread返回空用os.path.abspath确认路径避免中文目录InvalidArgumentError: Input tensor should be 4D单张字符图没有加 batch 维调用预测前做char_img[np.newaxis, ...]NodeDef mentions attr dtype报错TensorFlow 版本与 pb 文件不一致统一使用 tf1.15 加载冻结图最后一个小技巧识别结果拼接时不要用.join(str(d) for d in digits)而是把每个字符的置信度也带出来低于 0.9 的位标成红色。这样答辩或演示时能直接证明系统不是死记硬背而是有可解释的输出。把这段可视化代码放在识别循环里每次失败留下的不是一串孤零零的数字而是可以直接去检查坐标、置信度和预处理图的完整证据链。本文还有配套的精品资源点击获取