一、前言》》》》》》》
在现实生活中,发票、身份证和纸张通常不是完全正对着摄像头拍摄的。由于拍摄角度不同,原本的矩形会变成梯形,文字也会发生倾斜。
如果直接对这种图片进行识别,效果往往不理想。因此,通常需要先完成透视变换,把图像矫正成正面视图,再进行二值化、轮廓检测和模板匹配。
本篇主要介绍:
1,透视变换的原理;
2,四个角点的排序;
3,发票图像矫正;
4,形态学闭运算;
5,使用模板匹配识别身份证号码。
二、透视变换的基本原理
透视变换可以把原图中的任意四边形转换成一个规整的矩形。
例如,一张倾斜的发票有四个角:
左上、右上、右下、左下
通过透视变换后,这四个点会被映射到:
1(0, 0) 2(width, 0) 3(width, height) 4(0, height)
这样,发票就会被“拉正”。
1,透视变换的基本流程为:读取图像
2,缩放图像
3,灰度化
4,二值化
5,查找轮廓
6,找到最大四边形
7,计算透视矩阵
8,得到矫正结果
三、调整图片尺寸
发票图片通常比较大,直接处理会占用较多内存,因此可以先进行缩小。
1def resize( 2 image, 3 width=None, 4 height=None, 5 inter=cv2.INTER_AREA 6): 7 dim = None 8 h, w = image.shape[:2] 9 10 if width is None and height is None: 11 return image 12 13 if width is None: 14 ratio = height / float(h) 15 dim = ( 16 int(w * ratio), 17 height 18 ) 19 else: 20 ratio = width / float(w) 21 dim = ( 22 width, 23 int(h * ratio) 24 ) 25 26 return cv2.resize( 27 image, 28 dim, 29 interpolation=inter 30 )
使用方法:
1image = cv2.imread( 2 "fapiao.jpg" 3) 4 5orig = image.copy() 6 7ratio = image.shape[0] / 500.0 8 9image_small = resize( 10 orig, 11 height=500 12)
这里的 ratio 用于记录原图和缩小图之间的比例。后面在原图上进行透视变换时,需要将缩小图中得到的坐标还原回原始尺寸。
四、灰度化和自动阈值
1gray = cv2.cvtColor( 2 image_small, 3 cv2.COLOR_BGR2GRAY 4)
然后使用 Otsu 自动阈值法:
1edged = cv2.threshold( 2 gray, 3 0, 4 255, 5 cv2.THRESH_BINARY | cv2.THRESH_OTSU 6)[1]
Otsu 方法会根据图像的灰度分布自动寻找阈值,不需要我们手动设置具体数值。
普通二值化的写法是:
1ret, binary = cv2.threshold( 2 gray, 3 120, 4 255, 5 cv2.THRESH_BINARY 6)
而 Otsu 的阈值参数设置为 0:
1cv2.threshold( 2 gray, 3 0, 4 255, 5 cv2.THRESH_BINARY | cv2.THRESH_OTSU 6)
OpenCV 会自动计算合适的阈值。
五、查找发票轮廓
1contours = cv2.findContours( 2 edged.copy(), 3 cv2.RETR_LIST, 4 cv2.CHAIN_APPROX_SIMPLE 5)[-2]
将所有轮廓画出来:
1image_contours = cv2.drawContours( 2 image_small.copy(), 3 contours, 4 -1, 5 (0, 0, 255), 6 1 7) 8 9cv2.imshow( 10 "contours", 11 image_contours 12) 13 14cv2.waitKey(0)
发票通常是图像中面积较大的矩形,所以可以按照面积排序:
1screen_cnt = sorted( 2 contours, 3 key=cv2.contourArea, 4 reverse=True 5)[0]
接着计算周长:
1peri = cv2.arcLength( 2 screen_cnt, 3 True 4)
对轮廓进行近似:
1screen_cnt = cv2.approxPolyDP( 2 screen_cnt, 3 0.05 * peri, 4 True 5)
如果最终轮廓有四个点,就可以认为它是发票的四个角:
print(screen_cnt.shape)
六、四个角点排序
透视变换要求四个角点按照固定顺序排列,所以需要编写排序函数:
1import numpy as np 2 3def order_points(points): 4 rect = np.zeros( 5 (4, 2), 6 dtype="float32" 7 ) 8 9 total = points.sum(axis=1) 10 11 rect[0] = points[np.argmin(total)] 12 rect[2] = points[np.argmax(total)] 13 14 diff = np.diff( 15 points, 16 axis=1 17 ) 18 19 rect[1] = points[np.argmin(diff)] 20 rect[3] = points[np.argmax(diff)] 21 22 return rect
排序原理:
0,横坐标加纵坐标最小的点是左上角;
2,横坐标加纵坐标最大的点是右下角;
1,纵坐标减横坐标最小的点是右上角;
3,纵坐标减横坐标最大的点是左下角。
如果角点顺序混乱,透视后的图片可能会出现倒置或旋转。
七、计算变换后尺寸
假设四个角点为:
计算上下两条边的长度:
1width_a = np.sqrt( 2 ((br[0] - bl[0]) ** 2) + 3 ((br[1] - bl[1]) ** 2) 4) 5 6width_b = np.sqrt( 7 ((tr[0] - tl[0]) ** 2) + 8 ((tr[1] - tl[1]) ** 2) 9) 10 11max_width = max( 12 int(width_a), 13 int(width_b) 14)
计算左右两条边的长度:
1height_a = np.sqrt( 2 ((tr[0] - br[0]) ** 2) + 3 ((tr[1] - br[1]) ** 2) 4) 5 6height_b = np.sqrt( 7 ((tl[0] - bl[0]) ** 2) + 8 ((tl[1] - bl[1]) ** 2) 9) 10 11max_height = max( 12 int(height_a), 13 int(height_b) 14)
定义目标矩形的四个点:
1dst = np.array([ 2 [0, 0], 3 [max_width - 1, 0], 4 [max_width - 1, max_height - 1], 5 [0, max_height - 1] 6], dtype="float32")
八、执行透视变换
获取透视变换矩阵:
1M = cv2.getPerspectiveTransform( 2 rect, 3 dst 4)
执行透视变换:
1warped = cv2.warpPerspective( 2 image, 3 M, 4 (max_width, max_height) 5)
完整函数如下:
1def four_point_transform(image, points): 2 rect = order_points(points) 3 tl, tr, br, bl = rect 4 5 width_a = np.sqrt( 6 ((br[0] - bl[0]) ** 2) + 7 ((br[1] - bl[1]) ** 2) 8 ) 9 10 width_b = np.sqrt( 11 ((tr[0] - tl[0]) ** 2) + 12 ((tr[1] - tl[1]) ** 2) 13 ) 14 15 max_width = max( 16 int(width_a), 17 int(width_b) 18 ) 19 20 height_a = np.sqrt( 21 ((tr[0] - br[0]) ** 2) + 22 ((tr[1] - br[1]) ** 2) 23 ) 24 25 height_b = np.sqrt( 26 ((tl[0] - bl[0]) ** 2) + 27 ((tl[1] - bl[1]) ** 2) 28 ) 29 30 max_height = max( 31 int(height_a), 32 int(height_b) 33 ) 34 35 dst = np.array([ 36 [0, 0], 37 [max_width - 1, 0], 38 [max_width - 1, max_height - 1], 39 [0, max_height - 1] 40 ], dtype="float32") 41 42 matrix = cv2.getPerspectiveTransform( 43 rect, 44 dst 45 ) 46 47 return cv2.warpPerspective( 48 image, 49 matrix, 50 (max_width, max_height) 51 )
调用:
1warped = four_point_transform( 2 orig, 3 screen_cnt.reshape(4, 2) * ratio 4) 5 6cv2.imwrite( 7 "invoice_new.jpg", 8 warped 9)
这里乘以 ratio 是因为轮廓是在缩小后的图片中找到的,而变换使用的是原图。
九、透视变换后的图像处理
透视矫正后,可以继续进行灰度化:
1warped_gray = cv2.cvtColor( 2 warped, 3 cv2.COLOR_BGR2GRAY 4)
再进行二值化:
1ref = cv2.threshold( 2 warped_gray, 3 0, 4 255, 5 cv2.THRESH_BINARY | cv2.THRESH_OTSU 6)[1]
为了让文字和图形更加连续,可以使用闭运算:
1kernel = np.ones( 2 (2, 2), 3 np.uint8 4) 5 6ref_new = cv2.morphologyEx( 7 ref, 8 cv2.MORPH_CLOSE, 9 kernel 10)
闭运算的过程是:
先膨胀,再腐蚀
主要作用包括:
1,填补小空洞;
2,连接断开的文字;
3,修复细小缺口;
4,使区域更加完整。
之后可以缩放并旋转图像:
1ref_new = resize( 2 ref_new, 3 width=500 4) 5 6rotated = cv2.rotate( 7 ref_new, 8 cv2.ROTATE_90_COUNTERCLOCKWISE 9)
十、模板匹配的基本思想
模板匹配是在一张大图中寻找与模板相似的区域。
1,身份证号码识别的思路是:准备0到9的数字模板
2,提取身份证图片中的数字区域
3,将每个数字缩放为统一大小
4,分别与0到9进行模板匹配
5,选择得分最高的数字
模板匹配的核心函数:
1result = cv2.matchTemplate( 2 roi, 3 digit_roi, 4 cv2.TM_CCOEFF 5)
roi 是待识别的数字,digit_roi 是某一个数字模板。
获取匹配分数:
1_, score, _, _ = cv2.minMaxLoc( 2 result 3)
将所有得分保存起来:
1scores = [] 2 3for digit, digit_roi in digits.items(): 4 result = cv2.matchTemplate( 5 roi, 6 digit_roi, 7 cv2.TM_CCOEFF 8 ) 9 10 _, score, _, _ = cv2.minMaxLoc( 11 result 12 ) 13 14 scores.append(score)
选择最高分:
1number = str( 2 np.argmax(scores) 3)
十一、提取数字模板
模板图中包含 0 到 9 的数字。首先读取并二值化:
1template_image = cv2.imread( 2 "TP.png" 3) 4 5gray = cv2.imread( 6 "TP.png", 7 0 8) 9 10binary = cv2.threshold( 11 gray, 12 150, 13 255, 14 cv2.THRESH_BINARY_INV 15)[1]
查找外部轮廓:
1contours = cv2.findContours( 2 binary, 3 cv2.RETR_EXTERNAL, 4 cv2.CHAIN_APPROX_SIMPLE 5)[-2]
将轮廓从左到右排序,确保顺序与数字一致。
提取每个数字区域:
1digits = {} 2 3for i, contour in enumerate(contours): 4 x, y, w, h = cv2.boundingRect(contour) 5 6 roi = binary[ 7 y - 2:y + h + 2, 8 x - 2:x + w + 2 9 ] 10 11 roi = cv2.resize( 12 roi, 13 (57, 88) 14 ) 15 16 roi = cv2.bitwise_not(roi) 17 18 digits[i] = roi
所有数字模板都被缩放为 57×88,这样才能进行统一比较。
十二、定位身份证号码
读取身份证图片:
1image = cv2.imread( 2 "card_id.jpg" 3) 4 5gray = cv2.imread( 6 "card_id.jpg", 7 0 8)
二值化:
1binary = cv2.threshold( 2 gray, 3 120, 4 255, 5 cv2.THRESH_BINARY_INV 6)[1]
查找数字轮廓:
1contours = cv2.findContours( 2 binary.copy(), 3 cv2.RETR_EXTERNAL, 4 cv2.CHAIN_APPROX_SIMPLE 5)[-2]
遍历轮廓并通过坐标筛选号码:
1locations = [] 2 3for contour in contours: 4 x, y, w, h = cv2.boundingRect(contour) 5 6 if 330 < y < 360 and x > 220: 7 locations.append( 8 (x, y, w, h) 9 )
这里的筛选条件是根据当前图片中身份证号码的位置确定的。对于同一张或相似图片,这种方法比较简单有效。
然后按照横坐标排序:
1locations = sorted( 2 locations, 3 key=lambda item: item[0] 4)
这样可以保证数字按照从左到右的顺序识别。
十三、识别每个数字
1output = [] 2 3for x, y, w, h in locations: 4 group = gray[ 5 y - 2:y + h + 2, 6 x - 2:x + w + 2 7 ] 8 9 group = cv2.threshold( 10 group, 11 0, 12 255, 13 cv2.THRESH_BINARY | cv2.THRESH_OTSU 14 )[1] 15 16 roi = cv2.resize( 17 group, 18 (57, 88) 19 ) 20 21 scores = [] 22 23 for digit, digit_roi in digits.items(): 24 result = cv2.matchTemplate( 25 roi, 26 digit_roi, 27 cv2.TM_CCOEFF 28 ) 29 30 _, score, _, _ = cv2.minMaxLoc( 31 result 32 ) 33 34 scores.append(score) 35 36 result_digit = str( 37 np.argmax(scores) 38 ) 39 40 output.append(result_digit)
将结果拼接起来:
1card_number = "".join(output) 2 3print( 4 "Card ID #:", 5 card_number 6)
同时可以在图片上绘制识别结果:
1cv2.rectangle( 2 image, 3 (x - 5, y - 5), 4 (x + w + 5, y + h + 5), 5 (0, 0, 255), 6 1 7) 8 9cv2.putText( 10 image, 11 result_digit, 12 (x, y - 15), 13 cv2.FONT_HERSHEY_SIMPLEX, 14 0.65, 15 (0, 0, 255), 16 2 17)
十四、模板匹配的不足
模板匹配方法比较容易实现,但它对图片质量要求较高。
如果出现以下情况,识别效果可能下降:
身份证图片发生旋转;
数字大小发生变化;
数字字体不一致;
光照太暗或太亮;
数字区域被遮挡;
图片存在严重噪声;
号码位置发生改变。
此外,代码使用固定坐标筛选数字区域:
if 330 < y < 360 and x > 220:
这说明程序只适合当前尺寸和当前布局的图片。如果换一张身份证,号码位置可能不同,程序就无法识别。
更通用的做法是:
- 先进行透视变换;
- 统一图片大小;
- 根据相对比例定位号码区域;
- 使用 OCR 或深度学习模型识别。
总结
本篇主要学习了透视变换和模板匹配的实际应用。
透视变换可以将倾斜的发票、身份证和纸张矫正成正面图像。它的关键是找到目标区域的四个角点,并按照左上、右上、右下、左下的顺序排列,然后使用 getPerspectiveTransform() 和 warpPerspective() 完成变换。
模板匹配可以通过比较图像相似度,在图片中找到目标区域。课程中使用数字模板识别身份证号码,主要步骤是:
1,准备数字模板
2,模板二值化
3,查找每个数字轮廓
4,定位身份证号码
5,统一数字大小
6,逐个匹配0到9
7,得到识别结果
通过本次练习可以发现,计算机视觉任务往往需要多个步骤配合完成。单纯使用模板匹配可能不够稳定,但如果结合透视矫正、灰度化、二值化和形态学处理,就可以完成一些简单的证件和票据识别任务。
《OpenCV实战——透视变换与身份证号码模板识别》 是转载文章,点击查看原文。

