Skip to content

Commit c6a5c6f

Browse files
authored
[feat] Allowing specifying box_alpha for draw_box (#60)
* Allowing specifying box_alpha for draw_box * clean ups * alpharize all non-text based colors * Add alpha tests * Update doc and input checks * clean up
1 parent f027525 commit c6a5c6f

File tree

2 files changed

+171
-28
lines changed

2 files changed

+171
-28
lines changed

src/layoutparser/visualization.py

Lines changed: 167 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
1-
from PIL import Image, ImageFont, ImageDraw
2-
from .elements import *
3-
import numpy as np
1+
from typing import List, Union, Dict, Any, Tuple
42
import functools
53
import os
64
import sys
75
import warnings
8-
import layoutparser
96
from itertools import cycle
107

8+
import numpy as np
9+
from PIL import Image, ImageFont, ImageDraw, ImageColor
10+
11+
import layoutparser
12+
from .elements import (
13+
Layout,
14+
Interval,
15+
Rectangle,
16+
TextBlock,
17+
Quadrilateral,
18+
_cvt_coordinates_to_points,
19+
)
20+
1121
# We need to fix this ugly hack some time in the future
1222
_lib_path = os.path.dirname(sys.modules[layoutparser.__package__].__file__)
1323
_font_path = os.path.join(_lib_path, "misc", "NotoSerifCJKjp-Regular.otf")
@@ -46,8 +56,8 @@ def _draw_vertical_text(
4656
text_width = max([image_font.getsize(c)[0] for c in text])
4757
text_height = sum(char_heights) + character_spacing * len(text)
4858

49-
txt_img = Image.new("RGB", (text_width, text_height), color=text_background_color)
50-
txt_mask = Image.new("RGB", (text_width, text_height), color=text_background_color)
59+
txt_img = Image.new("RGBA", (text_width, text_height), color=text_background_color)
60+
txt_mask = Image.new("RGBA", (text_width, text_height), color=text_background_color)
5161

5262
txt_img_draw = ImageDraw.Draw(txt_img)
5363
txt_mask_draw = ImageDraw.Draw(txt_mask)
@@ -81,15 +91,15 @@ def _create_new_canvas(canvas, arrangement, text_background_color):
8191

8292
if arrangement == "lr":
8393
new_canvas = Image.new(
84-
"RGB",
94+
"RGBA",
8595
(canvas.width * 2, canvas.height),
8696
color=text_background_color or DEFAULT_TEXT_BACKGROUND,
8797
)
8898
new_canvas.paste(canvas, (canvas.width, 0))
8999

90100
elif arrangement == "ud":
91101
new_canvas = Image.new(
92-
"RGB",
102+
"RGBA",
93103
(canvas.width, canvas.height * 2),
94104
color=text_background_color or DEFAULT_TEXT_BACKGROUND,
95105
)
@@ -108,6 +118,56 @@ def _create_color_palette(types):
108118
}
109119

110120

121+
def _get_color_rgb(color_string: Any, alpha: float) -> Tuple[int, int, int, int]:
122+
if color_string[0] == "#" and len(color_string) == 7:
123+
# When color string is a hex string
124+
color_hex = color_string.lstrip("#")
125+
return (
126+
*tuple(int(color_hex[i : i + 2], 16) for i in (0, 2, 4)),
127+
int(255 * alpha),
128+
)
129+
else:
130+
try:
131+
rgb = ImageColor.getrgb(color_string)
132+
return rgb + (int(255 * alpha),)
133+
except:
134+
# ImageColor.getrgb will throw an ValueError when the color is not
135+
# a valid color string, even if it is in other formats supported by
136+
# PIL. As such, we return the color as it is if the first two cases
137+
# are not valid.
138+
return color_string
139+
140+
141+
def _draw_box_outline_on_handler(draw, block, color, width):
142+
143+
if not hasattr(block, "points"):
144+
points = (_cvt_coordinates_to_points(block.coordinates),)
145+
else:
146+
points = block.points
147+
148+
vertices = points.ravel().tolist()
149+
drawing_vertices = vertices + vertices[:2]
150+
151+
draw.line(
152+
drawing_vertices,
153+
width=width,
154+
fill=color,
155+
)
156+
157+
158+
def _draw_transparent_box_on_handler(draw, block, color, alpha):
159+
160+
if hasattr(block, "points"):
161+
vertices = [tuple(block) for block in block.points.tolist()]
162+
else:
163+
vertices = _cvt_coordinates_to_points(block.coordinates)
164+
165+
draw.polygon(
166+
vertices,
167+
_get_color_rgb(color, alpha),
168+
)
169+
170+
111171
def image_loader(func):
112172
@functools.wraps(func)
113173
def wrap(canvas, layout, *args, **kwargs):
@@ -124,18 +184,44 @@ def wrap(canvas, layout, *args, **kwargs):
124184
return wrap
125185

126186

187+
@image_loader
188+
def draw_transparent_box(
189+
canvas: "Image",
190+
blocks: Layout,
191+
color_map: Dict = None,
192+
alpha: float = 0.25,
193+
) -> "Image":
194+
"""Given the image, draw a series of transparent boxes based on the blocks,
195+
coloring using the specified color_map.
196+
"""
197+
198+
if color_map is None:
199+
all_types = set([b.type for b in blocks if hasattr(b, "type")])
200+
color_map = _create_color_palette(all_types)
201+
202+
canvas = canvas.copy()
203+
draw = ImageDraw.Draw(canvas, "RGBA")
204+
205+
for block in blocks:
206+
_draw_transparent_box_on_handler(draw, block, color_map[block.type], alpha)
207+
208+
return canvas
209+
210+
127211
@image_loader
128212
def draw_box(
129213
canvas,
130214
layout,
131215
box_width=None,
216+
box_alpha=0,
132217
color_map=None,
133218
show_element_id=False,
134219
show_element_type=False,
135220
id_font_size=None,
136221
id_font_path=None,
137222
id_text_color=None,
138223
id_text_background_color=None,
224+
id_text_background_alpha=1,
139225
):
140226
"""Draw the layout region on the input canvas(image).
141227
@@ -149,6 +235,10 @@ def draw_box(
149235
Defaults to None, when the boundary is automatically
150236
calculated as the the :const:`DEFAULT_BOX_WIDTH_RATIO`
151237
* the maximum of (height, width) of the canvas.
238+
box_alpha (:obj:`float`, optional):
239+
A float range from 0 to 1. Set to change the alpha of the
240+
drawn layout box.
241+
Defaults to 0 - the layout box will be fully transparent.
152242
color_map (dict, optional):
153243
A map from `block.type` to the colors, e.g., `{1: 'red'}`.
154244
You can set it to `{}` to use only the
@@ -178,12 +268,26 @@ def draw_box(
178268
Set to change the text region background used for drawing `block.id`.
179269
Defaults to None, when the color is set to
180270
:const:`DEFAULT_TEXT_BACKGROUND`.
271+
id_text_background_alpha (:obj:`float`, optional):
272+
A float range from 0 to 1. Set to change the alpha of the
273+
drawn text.
274+
Defaults to 1 - the text box will be solid.
181275
Returns:
182276
:obj:`PIL.Image.Image`:
183277
A Image object containing the `layout` draw upon the input `canvas`.
184278
"""
185279

186-
draw = ImageDraw.Draw(canvas)
280+
assert 0 <= box_alpha <= 1, ValueError(
281+
f"The box_alpha value {box_alpha} is not within range [0,1]."
282+
)
283+
assert 0 <= id_text_background_alpha <= 1, ValueError(
284+
f"The id_text_background_alpha value {id_text_background_alpha} is not within range [0,1]."
285+
)
286+
287+
draw = ImageDraw.Draw(canvas, mode="RGBA")
288+
289+
id_text_background_color = id_text_background_color or DEFAULT_TEXT_BACKGROUND
290+
id_text_color = id_text_color or DEFAULT_TEXT_COLOR
187291

188292
if box_width is None:
189293
box_width = _calculate_default_box_width(canvas)
@@ -206,12 +310,9 @@ def draw_box(
206310
else color_map.get(ele.type, DEFAULT_OUTLINE_COLOR)
207311
)
208312

209-
if not isinstance(ele, Quadrilateral):
210-
draw.rectangle(ele.coordinates, width=box_width, outline=outline_color)
313+
_draw_box_outline_on_handler(draw, ele, outline_color, box_width)
211314

212-
else:
213-
p = ele.points.ravel().tolist()
214-
draw.line(p + p[:2], width=box_width, fill=outline_color)
315+
_draw_transparent_box_on_handler(draw, ele, outline_color, box_alpha)
215316

216317
if show_element_id or show_element_type:
217318
text = ""
@@ -224,17 +325,23 @@ def draw_box(
224325
start_x, start_y = ele.coordinates[:2]
225326
text_w, text_h = font_obj.getsize(text)
226327

328+
text_box_object = Rectangle(
329+
start_x, start_y, start_x + text_w, start_y + text_h
330+
)
227331
# Add a small background for the text
228-
draw.rectangle(
229-
(start_x, start_y, start_x + text_w, start_y + text_h),
230-
fill=id_text_background_color or DEFAULT_TEXT_BACKGROUND,
332+
333+
_draw_transparent_box_on_handler(
334+
draw,
335+
text_box_object,
336+
id_text_background_color,
337+
id_text_background_alpha,
231338
)
232339

233340
# Draw the ids
234341
draw.text(
235342
(start_x, start_y),
236343
text,
237-
fill=id_text_color or DEFAULT_TEXT_COLOR,
344+
fill=id_text_color,
238345
font=font_obj,
239346
)
240347

@@ -250,10 +357,12 @@ def draw_text(
250357
font_path=None,
251358
text_color=None,
252359
text_background_color=None,
360+
text_background_alpha=1,
253361
vertical_text=False,
254362
with_box_on_text=False,
255363
text_box_width=None,
256364
text_box_color=None,
365+
text_box_alpha=0,
257366
with_layout=False,
258367
**kwargs,
259368
):
@@ -289,6 +398,10 @@ def draw_text(
289398
`block.text`.
290399
Defaults to None, when the color is set to
291400
:const:`DEFAULT_TEXT_BACKGROUND`.
401+
text_background_alpha (:obj:`float`, optional):
402+
A float range from 0 to 1. Set to change the alpha of the
403+
background of the canvas.
404+
Defaults to 1 - the text box will be solid.
292405
vertical_text (bool, optional):
293406
Whether the text in a block should be drawn vertically.
294407
Defaults to False.
@@ -301,10 +414,15 @@ def draw_text(
301414
Defaults to None, when the boundary is automatically
302415
calculated as the the :const:`DEFAULT_BOX_WIDTH_RATIO`
303416
* the maximum of (height, width) of the canvas.
417+
text_box_alpha (:obj:`float`, optional):
418+
A float range from 0 to 1. Set to change the alpha of the
419+
drawn text box.
420+
Defaults to 0 - the text box will be fully transparent.
304421
text_box_color (:obj:`int`, optional):
305422
Set to change the color of the drawn layout box boundary.
306423
Defaults to None, when the color is set to
307424
:const:`DEFAULT_OUTLINE_COLOR`.
425+
308426
with_layout (bool, optional):
309427
Whether to draw the layout boxes on the input (image) canvas.
310428
Defaults to False.
@@ -315,6 +433,14 @@ def draw_text(
315433
:obj:`PIL.Image.Image`:
316434
A Image object containing the drawn text from `layout`.
317435
"""
436+
437+
assert 0 <= text_background_alpha <= 1, ValueError(
438+
f"The text_background_color value {text_background_alpha} is not within range [0,1]."
439+
)
440+
assert 0 <= text_box_alpha <= 1, ValueError(
441+
f"The text_box_alpha value {text_box_alpha} is not within range [0,1]."
442+
)
443+
318444
if with_box_on_text:
319445
if text_box_width is None:
320446
text_box_width = _calculate_default_box_width(canvas)
@@ -327,29 +453,42 @@ def draw_text(
327453
text_color = text_color or DEFAULT_TEXT_COLOR
328454
text_background_color = text_background_color or DEFAULT_TEXT_BACKGROUND
329455

330-
canvas = _create_new_canvas(canvas, arrangement, text_background_color)
331-
draw = ImageDraw.Draw(canvas)
456+
canvas = _create_new_canvas(
457+
canvas,
458+
arrangement,
459+
_get_color_rgb(text_background_color, text_background_alpha),
460+
)
461+
draw = ImageDraw.Draw(canvas, "RGBA")
332462

333463
for idx, ele in enumerate(layout):
334464

335465
if with_box_on_text:
336-
p = (
337-
ele.pad(right=text_box_width, bottom=text_box_width)
338-
.points.ravel()
339-
.tolist()
340-
)
466+
modified_box = ele.pad(right=text_box_width, bottom=text_box_width)
341467

342-
draw.line(p + p[:2], width=text_box_width, fill=text_box_color)
468+
_draw_box_outline_on_handler(
469+
draw, modified_box, text_box_color, text_box_width
470+
)
471+
_draw_transparent_box_on_handler(
472+
draw, modified_box, text_box_color, text_box_alpha
473+
)
343474

344475
if not hasattr(ele, "text") or ele.text == "":
345476
continue
346477

347478
(start_x, start_y) = ele.coordinates[:2]
348479
if not vertical_text:
349-
draw.text((start_x, start_y), ele.text, font=font_obj, fill=text_color)
480+
draw.text(
481+
(start_x, start_y),
482+
ele.text,
483+
font=font_obj,
484+
fill=text_color,
485+
)
350486
else:
351487
text_segment = _draw_vertical_text(
352-
ele.text, font_obj, text_color, text_background_color
488+
ele.text,
489+
font_obj,
490+
text_color,
491+
_get_color_rgb(text_background_color, text_background_alpha),
353492
)
354493

355494
if with_box_on_text:

tests/test_visualization.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,14 +57,18 @@ def test_viz():
5757
font_size=15,
5858
text_color="pink",
5959
text_background_color="grey",
60+
text_background_alpha=0.1,
6061
with_box_on_text=True,
6162
text_box_width=2,
6263
text_box_color="yellow",
64+
text_box_alpha=0.2,
6365
with_layout=True,
6466
box_width=1,
6567
color_map={None: "blue"},
6668
show_element_id=True,
6769
id_font_size=8,
70+
box_alpha=0.25,
71+
id_text_background_alpha=0.25
6872
)
6973

7074
draw_box(image, layout)

0 commit comments

Comments
 (0)