OpenCV实践小项目(一): 信用卡数字识别

1. 写在前面

今天整理一个OpenCV实践的小项目, 前几天整理了一篇OpenCV处理图像的知识笔记,后面,就通过一些小项目把这些知识运用到实践中去,一个是加深理解,另一个是融会贯通,连成整体,因为我发现,如果这些东西不用的话,其实很快就会忘掉。 另外,就是我发现这些实践小项目非常使用,有些代码或者图像的处理技巧可以为以后所用,所以这也是我想整理下来的原因。

第一个实践项目是信用卡数字识别,就是给定一张信用卡, 做出下面的这种效果:

在这里插入图片描述
这个项目用到的知识其实在很多其他场景也会遇到,比如像车牌号识别检测,数字识别等,所以感觉还是比较实用的。 但其实, 本质上用到的知识并不复杂,完全是前面整理的OpenCV基本图像操作,那么究竟是如何做到的那?

下面首先分析这个项目的宏观实现逻辑,也就是拿到这样的一个小任务应该大致上怎么思考,然后给出具体的做法以及代码解释。

2. 实现逻辑

给定一个信用卡,最终要输出上面的卡号,且需要在原图中把卡号的位置圈出来。 本质上,这是一个模板匹配任务,如果想让计算机认识数字,我们需要给定一个模板,比如下面这个:

在这里插入图片描述
这样, 我们只要找到信用卡上的数字区域,然后拿着数字区域的数字一一与模板进行匹配,看看到底是啥数字,就能识别出来了。 但是,对于信用卡来说我们需要找到它的数字区域呀,对于给定的模板,我们虽然有它的数字区域,但是也得分割成一个个的数字,才能进行匹配工作呀,所以该任务,就转成了处理信用卡, 处理模板以及模板匹配三个子问题。 、

想起了小学学过的一篇课文《走一步,再走一步》。

如何处理信用卡,找到数字区域呢? 大致上思路如下:

  1. 使用轮廓检测算法,找到每个对象的大致轮廓以及外接矩形,即先定位到各个对象
  2. 找到对象轮廓之后,根据外接矩形的长宽比例,找到中间的这一长串数字部分,由于这个轮廓比较长比较窄,所以还是比较好找的
  3. 对于这一长串数字,用形态学操作使其更加突出,让这部分更加精准
  4. 接下来,对于这一部分,再次进行轮廓检测,分割成了四个小块,对于每个小块再进行轮廓检测,就能得到每个具体的数字了
  5. 对于每个数字,与模板进行匹配(直接有函数可用),就知道是几了。

如果处理模板呢?这个很简单。轮廓检测一次,就能找到这10个对象,然后给每个对象赋予值,然后建立成一个字典即可。

下面就一步一步的进行代码解释。

3. 处理模板图像

模板图像先进行三步操作: 读入 -> 转成灰度图 -> 二值化, 因为轮廓检测函数接收的是二值图。

# 读取模板图像
img = cv2.imread("images/ocr_a_reference.png")   # 读取的时候转灰度 cv2.imread("images/ocr_a_reference.png", 0)
# 转成灰度图
template = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 二值图像
template = cv2.threshold(template, 10, 255, cv2.THRESH_BINARY_INV)[1]

结果如下:
在这里插入图片描述

接下来,用cv2的轮廓检测函数拿到10个数字的轮廓

cv2.findContours()函数接受的参数为二值图, 即黑白图像(不是灰度图), cv2.RETR_EXTERNAL只检测外轮廓, cv2.CHAIN_APPROX_SIMPLE只保留终点坐标

# 最新版opencv只返回两个值了 3.2之后, 不会返回原来的二值图像了,直接返回轮廓信息和层级信息
contourss, hierarchy = cv2.findContours(template.copy(), cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)

len(contourss)  # 10个轮廓

效果如下:
在这里插入图片描述
这样就找到了每个数字的外轮廓,一个10个,但是要注意下,这10个轮廓的排列顺序并不一定是按照上面这个0-9的轮廓对应着来的,所以为了保险起见,我们需要根据每个轮廓左上角的坐标值,先从小到大排序。

# 下面将轮廓进行排序,这是因为必须保证轮廓的顺序是0-9的顺序排列着
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]  # 用一个最小矩形,把找到的形状包起来x,y,h,w
    
    # 根据每个轮廓左上角的点进行排序, 这样能保证轮廓的顺序就是0-9的数字排列顺序
    (cnts, boundingBoxes) = zip(*sorted(zip(cnts, boundingBoxes), key=lambda x:x[1][i], reverse=reverse))
    
    return cnts, boundingBoxes 

