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.
- custom_layoutparser-0.1.0.dist-info/METADATA +5 -0
- custom_layoutparser-0.1.0.dist-info/RECORD +36 -0
- custom_layoutparser-0.1.0.dist-info/WHEEL +5 -0
- custom_layoutparser-0.1.0.dist-info/top_level.txt +1 -0
- layoutparser/__init__.py +89 -0
- layoutparser/elements/__init__.py +25 -0
- layoutparser/elements/base.py +275 -0
- layoutparser/elements/errors.py +26 -0
- layoutparser/elements/layout.py +348 -0
- layoutparser/elements/layout_elements.py +1352 -0
- layoutparser/elements/utils.py +82 -0
- layoutparser/file_utils.py +235 -0
- layoutparser/io/__init__.py +2 -0
- layoutparser/io/basic.py +148 -0
- layoutparser/io/pdf.py +225 -0
- layoutparser/models/__init__.py +18 -0
- layoutparser/models/auto_layoutmodel.py +70 -0
- layoutparser/models/base_catalog.py +34 -0
- layoutparser/models/base_layoutmodel.py +88 -0
- layoutparser/models/detectron2/__init__.py +18 -0
- layoutparser/models/detectron2/catalog.py +142 -0
- layoutparser/models/detectron2/layoutmodel.py +168 -0
- layoutparser/models/effdet/__init__.py +16 -0
- layoutparser/models/effdet/catalog.py +88 -0
- layoutparser/models/effdet/layoutmodel.py +256 -0
- layoutparser/models/model_config.py +133 -0
- layoutparser/models/paddledetection/__init__.py +17 -0
- layoutparser/models/paddledetection/catalog.py +214 -0
- layoutparser/models/paddledetection/layoutmodel.py +297 -0
- layoutparser/ocr/__init__.py +16 -0
- layoutparser/ocr/base.py +41 -0
- layoutparser/ocr/gcv_agent.py +288 -0
- layoutparser/ocr/tesseract_agent.py +193 -0
- layoutparser/tools/__init__.py +5 -0
- layoutparser/tools/shape_operations.py +167 -0
- 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})
|