1 简介
在文字检测和文字识别的过程中,图像质量的好坏直接影响到检测率和识别率的高低,因此对于图像进行预处理是一个不可以被忽视的环节。
2 预处理方法之二值化
OCR的预处理方法可以分为以下几个大类:二值化、平滑去噪和倾斜角检测以及校正,脑图如下:本章由于篇幅的原因,此文只讲解二值化部分,以后更新会把后续的部分更新结束。
由上图可以看出,二值化预处理方法主要分为全局阈值方法、局部阈值方法和基于深度学习的方法以及其他方法。
2.1 全局阈值方法
2.1.1 固定阈值方法
固定阈值方法指的是将灰度值大于thresh(阈值)的像素设为白色,小于或等于thresh的像素设为黑色;或者反过来,将大于thresh的像素设为黑色,小于或等于thresh的像素设为白色。两者的区别只是呈现的形式不同。
对于的opencv函数如下:
import numpy as np import cv2 src = np.array([[123,234,68], [33,51,17],[48,98,234], [129, 89,27], [45,167,134], np.uint8]) thresh = 150 maxval = 255 dst = cv2.threshold(src, thresh, maxval, cv2.THRESH_BINARY) print(dst)
2.1.2 Ostu算法
Ostu算法又称最大类间方差法,是一种自适应阈值确定方法,该方法是在判别分析最小二乘法原理的基础上推导出来的,计算过程简单,是一种常用并且稳定的阈值算法。
原理解析:假设输入的图像为I,高为H、宽为W,代表归一化后的灰度直方图,代表灰度值等于k的像素点个数在图像中所占的比率,其中k∈[0,255]。算法的详细步骤如下:
- 第一步:计算灰度直方图的零阶累积矩(或者是累加直方图)
- 第二步:计算灰度直方图的一阶累积矩
- 第三步:计算图像I总体的灰度平均值mean,其实就是k=255时的一阶累积矩,即
- 第四步:计算每一个灰度级作为阈值时,前景区域的平均灰度、背景区域的平均灰度与整幅图像的平均灰度方差。对方差的衡量采用以下度量
- 第五步:找到上述最大的,然后对应的k即为Otsu自动选取的阈值,即
算法对应的opencv函数如下:
import cv2 from matplotlib import pyplot as plt image = cv2.imread("img/2-1.png") # 将输入图像转为灰度图 gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # 绘制灰度图 plt.subplot(311), plt.imshow(gray, "gray") plt.title("input image"), plt.xticks([]), plt.yticks([]) # 对灰度图使用Otsu算法 ret1, th1 = cv2.threshold(gray, 0, 255, cv2.THRESH_OTSU) # 绘制灰度直方图 plt.subplot(312), plt.hist(gray.ravel(), 256) # 标注Otsu阈值所在直线 plt.axvline(x=ret1, color='red', label='otsu') plt.legend(loc='upper right') plt.title("Histogram"), plt.xticks([]), plt.yticks([]) # 绘制二值化图像 plt.subplot(313), plt.imshow(th1, "gray") plt.title("output image"), plt.xticks([]), plt.yticks([]) plt.show()
执行结果如下:
分析可以看出,当目标物体与背景大小比例悬殊较大或灰度级接近时,Otsu算法的效果会有所损失。
2.2 局部阈值方法
2.2.1 自适应阈值算法
自适应阈值算法用到了积分的概念,它是一个快速且有效地对网格的矩形子区域计算和的算法。
自适应阈值算法的主要思想:以一个像素为中心设置大小为sxs的滑窗,滑窗扫过整张图片,每次扫描均对窗内的像素求均值并将均值作为局部阈值;若窗口中的某一像素值低于局部阈值t/100,赋值为0;高于局部阈值t/100,赋值255。因为涉及多次对有重叠的窗口进行加和运算,因此积分图的使用可以有效较低复杂度和操作次数。
假设输入图像为I,高为H、宽为W,平滑算子的尺寸记为HxW,其中W和H均为奇数,则自适应阈值算法的步骤如下:
- 第一步:对图像进行平滑处理,平滑结果记为,其中可以代表均值平滑、高斯平滑、中值平滑;
- 第二步:自适应阈值矩阵Thresh=(1-ratio)*,一般令ratio=0.15;
- 第三步:利用局部阈值算法规则:
算法对应的opencv函数如下:
def adaptiveThresh(I,winSize,ratio=0.15): # 第一步:对图像矩阵进行均值平滑 I_mean = cv2.boxFilter(I, cv2.CV_32FC1, winSize) # 第二步:原图像矩阵与平滑结果做差 out = I - (1.0 - ratio) * I_mean # 第三步:当差值大于或等于0时,输出值为255;反之,输出值为0 out[out>=0] = 255 out[out<0] = 0 return out
2.2.2 Niblack算法
Niblack算法同样是根据窗口内的像素值来计算局部阈值的,不同之处在于它不仅参考了区域内像素点的均值和方差,还考虑到用一个事先设定的修正系数k来决定影响程度:
其中,T(x,y)为阈值,m(x,y)、s(x,y)分别代表均值和方差。与m(x,y)相近的像素点被判定为背景,反之判定为前景,而相近的程度则由标准差和修正系数决定,这么做也保证了算法的灵活性。
算法对应的Matlab实现代码如下:
function output = niblack(image, varargin) % 初始化 if numvarargs > 4 error('myfunc:somefun2Alt:TooManyInputs',...); end optargs = {[3,3] -0.2 0 'replicate'}; optargs(1: numvarargs) = varargin [window, k, offset, padding] = optargs{:}; if ndims(image) ~= 2 error('The input image must be a teo-dimensional array.'); end image = double(image); % 计算均值 mean = averagefilter(image, window, padding); % 计算标准差 meanSquare=averagefilter(image.^2, window, padding); deviation = (meanSquare - mean.^2).^0.5; output = zeros(size(image)); % Niblack output(image>mean + k * deviation - offset) = 1;
Niblack算法的缺陷是:滑窗的尺寸r如果太小,则不能够很好的抑制噪声;如果尺寸很大,则会导致细节的丢失。
2.2.3 Sauvola算法
Sauvola算法是针对文档二值化处理,在Niblack算法的基础上进行的改进:
其中,R是标准方差的动态范围,若当前输入图像为8位灰度图像,则R=128。
由于上式起到了放大标准差s的作用,所以叫Niblack算法拥有更好的表现。算法对应的python实现代码如下:
def integral(img): ''' 计算图像的积分和平方积分 :param img:Mat--- 输入待处理图像 :return:integral_sum, integral_sqrt_sum:Mat--- 积分图和平方积分图 ''' integral_sum=np.zeros((img.shape[0],img.shape[1]),dtype=np.int32) integral_sqrt_sum=np.zeros((img.shape[0],img.shape[1]),dtype=np.int32) rows,cols=img.shape for r in range(rows): sum=0 sqrt_sum=0 for c in range(cols): sum+=img[r][c] sqrt_sum+=math.sqrt(img[r][c]) if r==0: integral_sum[r][c]=sum integral_sqrt_sum[r][c]=sqrt_sum else: integral_sum[r][c]=sum+integral_sum[r-1][c] integral_sqrt_sum[r][c]=sqrt_sum+integral_sqrt_sum[r-1][c] return integral_sum, integral_sqrt_sum def sauvola(img,k=0.1,kernerl=(31,31)): ''' sauvola阈值法。 根据当前像素点邻域内的灰度均值与标准方差来动态计算该像素点的阈值 :param img:Mat--- 输入待处理图像 :param k:float---修正参数,一般0<k<1 :param kernerl:set---窗口大小 :return:img:Mat---阈值处理后的图像 ''' if kernerl[0]%2!=1 or kernerl[1]%2!=1: raise ValueError('kernerl元组中的值必须为奇数, 请检查kernerl[0] or kernerl[1]是否为奇数!!!') # 计算积分图和积分平方和图 integral_sum,integral_sqrt_sum=integral(img) # integral_sum, integral_sqrt_sum = cv2.integral2(img) # integral_sum=integral_sum[1:integral_sum.shape[0],1:integral_sum.shape[1]] # integral_sqrt_sum=integral_sqrt_sum[1:integral_sqrt_sum.shape[0],1:integral_sqrt_sum.shape[1]] #创建图像 rows,cols=img.shape diff=np.zeros((rows,cols),np.float32) sqrt_diff=np.zeros((rows,cols),np.float32) mean=np.zeros((rows,cols),np.float32) threshold=np.zeros((rows,cols),np.float32) std=np.zeros((rows,cols),np.float32) whalf=kernerl[0]>>1#计算领域类半径的一半 for row in range(rows): print('第{}行处理中...'.format(row)) for col in range(cols): xmin=max(0,row-whalf) ymin=max(0,col-whalf) xmax=min(rows-1,row+whalf) ymax=min(cols-1,col+whalf) area=(xmax-xmin+1)*(ymax-ymin+1) if area<=0: sys.exit(1) if xmin==0 and ymin==0: diff[row,col]=integral_sum[xmax,ymax] sqrt_diff[row,col]=integral_sqrt_sum[xmax,ymax] elif xmin>0 and ymin==0: diff[row, col] = integral_sum[xmax, ymax]-integral_sum[xmin-1,ymax] sqrt_diff[row, col] = integral_sqrt_sum[xmax, ymax]-integral_sqrt_sum[xmin-1, ymax] elif xmin==0 and ymin>0: diff[row, col] = integral_sum[xmax, ymax] - integral_sum[xmax, ymax-1] sqrt_diff[row, col] = integral_sqrt_sum[xmax, ymax] - integral_sqrt_sum[xmax, ymax-1] else: diagsum=integral_sum[xmax, ymax]+integral_sum[xmin-1, ymin-1] idiagsum=integral_sum[xmax, ymin-1]+integral_sum[xmin-1, ymax] diff[row,col]=diagsum-idiagsum sqdiagsum=integral_sqrt_sum[xmax, ymax]+integral_sqrt_sum[xmin-1, ymin-1] sqidiagsum=integral_sqrt_sum[xmax, ymin-1]+integral_sqrt_sum[xmin-1, ymax] sqrt_diff[row,col]=sqdiagsum-sqidiagsum mean[row,col]=diff[row, col]/area std[row,col]=math.sqrt((sqrt_diff[row,col]-math.sqrt(diff[row,col])/area)/(area-1)) threshold[row,col]=mean[row,col]*(1+k*((std[row,col]/128)-1)) if img[row,col]<threshold[row,col]: img[row,col]=0 else: img[row,col]=255 return img
2.3 基于深度学习的方法
基于深度学习的方法主要是使用全卷积的二值化方法,具体读者可以阅读论文“Multi-Scale Fully Convolution Neural Network”是Chris Tensmeyer等人于2017年提出,利用多尺度全卷积神经网络对文档图像进行二值化,并在DIBCO和PLM这两个公开数据集上取得了很好的效果。
论文指出,传统的全局阈值或局部阈值忽略了像素点之间的排列,边缘检测、马尔可夫随机场等方法对于前景的形状有存在很强的偏置,但是FCN能从训练数据中学习并挖掘出像素点在空间上的联系,而不是依赖于在局部形状上人工设置的偏置。
2.4 其他方法
Rupinder Kaur等人于2016年提出了基于形态学和阈值的文档图像二值化方法,主要步骤有:
- 1、将RGB图像转为灰度图;
- 2、图像滤波处理;
- 3、数学形态学运算;
- 4、阈值运算。
其中,滤波分为维纳滤波和高斯滤波。算法对应的opencv函数如下:
import cv2 import numpy as np img = cv2.imread('img/2-1-4.png', 0) # 使用 getStructuringElement 定义结构元素,shape 为结构元素的形状,0:矩形;1:十字交 # 叉形;2:椭圆形;ksize 为结构元素的大小;anchor 为原点的位置,默认值(-1,-1)表示原点 # 为结构元素的中心点 k = cv2.getStructuringElement(shape=1, ksize=(3, 3), anchor=(-1, -1)) # k = np.ones((3,3),np.uint8) 也可以自己定义一个结构元素 # erode 函数实现腐蚀运算,src 为输入图像,kernel 为之前定义的结构元素,iterations 为 # 腐蚀操作次数 erosion = cv2.erode(src=img, kernel=k, iterations=1) cv2.imwrite("Eroded_Image.png", erosion) # dilate 函数实现膨胀运算,参数同上 dilation = cv2.dilate(img, k, iterations=1) cv2.imwrite("Dilated_Image.png", dilation) # morphologyEx 函数实现开闭运算, src 为输入图像,op 为运算类型,cv2.MORPH_OPEN:开 # 运算;cv2.MORPH_CLOSE:闭运算,kernel 为结构元素 opening = cv2.morphologyEx(src=img, op=cv2.MORPH_OPEN, kernel=k) closing = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel=k) cv2.imwrite("Opening_Image.png", opening) cv2.imwrite("Closing_Image.png", closing)
执行结果:
输入图片
腐蚀运算
膨胀运算
开运算
闭运算