Skip to content

Commit ac18f48

Browse files
Vítor Albierofacebook-github-bot
Vítor Albiero
authored andcommitted
Adding image random text wrapper (#254)
Summary: Pull Request resolved: #254 Adding a to add text to an image that can do the following after a pre-defined text is given: - Randomly generates x and y position to start writing the text - Breaks the text into N lines, so that it can fit in the image - Moves the x start point to the left if the text cannot fit - Reduces font size it text still cannot fit - Randomly selects color Reviewed By: erikbrinkman, jbitton Differential Revision: D64933482 fbshipit-source-id: e76853b2e2b39cdcac71c35f6baa4bda7901925f
1 parent 1113b1d commit ac18f48

File tree

10 files changed

+337
-2
lines changed

10 files changed

+337
-2
lines changed

augly/image/__init__.py

+4
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
overlay_onto_screenshot,
3232
overlay_stripes,
3333
overlay_text,
34+
overlay_wrap_text,
3435
pad,
3536
pad_square,
3637
perspective_transform,
@@ -106,6 +107,7 @@
106107
OverlayOntoScreenshot,
107108
OverlayStripes,
108109
OverlayText,
110+
OverlayWrapText,
109111
Pad,
110112
PadSquare,
111113
PerspectiveTransform,
@@ -153,6 +155,7 @@
153155
"OverlayOntoScreenshot",
154156
"OverlayStripes",
155157
"OverlayText",
158+
"OverlayWrapText",
156159
"Pad",
157160
"PadSquare",
158161
"PerspectiveTransform",
@@ -195,6 +198,7 @@
195198
"overlay_onto_screenshot",
196199
"overlay_stripes",
197200
"overlay_text",
201+
"overlay_wrap_text",
198202
"pad",
199203
"pad_square",
200204
"perspective_transform",

augly/image/functional.py

+110
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@
1111
import math
1212
import os
1313
import pickle
14+
import random
1415
from copy import deepcopy
1516
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
1617

1718
import numpy as np
1819
from augly import utils
1920
from augly.image import utils as imutils
21+
from augly.image.helpers import fit_text_in_bbox
2022
from augly.image.utils.bboxes import spatial_bbox_helper
2123
from PIL import Image, ImageDraw, ImageEnhance, ImageFilter, ImageFont
2224

@@ -1630,6 +1632,114 @@ def overlay_text(
16301632
return imutils.ret_and_save_image(image, output_path, src_mode)
16311633

16321634

1635+
def overlay_wrap_text(
1636+
image: Image.Image,
1637+
text: str,
1638+
output_path: Optional[str] = None,
1639+
min_font_size_ratio: float = 0.02,
1640+
max_font_size_ratio: float = 0.2,
1641+
font_file: str = utils.DEFAULT_TEXT_OVERLAY_FONT_PATH,
1642+
font_size: Optional[float] = None,
1643+
color: Optional[tuple[int, int, int]] = None,
1644+
metadata: Optional[list[dict[str, object]]] = None,
1645+
random_seed: Optional[int] = None,
1646+
) -> Image.Image:
1647+
"""Randomly overlay a pre-defined text on an image
1648+
1649+
@param img: Image to overlay text on
1650+
1651+
@param text: Text to overlay on image
1652+
1653+
@param output_path Path to save resulting image
1654+
1655+
@param min_font_size_ratio: Minimum font size ratio w.r.t. the image to use for text
1656+
1657+
@param max_font_size_ratio: Maximum font size ratio w.r.t. the image to use for text
1658+
1659+
@param font_size: Font size to use for text
1660+
1661+
@param color: Color to use for text
1662+
1663+
@param metadata : List to store metadata about the function execution
1664+
1665+
@returns: Image with text overlayed
1666+
"""
1667+
rand = random.Random(random_seed)
1668+
1669+
assert (
1670+
0.0 <= min_font_size_ratio <= 1.0
1671+
), "Font size must be a value in the range [0.0, 1.0]"
1672+
1673+
assert (
1674+
0.0 <= max_font_size_ratio <= 1.0
1675+
), "Font size must be a value in the range [0.0, 1.0]"
1676+
1677+
if font_size:
1678+
assert (
1679+
0.0 <= font_size <= 1.0
1680+
), "Font size must be a value in the range [0.0, 1.0]"
1681+
1682+
if color:
1683+
utils.validate_rgb_color(color)
1684+
1685+
image = imutils.validate_and_load_image(image)
1686+
1687+
func_kwargs = imutils.get_func_kwargs(metadata, locals())
1688+
src_mode = image.mode
1689+
1690+
width, height = image.size
1691+
1692+
min_font_size = int(min(width, height) * min_font_size_ratio)
1693+
max_font_size = int(min(width, height) * max_font_size_ratio)
1694+
1695+
if not font_size:
1696+
# get a random font size between min_font_size_ratio and max_font_size_ratio of the image size
1697+
font_size = rand.uniform(min_font_size_ratio, max_font_size_ratio)
1698+
1699+
font_size = int(min(width, height) * font_size)
1700+
# if font size is too small, increase it to min_font_size of the image size
1701+
font_size = max(font_size, min_font_size)
1702+
# if font size is too large, decrease it to max_font_size of the image size
1703+
font_size = min(font_size, max_font_size)
1704+
1705+
local_font_path = utils.pathmgr.get_local_path(font_file)
1706+
1707+
random_x, random_y, lines, line_height, font = fit_text_in_bbox(
1708+
text,
1709+
height,
1710+
width,
1711+
local_font_path,
1712+
font_size,
1713+
min_font_size,
1714+
rand,
1715+
)
1716+
1717+
if not color:
1718+
# get a random color
1719+
color = (rand.randrange(255), rand.randrange(255), rand.randrange(255))
1720+
1721+
red, green, blue = color
1722+
draw = ImageDraw.Draw(image)
1723+
for line in lines:
1724+
# draw text on the image
1725+
draw.text(
1726+
(random_x, random_y),
1727+
line,
1728+
fill=(red, green, blue),
1729+
font=font, # pyre-ignore [6]
1730+
)
1731+
random_y = random_y + line_height
1732+
1733+
imutils.get_metadata(
1734+
metadata=metadata,
1735+
function_name="overlay_wrap_text",
1736+
aug_image=image,
1737+
**func_kwargs,
1738+
)
1739+
1740+
return imutils.ret_and_save_image(image, output_path, src_mode)
1741+
1742+
16331743
def pad(
16341744
image: Union[str, Image.Image],
16351745
output_path: Optional[str] = None,

augly/image/helpers.py

+101-2
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@
77

88
# pyre-unsafe
99

10-
from typing import Callable
10+
import random
11+
from typing import Callable, List, Tuple
1112

1213
import numpy as np
13-
from PIL import Image
14+
from PIL import Image, ImageFont
1415

1516

1617
def aug_np_wrapper(
@@ -30,3 +31,101 @@ def aug_np_wrapper(
3031
pil_image = Image.fromarray(image)
3132
aug_image = aug_function(pil_image, **kwargs)
3233
return np.array(aug_image)
34+
35+
36+
def fit_text_in_bbox(
37+
text: str,
38+
img_height: int,
39+
img_width: int,
40+
font_path: str,
41+
font_size: int,
42+
min_font_size: int,
43+
rand: random.Random,
44+
) -> Tuple[int, int, List[str], int, ImageFont.FreeTypeFont]:
45+
"""Fits text into a bounding box by adjusting font size and x-coordinate
46+
47+
@param text: Text to fit into bounding box
48+
49+
@param img_height: Height of image
50+
51+
@param img_width: Width of image
52+
53+
@param font_path: Path to font file
54+
55+
@param font_size: Font size to start with
56+
57+
@param min_font_size: Minimum font size to try
58+
59+
@param rand: Random number generator
60+
61+
@returns: x and y coordinates to start writing, text split into lines, line heigh, and font style
62+
"""
63+
x_min = int(img_width * 0.05) # reserves 5% on the left
64+
x_max = int(img_width * 0.5) # starts writing at the center of the image
65+
random_x = rand.randint(
66+
x_min, x_max
67+
) # generate random x-coordinate to start writing
68+
69+
max_img_width = int(img_width * 0.95) # reserves 5% on the right side of image
70+
71+
while True:
72+
# loads font
73+
font = ImageFont.truetype(font_path, font_size)
74+
75+
# wrap text around image
76+
lines = wrap_text_for_image_overlay(text, font, int(max_img_width - random_x))
77+
_, _, _, line_height = font.getbbox("hg")
78+
79+
y_min = int(img_height * 0.05) # reserves 5% on the top
80+
y_max = int(img_height * 0.9) # reseves 10% to the bottom
81+
y_max -= (
82+
len(lines) * line_height
83+
) # adjust max y-coordinate for text height and number of lines
84+
85+
if y_max < y_min:
86+
if random_x > x_min:
87+
# adjust x-coordinate by 10% to try to fit text
88+
random_x = int(max(random_x - 0.1 * max_img_width, x_min))
89+
90+
elif font_size > min_font_size:
91+
# reduces font size by 1pt to try to fit text
92+
font_size -= 1
93+
else:
94+
raise ValueError("Text too long to fit onto image!")
95+
else:
96+
random_y = rand.randint(
97+
y_min, y_max
98+
) # generate random y-coordinate to start writing
99+
return random_x, random_y, lines, line_height, font
100+
101+
102+
def wrap_text_for_image_overlay(
103+
text: str, font: ImageFont.FreeTypeFont, max_width: int
104+
) -> List[str]:
105+
"""Wraps text around an image
106+
107+
@param text (str): Text to wrap
108+
109+
@param font (PIL.ImageFont): Font to use for text
110+
111+
@param max_width (int): Maximum width of the image
112+
113+
@returns: List of wrapped text, where each element is a line of text
114+
"""
115+
lines = []
116+
117+
if font.getbbox(text)[2] <= max_width:
118+
return [text]
119+
else:
120+
words = text.split(" ")
121+
line_words = []
122+
lines = []
123+
for word in words:
124+
if font.getbbox(" ".join(line_words + [word]))[2] <= max_width:
125+
line_words.append(word)
126+
else:
127+
lines.append(" ".join(line_words))
128+
line_words = [word]
129+
lines.append(" ".join(line_words))
130+
131+
return lines

augly/image/transforms.py

+82
Original file line numberDiff line numberDiff line change
@@ -1493,6 +1493,88 @@ def apply_transform(
14931493
)
14941494

14951495

1496+
class OverlayWrapText(BaseTransform):
1497+
def __init__(
1498+
self,
1499+
text: str,
1500+
min_font_size_ratio: float = 0.02,
1501+
max_font_size_ratio: float = 0.2,
1502+
font_file: str = utils.DEFAULT_TEXT_OVERLAY_FONT_PATH,
1503+
font_size: Optional[float] = None,
1504+
color: Optional[tuple[int, int, int]] = None,
1505+
random_seed: Optional[int] = None,
1506+
p: float = 1.0,
1507+
):
1508+
"""Randomly overlay a pre-defined text on an image
1509+
1510+
@param img: Image to overlay text on
1511+
1512+
@param text: Text to overlay on image
1513+
1514+
@param output_path Path to save resulting image
1515+
1516+
@param min_font_size_ratio: Minimum font size ratio w.r.t. the image to use for text
1517+
1518+
@param max_font_size_ratio: Maximum font size ratio w.r.t. the image to use for text
1519+
1520+
@param font_size: Font size to use for text
1521+
1522+
@param color: Color to use for text
1523+
1524+
@param metadata : List to store metadata about the function execution
1525+
1526+
@param p: the probability of the transform being applied; default value is 1.0
1527+
1528+
@returns: Image with text overlayed
1529+
"""
1530+
super().__init__(p)
1531+
self.text, self.color = text, color
1532+
self.min_font_size_ratio, self.max_font_size_ratio = (
1533+
min_font_size_ratio,
1534+
max_font_size_ratio,
1535+
)
1536+
self.font_file, self.font_size = font_file, font_size
1537+
self.random_seed = random_seed
1538+
1539+
def apply_transform(
1540+
self,
1541+
image: Image.Image,
1542+
metadata: Optional[List[Dict[str, Any]]] = None,
1543+
bboxes: Optional[List[Tuple]] = None,
1544+
bbox_format: Optional[str] = None,
1545+
) -> Image.Image:
1546+
"""
1547+
Randomly overlay a pre-defined text on an image
1548+
1549+
@param image: PIL Image to be augmented
1550+
1551+
@param metadata: if set to be a list, metadata about the function execution
1552+
including its name, the source & dest width, height, etc. will be appended to
1553+
the inputted list. If set to None, no metadata will be appended or returned
1554+
1555+
@param bboxes: a list of bounding boxes can be passed in here if desired. If
1556+
provided, this list will be modified in place such that each bounding box is
1557+
transformed according to this function
1558+
1559+
@param bbox_format: signifies what bounding box format was used in `bboxes`. Must
1560+
specify `bbox_format` if `bboxes` is provided. Supported bbox_format values
1561+
are "pascal_voc", "pascal_voc_norm", "coco", and "yolo"
1562+
1563+
@returns: Augmented PIL Image
1564+
"""
1565+
return F.overlay_wrap_text(
1566+
image,
1567+
text=self.text,
1568+
min_font_size_ratio=self.min_font_size_ratio,
1569+
max_font_size_ratio=self.max_font_size_ratio,
1570+
font_file=self.font_file,
1571+
font_size=self.font_size,
1572+
color=self.color,
1573+
metadata=metadata,
1574+
random_seed=self.random_seed,
1575+
)
1576+
1577+
14961578
class Pad(BaseTransform):
14971579
def __init__(
14981580
self,

augly/tests/assets/expected_metadata/image_tests/expected_metadata.json

+19
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,25 @@
428428
"y_pos": 0.5
429429
}
430430
],
431+
"overlay_wrap_text": [
432+
{
433+
"text": "Testing if the function can wrap this awesome text and not go out of bounds",
434+
"output_path": null,
435+
"max_font_size_ratio": 0.2,
436+
"min_font_size_ratio": 0.02,
437+
"font_file": "fonts/Allura-Regular.ttf",
438+
"font_size": 0.2,
439+
"color": null,
440+
"intensity": 0,
441+
"name": "overlay_wrap_text",
442+
"random_seed": 42,
443+
"rand": null,
444+
"dst_height": 1080,
445+
"dst_width": 1920,
446+
"src_height": 1080,
447+
"src_width": 1920
448+
}
449+
],
431450
"pad": [
432451
{
433452
"bbox_format": "yolo",
Loading

0 commit comments

Comments
 (0)