代码生成OCR训练集,老板:没有数据?你new一个

夕阳很强,甚至有些刺眼,它穿透玻璃照射到键盘上来。

这是个普通的塑料键盘,缝隙里落满了灰尘、毛屑,在强光的照射下,更加清晰可见,犹如显微镜下的视野。

工作这么多年,我早已明白,键盘只是工具,影响技术水平的只有老板提出的需求。

IDE上的光标在那里闪动,它始终没有移动过一格,但也不曾休息过,就像我现在的思路一样。

我的脑海中不断地重复着上午的场景:

老板说:你不是说现在OCR技术很成熟了吗?那我们就自己搞啊!

我说:框架是很成熟了。但是,我们没有数据啊……

老板问:要什么数据?

我回答:你要想让机器认识“1”,你起码得拿500张“1”的照片来训练它。

老板问:那要训练“2”呢?

我回答:500张“2”的照片,而且不能重复,重复的算1张。你想想,常用汉字就3000多个,我们没有素材啊!要不要去网上买点……

老板陷入了沉思,突然眼镜一闪:哎,你让程序员new一个出来,你们连老婆都能new出来。

我连忙解释:那是对象。

我刚想说,手里没人,就一个实习生。

老板的电话突然响了,他捂着电话跟我说:我要出差了,3天,我回来时,你一定要把老婆……不是,把数据集给new出来!

微信图片_20220716004140.jpg

第一天:画黑框,输个字

一大早,我提着豆浆,走进办公室。

我的办公室不大,里面只有两个工位,一个我,一个实习生小王。但是,门口的标牌却赫然写着“产业园软件研发中心”几个字,老板说以后要扩到200百人的技术团队。

小王现在读大四,这一年在这里实习,岗位是研发工程师。小王工作很认真,每天来的比我还早,好好培养应该是个好苗子。

“请问,这里是财务部不?”,门口探出个脑袋,一个大爷问。

“不是!”。

“不是就不是,你那么大声音干什么!你这个房间很像财务部啊,一个小屋2个人,你是会计,他是出纳”。

我走向小王,小王正在看掘金博客,里面有好多大神:TF男孩,春哥,林三心……。

小王叫我老大,因为我是“产业园软件研发中心”的负责人,我负责他,他对我负责。

跟你安排一个任务,很简单,用python先画一个32*32像素的黑色背景,然后在上面写上白色的字,你去写吧。

小王很快就写好了。

from PIL import Image
from PIL import ImageDraw

# 画出一个32*32的黑色框
img = Image.new("RGB", (32, 32), "black") 
# 在黑框里写上字
draw = ImageDraw.Draw(img)
draw.text((0,0), "2", (255, 255, 255))
# 保存画好的图片
img.save("2.png")

小王上午就来找我汇报工作。

我试过了在黑色背景图上写上字母、数字、符号,都是可以的。

未标题-1 拷贝.png

我连连点头,称赞小王很棒。

小王问,接下来要做什么。我说,明天再告诉你。

第二天:先加载字体,再绘制字符

第二天,小王问我,老大,今天什么任务啊,我昨天等了一下午。

我说,运行你昨天的代码,输出个汉字试试。

小王执行了下面的代码: draw.text((0,0), "汉", (255, 255, 255)),结果报错了:AttributeError: 'ImageFont' object has no attribute 'getmask2'

小王愣在一旁,我却微微一笑:

你没有加载支持汉字的字体,就直接绘制汉字是不行的。今天的任务就是:你百度解决汉字的绘制。

小王直到下午才来找我汇报工作。

汉.png

他不但展示了效果,还跟我汇报了代码。

from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont

img = Image.new("RGB", (320, 320), "black") 
draw = ImageDraw.Draw(img)
# 加载一种字体, 320是字体的大小,和黑框一样大
font = ImageFont.truetype("chinese_fonts/fangzheng_heiti.TTF", 320)
# 将字体作为参数传入
draw.text((0,0), "汉", (255, 255, 255),font)
img.save("汉.png")

跟昨天的相比,区别就是调用 ImageFont.truetype("字体文件路径", 字体大小)加载了字体文件,然后调用draw.text(……,font)的时候,把字体font传入,这样字体大小也可以控制了。

我连连点头,称赞小王很棒。并继续说,汉字的可以了,你再试试数字和符号。

小王又调用draw.text(",")、raw.text("2")draw了个“,”和“2”。

