ocr

这节我们来讲OCR识别是怎么实现的并附带一个例子

基础知识

实现简单的OCR算法只需要简单的一些东西:

  • 良好的线性代数基础
  • 良好的概率论基础
  • opencv库的函数使用
  • tesseract的使用

环境安装

我们的代码是用python实现的,为此需要安装一些东西,首先是opencv库

1
pip install opencv-python

安装完后在解释器中输入

1
import cv2

如果没有报错就是成功安装了。我们安装opencv库的主要原因是要对图形做处理。

其次是tesseract的安装。在官网按照引导下载即可。记得安装完配置环境变量。然后安装对应的python包

1
pip install pytesseract

现在,准备工作做好之后,就让我们开始下一步。

这次用的是别人的代码和图片,首先我们准备这么一张图

现在,为了识别小票的内容,我们需要做几件事情,第一步,检测小票的位置并识别其边缘在哪里,第二步,找出到底我们要检测的边缘在哪。第三步,把小票变换位置到能直接检测。

边缘检测

  • 第一步,我们先读取图像。
1
2
3
4
5
6
import cv2 as cv
#读取输入
img = cv.imread('picture/pic.png')
定义一个比率
ratio = img.shape[0] / 400.0
orig = img.copy() #复制一个未处理的图像
  • 第二步,处理图像

我们来对图像进行预处理工作,为了更好的识别,图片的原尺寸是2448 x 3264的大小,我们对其进行缩小以便做边缘检测。所以,我们要做如下的步骤

  • 用函数resize()用来计算输出宽度和高度
  • 对图像进行灰度处理,我们运用cvtColor函数
  • 对图像进行滤波作用去除噪点。

首先是第一步,比例缩放,为了缩小,我们用到INTER_AREA()来缩小图像。

1
2
3
4
5
6
7
8
9
10
11
12
13
def resize(image, width = None, height = None, inter=cv.INTER_AREA):
dim = None
(h,w) = image.shape[:2]
if width is None and height is None:
return image
if width is None:
r = height / float(h)
dim = (width,int(h*r))
else:
r = width / fload(w)
dim = (width, int(h*r))
resized = cv.resize(image,dim, interpolation = inter)
return resized

然后我们只需要轻轻的调用一下就行了

1
image = resize(orig, height=500)

接下来我们进行灰度处理,这是识别最常用的方法,利用cvtColor函数,我们可以把图像映射到灰度空间

1
gray = cv.cvtColor(image, cv.COLOR_BGR2GRAY)

然后是使用高斯滤波去除噪音

1
bimg = cv.GaussianBlur(gray, (5,5), 0)

其中(5,5)指的是卷积核大小。这里我们实现的是高斯滤波的一种方法,离散化窗口滑窗卷积,另外一种是傅里叶变换。

我们假设读者有一般的概率论基础(知道方差,标准差,期望和概率分布等等知识及其意义。)

高斯滤波的主要原理是,通过高斯函数进行函数的平滑化处理。

当我们要对一个图像模糊的时候,首先注意的是什么是模糊?一个图像可以表示为一个矩阵$A$,矩阵的元素是一些取值不大于255的值,所谓的模糊就是对矩阵进行分块,得到一系列的分块矩阵$A = \bigcup A_i$,再取分块的中心分别计算周遭的平均值再替换。这样子的好处是把图像变得更平滑。但这会导致一个问题,既然每个点都是取周遭的平均值,那么该怎么取呢。为此我们引入权重的概念。对于正态分布,有一个比较关注的点就是标准差,我们都知道正态分布的中心点正负三个标准差已经包含了几乎全部的信息。对高斯模糊,我们的重点也是离“中心点”越近的点取值就越高,反之越小。

现在,我们给定一组一维的像素值

相对坐标 -1 0 1
具体像素值 120 230 124

为了计算它们的权重,现在引入高斯函数(一维)

$f(x) =\frac{1}{\sigma\sqrt{2\pi}}e^{\frac{-x^2}{2\sigma^2}} $

给定一个方差$\sigma = 1.5$,我们现在开始计算权重,带入$-1,0,1$到$f(x)$中分别得到$0.212965, 0.265962,0.212965$三个不同的值。然后加起来得到$0.691892$。为了得到权重,我们需要用得到的三个不同值去除以它们的和。就可以得到

相对坐标 -1 0 1
权重 0.307801 0.384398 0.307801

然后我们就可以计算中心点的平均值了,利用权重有

平均值 = $120*0.307801 + 230*0.384398+ 124*0.307801 = 163.514984$

处理过后的像素值就接近了左右两边的值,这就是我们说的平滑化。

现在我们拓展到二维的情况,它的函数如下

