python 图片中的表格识别

接到个任务需要将几万张带表格的图片转换成结构化数据。

1. 大步骤

最终算是完成任务,但是识别率上还有一点问题,人工再过一下,还是蛮快的。先说一下大的步骤:

  1. 分割单元格。将图片中的表格全部定位出来,然后按单元格裁剪成一个个小图片,以便后续分析及操作;

  2. 聚焦。其实就是将单元格中的文本区域裁剪出来,将多余的空白去掉;

  3. 大图片的识别。对于大图片用图像相似性的算法(phash+汉明距离)做识别;

  4. 小图片的识别。对于小图片,做字符分割,然后用NN做分类识别;

  5. 识别结果输出到txt;

  6. txt输出到excel。将全部txt按照目标表格的格式,解析输出到excel。

1.1 分割单元格

既然只关心表格区域,所以第一步先将各个单元格拆分出来,截取成一个个小图片。尝试用图像的膨胀、腐蚀来定位表格区域,图像处理包skimage,最后算是定位出了表格区域,也分割出了各个单元格图片,其中部分中间过程的图片如下:

分割单元格这一块的基本流程是:

  1. 读取图像;

  2. 二值化处理;

  3. 横向、纵向的膨胀、腐蚀操作,得到横线图img_row和竖线图img_col;

  4. 得到点图,img_row + img_col=img_dot;

  5. 得到线图,img_row × img_col=img_line(线图只是拿来看看的,后续没有用到);

  6. 浓缩点团到单个像素;

  7. 开始遍历各行的点,将各个单元格从二值图像上裁剪出来,保存到temp文件夹。

参考资料:

  1. OpenCV-检测并提取表格 - CSDN博客

  2. python数字图像处理(1):环境安装与配置 - denny402 - 博客园

1.1.1 读取图像、二值化

import skimage# 读取图片,并转灰度图img=io.imread(imgFilePath,True)#二值化bi_th=0.81img[img<=bi_th]=0img[img>bi_th]=1

1.1.2 膨胀、腐蚀操作

# 膨胀腐蚀操作def dil2ero(img,selem):    img=morphology.dilation(img,selem)    imgres=morphology.erosion(img,selem)    return imgres# 求图像中的横线和竖线rows,cols=img.shapescale=80col_selem=morphology.rectangle(cols//scale,1)img_cols=dil2ero(img,col_selem)row_selem=morphology.rectangle(1,rows//scale)img_rows=dil2ero(img,row_selem)

1.1.3 得到点图、线图

# 线图img_line=img_cols*img_rows# 点图img_dot=img_cols+img_rows_tempimg_dot[img_dot>0]=1img_dot=clearEdge(img_dot,3)

1.1.4 浓缩点为单个像素

# 收缩点团为单像素点(3×3)def isolate(img):    idx=np.argwhere(img<1)    rows,cols=img.shape        for i in range(idx.shape[0]):        c_row=idx[i,0]        c_col=idx[i,1]        if c_col+1<cols and c_row+1<rows:            img[c_row,c_col+1]=1            img[c_row+1,c_col]=1            img[c_row+1,c_col+1]=1        if c_col+2<cols and c_row+2<rows:            img[c_row+1,c_col+2]=1            img[c_row+2,c_col]=1            img[c_row,c_col+2]=1            img[c_row+2,c_col+1]=1            img[c_row+2,c_col+2]=1    return imgimg_dot=isolate(img_dot)

1.1.5 遍历各dot,裁剪图片

按行遍历各个顶点,判断这些顶点是否是目标单元格的顶点,即可将图片裁出。

1.2 聚焦

图片中的表格的各个单元格都已经截取出来,但是单元格有高有矮,不利于后续分析,所以想到只要聚焦文字区域就行,即单元格里的文字区域截取出,其余空白就不要了。聚焦方法就是按行、列分别求和,因为图片白色区域值为1,黑色区域值为0,所以按行求和后,全白部分的sum=width,文字区域及sum<width的部分。比如下边这张图,通过行列求和,即可定位出文字区域,截图即可。

