深入学习OpenCV文档扫描OCR识别及答题卡识别判卷(文档扫描,图像矫正,透视变换,OCR识别)

 

人工智能学习离不开实践的验证,推荐大家可以多在FlyAI-AI竞赛服务平台多参加训练和竞赛,以此来提升自己的能力。FlyAI是为AI开发者提供数据竞赛并支持GPU离线训练的一站式服务平台。每周免费提供项目开源算法样例,支持算法能力变现以及快速的迭代算法模型。

如果需要处理的原图及代码,请移步小编的GitHub地址

  传送门:请点击我

  如果点击有误:https://github.com/LeBron-Jian/ComputerVisionPractice

  下面准备学习如何对文档扫描摆正及其OCR识别的案例,主要想法是对一张不规则的文档进行矫正,然后通过tesseract进行OCR文字识别,最后返回结果。下面进入正文:

  现代生活中,手机像素比较高,所以大家拍这些照片都很随意,随便拍,比如下面的照片,如发票,文本等等:

   对于这些图像矫正的问题,在图像处理领域还真的很多,比如文本的矫正,车牌的矫正,身份证的矫正等等。这些都是因为拍摄者拍照随意,这就要求我们通过后期的图像处理技术将图片还原好,才能进行下一步处理,比如数字分割,数字识别,字母识别,文字识别等等。

  上面的问题,我们在日常生活中遇到的可不少,因为拍摄时拍的不好,导致拍出来的图片歪歪扭扭的,很不自然,那么我们如何将图片矫正过来呢?

  总的来说,要进行图像矫正,至少需要以下几步:

  • 1,文档的轮廓提取技术
  • 2,原始与变换坐标的计算
  • 3,通过透视变换获取目标区域

  本文通过两个案例,一个是菜单矫正及OCR识别;另一个是答题卡矫正及OCR识别。

1,如何扫描菜单并获取菜单内容

  下面以菜单为例,慢慢剖析如何实现图像矫正,并获取菜单内容。

   上面的斜着的菜单,如何扫描到如右图所示的照片呢?其实步骤有以下几步:

  • 1,探测边缘
  • 2,提取菜单矩阵轮廓四点进行透视变换
  • 3,应用一个透视的转换去获取一个文档的自顶向下的正图

  知道步骤后,我们开始做吧!

1.1,文档轮廓提取

  我们拿到图像之后,首先进行边缘检测,其中预处理包括对噪音进行高斯模糊,然后进行边缘检测(这里采用了Canny算子提取特征),下面我们可以看一下边缘检测的代码与结果:

  代码:

1

2

3

4

5

6

7

8

9

10

11

12

13

def edge_detection(img_path):

    # 读取输入

    img = cv2.imread(img_path)

    # 坐标也会相同变换

    ratio = img.shape[0] / 500.0

    orig = img.copy()

 

    image = resize(orig, height=500)

    # 预处理

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

    blur = cv2.GaussianBlur(gray, (5, 5), 0)

    edged = cv2.Canny(blur, 75, 200)

    show(edged)

   效果如下:

  我们从上图可以看到,已经将菜单的所有轮廓都检测出来了,而我们其实只需要最外面的轮廓,下面我们通过过滤得到最边缘的轮廓即可。

  代码如下:

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

33

def edge_detection(img_path):

    # *********  预处理 ****************

    # 读取输入

    img = cv2.imread(img_path)

    # 坐标也会相同变换

    ratio = img.shape[0] / 500.0

    orig = img.copy()

 

    image = resize(orig, height=500)

    # 预处理

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

    blur = cv2.GaussianBlur(gray, (5, 5), 0)

    edged = cv2.Canny(blur, 75, 200)

 

    # *************  轮廓检测 ****************

    # 轮廓检测

    contours, hierarchy = cv2.findContours(edged.copy(), cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)

    cnts = sorted(contours, key=cv2.contourArea, reverse=True)[:5]

 

    # 遍历轮廓

    for in cnts:

        # 计算轮廓近似

        peri = cv2.arcLength(c, True)

        # c表示输入的点集,epsilon表示从原始轮廓到近似轮廓的最大距离,它是一个准确度参数

        approx = cv2.approxPolyDP(c, 0.02*peri, True)

 

        # 4个点的时候就拿出来

        if len(approx) == 4:

            screenCnt = approx

            break

 

    res = cv2.drawContours(image, [screenCnt], -1, (0, 255, 0), 2)

    show(res)

   效果如下:

   如果说对轮廓排序后,不进行近似的话,我们直接取最大的轮廓,效果图如下:

1.2,透视变换(摆正图像)

  当获取到图片的最外轮廓后,接下来,我们需要摆正图像,在摆正图形之前,我们需要先学习透视变换。

1.2.1,cv2.getPerspectiveTransform()

  透视变换(Perspective Transformation)是将成像投影到一个新的视平面(Viewing Plane),也称作投影映射(Projective mapping),如下图所示,通过透视变换ABC变换到A'B'C'。

  cv2.getPerspectiveTransform() 获取投射变换后的H矩阵。

  cv2.getPerspectiveTransform() 函数的opencv 源码如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

def getPerspectiveTransform(src, dst, solveMethod=None): # real signature unknown; restored from __doc__

    """

    getPerspectiveTransform(src, dst[, solveMethod]) -> retval

    .   @brief Calculates a perspective transform from four pairs of the corresponding points.

    .  

    .   The function calculates the \f$3 \times 3\f$ matrix of a perspective transform so that:

    .  

    .   \f[\begin{bmatrix} t_i x'_i \\ t_i y'_i \\ t_i \end{bmatrix} = \texttt{map_matrix} \cdot \begin{bmatrix} x_i \\ y_i \\ 1 \end{bmatrix}\f]

    .  

    .   where

    .  

    .   \f[dst(i)=(x'_i,y'_i), src(i)=(x_i, y_i), i=0,1,2,3\f]

    .  

    .   @param src Coordinates of quadrangle vertices in the source image.

    .   @param dst Coordinates of the corresponding quadrangle vertices in the destination image.

    .   @param solveMethod method passed to cv::solve (#DecompTypes)

    .  

    .   @sa  findHomography, warpPerspective, perspectiveTransform

    """

    pass

   参数说明:

  • rect(即函数中src)表示待测矩阵的左上,右上,右下,左下四点坐标
  • transform_axes(即函数中dst)表示变换后四个角的坐标,即目标图像中矩阵的坐标

  返回值由原图像中矩阵到目标图像矩阵变换的矩阵,得到矩阵接下来则通过矩阵来获得变换后的图像,下面我们学习第二个函数。

1.2.2,cv2.warpPerspective()

  cv2.warpPerspective()  根据H获得变换后的图像。

  opencv源码如下:

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

def warpPerspective(src, M, dsize, dst=None, flags=None, borderMode=None, borderValue=None): # real signature unknown; restored from __doc__

    """

    warpPerspective(src, M, dsize[, dst[, flags[, borderMode[, borderValue]]]]) -> dst

    .   @brief Applies a perspective transformation to an image.

    .  

    .   The function warpPerspective transforms the source image using the specified matrix:

    .  

    .   \f[\texttt{dst} (x,y) =  \texttt{src} \left ( \frac{M_{11} x + M_{12} y + M_{13}}{M_{31} x + M_{32} y + M_{33}} ,

    .        \frac{M_{21} x + M_{22} y + M_{23}}{M_{31} x + M_{32} y + M_{33}} \right )\f]

    .  

    .   when the flag #WARP_INVERSE_MAP is set. Otherwise, the transformation is first inverted with invert

    .   and then put in the formula above instead of M. The function cannot operate in-place.

    .  

    .   @param src input image.

    .   @param dst output image that has the size dsize and the same type as src .

    .   @param M \f$3\times 3\f$ transformation matrix.

    .   @param dsize size of the output image.

    .   @param flags combination of interpolation methods (#INTER_LINEAR or #INTER_NEAREST) and the

    .   optional flag #WARP_INVERSE_MAP, that sets M as the inverse transformation (

    .   \f$\texttt{dst}\rightarrow\texttt{src}\f$ ).

    .   @param borderMode pixel extrapolation method (#BORDER_CONSTANT or #BORDER_REPLICATE).

    .   @param borderValue value used in case of a constant border; by default, it equals 0.

    .  

    .   @sa  warpAffine, resize, remap, getRectSubPix, perspectiveTransform

    """

    pass

   参数说明:

  • src 表示输入的灰度图像
  • M 表示变换矩阵
  • dsize 表示目标图像的shape,(width, height)表示变换后的图像大小
  • flags:插值方式,interpolation方法INTER_LINEAR或者INTER_NEAREST
  • borderMode:边界补偿方式,BORDER_CONSTANT or BORDER_REPLCATE
  • borderValue:边界补偿大小,常值,默认为0

1.2.3 cv2.perspectiveTransform()

  cv2.perspectiveTransform() 和 cv2.warpPerspective()大致作用相同,但是区别在于 cv2.warpPerspective()适用于图像,而cv2.perspectiveTransform() 适用于一组点。

  cv2.perspectiveTransform() 的opencv源码如下:

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

def perspectiveTransform(src, m, dst=None): # real signature unknown; restored from __doc__

    """

    perspectiveTransform(src, m[, dst]) -> dst

    .   @brief Performs the perspective matrix transformation of vectors.

    .  

    .   The function cv::perspectiveTransform transforms every element of src by

    .   treating it as a 2D or 3D vector, in the following way:

    .   \f[(x, y, z)  \rightarrow (x'/w, y'/w, z'/w)\f]

    .   where

    .   \f[(x', y', z', w') =  \texttt{mat} \cdot \begin{bmatrix} x & y & z & 1  \end{bmatrix}\f]

    .   and

    .   \f[w =  \fork{w'}{if \(w' \ne 0\)}{\infty}{otherwise}\f]

    .  

    .   Here a 3D vector transformation is shown. In case of a 2D vector

    .   transformation, the z component is omitted.

    .  

    .   @note The function transforms a sparse set of 2D or 3D vectors. If you

    .   want to transform an image using perspective transformation, use

    .   warpPerspective . If you have an inverse problem, that is, you want to

    .   compute the most probable perspective transformation out of several

    .   pairs of corresponding points, you can use getPerspectiveTransform or

    .   findHomography .

    .   @param src input two-channel or three-channel floating-point array; each

    .   element is a 2D/3D vector to be transformed.

    .   @param dst output array of the same size and type as src.

    .   @param m 3x3 or 4x4 floating-point transformation matrix.

    .   @sa  transform, warpPerspective, getPerspectiveTransform, findHomography

    """

    pass

   参数含义:

  • src:输入的二通道或三通道的图像
  • m:变换矩阵
  • 返回结果为相同size的图像

1.2.4  摆正图像

  将图像框出来后,我们计算出变换前后的四个点的坐标,然后得到最终的变换结果。

  代码如下:

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

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

def order_points(pts):

    # 一共四个坐标点

    rect = np.zeros((4, 2), dtype='float32')

     

    # 按顺序找到对应的坐标0123 分别是左上,右上,右下,左下

    # 计算左上,由下

    # numpy.argmax(array, axis) 用于返回一个numpy数组中最大值的索引值

    s = pts.sum(axis=1)  # [2815.2   1224.    2555.712 3902.112]

    print(s)

    rect[0] = pts[np.argmin(s)]

    rect[2] = pts[np.argmax(s)]

 

    # 计算右上和左

    # np.diff()  沿着指定轴计算第N维的离散差值  后者-前者

    diff = np.diff(pts, axis=1)

    rect[1] = pts[np.argmin(diff)]

    rect[3] = pts[np.argmax(diff)]

    return rect

 

 

# 透视变换

def four_point_transform(image, pts):

    # 获取输入坐标点

    rect = order_points(pts)

    (tl, tr, br, bl) = rect

 

    # 计算输入的w和h的值

    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 = cv2.getPerspectiveTransform(rect, dst)

    warped = cv2.warpPerspective(image, M, (maxWidth, maxHeight))

 

    # 返回变换后的结果

    return warped

 

 

# 对透视变换结果进行处理