$G(x,y) = \frac{1}{2\pi\sigma^2}e^{-\frac{x^2+y^2}{2\sigma^2}}$

它有一个非常好的性质,即:

$\begin{aligned} G(x,y) =& \frac{1}{2\pi\sigma^2}e^{-\frac{x^2+y^2}{2\sigma^2}}\\ =&\frac{1}{2\pi\sigma^2}e^{-\frac{x^2}{-2\sigma^2} - \frac{y^2}{2\sigma^2}}\\ =&G(x) \times G(y) \end{aligned}$

这说明在使用高斯模糊的时候,我们可以把一个二维矩阵做降维处理。


接下来让我们继续代码,使用Canny函数进行轮廓检测。

1
edged = cv.Canny(bimg, 75, 200)

这是在说,对像素值高于200和低于75的值做双向检测。然后我们就可以得到输出有:

im2

那么第一步就到此结束了。

由于图形不是平铺在平面上的,需要做点变换,那么如何变呢?一个问题是计算机如何识别边界。当识别到边界之后,我们才可以把这种歪歪斜斜的要识别的地方给摆正。才能做识别。所以,第二步,轮廓检测

轮廓检测

为了找出轮廓,我们从图中看到的东西有:有很多小圈圈,一个最大的矩形。而我们要找的轮廓就是哪个最大的矩形。

思路如下:我们遍历每个轮廓的面积,做排序,然后选出面积大小排前五的。首先,为了找出轮廓,我们用到 findContours函数,我们做如下解释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
findContours(image, mode ,method)

第一个参数是输入图像,第二个是轮廓的模式,第三个是近似轮廓的方法

mode有4种不同的模式,我们主要用到两种,一是cv2.RETR_LIST,用它检测的轮廓不具备等级关系,且
返回全部的轮廓而另一种cv2.RETR_CCOMP是建立两个等级的轮廓,类似的说,就是把轮廓分为内外两个
表示。但这里不需要用到,我们另外在做解释。

method参数决定了我们如何表达轮廓,我们对下述两种参数做描述。

+ cv2.CHAIN_APPROX_NONE: 储存所有点,相邻两个点位置不超过1,
即 abx(xi-xj) < max(abs(xs -xt))

+ cv2.CHAIN_APPROX_SIMPLE:压缩水平、垂直、对角线方向的元素并保留该方向的终点坐标,即只留下
特征信息(矩形保留四个顶点)来储存轮廓信息。

所以,我们先来做轮廓检测,我们使用RETR_LIST和SIMPLE参数,代码如下

1
cts = cv.findContours(edged.copy(), cv.RETR_LIST, cv.CHAIN_APPROX_SIMPLE)[1]

然后我们对面积的大小排序并取出前五个储存备用

1
bcts = sorted(cts, key = cv.contoursArea, reverse =True)[:5]

现在我们编写一个遍历轮廓的循环

1
2
3
4
5
6
7
8
9
10
11
for c in cts:
peri = cv.arcLength(c,True) #获取轮廓长度
approx = cv.approxPolyDP(c, 0.02*peri, True) #计算轮廓的近似

if len(approx) == 4:
screenCts = approx
break
#由于取的是矩形,当点的数量是4的时候取出即可。

cv.DrawContouts(img,[screenCts], -1 ,(0,0,255), 2 )
# -1表示取出全部点,然后是线的颜色,最后是线的宽度

输出:

image

投影变换

接下来的解释我希望读者有一些线性代数的基础以便我讲解。

提到线性代数的时候我们应该立马就想到线性空间$V$,$V$上的一个矩阵可以通过一些操作变成另一个矩阵,这种方法我们称为线性变换。

一个线性变换$A$指的是对任意$V$中的元素$\alpha,\beta$和数域$P$上的任意$k$满足

  • $A(\alpha+\beta) = A\alpha+A\beta$
  • $A(k\alpha) = kA(\alpha)$

它带有一些性质:

  • $A(0) = 0, A(-\alpha) = -A(\alpha)$

线性变换和矩阵的关系很近,当我们确定空间中的一组基时,我们可以通过一组基去确定一个$V$中的向量$\alpha$。在同一个空间中,你可以通过变换把$\alpha$从一个位置移到另一个位置得到向量$\beta$,这说明向量$\alpha$的每个分量$a_i$都会改变,而这种改变和原来的$\alpha$有一定关系,这种关系可以表示为一个变换$A$作用在$\alpha$得到向量$\beta$,而$A$可以看作是一个矩阵乘在$\alpha$上起了作用。

