custom-layoutparser 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. custom_layoutparser-0.1.0.dist-info/METADATA +5 -0
  2. custom_layoutparser-0.1.0.dist-info/RECORD +36 -0
  3. custom_layoutparser-0.1.0.dist-info/WHEEL +5 -0
  4. custom_layoutparser-0.1.0.dist-info/top_level.txt +1 -0
  5. layoutparser/__init__.py +89 -0
  6. layoutparser/elements/__init__.py +25 -0
  7. layoutparser/elements/base.py +275 -0
  8. layoutparser/elements/errors.py +26 -0
  9. layoutparser/elements/layout.py +348 -0
  10. layoutparser/elements/layout_elements.py +1352 -0
  11. layoutparser/elements/utils.py +82 -0
  12. layoutparser/file_utils.py +235 -0
  13. layoutparser/io/__init__.py +2 -0
  14. layoutparser/io/basic.py +148 -0
  15. layoutparser/io/pdf.py +225 -0
  16. layoutparser/models/__init__.py +18 -0
  17. layoutparser/models/auto_layoutmodel.py +70 -0
  18. layoutparser/models/base_catalog.py +34 -0
  19. layoutparser/models/base_layoutmodel.py +88 -0
  20. layoutparser/models/detectron2/__init__.py +18 -0
  21. layoutparser/models/detectron2/catalog.py +142 -0
  22. layoutparser/models/detectron2/layoutmodel.py +168 -0
  23. layoutparser/models/effdet/__init__.py +16 -0
  24. layoutparser/models/effdet/catalog.py +88 -0
  25. layoutparser/models/effdet/layoutmodel.py +256 -0
  26. layoutparser/models/model_config.py +133 -0
  27. layoutparser/models/paddledetection/__init__.py +17 -0
  28. layoutparser/models/paddledetection/catalog.py +214 -0
  29. layoutparser/models/paddledetection/layoutmodel.py +297 -0
  30. layoutparser/ocr/__init__.py +16 -0
  31. layoutparser/ocr/base.py +41 -0
  32. layoutparser/ocr/gcv_agent.py +288 -0
  33. layoutparser/ocr/tesseract_agent.py +193 -0
  34. layoutparser/tools/__init__.py +5 -0
  35. layoutparser/tools/shape_operations.py +167 -0
  36. layoutparser/visualization.py +571 -0