2.png

“什么感觉?看完这些图片,你什么感觉?”,我问小王,声音有些严厉。

“没什么感觉啊,这……这不挺好的!”,小王回答道。

我提高了音量,敲着屏幕:“不居中啊,大哥,逗号那么明显,你看不出来吗?”。

刚刚还沉浸在骄傲中的小王有些疑惑,但是他依然很镇定:这个好弄,draw.text((x,y),……)我改下x和y坐标就行了。

“今天能改完吗?”,我问他。

“肯定能改完!调个坐标就完事了”,小王很自信的样子。

“好”,我告诉小王:“你要记得一件事,不要针对某一个字符调坐标,多调几个,不管是出1,2,3,4,还是甲乙丙丁,都要居中,记住了吗?”。

下午,我回家时,小王说要加个班。

凌晨2点,我在家上厕所时,远程查看了一下公司的网络流量数据,不断有关于“python”、“字体居中”的搜索。

第三天:字体居中,添加椒盐

第三天一早,我在家吃过饭,又从楼下买了包子和豆浆。

一进办公室,我发现小王趴在办公桌上不动了。

我心里就是一惊,别再是怎么着了吧。昨天的任务对他来说可真是够难的,我连忙晃动他:小王,醒醒,醒醒,小王!

小王慢慢地睁开眼,打了个哈欠,伸了个懒腰:天亮了吗?

小王发现我在旁边,才反应过来:哦,这是在公司啊,我的“居中”功能还没有实现呢!

老大,为什么我怎么调都有问题,x=20,y=30,对这个字符可以,换别字符的就不行了呢?如何才能写出通用的代码啊?那得加多少ifelse if判断啊?

我说,你先把早饭吃了,完了我告诉你个知识点,你马上就能完成任务了。

整合.png

其实,字体制作时,有很多规则。图中红框表示的区域,代表这是字体的范围,就算内部是空白也是人家的领地。里面,还有一个字符区域,是具体显示的内容,比如逗号,就靠下,为了便于标记,字符在字体内有一个偏移量offset属性,表示它相对于字体区域的偏移情况。

所以,你要让一个字在背景里居中,他的坐标绝不是你肉眼看到的,而且是千变万化的,需要你结合字体的宽高以及偏移量来计算。 整合2.png

小王很兴奋,有规则就好办了。

很快,他就写好了代码。

from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont

width,height = 32,32 # 因为宽高多处使用,定义成变量
font_size = 32
char = "好" # 要绘制的字符

img = Image.new("RGB", (width, height), "black") 
draw = ImageDraw.Draw(img)
# 加载一种字体, 32是字体的大小,和黑框一样大
font = ImageFont.truetype("chinese_fonts/fangzheng_fangsong.ttf", font_size)
# 获取字体的宽高
font_width, font_height = draw.textsize(char, font)
offset_x, offset_y = font.getoffset(char)

# 计算字体绘制的x,y坐标,主要是让文字画在图标中心
x = (width - font_width - offset_x) // 2
y = (height - font_height - offset_y) // 2
# 将字体作为参数传入
draw.text((x,y), char, (255,255, 255),font)
img.save("好.png")

小王试了试,不管是数字、字母、符号还是汉字,确实都可以居中了。 2022-07-15_235428.png

其实,关键点就是通过draw.textsize(char, font)获取了字体的宽高,通过font.getoffset(char)获取了偏移量的信息。如果要将字体在一个背景中居中,其实就是背景的长度减去字体长度再减去偏移量长度,这是字符和背景的缝隙,缝隙除以2,那就是把缝隙分到两边了,它就居中了。

今天,小王虽然没有睡觉,但是他却很开心,我告诉他帮公司解决了一个大难题,让他早早地回去休息了。

今天晚上,老板应该出差回来了。

我拿出3天前就写好的代码,跑了起来。

from __future__ import print_function
from PIL import Image
from PIL import ImageFont
from PIL import ImageDraw
import os
import shutil
import time
import cv2

# 要生成的文本
label_dict = {0: '你', 1: '好', 2: '掘', 3: '金', 4: ':', 5: '1', 6: '+', 7: '2', 8: ',', 9: 'g', 10: 'o', 11: '!'}

