CNN基础识别-想为女儿批作业(三):图像裁剪和结果展示

一、亮出效果

最近在线教育行业遭遇一点小波折,一些搜题、智能批改类的功能要下线。

退1024步讲,要不要自己做一个自动批改的功能啊?万一哪天孩子要用呢!

昨晚我做了一个梦,梦见我实现了这个功能,如下图所示:

GIF20210903210108.gif

功能简介: 作对了,能打对号;做错了,能打叉号;没做的,能补上答案。

醒来后,我环顾四周,赶紧再躺下,希望梦还能接上。

二、实现步骤

今天主要讲如何切分图片、计算结果,并将结果反馈出来。

往期回顾

之前我们准备了数据,训练了数据,并且拿图片进行了识别,识别结果正确。

到目前为止,看来问题不大……没有大问题,有问题也大不了。

下面就是把图片进行切割识别了。

下面这张大图片,怎么把它搞一搞,搞成单个小数字的图片。

原图.png

2.4 切割图像

上帝说要有光,就有了光。

于是,当光投过来时,物体的背后就有了影。

我们就知道了,有影的地方就有东西,没影的地方是空白。 image.png

这就是投影。

这个简单的道理放在图像切割上也很实用。

我们把文字的像素做个投影,这样我们就知道某个区间有没有文字,并且知道这个区间文字是否集中。

下面是示意图:

1.png

2.4.1 投影大法

最有效的方法,往往都是用循环实现的。

要计算投影,就得一个像素一个像素地数,查看有几个像素,然后记录下这一行有N个像素点。如此循环。

截图20210907064404.png

首先导入包:

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个像素点。

如果我们想要从视觉呈现出来怎么处理呢?那可以把它立起来拉直画出来。

graph LR
n1[1] --> s1[.    ]
n2[5] --> s2[.....]
n3[2] --> s3[..   ]
# 展示图片
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) 
复制代码

二值化并反色后的变化如下所示:

GIF20210907231259.gif

上面的操作很有作用,通过二值化,过滤掉杂色,通过反色将黑白对调,原来白纸区域都是255,现在黑色都是0,更利于计算。

计算投影并展示的代码:

img_y_shadow_a = img_y_shadow(img_b)
show_shadow(img_y_shadow_a, 'y') # 如果要显示投影
复制代码

下面的图是上面图在Y轴上的投影

rt.png

从视觉上看,基本上能区分出来哪一行是哪一行。

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
复制代码

通过投影,计算哪些区域在一定范围内是连续的,如果连续了很长时间,我们就认为是同一区域,如果断开了很长一段时间,我们就认为是另一个区域。

11.png

通过这项操作,我们就可以获得Y轴上某一行的上下两个边界点的坐标,再结合图片宽度,其实我们也就知道了一行图片的四个顶点的坐标了mark_boxs存下的是[坐,上,右,下]

2021-09-08_134741.png

如果调用如下代码:

(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)
复制代码

图片是下面这样的:

image.png

2.4.4 循环可去油腻

还是循环。

横着行我们掌握了,那么针对每一行图片,我们竖着切成三块是不是也会了,一个道理。

part_2.jpg

需要注意的是,横竖是稍微有区别的,下面是上图的x轴投影。

output.png

横着的时候,字与字之间本来就是有空隙的,然后块与块也有空隙,这个空隙的度需要掌握好,以便更好地区分出来是字的间距还是算式块的间距。

幸好,有种方法叫膨胀。

膨胀对人来说不积极,但是对于技术来说,不管是膨胀(dilate),还是腐蚀(erode),只要能达到目的,都是好的。

kernel=np.ones((3,3),np.uint8)  # 膨胀核大小
row_img_b=cv2.dilate(img_b,kernel,iterations=6) # 图像膨胀6次
复制代码

膨胀之后再投影,就很好地区分出了块。

output.png

根据投影裁剪之后如下图所示:

image.png

同理,不膨胀可截取单个字符。

image.png

这样,这是一块区域的字符。

一行的,一页的,通过循环,都可以截取出来。

有了图片,就可以识别了。有了位置,就可以判断识别结果的关系了。

下面提供一些代码,这些代码不全,有些函数你可能找不到,但是思路可以参考,详细的代码可以去我的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]]]]

它是有结构的。它的结构是:

graph TD
A[第一层-数组] --> 行1
A --> 行2
A --> B[第二层-行3]
A --> 行4
A --> 行5
B --> 块1
B --> C[第三层-块2]
B -->块3
C --> 字符1
C --> D[第四层-字符2]
C --> 字符3
D --> E[左上右下 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)
复制代码

上面代码做的就是以块为单位,传递给神经网络进行预测,然后返回识别结果。

针对这张图,我们来进行裁剪和识别。

question.jpg

看底部的最后一行

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', '=']
复制代码

和图片是对应的:

image.png

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)
复制代码

结果是下面这样的:

result.jpg

一个很长的梦,多少个夜晚的coding和思考,梦醒了,凌晨2点,终究还是实现了。

猜你喜欢

转载自juejin.im/post/7006732549451939847