@@ -0,0 +1,1352 @@
1
+ # Copyright 2021 The Layout Parser team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from typing import List, Union, Dict, Dict, Any, Optional, Tuple
16
+ from collections.abc import Iterable
17
+ from copy import copy
18
+ from inspect import getmembers, isfunction
19
+ import warnings
20
+ import functools
21
+
22
+ import numpy as np
23
+ import pandas as pd
24
+ from PIL import Image
25
+ from cv2 import getPerspectiveTransform as _getPerspectiveTransform
26
+ from cv2 import warpPerspective as _warpPerspective
27
+
28
+ from .base import BaseCoordElement, BaseLayoutElement
29
+ from .utils import (
30
+ cvt_coordinates_to_points,
31
+ cvt_points_to_coordinates,
32
+ perspective_transformation,
33
+ vertice_in_polygon,
34
+ polygon_area,
35
+ )
36
+ from .errors import NotSupportedShapeError, InvalidShapeError
37
+
38
+
39
+ def mixin_textblock_meta(func):
40
+ @functools.wraps(func)
41
+ def wrap(self, *args, **kwargs):
42
+ out = func(self, *args, **kwargs)
43
+ if isinstance(out, BaseCoordElement):
44
+ self = copy(self)
45
+ self.block = out
46
+ return self
47
+
48
+ return wrap
49
+
50
+
51
+ def inherit_docstrings(cls=None, *, base_class=None):
52
+
53
+ # Refer to https://stackoverflow.com/a/17393254
54
+ if cls is None:
55
+ return functools.partial(inherit_docstrings, base_class=base_class)
56
+
57
+ for name, func in getmembers(cls, isfunction):
58
+ if func.__doc__:
59
+ continue
60
+ if base_class == None:
61
+ for parent in cls.__mro__[1:]:
62
+ if hasattr(parent, name):
63
+ func.__doc__ = getattr(parent, name).__doc__
64
+ break
65
+ else:
66
+ if hasattr(base_class, name):
67
+ func.__doc__ = getattr(base_class, name).__doc__
68
+
69
+ return cls
70
+
71
+
72
+ def support_textblock(func):
73
+ @functools.wraps(func)
74
+ def wrap(self, other, *args, **kwargs):
75
+ if isinstance(other, TextBlock):
76
+ other = other.block
77
+ out = func(self, other, *args, **kwargs)
78
+ return out
79
+
80
+ return wrap
81
+
82
+
83
+ @inherit_docstrings
84
+ class Interval(BaseCoordElement):
85
+ """
86
+ This class describes the coordinate system of an interval, a block defined by a pair of start and end point
87
+ on the designated axis and same length as the base canvas on the other axis.
88
+
89
+ Args:
90
+ start (:obj:`numeric`):
91
+ The coordinate of the start point on the designated axis.
92
+ end (:obj:`numeric`):
93
+ The end coordinate on the same axis as start.
94
+ axis (:obj:`str`):
95
+ The designated axis that the end points belong to.
96
+ canvas_height (:obj:`numeric`, `optional`, defaults to 0):
97
+ The height of the canvas that the interval is on.
98
+ canvas_width (:obj:`numeric`, `optional`, defaults to 0):
99
+ The width of the canvas that the interval is on.
100
+ """
101
+
102
+ _name = "interval"
103
+ _features = ["start", "end", "axis", "canvas_height", "canvas_width"]
104
+
105
+ def __init__(self, start, end, axis, canvas_height=None, canvas_width=None):
106
+
107
+ assert start <= end, f"Invalid input for start and end. Start must <= end."
108
+ self.start = start
109
+ self.end = end
110
+
111
+ assert axis in ["x", "y"], f"Invalid axis {axis}. Axis must be in 'x' or 'y'"
112
+ self.axis = axis
113
+
114
+ self.canvas_height = canvas_height or 0
115
+ self.canvas_width = canvas_width or 0
116
+
117
+ @property
118
+ def height(self):
119
+ """
120
+ Calculate the height of the interval. If the interval is along the x-axis, the height will be the
121
+ height of the canvas, otherwise, it will be the difference between the start and end point.
122
+
123
+ Returns:
124
+ :obj:`numeric`: Output the numeric value of the height.
125
+ """
126
+
127
+ if self.axis == "x":
128
+ return self.canvas_height
129
+ else:
130
+ return self.end - self.start
131
+
132
+ @property
133
+ def width(self):
134
+ """
135
+ Calculate the width of the interval. If the interval is along the y-axis, the width will be the
136
+ width of the canvas, otherwise, it will be the difference between the start and end point.
137
+
138
+ Returns:
139
+ :obj:`numeric`: Output the numeric value of the width.
140
+ """
141
+
142
+ if self.axis == "y":
143
+ return self.canvas_width
144
+ else:
145
+ return self.end - self.start
146
+
147
+ @property
148
+ def coordinates(self):
149
+ """
150
+ This method considers an interval as a rectangle and calculates the coordinates of the upper left
151
+ and lower right corners to define the interval.
152
+
153
+ Returns:
154
+ :obj:`Tuple(numeric)`:
155
+ Output the numeric values of the coordinates in a Tuple of size four.
156
+ """
157
+
158
+ if self.axis == "x":
159
+ coords = (self.start, 0, self.end, self.canvas_height)
160
+ else:
161
+ coords = (0, self.start, self.canvas_width, self.end)
162
+
163
+ return coords
164
+
165
+ @property
166
+ def points(self):
167
+ """
168
+ Return the coordinates of all four corners of the interval in a clockwise fashion
169
+ starting from the upper left.
170
+
171
+ Returns:
172
+ :obj:`Numpy array`: A Numpy array of shape 4x2 containing the coordinates.
173
+ """
174
+
175
+ return cvt_coordinates_to_points(self.coordinates)
176
+
177
+ @property
178
+ def center(self):
179
+ """
180
+ Calculate the mid-point between the start and end point.
181
+
182
+ Returns:
183
+ :obj:`Tuple(numeric)`: Returns of coordinate of the center.
184
+ """
185
+
186
+ return (self.start + self.end) / 2.0
187
+
188
+ @property
189
+ def area(self):
190
+ """Return the area of the covered region of the interval.
191
+ The area is bounded to the canvas. If the interval is put
192
+ on a canvas, the area equals to interval width * canvas height
193
+ (axis='x') or interval height * canvas width (axis='y').
194
+ Otherwise, the area is zero.
195
+ """
196
+ return self.height * self.width
197
+
198
+ def put_on_canvas(self, canvas):
199
+ """
200
+ Set the height and the width of the canvas that the interval is on.
201
+
202
+ Args:
203
+ canvas (:obj:`Numpy array` or :obj:`BaseCoordElement` or :obj:`PIL.Image.Image`):
204
+ The base element that the interval is on. The numpy array should be the
205
+ format of `[height, width]`.
206
+
207
+ Returns:
208
+ :obj:`Interval`:
209
+ A copy of the current Interval with its canvas height and width set to
210
+ those of the input canvas.
211
+ """
212
+
213
+ if isinstance(canvas, np.ndarray):
214
+ h, w = canvas.shape[:2]
215
+ elif isinstance(canvas, BaseCoordElement):
216
+ h, w = canvas.height, canvas.width
217
+ elif isinstance(canvas, Image.Image):
218
+ w, h = canvas.size
219
+ else:
220
+ raise NotImplementedError
221
+
222
+ return self.set(canvas_height=h, canvas_width=w)
223
+
224
+ @support_textblock
225
+ def condition_on(self, other):
226
+
227
+ if isinstance(other, Interval):
228
+ if other.axis == self.axis:
229
+ d = other.start
230
+ # Reset the canvas size in the absolute coordinates
231
+ return self.__class__(self.start + d, self.end + d, self.axis)
232
+ else:
233
+ return copy(self)
234
+
235
+ elif isinstance(other, Rectangle):
236
+
237
+ return self.put_on_canvas(other).to_rectangle().condition_on(other)
238
+
239
+ elif isinstance(other, Quadrilateral):
240
+
241
+ return self.put_on_canvas(other).to_quadrilateral().condition_on(other)
242
+
243
+ else:
244
+ raise Exception(f"Invalid input type {other.__class__} for other")
245
+
246
+ @support_textblock
247
+ def relative_to(self, other):
248
+
249
+ if isinstance(other, Interval):
250
+ if other.axis == self.axis:
251
+ d = other.start
252
+ # Reset the canvas size in the absolute coordinates
253
+ return self.__class__(self.start - d, self.end - d, self.axis)
254
+ else:
255
+ return copy(self)
256
+
257
+ elif isinstance(other, Rectangle):
258
+
259
+ return self.put_on_canvas(other).to_rectangle().relative_to(other)
260
+
261
+ elif isinstance(other, Quadrilateral):
262
+
263
+ return self.put_on_canvas(other).to_quadrilateral().relative_to(other)
264
+
265
+ else:
266
+ raise Exception(f"Invalid input type {other.__class__} for other")
267
+
268
+ @support_textblock
269
+ def is_in(self, other, soft_margin={}, center=False):
270
+
271
+ other = other.pad(**soft_margin)
272
+
273
+ if isinstance(other, Interval):
274
+ if self.axis != other.axis:
275
+ return False
276
+ else:
277
+ if not center:
278
+ return other.start <= self.start <= self.end <= other.end
279
+ else:
280
+ return other.start <= self.center <= other.end
281
+
282
+ elif isinstance(other, Rectangle) or isinstance(other, Quadrilateral):
283
+ x_1, y_1, x_2, y_2 = other.coordinates
284
+
285
+ if center:
286
+ if self.axis == "x":
287
+ return x_1 <= self.center <= x_2
288
+ else:
289
+ return y_1 <= self.center <= y_2
290
+ else:
291
+ if self.axis == "x":
292
+ return x_1 <= self.start <= self.end <= x_2
293
+ else:
294
+ return y_1 <= self.start <= self.end <= y_2
295
+
296
+ else:
297
+ raise Exception(f"Invalid input type {other.__class__} for other")
298
+
299
+ @support_textblock
300
+ def intersect(self, other: BaseCoordElement, strict: bool = True):
301
+ """"""
302
+
303
+ if isinstance(other, Interval):
304
+ if self.axis != other.axis:
305
+ if self.axis == "x" and other.axis == "y":
306
+ return Rectangle(self.start, other.start, self.end, other.end)
307
+ else:
308
+ return Rectangle(other.start, self.start, other.end, self.end)
309
+ else:
310
+ return self.__class__(
311
+ max(self.start, other.start),
312
+ min(self.end, other.end),
313
+ self.axis,
314
+ self.canvas_height,
315
+ self.canvas_width,
316
+ )
317
+
318
+ elif isinstance(other, Rectangle):
319
+ x_1, y_1, x_2, y_2 = other.coordinates
320
+ if self.axis == "x":
321
+ return Rectangle(max(x_1, self.start), y_1, min(x_2, self.end), y_2)
322
+ elif self.axis == "y":
323
+ return Rectangle(x_1, max(y_1, self.start), x_2, min(y_2, self.end))
324
+
325
+ elif isinstance(other, Quadrilateral):
326
+ if strict:
327
+ raise NotSupportedShapeError(
328
+ "The intersection between an Interval and a Quadrilateral might generate Polygon shapes that are not supported in the current version of layoutparser. You can pass `strict=False` in the input that converts the Quadrilateral to Rectangle to avoid this Exception."
329
+ )
330
+ else:
331
+ warnings.warn(
332
+ f"With `strict=False`, the other of shape {other.__class__} will be converted to {Rectangle} for obtaining the intersection"
333
+ )
334
+ return self.intersect(other.to_rectangle())
335
+
336
+ else:
337
+ raise Exception(f"Invalid input type {other.__class__} for other")
338
+
339
+ @support_textblock
340
+ def union(self, other: BaseCoordElement, strict: bool = True):
341
+ """"""
342
+ if isinstance(other, Interval):
343
+ if self.axis != other.axis:
344
+ raise InvalidShapeError(
345
+ f"Unioning two intervals of different axes is not allowed."
346
+ )
347
+ else:
348
+ return self.__class__(
349
+ min(self.start, other.start),
350
+ max(self.end, other.end),
351
+ self.axis,
352
+ self.canvas_height,
353
+ self.canvas_width,
354
+ )
355
+
356
+ elif isinstance(other, Rectangle):
357
+ x_1, y_1, x_2, y_2 = other.coordinates
358
+ if self.axis == "x":
359
+ return Rectangle(min(x_1, self.start), y_1, max(x_2, self.end), y_2)
360
+ elif self.axis == "y":
361
+ return Rectangle(x_1, min(y_1, self.start), x_2, max(y_2, self.end))
362
+
363
+ elif isinstance(other, Quadrilateral):
364
+ if strict:
365
+ raise NotSupportedShapeError(
366
+ "The intersection between an Interval and a Quadrilateral might generate Polygon shapes that are not supported in the current version of layoutparser. You can pass `strict=False` in the input that converts the Quadrilateral to Rectangle to avoid this Exception."
367
+ )
368
+ else:
369
+ warnings.warn(
370
+ f"With `strict=False`, the other of shape {other.__class__} will be converted to {Rectangle} for obtaining the intersection"
371
+ )
372
+ return self.union(other.to_rectangle())
373
+
374
+ else:
375
+ raise Exception(f"Invalid input type {other.__class__} for other")
376
+
377
+ def pad(self, left=0, right=0, top=0, bottom=0, safe_mode=True):
378
+
379
+ if self.axis == "x":
380
+ start = self.start - left
381
+ end = self.end + right
382
+ if top or bottom:
383
+ warnings.warn(
384
+ f"Invalid padding top/bottom for an x axis {self.__class__.__name__}"
385
+ )
386
+ else:
387
+ start = self.start - top
388
+ end = self.end + bottom
389
+ if left or right:
390
+ warnings.warn(
391
+ f"Invalid padding right/left for a y axis {self.__class__.__name__}"
392
+ )
393
+
394
+ if safe_mode:
395
+ start = max(0, start)
396
+
397
+ return self.set(start=start, end=end)
398
+
399
+ def shift(self, shift_distance):
400
+ """
401
+ Shift the interval by a user specified amount along the same axis that the interval is defined on.
402
+
403
+ Args:
404
+ shift_distance (:obj:`numeric`): The number of pixels used to shift the interval.
405
+
406
+ Returns:
407
+ :obj:`BaseCoordElement`: The shifted Interval object.
408
+ """
409
+
410
+ if isinstance(shift_distance, Iterable):
411
+ shift_distance = (
412
+ shift_distance[0] if self.axis == "x" else shift_distance[1]
413
+ )
414
+ warnings.warn(
415
+ f"Input shift for multiple axes. Only use the distance for the {self.axis} axis"
416
+ )
417
+
418
+ start = self.start + shift_distance
419
+ end = self.end + shift_distance
420
+ return self.set(start=start, end=end)
421
+
422
+ def scale(self, scale_factor):
423
+ """
424
+ Scale the layout element by a user specified amount the same axis that the interval is defined on.
425
+
426
+ Args:
427
+ scale_factor (:obj:`numeric`): The amount for downscaling or upscaling the element.
428
+
429
+ Returns:
430
+ :obj:`BaseCoordElement`: The scaled Interval object.
431
+ """
432
+
433
+ if isinstance(scale_factor, Iterable):
434
+ scale_factor = scale_factor[0] if self.axis == "x" else scale_factor[1]
435
+ warnings.warn(
436
+ f"Input scale for multiple axes. Only use the factor for the {self.axis} axis"
437
+ )
438
+
439
+ start = self.start * scale_factor
440
+ end = self.end * scale_factor
441
+ return self.set(start=start, end=end)
442
+
443
+ def crop_image(self, image):
444
+ x_1, y_1, x_2, y_2 = self.put_on_canvas(image).coordinates
445
+ return image[int(y_1) : int(y_2), int(x_1) : int(x_2)]
446
+
447
+ def to_rectangle(self):
448
+ """
449
+ Convert the Interval to a Rectangle element.
450
+
451
+ Returns:
452
+ :obj:`Rectangle`: The converted Rectangle object.
453
+ """
454
+ return Rectangle(*self.coordinates)
455
+
456
+ def to_quadrilateral(self):
457
+ """
458
+ Convert the Interval to a Quadrilateral element.
459
+
460
+ Returns:
461
+ :obj:`Quadrilateral`: The converted Quadrilateral object.
462
+ """
463
+ return Quadrilateral(self.points)
464
+
465
+
466
+ @inherit_docstrings
467
+ class Rectangle(BaseCoordElement):
468
+ """
469
+ This class describes the coordinate system of an axial rectangle box using two points as indicated below::
470
+
471
+ (x_1, y_1) ----
472
+ | |
473
+ | |
474
+ | |
475
+ ---- (x_2, y_2)
476
+
477
+ Args:
478
+ x_1 (:obj:`numeric`):
479
+ x coordinate on the horizontal axis of the upper left corner of the rectangle.
480
+ y_1 (:obj:`numeric`):
481
+ y coordinate on the vertical axis of the upper left corner of the rectangle.
482
+ x_2 (:obj:`numeric`):
483
+ x coordinate on the horizontal axis of the lower right corner of the rectangle.
484
+ y_2 (:obj:`numeric`):
485
+ y coordinate on the vertical axis of the lower right corner of the rectangle.
486
+ """
487
+
488
+ _name = "rectangle"
489
+ _features = ["x_1", "y_1", "x_2", "y_2"]
490
+
491
+ def __init__(self, x_1, y_1, x_2, y_2):
492
+
493
+ self.x_1 = x_1
494
+ self.y_1 = y_1
495
+ self.x_2 = x_2
496
+ self.y_2 = y_2
497
+
498
+ @property
499
+ def height(self):
500
+ """
501
+ Calculate the height of the rectangle.
502
+
503
+ Returns:
504
+ :obj:`numeric`: Output the numeric value of the height.
505
+ """
506
+
507
+ return self.y_2 - self.y_1
508
+
509
+ @property
510
+ def width(self):
511
+ """
512
+ Calculate the width of the rectangle.
513
+
514
+ Returns:
515
+ :obj:`numeric`: Output the numeric value of the width.
516
+ """
517
+
518
+ return self.x_2 - self.x_1
519
+
520
+ @property
521
+ def coordinates(self):
522
+ """
523
+ Return the coordinates of the two points that define the rectangle.
524
+
525
+ Returns:
526
+ :obj:`Tuple(numeric)`: Output the numeric values of the coordinates in a Tuple of size four.
527
+ """
528
+
529
+ return (self.x_1, self.y_1, self.x_2, self.y_2)
530
+
531
+ @property
532
+ def points(self):
533
+ """
534
+ Return the coordinates of all four corners of the rectangle in a clockwise fashion
535
+ starting from the upper left.
536
+
537
+ Returns:
538
+ :obj:`Numpy array`: A Numpy array of shape 4x2 containing the coordinates.
539
+ """
540
+
541
+ return cvt_coordinates_to_points(self.coordinates)
542
+
543
+ @property
544
+ def center(self):
545
+ """
546
+ Calculate the center of the rectangle.
547
+
548
+ Returns:
549
+ :obj:`Tuple(numeric)`: Returns of coordinate of the center.
550
+ """
551
+
552
+ return (self.x_1 + self.x_2) / 2.0, (self.y_1 + self.y_2) / 2.0
553
+
554
+ @property
555
+ def area(self):
556
+ """
557
+ Return the area of the rectangle.
558
+ """
559
+ return self.width * self.height
560
+
561
+ @support_textblock
562
+ def condition_on(self, other):
563
+
564
+ if isinstance(other, Interval):
565
+ if other.axis == "x":
566
+ dx, dy = other.start, 0
567
+ else:
568
+ dx, dy = 0, other.start
569
+
570
+ return self.__class__(
571
+ self.x_1 + dx, self.y_1 + dy, self.x_2 + dx, self.y_2 + dy
572
+ )
573
+
574
+ elif isinstance(other, Rectangle):
575
+ dx, dy, _, _ = other.coordinates
576
+
577
+ return self.__class__(
578
+ self.x_1 + dx, self.y_1 + dy, self.x_2 + dx, self.y_2 + dy
579
+ )
580
+
581
+ elif isinstance(other, Quadrilateral):
582
+ transformed_points = perspective_transformation(
583
+ other.perspective_matrix, self.points, is_inv=True
584
+ )
585
+
586
+ return other.__class__(transformed_points, self.height, self.width)
587
+
588
+ else:
589
+ raise Exception(f"Invalid input type {other.__class__} for other")
590
+
591
+ @support_textblock
592
+ def relative_to(self, other):
593
+ if isinstance(other, Interval):
594
+ if other.axis == "x":
595
+ dx, dy = other.start, 0
596
+ else:
597
+ dx, dy = 0, other.start
598
+
599
+ return self.__class__(
600
+ self.x_1 - dx, self.y_1 - dy, self.x_2 - dx, self.y_2 - dy
601
+ )
602
+
603
+ elif isinstance(other, Rectangle):
604
+ dx, dy, _, _ = other.coordinates
605
+
606
+ return self.__class__(
607
+ self.x_1 - dx, self.y_1 - dy, self.x_2 - dx, self.y_2 - dy
608
+ )
609
+
610
+ elif isinstance(other, Quadrilateral):
611
+ transformed_points = perspective_transformation(
612
+ other.perspective_matrix, self.points, is_inv=False
613
+ )
614
+
615
+ return other.__class__(transformed_points, self.height, self.width)
616
+
617
+ else:
618
+ raise Exception(f"Invalid input type {other.__class__} for other")
619
+
620
+ @support_textblock
621
+ def is_in(self, other, soft_margin={}, center=False):
622
+
623
+ other = other.pad(**soft_margin)
624
+
625
+ if isinstance(other, Interval):
626
+ if not center:
627
+ if other.axis == "x":
628
+ start, end = self.x_1, self.x_2
629
+ else:
630
+ start, end = self.y_1, self.y_2
631
+ return other.start <= start <= end <= other.end
632
+ else:
633
+ c = self.center[0] if other.axis == "x" else self.center[1]
634
+ return other.start <= c <= other.end
635
+
636
+ elif isinstance(other, Rectangle):
637
+ x_interval = other.to_interval(axis="x")
638
+ y_interval = other.to_interval(axis="y")
639
+ return self.is_in(x_interval, center=center) and self.is_in(
640
+ y_interval, center=center
641
+ )
642
+
643
+ elif isinstance(other, Quadrilateral):
644
+
645
+ if not center:
646
+ # This is equivalent to determine all the points of the
647
+ # rectangle is in the quadrilateral.
648
+ is_vertice_in = [
649
+ vertice_in_polygon(vertice, other.points) for vertice in self.points
650
+ ]
651
+ return all(is_vertice_in)
652
+ else:
653
+ center = np.array(self.center)
654
+ return vertice_in_polygon(center, other.points)
655
+
656
+ else:
657
+ raise Exception(f"Invalid input type {other.__class__} for other")
658
+
659
+ @support_textblock
660
+ def intersect(self, other: BaseCoordElement, strict: bool = True):
661
+ """"""
662
+
663
+ if isinstance(other, Interval):
664
+ return other.intersect(self)
665
+
666
+ elif isinstance(other, Rectangle):
667
+
668
+ return self.__class__(
669
+ max(self.x_1, other.x_1),
670
+ max(self.y_1, other.y_1),
671
+ min(self.x_2, other.x_2),
672
+ min(self.y_2, other.y_2),
673
+ )
674
+
675
+ elif isinstance(other, Quadrilateral):
676
+ if strict:
677
+ raise NotSupportedShapeError(
678
+ "The intersection between a Rectangle and a Quadrilateral might generate Polygon shapes that are not supported in the current version of layoutparser. You can pass `strict=False` in the input that converts the Quadrilateral to Rectangle to avoid this Exception."
679
+ )
680
+ else:
681
+ warnings.warn(
682
+ f"With `strict=False`, the other of shape {other.__class__} will be converted to {Rectangle} for obtaining the intersection"
683
+ )
684
+ return self.intersect(other.to_rectangle())
685
+
686
+ else:
687
+ raise Exception(f"Invalid input type {other.__class__} for other")
688
+
689
+ @support_textblock
690
+ def union(self, other: BaseCoordElement, strict: bool = True):
691
+ """"""
692
+ if isinstance(other, Interval):
693
+ return other.intersect(self)
694
+
695
+ elif isinstance(other, Rectangle):
696
+ return self.__class__(
697
+ min(self.x_1, other.x_1),
698
+ min(self.y_1, other.y_1),
699
+ max(self.x_2, other.x_2),
700
+ max(self.y_2, other.y_2),
701
+ )
702
+
703
+ elif isinstance(other, Quadrilateral):
704
+ if strict:
705
+ raise NotSupportedShapeError(
706
+ "The intersection between an Interval and a Quadrilateral might generate Polygon shapes that are not supported in the current version of layoutparser. You can pass `strict=False` in the input that converts the Quadrilateral to Rectangle to avoid this Exception."
707
+ )
708
+ else:
709
+ warnings.warn(
710
+ f"With `strict=False`, the other of shape {other.__class__} will be converted to {Rectangle} for obtaining the intersection"
711
+ )
712
+ return self.union(other.to_rectangle())
713
+
714
+ else:
715
+ raise Exception(f"Invalid input type {other.__class__} for other")
716
+
717
+ def pad(self, left=0, right=0, top=0, bottom=0, safe_mode=True):
718
+
719
+ x_1 = self.x_1 - left
720
+ y_1 = self.y_1 - top
721
+ x_2 = self.x_2 + right
722
+ y_2 = self.y_2 + bottom
723
+
724
+ if safe_mode:
725
+ x_1 = max(0, x_1)
726
+ y_1 = max(0, y_1)
727
+
728
+ return self.__class__(x_1, y_1, x_2, y_2)
729
+
730
+ def shift(self, shift_distance=0):
731
+
732
+ if not isinstance(shift_distance, Iterable):
733
+ shift_x = shift_distance
734
+ shift_y = shift_distance
735
+ else:
736
+ assert (
737
+ len(shift_distance) == 2
738
+ ), "shift_distance should have 2 elements, one for x dimension and one for y dimension"
739
+ shift_x, shift_y = shift_distance
740
+
741
+ x_1 = self.x_1 + shift_x
742
+ y_1 = self.y_1 + shift_y
743
+ x_2 = self.x_2 + shift_x
744
+ y_2 = self.y_2 + shift_y
745
+ return self.__class__(x_1, y_1, x_2, y_2)
746
+
747
+ def scale(self, scale_factor=1):
748
+
749
+ if not isinstance(scale_factor, Iterable):
750
+ scale_x = scale_factor
751
+ scale_y = scale_factor
752
+ else:
753
+ assert (
754
+ len(scale_factor) == 2
755
+ ), "scale_factor should have 2 elements, one for x dimension and one for y dimension"
756
+ scale_x, scale_y = scale_factor
757
+
758
+ x_1 = self.x_1 * scale_x
759
+ y_1 = self.y_1 * scale_y
760
+ x_2 = self.x_2 * scale_x
761
+ y_2 = self.y_2 * scale_y
762
+ return self.__class__(x_1, y_1, x_2, y_2)
763
+
764
+ def crop_image(self, image):
765
+ x_1, y_1, x_2, y_2 = self.coordinates
766
+ return image[int(y_1) : int(y_2), int(x_1) : int(x_2)]
767
+
768
+ def to_interval(self, axis, **kwargs):
769
+ if axis == "x":
770
+ start, end = self.x_1, self.x_2
771
+ else:
772
+ start, end = self.y_1, self.y_2
773
+
774
+ return Interval(start, end, axis=axis, **kwargs)
775
+
776
+ def to_quadrilateral(self):
777
+ return Quadrilateral(self.points)
778
+
779
+
780
+ @inherit_docstrings
781
+ class Quadrilateral(BaseCoordElement):
782
+ """
783
+ This class describes the coodinate system of a four-sided polygon. A quadrilateral is defined by
784
+ the coordinates of its 4 corners in a clockwise order starting with the upper left corner (as shown below)::
785
+
786
+ points[0] -...- points[1]
787
+ | |
788
+ . .
789
+ . .
790
+ . .
791
+ | |
792
+ points[3] -...- points[2]
793
+
794
+ Args:
795
+ points (:obj:`Numpy array` or `list`):
796
+ A `np.ndarray` of shape 4x2 for four corner coordinates
797
+ or a list of length 8 for in the format of
798
+ `[p0_x, p0_y, p1_x, p1_y, p2_x, p2_y, p3_x, p3_y]`
799
+ or a list of length 4 in the format of
800
+ `[[p0_x, p0_y], [p1_x, p1_y], [p2_x, p2_y], [p3_x, p3_y]]`.
801
+ height (:obj:`numeric`, `optional`, defaults to `None`):
802
+ The height of the quadrilateral. This is to better support the perspective
803
+ transformation from the OpenCV library.
804
+ width (:obj:`numeric`, `optional`, defaults to `None`):
805
+ The width of the quadrilateral. Similarly as height, this is to better support the perspective
806
+ transformation from the OpenCV library.
807
+ """
808
+
809
+ _name = "quadrilateral"
810
+ _features = ["points", "height", "width"]
811
+
812
+ def __init__(
813
+ self, points: Union[np.ndarray, List, List[List]], height=None, width=None
814
+ ):
815
+
816
+ if isinstance(points, np.ndarray):
817
+ if points.shape != (4, 2):
818
+ raise ValueError(f"Invalid points shape: {points.shape}.")
819
+ elif isinstance(points, list):
820
+ if len(points) == 8:
821
+ points = np.array(points).reshape(4, 2)
822
+ elif len(points) == 4 and isinstance(points[0], list):
823
+ points = np.array(points)
824
+ else:
825
+ raise ValueError(
826
+ f"Invalid number of points element {len(points)}. Should be 8."
827
+ )
828
+ else:
829
+ raise ValueError(
830
+ f"Invalid input type for points {type(points)}."
831
+ "Please make sure it is a list of np.ndarray."
832
+ )
833
+
834
+ self._points = points
835
+ self._width = width
836
+ self._height = height
837
+
838
+ @property
839
+ def height(self):
840
+ """
841
+ Return the user defined height, otherwise the height of its circumscribed rectangle.
842
+
843
+ Returns:
844
+ :obj:`numeric`: Output the numeric value of the height.
845
+ """
846
+
847
+ if self._height is not None:
848
+ return self._height
849
+ return self.points[:, 1].max() - self.points[:, 1].min()
850
+
851
+ @property
852
+ def width(self):
853
+ """
854
+ Return the user defined width, otherwise the width of its circumscribed rectangle.
855
+
856
+ Returns:
857
+ :obj:`numeric`: Output the numeric value of the width.
858
+ """
859
+
860
+ if self._width is not None:
861
+ return self._width
862
+ return self.points[:, 0].max() - self.points[:, 0].min()
863
+
864
+ @property
865
+ def coordinates(self):
866
+ """
867
+ Return the coordinates of the upper left and lower right corners points that
868
+ define the circumscribed rectangle.
869
+
870
+ Returns
871
+ :obj:`Tuple(numeric)`: Output the numeric values of the coordinates in a Tuple of size four.
872
+ """
873
+
874
+ return cvt_points_to_coordinates(self.points)
875
+
876
+ @property
877
+ def points(self):
878
+ """
879
+ Return the coordinates of all four corners of the quadrilateral in a clockwise fashion
880
+ starting from the upper left.
881
+
882
+ Returns:
883
+ :obj:`Numpy array`: A Numpy array of shape 4x2 containing the coordinates.
884
+ """
885
+
886
+ return self._points
887
+
888
+ @property
889
+ def center(self):
890
+ """
891
+ Calculate the center of the quadrilateral.
892
+
893
+ Returns:
894
+ :obj:`Tuple(numeric)`: Returns of coordinate of the center.
895
+ """
896
+
897
+ return tuple(self.points.mean(axis=0).tolist())
898
+
899
+ @property
900
+ def area(self):
901
+ """
902
+ Return the area of the quadrilateral.
903
+ """
904
+ return polygon_area(self.points[:, 0], self.points[:, 1])
905
+
906
+ @property
907
+ def mapped_rectangle_points(self):
908
+
909
+ x_map = {0: 0, 1: 0, 2: self.width, 3: self.width}
910
+ y_map = {0: 0, 1: 0, 2: self.height, 3: self.height}
911
+
912
+ return self.map_to_points_ordering(x_map, y_map)
913
+
914
+ @property
915
+ def perspective_matrix(self):
916
+ return _getPerspectiveTransform(
917
+ self.points.astype("float32"),
918
+ self.mapped_rectangle_points.astype("float32"),
919
+ )
920
+
921
+ def map_to_points_ordering(self, x_map, y_map):
922
+
923
+ points_ordering = self.points.argsort(axis=0).argsort(axis=0)
924
+ # Ref: https://github.com/numpy/numpy/issues/8757#issuecomment-355126992
925
+
926
+ return np.vstack(
927
+ [
928
+ np.vectorize(x_map.get)(points_ordering[:, 0]),
929
+ np.vectorize(y_map.get)(points_ordering[:, 1]),
930
+ ]
931
+ ).T
932
+
933
+ @support_textblock
934
+ def condition_on(self, other):
935
+
936
+ if isinstance(other, Interval):
937
+
938
+ if other.axis == "x":
939
+ return self.shift([other.start, 0])
940
+ else:
941
+ return self.shift([0, other.start])
942
+
943
+ elif isinstance(other, Rectangle):
944
+
945
+ return self.shift([other.x_1, other.y_1])
946
+
947
+ elif isinstance(other, Quadrilateral):
948
+
949
+ transformed_points = perspective_transformation(
950
+ other.perspective_matrix, self.points, is_inv=True
951
+ )
952
+ return self.__class__(transformed_points, self.height, self.width)
953
+
954
+ else:
955
+ raise Exception(f"Invalid input type {other.__class__} for other")
956
+
957
+ @support_textblock
958
+ def relative_to(self, other):
959
+
960
+ if isinstance(other, Interval):
961
+
962
+ if other.axis == "x":
963
+ return self.shift([-other.start, 0])
964
+ else:
965
+ return self.shift([0, -other.start])
966
+
967
+ elif isinstance(other, Rectangle):
968
+
969
+ return self.shift([-other.x_1, -other.y_1])
970
+
971
+ elif isinstance(other, Quadrilateral):
972
+
973
+ transformed_points = perspective_transformation(
974
+ other.perspective_matrix, self.points, is_inv=False
975
+ )
976
+ return self.__class__(transformed_points, self.height, self.width)
977
+
978
+ else:
979
+ raise Exception(f"Invalid input type {other.__class__} for other")
980
+
981
+ @support_textblock
982
+ def is_in(self, other, soft_margin={}, center=False):
983
+
984
+ other = other.pad(**soft_margin)
985
+
986
+ if isinstance(other, Interval):
987
+ if not center:
988
+ if other.axis == "x":
989
+ start, end = self.coordinates[0], self.coordinates[2]
990
+ else:
991
+ start, end = self.coordinates[1], self.coordinates[3]
992
+ return other.start <= start <= end <= other.end
993
+ else:
994
+ c = self.center[0] if other.axis == "x" else self.center[1]
995
+ return other.start <= c <= other.end
996
+
997
+ elif isinstance(other, Rectangle):
998
+ x_interval = other.to_interval(axis="x")
999
+ y_interval = other.to_interval(axis="y")
1000
+ return self.is_in(x_interval, center=center) and self.is_in(
1001
+ y_interval, center=center
1002
+ )
1003
+
1004
+ elif isinstance(other, Quadrilateral):
1005
+
1006
+ if not center:
1007
+ # This is equivalent to determine all the points of the
1008
+ # rectangle is in the quadrilateral.
1009
+ is_vertice_in = [
1010
+ vertice_in_polygon(vertice, other.points) for vertice in self.points
1011
+ ]
1012
+ return all(is_vertice_in)
1013
+ else:
1014
+ center = np.array(self.center)
1015
+ return vertice_in_polygon(center, other.points)
1016
+
1017
+ else:
1018
+ raise Exception(f"Invalid input type {other.__class__} for other")
1019
+
1020
+ @support_textblock
1021
+ def intersect(self, other: BaseCoordElement, strict: bool = True):
1022
+ """"""
1023
+
1024
+ if strict:
1025
+ raise NotSupportedShapeError(
1026
+ "The intersection between a Quadrilateral and other objects might generate Polygon shapes that are not supported in the current version of layoutparser. You can pass `strict=False` in the input that converts the Quadrilateral to Rectangle to avoid this Exception."
1027
+ )
1028
+ else:
1029
+ if isinstance(other, Interval) or isinstance(other, Rectangle):
1030
+ warnings.warn(
1031
+ f"With `strict=False`, the current Quadrilateral object will be converted to {Rectangle} for obtaining the intersection"
1032
+ )
1033
+ return other.intersect(self.to_rectangle())
1034
+ elif isinstance(other, Quadrilateral):
1035
+ warnings.warn(
1036
+ f"With `strict=False`, both input Quadrilateral objects will be converted to {Rectangle} for obtaining the intersection"
1037
+ )
1038
+ return self.to_rectangle().intersect(other.to_rectangle())
1039
+ else:
1040
+ raise Exception(f"Invalid input type {other.__class__} for other")
1041
+
1042
+ @support_textblock
1043
+ def union(self, other: BaseCoordElement, strict: bool = True):
1044
+ """"""
1045
+ if strict:
1046
+ raise NotSupportedShapeError(
1047
+ "The intersection between a Quadrilateral and other objects might generate Polygon shapes that are not supported in the current version of layoutparser. You can pass `strict=False` in the input that converts the Quadrilateral to Rectangle to avoid this Exception."
1048
+ )
1049
+ else:
1050
+ if isinstance(other, Interval) or isinstance(other, Rectangle):
1051
+ warnings.warn(
1052
+ f"With `strict=False`, the current Quadrilateral object will be converted to {Rectangle} for obtaining the intersection"
1053
+ )
1054
+ return other.union(self.to_rectangle())
1055
+ elif isinstance(other, Quadrilateral):
1056
+ warnings.warn(
1057
+ f"With `strict=False`, both input Quadrilateral objects will be converted to {Rectangle} for obtaining the intersection"
1058
+ )
1059
+ return self.to_rectangle().union(other.to_rectangle())
1060
+ else:
1061
+ raise Exception(f"Invalid input type {other.__class__} for other")
1062
+
1063
+ def pad(self, left=0, right=0, top=0, bottom=0, safe_mode=True):
1064
+
1065
+ x_map = {0: -left, 1: -left, 2: right, 3: right}
1066
+ y_map = {0: -top, 1: -top, 2: bottom, 3: bottom}
1067
+
1068
+ padding_mat = self.map_to_points_ordering(x_map, y_map)
1069
+
1070
+ points = self.points + padding_mat
1071
+ if safe_mode:
1072
+ points = np.maximum(points, 0)
1073
+
1074
+ return self.set(points=points)
1075
+
1076
+ def shift(self, shift_distance=0):
1077
+
1078
+ if not isinstance(shift_distance, Iterable):
1079
+ shift_mat = [shift_distance, shift_distance]
1080
+ else:
1081
+ assert (
1082
+ len(shift_distance) == 2
1083
+ ), "shift_distance should have 2 elements, one for x dimension and one for y dimension"
1084
+ shift_mat = shift_distance
1085
+
1086
+ points = self.points + np.array(shift_mat)
1087
+
1088
+ return self.set(points=points)
1089
+
1090
+ def scale(self, scale_factor=1):
1091
+
1092
+ if not isinstance(scale_factor, Iterable):
1093
+ scale_mat = [scale_factor, scale_factor]
1094
+ else:
1095
+ assert (
1096
+ len(scale_factor) == 2
1097
+ ), "scale_factor should have 2 elements, one for x dimension and one for y dimension"
1098
+ scale_mat = scale_factor
1099
+
1100
+ points = self.points * np.array(scale_mat)
1101
+
1102
+ return self.set(points=points)
1103
+
1104
+ def crop_image(self, image):
1105
+ """
1106
+ Crop the input image using the points of the quadrilateral instance.
1107
+
1108
+ Args:
1109
+ image (:obj:`Numpy array`): The array of the input image.
1110
+
1111
+ Returns:
1112
+ :obj:`Numpy array`: The array of the cropped image.
1113
+ """
1114
+
1115
+ return _warpPerspective(
1116
+ image, self.perspective_matrix, (int(self.width), int(self.height))
1117
+ )
1118
+
1119
+ def to_interval(self, axis, **kwargs):
1120
+
1121
+ x_1, y_1, x_2, y_2 = self.coordinates
1122
+ if axis == "x":
1123
+ start, end = x_1, x_2
1124
+ else:
1125
+ start, end = y_1, y_2
1126
+
1127
+ return Interval(start, end, axis=axis, **kwargs)
1128
+
1129
+ def to_rectangle(self):
1130
+ return Rectangle(*self.coordinates)
1131
+
1132
+ def __eq__(self, other):
1133
+ if other.__class__ is not self.__class__:
1134
+ return False
1135
+ return np.isclose(self.points, other.points).all()
1136
+
1137
+ def __repr__(self):
1138
+ keys = ["points", "width", "height"]
1139
+ info_str = ", ".join([f"{key}={getattr(self, key)}" for key in keys])
1140
+ return f"{self.__class__.__name__}({info_str})"
1141
+
1142
+ def to_dict(self) -> Dict[str, Any]:
1143
+
1144
+ """
1145
+ Generate a dictionary representation of the current object::
1146
+
1147
+ {
1148
+ "block_type": "quadrilateral",
1149
+ "points": [
1150
+ p[0,0], p[0,1],
1151
+ p[1,0], p[1,1],
1152
+ p[2,0], p[2,1],
1153
+ p[3,0], p[3,1]
1154
+ ],
1155
+ "height": value,
1156
+ "width": value
1157
+ }
1158
+ """
1159
+ data = super().to_dict()
1160
+ data["points"] = data["points"].reshape(-1).tolist()
1161
+ return data
1162
+
1163
+
1164
+ ALL_BASECOORD_ELEMENTS = [Interval, Rectangle, Quadrilateral]
1165
+
1166
+ BASECOORD_ELEMENT_NAMEMAP = {ele._name: ele for ele in ALL_BASECOORD_ELEMENTS}
1167
+ BASECOORD_ELEMENT_INDEXMAP = {
1168
+ ele._name: idx for idx, ele in enumerate(ALL_BASECOORD_ELEMENTS)
1169
+ }
1170
+
1171
+
1172
+ @inherit_docstrings(base_class=BaseCoordElement)
1173
+ class TextBlock(BaseLayoutElement):
1174
+ """
1175
+ This class constructs content-related information of a layout element in addition to its coordinate definitions
1176
+ (i.e. Interval, Rectangle or Quadrilateral).
1177
+
1178
+ Args:
1179
+ block (:obj:`BaseCoordElement`):
1180
+ The shape-specific coordinate systems that the text block belongs to.
1181
+ text (:obj:`str`, `optional`, defaults to None):
1182
+ The ocr'ed text results within the boundaries of the text block.
1183
+ id (:obj:`int`, `optional`, defaults to `None`):
1184
+ The id of the text block.
1185
+ type (:obj:`int`, `optional`, defaults to `None`):
1186
+ The type of the text block.
1187
+ parent (:obj:`int`, `optional`, defaults to `None`):
1188
+ The id of the parent object.
1189
+ next (:obj:`int`, `optional`, defaults to `None`):
1190
+ The id of the next block.
1191
+ score (:obj:`numeric`, defaults to `None`):
1192
+ The prediction confidence of the block
1193
+ """
1194
+
1195
+ _name = "textblock"
1196
+ _features = ["text", "id", "type", "parent", "next", "score"]
1197
+
1198
+ def __init__(
1199
+ self, block, text=None, id=None, type=None, parent=None, next=None, score=None
1200
+ ):
1201
+
1202
+ assert isinstance(block, BaseCoordElement)
1203
+ self.block = block
1204
+
1205
+ self.text = text
1206
+ self.id = id
1207
+ self.type = type
1208
+ self.parent = parent
1209
+ self.next = next
1210
+ self.score = score
1211
+
1212
+ @property
1213
+ def height(self):
1214
+ """
1215
+ Return the height of the shape-specific block.
1216
+
1217
+ Returns:
1218
+ :obj:`numeric`: Output the numeric value of the height.
1219
+ """
1220
+
1221
+ return self.block.height
1222
+
1223
+ @property
1224
+ def width(self):
1225
+ """
1226
+ Return the width of the shape-specific block.
1227
+
1228
+ Returns:
1229
+ :obj:`numeric`: Output the numeric value of the width.
1230
+ """
1231
+
1232
+ return self.block.width
1233
+
1234
+ @property
1235
+ def coordinates(self):
1236
+ """
1237
+ Return the coordinates of the two corner points that define the shape-specific block.
1238
+
1239
+ Returns:
1240
+ :obj:`Tuple(numeric)`: Output the numeric values of the coordinates in a Tuple of size four.
1241
+ """
1242
+
1243
+ return self.block.coordinates
1244
+
1245
+ @property
1246
+ def points(self):
1247
+ """
1248
+ Return the coordinates of all four corners of the shape-specific block in a clockwise fashion
1249
+ starting from the upper left.
1250
+
1251
+ Returns:
1252
+ :obj:`Numpy array`: A Numpy array of shape 4x2 containing the coordinates.
1253
+ """
1254
+
1255
+ return self.block.points
1256
+
1257
+ @property
1258
+ def area(self):
1259
+ """
1260
+ Return the area of associated block.
1261
+ """
1262
+ return self.block.area
1263
+
1264
+ @mixin_textblock_meta
1265
+ def condition_on(self, other):
1266
+ return self.block.condition_on(other)
1267
+
1268
+ @mixin_textblock_meta
1269
+ def relative_to(self, other):
1270
+ return self.block.relative_to(other)
1271
+
1272
+ def is_in(self, other, soft_margin={}, center=False):
1273
+ return self.block.is_in(other, soft_margin, center)
1274
+
1275
+ @mixin_textblock_meta
1276
+ def union(self, other: BaseCoordElement, strict: bool = True):
1277
+ return self.block.union(other, strict=strict)
1278
+
1279
+ @mixin_textblock_meta
1280
+ def intersect(self, other: BaseCoordElement, strict: bool = True):
1281
+ return self.block.intersect(other, strict=strict)
1282
+
1283
+ @mixin_textblock_meta
1284
+ def shift(self, shift_distance):
1285
+ return self.block.shift(shift_distance)
1286
+
1287
+ @mixin_textblock_meta
1288
+ def pad(self, left=0, right=0, top=0, bottom=0, safe_mode=True):
1289
+ return self.block.pad(left, right, top, bottom, safe_mode)
1290
+
1291
+ @mixin_textblock_meta
1292
+ def scale(self, scale_factor):
1293
+ return self.block.scale(scale_factor)
1294
+
1295
+ def crop_image(self, image):
1296
+ return self.block.crop_image(image)
1297
+
1298
+ def to_interval(self, axis: Optional[str] = None, **kwargs):
1299
+ if isinstance(self.block, Interval):
1300
+ return self
1301
+ else:
1302
+ if not axis:
1303
+ raise ValueError(
1304
+ f"Please provide valid `axis` values {'x' or 'y'} as the input"
1305
+ )
1306
+ return self.set(block=self.block.to_interval(axis=axis, **kwargs))
1307
+
1308
+ def to_rectangle(self):
1309
+ if isinstance(self.block, Rectangle):
1310
+ return self
1311
+ else:
1312
+ return self.set(block=self.block.to_rectangle())
1313
+
1314
+ def to_quadrilateral(self):
1315
+ if isinstance(self.block, Quadrilateral):
1316
+ return self
1317
+ else:
1318
+ return self.set(block=self.block.to_quadrilateral())
1319
+
1320
+ def to_dict(self) -> Dict[str, Any]:
1321
+ """
1322
+ Generate a dictionary representation of the current textblock of the format::
1323
+
1324
+ {
1325
+ "block_type": <name of self.block>,
1326
+ <attributes of self.block combined with
1327
+ non-empty self._features>
1328
+ }
1329
+ """
1330
+ base_dict = self.block.to_dict()
1331
+ for f in self._features:
1332
+ val = getattr(self, f)
1333
+ if val is not None:
1334
+ base_dict[f] = getattr(self, f)
1335
+ return base_dict
1336
+
1337
+ @classmethod
1338
+ def from_dict(cls, data: Dict[str, Any]) -> "TextBlock":
1339
+ """Initialize the textblock based on the dictionary representation.
1340
+ It generate the block based on the `block_type` and `block_attr`,
1341
+ and loads the textblock specific features from the dict.
1342
+
1343
+ Args:
1344
+ data (:obj:`dict`): The dictionary representation of the object
1345
+ """
1346
+ assert (
1347
+ data["block_type"] in BASECOORD_ELEMENT_NAMEMAP
1348
+ ), f"Invalid block_type {data['block_type']}"
1349
+
1350
+ block = BASECOORD_ELEMENT_NAMEMAP[data["block_type"]].from_dict(data)
1351
+
1352
+ return cls(block, **{f: data.get(f, None) for f in cls._features})