# 文本对应的文件夹,给每一个分类建一个文件
for value,char in label_dict.items():
    train_images_dir = "dataset"+"/"+str(value)
    if os.path.isdir(train_images_dir):
        shutil.rmtree(train_images_dir)
    os.makedirs(train_images_dir)

def makeImage(label_dict, font_path, width=32, height=32, rotate = 0, salt = 22):

    # 从字典中取出键值对
    for value,char in label_dict.items():
        # 创建一个黑色背景的图片
        img = Image.new("RGB", (width, height), "black") 
        draw = ImageDraw.Draw(img)
        # 加载一种字体,字体大小是图片宽度的90%
        font = ImageFont.truetype(font_path, int(width*0.9))
        # 获取字体的宽高
        font_width, font_height = draw.textsize(char, font)
        offset_x, offset_y = font.getoffset(char)
        # 计算字体绘制的x,y坐标,主要是让文字画在图标中心
        x = (width - font_width - offset_x) // 2
        y = (height - font_height - offset_y) // 2
        # 绘制图片,在那里画,画啥,什么颜色,什么字体
        draw.text((x,y), char, (255, 255, 255), font)
        # 设置图片倾斜角度
        if rotate != 0:
            img = img.rotate(rotate)
        
        # 将数据转为np格式
        np_img = np.asarray(img.getdata(), dtype='uint8')
        # 降维,3通道转为1通道,并组成矩阵
        np_img = np_img[:, 0].reshape((height, width))
        for i in range(salt): #添加噪声
            temp_x = np.random.randint(0,np_img.shape[0])
            temp_y = np.random.randint(0,np_img.shape[1])
            np_img[temp_x][temp_y] = 255

        # 命名文件保存,命名规则:dataset/编号/img-编号_r-选择角度_时间戳.png
        time_value = int(round(time.time() * 1000))
        img_path = "dataset/{}/{}_{}.png".format(value, time_value, rotate)
        cv2.imwrite(img_path, np_img)
        
# 存放字体的路径
font_dir = "./chinese_fonts"
for font_name in os.listdir(font_dir):
    # 把每种字体都取出来,每种字体都生成一批图片
    path_font_file = os.path.join(font_dir, font_name)
    # 倾斜角度从-5到5度,每个角度都生成一批图片
    for k in range(-5, 5, 1):	
        # 每个字符都生成图片
        makeImage(label_dict, path_font_file, rotate = k, salt = 5-k)

这段代码,不但生成了文字,而且还添加了干扰项,比如对图片进行适度地旋转,比如给图片添加随机的噪点,我们叫椒盐(胡椒是黑色的,盐是白色的,表示黑白噪点)。因为,当送给我们识别的文档,也会有不清晰的情况,所以我们就要按照有干扰来训练,这样出来的效果才是更贴近真实情况的。

2022-07-16_001029.png

其实,生成字符集的代码很简单,我和老板争论的那天下午,我就写好了。

让我一直纠结的是,这次要不要让小王来写。让小王写,我需要多耗费几倍的精力,因为跟他说的功夫,我都能写完了。想起,小王已经是公司来的第10位实习生了,前几位都稍微学有所长就走了。

最后,我还是选择了培养小王。尽人事,听天命。

现在看来,虽然只有3天,小王的进步已经很大了。

第四天:交差了

我把字符集交给了老板,说是小王开发的。老板很开心,说要给小王涨200块钱的工资。

我正在犹豫要不要告诉小王。小王找到了我,他有点不好意思。

其实,我也想到了。

小王说:老大,我找到了一份新工作,对方很认可我的能力,尤其对于我可以自动生成字符集,他们也很需要,工资给我翻了一番……所以……

“好啊,祝福你!打算什么时候走?”,我的内心没有一丝波澜。

小王急迫地说:今天下午……可以吗?

可以!

望着小王离去的工位,我从抽屉里拿出了一份策划案,那是培养小王完成整个OCR识别项目的剧本。

我随便翻开一页,这页的知识点是:为什么图片的训练数据集多采用黑色背景,白色字体呢?

我笑了笑:那是因为黑色色值是0,白色是255,计算机对于0是忽略的,更关注白色的255就好。 如果是白底黑字,那么需要计算机去关注0,这会让它很痛苦。

后面还有如何训练,如何调优,如何部署等等等等。

也罢,他有个好的去处,我也算是交差了。

交差了,都交差喽。

微信图片_20220716004134.jpg

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿

猜你喜欢

转载自juejin.im/post/7120640342298198024