refCnts = sort_contours(contourss, method='left-to-right')[0]  

这样每个轮廓就按照0-9排列好了, 那下面思路就很清晰了,遍历每个轮廓对象,给他附上真正的数字即可,即建立数字->轮廓的关联映射。

# 每个轮廓进行数字编号
digits2Cnt = {
    
    }
# 遍历每个轮廓
for i, c in enumerate(refCnts):
    # 计算外接矩形,并且resize成合适大小
    (x, y, w, h) = cv2.boundingRect(c)
    # 单独把每个数字框拿出来 坐标系竖着的是y, 横着的是x
    roi = template[y:y+h, x:x+w] 
    # 重新改变大小
    roi = cv2.resize(roi, (57, 88))
    
    # 框与字典对应
    digits2Cnt[i] = roi

# 把处理好的模板进行保存
pickle.dump(digits2Cnt, open('digits2Cnt.pkl', 'wb'))

这里有两个点,首先对于每个轮廓,先计算它的外接矩形,也就是先框起来,然后从原始的模板图像中,拿出这个框,这才是每个数字。 然后为了能和后面信用卡上的数字进行匹配,这里还需要resize下。

这样模板图像处理完毕,拿到了ditits2Cnt字典,字典的键就是数字值,而值就是模板中的轮廓对象。

4. 处理信用卡并进行匹配

信用卡这部分要稍微复杂一些,因为我们还得先定位到信用卡上的数字区域,然后通过一些操作对这块区域增强等。

第一步,读取图像,改变大小,并转成灰度图。

# 读取图像
base_path = 'images'
file_name = 'credit_card_01.png'
credit_card = cv2.imread(os.path.join(base_path, file_name))
credit_card = resize(credit_card, width=300)
credit_gray = cv2.cvtColor(credit_card, cv2.COLOR_BGR2GRAY)

效果如下:
在这里插入图片描述
接下来,进行顶帽操作, 这个操作可以突出更加明亮的区域,而黑帽操作可以突出更加黑暗的区域。

# 顶帽操作,突出更明亮的区域

# 初始化卷积核
rectKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (9, 3))  # 自定义卷积核的大小了
sqKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))

tophat = cv2.morphologyEx(credit_gray, cv2.MORPH_TOPHAT, rectKernel)

效果如下:
在这里插入图片描述
接下来,要进行边缘检测, 把上面的各个对象的边缘给他突出出来。边缘检测那里我们学习了水平边缘检测,垂直边缘检测,以及两者的合并操作,往往效果较好。 但这里发现单独水平边缘检测就可以。

# 水平边缘检测  
gradX = cv2.Sobel(tophat, ddepth=cv2.CV_32F, dx=1, dy=0, ksize=-1)  # 水平边缘检测
# gradX = cv2.convertScaleAbs(gradX)    这个操作会把一些背景边缘也给检测出来,加了一些噪声

# 所以下面手动归一化操作
gradX = np.absolute(gradX)
(minVal, maxVal) = (np.min(gradX), np.max(gradX))
gradX = (255 * ((gradX-minVal) / (maxVal-minVal)))
gradX = gradX.astype('uint8')

# 这里也可以按照之前的常规, 先水平,后垂直,然后合并,但是效果可能不如单独x的效果好

效果如下:
在这里插入图片描述
目前确实能找到边缘了,但是想把数字挨着近的连接成片,就需要用到形态学相关操作了。

# 闭操作: 先膨胀, 后腐蚀  膨胀就能连成一块了
gradX = cv2.morphologyEx(gradX, cv2.MORPH_CLOSE, rectKernel)

效果如下:
在这里插入图片描述
然后会发现,数字虽然大部分连成一块一块的了,但是有些地方有些黑洞,且颜色还不是特别命令和明显,所以下面转成二值图片,突出对象,阈值+闭操作增强。

#THRESH_OTSU会自动寻找合适的阈值,适合双峰,需把阈值参数设置为0  让opencv自动的去做判断,找合适的阈值,这样就能自动找出哪些有用,哪些没用
thresh = cv2.threshold(gradX, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1] 
cv_show('thresh',thresh)
#再来一个闭操作
thresh = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, sqKernel) #再来一个闭操作