举几个简单的例子,取两个向量$\alpha = (0,1)’$和$\beta = (1,0)’$围起来则得到一个在第一象限的正方形,现在我们给出一些线性变换。来看看作用在两个向量上会得到什么。

我们把$\alpha$和$\beta$分别乘矩阵,得到新的向量,它在平面上表示为下面的样子:

2

看,我们就把原来的矩阵给翻转到下面了。所以,这也是为什么我们研究线性变换的目的,我们可以把原来歪歪扭扭的小票给识别出来,然后通过线性变换给放大、缩小、调整位置到软件容易识别。但用什么样的矩阵可以做到我们想要的结果。

为了完成变换的操作,我们需要四个输入的坐标和四个输出坐标,我们需要选择图像中四个我们需要的点(顶点)。我们应该确保选择的点是定义良好的,原始图像和目标图像有明确的对应关系,然后通过高度和宽度计算出小票在图像上的四个控制点。

另一个问题是矩阵的问题,我们知道一个变换可以通过一个矩阵的实现,那么我们如何寻找这种矩阵呢?opencv给我们提供了一个函数cv2.getPerspectiveTransform来得到这个变换矩阵。现在我们看看如何实现代码。

定义一些函数来获取座标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def order_points(pts):
rect = np.zeros((4,2),dtype = "float32")
#定义一个4x2的矩阵用来存储元素,类型为float32

s = pts.sum(axis = 1)
#把矩阵的每行加起来得到新的向量

#开始找顶点,利用argmax和argminx函数找出列中最小元素
#计算左上和右下的顶点
rect[0] = pts[np.argmin(s)]
rect[2] = pts[np.argmax(s)]

#计算右上和左下的顶点

diff = np.diff(pts,axis =1)
rect[1] = pts[np.argmin(diff)]
rect[3] = pts[np.argmax(diff)]

return rect

然后我们定义一个变换函数计算变换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def transform(image, pts):
rect = order_point(pts)
(tl,tr,br,bl) = rect #把4个2维向量赋值给四元组。

#计算距离
widthA = np.sqrt(((br[0]-bl[0]) ** 2) + ((br[1]-bl[1]) ** 2))
widthB = np.sqrt(((tr[0]-tl[0]) ** 2) + ((tr[1]-tl[1]) ** 2))
maxwidth = max(int(widthA),int(widthB))

heightA = np.sqrt(((tr[0]-br[0]) ** 2) + ((tr[1]-br[1]) ** 2))
heightB = np.sqrt(((tl[0]-bl[0]) ** 2) + ((tl[1]-bl[1]) ** 2))

maxHeight = max(int(heightA),int(heightB))

#定义变换后想要的坐标
dst = np.array([
[0,0],
[maxwidth -1 ,0]
[maxwidth -1, maxHeight -1],
[0,maxHeight -1],]dtype="float32"
)
#有了两个向量,现在我们可以来计算矩阵,利用函数
M = cv.getPerspectiveTransform(rect,dst)

#进行图片的变换操作
warped = cv.warpPerspective(image,M, (maxwidth, maxHeight))

return warped

接着我们就可以做变换

1
2
3
4
5
6
7
8
9
10
11
warped = transform(orig, screenCts.reshape(4,2) * ratio)
#变回原比例。

#二值化

warped = cv.cvtColor(warped, cv.COLOR_BGR2GRAY)
ref = cv.threshold(warped, 100,255, cv.THRESH_BINARY)[1]
cv.imwrite('scan.jpg',ref)

#输出
cv.imshow("put",resize(ref,height = 650))

结果就是
imag

接下来就是识别的代码编写了,我们的前期任务已完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from PIL import Image
import pytesseract
import cv2 as cv
import os

#读取图片
image = cv.imread('scan.jpg的图片路径')

#灰度&二值化处理

gray = cv.cvtColor(image,cv.COLOR_BGR2GRAY)

gray = cv.threshold(gray,0,255,cv.THRESH_BINARY | cv.THRESH_OTSU)[1]

fn = "{}.png".format(os.getpid())

cv.imwrite(fn, gray)

#识别和提取字符

text = pytesseract.image_to_string(Image.open(fn))
print(text)

结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
x * KK K KK K K K KK OK

WHOLE.
FOODS
TM AR KE T)

WHOLE FOODS MARKET - WESTPORT, CT 06880
399 POST RD WEST - (203) 227-6858

BEEF GRND

36%
365
365
365

BACON LS
BACON LS
BACON LS
BACUN LS

BROTH CHIC
FLOUR ALMUND
CHKN BRST BNLSS SK
HEAVY CREAM
BALSMC REDUCT

85/15

JUICE COF CASHEW ©
DOCS PINT ORGANIC
HNY ALMOND BUTTER