基于深度学习的图片识别系统(上)
文章目录
- 前言
- 1.数据探索
- 2.表格旋转
- 2.1图像大小调节
- 2.2二值化处理
- 2.3线条提取
- 2.4边缘检测
- 2.5直线检测
- 2.6倾斜角度统计
- 2.7旋转
- 2.8总结
- 3.表格提取
- 3.1基础工作
- 3.2轮廓检测
- 3.3裁剪
- 4.线条去除
- 4.1基础工作
- 4.2线条提取
- 4.3线条去除
- 结语
前言
在实际工作中,通常有大量的表格需要在现场手工填写,然后再回录到信息系统,存在重复工作且复录时容易出现录错的问题,而采用中文手写体对相应表格内容进行实时识别和自动录入,人工进行检查校验即可,可减少大量人工工作量。本案例所实现的就是中文手写体的识别。该技术即图片识别技术,亦称光学字符识别(OCR)
1.数据探索
因为在实际开发环节是有很多数据的,这里为了方便起见,我们只选择了一张表格进行处理。在处理中使用了较多的opencv库
因此如果没有的话,执行下述代码,可以安装一下
pip install opencv-python
首先我们先看一下图片的形状及外观:
import cv2
image = cv2.imread('data/test.jpg')
cv2.imshow("image",image)
cv2.waitKey(0)
cv2.destroyAllWindows()
image.shape
运行结果:
这里需要手动关闭这个窗口,不然会一直处于运行中。
通过结果发现,该图片是1820*2809*3
形状的图片,3
表示该图片是一个三通道的彩色图片,同时通过外观发现,该图片有些许倾斜,这就是属于数据预处理的一部分工作了。下面只对一张图片处理,多张处理流程保持一致。
下述为数据预处理环节,此处按步骤进行展开阐述。
2.表格旋转
因为通过上述运行结果发现,图片中的表格属于倾斜状态,因此需要对表格进行加工,调整。
2.1图像大小调节
因为此处只处理了一张图片,为了批处理操作的统一性,我们首先需要对图片大小进行调整。这里使用了imutils库
,因此也需要安装。
pip install imutils
相关代码如下:
import imutils
image_height = 1080
img=imutils.resize(image, height=image_height)
img.shape
运行结果:
此时图片大小已经被调整为1080*1666*3
,因为宽度默认等比例自适应,之所以宽度未作固定,防止数据丢失。
2.2二值化处理
首先需要将图片转换为灰度图,即单通道
代码如下:
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
gray,gray.shape
运行结果:
通过结果发现,图片已经转化为单通道,通过结果发现,有很多值是255,因为图片的大部分偏白色的原因。
其次阈值化处理,此处使用cv2.threshold函数
。
cv2.threshold
是 OpenCV 库中用于图像阈值处理的函数,它能够根据设定的阈值将图像的像素值划分为两个类别,常用于图像的二值化处理。
代码如下:
thresh,blur_gray=cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)
thresh
这里设置了cv2.THRESH_OTSU
参数,因此你设置的阈值不会起作用,因为它会自动计算一个最优的阈值,结果如下
此时你可以再查看一下图片:
cv2.imshow("blur_gray",blur_gray)
cv2.waitKey(0)
cv2.destroyAllWindows()
运行结果:
结果发现,该图片已经二值反转,即高于阈值的置为0,低于阈值的置为255。
- 注:使用此方法查看图片时,尽量不要通过小窗关闭该窗口,因为我的会出bug,显示检测不到窗口已关闭
2.3线条提取
因为目的为调整倾斜的表格,因此需要检测该表格的倾斜程度,即直线的倾斜程度,因此需要将直线提取出来。
cv2.getStructuringElement
是 OpenCV 里用于创建形态学操作所需结构元素的函数。形态学操作是基于形状的一系列图像处理操作,像膨胀、腐蚀、开运算和闭运算等,而结构元素则是形态学操作的基础,它定义了操作的形状和大小。
首先定义一个矩形元素,表示要提取的结构元素:
代码如下:
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (30,1))
kernel.shape
通过结果发现,此时得到一个结构元素形状(1,30)
为什么呢?因为在 OpenCV 的 cv2.getStructuringElement 函数里,ksize 参数的顺序是 (width, height),也就是先宽度后高度,而不是正常的(height,width).
该元素提取的自然是一条横线,而不是竖线。
提取元素,并展示结果。
代码如下:
morhp_img = cv2.morphologyEx(blur_gray, cv2.MORPH_OPEN, kernel)
cv2.imshow("morhp_img",morhp_img)
cv2.waitKey(0)
cv2.destroyAllWindows()
运行结果:
通过将结果发大就可以看出,该图像若干大小相等的小矩形组成,这就取决于定义的kernel
注:此操作不能通过小窗关闭该窗口,因为我的会出bug,显示检测不到窗口已关闭。所以此后就不采用此方式进行展示图片。
这里结果保存如下:
cv2.imwrite("data/linesDetected.jpg",morhp_img)
如果存储失败,会返回False,这里你可以查看是不是文件路径出现了错误
这里为了使线条更加明显可以再次膨胀,也可以在之前cv2.morphologyEx
函数中修改iterations
参数值
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3,3),(-1,-1))
dilate_img = cv2.dilate(morhp_img, kernel,iterations=1)
cv2.imwrite("data/lines_dilated.jpg",dilate_img)
结果为:
2.4边缘检测
由于该直线过于粗,准确来说矩形的高度太大了,因此需要进行边缘检测,将矩形轮廓检测出来。这里使用Canny算法
进行边缘检测,你也可以使用拉普拉斯滤波器
。
low_threshold = 0
high_threshold = 150
edges = cv2.Canny(dilate_img, low_threshold, high_threshold)
cv2.imwrite("data/lines_edges.jpg",edges)
运行结果:
2.5直线检测
这里使用霍夫曼直线检测算法,查找图中所有的直线段。
如果对该算法和Canny算法的具体算法流程感兴趣的话,我也可以写博客介绍以下,因为视觉刚学过,哈哈,不过不清楚也没关系,这里会用即可
代码如下:
import numpy as np
lines = cv2.HoughLinesP(edges, 1, np.pi/180, threshold=15,lines=np.array([]),minLineLength=50, maxLineGap=20)
lines,len(lines)
运行结果:
从结果来看,共计得到314条直线,每条直线有四个数值确定,因为二维空间中,每个点有两个数字组成,同时两点确定一条直线,因此每条直线由四个数字组成。
2.6倾斜角度统计
共计314条直线,需要统计它们的平均倾斜程度,然后再将图片旋转该角度,即可完成表格旋转处理。
代码如下:
angles = []
for line in lines:
x1, y1, x2, y2 = line[0]
angle = np.arctan2(y2 - y1, x2 - x1) * 180. / np.pi
angles.append(angle)
median_angle = np.median(angles)
median_angle
运行结果:
结果表明,所有直线平均倾斜角度为
−
2.1
π
180
\frac{-2.1\pi}{180}
180−2.1π°。
2.7旋转
此时已经得到平均倾斜角度,因此只需要将图片旋转调整即可。
代码如下:
from scipy import ndimage
img_rotated = ndimage.rotate(img, median_angle)
cv2.imwrite("data/rotated.jpg",img_rotated)
运行结果:
通过结果可以看到,表格已经完成旋转调整。
2.8总结
在实际开发环节,因为需要批量处理图片,因此一般将该操作封装成函数,代码如下:
def rotate_image(img_for_box_extraction_path):
image_height = 1080
image = cv2.imread(img_for_box_extraction_path)
img = imutils.resize(image, height=image_height)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
(thresh, blur_gray) = cv2.threshold(gray, 128, 255, cv2.THRESH_BINARY_INV |
cv2.THRESH_OTSU)
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (30, 1))
morhp_img = cv2.morphologyEx(blur_gray, cv2.MORPH_OPEN, kernel, (-1, -1))
cv2.imwrite('tmp/linesDetected.jpg', morhp_img)
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3), (-1, -1))
lines_img = cv2.dilate(morhp_img, kernel, iterations=1)
cv2.imwrite("tmp/lines_dilated.jpg", lines_img)
low_threshold = 50
high_threshold = 150
edges = cv2.Canny(lines_img, low_threshold, high_threshold)
lines = cv2.HoughLinesP(edges, rho=1, theta=np.pi / 180, threshold=15,
lines=np.array([]), minLineLength=50, maxLineGap=20)
angles = []
for line in lines:
for x1, y1, x2, y2 in line:
angle = math.degrees(math.atan2(y2 - y1, x2 - x1))
angles.append(angle)
median_angle = np.median(angles)
img_rotated = ndimage.rotate(img, median_angle)
print("Angle is {}".format(median_angle))
cv2.imwrite('tmp/rotated.jpg', img_rotated)
return img_rotated
3.表格提取
为了对表格中的内容进行识别,需要先将表格提取出来,然后再进行单元格的提取,这里采用OpenCV中的轮廓检测(findContours)
方法提取表格的轮廓信息,并选择最大的四边形轮廓作为表格,将其裁剪出来。
3.1基础工作
这里的基础工作指的是大小调整,二值图像转换等操作,因为之前工作相对重复,这里就不再展开。
orig=img_rotated.copy()
image_height=500
ratio = orig.shape[0] / float(image_height)
image=imutils.resize(orig, height = image_height)
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
gray=cv2.adaptiveThreshold(gray,255,cv2.ADAPTIVE_THRESH_MEAN_C,cv2.THRESH_BINARY_INV,11,0)
对图片完成上述操作后,这里不再使用之前的可视化方法,而是用matplotlib
进行可视化,代码如下:
from matplotlib import pyplot as plt
plt.subplot(1,2,1),plt.imshow(orig),plt.title('Original')
plt.xticks([]), plt.yticks([])
plt.subplot(1,2,2),plt.imshow(gray),plt.title('gray')
plt.xticks([]), plt.yticks([])
plt.show()
运行结果:
3.2轮廓检测
cv2.findContours 是 OpenCV 库中一个非常重要的函数,主要用于在二值图像中查找轮廓。轮廓是由连续的点构成的曲线,代表了图像中物体的边界。
这里直接调用该函数:
countours,hierarchy=cv2.findContours(gray,cv2.RETR_LIST,cv2.CHAIN_APPROX_SIMPLE)
len(countours)
这里参数是可选的,只要明白目的——将表格裁剪出来即可
运行结果:
从结果来看,共检测到了1728轮廓,这里我们可以进行可视化查看一下:
for i in range(len(countours)-1700):
for point in countours[i].reshape(-1,2):
cv2.circle(image,tuple(point),5,(0,0,255),4)
plt.imshow(image),plt.title('contours')
plt.xticks([]), plt.yticks([])
plt.show()
运行结果:
这里只可视化了28个轮廓点,而我们想找的是表格的四个角,显然这些并不符合条件,因此需要筛选出符合要求的轮廓点。
这里我们可以通过面积进行筛选,同时由于拍摄原因或纸张变形等原因产生形变,所以此处选用cv2.approxPolyDP
进行辅助筛选。
代码如下:
countours=sorted(countours,key=cv2.contourArea,reverse=True)
screen_cut=None
for c in countours:
epsilon=0.02*cv2.arcLength(c,True)
approx=cv2.approxPolyDP(c,epsilon,True)
area=cv2.contourArea(c)
if area<2500:
continue
if len(approx)==4:
screen_cut=approx
break
if screen_cut is None:
detected=0
print("No contour detected")
运行结果:
因此已经轮廓,我们将它进行可视化,代码如下:
cv2.drawContours(image,[screen_cut],-1,(0,255,0),3)
for point in screen_cut.reshape(4,2):
cv2.circle(image,tuple(point),5,(0,0,255),4)
plt.imshow(image),plt.title('contours')
plt.xticks([]), plt.yticks([])
plt.show()
运行结果:
3.3裁剪
上述已完成表格的获取,下面需要对图片进行裁剪,获取表格内容,这里裁剪已封装成函数,代码如下,具体功能就是,根据四个点,将图片进行裁剪。
def order_points(pts):
rect = np.zeros((4, 2), dtype="float32")
s = pts.sum(axis=1)
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
def four_point_transform(image, pts):
rect = order_points(pts)
(tl, tr, br, bl) = rect
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)
return cv2.warpPerspective(image, M, (maxWidth, maxHeight))
这里直接调用该函数并保存图像:
from shang import four_point_transform
warp=four_point_transform(orig,screen_cut.reshape(4,2)*ratio)
cv2.imwrite("data/warped.jpg",warp)
这里之所以使用导入语句,因为该函数并不在本文件中,如果是在一个文件中就不需要导入语句了。
运行结果如下:
结果表明,经过裁剪标记的四个点已经被裁剪掉了。下一步对表格中的线条进行去除。
4.线条去除
4.1基础工作
这里的基础工作和上面一样,裁剪、二值化处理。代码如下:
img_bin=imutils.resize(warp, height=1080)
img_bin=cv2.cvtColor(img_bin,cv2.COLOR_BGR2GRAY)
thresh,binary_src=cv2.threshold(img_bin, 128, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)
plt.imshow(binary_src),plt.title('binary')
plt.xticks([]), plt.yticks([])
plt.show()
cv2.imwrite("data/Image_bin_warp_invert.jpg",binary_src)
运行结果如下:
4.2线条提取
同理,这里我们使用cv2.getStructuringElement
提取直线,通过腐蚀和膨胀将直线提取,不同的是,这里需要对横线和竖线都要进行操作,代码如下:
kernel_length_horizontal=binary_src.shape[1]//100
kernel_length_vertical=binary_src.shape[0]//30
kernel_vertical=cv2.getStructuringElement(cv2.MORPH_RECT,(1,kernel_length_vertical))
kernel_horizontal=cv2.getStructuringElement(cv2.MORPH_RECT,(kernel_length_horizontal,1))
kernel_vertical.shape,kernel_horizontal.shape
这里分别定义了横向和纵向处理的结构化元素,接着需要将线条提取出来,这里将其保存了。代码如下:
4.3线条去除
将纵向表格线与横向表格线进行加操作,构建蒙版层,并和原始图像进行异或操作,即可去除线条,代码如下:
mask_img = verticle_lines_img + horizontal_lines_img
binary_src = np.bitwise_xor(binary_src, mask_img)
plt.imshow(binary_src),plt.title('binary')
plt.xticks([]), plt.yticks([])
plt.show()
cv2.imwrite("data/no_border_image.jpg", binary_src)
运行结果:
从结果来看,部分表格线并未处理干净,存在细小的线条,因此对其再次进行腐蚀和膨胀,代码如下:
clean_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (2, 2))
img_erode = cv2.erode(binary_src, clean_kernel, iterations=1)
binary_src = cv2.dilate(img_erode, clean_kernel, iterations=1)
plt.imshow(binary_src),plt.title('binary')
plt.xticks([]), plt.yticks([])
plt.show()
cv2.imwrite("data/no_border_image_clean.jpg", binary_src)
运行结果:
完整版代码如下:
import cv2
import imutils
import numpy as np
import math
from math import *
from matplotlib import pyplot as plt
from scipy import ndimage
def rotate_image(img_for_box_extraction_path):
image_height = 1080
image = cv2.imread(img_for_box_extraction_path)
img = imutils.resize(image, height=image_height)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
(thresh, blur_gray) = cv2.threshold(gray, 128, 255, cv2.THRESH_BINARY_INV |
cv2.THRESH_OTSU)
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (30, 1))
morhp_img = cv2.morphologyEx(blur_gray, cv2.MORPH_OPEN, kernel, (-1, -1))
cv2.imwrite('tmp/linesDetected.jpg', morhp_img)
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3), (-1, -1))
lines_img = cv2.dilate(morhp_img, kernel, iterations=1)
cv2.imwrite("tmp/lines_dilated.jpg", lines_img)
low_threshold = 50
high_threshold = 150
edges = cv2.Canny(lines_img, low_threshold, high_threshold)
lines = cv2.HoughLinesP(edges, rho=1, theta=np.pi / 180, threshold=15,
lines=np.array([]), minLineLength=50, maxLineGap=20)
angles = []
for line in lines:
for x1, y1, x2, y2 in line:
angle = math.degrees(math.atan2(y2 - y1, x2 - x1))
angles.append(angle)
median_angle = np.median(angles)
img_rotated = ndimage.rotate(img, median_angle)
print("Angle is {}".format(median_angle))
cv2.imwrite('tmp/rotated.jpg', img_rotated)
return img_rotated
def order_points(pts):
rect = np.zeros((4, 2), dtype="float32")
s = pts.sum(axis=1)
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
def four_point_transform(image, pts):
rect = order_points(pts)
(tl, tr, br, bl) = rect
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)
return cv2.warpPerspective(image, M, (maxWidth, maxHeight))
def warp_image(image_height, image):
orig = image.copy()
ratio = image.shape[0] / float(image_height)
image = imutils.resize(image, height=image_height)
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
gray = cv2.adaptiveThreshold(gray, 255,cv2.ADAPTIVE_THRESH_MEAN_C,
cv2.THRESH_BINARY_INV, 11, 0)
# 可视化
plt.subplot(121), plt.imshow(orig), plt.title('Original')
plt.xticks([]), plt.yticks([])
plt.subplot(122), plt.imshow(gray), plt.title('gray')
plt.xticks([]), plt.yticks([])
plt.show()
# major = cv2.__version__.split('.')[0]
# print(major)
contours, hierarchy = cv2.findContours(gray, cv2.RETR_TREE,
cv2.CHAIN_APPROX_SIMPLE)
contours = sorted(contours, key=cv2.contourArea, reverse=True)
screen_cnt = None
for c in contours:
epsilon = cv2.arcLength(c, True)
approx = cv2.approxPolyDP(c, 0.02 * epsilon, True)
area=cv2.contourArea(c)
if area<2500:
continue
if len(approx) == 4:
screen_cnt = approx
break
if screen_cnt is None:
return -1,orig
warped = four_point_transform(orig, screen_cnt.reshape(4, 2) * ratio)
cv2.drawContours(image, [screen_cnt], -1, (0, 255, 0), 2)
for point in screen_cnt.reshape(4, 2):
cv2.circle(image, (point[0], point[1]), 5, (0, 0, 255), 4)
plt.subplot(121), plt.imshow(image), plt.title('Original')
plt.xticks([]), plt.yticks([])
plt.subplot(122), plt.imshow(warped), plt.title('Warped')
plt.xticks([]), plt.yticks([])
plt.show()
cv2.imwrite("tmp/warped.jpg", warped)
return 0,warped
if __name__ == '__main__':
# rotate_image("data/test.jpg")
warp_image(500, rotate_image("data/test.jpg"))
warped_image = cv2.imread("tmp/warped.jpg")
img_bin = imutils.resize(warped_image, height=1080)
img_bin = cv2.cvtColor(img_bin, cv2.COLOR_BGR2GRAY)
(thresh, binary_src) = cv2.threshold(img_bin, 128, 255, cv2.THRESH_BINARY_INV |
cv2.THRESH_OTSU)
cv2.imwrite("tmp/Image_bin_warp_invert.jpg", binary_src)
kernel_length_horizontal = np.array(binary_src).shape[1] // 100
kernel_length_vertical = np.array(binary_src).shape[0] // 30
verticle_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (1, kernel_length_vertical))
hori_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (kernel_length_horizontal, 1))
img_temp1 = cv2.erode(binary_src, verticle_kernel, iterations=4)
verticle_lines_img = cv2.dilate(img_temp1, verticle_kernel, iterations=4)
cv2.imwrite("tmp/verticle_lines.jpg", verticle_lines_img)
img_temp2 = cv2.erode(binary_src, hori_kernel, iterations=3)
horizontal_lines_img = cv2.dilate(img_temp2, hori_kernel, iterations=3)
cv2.imwrite("tmp/horizontal_lines.jpg", horizontal_lines_img)
mask_img = verticle_lines_img + horizontal_lines_img
binary_src = np.bitwise_xor(binary_src, mask_img)
cv2.imwrite("tmp/no_border_image.jpg", binary_src)
clean_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (2, 2))
img_erode = cv2.erode(binary_src, clean_kernel, iterations=1)
binary_src = cv2.dilate(img_erode, clean_kernel, iterations=1)
cv2.imwrite("tmp/no_border_image_clean.jpg", binary_src)
结语
至此数据预处理部分已算基本完成,虽然需要对表格进行裁剪,但此难度过大,因此本案例并不涉及该操作,后续训练会基于已处理好的数据集进行处理,撒花撒花!!!