代码如下:

def focusImg(imgPath):    img=io.imread(imgPath)    img=color.rgb2gray(img)    img=img_as_float(img)    img=clearEdge(img,3)    # 求各列的和    col_sum=img.sum(axis=0)    # 求各行的和    row_sum=img.sum(axis=1)    idx_col_sum=np.argwhere(col_sum<col_sum.max())    if len(idx_col_sum)==0:        os.remove(imgPath)        return    col_start,col_end=idx_col_sum[0,0]-1,idx_col_sum[-1,0]+2        idx_row_sum=np.argwhere(row_sum<row_sum.max())    if len(idx_row_sum)==0:        os.remove(imgPath)        return    row_start,row_end=idx_row_sum[0,0]-1,idx_row_sum[-1,0]+2

经过聚焦操作,图片中的各个元素大可分成以下这些类别:

大概分析了一下图片,碰巧发现,焦后的标题类图片高度都大于13(大图片),而数值类图片高度都小于等于13(小图片)。大图片的种类数量不算多,大概一百多种。小图片基本上是金融、日期,还有一些特定字符比如 “/”、“N”、"*"什么的,所以对小图再做字符分割,也就十多种。

这时关于单元格里内容的识别,就有了以下操作:

  1. 对于大图片,将一百多张图片作为模板,将输入的图片逾模板里的图片做相似性对比(phash+汉明距离);

  2. 对于小图片,做字符分割,使用pytorch+CNN做识别;

1.3 大图片识别(标题部分)

大图片内容的识别,基本上就是靠的模板匹配,具体算法原理看参考资料:

  1. 在Python中用小波分析图像的哈希值 – 麦穗技术

  2. 相似性图片搜索的原理——阮一峰的网络日志

耗费了些时间整理好了模板文件夹,将图片里的文字作为文件名,在做模板初始化的时候就很方便的生成模板phash字典了:dic{key,value} => dic{fname,phash}

代码如下:

hash_size=20phashHamming_th=100def hamming(h1, h2):    '''计算两图的汉明距离'''    return sum(sum(h1.hash^h2.hash))def getText(img,bigPicTempleDic):    hashHere=imagehash.phash(img,hash_size=hash_size)    seq=[]    for key in bigPicTempleDic.keys():        seq.append((hamming(hashHere,bigPicTempleDic[key]),key))            res_minDis,res_key=sorted(seq,key=lambda x:x[0])[0]    #print('HMdis:{0}'.format(res_minDis))    if res_minDis<phashHamming_th:        return res_key    else:        return '未识别出来'        #模板文件夹的路径templePath=r'...\bigPicTemple'bigPicTempleDic={'keyname':'phash'}# 初始化模板字典# dict{文件名,phash}for fpath,fdir,fs in os.walk(templePath):    for f in fs:        fname,fext=os.path.splitext(f)        bigPicTempleDic[fname]=imagehash.phash(            Image.open(os.path.join(fpath,f)),            hash_size=hash_size)        del bigPicTempleDic['keyname']

1.4 小图片识别(字符分割+CNN)

对于小图片,则继续分割字符,再将单个字符输入CNN,识别结果。

1.4.1 字符分割

比如‘1,234’,分割为五个字符:‘1’,‘,’,‘2’,‘3’,‘4’。基本原理跟上文的聚焦差不多,就是找到字符之间的空隙。

def splitChar(img):    imgF=img_as_float(img)    # 求各列的和    col_sum=imgF.sum(axis=0)    idx=np.argwhere(col_sum==col_sum.max())    images=[]    for i in range(1,len(idx)):        if idx[i,0]-idx[i-1,0]>1:            imgHere=img.crop((idx[i-1,0],0,idx[i,0]+1,img.height))            images.append(imgHere)        return images

1.4.2 pytorch+CNN字符识别

