做OCR数据合成需要三个条件,字体,语料,背景图。之前闹过一段字体荒,只有几百个字体,生成出来的效果比较单一,然后一口气收集了2W个字体,可把自己坑了。
字体文件中常见的问题
为啥说把自己坑了呢?因为字体真的不像是大家想象的那么简单,里面其实会遇到各种情况,包括但不限于:
- 字体文件里面没这个字
- 这是最最最常见的情况了,99.9%的字体文件都会出现这种情况。如果一个字体能兼容各种字符,那简直是宝藏字体,赶快珍藏
- 这个也是最最简单的情况,只需要读取一下「这个字体文件支持啥字符」就行了
- 字体文件里这个字是错的
- 这个就比较烦了,而且大多数情况下用代码是解决不了的
- 比如,有些字体在遇到「没这个字」的时候,会变成空白、方框、方框叉叉、两道杠
- 比如,将藏语塞进了原本属于拉丁字母的区域
- 比如,中文里面为了可爱,将「信」替换成了「✉️」的表情(此处忍住不说脏话……
- 需要将符号字体干掉
- 符号字体还是很多的,比如奥林匹克运动项目字体,复旦Logo字体,笑脸字体,海底生物字体,军舰字体,惊雷体(这个字体就叫做Thunder,就给翻译成 惊雷 了),「历史上的名人」字体(里面有美国历届总统、名人的头像)等,都应该被干掉
- 还有一些是Demo字体,不一定哪个字符被映射到了「这是个Demo,请购买完整字体」的Logo上,也需要被干掉
- 有些字体很花哨,不适合用来做数据生成
- 比如英文中的一些手写体,很飘逸,人眼辨识起来就像在看医生的处方笺
- 有一些字体形式远远大于内容,例如小人举牌体,就应该被干掉
- 有一些字体十分抽象,例如莫尔斯码体,例如数字用的是二进制的,也应该被干掉
2、3、4的一些例子可以见我筛字体的时候发的Twitter:
我是怎么筛选字体的
2W字体,挨个看,会出人命的(额,话虽这么说,我自己已经人工筛过不下10遍了,每次筛完都是眼冒金星)。所以我选择使用脚本+工具来进行筛选。
Python筛选脚本
首先写一个Python函数,获取每个字体文件都支持啥字符
import pickle
import sys
from argparse import ArgumentParser
from concurrent.futures import ProcessPoolExecutor, as_completed
from functools import partial
from hashlib import md5
from pathlib import Path
from PIL import ImageFont
from fontTools.ttLib import TTFont
from tqdm import tqdm
def get_font_support(font: Path):
supported_chars = set()
with TTFont(str(font), 0, allowVID=0, ignoreDecompileErrors=True, fontNumber=-1) as ttf:
for table in ttf["cmap"].tables:
supported_chars |= table.cmap.keys()
result = set()
for c in supported_chars:
bbox = ImageFont.truetype(str(font), 12).getmask(chr(c)).getbbox()
if bbox is not None or chr(c) in " \t":
result.add(c)
return result
def doit(src: Path, dst: Path):
tqdm.write(f"Processing {src}")
md5sum = md5(src.open("rb").read()).hexdigest()
pickle_file = dst / f"{md5sum}.pkl"
if not pickle_file.exists():
try:
font_support = get_font_support(src)
pickle.dump(font_support, pickle_file.open("wb"))
except Exception as e:
tqdm.write(f"Error while processing file {src}: {e}", file=sys.stderr)
else:
tqdm.write(f"File {src} is processed, pass", file=sys.stderr)
if __name__ == '__main__':
parser = ArgumentParser()
parser.add_argument("src", type=Path)
parser.add_argument("dst", type=Path)
parser.add_argument("-T", "--threads", type=int, default=8)
args = parser.parse_args()
if not args.dst.exists():
args.dst.mkdir(exist_ok=True, parents=True)
fn = partial(doit, dst=args.dst)
with ProcessPoolExecutor(max_workers=args.threads) as executor:
jobs = [executor.submit(fn, x) for x in args.src.rglob("*.*")]
for job in tqdm(as_completed(jobs), total=len(jobs)):
pass
这个脚本的功能是,获取到所有支持的字符,并把字符画出来,获取边界框。如果画出来的东西没有边界框,那么也认为这个字体不能画出这个字符。不过这里有几个小坑:
- 脚本运行会很慢,处理2W个字体需要大概10个小时。所以最好多线程来跑,并且做个缓存。
- 缓存的话,pickle啥的都行,看自己喜好加。我是将字体取md5,然后缓存了
resul
t - 多线程的话,如果遇到用
futures.ProcessPoolExecuto
r跑报错的话,用multiprocessing.set_start_method('fork'
)救一下就好了。futures.ThreadPoolExecutor
我没跑起来,不知道是什么错 - 脚本有时候会出错,记得做好Try Catch
然后确定你需要的字符范围,比如西里尔文字母,或者某个国家的全部字母。如果不幸,你的任务是中文和日文,那么只能随机取一些字了:
- 中文的话,我喜欢用「永和九年,岁在癸丑,暮春之初,会于会稽山阴之兰亭,修禊事也。群贤毕至,少长咸集。此地有崇山峻岭,茂林修竹,又有清流激湍,映带左右」和「归去来兮,田园将芜胡不归,既自以心为形役,奚惆怅而独悲,悟以往之不谏,知来者之可追」
- 日语的话,随便找了一句「めぐり逢ひて 見しやそれとも わかぬ間に 雲隠れにし 夜半の月かな」,最好一句话里面同时有平假名和片假名
最后设定规则,如果你选定的字符范围中有超过80%的字符都被某个字体所支持,那么恭喜,大胆地将这个字体收入囊中吧~
人工筛选
最上面说了,脚本不能搞定所有Case,所以还需要人工来筛一下。这里推荐 《RightFon 5》 这个app,收费的,不算太贵(哦反正让公司财务采购了十几个,我们组每人一个,不是我出钱),算是用过的字体管理类App里面最好用的了。
创建一个新的字体列表,将前面自动筛选过的字体全部导入这个列表,然后右上角输入你希望检视的文字,并开始快速预览,将有问题的字体(一般是灰色显示)从列表中删掉(或者将没问题的字体添加到另外一个列表中,因为删除目前有Bug)。
最后是将列表导出。导出之后的东西就能用啦~不过文件名可谓一头雾水,我目前不太能摸得准文件名的规律——完全不影响使用。
我是怎么管理字体的
依然是用RightFont 5。在里面建立了一个字体库,将左右字体一股脑倒腾进这个字体库里面。
然后,用上面的筛选方法筛选字体,依据语言支持情况分门别类建立字体列表:中文、日语、韩语、拉丁字母、西里尔文、阿拉伯语字母、泰语、藏语、符号字体……
然后里面再手动细分:常规体,手写体,全大写、全小写、简体/繁体、点阵……
嗯这样就差不多了。
吐槽
即使用上了RightFont,人工筛选也是蛮耗费时间的……比如筛基本拉丁字母的字体,花了我整整三天,眼都快看瞎了。
所以,还是需要写一个更强大的脚本,替我判断一些东西,比如大写和小写是否一致,等等。——难度有点大,再说吧
发表回复