最近一些软件的搜题、智能批改类的功能要下线。
退1024步讲,要不要自己做一个自动批改的功能啊?万一哪天孩子要用呢!
昨晚我做了一个梦,梦见我实现了这个功能,如下图所示:
作对了,能打对号;做错了,能打叉号;没做的,能补上答案。
醒来后,我环顾四周,赶紧再躺下,希望梦还能接上。
其实,搞定两点就成,第一是能识别数字,第二是能切分数字。
首先得能认识5是5,这是前提条件,其次是能找到5、6、7、8这些数字区域的位置。
前者是图像识别,后者是图像切割。
对于图像识别,一般的套路是下面这样的(CNN卷积神经网络):
对于图像切割,一般的套路是下面的这样(横向纵向投影法):
既然思路能走得通,那么咱们先搞图像识别。准备数据->训练数据并保存模型->使用训练模型预测结果。
对于男友,找一个油嘴滑舌的花花公子,不如找一个闷葫芦IT男,亲手把他培养成你期望的样子。
咱们不用什么官方的mnist数据集,因为那是官方的,不是你的,你想要添加±×÷它也没有。
有些通用的数据集,虽然很强大,很方便,但是一旦放到你的场景中,效果一点也不如你的愿。
只有训练自己手里的数据,然后自己用起来才顺手。更重要的是,我们享受创造的过程。
假设,我们只给口算做识别,那么我们需要的图片数据有如下几类:
如果能识别这些,基本上能满足整数的加减乘除运算了。
好了,图片哪里来?!
是啊,图片哪里来?
吓得我差点从梦里醒来,500万都规划好该怎么花了,居然双色球还没有选号!
梦里,一个老者跟我说,图片要自己生成。我问他如何生成,他呵呵一笑,消失在迷雾中……
仔细一想,其实也不难,打字我们总会吧,生成数字无非就是用代码把字写在图片上。
字之所以能展示,主要是因为有字体的支撑。
我们写代码调用这些字体,然后把它打印到一张图片上,是不是就有数据了。
而且这些数据完全是由我们控制的,想多就多,想少就少,想数字、字母、汉字、符号都可以,今天你搞出来数字识别,也就相当于你同时拥有了所有识别!想想还有点小激动呢!
看看,这就是打工和创业的区别。你用别人的数据相当于打工,你是不用操心,但是他给你什么你才有什么。自己造数据就相当于创业,虽然前期辛苦,你可以完全自己把握节奏,需要就加上,没用就去掉。
建一个fonts文件夹,从字体库里拷一部分字体放进来,我这里是拷贝了13种字体文件。
好的,准备工作做好了,肯定很累吧,休息休息休息,一会儿再搞!
代码如下,可以直接运行
核心代码就是画文字。
核心逻辑就是三层循环。
如果代码你运行的没有问题,最终会生成如下结果:
好了,数据准备好了。总共15个文件夹,每个文件夹下对应的各种字体各种倾斜角的字符图片3900个(字符15类×字体13种×角度20个),图片的大小是24×24像素。
有了数据,我们就可以再进行下一步了,下一步是训练和使用数据。
你先看代码,外行感觉好深奥,内行偷偷地笑。
这么多层都是干什么的,有什么用?和衣服一样,肯定是有用的,内衣、衬衣、毛衣、棉衣各有各的用处。
各个职能部门的调查员,搜集和整理某单位区域内的特定数据。我们输入的是一个图像,它是由像素组成的,这就是Rescaling(1./255,input_shape=(24,24,1))Rescaling(1./255,inputshape=(24,24,1))中,input_shape输入形状是24*24像素1个通道(彩色是RGB3个通道)的图像。
卷积层代码中的定义是Conv2D(24,3),意思是用3*3像素的卷积核,去提取24个特征。
我把图转到地图上来,你就能理解了。以我大济南的市中区为例子。
卷积的作用就相当于从地图的某级单位区域中收集多组特定信息。比如以小区为单位去提取住宅数量、车位数量、学校数量、人口数、年收入、学历、年龄等等24个维度的信息。小区相当于卷积核。
提取完成之后是这样的。
第一次卷积之后,我们从市中区得到N个小区的数据。
卷积是可以进行多次的。
比如在小区卷积之后,我们还可在小区的基础上再来一次卷积,在卷积就是街道了。
通过再次以街道为单位卷积小区,我们就从市中区得到了N个街道的数据。
这就是卷积的作用。
通过一次次卷积,就把一张大图,通过特定的方法卷起来,最终留下来的是固定几组有目的数据,以此方便后续的评选决策。这是评选一个区的数据,要是评选济南市,甚至山东省,也是这么卷积。这和现实生活中评选文明城市、经济强省也是一个道理。
说白了就是四舍五入。
池化层干的就是这个事情。池化的代码定义是这样的MaxPooling2D((2,2)),这里是最大值池化。其中(2,2)是池化层的大小,其实就是在2*2的区域内,我们认为这一片可以合成一个单位。
再以地图举个例子,比如下面的16个格子里的数据,是16个街道的学校数量。
为了进一步提高计算效率,少计算一些数据,我们用2*2的池化层进行池化。
池化的方格是4个街道合成1个,新单位学校数量取成员中学校数量最大(也有取最小,取平均多种池化)的那一个。池化之后,16个格子就变为了4个格子,从而减少了数据。
这就是池化层的作用。
弱水三千,只取一瓢。
在这里,它其实是一个分类器。
我们构建它时,代码是这样的Dense(15)。
它所做的事情,不管你前面是怎么样,有多少维度,到我这里我要强行转化为固定的通道。
比如识别字母a~z,我有500个神经元参与判断,但是最终输出结果就是26个通道(a,b,c,……,y,z)。
我们这里总共有15类字符,所以是15个通道。给定一个输入后,输出为每个分类的概率。
“
注意:上面都是二维的输入,比如24×24,但是全连接层是一维的,所以代码中使用了layers.Flatten()layers.Flatten()layers.Flatten()将二维数据拉平为一维数据([[11,12],[21,22]]->[11,12,21,22])。
对于总体的模型,调用model.summary()打印序列的网络结构如下:
model.compilemodel.compile就是配置模型的几个参数,这个现阶段记住就可以。
执行就完了。
终于到了享受成果的时候了。
我们要同时验证两张图,所以把两张图再组成imgs放到一起,imgs的结构是(2,24,24)。
下面是构建模型,然后加载权重。通过调用predicts=model.predict(imgs)将imgs传递给模型进行预测得出predicts。
predicts的结构是(2,15),数值如下面所示:
然后根据index=np.argmax(predict)找出最大可能的索引。
根据索引找到字符的数值结果是[‘6’,‘8’]。
下面是数据在内存中的监控:
可见,我们的预测是准确的。
下面,我们将要把图片中数字切割出来,进行识别了。
之前我们准备了数据,训练了数据,并且拿图片进行了识别,识别结果正确。
到目前为止,看来问题不大……没有大问题,有问题也大不了。
下面就是把图片进行切割识别了。
下面这张大图片,怎么把它搞一搞,搞成单个小数字的图片。
上帝说要有光,就有了光。
于是,当光投过来时,物体的背后就有了影。
我们就知道了,有影的地方就有东西,没影的地方是空白。
这就是投影。
这个简单的道理放在图像切割上也很实用。
我们把文字的像素做个投影,这样我们就知道某个区间有没有文字,并且知道这个区间文字是否集中。
下面是示意图:
最有效的方法,往往都是用循环实现的。
要计算投影,就得一个像素一个像素地数,查看有几个像素,然后记录下这一行有N个像素点。如此循环。
首先导入包:
如果我们想要从视觉呈现出来怎么处理呢?那可以把它立起来拉直画出来。
我们将上面的原图片命名为question.jpg放到代码同级目录。
上面的操作很有作用,通过二值化,过滤掉杂色,通过反色将黑白对调,原来白纸区域都是255,现在黑色都是0,更利于计算。
计算投影并展示的代码:
从视觉上看,基本上能区分出来哪一行是哪一行。
最有效的方法,往往还得用循环来实现。
上面投影那张图,你如何计算哪里到哪里是一行,虽然肉眼可见,但是计算机需要规则和算法。
通过这项操作,我们就可以获得Y轴上某一行的上下两个边界点的坐标,再结合图片宽度,其实我们也就知道了一行图片的四个顶点的坐标了mark_boxs存下的是[坐,上,右,下]。
如果调用如下代码:
最有效的方法,最终也得用循环来实现。这也是计算机体现它强大的地方。
如果保存下来:
还是循环。横着行我们掌握了,那么针对每一行图片,我们竖着切成三块是不是也会了,一个道理。
需要注意的是,横竖是稍微有区别的,下面是上图的x轴投影。
横着的时候,字与字之间本来就是有空隙的,然后块与块也有空隙,这个空隙的度需要掌握好,以便更好地区分出来是字的间距还是算式块的间距。
幸好,有种方法叫膨胀。
膨胀对人来说不积极,但是对于技术来说,不管是膨胀(dilate),还是腐蚀(erode),只要能达到目的,都是好的。
根据投影裁剪之后如下图所示:
同理,不膨胀可截取单个字符。
这样,这是一块区域的字符。
一行的,一页的,通过循环,都可以截取出来。
有了图片,就可以识别了。有了位置,就可以判断识别结果的关系了。
下面提供一些代码,这些代码不全,有些函数你可能找不到,但是思路可以参考,详细的代码可以去我的github去看。
all_char_imgs这个返回值,里面是上面坐标结构对应位置的图片。img_o就是原图了。
循环,循环,还是TM循环!
对于识别,2.3预测数据已经讲过了,那次是对于2张独立图片的识别,现在我们要对整张大图切分后的小图集合进行识别,这就又用到了循环。
翠花,上代码!
针对这张图,我们来进行裁剪和识别。
看底部的最后一行
循环……
我们获取到了10-2=、8-6=2,也获取到了他们在原图的位置坐标[左,上,右,下],那么怎么把结果反馈到原图上呢?
往往到这里就剩最后一步了。
再来温习一遍需求:作对了,能打对号;做错了,能打叉号;没做的,能补上答案。
实现分两步走:计算(是作对做错还是没错)和反馈(把预期结果写到原图上)。
所以,一切都靠它了。
但是实现起来,居然很繁琐。
得找坐标吧,得计算结果呈现的位置吧,我们还想标记不同的颜色,比如对了是绿色,错了是红色,补齐答案是灰色。
下面代码是在一个图img上,把文本内容text画到(left,top)位置,以特定颜色和大小。