2.1 二值化
图像二值化(Image Binarization)是指将像素点的灰度值设为0或255,使图像呈现明显的黑白效果。二值化一方面减少了数据维度,另一方面通过排除原图中噪声带来的干扰,可以凸显有效区域的轮廓结构。OCR效果很大程度上取决于该步骤,高质量的二值图像可以显著提升识别的准确率。目前,二值化的方法主要分为全局阈值方法(Global Binarization)、局部阈值方法(Local Binarization)、基于深度学习的方法和其他方法。本节将对以上四大类二值化方法依次展开讨论。
2.1.1 全局阈值方法
1.固定阈值方法
该方法是对于输入图像中的所有像素点统一使用同一个固定阈值。其基本思想如下:
其中,T为全局阈值。下面我们用一组图(图2-2a到图2-2d)展示不同阈值的设定对输出结果的影响。输入图片如图2-1所示。
图2-1 输入图片
如图2-2所示,T=110时,二值化的效果最佳,保留了较为完整的目标区域结构,但同时也暴露了固定阈值方法的主要缺陷:很难为不同的输入图像确定最佳阈值。为解决这一问题,下面介绍的几种方法均采用了根据输入图片计算最佳阈值的思想。
图2-2 输出图片
2. Otsu算法
Otsu算法[1]又称最大类间方差法,由日本学者Nobuyuki Otsu于1979年提出,是一种自适应的阈值确定方法。其基本思想说明如下。
将输入图像视为 L 个灰度级,ni表示灰度级为 i 的像素个数,那么可知像素总数N=n1+n2+…+nL。为了简化讨论,这里使用归一化的灰度直方图,并将其视为输入图像的概率分布:
现假设在第 k 个灰度级设置阈值,将图像二分为C0和C1(背景和目标物体), C0表示灰度级为 [1, …, k] 的像素点,C1表示灰度级为[k+1,…, L]的像素点,那么两类出现的概率以及类内灰度级的均值分别为:
其中,ω(k) 和 µ(k) 分别为灰度级从1到 k 的累积出现概率和平均灰度级,而µT则是整张图像的平均灰度级。我们很容易就能验证,对于任意 k 值均有:
这两类的类内方差由以下两个公式给出:
为了评价阈值 k 的好坏,我们需要引入判别式,根据判别式的标准来进行测量:
其中,
σW、σB、σT、分别为类内方差、类间方差和灰度级的总方差。现在,我们将问题转化为一个优化问题,即找到一个k,使其能够最大化式(2.1)中的目标函数。我们注意到以下关系始终存在:
并且和都是k 的函数,但却与k无关;我们还注意到 是基于二阶统计(类方差),而则是基于一阶统计(类均值)。因此,η是判别k取值好坏的最简单的衡量标准:
至此,我们得到最佳的k值选择,即:
为了更加形象地解释Otsu算法,我们下面先用代码将图2-3a与图2-4a两张输入图像的灰度直方图以及算法得到的结果阈值绘制出来,首先来看代码清单2-1。
图2-3 Otsu算法实测一
图2-4 Otsu算法实测二
代码清单2-1 Otsu Python代码
1. import cv2 2. from matplotlib import pyplot as plt 3. image = cv2.imread("test.png") 4. #将输入图像转为灰度图 5. gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) 6. #绘制灰度图 7. plt.subplot(311), plt.imshow(gray, "gray") 8. plt.title("input image"), plt.xticks([]), plt.yticks([]) 9. #对灰度图使用Ostu算法 10. ret1, th1 = cv2.threshold(gray, 0, 255, cv2.THRESH_OTSU) 11. #绘制灰度直方图 12. plt.subplot(312), plt.hist(gray.ravel(), 256) 13. #标注Ostu阈值所在直线 14. plt.axvline(x=ret1, color='red', label='otsu') 15. plt.legend(loc='upper right') 16. plt.title("Histogram"), plt.xticks([]), plt.yticks([]) 17. #绘制二值化图像 18. plt.subplot(313), plt.imshow(th1, "gray") 19. plt.title("output image"), plt.xticks([]), plt.yticks([]) 20. plt.show()
观察图2-3b、图2-3c与图2-4b、图2-4c可以发现,对于灰度直方图呈现两个峰值的图像,Otsu算法得到的阈值(如图2-3b中的中线所示)为峰值间的低谷位置,二值化的效果也比较好;然而当目标物体与背景大小比例悬殊或灰度级接近,导致直方图呈现三峰或双峰峰值差距极大时,Otsu算法往往得不到满意的结果。因为单独将灰度分布作为设置阈值的依据,不仅会使结果对噪声极其敏感,而且还容易丢失图像中重要的空间结构关系。鉴于Otsu算法简单易懂,如代码清单2-1所示,Python的OpenCV库对其进行了封装,以方便调用。在实际应用中,Otsu算法常常与其他方法一起组合使用。
2.1.2 局部阈值方法
1.自适应阈值算法
自适应阈值算法[2]用到了积分图(Integral Image)的概念,它是一个快速且有效地对网格的矩形子区域计算和的算法。积分图中任意一点 (x, y) 的值是从图左上角到该点形成的矩形区域内所有值之和。图2-5b中的灰色部分为图2-5a中灰色矩阵内值的加和:
图2-5 积分图示例
自适应阈值算法的主要思想是以一个像素点为中心设置大小为 s×s 的滑窗,滑窗扫过整张图像,每次扫描均对窗口内的像素求均值并将均值作为局部阈值。若窗口中的某一像素值低于局部阈值t/100,赋值为0;高于局部阈值t/100,赋值为255。因为涉及多次对有重叠的窗口进行加和计算,因此积分图的使用可以有效地降低复杂度和操作次数。为了计算积分图,我们在每个位置存储其左边和上方所有 f(x, y) 值的总和,这一步骤会在线性时间内完成:
I(x, y)= f(x, y)+I(x-1, y)+I(x, y-1)-I(x-1, y-1)
一旦得到积分图,对于任意从左上角 (x1y1, ) 到右下角 (x2y2, ) 的矩形内(如图2-6所示)的数值总和均可以使用式(2.2)计算:
图2-6 积分图
根据式(2.2), D=(A+B+C+B)-(A+B)-(A+C)+A算法的伪代码如下:
流程共扫描全图两次,第一次扫描获得积分图intImg,第二次扫描根据式(2.2)计算每次扫描窗口内像素值的平均值的(100-t)/100,并将计算结果作为局部阈值。若窗口内某一像素点的值乘以窗口大小大于该阈值,则对应输出255,反之则输出0。
2. Niblack算法
Niblack算法[3]同样是根据窗口内的像素值来计算局部阈值的,不同之处在于它不仅考虑到区域内像素点的均值和方差,还考虑到用一个事先设定的修正系数 k 来决定影响程度。
T(x, y)=m(x, y)+ks(x, y)
其中,T(x, y) 为阈值,m(x, y)、s(x, y) 分别代表均值与方差。与 m(x, y) 相近的像素点被判定为背景,反之则判定为前景,而相近的程度则由标准差和修正系数来决定,这样做可以保证算法的灵活性。Niblack算法的缺陷一方面在于 r × r 的滑窗会导致在边界区域(r-1)/2的像素范围内无法求取阈值;另一方面在于如果 r×r 滑窗内全部是背景,那么该算法必然会使一部分像素点成为前景,形成伪噪声。因此,r的选择非常关键,若窗口太小,则不能有效地抑制噪声;若窗口太大,则会导致细节丢失。Niblack算法的MATLAB代码见代码清单2-2所示。
代码清单2-2 Niblack MATLAB代码
1. function output = niblack(image, varargin) 2. % 初始化 3. numvarargs = length(varargin); 4. if numvarargs > 4 5. error('myfuns:somefun2Alt:TooManyInputs', ... 6. 'Possible parameters are: (image, [m n], k, offset, padding)'); 7. end 8. 9. optargs = {[3 3] -0.2 0 'replicate'}; 10. 11. optargs(1:numvarargs) = varargin; 12. [window, k, offset, padding] = optargs{:}; 13. if ndims(image) ~= 2 14. error('The input image must be a two-dimensional array.'); 15. end 16. image = double(image); 17. % 计算均值 18. mean = averagefilter(image, window, padding); 19. % 计算标准差 20. meanSquare = averagefilter(image.^2, window, padding); 21. deviation = (meanSquare - mean.^2).^0.5; 22. output = zeros(size(image)); 23. % Niblack 24. output(image > mean + k * deviation - offset) = 1;
3. Sauvola算法
Sauvola算法[4][5] 是针对文档二值化处理,在Niblack算法基础上的改进:
其中,R是标准方差的动态范围,若当前输入图像是8位灰度图像,则 R=128。Sauvola算法在处理光线不均匀或染色图像时,比Niblack算法拥有更好的表现,因为式(2.3)以自适应的方式放大了标准差s的作用。假设我们要处理一份浅色但有污渍的文档,那么乘以m系数会降低背景区域阈值的范围,这可以有效地减少染色、污渍等带来的影响。表2-1展示了Sauvola与Niblack算法在处理染色文档时的效果差异。
表2-1 Niblack和Sauvola算法对染色文档的二值化结果对比
2.1.3 基于深度学习的方法
随着深度学习技术的快速发展,越来越多的工作人员尝试构建神经网络对图像进行二值化,并达到了state-of-the-art的水平。在具体讨论这些方法前,有必要先介绍一下文档图像二值化的两个公开数据集,它们是后续一系列文档图像二值化处理后的评价标准。
DIBCO(Document Image Binarization Contest)[6]于2009年首次举办,作为第一个国际文档图像二值化比赛,其目的是记录当前文档图像二值化领域的最新发展,并公开反映benchmarking数据集在图像二值化领域面临的各种潜在问题。如图2-7所示,DIBCO数据集包括灰度图、彩图、机器打印、手写文档、真实图像和模拟数据。
图2-7 DIBCO数据集部分数据展示
PLM(Palm Leaf Manuscripts)[7]数据集是ICFHR2016手写文字图像分析竞赛的baseline数据集(如图2-8所示)。鉴于其特殊的物理特性和手稿的自身条件,该数据集对文档图像分析提出了新的挑战。棕榈叶手稿因老化、低对比度等产生脱色、被污染的部分;字体非常见,且字符之间、行与行之间的间距不同;字符形状的畸变和损坏都是显而易见的。一些常见的二值化算法无法在该数据集上得到令人满意的结果,它们通常会将不常见字符识别成噪声,因此这项任务需要采用特殊且具有适应性的二值化技术。
图2-8 PLM数据集部分数据展示
使用全卷积的二值化方法
Multi-Scale Fully Convolutional Neural Network是Chris Tensmeyer等人于2017年提出来的[8],利用多尺度全卷积神经网络对文档图像进行二值化,并在DIBCO和PLM这两个公开数据集上均取得了较好的结果。论文[8]中指出,传统的全局或局部阈值忽略了像素点间的排列,边缘检测、马尔可夫随机场等方法对于前景的形状又存在很强的偏置,但是Fully Convolutional Network(FCN)能从训练数据中学习并挖掘出像素点在空间上的联系,而不是依赖于在局部形状上人工设置的偏置。图2-9为多尺度全卷积神经网络的网络结构。
图2-9 多尺度全卷积神经网络结构
论文中提出的FCN将 x∈RD×H×W映射到 y∈R H ×W,其中 yij∈[0,1] 是像素点xij属于前景
的概率,第 l 层(1≤l≤L)在卷积操作之后紧跟ReLu激活函数:
xl=ReLu(Wl xl-1+bl)
其中,是第 l 层的输出,x0是输入图片,ReLu(z)=max(0, z) 是element-wise的修正,是卷积核,是每个核对应的偏置项。根据以往FCN的应用经验,合并多尺度信息可以有效提升模型表现。该方法使用了多个FCN分别在1、1/2、1/4、1/8四个尺度上对DIBCO数据集图像做特征计算。在各个尺度进行了一系列的卷积操作之后,通过双线性插值的方法将特征图采样至原始图片大小,经concat函数处理后再经过两层卷积得到最后的二值化图像。
该方法超过了DIBCO多次比赛中的最好成绩(见表2-2)。
表2-2 全卷积方法与2009—2016 年DIBCO比赛首位的结果比较
2.1.4 其他方法
Rupinder Kaur等人于2016年提出了基于形态学和阈值的文档图像二值化方法[9]。该方法的实现可分为如下四步。
1)将RGB图像转化为灰度图。
2)图像滤波处理。
3)数学形态学运算。
4)阈值计算。
其中,滤波分为维纳滤波(Wiener Filter)和高斯滤波(Gaussian Filter)。维纳滤波又称最小二值滤波器,是利用平稳随机过程的相关特性和频谱特性对混有噪声的信号进行滤波;高斯滤波是一种线性平滑滤波器,适用于消除高斯噪声。
数学形态学(Mathematical Morphology)是图像处理中被广为应用的技术之一,通过从图像中提取对表达和描绘区域形状有意义的图像分量,使后续工作能够抓住目标对象最具区分性的形状特征。其中,二值图像的基本运算包括:腐蚀、膨胀、开运算和闭运算。
对于灰度图而言,腐蚀和膨胀运算都类似于卷积操作——将结构元素在原图上平移,而结构元素上的原点就相当于卷积核的核中心。首先我们约定,将结构元素覆盖住的原图区域记为P。如图2-10所示,腐蚀运算即在平移过程中依次计算 P 和 结构元素的差矩阵,并将该矩阵中的最小值赋给原点对应的原图位置;如图2-11所示,膨胀运算则是计算 P 和结构元素的和矩阵,并将该矩阵中的最大值赋给原点对应的原图位置。
图2-10 腐蚀运算示意图
图2-11 膨胀运算示意图
了解了最基本的腐蚀运算与膨胀运算,我们来学习开、闭运算。开、闭运算即二者的组合,开运算是先腐蚀后膨胀,闭运算是先膨胀后腐蚀。一般来说,开运算可以使图像的轮廓变得光滑,断开狭窄的连接并消除细毛刺;闭运算同样可以平滑轮廓,但其具体作用是排除小型空洞,弥合狭窄的间断点、沟壑以及填补断裂的轮廓线。
Python的OpenCV库提供了相关函数。下面以图2-12作为输入图像,图2-13展示四种形态学方法的输出结果,实现代码如下。
图2-12 输入图片
图2-13 四种形态学方法输出结果对比
1. import cv2 2. import numpy as np 3. img = cv2.imread('test.png',0) 4. #使用getStructuringElement定义结构元素,shape为结构元素的形状,0表示矩形、1表示十字 5. 交叉形、2表示椭圆形;ksize为结构元素的大小;anchor为原点的位置,默认值(-1, -1)表示原 6. 点为结构元素的中心点 7. k = cv2.getStructuringElement(shape = 1, ksize = (3,3), anchor = (-1, -1)) 8. # k = np.ones((3,3), np.uint8) 也可以定义一个结构元素 9. # erode函数实现腐蚀运算,src为输入图像,kernel为之前定义的结构元素, 10. iterations为腐蚀操作次数 11. erosion = cv2.erode(src = img, kernel = k, iterations = 1) 12. cv2.imshow("Eroded Image", erosion) 13. # dilate函数实现膨胀运算,参数同上 14. dilation = cv2.dilate(img, k, iterations = 1) 15. cv2.imshow("Dilated Image", dilation) 16. # morphologyEx函数实现开闭运算,src为输入图像,op为运算类型,cv2.MORPH_OPEN表示 17. 开运算;cv2.MORPH_CLOSE表示闭运算,kernel为结构元素 18. opening = cv2.morphologyEx(src = img, op = cv2.MORPH_OPEN, kernel = k) 19. closing = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel) 20. cv2.imshow("Opening Image", opening) 21. cv2.imshow("Closing Image", closing)