用这个主要是之前看到些minst的文章,也想试试手pytorch这些ML框架。其实一开始使用的softmax多分类,测试发现效果并不好,比如数字2、3、7经常会搞混。后来考虑到softmax输入的图片是转为一维数组输入,也丢失图像的像素之间的结构关系,所以就换成CNN了。

参考资料:

  1. PyTorch学习之路(level1)——训练一个图像分类模型 - CSDN博客

  2. PyTorch学习之路(level2)——自定义数据读取 - CSDN博客

  3. 10分钟快速入门 PyTorch (4) - CNN - PyTorch Tutorialt

pytorch下载的时候,可能是上网不科学的缘故,安装地址刷不出,可以去这里试试:PyTorch 中文社区 - ApacheCNW

代码如下:

import torchimport torch.nn as nnimport torch.nn.functional as Fimport torch.optim as optimfrom torchvision import datasets, transformsfrom torch.autograd import VariabledicRes={    0:'0',    1:'1',    2:'2',           3:'3',    4:'4',    5:'5',           6:'6',    7:'7',    8:'8',           9:'9',    10:'N',    11:',',         12:'.',    13:'—',    14:'/',        15:'*',    16:'无'           }def cnnPred(data):    '''data是一个[1,1,28,28]的图'''    data.resize((1,1,28,28))    data=torch.FloatTensor(data)    data=Variable(data, volatile=True)    output = model(data)    # get the index of the max    pred = output.data.max(1, keepdim=True)[1]    return dicRes[int(pred)]class CNN(nn.Module):    def __init__(self):        super(CNN, self).__init__()        #1*28*28        self.conv1=nn.Sequential(            #16*28*28            #padding=(ks-1)/2时,图像大小不变            nn.Conv2d(in_channels=1,out_channels=16,kernel_size=5,stride=1,padding=2),            nn.ReLU(),            #16*14*14            nn.MaxPool2d(kernel_size=2)        )        self.conv2=nn.Sequential(            #32*14*14            nn.Conv2d(16,32,5,1,2),            nn.ReLU(),            #32*7*7            nn.MaxPool2d(2)        )        self.out=nn.Linear(32*7*7,17)            def forward(self, x):        x = self.conv1(x)        x = self.conv2(x)        x = x.view(x.size(0),-1)        output = self.out(x)        return outputmodel = CNN()# 加载已训练好的参数model.load_state_dict(torch.load('my_CNN_params.pkl'))

1.5 结果输出

单个图片输入,识别的结果输出到txt文本。将全部txt按照目标表格的样式,输出到excel,这些基本都是文件、文本、字符串的操作,就不细说了,参考资料:

  1. python操作Excel的几种方式 - lingwang3 - 博客园

2. 其他问题

2.1 有的表格的一条边颜色较浅

由于之前采用的是全局二值化,所以遇到这种表格一条边偏白的状况,二值化后就没了,导致表格的一些顶点没定位出来,最终结果就是输出到excel的时候,才发现一些数据错列了。

解决思路无非就是考虑自适应的二值化,或者是粗暴一点的分类二值化。由于赶时间,我用了比较粗暴的方法:

  1. 即原全局二值化的图片img_forSplit,继续作为分割单元格的底图;

  2. 查看表格的浅色边的灰度值,直接以该值再做一个全局二值化图img,作为定位顶点的图。

代码如下:

# 读取图片,并转灰度图img=io.imread(imgFilePath,True)#二值化img_forSplit=copy.deepcopy(img)#img 提取边框用bi_th=img.max()*0.875img[img<bi_th]=0img[img>=bi_th]=1# img_forSplit 分割用bi_th=0.733img_forSplit[img_forSplit<bi_th]=0img_forSplit[img_forSplit>=bi_th]=1

2.2 字符分割有出错

测试发现对于两个0相连,或者是8相连这种容易分割失败。比如‘000’,理想状况是分割出3个‘0’,但往往还是割出‘000’。放大图像就发现了,0这种算是数字里比较宽的字符了,几个0放一起,中间的像素会有些模糊的黏连,这块我还没想好解决方法。

(0)

相关推荐