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
42import functools
53import os
64import sys
75import warnings
8- import layoutparser
96from 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+
111171def 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
128212def 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 :
0 commit comments