当前位置 博文首页 > 使用Python+OpenCV进行卡类型及16位卡号数字的OCR功能

    使用Python+OpenCV进行卡类型及16位卡号数字的OCR功能

    作者:程序媛一枚~ 时间:2021-09-16 17:51

    目录
    • 1. 效果图
    • 2. 原理
      • 2.1 OCR-A字体
      • 2.2 检测过程步骤
      • 2.3 优化
    • 3. 源代码

      这篇博客将介绍如何通过OpenCV和Python使用模板匹配执行光学字符识别(OCR)。具体来说,将使用Python+OpenCV实现模板匹配算法,以自动识别卡的类型和以及16位卡号数字。

      在比较数字时,模板匹配是一种非常快速的方法。

      为此将图像处理管道分为4个步骤:

      1. 通过各种图像处理技术检测信用卡上四组四个数字,包括形态学操作、阈值和轮廓提取。
      2. 从四个分组中提取每个单独的数字,得到16个需要分类的数字。
      3. 将模板匹配应用于每个数字,将其与OCR-A字体进行比较,以获得数字分类。
      4. 检查信用卡号的第一位数字以确定发卡公司。

      在对信用卡OCR系统进行评估后,发现如果发卡信用卡公司使用OCR-A字体作为数字,该系统的准确率为100%。 优化可以考虑在野外采集信用卡的真实图像,并训练机器学习模型(通过标准特征提取或训练或卷积神经网络),以进一步提高此系统的准确性。

      1. 效果图

      首先了解一下卡的组成:

      在这里插入图片描述

      OCR-A 参考字体识别如下:原始图 VS 灰度图 VS 阈值化图 VS 轮廓每个数字提取图:
      灰度图:忽略颜色对轮廓提取的影响
      阈值化图:使得轮廓在前景白色,背景黑色便于轮廓提取。
      轮廓提取图:提取每个数字ROI并记录,方便后续对比卡片中的区域以识别出对应的数字。

      在这里插入图片描述

      以下卡号均是演示卡,
      正确的识别卡的类型和卡号,效果图1:

      在这里插入图片描述

      识别过程1——原图 VS 灰度图 VS 白帽图 VS 梯度图如下:
      灰度图:忽略色彩影响
      白帽图:从较暗的背景中提取较亮的区域
      梯度图:计算Schaar梯度图,便于了解图像的色彩分配及提取;

      在这里插入图片描述

      识别过程2——形态学闭合图 VS 二值化图1 VS 阈值化图2 如下:
      形态学闭合图:矩形框形态学闭合操作,以帮助闭合信用卡数字之间的小的缝隙
      二值化图:以便于提取
      阈值化图:方形框形态学闭合操作,以二次帮助闭合信用卡数字区域之间的缝隙

      在这里插入图片描述

      识别过程3——轮廓过滤图 VS 提取最终效果图 如下:
      轮廓过滤图:根据面积及纵横比,只保留卡片中的卡号区
      最终效果图:提取4组4数字每一个组,然后对每一个组中的4个数字进行截取ROI并识别,并与之前存储的数字ROI进行模板匹配,选取匹配值最高的作为最终结果。

      在这里插入图片描述

      2. 原理

      2.1 OCR-A字体

      OCR-A字体,是一种专门用于辅助光学字符识别算法的字体。

      主要分为:

      检测图像中信用卡的位置;本地化信用卡上的四组四位数字;应用OCR识别信用卡上的16位数字;识别信用卡的类型。

      Tesseract库在某些情况无法正确识别数字(这可能是因为Tesseract未接受信用卡示例字体培训)。

      2.2 检测过程步骤

      在字典中存储卡类型映射关系(卡号的第一位数字代表卡类型)。获取参考图像并提取数字。将数字模板存储在字典中。本地化四个信用卡号组,每个组有四位数字(总共16位)。提取要“匹配”的数字。对每个数字执行模板匹配,将每个单独的ROI与每个数字模板0-9进行比较,同时存储每个尝试匹配的分数。查找每个候选数字的最高分数,并构建一个名为“输出”的列表。其中包含信用卡号。将信用卡号和信用卡类型输出到终端,并将输出图像显示到屏幕上。

      2.3 优化

      使用OpenCV和Python匹配OCR脚本的模板在100%的时间内正确识别了16位数字中的每一位。然而在将OCR图像应用于真实的信用卡图像时,考虑到照明条件、视角和其他一般噪音的变化,可能需要采取更面向机器学习的方法。

      3. 源代码

      # 信用卡类型及卡号OCR系统
      # USAGE
      # python ocr_template_match.py --reference images/ocr_a_reference.png --image images/credit_card_05.png
      
      import argparse
      
      import cv2
      import imutils
      import numpy as np
      # 导入必要的包
      from imutils import contours
      
      # 构建命令行参数及解析
      # --image 必须 要进行OCR的输入图像
      # --reference 必须 参考OCR-A图像
      ap = argparse.ArgumentParser()
      ap.add_argument("-i", "--image", required=True,
                      help="path to input image")
      ap.add_argument("-r", "--reference", required=True,
                      help="path to reference OCR-A image")
      args = vars(ap.parse_args())
      
      # 定义一个字典(映射信用卡第一位数字和信用卡类型的编号)
      FIRST_NUMBER = {
          "3": "American Express",
          "4": "Visa",
          "5": "MasterCard",
          "6": "Discover Card"
      }
      
      # 从磁盘加载参考OCR-A图像,转换为灰度图,阈值化图像以显示为白色前景和黑色背景
      # 并反转图像
      # and invert it, such that the digits appear as *white* on a *black*
      ref_origin = cv2.imread(args["reference"])
      cv2.imshow("ref_origin", ref_origin)
      ref = ref_origin.copy()
      ref = cv2.cvtColor(ref, cv2.COLOR_BGR2GRAY)
      cv2.imshow("ref_gray", ref)
      ref = cv2.threshold(ref, 180, 255, cv2.THRESH_BINARY)[1]
      cv2.imshow("ref_threshhold", ref)
      cv2.waitKey(0)
      
      # 寻找OCR-A图像中的轮廓(数字的外轮廓线)
      # 并从左到右排序轮廓,初始化一个字典来存储数字ROI
      refCnts = cv2.findContours(ref.copy(), cv2.RETR_EXTERNAL,
                                 cv2.CHAIN_APPROX_SIMPLE)
      print('findContours: ', len(refCnts))
      refCnts = imutils.grab_contours(refCnts)
      refCnts = contours.sort_contours(refCnts, method="left-to-right")[0]
      digits = {}
      
      # 遍历OCR-A轮廓
      for (i, c) in enumerate(refCnts):
          # 计算数字的边界框,提取它,缩放到固定的大小
          (x, y, w, h) = cv2.boundingRect(c)
          cv2.rectangle(ref_origin, (x, y), (x + w, y + h), (0, 255, 0), 2)
          roi = ref[y:y + h, x:x + w]
          roi = cv2.resize(roi, (57, 88))
      
          # 更新数字字典,数字匹配ROI
          digits[i] = roi
      cv2.imshow("ref and digits", ref_origin)
      cv2.waitKey(0)
      
      # 初始化矩形和方形结构内核
      # 在图像上滑动它来进行(卷积)操作,如模糊、锐化、边缘检测或其他图像处理操作。
      # 使用矩形函数作为Top-hat形态学运算符,使用方形函数作为闭合运算。
      rectKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (9, 3))
      sqKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
      
      # 准备进行OCR的输入图像
      # 加载输入图像,保持纵横比缩放图像宽度为300,转换为灰度图
      origin = cv2.imread(args["image"])
      origin = imutils.resize(origin, width=300)
      image = origin.copy()
      cv2.imshow("origin", origin)
      gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
      cv2.imshow("gray", gray)
      
      # 执行形态学操作
      # 应用tophat(白帽)形态学操作以在暗的背景中提取出亮的区域(信用卡上的数字卡号)
      # Top hat操作在深色背景(即信用卡号)下显示浅色区域
      tophat = cv2.morphologyEx(gray, cv2.MORPH_TOPHAT, rectKernel)
      cv2.imshow("tophat", tophat)
      
      # 计算Scharr梯度,计算梯度值
      # 在白色礼帽上,计算x方向的Scharr梯度,然后缩放到范围[0, 255]
      gradX = cv2.Sobel(tophat, ddepth=cv2.CV_32F, dx=1, dy=0, ksize=-1)
      gradX = np.absolute(gradX)
      (minVal, maxVal) = (np.min(gradX), np.max(gradX))
      # 最小/最大归一化, 由float转换gradX到uint8范围[0-255]
      gradX = (255 * ((gradX - minVal) / (maxVal - minVal)))
      gradX = gradX.astype("uint8")
      cv2.imshow("gradient", gradX)
      
      # 使用矩形框应用闭合操作以帮助闭合信用卡数字之间的小的缝隙
      # 应用Otsu's阈值方法二值化图像
      gradX = cv2.morphologyEx(gradX, cv2.MORPH_CLOSE, rectKernel)
      cv2.imshow("morphologyEx", gradX)
      thresh = cv2.threshold(gradX, 0, 255,
                             cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
      cv2.imshow("thresh1", thresh)
      
      # 在二值化图像上,应用二次闭合操作
      # 再一次方形框形态学操作,帮助闭合信用卡数字区域之间的缝隙
      thresh = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, sqKernel)
      cv2.imshow("thresh2", thresh)
      
      # 阈值图像中查找轮廓,然后初始化数字位置列表
      cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL,
                              cv2.CHAIN_APPROX_SIMPLE)
      cnts = imutils.grab_contours(cnts)
      locs = []
      
      # 遍历轮廓
      for (i, c) in enumerate(cnts):
          # 计算轮廓的边界框,并计算纵横比
          (x, y, w, h) = cv2.boundingRect(c)
          ar = w / float(h)
      
          # 由于信用卡有固定的4组4数字,可以根据纵横比来寻找潜在的轮廓
          if ar > 2.5 and ar < 4.0:
              # 轮廓可以在最小/最大宽度上进一步修剪
              if (w > 40 and w < 55) and (h > 10 and h < 20):
                  # 添加数字组轮廓的编辑框轮廓到位置list
                  locs.append((x, y, w, h))
                  cv2.rectangle(origin, (x, y), (x + w, y + h), (255, 0, 0), -1)
      
      cv2.imshow("contours filter", origin)
      # 突出显示信用卡上四组四位数字(总共十六位)。
      # 从左到右排序轮廓,并初始化list来存储信用卡数字列表
      locs = sorted(locs, key=lambda x: x[0])
      output = []
      
      # 遍历四组四位数字
      for (i, (gX, gY, gW, gH)) in enumerate(locs):
          # 初始化存放每组数字的list
          groupOutput = []
      
          # 提取每组4位数字的灰度图ROI
          # 应用阈值方法从背景信用卡中分割数字
          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)
          digitCnts = imutils.grab_contours(digitCnts)
          digitCnts = contours.sort_contours(digitCnts,
                                             method="left-to-right")[0]
      
          # 遍历数字轮廓
          for c in digitCnts:
              # 计算每个单独数字的边界框
              # 提取数字,缩放以拥有和参考OCR-A字体模板图像相同的大小
              (x, y, w, h) = cv2.boundingRect(c)
              roi = group[y:y + h, x:x + w]
              roi = cv2.resize(roi, (57, 88))
      
              # 初始化模板匹配分数list
              scores = []
      
              # 遍历参考数字名和数字ROI
              for (digit, digitROI) in digits.items():
                  # 应用基于相关性的模板匹配,计算分数,更新分数list
                  # apply correlation-based template matching, take the
                  # score, and update the scores list
                  result = cv2.matchTemplate(roi, digitROI,
                                             cv2.TM_CCOEFF)
                  (_, score, _, _) = cv2.minMaxLoc(result)
                  scores.append(score)
      
              # 数字ROI的分类将取 模板匹配分数中分数最大的参考数字
              # the classification for the digit ROI will be the reference
              # digit name with the *largest* template matching score
              groupOutput.append(str(np.argmax(scores)))
      
          # 围绕每组画一个矩形,并以红色文本标识图像上的信用卡号
          # 绘制每组的数字识别分类结果
          cv2.rectangle(image, (gX - 5, gY - 5),
                        (gX + gW + 5, gY + gH + 5), (0, 0, 255), 2)
          cv2.putText(image, "".join(groupOutput), (gX, gY - 15),
                      cv2.FONT_HERSHEY_SIMPLEX, 0.65, (0, 0, 255), 2)
      
          # 更新输出数字分组列表
          # Pythonic的方法是使用extend函数,它将iterable对象的每个元素(本例中为列表)追加到列表的末尾
          output.extend(groupOutput)
      
      # 显示检测到的信用卡类型和卡号到屏幕上
      print("Credit Card Type: {}".format(FIRST_NUMBER[output[0]]))
      print("Credit Card #: {}".format("".join(output)))
      cv2.imshow("Image", image)
      cv2.waitKey(0)

      参考 https://www.pyimagesearch.com/2017/07/17/credit-card-ocr-with-opencv-and-python/

      jsjbwy