diff --git a/.gitignore b/.gitignore index d1bde00..0d84a41 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ __pycache__/ *.pyc .DS_Store +.devcontainer/ data/fonts/ data/corpus/** diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4ccda56 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM python:3.6 + +COPY . /the/workdir/text_render +WORKDIR /the/workdir/text_render + +RUN apt-get update && apt-get install libgl1-mesa-glx -y +RUN pip3 install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt + +CMD ["python3", "main.py"] \ No newline at end of file diff --git a/LICENSE b/LICENSE index 89c893d..90576dd 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ MIT License -Copyright (c) 2018 Qing +Original work Copyright (c) 2018 Qing +Modified work Copyright 2019 Tang Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 5e7ad99..6abdade 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,28 @@ -# Text Renderer -Generate text images for training deep learning OCR model (e.g. [CRNN](https://github.com/bgshih/crnn)). +# 项目简介 +基于原[text_render](https://github.com/Sanster/text_renderer) 根据自己的需求做了一些修改; +主要增加了对于色彩的支持(包含文字色彩与背景色彩图); Support both latin and non-latin text. -# Setup - Ubuntu 16.04 - python 3.5+ Install dependencies: ``` pip3 install -r requirements.txt + +Python3 main.py ``` # Demo -By default, simply run `python3 main.py` will generate 20 text images -and a labels.txt file in `output/default/`. - -![example1.jpg](./imgs/example1.jpg) -![example2.jpg](./imgs/example2.jpg) +![example1.jpg](./imgs/example1_c.jpg) +![example2.jpg](./imgs/example2_c.jpg) -![example3.jpg](./imgs/example3.jpg) -![example4.jpg](./imgs/example4.jpg) +![example3.jpg](./imgs/example3_c.jpg) +![example4.jpg](./imgs/example4_c.jpg) -# Use your own data to generate image -1. Please run `python3 main.py --help` to see all optional arguments and their meanings. -And put your own data in corresponding folder. - -2. Config text effects and fraction in `configs/default.yaml` file(or create a -new config file and use it by `--config_file` option), here are some examples: +# 文字变换特效 +编辑 `configs/default.yaml` 来实现文字弯曲,模糊等效果,详见原[text_render](https://github.com/Sanster/text_renderer); +新增表格线颜色控制,字体颜色控制,添加冗余文字 |Effect name|Image| |------------|----| @@ -40,12 +36,70 @@ new config file and use it by `--config_file` option), here are some examples: |Random char space small|![random char space small](./imgs/effects/random_space_small.jpg)| |Middle line|![middle line](./imgs/effects/line_middle.jpg)| |Table line|![table line](./imgs/effects/line_table.jpg)| +|Table line(offset)|![table line offset](./imgs/effects/line_table_offset.jpg)| |Under line|![under line](./imgs/effects/line_under.jpg)| |Emboss|![emboss](./imgs/effects/emboss.jpg)| |Reverse color|![reverse color](./imgs/effects/reverse.jpg)| |Blur|![blur](./imgs/effects/blur.jpg)| +|font_color|![font_color](./imgs/effects/colored.jpg)| +|line_color|![line_color](./imgs/effects/table.jpg)| +|extra_words|![extra_words](./imgs/effects/extra.jpg)| +|texture|![texture](./imgs/effects/texture.jpg)| +|second_font|![second](./imgs/effects/second_font.jpg) + +颜色配置说明: +程序会随机拾取下方所配置颜色种类(按照fraction中的概率,所有颜色fraction相加需要等于1),选中颜色种类后,rgb由l_boundary与h_boundary中取一个随机数确定; font_color 同理 +``` +line_color: + black: + fraction: 0.3 + l_boundary: 0,0,0 + h_boundary: 64,64,64 + gray: + fraction: 0.7 + l_boundary: 77,77,77 + h_boundary: 230,230,230 +``` +冗余文字说明: +如开启选项,程序会按照设定的总概率随机选取一段独立的文字(颜色,字体及字号大小均采用正文的设置),生成的文字长度在1到最长文本长度之间随机。 top 与 bottom 中的 fraction 决定了冗余文字生成在上方或下方的概率; +*注:由于冗余文字尽量生成在上方与下方,所以可能生成在图片之外,所以最终出现的概率会小于设定值; +``` +extra_words: + enable: true + fraction: 0.05 + top: + fraction: 0.5 + bottom: + fraction: 0.5 +``` +文字纹理说明: +开启选项后,会在文本上生成perling噪音的侵蚀效果(速度较慢)。可以调节octaves,persistence,scale参数调整纹理的效果(改变似乎不是非常明显) +``` +texture: + enable: true + fraction: 0.05 + + cloud: + enable: true + fraction: 1 + octaves: 2 + persistence: 0.4 + scale : 2000 +``` + +第二字体说明: +开启选项后,会在文本会在文本行上出现第二种字体,可以设置字体、颜色、大小是否与主字体相同。change_rate决定了在文本行中第二字体出现的概率 +``` +second_font: + enable: true + fraction: 0.5 + change_rate: 0.3 + font_change: true + font_size_change: true + font_color_change: true + +``` -3. Run `main.py` file. # Strict mode For no-latin language(e.g Chinese), it's very common that some fonts only support @@ -91,4 +145,5 @@ You can see how perspectiveTransform works and all bounding/rotated boxes. # Todo -See https://github.com/Sanster/text_renderer/projects/1 +- 增加同一张图中不同字体、大小与颜色的设置; +- 增加文字侵蚀,打印不完整的效果; diff --git a/configs/default.yaml b/configs/default.yaml index 8b4f4bf..0a1e65d 100644 --- a/configs/default.yaml +++ b/configs/default.yaml @@ -1,7 +1,50 @@ # Small font_size will make text looks like blured/prydown font_size: - min: 14 - max: 23 + min: 30 + max: 42 + +# choose font_color range +# color boundary is in R,G,B format +font_color: + black: + fraction: 0.3 + l_boundary: 0,0,0 + h_boundary: 64,64,64 + gray: + fraction: 0.4 + l_boundary: 77,77,77 + h_boundary: 230,230,230 + blue: + fraction: 0.2 + l_boundary: 0,0,150 + h_boundary: 60,60,255 + brown: + fraction: 0.1 + l_boundary: 139,70,19 + h_boundary: 160,82,43 + +# second font in one line +# choose what's different from first font + +second_font: + enable: false + fraction: 0.5 + change_rate: 0.3 + font_change: false + font_size_change: true + font_color_change: true + +# choose line_color range +# color boundary is in R,G,B format +line_color: + black: + fraction: 0.3 + l_boundary: 0,0,0 + h_boundary: 64,64,64 + gray: + fraction: 0.7 + l_boundary: 77,77,77 + h_boundary: 230,230,230 # By default, text is drawed by Pillow with (https://stackoverflow.com/questions/43828955/measuring-width-of-text-python-pil) # If `random_space` is enabled, some text will be drawed char by char with a random space @@ -11,10 +54,19 @@ random_space: min: -0.1 # -0.1 will make chars very close or even overlapped max: 0.1 +# draw extra random words on top or bottom of the img +extra_words: + enable: true + fraction: 0.05 + top: + fraction: 0.5 + bottom: + fraction: 0.5 + # Do remap with sin() # Currently this process is very slow! curve: - enable: false + enable: true fraction: 0.3 period: 360 # degree, sin 函数的周期 min: 1 # sin 函数的幅值范围 @@ -22,16 +74,16 @@ curve: # random crop text height crop: - enable: false + enable: true fraction: 0.5 # top and bottom will applied equally top: min: 5 - max: 10 # in pixel, this value should small than img_height + max: 15 # in pixel, this value should small than img_height bottom: min: 5 - max: 10 # in pixel, this value should small than img_height + max: 15 # in pixel, this value should small than img_height # Use image in bg_dir as background for text img_bg: @@ -68,17 +120,39 @@ perspective_transform: blur: enable: true - fraction: 0.03 + fraction: 0.1 -# If an image is applied blur, it will not be applied prydown -prydown: + gauss: + enable: true + fraction: 0.25 + + norm: + enable: true + fraction: 0.25 + + motion: + enable: true + fraction: 0.25 + + prydown: + enable: true + fraction: 0.25 + max_scale: 1.5 + +texture: enable: true - fraction: 0.03 - max_scale: 1.5 # Image will first resize to 1.5x, and than resize to 1x + fraction: 0.05 + + cloud: + enable: true + fraction: 1 + octaves: 2 + persistence: 0.4 + scale : 2000 noise: enable: true - fraction: 0.3 + fraction: 0.03 gauss: enable: true @@ -97,20 +171,21 @@ noise: fraction: 0.25 line: - enable: false - fraction: 0.05 + enable: true + fraction: 0.1 under_line: - enable: false - fraction: 0.2 + enable: true + fraction: 0.1 table_line: - enable: false - fraction: 0.3 + enable: true + fraction: 0.8 + random_offset: true # line will cross chars middle_line: - enable: false - fraction: 0.5 + enable: true + fraction: 0.1 # These operates are applied on the final output image, # so actually it can also be applied in training process as an data augmentation method. diff --git a/data/corpus/The Hitchhiker's Guide to the Galaxy.txt b/data/corpus/The Hitchhiker's Guide to the Galaxy.txt index 2749409..ef2eb47 100644 --- a/data/corpus/The Hitchhiker's Guide to the Galaxy.txt +++ b/data/corpus/The Hitchhiker's Guide to the Galaxy.txt @@ -1,11 +1,4 @@   在银河系西螺旋臂的末端那片未曾标明的寂静虚空中,悬挂着—颗不被人注意的小小的黄色太阳。距离它大约9800万英里的轨道上,运行着一颗完全无足轻重的蓝绿色小行星。这颗行星上由猿演化而来的生命形式原始得令人吃惊,他们居然还认为电子表是一项非常了不起的设想。   这颗行星上存在着——或者说曾经存在——一个问题,那就是:大部分的居民在大部分的时间里都感到不开心。针对这一问题曾提出过各种各样的解决方案,但其中大部分都是紧紧围绕那些绿色小纸片的运动来着手的——这很奇怪,因为实际上并不是这些绿色小纸片不开心。   于是,问题依旧困扰着这颗星球。所有的人都感到不自在,其中大部分简直可以说是凄凄惨惨,甚至包括那些拥有电子表的人。 -  不过这与我们这里要讲的故事无关。 -  我们这个故事是关于一场可怕而又愚蠢的灾难,以及它的一系列后果。 -  同时这个故事也是关于一本书的,书名叫做《银河系漫游指南》,这不是一本地球上的书,它从来没有在地球上出版过,并且一直到那场可怕的灾难发牛之前,没有任何地球人见过或者听说过它。 -  然而,这绝对是一本非凡的书。 -  事实上,它可能是小熊星座那些伟大的出版公司所出版过的最非凡的书了——当然,这些公司也没有任何—个地球人听说过。但是,在银河系东部外缘的许多更加开放的文明里,《银河系漫游指南》已经取代伟大的《银河系百科全书》成为所有知识和智慧的标准。这是因为,尽管显得冗长,并且包含许多虚假或者至少是不够准确的信息,但它却在两个极其重要的方面超越了那部更加陈旧和呆板的著作。 -  第一,它稍微便宜一点儿;第二,在它的封面上以大而友善的字体写着“不要恐慌”这句话。 -  不过,这个关于那个可怕而又愚蠢的星期四的故事,这个关于那次灾难的奇特后果的故事,这个关于这些后果如何无可逃避地与这本非凡的书交织在一起的故事,它的开始却非常简单。 -  它的开始与一所房子有关。 +  0123456789¥20.642018-06-01130km公里2020年10月01日 diff --git a/data/corpus_line/corpus_line.txt b/data/corpus_line/corpus_line.txt new file mode 100644 index 0000000..a677888 --- /dev/null +++ b/data/corpus_line/corpus_line.txt @@ -0,0 +1,3 @@ +2018-01-12 +2018年12月21日 +12.834 \ No newline at end of file diff --git a/data/fonts/chn/img_font/-.png b/data/fonts/chn/img_font/-.png new file mode 100644 index 0000000..836519a Binary files /dev/null and b/data/fonts/chn/img_font/-.png differ diff --git a/data/fonts/chn/img_font/..png b/data/fonts/chn/img_font/..png new file mode 100644 index 0000000..7e55107 Binary files /dev/null and b/data/fonts/chn/img_font/..png differ diff --git a/data/fonts/chn/img_font/0.png b/data/fonts/chn/img_font/0.png new file mode 100644 index 0000000..b25db7d Binary files /dev/null and b/data/fonts/chn/img_font/0.png differ diff --git a/data/fonts/chn/img_font/1.png b/data/fonts/chn/img_font/1.png new file mode 100644 index 0000000..12e009f Binary files /dev/null and b/data/fonts/chn/img_font/1.png differ diff --git a/data/fonts/chn/img_font/2.png b/data/fonts/chn/img_font/2.png new file mode 100644 index 0000000..7f168cd Binary files /dev/null and b/data/fonts/chn/img_font/2.png differ diff --git a/data/fonts/chn/img_font/3.png b/data/fonts/chn/img_font/3.png new file mode 100644 index 0000000..d4ab579 Binary files /dev/null and b/data/fonts/chn/img_font/3.png differ diff --git a/data/fonts/chn/img_font/4.png b/data/fonts/chn/img_font/4.png new file mode 100644 index 0000000..135e20b Binary files /dev/null and b/data/fonts/chn/img_font/4.png differ diff --git a/data/fonts/chn/img_font/5.png b/data/fonts/chn/img_font/5.png new file mode 100644 index 0000000..54d0a19 Binary files /dev/null and b/data/fonts/chn/img_font/5.png differ diff --git a/data/fonts/chn/img_font/6.png b/data/fonts/chn/img_font/6.png new file mode 100644 index 0000000..d8f60b4 Binary files /dev/null and b/data/fonts/chn/img_font/6.png differ diff --git a/data/fonts/chn/img_font/7.png b/data/fonts/chn/img_font/7.png new file mode 100644 index 0000000..bcd6bd2 Binary files /dev/null and b/data/fonts/chn/img_font/7.png differ diff --git a/data/fonts/chn/img_font/8.png b/data/fonts/chn/img_font/8.png new file mode 100644 index 0000000..70e7568 Binary files /dev/null and b/data/fonts/chn/img_font/8.png differ diff --git a/data/fonts/chn/img_font/9.png b/data/fonts/chn/img_font/9.png new file mode 100644 index 0000000..4b1ec90 Binary files /dev/null and b/data/fonts/chn/img_font/9.png differ diff --git a/data/fonts/chn/img_font/k.png b/data/fonts/chn/img_font/k.png new file mode 100644 index 0000000..64eb47a Binary files /dev/null and b/data/fonts/chn/img_font/k.png differ diff --git a/data/fonts/chn/img_font/m.png b/data/fonts/chn/img_font/m.png new file mode 100644 index 0000000..5958357 Binary files /dev/null and b/data/fonts/chn/img_font/m.png differ diff --git "a/data/fonts/chn/img_font/\302\245.png" "b/data/fonts/chn/img_font/\302\245.png" new file mode 100644 index 0000000..1d98ac2 Binary files /dev/null and "b/data/fonts/chn/img_font/\302\245.png" differ diff --git "a/data/fonts/chn/img_font/\345\205\203.png" "b/data/fonts/chn/img_font/\345\205\203.png" new file mode 100644 index 0000000..c0702cf Binary files /dev/null and "b/data/fonts/chn/img_font/\345\205\203.png" differ diff --git "a/data/fonts/chn/img_font/\345\205\254.png" "b/data/fonts/chn/img_font/\345\205\254.png" new file mode 100644 index 0000000..1208dae Binary files /dev/null and "b/data/fonts/chn/img_font/\345\205\254.png" differ diff --git "a/data/fonts/chn/img_font/\345\271\264.png" "b/data/fonts/chn/img_font/\345\271\264.png" new file mode 100644 index 0000000..df3201d Binary files /dev/null and "b/data/fonts/chn/img_font/\345\271\264.png" differ diff --git "a/data/fonts/chn/img_font/\346\227\245.png" "b/data/fonts/chn/img_font/\346\227\245.png" new file mode 100644 index 0000000..58a0f14 Binary files /dev/null and "b/data/fonts/chn/img_font/\346\227\245.png" differ diff --git "a/data/fonts/chn/img_font/\346\234\210.png" "b/data/fonts/chn/img_font/\346\234\210.png" new file mode 100644 index 0000000..4d56457 Binary files /dev/null and "b/data/fonts/chn/img_font/\346\234\210.png" differ diff --git "a/data/fonts/chn/img_font/\351\207\214.png" "b/data/fonts/chn/img_font/\351\207\214.png" new file mode 100644 index 0000000..0455e33 Binary files /dev/null and "b/data/fonts/chn/img_font/\351\207\214.png" differ diff --git a/data/fonts_list/img_font.txt b/data/fonts_list/img_font.txt new file mode 100644 index 0000000..6587d4c --- /dev/null +++ b/data/fonts_list/img_font.txt @@ -0,0 +1 @@ +./data/fonts/chn/img_font \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..7193e3a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,8 @@ +version: "3" + +services: + text_render: + build: . + volumes: + - ./data/:/the/workdir/text_render/data/ + - ./output/:/the/workdir/text_render/output/ diff --git a/help_runner.py b/help_runner.py index 7d29bb5..78637c1 100644 --- a/help_runner.py +++ b/help_runner.py @@ -6,13 +6,16 @@ # See parse_args for supported arguments configs = [ dict(tag='test1', - num_img=10, - img_width=150), - dict(tag='test2', - num_img=20, - debug=False, + num_img=50, + img_width=150, + fonts_by_image=True, + corpus_dir='./data/corpus_line', corpus_mode='list', - corpus_dir='./data/list_corpus') + fonts_list='./data/fonts_list/img_font.txt', + strict=True), + dict(tag='test2', + num_img=50, + strict=True) ] diff --git a/imgs/effects/colored.jpg b/imgs/effects/colored.jpg new file mode 100644 index 0000000..f42367f Binary files /dev/null and b/imgs/effects/colored.jpg differ diff --git a/imgs/effects/extra.jpg b/imgs/effects/extra.jpg new file mode 100644 index 0000000..2fe8ea8 Binary files /dev/null and b/imgs/effects/extra.jpg differ diff --git a/imgs/effects/line_table_offset.jpg b/imgs/effects/line_table_offset.jpg new file mode 100644 index 0000000..c4cd69a Binary files /dev/null and b/imgs/effects/line_table_offset.jpg differ diff --git a/imgs/effects/second_font.jpg b/imgs/effects/second_font.jpg new file mode 100644 index 0000000..057e6f7 Binary files /dev/null and b/imgs/effects/second_font.jpg differ diff --git a/imgs/effects/table.jpg b/imgs/effects/table.jpg new file mode 100644 index 0000000..ecc79de Binary files /dev/null and b/imgs/effects/table.jpg differ diff --git a/imgs/effects/texture.jpg b/imgs/effects/texture.jpg new file mode 100644 index 0000000..12e9ab3 Binary files /dev/null and b/imgs/effects/texture.jpg differ diff --git a/imgs/example1.jpg b/imgs/example1.jpg deleted file mode 100644 index eadd432..0000000 Binary files a/imgs/example1.jpg and /dev/null differ diff --git a/imgs/example1_c.jpg b/imgs/example1_c.jpg new file mode 100644 index 0000000..cfda43d Binary files /dev/null and b/imgs/example1_c.jpg differ diff --git a/imgs/example2.jpg b/imgs/example2.jpg deleted file mode 100644 index 6b0d3aa..0000000 Binary files a/imgs/example2.jpg and /dev/null differ diff --git a/imgs/example2_c.jpg b/imgs/example2_c.jpg new file mode 100644 index 0000000..cb2af52 Binary files /dev/null and b/imgs/example2_c.jpg differ diff --git a/imgs/example3.jpg b/imgs/example3.jpg deleted file mode 100644 index 1196740..0000000 Binary files a/imgs/example3.jpg and /dev/null differ diff --git a/imgs/example3_c.jpg b/imgs/example3_c.jpg new file mode 100644 index 0000000..f0fcfc0 Binary files /dev/null and b/imgs/example3_c.jpg differ diff --git a/imgs/example4.jpg b/imgs/example4.jpg deleted file mode 100644 index 0f5420f..0000000 Binary files a/imgs/example4.jpg and /dev/null differ diff --git a/imgs/example4_c.jpg b/imgs/example4_c.jpg new file mode 100644 index 0000000..209689e Binary files /dev/null and b/imgs/example4_c.jpg differ diff --git a/libs/font_utils.py b/libs/font_utils.py index 40c53b8..64d3d03 100644 --- a/libs/font_utils.py +++ b/libs/font_utils.py @@ -27,7 +27,7 @@ def get_font_paths(fonts_dir): def get_font_paths_from_list(list_filename): - with open(list_filename,encoding="utf-8") as f: + with open(list_filename, encoding="utf-8") as f: lines = f.readlines() fonts = [os.path.abspath(l.strip()) for l in lines] @@ -66,11 +66,11 @@ def check_font_chars(ttf, charset): :param charset: chars :return: unsupported_chars, supported_chars """ - #chars = chain.from_iterable([y + (Unicode[y[0]],) for y in x.cmap.items()] for x in ttf["cmap"].tables) - chars_int=set() + # chars = chain.from_iterable([y + (Unicode[y[0]],) for y in x.cmap.items()] for x in ttf["cmap"].tables) + chars_int = set() for table in ttf['cmap'].tables: - for k,v in table.cmap.items(): - chars_int.add(k) + for k, v in table.cmap.items(): + chars_int.add(k) unsupported_chars = [] supported_chars = [] @@ -125,7 +125,7 @@ def get_fonts_chars(fonts, chars_file): return out -def get_unsupported_chars(fonts, chars_file): +def get_unsupported_chars(fonts, chars_file, fonts_by_image=False): """ Get fonts unsupported chars by loads/saves font supported chars from cache file :param fonts: @@ -136,11 +136,17 @@ def get_unsupported_chars(fonts, chars_file): """ charset = load_chars(chars_file) charset = ''.join(charset) - fonts_chars = get_fonts_chars(fonts, chars_file) fonts_unsupported_chars = {} - for font_path, chars in fonts_chars.items(): - unsupported_chars = list(filter(lambda x: x not in chars, charset)) - fonts_unsupported_chars[font_path] = unsupported_chars + if fonts_by_image: + for font_path in fonts: + chars = [x.replace(".png", "") for x in os.listdir(font_path)] + unsupported_chars = list(filter(lambda x: x not in chars, charset)) + fonts_unsupported_chars[font_path] = unsupported_chars + else: + fonts_chars = get_fonts_chars(fonts, chars_file) + for font_path, chars in fonts_chars.items(): + unsupported_chars = list(filter(lambda x: x not in chars, charset)) + fonts_unsupported_chars[font_path] = unsupported_chars return fonts_unsupported_chars diff --git a/libs/utils.py b/libs/utils.py index cb2c7b2..e09c0e6 100644 --- a/libs/utils.py +++ b/libs/utils.py @@ -84,7 +84,7 @@ def load_bgs(bg_dir): image_path = os.path.join(root, file_name) # For load non-ascii image_path on Windows - bg = cv2.imdecode(np.fromfile(image_path, dtype=np.uint8), cv2.IMREAD_GRAYSCALE) + bg = cv2.imdecode(np.fromfile(image_path, dtype=np.uint8), cv2.IMREAD_COLOR) dst.append(bg) diff --git a/main.py b/main.py index fc01353..4e187ce 100644 --- a/main.py +++ b/main.py @@ -39,7 +39,8 @@ clip_max_chars=flags.clip_max_chars, debug=flags.debug, gpu=flags.gpu, - strict=flags.strict) + strict=flags.strict, + fonts_by_image=flags.fonts_by_image) def start_listen(q, fname): @@ -61,7 +62,7 @@ def start_listen(q, fname): f.close() -@retry +# @retry def gen_img_retry(renderer, img_index): try: return renderer.gen_img(img_index) diff --git a/parse_args.py b/parse_args.py index e81c687..993388e 100644 --- a/parse_args.py +++ b/parse_args.py @@ -25,6 +25,9 @@ def parse_args(): parser.add_argument('--config_file', type=str, default='./configs/default.yaml', help='Set the parameters when rendering images') + parser.add_argument('--fonts_by_image', type=bool, default=False, + help='Fonts use image') + parser.add_argument('--fonts_list', type=str, default='./data/fonts_list/chn.txt', help='Fonts file path to use') diff --git a/requirements.txt b/requirements.txt index 7dcdede..5bb6c22 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,10 @@ -Cython -opencv-python -pillow -numpy -matplotlib -fontTools -tenacity -easyDict -pyyaml \ No newline at end of file +Cython == 0.29.21 +opencv-python == 4.4.0.44 +pillow == 7.0.0 +numpy == 1.15.0 +matplotlib == 3.3.2 +fontTools == 4.16.1 +tenacity == 6.1.0 +easyDict == 1.9 +pyyaml == 5.3.1 +noise == 1.2.2 \ No newline at end of file diff --git a/textrenderer/liner.py b/textrenderer/liner.py index 6601364..b8ecafe 100644 --- a/textrenderer/liner.py +++ b/textrenderer/liner.py @@ -21,7 +21,23 @@ def __init__(self, cfg): self.linestate = LineState() self.cfg = cfg - def apply(self, word_img, text_box_pnts, word_color): + def get_line_color(self): + colors = [i for i in self.cfg.line_color] + p = [self.cfg.line_color[i].fraction for i in self.cfg.line_color] + # pick color by fraction + color_name = np.random.choice(colors, p=p) + l_boundary = [int(i) for i in self.cfg.line_color[color_name].l_boundary.split(',')] + h_boundary = [int(i) for i in self.cfg.line_color[color_name].h_boundary.split(',')] + # random color by low and high RGB boundary + if color_name == 'black' or color_name == 'gray': + r = g = b = np.random.randint(l_boundary[0], h_boundary[0]) + else: + r = np.random.randint(l_boundary[0], h_boundary[0]) + g = np.random.randint(l_boundary[1], h_boundary[1]) + b = np.random.randint(l_boundary[2], h_boundary[2]) + return (b, g, r) + + def apply(self, word_img, text_box_pnts): """ :param word_img: word image with big background :param text_box_pnts: left-top, right-top, right-bottom, left-bottom of text word @@ -46,17 +62,15 @@ def apply(self, word_img, text_box_pnts, word_color): return word_img, text_box_pnts line_effect_func = np.random.choice(funcs, p=line_p) + line_color = self.get_line_color() + return line_effect_func(word_img, text_box_pnts, line_color) - return line_effect_func(word_img, text_box_pnts, word_color) - - def apply_under_line(self, word_img, text_box_pnts, word_color): + def apply_under_line(self, word_img, text_box_pnts, line_color): y_offset = random.choice([0, 1]) text_box_pnts[2][1] += y_offset text_box_pnts[3][1] += y_offset - line_color = word_color + random.randint(0, 10) - dst = cv2.line(word_img, (text_box_pnts[2][0], text_box_pnts[2][1]), (text_box_pnts[3][0], text_box_pnts[3][1]), @@ -66,21 +80,25 @@ def apply_under_line(self, word_img, text_box_pnts, word_color): return dst, text_box_pnts - def apply_table_line(self, word_img, text_box_pnts, word_color): + def apply_table_line(self, word_img, text_box_pnts, line_color): """ 共有 8 种可能的画法,横线横穿整张 word_img 0/1/2/3: 仅单边(左上右下) 4/5/6/7: 两边都有线(左上,右上,右下,左下) """ + if self.cfg.line.table_line.random_offset: + x_rand = random.randint(-1, 1) + y_rand = random.randint(-1, 1) + else: + x_rand = 1 + y_rand = 1 dst = word_img option = random.choice(self.linestate.tableline_options) thickness = random.choice(self.linestate.tableline_thickness) - line_color = word_color + random.randint(0, 10) - - top_y_offset = random.choice(self.linestate.tableline_y_offsets) - bottom_y_offset = random.choice(self.linestate.tableline_y_offsets) - left_x_offset = random.choice(self.linestate.tableline_x_offsets) - right_x_offset = random.choice(self.linestate.tableline_x_offsets) + top_y_offset = random.choice(self.linestate.tableline_y_offsets) * y_rand + bottom_y_offset = random.choice(self.linestate.tableline_y_offsets) * y_rand + left_x_offset = random.choice(self.linestate.tableline_x_offsets) * x_rand + right_x_offset = random.choice(self.linestate.tableline_x_offsets) * x_rand def is_top(): return option in [1, 4, 5] @@ -144,16 +162,15 @@ def is_right(): return dst, text_box_pnts - def apply_middle_line(self, word_img, text_box_pnts, word_color): + def apply_middle_line(self, word_img, text_box_pnts, line_color): y_center = int((text_box_pnts[0][1] + text_box_pnts[3][1]) / 2) - img_mean = int(np.mean(word_img)) thickness = np.random.choice(self.linestate.middleline_thickness, p=self.linestate.middleline_thickness_p) dst = cv2.line(word_img, (text_box_pnts[0][0], y_center), (text_box_pnts[1][0], y_center), - color=img_mean, + color=line_color, thickness=thickness, lineType=cv2.LINE_AA) diff --git a/textrenderer/noiser.py b/textrenderer/noiser.py index b74309e..1600f4a 100644 --- a/textrenderer/noiser.py +++ b/textrenderer/noiser.py @@ -1,4 +1,6 @@ import numpy as np +from PIL import Image +import noise import cv2 @@ -41,11 +43,11 @@ def apply_gauss_noise(self, img): """ Gaussian-distributed additive noise. """ - row, col = img.shape + row, col, channel = img.shape mean = 0 stddev = np.sqrt(15) - gauss_noise = np.zeros((row, col)) + gauss_noise = np.zeros((row, col, channel)) cv2.randn(gauss_noise, mean, stddev) out = img + gauss_noise @@ -55,10 +57,10 @@ def apply_uniform_noise(self, img): """ Apply zero-mean uniform noise """ - row, col = img.shape + row, col, channel = img.shape alpha = 0.05 - gauss = np.random.uniform(0 - alpha, alpha, (row, col)) - gauss = gauss.reshape(row, col) + gauss = np.random.uniform(0 - alpha, alpha, (row, col, channel)) + gauss = gauss.reshape(row, col, channel) out = img + img * gauss return out @@ -66,7 +68,7 @@ def apply_sp_noise(self, img): """ Salt and pepper noise. Replaces random pixels with 0 or 255. """ - row, col = img.shape + row, col, channel = img.shape s_vs_p = 0.5 amount = np.random.uniform(0.004, 0.01) out = np.copy(img) @@ -95,3 +97,54 @@ def apply_poisson_noise(self, img): noisy = np.random.poisson(img * vals) / float(vals) return noisy + + +class Texture(object): + + def __init__(self, cfg): + self.cfg = cfg + + def apply(self, img): + """ + :param img: text only img + """ + + p = [] + funcs = [] + if self.cfg.texture.cloud.enable: + p.append(self.cfg.texture.cloud.fraction) + funcs.append(self.apply_cloud_texture) + + if len(p) == 0: + return img + + def apply_cloud_texture(self, pure_bg, text): + height = pure_bg.size[1] + width = pure_bg.size[0] + noise = self.generate_fractal_noise_2d((height, width), + octaves = int(self.cfg.texture.cloud.octaves), + persistence = float(self.cfg.texture.cloud.persistence), + scale = float(self.cfg.texture.cloud.scale)) + noise = (((noise + 1) / 2) * 255) + text = np.array(text) + text[:, :, 3] = noise + img = Image.alpha_composite(pure_bg, Image.fromarray(text)) + return img.convert('RGB') + + def generate_fractal_noise_2d(self, shape, octaves=2, persistence=0.5, lacunarity = 6.0, scale = 100.0): + np_noise = np.zeros(shape) + for i in range(shape[0]): + for j in range(shape[1]): + pixel = noise.pnoise2(i/scale, + j/scale, + octaves=octaves, + persistence=persistence, + lacunarity=lacunarity, + base=np.random.randint(1,100)) + if pixel > 0.2: + np_noise[i][j] = 0.5 + elif pixel < -0.1: + np_noise[i][j] = -1 + else: + np_noise[i][j] = 0.9 + return np_noise diff --git a/textrenderer/renderer.py b/textrenderer/renderer.py index 578f08b..e81cbaf 100644 --- a/textrenderer/renderer.py +++ b/textrenderer/renderer.py @@ -4,12 +4,14 @@ import cv2 from PIL import ImageFont, Image, ImageDraw from tenacity import retry +import os +import time import libs.math_utils as math_utils from libs.utils import draw_box, draw_bbox, prob, apply from libs.timer import Timer from textrenderer.liner import Liner -from textrenderer.noiser import Noiser +from textrenderer.noiser import Noiser, Texture import libs.font_utils as font_utils # noinspection PyMethodMayBeStatic @@ -18,7 +20,7 @@ class Renderer(object): def __init__(self, corpus, fonts, bgs, cfg, width=256, height=32, - clip_max_chars=False, debug=False, gpu=False, strict=False): + clip_max_chars=False, debug=False, gpu=False, strict=False, fonts_by_image=False): self.corpus = corpus self.fonts = fonts self.bgs = bgs @@ -30,44 +32,56 @@ def __init__(self, corpus, fonts, bgs, cfg, width=256, height=32, self.gpu = gpu self.strict = strict self.cfg = cfg + self.fonts_by_image = fonts_by_image self.timer = Timer() self.liner = Liner(cfg) self.noiser = Noiser(cfg) + self.texture = Texture(cfg) self.remaper = Remaper(cfg) + self.start = time.time() self.create_kernals() if self.strict: - self.font_unsupport_chars = font_utils.get_unsupported_chars(self.fonts, corpus.chars_file) + self.font_unsupport_chars = font_utils.get_unsupported_chars( + self.fonts, corpus.chars_file, self.fonts_by_image) def gen_img(self, img_index): - word, font, word_size = self.pick_font(img_index) + word, font, word_size, font2 = self.pick_font(img_index) self.dmsg("after pick font") # Background's height should much larger than raw word image's height, # to make sure we can crop full word image after apply perspective bg = self.gen_bg(width=word_size[0] * 8, height=word_size[1] * 8) - word_img, text_box_pnts, word_color = self.draw_text_on_bg(word, font, bg) + word_img, text_box_pnts, word_color = self.draw_text_on_bg( + word, font, bg, font2) self.dmsg("After draw_text_on_bg") if apply(self.cfg.crop): text_box_pnts = self.apply_crop(text_box_pnts, self.cfg.crop) if apply(self.cfg.line): - word_img, text_box_pnts = self.liner.apply(word_img, text_box_pnts, word_color) + word_img, text_box_pnts = self.liner.apply(word_img, text_box_pnts) self.dmsg("After draw line") if self.debug: word_img = draw_box(word_img, text_box_pnts, (0, 255, 155)) if apply(self.cfg.curve): - word_img, text_box_pnts = self.remaper.apply(word_img, text_box_pnts, word_color) + word_img, text_box_pnts = self.remaper.apply( + word_img, text_box_pnts, word_color) self.dmsg("After remapping") if self.debug: word_img = draw_box(word_img, text_box_pnts, (155, 255, 0)) + word_img = self.mix_seamless_bg(word_img, bg) + if apply(self.cfg.extra_words): + word_img = self.draw_extra_random_word( + word_img, text_box_pnts, img_index) + self.dmsg("After add extra words") + word_img, img_pnts_transformed, text_box_pnts_transformed = \ self.apply_perspective_transform(word_img, text_box_pnts, max_x=self.cfg.perspective_transform.max_x, @@ -81,7 +95,8 @@ def gen_img(self, img_index): _, crop_bbox = self.crop_img(word_img, text_box_pnts_transformed) word_img = draw_bbox(word_img, crop_bbox, (255, 0, 0)) else: - word_img, crop_bbox = self.crop_img(word_img, text_box_pnts_transformed) + word_img, crop_bbox = self.crop_img( + word_img, text_box_pnts_transformed) self.dmsg("After crop_img") @@ -90,17 +105,10 @@ def gen_img(self, img_index): word_img = self.noiser.apply(word_img) self.dmsg("After noiser") - blured = False if apply(self.cfg.blur): - blured = True word_img = self.apply_blur_on_output(word_img) self.dmsg("After blur") - if not blured: - if apply(self.cfg.prydown): - word_img = self.apply_prydown(word_img) - self.dmsg("After prydown") - word_img = np.clip(word_img, 0., 255.) if apply(self.cfg.reverse_color): @@ -185,7 +193,8 @@ def crop_img(self, img, text_box_pnts_transformed): np.around(bbox[2] / scale), np.around(bbox[3] / scale)) - x_offset, y_offset = self.random_xy_offset(s_bbox_height, s_bbox_width, self.out_height, dst_width) + x_offset, y_offset = self.random_xy_offset( + s_bbox_height, s_bbox_width, self.out_height, dst_width) dst_bbox = ( self.int_around((s_bbox[0] - x_offset) * scale), @@ -195,32 +204,36 @@ def crop_img(self, img, text_box_pnts_transformed): ) # It's important do crop first and than do resize for speed consider - dst = img[dst_bbox[1]:dst_bbox[1] + dst_bbox[3], dst_bbox[0]:dst_bbox[0] + dst_bbox[2]] + dst = img[dst_bbox[1]:dst_bbox[1] + dst_bbox[3], + dst_bbox[0]:dst_bbox[0] + dst_bbox[2]] - dst = cv2.resize(dst, (dst_width, self.out_height), interpolation=cv2.INTER_CUBIC) + dst = cv2.resize(dst, (dst_width, self.out_height), + interpolation=cv2.INTER_CUBIC) return dst, dst_bbox def int_around(self, val): return int(np.around(val)) - def get_word_color(self, bg, text_x, text_y, word_height, word_width): - """ - Only use word roi area to get word color - """ - offset = 10 - ymin = text_y - offset - ymax = text_y + word_height + offset - xmin = text_x - offset - xmax = text_x + word_width + offset - - word_roi_bg = bg[ymin: ymax, xmin: xmax] - - bg_mean = int(np.mean(word_roi_bg) * (2 / 3)) - word_color = random.randint(0, bg_mean) - return word_color + def get_word_color(self): + colors = [i for i in self.cfg.font_color] + p = [self.cfg.font_color[i].fraction for i in self.cfg.font_color] + # pick color by fraction + color_name = np.random.choice(colors, p=p) + l_boundary = [ + int(i) for i in self.cfg.font_color[color_name].l_boundary.split(',')] + h_boundary = [ + int(i) for i in self.cfg.font_color[color_name].h_boundary.split(',')] + # random color by low and high RGB boundary + if color_name == 'black' or color_name == 'gray': + r = g = b = np.random.randint(l_boundary[0], h_boundary[0]) + else: + r = np.random.randint(l_boundary[0], h_boundary[0]) + g = np.random.randint(l_boundary[1], h_boundary[1]) + b = np.random.randint(l_boundary[2], h_boundary[2]) + return (b, g, r) - def draw_text_on_bg(self, word, font, bg): + def draw_text_on_bg(self, word, font, bg, font2): """ Draw word in the center of background :param word: word to draw @@ -237,29 +250,41 @@ def draw_text_on_bg(self, word, font, bg): word_height = word_size[1] word_width = word_size[0] - offset = font.getoffset(word) + offset = (0, 0) if self.fonts_by_image else font.getoffset(word) + pure_bg = np.ones((bg_height, bg_width, 3)) * 255 - pil_img = Image.fromarray(np.uint8(bg)) + pil_img = Image.fromarray(np.uint8(pure_bg)) draw = ImageDraw.Draw(pil_img) # Draw text in the center of bg text_x = int((bg_width - word_width) / 2) text_y = int((bg_height - word_height) / 2) - word_color = self.get_word_color(bg, text_x, text_y, word_height, word_width) + word_color = self.get_word_color() if apply(self.cfg.random_space): text_x, text_y, word_width, word_height = self.draw_text_with_random_space(draw, font, word, word_color, - bg_width, bg_height) - np_img = np.array(pil_img).astype(np.float32) + bg_width, bg_height, pure_bg) else: - if apply(self.cfg.seamless_clone): - np_img = self.draw_text_seamless(font, bg, word, word_color, word_height, word_width, offset) + if apply(self.cfg.texture) and not self.fonts_by_image: + pure_bg = Image.new( + 'RGBA', (bg_width, bg_height), (255, 255, 255, 255)) + pil_img = Image.new( + 'RGBA', (bg_width, bg_height), (255, 255, 255, 0)) + draw = ImageDraw.Draw(pil_img) + _ = self.draw_text_wrapper( + draw, word, text_x - offset[0], text_y - offset[1], font, word_color, font2, pil_img) + pil_img = self.texture.apply_cloud_texture(pure_bg, pil_img) else: - self.draw_text_wrapper(draw, word, text_x - offset[0], text_y - offset[1], font, word_color) - # draw.text((text_x - offset[0], text_y - offset[1]), word, fill=word_color, font=font) + if self.fonts_by_image: + pil_img = self.draw_text_wrapper( + draw, word, text_x - offset[0], text_y - offset[1], font, word_color, font2, pure_bg) + else: + _ = self.draw_text_wrapper( + draw, word, text_x - offset[0], text_y - offset[1], font, word_color, font2, pure_bg) + # draw.text((text_x - offset[0], text_y - offset[1]), word, fill=word_color, font=font) - np_img = np.array(pil_img).astype(np.float32) + np_img = np.array(pil_img).astype(np.float32) text_box_pnts = [ [text_x, text_y], @@ -270,56 +295,50 @@ def draw_text_on_bg(self, word, font, bg): return np_img, text_box_pnts, word_color - def draw_text_seamless(self, font, bg, word, word_color, word_height, word_width, offset): - # For better seamlessClone - seamless_offset = 6 - - # Draw text on a white image, than draw it on background - white_bg = np.ones((word_height + seamless_offset, word_width + seamless_offset)) * 255 - text_img = Image.fromarray(np.uint8(white_bg)) - draw = ImageDraw.Draw(text_img) - - # draw.text((0 + seamless_offset // 2, 0 - offset[1] + seamless_offset // 2), word, - # fill=word_color, font=font) - - self.draw_text_wrapper(draw, word, - 0 + seamless_offset // 2, - 0 - offset[1] + seamless_offset // 2, - font, word_color) - - # assume whole text_img as mask + def mix_seamless_bg(self, text_img, bg, anchor=None): text_img = np.array(text_img).astype(np.uint8) text_mask = 255 * np.ones(text_img.shape, text_img.dtype) + if self.fonts_by_image and anchor: + h, w = text_img.shape[:2] + center = (int(anchor[0] + w / 2), int(anchor[1] + h / 2)) + bg = bg.astype(np.uint8) + else: + center = (bg.shape[1] // 2, bg.shape[0] // 2) + mixed_clone = cv2.seamlessClone( + text_img, bg, text_mask, center, cv2.MIXED_CLONE) + return mixed_clone - # This is where the CENTER of the airplane will be placed - center = (bg.shape[1] // 2, bg.shape[0] // 2) - - # opencv seamlessClone require bgr image - text_img_bgr = np.ones((text_img.shape[0], text_img.shape[1], 3), np.uint8) - bg_bgr = np.ones((bg.shape[0], bg.shape[1], 3), np.uint8) - cv2.cvtColor(text_img, cv2.COLOR_GRAY2BGR, text_img_bgr) - cv2.cvtColor(bg, cv2.COLOR_GRAY2BGR, bg_bgr) - - flag = np.random.choice([ - cv2.NORMAL_CLONE, - cv2.MIXED_CLONE, - cv2.MONOCHROME_TRANSFER - ]) - - mixed_clone = cv2.seamlessClone(text_img_bgr, bg_bgr, text_mask, center, flag) - - np_img = cv2.cvtColor(mixed_clone, cv2.COLOR_BGR2GRAY) - + def draw_extra_random_word(self, text_img, text_box_pnts, img_index): + pil_img = Image.fromarray(np.uint8(text_img)) + draw = ImageDraw.Draw(pil_img) + word_color = self.get_word_color() + word, font, word_size, font2 = self.pick_font(img_index) + word_len = np.random.randint(1, len(word)) + word_height = word_size[1] + word_width = word_size[0] + # calculate text x,y + text_x = np.random.randint(text_box_pnts[0][0], text_box_pnts[1][0]) + text_y_b = text_box_pnts[2][1] + text_y_t = 2 * text_box_pnts[0][1] - \ + text_box_pnts[2][1] - word_height * 0.5 + text_y = np.random.choice([text_y_t, text_y_b], + p=[self.cfg.extra_words.top.fraction, self.cfg.extra_words.bottom.fraction]) + _ = self.draw_text_wrapper( + draw, word[:word_len], text_x, text_y, font, word_color, font2, np.uint8(text_img)) + np_img = np.array(pil_img).astype(np.float32) return np_img - def draw_text_with_random_space(self, draw, font, word, word_color, bg_width, bg_height): + def draw_text_with_random_space(self, draw, font, word, word_color, bg_width, bg_height, pil_img): """ If random_space applied, text_x, text_y, word_width, word_height may change""" width = 0 height = 0 chars_size = [] y_offset = 10 ** 5 for c in word: - size = font.getsize(c) + if self.fonts_by_image: + size = font[c].shape[:2] if c in font else (font_size, font_size) + else: + size = font.getsize(c) chars_size.append(size) width += size[0] @@ -329,11 +348,12 @@ def draw_text_with_random_space(self, draw, font, word, word_color, bg_width, bg # Min chars y offset as word y offset # Assume only y offset - c_offset = font.getoffset(c) + c_offset = (0, 0) if self.fonts_by_image else font.getoffset(c) if c_offset[1] < y_offset: y_offset = c_offset[1] - char_space_width = int(height * np.random.uniform(self.cfg.random_space.min, self.cfg.random_space.max)) + char_space_width = int( + height * np.random.uniform(self.cfg.random_space.min, self.cfg.random_space.max)) width += (char_space_width * (len(word) - 1)) @@ -345,20 +365,48 @@ def draw_text_with_random_space(self, draw, font, word, word_color, bg_width, bg for i, c in enumerate(word): # self.draw_text_wrapper(draw, c, c_x, c_y - y_offset, font, word_color, force_text_border) - draw.text((c_x, c_y - y_offset), c, fill=word_color, font=font) + if self.fonts_by_image: + pil_img = self.mix_seamless_bg(font[c], pil_img, (c_x, c_y - y_offset)) + else: + draw.text((c_x, c_y - y_offset), c, fill=word_color, font=font) c_x += (chars_size[i][0] + char_space_width) return text_x, text_y, width, height - def draw_text_wrapper(self, draw, text, x, y, font, text_color): + def draw_text_wrapper(self, draw, text, x, y, font, text_color, font2, pil_img): """ :param x/y: 应该是移除了 offset 的 """ if apply(self.cfg.text_border): self.draw_border_text(draw, text, x, y, font, text_color) + + #todo: 整合draw_text_wrapper 与 draw_text_with_random_space + elif apply(self.cfg.second_font): + if self.cfg.second_font.font_color_change: + text_color2 = self.get_word_color() + else: + text_color2 = text_color + for i, c in enumerate(text): + if random.random() < self.cfg.second_font.change_rate and not self.fonts_by_image: + y_offset = np.random.uniform(0, font2.getoffset(c)[1]) + draw.text((x, y + y_offset), c, fill=text_color2, font=font2) + x += font2.getsize(c)[0] + else: + if self.fonts_by_image: + pil_img = self.mix_seamless_bg(font[c], pil_img, (x, y)) + x += font[c].shape[1] + else: + draw.text((x, y), c, fill=text_color, font=font) + x += font.getsize(c)[0] else: - draw.text((x, y), text, fill=text_color, font=font) + if self.fonts_by_image: + for c in text: + pil_img = self.mix_seamless_bg(font[c], pil_img, (x, y)) + x += font[c].shape[1] + else: + draw.text((x, y), text, fill=text_color, font=font) + return pil_img def draw_border_text(self, draw, text, x, y, font, text_color): """ @@ -379,7 +427,8 @@ def draw_border_text(self, draw, text, x, y, font, text_color): light_or_dark = np.random.choice(choices, p=p) if light_or_dark == 0: - border_color = text_color + np.random.randint(0, 255 - text_color - 1) + border_color = text_color + \ + np.random.randint(0, 255 - text_color - 1) elif light_or_dark == 1: border_color = text_color - np.random.randint(0, text_color + 1) @@ -390,32 +439,20 @@ def draw_border_text(self, draw, text, x, y, font, text_color): draw.text((x, y + thickness), text, font=font, fill=border_color) # thicker border - draw.text((x - thickness, y - thickness), text, font=font, fill=border_color) - draw.text((x + thickness, y - thickness), text, font=font, fill=border_color) - draw.text((x - thickness, y + thickness), text, font=font, fill=border_color) - draw.text((x + thickness, y + thickness), text, font=font, fill=border_color) + draw.text((x - thickness, y - thickness), + text, font=font, fill=border_color) + draw.text((x + thickness, y - thickness), + text, font=font, fill=border_color) + draw.text((x - thickness, y + thickness), + text, font=font, fill=border_color) + draw.text((x + thickness, y + thickness), + text, font=font, fill=border_color) # now draw the text over it draw.text((x, y), text, font=font, fill=text_color) def gen_bg(self, width, height): - if apply(self.cfg.img_bg): - bg = self.gen_bg_from_image(int(width), int(height)) - else: - bg = self.gen_rand_bg(int(width), int(height)) - return bg - - def gen_rand_bg(self, width, height): - """ - Generate random background - """ - bg_high = random.uniform(220, 255) - bg_low = bg_high - random.uniform(1, 60) - - bg = np.random.randint(bg_low, bg_high, (height, width)).astype(np.uint8) - - bg = self.apply_gauss_blur(bg) - + bg = self.gen_bg_from_image(int(width), int(height)) return bg def gen_bg_from_image(self, width, height): @@ -428,15 +465,18 @@ def gen_bg_from_image(self, width, height): scale = max(width / bg.shape[1], height / bg.shape[0]) + rand_scale = scale * random.random() + + scale = scale if width > rand_scale * bg.shape[1] else rand_scale + out = cv2.resize(bg, None, fx=scale, fy=scale) - x_offset, y_offset = self.random_xy_offset(height, width, out.shape[0], out.shape[1]) + x_offset, y_offset = self.random_xy_offset( + height, width, out.shape[0], out.shape[1]) out = out[y_offset:y_offset + height, x_offset:x_offset + width] - out = self.apply_gauss_blur(out, ks=[7, 11, 13, 15, 17]) - - bg_mean = int(np.mean(out)) + # out = self.apply_gauss_blur(out, ks=[7, 11, 13, 15, 17]) 尝试不再模糊背景 # TODO: find a better way to deal with background # alpha = 255 / bg_mean # 对比度 @@ -459,9 +499,11 @@ def pick_font(self, img_index): word = word[:self.max_chars] font_path = random.choice(self.fonts) + font_path_2 = random.choice(self.fonts) if self.strict: unsupport_chars = self.font_unsupport_chars[font_path] + # unsupport_chars.extend(self.font_unsupport_chars[font_path_2]) for c in word: if c == ' ': continue @@ -471,12 +513,38 @@ def pick_font(self, img_index): raise Exception # Font size in point - font_size = random.randint(self.cfg.font_size.min, self.cfg.font_size.max) - font = ImageFont.truetype(font_path, font_size) + font_size = random.randint( + self.cfg.font_size.min, self.cfg.font_size.max) + if self.fonts_by_image: + font = self.loads_fonts_by_image(font_path, font_size) + else: + font = ImageFont.truetype(font_path, font_size) + if self.cfg.second_font.font_size_change: + font_size_2 = font_size - random.randint(1, 10) + else: + font_size_2 = font_size + if self.cfg.second_font.font_change: + if self.fonts_by_image: + font2 = self.loads_fonts_by_image(font_path_2, font_size_2) + else: + font2 = ImageFont.truetype(font_path_2, font_size_2) + else: + if self.fonts_by_image: + font2 = self.loads_fonts_by_image(font_path, font_size_2) + else: + font2 = ImageFont.truetype(font_path, font_size_2) + + return word, font, self.get_word_size(font, word, font_size), font2 - return word, font, self.get_word_size(font, word) + def loads_fonts_by_image(self, font_path, font_size): + fonts = {} + for x in os.listdir(font_path): + img = cv2.imdecode(np.fromfile(font_path + "/" + x, dtype=np.uint8), cv2.IMREAD_COLOR) + h, w = img.shape[:2] + fonts[x.replace(".png", "")] = cv2.resize(img, (font_size, h * font_size // w)) + return fonts - def get_word_size(self, font, word): + def get_word_size(self, font, word, default=None): """ Get word size removed offset :param font: truetype @@ -484,8 +552,20 @@ def get_word_size(self, font, word): :return: size: word size, removed offset (width, height) """ - offset = font.getoffset(word) - size = font.getsize(word) + if self.fonts_by_image: + offset = (0, 0) + w_list, h_list = [], [] + for c in word: + if c in font: + h, w = font[c].shape[:2] + else: + h, w = default, default + w_list.append(w) + h_list.append(h) + size = (sum(w_list), max(h_list)) + else: + offset = font.getoffset(word) + size = font.getsize(word) size = (size[0] - offset[0], size[1] - offset[1]) return size @@ -509,7 +589,8 @@ def apply_perspective_transform(self, img, text_box_pnts, max_x, max_y, max_z, g # print("x: %f, y: %f, z: %f" % (x, y, z)) - transformer = math_utils.PerspectiveTransform(x, y, z, scale=1.0, fovy=50) + transformer = math_utils.PerspectiveTransform( + x, y, z, scale=1.0, fovy=50) dst_img, M33, dst_img_pnts = transformer.transform_image(img, gpu) dst_text_pnts = transformer.transform_pnts(text_box_pnts, M33) @@ -517,10 +598,30 @@ def apply_perspective_transform(self, img, text_box_pnts, max_x, max_y, max_z, g return dst_img, dst_img_pnts, dst_text_pnts def apply_blur_on_output(self, img): - if prob(0.5): - return self.apply_gauss_blur(img, [3, 5]) - else: - return self.apply_norm_blur(img) + p = [] + funcs = [] + if self.cfg.blur.gauss.enable: + p.append(self.cfg.blur.gauss.fraction) + funcs.append(self.apply_gauss_blur) + + if self.cfg.blur.norm.enable: + p.append(self.cfg.blur.norm.fraction) + funcs.append(self.apply_norm_blur) + + if self.cfg.blur.motion.enable: + p.append(self.cfg.blur.motion.fraction) + funcs.append(self.apply_motion_blur) + + if self.cfg.blur.prydown.enable: + p.append(self.cfg.blur.prydown.fraction) + funcs.append(self.apply_prydown) + + if len(p) == 0: + return img + + blur_func = np.random.choice(funcs, p=p) + + return blur_func(img) def apply_gauss_blur(self, img, ks=None): if ks is None: @@ -542,15 +643,30 @@ def apply_norm_blur(self, img, ks=None): img = cv2.blur(img, (kernel, kernel)) return img + def apply_motion_blur(self, img, ks=None, angle=None): + if ks is None: + ks = [7, 9, 11, 13] + ksize = random.choice(ks) + if angle is None: + angle = random.randint(0, 360) + kernel = np.zeros((ksize, ksize), dtype=np.float32) + kernel[ (ksize-1)// 2 , :] = np.ones(ksize, dtype=np.float32) + kernel = cv2.warpAffine(kernel, cv2.getRotationMatrix2D( (ksize / 2 -0.5 , ksize / 2 -0.5 ) , angle, 1.0), (ksize, ksize) ) + kernel = kernel * ( 1.0 / np.sum(kernel) ) + img = cv2.filter2D(img, -1, kernel) + return img + def apply_prydown(self, img): """ 模糊图像,模拟小图片放大的效果 """ - scale = random.uniform(1, self.cfg.prydown.max_scale) + scale = random.uniform( + 1, self.cfg.blur.prydown.max_scale) # todo: use different h/w scale height = img.shape[0] width = img.shape[1] - out = cv2.resize(img, (int(width / scale), int(height / scale)), interpolation=cv2.INTER_AREA) + out = cv2.resize( + img, (int(width / scale), int(height / scale)), interpolation=cv2.INTER_AREA) return cv2.resize(out, (width, height), interpolation=cv2.INTER_AREA) def reverse_img(self, word_img): @@ -590,12 +706,14 @@ def apply_crop(self, text_box_pnts, crop_cfg): croped_text_box_pnts = text_box_pnts if prob(0.5): - top_crop = int(random.randint(crop_cfg.top.min, crop_cfg.top.max) * scale) + top_crop = int(random.randint( + crop_cfg.top.min, crop_cfg.top.max) * scale) self.dmsg("top crop %d" % top_crop) croped_text_box_pnts[0][1] += top_crop croped_text_box_pnts[1][1] += top_crop else: - bottom_crop = int(random.randint(crop_cfg.bottom.min, crop_cfg.bottom.max) * scale) + bottom_crop = int(random.randint( + crop_cfg.bottom.min, crop_cfg.bottom.max) * scale) self.dmsg("bottom crop %d " % bottom_crop) croped_text_box_pnts[2][1] -= bottom_crop croped_text_box_pnts[3][1] -= bottom_crop