效果如下:

在这里插入图片描述
接下来的话,就能很容易的通过轮廓检测算法找到轮廓,但是如果想拿到数字的轮廓,这里还需要根据长宽比例进行筛选。

threshCnts, hierarchy = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = threshCnts
cur_img = credit_card.copy()

# 把轮廓画出来
cv2.drawContours(cur_img, cnts, -1, (0, 0, 255), 3)
cv_show('img', cur_img)

算法找到的轮廓如下:
在这里插入图片描述
接下来遍历每个轮廓,锁定住中间的四个数字轮廓:

# 找到包围数字的那四个大轮廓
locs = []
# 遍历轮廓
for i, c in enumerate(cnts):
    # 计算外接矩形
    (x, y, w, h) = cv2.boundingRect(c)
    ar = w / float(h)
    
    # 选择合适的区域, 这里的基本都是四个数字一组
    if ar > 2.5 and ar < 4.0:
        if (w > 40 and w < 55) and (h > 10 and h < 20):
            # 符合
            locs.append((x, y, w, h))

# 轮廓从左到右排序
locs = sorted(locs, key=lambda x: x[0])

这里的操作依然是先用外接矩形包起对象,然后再进行选择。这样就拿到了四个大轮廓。

接下来就非常简单了:

  1. 遍历每个大轮廓

    1. 对于每个轮廓, 进行和处理模板一样的操作,就能拿到数字
    2. 对于每个数字,进行模板匹配即可
    outputs = []
    
    # 遍历每一个轮廓中的的数字
    for (i, (gX, gY, gW, gH)) in enumerate(locs):
        # 初始化组
        groupOutput = []
        
        # 根据坐标提取每一组
        group = credit_gray[gY-5:gY+gH+5, gX-5:gX+gW+5]  # 有5的一个容错长度
        
        # 对于这每一组,先预处理  
        # 二值化,自动寻找合适阈值,增强对比,更突出有用的部分,即数字
        group = cv2.threshold(group, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
        
        # 计算每一组的轮廓
        digitCnts, hierarchy = cv2.findContours(group.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
        digitCnts = sort_contours(digitCnts, method='left-to-right')[0]
        
        # 拿到每一组的每一个数字,然后进行模板匹配
        for c in digitCnts:
            # 找到当前数值的轮廓,resize成合适的大小
            (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 digits2Cnt.items():
                result = cv2.matchTemplate(roi, digitROI, cv2.TM_CCOEFF)
                (_, score, _, _) = cv2.minMaxLoc(result)
                scores.append(score)
            
            # 得到合适的数字
            # 这是个列表,存储的每个小组里面的数字识别结果
            groupOutput.append(str(np.argmax(scores)))
        
        # 画出来
        cv2.rectangle(credit_card, (gX - 5, gY - 5), (gX + gW + 5, gY + gH + 5), (0, 0, 255), 1)
        cv2.putText(credit_card, "".join(groupOutput), (gX, gY - 15), cv2.FONT_HERSHEY_SIMPLEX, 0.65, (0, 0, 255), 2)
        
        # 合并到最后的结果里面
        outputs.extend(groupOutput)
    
  2. 输出结果

    # 打印结果
    print("Credit Card Type: {}".format(FIRST_NUMBER[outputs[0]]))
    print("Credit Card #: {}".format("".join(outputs)))
    cv2.imshow("Image", credit_card)
    

5. 小总

这个项目到这里就结束了,整体比较简单,但是这里面涉及到的很多知识点都比较常用。总结如下:

  1. 图像的读取 ->转灰度->二值化操作
  2. 找轮廓操作(cv2.findContours)
  3. 基本的形态学操作(顶帽,黑帽,开闭,膨胀腐蚀)
  4. 边缘检测操作(Sobel算子, Sharr算子等)
  5. 轮廓排序,一定要注意,找到的轮廓数组可能是乱序的
  6. 画外接矩形,然后拿出具体的某个对象

当然,并没有涉及到很复杂的逻辑,全是Opencv的基础函数以及python基础操作,算是图像处理的一个小入门项目吧。

本次项目代码地址https://github.com/zhongqiangwu960812/OpenCVLearning, 感兴趣的可以玩一下啦。

猜你喜欢

转载自blog.csdn.net/wuzhongqiang/article/details/123796571