CNN基础识别-想为女儿批作业(三):图像裁剪和结果展示
一、亮出效果
最近在线教育行业遭遇一点小波折,一些搜题、智能批改类的功能要下线。
退1024步讲,要不要自己做一个自动批改的功能啊?万一哪天孩子要用呢!
昨晚我做了一个梦,梦见我实现了这个功能,如下图所示:
功能简介: 作对了,能打对号;做错了,能打叉号;没做的,能补上答案。
醒来后,我环顾四周,赶紧再躺下,希望梦还能接上。
二、实现步骤
今天主要讲如何切分图片、计算结果,并将结果反馈出来。
往期回顾
2.1 准备数据
2.1.1 准备字体
2.1.2 生成图片
2.2 训练数据
2.2.1 构建模型
2.2.2 卷积层 Conv2D
2.2.3 池化层 MaxPooling2D
2.2.4 全连接层 Dense
2.2.5 训练数据
2.3 预测数据
之前我们准备了数据,训练了数据,并且拿图片进行了识别,识别结果正确。
到目前为止,看来问题不大……没有大问题,有问题也大不了。
下面就是把图片进行切割识别了。
下面这张大图片,怎么把它搞一搞,搞成单个小数字的图片。
2.4 切割图像
上帝说要有光,就有了光。
于是,当光投过来时,物体的背后就有了影。
我们就知道了,有影的地方就有东西,没影的地方是空白。
这就是投影。
这个简单的道理放在图像切割上也很实用。
我们把文字的像素做个投影,这样我们就知道某个区间有没有文字,并且知道这个区间文字是否集中。
下面是示意图:
2.4.1 投影大法
最有效的方法,往往都是用循环实现的。
要计算投影,就得一个像素一个像素地数,查看有几个像素,然后记录下这一行有N个像素点。如此循环。
首先导入包:
import numpy as np import cv2 from PIL import Image, ImageDraw, ImageFont import PIL import matplotlib.pyplot as plt import os import shutil from numpy.core.records import array from numpy.core.shape_base import block import time 复制代码
比如说要看垂直方向的投影,代码如下:
# 整幅图片的Y轴投影,传入图片数组,图片经过二值化并反色 def img_y_shadow(img_b): ### 计算投影 ### (h,w)=img_b.shape # 初始化一个跟图像高一样长度的数组,用于记录每一行的黑点个数 a=[0 for z in range(0,h)] # 遍历每一列,记录下这一列包含多少有效像素点 for i in range(0,h): for j in range(0,w): if img_b[i,j]==255: a[i]+=1 return a 复制代码
最终得到是这样的结构: [0, 79, 67, 50, 50, 50, 109, 137, 145, 136, 125, 117, 123, 124, 134, 71, 62, 68, 104, 102, 83, 14, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, ……38, 44, 56, 106, 97, 83, 0, 0, 0, 0, 0, 0, 0]
表示第几行总共有多少个像素点,第1行是0,表示是空白的白纸,第2行有79个像素点。
如果我们想要从视觉呈现出来怎么处理呢?那可以把它立起来拉直画出来。
1.5.....2..
# 展示图片 def img_show_array(a): plt.imshow(a) plt.show() # 展示投影图, 输入参数arr是图片的二维数组,direction是x,y轴 def show_shadow(arr, direction = 'x'): a_max = max(arr) if direction == 'x': # x轴方向的投影 a_shadow = np.zeros((a_max, len(arr)), dtype=int) for i in range(0,len(arr)): if arr[i] == 0: continue for j in range(0, arr[i]): a_shadow[j][i] = 255 elif direction == 'y': # y轴方向的投影 a_shadow = np.zeros((len(arr),a_max), dtype=int) for i in range(0,len(arr)): if arr[i] == 0: continue for j in range(0, arr[i]): a_shadow[i][j] = 255 img_show_array(a_shadow) 复制代码
我们来试验一下效果:
我们将上面的原图片命名为question.jpg放到代码同级目录。
# 读入图片 img_path = 'question.jpg' img=cv2.imread(img_path,0) thresh = 200 # 二值化并且反色 ret,img_b=cv2.threshold(img,thresh,255,cv2.THRESH_BINARY_INV) 复制代码
二值化并反色后的变化如下所示:
上面的操作很有作用,通过二值化,过滤掉杂色,通过反色将黑白对调,原来白纸区域都是255,现在黑色都是0,更利于计算。
计算投影并展示的代码:
img_y_shadow_a = img_y_shadow(img_b) show_shadow(img_y_shadow_a, 'y') # 如果要显示投影 复制代码
下面的图是上面图在Y轴上的投影
从视觉上看,基本上能区分出来哪一行是哪一行。
2.4.2 根据投影找区域
最有效的方法,往往还得用循环来实现。
上面投影那张图,你如何计算哪里到哪里是一行,虽然肉眼可见,但是计算机需要规则和算法。
# 图片获取文字块,传入投影列表,返回标记的数组区域坐标[[左,上,右,下]] def img2rows(a,w,h): ### 根据投影切分图块 ### inLine = False # 是否已经开始切分 start = 0 # 某次切分的起始索引 mark_boxs = [] for i in range(0,len(a)): if inLine == False and a[i] > 10: inLine = True start = i # 记录这次选中的区域[左,上,右,下],上下就是图片,左右是start到当前 elif i-start >5 and a[i] < 10 and inLine: inLine = False if i-start > 10: top = max(start-1, 0) bottom = min(h, i+1) box = [0, top, w, bottom] mark_boxs.append(box) return mark_boxs 复制代码
通过投影,计算哪些区域在一定范围内是连续的,如果连续了很长时间,我们就认为是同一区域,如果断开了很长一段时间,我们就认为是另一个区域。
通过这项操作,我们就可以获得Y轴上某一行的上下两个边界点的坐标,再结合图片宽度,其实我们也就知道了一行图片的四个顶点的坐标了mark_boxs
存下的是[坐,上,右,下]
。
如果调用如下代码:
(img_h,img_w)=img.shape row_mark_boxs = img2rows(img_y_shadow_a,img_w,img_h) print(row_mark_boxs) 复制代码
我们获取到的是所有识别出来每行图片的坐标,格式是这样的:[[0, 26, 596, 52], [0, 76, 596, 103], [0, 130, 596, 155], [0, 178, 596, 207], [0, 233, 596, 259], [0, 282, 596, 311], [0, 335, 596, 363], [0, 390, 596, 415]]
2.4.3 根据区域切图片
最有效的方法,最终也得用循环来实现。这也是计算机体现它强大的地方。
# 裁剪图片,img 图片数组, mark_boxs 区域标记 def cut_img(img, mark_boxs): img_items = [] # 存放裁剪好的图片 for i in range(0,len(mark_boxs)): img_org = img.copy() box = mark_boxs[i] # 裁剪图片 img_item = img_org[box[1]:box[3], box[0]:box[2]] img_items.append(img_item) return img_items 复制代码
这一步骤是拿着方框,从大图上用小刀划下小图,核心代码是img_org[box[1]:box[3], box[0]:box[2]]
图片裁剪,参数是数组的[上:下,左:右]
,获取的数据还是二维的数组。
如果保存下来:
# 保存图片 def save_imgs(dir_name, imgs): if os.path.exists(dir_name): shutil.rmtree(dir_name) if not os.path.exists(dir_name): os.makedirs(dir_name) img_paths = [] for i in range(0,len(imgs)): file_path = dir_name+'/part_'+str(i)+'.jpg' cv2.imwrite(file_path,imgs[i]) img_paths.append(file_path) return img_paths # 切图并保存 row_imgs = cut_img(img, row_mark_boxs) imgs = save_imgs('rows', row_imgs) # 如果要保存切图 print(imgs) 复制代码
图片是下面这样的:
2.4.4 循环可去油腻
还是循环。
横着行我们掌握了,那么针对每一行图片,我们竖着切成三块是不是也会了,一个道理。
需要注意的是,横竖是稍微有区别的,下面是上图的x轴投影。
横着的时候,字与字之间本来就是有空隙的,然后块与块也有空隙,这个空隙的度需要掌握好,以便更好地区分出来是字的间距还是算式块的间距。
幸好,有种方法叫膨胀。
膨胀对人来说不积极,但是对于技术来说,不管是膨胀(dilate),还是腐蚀(erode),只要能达到目的,都是好的。
kernel=np.ones((3,3),np.uint8) # 膨胀核大小 row_img_b=cv2.dilate(img_b,kernel,iterations=6) # 图像膨胀6次 复制代码
膨胀之后再投影,就很好地区分出了块。
根据投影裁剪之后如下图所示:
同理,不膨胀可截取单个字符。
这样,这是一块区域的字符。
一行的,一页的,通过循环,都可以截取出来。
有了图片,就可以识别了。有了位置,就可以判断识别结果的关系了。
下面提供一些代码,这些代码不全,有些函数你可能找不到,但是思路可以参考,详细的代码可以去我的github去看。
def divImg(img_path, save_file = False): img_o=cv2.imread(img_path,1) # 读入图片 img=cv2.imread(img_path,0) (img_h,img_w)=img.shape thresh = 200 # 二值化整个图,用于分行 ret,img_b=cv2.threshold(img,thresh,255,cv2.THRESH_BINARY_INV) # 计算投影,并截取整个图片的行 img_y_shadow_a = img_y_shadow(img_b) row_mark_boxs = img2rows(img_y_shadow_a,img_w,img_h) # 切行的图片,切的是原图 row_imgs = cut_img(img, row_mark_boxs) all_mark_boxs = [] all_char_imgs = [] # ===============从行切块====================== for i in range(0,len(row_imgs)): row_img = row_imgs[i] (row_img_h,row_img_w)=row_img.shape # 二值化一行的图,用于切块 ret,row_img_b=cv2.threshold(row_img,thresh,255,cv2.THRESH_BINARY_INV) kernel=np.ones((3,3),np.uint8) #图像膨胀6次 row_img_b_d=cv2.dilate(row_img_b,kernel,iterations=6) img_x_shadow_a = img_x_shadow(row_img_b_d) block_mark_boxs = row2blocks(img_x_shadow_a, row_img_w, row_img_h) row_char_boxs = [] row_char_imgs = [] # 切块的图,切的是原图 block_imgs = cut_img(row_img, block_mark_boxs) if save_file: b_imgs = save_imgs('cuts/row_'+str(i), block_imgs) # 如果要保存切图 print(b_imgs) # =============从块切字==================== for j in range(0,len(block_imgs)): block_img = block_imgs[j] (block_img_h,block_img_w)=block_img.shape # 二值化块,因为要切字符图片了 ret,block_img_b=cv2.threshold(block_img,thresh,255,cv2.THRESH_BINARY_INV) block_img_x_shadow_a = img_x_shadow(block_img_b) row_top = row_mark_boxs[i][1] block_left = block_mark_boxs[j][0] char_mark_boxs,abs_char_mark_boxs = block2chars(block_img_x_shadow_a, block_img_w, block_img_h,row_top,block_left) row_char_boxs.append(abs_char_mark_boxs) # 切的是二值化的图 char_imgs = cut_img(block_img_b, char_mark_boxs, True) row_char_imgs.append(char_imgs) if save_file: c_imgs = save_imgs('cuts/row_'+str(i)+'/blocks_'+str(j), char_imgs) # 如果要保存切图 print(c_imgs) all_mark_boxs.append(row_char_boxs) all_char_imgs.append(row_char_imgs) return all_mark_boxs,all_char_imgs,img_o 复制代码
最后返回的值是3
个,all_mark_boxs
是标记的字符位置的坐标集合。[左,上,右,下]
是指某个字符在一张大图里的坐标,打印一下是这样的: [[[[19, 26, 34, 53], [36, 26, 53, 53], [54, 26, 65, 53], [66, 26, 82, 53], [84, 26, 101, 53], [102, 26, 120, 53], [120, 26, 139, 53]], [[213, 26, 229, 53], [231, 26, 248, 53], [249, 26, 268, 53], [268, 26, 285, 53]], [[408, 26, 426, 53], [427, 26, 437, 53], [438, 26, 456, 53], [456, 26, 474, 53], [475, 26, 492, 53]]], [[[20, 76, 36, 102], [38, 76, 48, 102], [50, 76, 66, 102], [67, 76, 85, 102], [85, 76, 104, 102]], [[214, 76, 233, 102], [233, 76, 250, 102], [252, 76, 268, 102], [270, 76, 287, 102]], [[411, 76, 426, 102], [428, 76, 445, 102], [446, 76, 457, 102], [458, 76, 474, 102], [476, 76, 493, 102], [495, 76, 511, 102]]]]
它是有结构的。它的结构是:
第一层-数组行1行2第二层-行3行4行5块1第三层-块2块3字符1第四层-字符2字符3左上右下 495, 76, 511, 102
all_char_imgs
这个返回值,里面是上面坐标结构对应位置的图片。img_o
就是原图了。
2.5 识别
循环,循环,还是TM循环!
对于识别,2.3 预测数据已经讲过了,那次是对于2张独立图片的识别,现在我们要对整张大图切分后的小图集合进行识别,这就又用到了循环。
翠花,上代码!
all_mark_boxs,all_char_imgs,img_o = divImg(path,save) model = cnn.create_model() model.load_weights('checkpoint/char_checkpoint') class_name = np.load('class_name.npy') #遍历行 for i in range(0,len(all_char_imgs)): row_imgs = all_char_imgs[i] # 遍历块 for j in range(0,len(row_imgs)): block_imgs = row_imgs[j] block_imgs = np.array(block_imgs) results = cnn.predict(model, block_imgs, class_name) print('recognize result:',results) 复制代码
上面代码做的就是以块为单位,传递给神经网络进行预测,然后返回识别结果。
针对这张图,我们来进行裁剪和识别。
看底部的最后一行
recognize result: ['1', '0', '12', '2', '10'] recognize result: ['8', '12', '6', '10'] recognize result: ['1', '0', '12', '7', '10'] 复制代码
结果是索引,不是真实的字符,我们根据字典10: '=', 11: '+', 12: '-', 13: '×', 14: '÷'
转换过来之后结果是:
recognize result: ['1', '0', '-', '2', '='] recognize result: ['8', '-', '6', '='] recognize result: ['1', '0', '-', '7', '='] 复制代码
和图片是对应的:
2.6 计算并反馈
循环……
我们获取到了10-2=
、8-6=2
,也获取到了他们在原图的位置坐标[左,上,右,下]
,那么怎么把结果反馈到原图上呢?
往往到这里就剩最后一步了。
再来温习一遍需求:作对了,能打对号;做错了,能打叉号;没做的,能补上答案。
实现分两步走:计算(是作对做错还是没错)和反馈(把预期结果写到原图上)。
2.6.1 计算
python有个函数很强大,就是eval
函数,能计算字符串算式,比如直接计算eval("5+3-2")
。
所以,一切都靠它了。
# 计算数值并返回结果 参数chars:['8', '-', '6', '='] def calculation(chars): cstr = ''.join(chars) result = '' if("=" in cstr): # 有等号 str_arr = cstr.split('=') c_str = str_arr[0] r_str = str_arr[1] c_str = c_str.replace("×","*") c_str = c_str.replace("÷","/") try: c_r = int(eval(c_str)) except Exception as e: print("Exception",e) if r_str == "": result = c_r else: if str(c_r) == str(r_str): result = "√" else: result = "×" return result 复制代码
执行之后获得的结果是:
recognize result: ['8', '×', '4', '='] calculate result: 32 recognize result: ['2', '-', '1', '=', '1'] calculate result: √ recognize result: ['1', '0', '-', '5', '='] calculate result: 5 复制代码
2.6.2 反馈
有了结果之后,把结果写到图片上,这是最后一步,也是最简单的一步。
但是实现起来,居然很繁琐。
得找坐标吧,得计算结果呈现的位置吧,我们还想标记不同的颜色,比如对了是绿色,错了是红色,补齐答案是灰色。
下面代码是在一个图img上,把文本内容text画到(left,top)位置,以特定颜色和大小。
# 绘制文本 def cv2ImgAddText(img, text, left, top, textColor=(255, 0, 0), textSize=20): if (isinstance(img, np.ndarray)): # 判断是否OpenCV图片类型 img = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) # 创建一个可以在给定图像上绘图的对象 draw = ImageDraw.Draw(img) # 字体的格式 fontStyle = ImageFont.truetype("fonts/fangzheng_shusong.ttf", textSize, encoding="utf-8") # 绘制文本 draw.text((left, top), text, textColor, font=fontStyle) # 转换回OpenCV格式 return cv2.cvtColor(np.asarray(img), cv2.COLOR_RGB2BGR) 复制代码
结合着切图的信息、计算的信息,下面代码提供思路参考:
# 获取切图标注,切图图片,原图图图片 all_mark_boxs,all_char_imgs,img_o = divImg(path,save) # 恢复模型,用于图片识别 model = cnn.create_model() model.load_weights('checkpoint/char_checkpoint') class_name = np.load('class_name.npy') #遍历行 for i in range(0,len(all_char_imgs)): row_imgs = all_char_imgs[i] # 遍历块 for j in range(0,len(row_imgs)): block_imgs = row_imgs[j] block_imgs = np.array(block_imgs) # 图片识别 results = cnn.predict(model, block_imgs, class_name) print('recognize result:',results) # 计算结果 result = calculation(results) print('calculate result:',result) # 获取块的标注坐标 block_mark = all_mark_boxs[i][j] # 获取结果的坐标,写在块的最后一个字 answer_box = block_mark[-1] # 计算最后一个字的位置 x = answer_box[2] y = answer_box[3] iw = answer_box[2] - answer_box[0] ih = answer_box[3] - answer_box[1] # 计算字体大小 textSize = max(iw,ih) # 根据结果设置字体颜色 if str(result) == "√": color = (0, 255, 0) elif str(result) == "×": color = (255, 0, 0) else: color = (192, 192,192) # 将结果写到原图上 img_o = cv2ImgAddText(img_o, str(result), answer_box[2], answer_box[1],color, textSize) # 将写满结果的原图保存 cv2.imwrite('result.jpg', img_o) 复制代码
结果是下面这样的:
一个很长的梦,多少个夜晚的coding和思考,梦醒了,凌晨2点,终究还是实现了。
作者:TF男孩
链接:https://juejin.cn/post/7006732549451939847