def get_image_processingResult():

    img_path = 'images/receipt.jpg'

    orig, ratio, screenCnt = edge_detection(img_path)

    # screenCnt 为四个顶点的坐标值,但是我们这里需要将图像还原,即乘以以前的比率

    # 透视变换  这里我们需要将变换后的点还原到原始坐标里面

    warped = four_point_transform(orig, screenCnt.reshape(4, 2)*ratio)

    # 二值处理

    gray = cv2.cvtColor(warped, cv2.COLOR_BGR2GRAY)

    thresh = cv2.threshold(gray, 100, 255, cv2.THRESH_BINARY)[1]

 

    thresh_resize = resize(thresh, height = 400)

    show(thresh_resize)

  效果如下:

1.2.5  其他图片矫正实践

  这里图片原图都可以去我的GitHub里面去拿(地址:https://github.com/LeBron-Jian/ComputerVisionPractice)。

  对于下面这张图:

   我们使用透视变换抠出来效果如下:

   这个图使用和之前的代码就可以,不用修改任何东西就可以拿到其目标区域。

  下面看这张图:

   其实和上面图类似,不过这里我们依次看一下其图像处理过程,毕竟和上面两张图完全不是一个类型了。

  首先是 Canny算子得到的结果:

   其实拿到全轮廓后,我们就直接获取最外面的轮廓即可。

   我自己更改了一下,效果一样,但是还是贴上代码:

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

def edge_detection(img_path):

    # *********  预处理 ****************

    # 读取输入

    img = cv2.imread(img_path)

    # 坐标也会相同变换

    ratio = img.shape[0] / 500.0

    orig = img.copy()

 

    image = resize(orig, height=500)

    # 预处理

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

    blur = cv2.GaussianBlur(gray, (5, 5), 0)

    edged = cv2.Canny(blur, 75, 200)

    # show(edged)

    # *************  轮廓检测 ****************

    # 轮廓检测

    contours, hierarchy = cv2.findContours(edged.copy(), cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)

    #cnts = sorted(contours, key=cv2.contourArea, reverse=True)[:5]

 

    max_area = 0

    myscreenCnt = []

    for in contours:

        temp = cv2.contourArea(i)

        if max_area < temp:

            myscreenCnt = i

 

    # res = cv2.drawContours(image, myscreenCnt, -1, (0, 255, 0), 2)

    # show(res)

    return orig, ratio, screenCnt

   最后我们不对发票做任何处理,看原图效果:

   部分代码如下:

1

2

3

4

5

6

7

8

9

10

11

# 对透视变换结果进行处理

def get_image_processingResult():

    img_path = 'images/fapiao.jpg'

    orig, ratio, screenCnt = edge_detection(img_path)

    # screenCnt 为四个顶点的坐标值,但是我们这里需要将图像还原,即乘以以前的比率

    # 透视变换  这里我们需要将变换后的点还原到原始坐标里面

    warped = four_point_transform(orig, screenCnt.reshape(4, 2)*ratio)

 

    thresh_resize = resize(warped, height = 400)

    show(thresh_resize)

    return thresh

   下面再看一个例子:

  首先,它得到的Canny结果如下:

   我们需要对它进行一些小小的处理。

  我做了一些尝试,如果直接对膨胀后的图像,进行外接矩形,那么效果如下:

   代码如下:

1

2

3

x, y, w, h = cv2.boundingRect(myscreenCnt)

res = cv2.rectangle(image, (x,y), (x+w,y+h), (0, 255, 0), 2)

show(res)

   所以对轮廓取近似,效果稍微好点:

1

2

3

4

5

6

# 计算轮廓近似

peri = cv2.arcLength(myscreenCnt, True)

# c表示输入的点集,epsilon表示从原始轮廓到近似轮廓的最大距离,它是一个准确度参数

approx = cv2.approxPolyDP(myscreenCnt, 0.015*peri, True)

res = cv2.drawContours(image, [approx], -1, (0, 255, 0), 2)

show(res)

   效果如下:

   因为这个是不规整图形,所以无法进行四个角的转换,需要更多角,这里不再继续尝试。

1.3,OCR识别

  这里回到我们的菜单来,我们已经得到了扫描后的结果,下面我们进行OCR文字识别。

  这里使用tesseract进行识别,不懂的可以参考我之前的博客(包括安装tesseract,和通过tesseract训练自己的字库):

深入学习使用ocr算法识别图片中文字的方法

深入学习Tesseract-ocr识别中文并训练字库的方法

  配置好tesseract之后(这里不再show过程,因为我已经有了),我们通过其进行文字识别。

1.3.1  通过Python使用tesseract的坑

  如果直接使用Python进行OCR识别的话,会出现下面问题:

   这里因为anaconda下载的 pytesseract 默认运行的tesseract.exe 是默认文件夹,所以有问题,我们改一下。

  注意,找到安装地址,我们会发现有两个文件夹,我们进入上面文件夹即可

   进入之后如下,我们打开 pytesseract.py。

   注意这里的地址:

   我们需要修改为我们安装的地址,即使我们之前设置了全局变量,但是Python还是不care的。

   这里注意地址的话,我们通过 / 即可,不要 \,避免windows出现问题。

1.3.2  OCR识别

  安装好一切之后,就可以识别了,我们这里有两种方法,一种是直接在人家的环境下运行,一种是在Python中通过安装pytesseract 库运行,效果都一样。

  代码如下:

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

from PIL import Image

import pytesseract

import cv2

import os

 

preprocess = 'blur' #thresh

 

image = cv2.imread('scan.jpg')

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

 

if preprocess == "thresh":

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

 

if preprocess == "blur":

    gray = cv2.medianBlur(gray, 3)

     

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

cv2.imwrite(filename, gray)

     

text = pytesseract.image_to_string(Image.open(filename))

print(text)

os.remove(filename)

 

cv2.imshow("Image", image)

cv2.imshow("Output", gray)

cv2.waitKey(0)                                  

  使用Python运行,效果如下:

  直接在tesseract.exe运行:

  效果如下:

   可能识别效果不是很好。不过不重要,因为图片也比较模糊,不是那么工整的。

1.4,完整代码

  当然也可以去我的GitHub直接去下载。

  代码如下:

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

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

import cv2

import numpy as np

from PIL import Image

import pytesseract

 

 

def show(image):

    cv2.imshow('image', image)

    cv2.waitKey(0)

    cv2.destroyAllWindows()

 

def resize(image, width=None, height=None, inter=cv2.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 = (int(w*r), height)

    else:

        r = width / float(w)

        dim = (width, int(h*r))

    resized = cv2.resize(image, dim, interpolation=inter)

    return resized

 

 

def edge_detection(img_path):

    # *********  预处理 ****************

    # 读取输入

    img = cv2.imread(img_path)

    # 坐标也会相同变换

    ratio = img.shape[0] / 500.0

    orig = img.copy()

 

    image = resize(orig, height=500)

    # 预处理

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

    blur = cv2.GaussianBlur(gray, (5, 5), 0)

    edged = cv2.Canny(blur, 75, 200)

 

    # *************  轮廓检测 ****************

    # 轮廓检测

    contours, hierarchy = cv2.findContours(edged.copy(), cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)

    cnts = sorted(contours, key=cv2.contourArea, reverse=True)[:5]

 

    # 遍历轮廓

    for in cnts:

        # 计算轮廓近似

        peri = cv2.arcLength(c, True)

        # c表示输入的点集,epsilon表示从原始轮廓到近似轮廓的最大距离,它是一个准确度参数

        approx = cv2.approxPolyDP(c, 0.02*peri, True)

 

        # 4个点的时候就拿出来

        if len(approx) == 4:

            screenCnt = approx

            break

 

    # res = cv2.drawContours(image, [screenCnt], -1, (0, 255, 0), 2)

    # res = cv2.drawContours(image, cnts[0], -1, (0, 255, 0), 2)

    # show(orig)

    return orig, ratio, screenCnt

 

 

def order_points(pts):

    # 一共四个坐标点

    rect = np.zeros((4, 2), dtype='float32')

     

    # 按顺序找到对应的坐标0123 分别是左上,右上,右下,左下

    # 计算左上,由下

    # numpy.argmax(array, axis) 用于返回一个numpy数组中最大值的索引值

    s = pts.sum(axis=1)  # [2815.2   1224.    2555.712 3902.112]

    print(s)

    rect[0] = pts[np.argmin(s)]

    rect[2] = pts[np.argmax(s)]

 

    # 计算右上和左

    # np.diff()  沿着指定轴计算第N维的离散差值  后者-前者

    diff = np.diff(pts, axis=1)

    rect[1] = pts[np.argmin(diff)]

    rect[3] = pts[np.argmax(diff)]

    return rect

 

 

# 透视变换

def four_point_transform(image, pts):

    # 获取输入坐标点

    rect = order_points(pts)

    (tl, tr, br, bl) = rect

 

    # 计算输入的w和h的值

    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 = cv2.getPerspectiveTransform(rect, dst)

    warped = cv2.warpPerspective(image, M, (maxWidth, maxHeight))

 

    # 返回变换后的结果

    return warped

 

 

# 对透视变换结果进行处理

def get_image_processingResult():

    img_path = 'images/receipt.jpg'

    orig, ratio, screenCnt = edge_detection(img_path)

    # screenCnt 为四个顶点的坐标值,但是我们这里需要将图像还原,即乘以以前的比率

    # 透视变换  这里我们需要将变换后的点还原到原始坐标里面

    warped = four_point_transform(orig, screenCnt.reshape(4, 2)*ratio)

    # 二值处理

    gray = cv2.cvtColor(warped, cv2.COLOR_BGR2GRAY)

    thresh = cv2.threshold(gray, 100, 255, cv2.THRESH_BINARY)[1]

 

    cv2.imwrite('scan.jpg', thresh)

 

    thresh_resize = resize(thresh, height = 400)

    # show(thresh_resize)

    return thresh

 

 

 

def ocr_recognition(filename='tes.jpg'):

    img = Image.open(filename)

    text = pytesseract.image_to_string(img)

    print(text)

 

 

if __name__ == '__main__':

    # 获取矫正之后的图片

    # get_image_processingResult()

    # 进行OCR文字识别

    ocr_recognition()

 

2,如何扫描答题卡并识别

  答题卡识别判卷,大家应该都不陌生。那么它需要做什么呢?肯定是将我们在答题卡上画圈圈的地方识别出来。

  这是答题卡样子(原图请去我GitHub上拿:https://github.com/LeBron-Jian/ComputerVisionPractice):

  我们肯定是需要分为两步走,第一步就是和上面处理类似,拿到答题卡的最终透视变换结果,使得图片中的答题卡可以凸显出来。第二步就是根据正确答案和答题卡的答案来判断正确率。

2.1 扫描答题卡及透视变换

  这里我们对答题卡进行透视变换,因为之前已经详细的学习了这一部分,这里不再赘述,只是简单记录一下流程和图像处理效果,并展示代码。

  下面详细的总结处理步骤:

  • 1,图像灰度化
  • 2,高斯滤波处理
  • 3,使用Canny算子找到图片边缘信息
  • 4,寻找轮廓
  • 5,找到最外层轮廓,并确定四个坐标点
  • 6,根据四个坐标位置计算出变换后的四个角位置
  • 7,获取变换矩阵H,得到最终变换结果

  下面直接使用上面代码进行跑,首先展示Canny效果:

   当Canny效果不错的时候,我们拿到图像的轮廓进行筛选,找到最外面的轮廓,如下图所示:

   最后通过透视变换,获得答题卡的区域,如下图所示:

2.2  根据正确答案和图卡判断正确率

  这里我们拿到上面得到的答题卡图像,然后进行操作,获取到涂的位置,然后和正确答案比较,最后获得正确率。

  这里分为以下几个步骤:

  • 1,对图像进行二值化,将涂了颜色的地方变为白色
  • 2,对轮廓进行筛选,找到正确答案的轮廓
  • 3,对轮廓从上到下进行排序
  • 4,计算颜色最大值的位置和Nonezeros的值
  • 5,结合正确答案计算正确率
  • 6,将正确答案打印在图像上

  下面开始实践:

  首先对图像进行二值化,如下图所示:

   如果对二值化后的图直接进行画轮廓,如下:

   所以不能直接处理,这里我们需要做细微处理,然后拿到图像如下:

   这样就可以获得其涂的轮廓,如下所示:

   然后筛选出我们需要的涂了答题卡的位置,效果如下:

   然后通过这五个坐标点,确定答题卡的位置,如下图所示:

   然后根据真实答案和图中答案对比结果,我们将最终结果与圈出来答案展示在图上,如下:

   此项目到此结束。

 2.3 部分代码展示

  完整代码可以去我的GitHub上拿(地址:https://github.com/LeBron-Jian/ComputerVisionPractice)

   代码如下:

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

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

import cv2

import numpy as np

from PIL import Image

import pytesseract

 

 

def show(image):

    cv2.imshow('image', image)

    cv2.waitKey(0)

    cv2.destroyAllWindows()

 

 

def sorted_contours(cnt, model='left-to-right'):

    if model == 'top-to-bottom':

        cnt = sorted(cnt, key=lambda x:cv2.boundingRect(x)[1])

 

    elif model == 'left-to-right':

        cnt = sorted(cnt, key=lambda x:cv2.boundingRect(x)[0])

 

    return cnt

 

# 正确答案

ANSWER_KEY = {0:1, 1:4, 2:0, 3:3, 4:1}

 

def answersheet_comparison(filename='finalanswersheet.jpg'):

    '''

        对变换后的图像进行操作(wraped),构造mask

        根据有无填涂的特性,进行位置的计算

    '''

    img = cv2.imread(filename)

    # print(img.shape)   # 156*194

    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

    # 对图像进行二值化操作

    thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]

    # show(thresh)

 

    # 对图像进行细微处理

    kernele = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, ksize=(3, 3))

    erode = cv2.erode(thresh, kernele)

    kerneld = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, ksize=(3, 3))

    dilate = cv2.dilate(erode, kerneld)

    # show(dilate)

 

    # 对图像进行轮廓检测

    cnts = cv2.findContours(dilate, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0]

    # res = cv2.drawContours(img.copy(), cnts, -1, (0, 255, 0), 2)

    # # show(res)

 

 

    questionCnts = []

    for in cnts:

        (x, y, w, h) = cv2.boundingRect(c)

        arc = w/float(h)

 

        # 根据实际情况找出合适的轮廓

        if  w > 8 and h > 8 and arc >= 0.7 and arc <= 1.3:

            questionCnts.append(c)

 

    # print(len(questionCnts))  # 这里总共圈出五个轮廓 分别为五个位置的轮廓

    # 第四步,将轮廓进行从上到下的排序

    questionCnts = sorted_contours(questionCnts, model='top-to-bottom')

 

 

    correct = 0

    all_length = len(questionCnts)

    for in range(len(questionCnts)):

        x, y, w, h = cv2.boundingRect(questionCnts[i])

        answer = round((x-32)/float(100)*5)

        print(ANSWER_KEY[i])

        if answer == ANSWER_KEY[i]:

            correct += 1

            img = cv2.drawContours(img, questionCnts[i], -1, 0, 2)

     

    score = float(correct)/float(all_length)

    print(correct, all_length, score)

 

    cv2.putText(img, 'correct_score:%s'%score, (10, 15), cv2.FONT_HERSHEY_SIMPLEX,

        0.4, 0.3)

    show(img)

 

 

 

if __name__ == '__main__':

    answersheet_comparison()

 

参考文献:

https://www.pyimagesearch.com/2014/09/01/build-kick-ass-mobile-document-scanner-just-5-minutes/

https://blog.csdn.net/weixin_30666753/article/details/99054383

https://www.cnblogs.com/my-love-is-python/archive/2004/01/13/10439224.html

 

 

更多精彩内容请访问FlyAI-AI竞赛服务平台;为AI开发者提供数据竞赛并支持GPU离线训练的一站式服务平台;每周免费提供项目开源算法样例,支持算法能力变现以及快速的迭代算法模型。

挑战者,都在FlyAI!!!