Python实现增值税发票OCR(带源码)

发票识别日常生活中经常能用到,之前浏览博客发现类似的文章, 但源码只给了一小部分,所以决定自己来实现。

1.原始图片视角变换及裁剪:

现实中,我们拍照不可能像扫描那样端正,图片中的票据总会有这样那样的视角上的歪斜,使用这样的图片直接来识别极易出错,为了提高识别的准确性,我们需要先将图片预处理为扫描样式的正视图,并将不属于票据的图片部分剪切掉。

针对这一过程,参考相关资料本项目采用”整体图片边缘检测”(采用cv2.Canny函数)、”票据轮廓检测”(采用cv2.findContours函数),得到所需处理票据的轮廓,之后确定票据的四个顶点,最后使用cv2.getPerspectiveTransform和cv2.warpPerspective对于原始图片进行透视变换得到相应的票据的正视图。

2.根据已知位置识别相应文字:

作为第一个版本,简单起见,这里采用了提前找到感兴趣的如发票代码、销售方名称、备注等条目的位置,直接在上述正视图图片中截取对应的区域的方式找到相应信息的图片,通过cnocr库对于得到的区域图片进行文字识别。

3.将识别到的信息保存到相应excel文件中:

import cv2import numpy as npfrom cnocr import CnOcrimport pandas as pdfrom pandas import DataFrameimport os#后续生成票据图像时的大小,按照标准增值税发票版式240mmX140mm来设定height_resize = 1400width_resize = 2400# 调整原始图片尺寸def resizeImg(image, height=height_resize):h, w = image.shape[:2]pro = height / hsize = (int(w * pro), int(height))img = cv2.resize(image, size)return img# 边缘检测def getCanny(image):# 高斯模糊binary = cv2.GaussianBlur(image, (3, 3), 2, 2)# 边缘检测binary = cv2.Canny(binary, 60, 240, apertureSize=3)# 膨胀操作,尽量使边缘闭合kernel = np.ones((3, 3), np.uint8)binary = cv2.dilate(binary, kernel, iterations=1)return binary# 求出面积最大的轮廓def findMaxContour(image):# 寻找边缘contours, _ = cv2.findContours(image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)# 计算面积max_area = 0.0max_contour = []for contour in contours:currentArea = cv2.contourArea(contour)if currentArea > max_area:max_area = currentAreamax_contour = contourreturn max_contour, max_area# 多边形拟合凸包的四个顶点def getBoxPoint(contour):# 多边形拟合凸包hull = cv2.convexHull(contour)epsilon = 0.02 * cv2.arcLength(contour, True)approx = cv2.approxPolyDP(hull, epsilon, True)approx = approx.reshape((len(approx), 2))return approx# 适配原四边形点集def adapPoint(box, pro):box_pro = boxif pro != 1.0:box_pro = box/probox_pro = np.trunc(box_pro)return box_pro# 四边形顶点排序,[top-left, top-right, bottom-right, bottom-left]def orderPoints(pts):rect = np.zeros((4, 2), dtype=”float32″)s = pts.sum(axis=1)rect[0] = pts[np.argmin(s)]rect[2] = pts[np.argmax(s)]diff = np.diff(pts, axis=1)rect[1] = pts[np.argmin(diff)]rect[3] = pts[np.argmax(diff)]return rect# 计算长宽def pointDistance(a, b):return int(np.sqrt(np.sum(np.square(a – b))))# 透视变换def warpImage(image, box):w, h = pointDistance(box[0], box[1]), \pointDistance(box[1], box[2])dst_rect = np.array([[0, 0],[w – 1, 0],[w – 1, h – 1],[0, h – 1]], dtype=’float32′)M = cv2.getPerspectiveTransform(box, dst_rect)warped = cv2.warpPerspective(image, M, (w, h))return warped# 统合图片预处理def imagePreProcessing(path):image = cv2.imread(path)# 转灰度、降噪#image = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)#image = cv2.GaussianBlur(image, (3,3), 0)# 边缘检测、寻找轮廓、确定顶点ratio = height_resize / image.shape[0]img = resizeImg(image)binary_img = getCanny(img)max_contour, max_area = findMaxContour(binary_img)boxes = getBoxPoint(max_contour)boxes = adapPoint(boxes, ratio)boxes = orderPoints(boxes)# 透视变化warped = warpImage(image, boxes)# 调整最终图片大小height, width = warped.shape[:2]#size = (int(width*height_resize/height), height_resize)size = (width_resize, height_resize)warped = cv2.resize(warped, size, interpolation=cv2.INTER_CUBIC)return warped# 截取图片中部分区域图像,测试阶段使用,包括显示与保存图片,实际使用时不使用这个函数,使用下面的正式版函数def cropImage_test(img, crop_range, filename=’Undefined’):xpos, ypos, width, height = crop_rangecrop = img[ypos:ypos+height, xpos:xpos+width]if filename==’Undefined’: #如果未指定文件名,采用坐标来指定文件名filename = ‘crop-‘+str(xpos)+’-‘+str(ypos)+’-‘+str(width)+’-‘+str(height)+’.jpg’cv2.imshow(filename, crop) #展示截取区域图片—测试用#cv2.imwrite(filename, crop) #imwrite在文件名含有中文时会有乱码,应该采用下方imencode—测试用# 保存截取区域图片—测试用cv2.imencode(‘.jpg’, crop)[1].tofile(filename)return crop# 截取图片中部分区域图像def cropImage(img, crop_range):xpos, ypos, width, height = crop_rangecrop = img[ypos:ypos+height, xpos:xpos+width]return crop# 从截取图片中识别文字def cropOCR(crop, ocrType):if ocrType==0:text_crop_list = ocr.ocr_for_single_line(crop)elif ocrType==1:text_crop_list = ocr_numbers.ocr_for_single_line(crop)elif ocrType==2:text_crop_list = ocr_UpperSerial.ocr_for_single_line(crop)text_crop = ”.join(text_crop_list)return text_cropif name == ‘main‘:# 实例化不同用途CnOcr对象ocr = CnOcr(name=”) #混合字符ocr_numbers = CnOcr(name=’numbers’, cand_alphabet=’0123456789′) #纯数字ocr_UpperSerial = CnOcr(name=’UpperSerial’, cand_alphabet=’0123456789ABCDEFGHIJKLMNPQRSTUVWXYZ’) #编号,只包括大写字母(没有O)与数字# 截取图片中部分区域图像-名称crop_range_list_name = [‘发票代码’, ‘发票号码’, ‘开票日期’,’校验码’, ‘销售方名称’, ‘销售方纳税人识别号’,’销售方地址电话’, ‘销售方开户行及账号’, ‘价税合计’,’备注’]# 截取图片中部分区域图像-坐标crop_range_list_data = [[1870, 40, 380, 38], [1867, 104, 380,38], [1866, 166, 380, 50],[1867, 230, 450, 50], [421, 1046, 933, 46], [419, 1091, 933, 48],[420, 1145, 933, 47], [421, 1193, 933, 40], [1892, 976, 414, 48],[1455, 1045, 325, 38]]# 截取图片中部分区域图像-使用ocr的类型,0:混合字符,1:纯数字,2:编号crop_range_list_type = [1, 1, 0,1, 0, 2,0, 0, 0,0]# 预处理图像path = ‘test.jpg’warped = imagePreProcessing(path)# 展示与保存预处理的图片—测试用#cv2.imshow(‘warpImage’, warped)cv2.imwrite(‘result.jpg’,warped)# 处理预处理图像并将结果保存到text_ocr列表中text_ocr = []for i in range(len(crop_range_list_data)):#filename = crop_range_list_name[i]+’.jpg’ #测试阶段保存截取图片时使用的文件名,实际使用时不需要crop = cropImage(warped, crop_range_list_data[i])crop_text = cropOCR(crop, crop_range_list_type[i])crop_text = crop_text.replace(‘o’,’0′) #发票中不会有小写字母o,凡是出现o的都使用0替代print(crop_range_list_name[i],’:’,crop_text)text_ocr.append(crop_text)# 按年月来保存结果到xlsx文件中,计算文件名date_temp = text_ocr[2].split(‘年’)year_num = date_temp[0]month_num = date_temp[1].split(‘月’)[0]filename = year_num+’-‘+month_num+’.xlsx’# 如果文件还没建立,新建文件if not os.path.exists(filename):dic = {}for i in range(len(crop_range_list_name)):dic[crop_range_list_name[i]] = []df = pd.DataFrame(dic)df.to_excel(filename, index=False)data = pd.read_excel(filename)if not int(text_ocr[1]) in data[‘发票号码’].values.tolist():new_line_num = data.shape[0]data.loc[new_line_num] = text_ocrDataFrame(data).to_excel(filename, index=False, header=True)else:print(path,’is already in’,filename,’!’)cv2.waitKey(0)

Original: https://blog.csdn.net/egowell/article/details/126626760
Author: egowell
Title: Python实现增值税发票OCR(带源码)

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/628149/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球