基于Python skimage的表格识别程序
1. 开发环境
一提到数字图像处理编程,可能大多数人就会想到matlab,但matlab也有自身的缺点:
- 不开源,价格贵
- 软件容量大。一般3G以上,高版本甚至达5G以上。
- 只能做研究,不易转化成软件。
因此,我们这里使用python这个脚本语言来进行数字图像处理。
要使用python,必须先安装python,一般是2.7版本以上,不管是在windows系统,还是linux系统,安装都是非常简单的。要使用python进行各种开发和科学计算,还需要安装对应的包。这和matlab非常相似,只是matlab里面叫工具箱(toolbox),而python里面叫库或包。基于python脚本语言开发的数字图片处理包,其实很多,比如PIL,Pillow, opencv, scikit-image等。
对比这些包,PIL和Pillow只提供最基础的数字图像处理,功能有限;opencv实际上是一个c++库,只是提供了python接口,更新速度非常慢。到现在python都发展到了3.5版本,而opencv只支持到python 2.7版本;scikit-image是基于scipy的一款图像处理包,它将图片作为numpy数组进行处理,正好与matlab一样,因此,我们最终选择scikit-image进行数字图像处理。
1.1. 需要的安装包
因为scikit-image是基于scipy进行运算的,因此安装numpy和scipy是肯定的。要进行图片的显示,还需要安装matplotlib包,综合起来,需要的包有:
Python >= 2.6
Numpy >= 1.6.1
Cython >= 0.21
Six >=1.4
SciPy >=0.9
Matplotlib >= 1.1.0
NetworkX >= 1.8
Pillow >= 1.7.8
dask[array] >= 0.5.0
1.2. 下载并安装 anaconda
由于依赖的包比较多,安装起来非常费事,我们选择一款集成安装环境就行了,在此推荐Anaconda, 它把以上需要的包都集成在了一起,因此我们实际上从头到尾只需要安装Anaconda软件就行了,其它什么都不用装。
1.3. 简单测试
anaconda自带了一款编辑器spyder,我们以后就可以用这款编辑器来编写代码。我们简单编写一个程序来测试一下安装是否成功,该程序用来打开一张图片并显示。首先准备一张图片,然后打开spyder,编写如下代码:
from skimage import io
img=io.imread('png/01.png')
io.imshow(img)
如果右下角“ Ipython console" 能显示出图片,说明我们的运行环境安装成功。
1.4. skimage包的子模块
skimage包的全称是scikit-image SciKit (toolkit for SciPy) ,它对scipy.ndimage进行了扩展,提供了更多的图片处理功能。它是由python语言编写的,由scipy 社区开发和维护。skimage包由许多的子模块组成,各个子模块提供不同的功能。主要子模块列表如下:
子模块名称 | 主要实现功能 |
---|---|
io | 读取、保存和显示图片或视频 |
data | 提供一些测试图片和样本数据 |
color | 颜色空间变换 |
filters | 图像增强、边缘检测、排序滤波器、自动阈值等 |
draw | 操作于numpy数组上的基本图形绘制,包括线条、矩形、圆和文本等 |
transform | 几何变换或其它变换,如旋转、拉伸和拉东变换等 |
morphology | 形态学操作,如开闭运算、骨架提取等 |
exposure | 图片强度调整,如亮度调整、直方图均衡等 |
feature | 特征检测与提取等 |
measure | 图像属性的测量,如相似性或等高线等 |
segmentation | 图像分割 |
restoration | 图像恢复 |
util | 通用函数 |
用到一些图片处理的操作函数时,需要导入对应的子模块,如果需要导入多个子模块,则用逗号隔开,如:
from skimage import io,data,color
2. 图片中的表格识别
- 分割单元格。将图片中的表格全部定位出来,然后按单元格裁剪成一个个小图片,以便后续分析及操作;
- 聚焦。其实就是将单元格中的文本区域裁剪出来,将多余的空白去掉;
- 大图片的识别。对于大图片用图像相似性的算法(phash+汉明距离)做识别;
- 小图片的识别。对于小图片,做字符分割,然后用NN做分类识别;
- 识别结果输出到txt;
- txt输出到excel。将全部txt按照目标表格的格式,解析输出到excel。
2.1. 分割单元
既然只关心表格区域,所以第一步先将各个单元格拆分出来,截取成一个个小图片。尝试用图像的膨胀、腐蚀来定位表格区域,图像处理包skimage,最后算是定位出了表格区域,也分割出了各个单元格图片,其中部分中间过程的图片如下:
分割单元格这一块的基本流程是:
- 读取图像;
- 二值化处理;
- 横向、纵向的膨胀、腐蚀操作,得到横线图img_row和竖线图img_col;
- 得到点图,img_row + img_col=img_dot;
- 得到线图,img_row × img_col=img_line(线图只是拿来看看的,后续没有用到);
- 浓缩点团到单个像素;
- 开始遍历各行的点,将各个单元格从二值图像上裁剪出来,保存到temp文件夹。
2.1.1. 读取图像、二值化
#读取图像
from skimage import io
img=io.imread('png/01.png')
io.imshow(img)
#二值化
bi_th=0.81
img[img<=bi_th]=0
img[img>bi_th]=1
io.imshow(img)
2.1.2. 膨胀、腐蚀操作
# 膨胀腐蚀操作
def dil2ero(img,selem):
img=morphology.dilation(img,selem)
imgres=morphology.erosion(img,selem)
return imgres
# 求图像中的横线和竖线
rows,cols=img.shape
scale=80
col_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)
2.1.3. 得到点图、线图
# 线图
img_line=img_cols*img_rows
# 点图
img_dot=img_cols+img_rows
img_dot[img_dot>0]=1
io.imsave('png/tmp/table_dot.jpg',img_dot)
2.1.4. 收缩点团为单位像素点(3×3)
# 收缩点团为单像素点(3×3)
def isolate(imgdot):
idx=np.argwhere(imgdot<1) # img值小于1的索引数组
rows,cols=imgdot.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:
imgdot[c_row,c_col+1]=1
imgdot[c_row+1,c_col]=1
imgdot[c_row+1,c_col+1]=1
if c_col+2<cols and c_row+2<rows:
imgdot[c_row+1,c_col+2]=1
imgdot[c_row+2,c_col]=1
imgdot[c_row,c_col+2]=1
imgdot[c_row+2,c_col+1]=1
imgdot[c_row+2,c_col+2]=1
return imgdot
img_dot=isolate(img_dot)
io.imsave('png/tmp/table_dot_del.jpg',img_dot)
2.1.5. 获得交点
# print(dot_idxs.size)
for m in range(img_dot.shape[0]):
if m > 100: break
print('col{}'.format(m),end=" ")
for n in range(img_dot.shape[1]):
if img_dot[m][n]==0 :
print(img_dot[m][n], end= " ")
print("",end="\n")
dot_idxs=np.argwhere(img_dot<1) # img_dot值等于0的索引数组
for i in range(len(dot_idxs)):
for j in range(i,len(dot_idxs)):
if(dot_idxs[i][0]==dot_idxs[j][0]):
if(dot_idxs[i][1]>dot_idxs[j][1]):
tmp = dot_idxs[i][1];
dot_idxs[i][1] = dot_idxs[j][1]
dot_idxs[j][1] = tmp
在这里我们可以手动检查dot_idxs数据,观察其中的问题,最容易的可以手动修改dot_idxs数据,这样就可以得到正确的单元。
2.1.6. 检验单元格
def checkTableCols(dot_idxs):
table_cols = [] #记录每行有几个单元格
table_rows = 1
table_row_index = dot_idxs[0][0] #第一行点图y值坐标
colu_dot = 0 # 单元格数量
for n,indx in enumerate(dot_idxs):
if indx[0] == table_row_index : # 如果点图y值坐标相同则为同一行
colu_dot = colu_dot + 1
print(indx,end=' ')
else :
table_cols.append(colu_dot-1) # 将行单元格数量加入table_cols列表
table_row_index = dot_idxs[n+1][0] #记录下一行位置索引
colu_dot = 1 # 清0后,换行记录单元格数量
print(' ',end="\n")
print(indx, end=" ")
print("")
for n,v in enumerate(table_cols):
print('row{}:{}'.format(n,v))
return table_cols
2.1.7. 剪裁图片
def cutImage(img,table_cols,dot_idxs):
#开始识别单元格
cell_num = 0
num = 0
#if min_row == max_row : # 标准表格 n*n
for n,v in enumerate(table_cols):
print('row{}:'.format(n),end= '\n')
row_dot_num = v + 1
#cell_num += n * row_dot_num
for i in range(v):
cell_a_index = cell_num + i
cell_b_index = cell_a_index + 1
cell_c_index = cell_a_index + row_dot_num
cell_d_index = cell_c_index +1
print('cell[{}]:a{} b{} c{} d{}'.format(num+i,dot_idxs[cell_a_index],
dot_idxs[cell_b_index],dot_idxs[cell_c_index],dot_idxs[cell_d_index]),end= ' ')
x1=dot_idxs[cell_a_index][1]+3 # 加1去除列边框线
x2=dot_idxs[cell_b_index][1]
y1=dot_idxs[cell_a_index][0]+3 # 加1去除行边框线
y2=dot_idxs[cell_c_index][0]
print(x1,x2,y1,y2)
roi=img[y1:y2,x1:x2] # 50~100 行,50~100 列(不包括第 100 行和第 100 列)
io.imsave('png/tmp/pic_{}.jpg'.format(num+i),roi)
print('', end="\n")
cell_num += row_dot_num
num += v
return num
2.2. 训练语言库——数字为例
- 训练环境:首先安装jdk-10.0.1_windows-x64_bin.exe,它是java的运行环境。然后下载工具jTessBoxEditor,它是训练样本的工具。
-
样本图像:截图如下图像(越多越好,不过总共也就10个数字)。
- 合并图像:运行jTessBoxEditor,菜单栏中Tools--Merge TIFF。在弹出的对话框中选择样本图像(按Shift选择多张),合并名为num.font.exp0.tif文件。
-
生成Box file文件:
-
文字校正:jTessBoxEditor工具打开num.font.exp0.tif,增加未识别的、修改识别错误的数字。
-
生成语言库: