欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

基于KNN的发票识别

程序员文章站 2022-07-09 19:22:57
项目概况: 有一个PDF文件,里面的每页都是一张发票,把每页的发票单独存为一个PDF并用该发票的的发票号码进行文件的命名,发票号码需要OCR识别,即识别下图中红色方块的内容。 一:拆分PDF 现有一个PDF文件,里面有很多张发票图片,每张发票占一页 我们先把这整个PDF拆分为单独的PDF 使用PyP ......

项目概况:

有一个pdf文件,里面的每页都是一张发票,把每页的发票单独存为一个pdf并用该发票的的发票号码进行文件的命名,发票号码需要ocr识别,即识别下图中红色方块的内容。

 

基于KNN的发票识别

 一:拆分pdf

现有一个pdf文件,里面有很多张发票图片,每张发票占一页

基于KNN的发票识别

 

我们先把这整个pdf拆分为单独的pdf

使用pypdf2这个包

代码如下,基本上每句都写了注释

from pypdf2 import pdffilewriter,pdffilereader

def test1(file_path,folder_path,num,end_page,start_page=0):
    """
    :param file_path: pdf文件路径
    :param folder_path: 存放路径
    :param num: 拆分后的pdf存在几个原pdf页数
    :param end_page: 拆分到的最后一页
    :param start_page: 起始的页数,默认为0
    :return:
    """
    # 打开pdf文件
    pdf_file = pdffilereader(open(file_path, 'rb'))
    # 获取pdf的页数
    pdf_file_num = pdf_file.getnumpages()
    # 如果输入的end_page页数比pdf文件的页数大或者小于等于0,让停止的页数为pdf最大的页数
    if end_page>pdf_file_num or end_page<=0:
        end_page=pdf_file_num
    # 从起始页到最后一页进行遍历
    for i in range(start_page,end_page,num):
        #创建一个pdffilewriter的对象
        out_put = pdffilewriter()
        # 给out_put这个对象传num数的页,项目中每个发票都只占了1页,所以num为1,如果发票占据2页,那么num为2
        for k in range(num):
            out_put.addpage(pdf_file.getpage(i))
        # 设置保存的路径
        out_file = folder_path + "\\" + f"{i}.pdf"
        # 把out_put里面的数据写入到文件中
        out_put.write(open(out_file, 'wb'))

运行结果如下:

基于KNN的发票识别

 

 

 二:把pdf变成图片,并进行切分

现在发票是pdf格式,我们需要转为图片格式,而且我需要的发票号码在发票的右上角,所以对图片进行大致的切分有助于提高后面的识别速率。

这里解释一下rect = page.rect,rect可以获取页面的大小,rect.tl,tl为topleft的缩写,也就是左上角的意思,所以有tl(左上),tf(右上),bl(左下),bf(右下)等坐标

import fitz

def my_fitz(pdfpath, imagepath):
    """
    :param pdfpath: pdf的路径
    :param imagepath: 图片文件夹的路径,不是图片路径
    :return:
    """
    # 打开pdf文件
    pdfdoc = fitz.open(pdfpath)
    for pg in range(pdfdoc.pagecount):
        page = pdfdoc[pg]
        rotate = int(0)
        # 每个尺寸的缩放系数为2,生成的图像的分辨率会提高,参数也可以*设置,没有硬性要求
        zoom_x = 2
        zoom_y = 2
        # 这个函数可以理解为,把zoom_x,zoom_y这两个参数保存起来
        mat = fitz.matrix(zoom_x, zoom_y).prerotate(rotate)
        rect = page.rect  # 页面大小
        # mp为截取矩形的左上角坐标
        mp=rect.tr-(500/zoom_x,0)
        # tem为截取矩形的右下角坐标
        tem=rect.tr+(0,200/zoom_y)
        # clip为截取的矩形
        clip = fitz.rect(mp, tem)
        # 进行图片的截取
        pix = page.getpixmap(matrix=mat, alpha=false,clip=clip)
        if not os.path.exists(imagepath):  # 判断存放图片的文件夹是否存在
            os.makedirs(imagepath)  # 若图片文件夹不存在就创建
        new_img_path = imagepath + '/' + '0.png'
        pix.writepng(new_img_path)  # 将图片写入指定的文件夹内

        return new_img_path

运行结果如图所示:

基于KNN的发票识别

 

 

 

 基于KNN的发票识别

 

三:检测边缘,把中间的数字截取出来

边缘检测我使用的cv2模块,注意使用cv2.threshold函数时,里面的图片必须为灰度图,不然会报错

import cv2

def my_croping(imgpath):
    # 读取图片的路径
    img = cv2.imread(imgpath)
    # 把该图片转换为灰度图
    gray = cv2.cvtcolor(img, cv2.color_bgr2gray)
    #设置固定级别的阈值应用于矩阵
    ret, binary = cv2.threshold(gray, 127, 255, cv2.thresh_binary)
    # 寻找边缘,返回的contours为边缘数据的集合
    _, contours, hierarchy = cv2.findcontours(binary, cv2.retr_tree, cv2.chain_approx_tc89_l1)
    # 画出边缘,-1为画出所有的边缘,如果为任意自然数那么为contours的索引,(0,0,255)为颜色,最后的2是线条的粗细,数值越大,线条越粗
    cv2.drawcontours(img, contours, -1, (0, 0, 255), 2)
    # 展示图片
    cv2.imshow("pic", img)
    # 等待,当参数为0时,为无限等待,直到有键盘指令
    cv2.waitkey(0)

运行结果:

基于KNN的发票识别

 

可见上一步骤的图片中的发票号码已经被圈起来了,但是有很多不必要的东西也被圈进来了,所以我们需要对初始的的contours进行筛选。

contours是一个包含多个列表的列表,我们需要的中间的数字,观察可知,中间数字的边缘比较大,所以我们只需要通过len()方法就可以进行初步的过滤

contours.sort(key=lambda x: len(x), reverse=true)
for i in range(len(contours)):
        if len(contours[i]) > 10:
            continue
        else:
            contours = contours[:i]
            break

加入过滤后运行结果:

基于KNN的发票识别

 

 我们初步的缩小了范围,下面需要制定具体的规则来确定想要获得的对象

首先,我们先获取各个边缘所组成的矩形的坐标

rect_list=[]
for i in range(len(contours)):
        cont_ = contours[i]
        # 找到boundingrect
        rect = cv2.boundingrect(cont_)
        print(rect)
        rect_list.append(rect)
        

运行结果如下:

基于KNN的发票识别

从左到右分别是x,y,宽度,高度

很明显,我们要找的坐标是8个,宽度,高度差不多的坐标,n为阈值,初始为10,当两个矩阵的宽和高直接的差的绝对值在阈值范围内,填入集合,如果这样的元素超过8个,那么则找到号码对应的矩阵,在传入之前,用x坐标的大小进行排序,能减少很多时间

def xyhw(li):
    n=10
    while n<30:
        for i in range(len(li)):
            tem_li=[li[i]]
            for k in range(i+1,len(li)):
                if abs(li[i][1]-li[k][1])+abs(li[i][2]-li[k][2])+abs(li[i][3]-li[k][3])<n:
                    tem_li.append(li[k])
            if len(tem_li)>=8:
                return tem_li
        n+=1

但是这个筛选完,还有一个问题,有时候会出现分割后no没有分割掉的情况,所以需要过滤掉no

 

def filter_li(li):
    if len(li)>8:
        li = li[:9]
    interval=li[0][0]-li[1][0]
    test_interval=li[-2][0]-li[-1][0]
    if test_interval/interval>1.5:
        li=li[:-1]
    return li

这样我们就可以获得号码的八个矩阵坐标,我们只需要把这八个矩阵融合即可

#进行排序
rect_list.sort(key= lambda x:x[0],reverse=true)
#进行筛选
rect_list=filter_li(rect_list)
#x0,y0为矩阵的左上角,x1,y1为矩阵的右下角
y0=rect_list[0][1]
y1=rect_list[0][1]+rect_list[0][3]
x0=rect_list[-1][0]
x1=rect_list[0][0]+rect_list[0][2]
print(y0,y1,x0,x1)
#进行图片切割
cropimg = img2[y0:y1,x0:x1]
#写入图片
cv2.imwrite(img_path,cropimg)

可以获得这样的图片:

基于KNN的发票识别

 

 

四:把图片中的数字分别截取出来

第四步和第三步的原理一样,先边缘检测,然后获取矩形坐标后进行截图,比第三步简单不少,这里就不多赘述了

 

import cv2
import numpy as np


def xyhw(li):
    n=10
    tem_li=[]
    while n<30:
        for i in range(len(li)):
            tem_li=[li[i]]
            for k in range(i+1,len(li)):
                if abs(li[i][1]-li[k][1])+abs(li[i][2]-li[k][2])+abs(li[i][3]-li[k][3])<n:
                    tem_li.append(li[k])
            if len(tem_li)>=8:
                return tem_li
        n+=1
    else:
        return tem_li


# 将img的高度调整为28,先后对图像进行如下操作:直方图均衡化,形态学,阈值分割
def pre_treat(img):
    height_ = 28
    ratio_ = float(img.shape[1]) / float(img.shape[0])
    gray = cv2.cvtcolor(img, cv2.color_bgr2gray)
    gray = cv2.resize(gray, (int(ratio_ * height_), height_))
    gray = cv2.equalizehist(gray)
    _, binary = cv2.threshold(gray, 190, 255, cv2.thresh_binary)
    img_ = 255 - binary  # 反转:文字置为白色,背景置为黑色
    return img_


def get_roi(contours):
    rect_list = []
    for i in range(len(contours)):
        rect = cv2.boundingrect(contours[i])
        if rect[3] > 10:
            rect_list.append(rect)
    return rect_list


def get_rect(img):
    _, contours, hierarchy = cv2.findcontours(img,cv2.retr_tree, cv2.chain_approx_tc89_l1)
    rect_list = get_roi(contours)
    rect_list.sort(key= lambda x:x[0],reverse=true)
    rect_list=xyhw(rect_list)
    
    return rect_list

def change_(img):
    length = 28
    h,w = img.shape
    h = np.float32([[1,0,(length-w)/2],[0,1,(length-h)/2]])
    img = cv2.warpaffine(img,h,(length,length))
    m = cv2.getrotationmatrix2d((length/2,length/2),0,26/float(img.shape[0]))
    return cv2.warpaffine(img,m,(length,length))

def fenge(img_path):
    cont = 0
    img = cv2.imread(img_path)
    img = pre_treat(img)
    contours = get_rect(img)
    folder_path=r"c:\users\86173\desktop\jetbrains2019.2\new\tem"
    file_list=[]
    # img=cv2.drawcontours(img,contours,2,(0, 0, 255),3)
    print("*********************%s*************" %contours)
    for i in range(len(contours)):
        y0 = contours[i][1]
        y1 = contours[i][1] + contours[i][3]
        x0 = contours[i][0]
        x1 = contours[i][0] + contours[i][2]
        print(y0, y1, x0, x1)
        cropimg = img[y0:y1, x0:x1]
        cropimg = change_(cropimg)
        fenge_img=rf"{folder_path}\{cont}.png"
        cv2.imwrite(fenge_img, cropimg)
        cont += 1
        file_list.append(fenge_img)
    return file_list

五:苦力活

通过第四步的分割,我们可以得到分割后的数字,那么第一步就是给这些分割后的数字命名,类似这样:

基于KNN的发票识别

 

建议在分割的时候,用input输入来命名嗷

 第二步就是把这些图片转为矩阵存入txt中:

from pil import image
import numpy

