gyrojeff's picture
feat: more robust generation script
0e2310c
raw
history blame
12 kB
from typing import Tuple
__all__ = ["generate_font_image", "TextSizeTooSmallException"]
epislon = 1e-6
render_calculation_size = 128
# text direction
ltr_ratio = 0.5
ttb_ratio = 0.5
assert ltr_ratio + ttb_ratio - 1 < epislon
# text length
short_ratio = 0.1
median_ratio = 0.6
long_ratio = 0.3
from .text import CorpusGenerationConfig, CorpusGeneratorManager
short_condition = CorpusGenerationConfig(
min_num_line=1, max_num_line=1, min_num_char_per_line=2, max_num_char_per_line=5
)
median_condition = CorpusGenerationConfig(
min_num_line=1, max_num_line=4, min_num_char_per_line=1, max_num_char_per_line=20
)
long_condition = CorpusGenerationConfig(
min_num_line=5, max_num_line=10, min_num_char_per_line=1, max_num_char_per_line=20
)
assert short_ratio + median_ratio + long_ratio - 1 < epislon
# text color
gray_ratio = 0.3
color_ratio = 0.7
# whether use stroke, only stroke when color
pure_color_ratio = 0.5
stroke_color_ratio = 0.5
assert pure_color_ratio + stroke_color_ratio - 1 < epislon
# stroke width
stroke_width_max_ratio = 0.25
assert gray_ratio + color_ratio - 1 < epislon
# clip size ratio
clip_width_max_ratio = 0.8
clip_width_min_ratio = 0.3
clip_width_height_min_ratio = 0.75
clip_width_height_max_ratio = 1.25
# text longer edge ratio
text_longer_max_ratio = 1.0
text_longer_min_ratio = 0.6
# line spacing
line_spacing_max_ratio = 1.5
line_spacing_min_ratio = 0.0
# rotation
no_rotation_ratio = 0.3
rotation_ratio = 0.7
assert no_rotation_ratio + rotation_ratio - 1 < epislon
# in degree
rotation_max_angle = 30
text_size_min = 15
# ratio of dataset size for cjk
cjk_distribution = {
"ja": 0.3,
"ko": 0.2,
"zh-Hans": 0.3,
"zh-Hant": 0.07,
"zh-Hant-HK": 0.06,
"zh-Hant-TW": 0.06,
}
assert sum(cjk_distribution.values()) - 1 < epislon
import math
import random
import traceback
from PIL import Image, ImageDraw, ImageFont
from .fontlabel import FontLabel
from .font import DSFont
def random_color():
return random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)
def render_bbox(
draw,
xy,
text: str,
font=None,
anchor=None,
spacing=4,
align="left",
direction=None,
features=None,
language=None,
stroke_width=0,
embedded_color=False,
):
if ("\n" in text or "\r" in text) and direction == "ttb":
lines = text.splitlines(keepends=False)
height = 0
width = 0
x = 0
y = 0
for i, line in enumerate(lines):
bbox = draw.textbbox(
(0, 0),
line,
font,
anchor,
spacing,
align,
direction,
features,
language,
stroke_width,
embedded_color,
)
height = max(height, bbox[3] - bbox[1])
width += bbox[2] - bbox[0]
if i > 0:
width += spacing
else:
x = bbox[0]
y = bbox[1]
return x, y, x + width, y + height
else:
return draw.textbbox(
xy,
text,
font,
anchor,
spacing,
align,
direction,
features,
language,
stroke_width,
embedded_color,
)
def render_text(
draw,
xy,
text,
fill=None,
font=None,
anchor=None,
spacing=4,
align="left",
direction=None,
features=None,
language=None,
stroke_width=0,
stroke_fill=None,
embedded_color=False,
*args,
**kwargs,
):
if ("\n" in text or "\r" in text) and direction == "ttb":
lines = text.splitlines(keepends=False)
margin_x = 0
x, y = xy
for i, line in enumerate(lines):
bbox = draw.textbbox(
(0, 0),
line,
font,
anchor,
spacing,
align,
direction,
features,
language,
stroke_width,
embedded_color,
)
draw.text(
(x + margin_x, y),
line,
fill,
font,
anchor,
spacing,
align,
direction,
features,
language,
stroke_width,
stroke_fill,
embedded_color,
*args,
**kwargs,
)
margin_x += bbox[2] - bbox[0]
margin_x += spacing
else:
draw.text(
xy,
text,
fill,
font,
anchor,
spacing,
align,
direction,
features,
language,
stroke_width,
stroke_fill,
embedded_color,
*args,
**kwargs,
)
def RGB2RGBA(color):
if color is None:
return None
return color + (255,)
class TextSizeTooSmallException(Exception):
def __init__(self):
super().__init__(f"Text Size Too Small")
def generate_font_image(
img_path: str, font: DSFont, corpus_manager: CorpusGeneratorManager
) -> Tuple[Image.Image, FontLabel]:
im = Image.open(img_path)
# crop image
width, height = im.size
clip_width = random.randint(
int(width * clip_width_min_ratio), int(width * clip_width_max_ratio)
)
clip_height = random.randint(
int(clip_width * clip_width_height_min_ratio),
int(clip_width * clip_width_height_max_ratio),
)
if clip_height > height:
clip_height = height
clip_x = random.randint(0, width - clip_width)
clip_y = random.randint(0, height - clip_height)
im = im.crop((clip_x, clip_y, clip_x + clip_width, clip_y + clip_height))
# language
render_language = font.language
if render_language == "CJK":
render_language = random.choices(
list(cjk_distribution.keys()), list(cjk_distribution.values())
)[0]
elif render_language == "zh":
render_language = random.choice(["zh-Hans", "zh-Hant"])
# text direction
if random.random() < ltr_ratio:
text_direction = "ltr"
else:
text_direction = "ttb"
# text length
if random.random() < short_ratio:
text = corpus_manager.generate(short_condition, font, render_language)
elif random.random() < median_ratio:
text = corpus_manager.generate(median_condition, font, render_language)
else:
text = corpus_manager.generate(long_condition, font, render_language)
# text color & stroke
if random.random() < gray_ratio:
text_color = random.randint(0, 255)
text_color = (text_color, text_color, text_color)
# no stroke in gray
stroke_ratio = 0
stroke_color = None
im = im.convert("L")
else:
text_color = random_color()
# whether use stroke
if random.random() < pure_color_ratio:
stroke_ratio = 0
stroke_color = None
else:
stroke_ratio = random.random() * stroke_width_max_ratio
stroke_color = random_color()
# line spacing
line_spacing_ratio = (
random.random() * (line_spacing_max_ratio - line_spacing_min_ratio)
+ line_spacing_min_ratio
)
# calculate render ratio
render_calculation_stroke_width = int(stroke_ratio * render_calculation_size)
render_calculation_line_spacing = int(line_spacing_ratio * render_calculation_size)
pil_font = ImageFont.truetype(font.path, size=render_calculation_size)
text_bbox = render_bbox(
ImageDraw.Draw(im),
(0, 0),
text,
font=pil_font,
direction=text_direction,
spacing=render_calculation_line_spacing,
stroke_width=render_calculation_stroke_width,
language=render_language,
)
(
render_calculation_width_no_rotation,
render_calculation_height_no_rotation,
) = (text_bbox[2] - text_bbox[0], text_bbox[3] - text_bbox[1])
render_calculation_font_x_no_rotation = text_bbox[0]
render_calculation_font_y_no_rotation = text_bbox[1]
if random.random() < no_rotation_ratio:
render_angle = 0
render_calculation_width = render_calculation_width_no_rotation
render_calculation_height = render_calculation_height_no_rotation
else:
render_angle = random.randint(-rotation_max_angle, rotation_max_angle)
render_calculation_width = int(
render_calculation_width_no_rotation
* math.cos(math.radians(abs(render_angle)))
+ render_calculation_height_no_rotation
* math.sin(math.radians(abs(render_angle)))
)
render_calculation_height = int(
render_calculation_width_no_rotation
* math.sin(math.radians(abs(render_angle)))
+ render_calculation_height_no_rotation
* math.cos(math.radians(abs(render_angle)))
)
# calculate render size
render_ratio = (
random.random() * (text_longer_max_ratio - text_longer_min_ratio)
+ text_longer_min_ratio
)
if render_calculation_width / render_calculation_height < clip_width / clip_height:
# height is the limit
render_height = int(clip_height * render_ratio)
render_width = int(
render_calculation_width / render_calculation_height * render_height
)
else:
# width is the limit
render_width = int(clip_width * render_ratio)
render_height = int(
render_calculation_height / render_calculation_width * render_width
)
# calculate text size
text_size = int(render_calculation_size * render_height / render_calculation_height)
if text_size < text_size_min:
raise TextSizeTooSmallException()
render_width_no_rotation = int(
render_calculation_width_no_rotation / render_calculation_height * render_height
)
render_height_no_rotation = int(
render_calculation_height_no_rotation
/ render_calculation_height
* render_height
)
render_font_x_no_rotation = int(
render_calculation_font_x_no_rotation
/ render_calculation_height
* render_height
)
render_font_y_no_rotation = int(
render_calculation_font_y_no_rotation
/ render_calculation_height
* render_height
)
stroke_width = int(text_size * stroke_ratio)
line_spacing = int(text_size * line_spacing_ratio)
# calculate render position
render_x = random.randint(0, clip_width - render_width)
render_y = random.randint(0, clip_height - render_height)
font_image = Image.new(
"RGBA",
(render_width_no_rotation, render_height_no_rotation),
(0, 0, 0, 0),
)
pil_font = ImageFont.truetype(font.path, size=text_size)
render_text(
ImageDraw.Draw(font_image),
(-render_font_x_no_rotation, -render_font_y_no_rotation),
text,
font=pil_font,
fill=RGB2RGBA(text_color),
direction=text_direction,
spacing=line_spacing,
stroke_width=stroke_width,
stroke_fill=RGB2RGBA(stroke_color),
language=render_language,
)
if rotation_max_angle != 0:
font_image = font_image.rotate(
render_angle, expand=True, fillcolor=(0, 0, 0, 0)
)
im.paste(font_image, (render_x, render_y), font_image)
return im, FontLabel(
clip_width,
clip_height,
text,
font,
text_color,
text_size,
text_direction,
stroke_width,
stroke_color,
line_spacing,
render_language,
(render_x, render_y, render_width, render_height),
render_angle,
)