def noise_remove_pil(image_name, k):
    """
    8邻域降噪
    args:
        image_name: 图片文件命名
        k: 判断阈值

    returns:

    """

    def calculate_noise_count(img_obj, w, h):
        """
        计算邻域非白色的个数
        args:
            img_obj: img obj
            w: width
            h: height
        returns:
            count (int)
        """
        count = 0
        width, height = img_obj.size
        for _w_ in [w - 1, w, w + 1]:
            for _h_ in [h - 1, h, h + 1]:
                if _w_ > width - 1:
                    continue
                if _h_ > height - 1:
                    continue
                if _w_ == w and _h_ == h:
                    continue
                if img_obj.getpixel((_w_, _h_)) < 190:  # 这里因为是灰度图像,设置小于230为非白色
                    count += 1
        return count

    img = image.open(image_name)
    # 灰度
    gray_img = img.convert('l')

    w, h = gray_img.size
    for _w in range(w):
        for _h in range(h):
            if _w == 0 or _h == 0:
                gray_img.putpixel((_w, _h), 255)
                continue
            # 计算邻域非白色的个数
            pixel = gray_img.getpixel((_w, _h))
            if pixel == 255:
                continue

            if calculate_noise_count(gray_img, _w, _h) < k:
                gray_img.putpixel((_w, _h), 255)
    # gray_img = gray_img.resize((32, 32), image.lanczos)
    gray_img.save(image_name)
    # gray_img.show()
    im = numpy.array(gray_img)
    for i in range(im.shape[0]):  # 转化为二值矩阵
        for j in range(im.shape[1]):
            if im[i, j] <190:
                im[i, j] = 1
            else:
                im[i, j] = 0
    return im




if __name__ == '__main__':
    for i in range(0,10):
        for k in range(0,100):
            png_file_path=rf"c:\users\86173\desktop\jetbrains2019.2\model_test\{i}_{k}.png"
            txt_file_path=rf"c:\users\86173\desktop\jetbrains2019.2\model_test\txt_folder\{i}_{k}.txt"
            try:
                im = noise_remove_pil(png_file_path, 4)
                with open(txt_file_path,'at',encoding='utf-8')as f:
                    for n in im:
                        f.writelines(str(n).replace("[","").replace("]","").replace(" ","")+"\n")
            except exception as e:
                continue

运行结果:

基于KNN的发票识别

 

 基于KNN的发票识别

 

获得这样的文件,那么准备工作就结束了

六:knn模型的使用

 导入sklearn使用knn模型非常简单,代码量很少

import numpy as np
from os import listdir
from sklearn.neighbors import kneighborsclassifier as knn

def np2vector(im):
    returnvect = np.zeros((1, 784))
    for i in range(28):
        # 读一行数据
        linestr = im[i]
        # 每一行的前28个元素依次添加到returnvect中
        for j in range(28):
            returnvect[0, 28 * i + j] = int(linestr[j])
    # 返回转换后的1x784向量
    return returnvect
def img2vector(filename):
    #创建1x784零向量
    returnvect = np.zeros((1, 784))
    #打开文件
    fr = open(filename)
    #按行读取
    for i in range(28):
        #读一行数据
        linestr = fr.readline()
        #每一行的前28个元素依次添加到returnvect中
        for j in range(28):

            returnvect[0,28*i+j] = int(linestr[j])
    #返回转换后的1x784向量
    return returnvect

def handwritingclasstest(im):
    #测试集的labels
    hwlabels = []
    #返回trainingdigits目录下的文件名
    trainingfilelist = listdir(r"c:\users\86173\desktop\jetbrains2019.2\model_test\txt_folder")
    #返回文件夹下文件的个数
    m = len(trainingfilelist)
    #初始化训练的mat矩阵,测试集
    trainingmat = np.zeros((m, 784))
    #从文件名中解析出训练集的类别
    for i in range(m):
        #获得文件的名字
        filenamestr = trainingfilelist[i]
        #获得分类的数字
        classnumber = int(filenamestr.split('_')[0])
        #将获得的类别添加到hwlabels中
        hwlabels.append(classnumber)
        trainingmat[i,:] = img2vector(r'c:\users\86173\desktop\jetbrains2019.2\model_test\txt_folder\%s' % (filenamestr))
    #构建knn分类器
    neigh = knn(n_neighbors = 4, algorithm = 'auto')
    #拟合模型, trainingmat为测试矩阵,hwlabels为对应的标签
    neigh.fit(trainingmat, hwlabels)
    
    vectorundertest = np2vector(im)

    classifierresult = neigh.predict(vectorundertest)
    return classifierresult

有这个模型,我们调用一下,就可以获取到对应的发票号码了

最终运行结果:

基于KNN的发票识别

 

 

 

 

 最后:

knn的原理比较简单,但是因为是在工作之余写的,写的比较匆忙,有些步骤说的不够详细,如果有什么问题欢迎在评论区留言,如果有改进方案那就更好了,博主只是一个初入机器学习的小学生,欢迎各位大佬的指点,谢谢