custom-layoutparser 9.9.9__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-9.9.9.dist-info/METADATA +5 -0
- custom_layoutparser-9.9.9.dist-info/RECORD +36 -0
- custom_layoutparser-9.9.9.dist-info/WHEEL +5 -0
- custom_layoutparser-9.9.9.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 +70 -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,348 @@
|
|
|
1
|
+
# Copyright 2021 The Layout sParser 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
|
|
16
|
+
from collections.abc import MutableSequence, Iterable
|
|
17
|
+
from copy import copy
|
|
18
|
+
|
|
19
|
+
import pandas as pd
|
|
20
|
+
|
|
21
|
+
from .base import BaseCoordElement, BaseLayoutElement
|
|
22
|
+
from .layout_elements import (
|
|
23
|
+
Interval,
|
|
24
|
+
Rectangle,
|
|
25
|
+
Quadrilateral,
|
|
26
|
+
TextBlock,
|
|
27
|
+
ALL_BASECOORD_ELEMENTS,
|
|
28
|
+
BASECOORD_ELEMENT_NAMEMAP,
|
|
29
|
+
BASECOORD_ELEMENT_INDEXMAP,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Layout(MutableSequence):
|
|
34
|
+
"""
|
|
35
|
+
The :obj:`Layout` class id designed for processing a list of layout elements
|
|
36
|
+
on a page. It stores the layout elements in a list and the related `page_data`,
|
|
37
|
+
and provides handy APIs for processing all the layout elements in batch. `
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
blocks (:obj:`list`):
|
|
41
|
+
A list of layout element blocks
|
|
42
|
+
page_data (Dict, optional):
|
|
43
|
+
A dictionary storing the page (canvas) related information
|
|
44
|
+
like `height`, `width`, etc. It should be passed in as a
|
|
45
|
+
keyword argument to avoid any confusion.
|
|
46
|
+
Defaults to None.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, blocks: Optional[List] = None, *, page_data: Dict = None):
|
|
50
|
+
|
|
51
|
+
if not (
|
|
52
|
+
(blocks is None)
|
|
53
|
+
or (isinstance(blocks, Iterable) and blocks.__class__.__name__ != "Layout")
|
|
54
|
+
):
|
|
55
|
+
|
|
56
|
+
if blocks.__class__.__name__ == "Layout":
|
|
57
|
+
error_msg = f"Please check the input: it should be lp.Layout([layout]) instead of lp.Layout(layout)"
|
|
58
|
+
else:
|
|
59
|
+
error_msg = f"Blocks should be a list of layout elements or empty (None), instead got {blocks}.\n"
|
|
60
|
+
raise ValueError(error_msg)
|
|
61
|
+
|
|
62
|
+
if isinstance(blocks, tuple):
|
|
63
|
+
blocks = list(blocks) # <- more robust handling for tuple-like inputs
|
|
64
|
+
|
|
65
|
+
self._blocks = blocks if blocks is not None else []
|
|
66
|
+
self.page_data = page_data or {}
|
|
67
|
+
|
|
68
|
+
def __getitem__(self, key):
|
|
69
|
+
blocks = self._blocks[key]
|
|
70
|
+
if isinstance(key, slice):
|
|
71
|
+
return self.__class__(self._blocks[key], page_data=self.page_data)
|
|
72
|
+
else:
|
|
73
|
+
return blocks
|
|
74
|
+
|
|
75
|
+
def __setitem__(self, key, newvalue):
|
|
76
|
+
self._blocks[key] = newvalue
|
|
77
|
+
|
|
78
|
+
def __delitem__(self, key):
|
|
79
|
+
del self._blocks[key]
|
|
80
|
+
|
|
81
|
+
def __len__(self):
|
|
82
|
+
return len(self._blocks)
|
|
83
|
+
|
|
84
|
+
def __iter__(self):
|
|
85
|
+
for ele in self._blocks:
|
|
86
|
+
yield ele
|
|
87
|
+
|
|
88
|
+
def __repr__(self):
|
|
89
|
+
info_str = ", ".join([f"{key}={val}" for key, val in vars(self).items()])
|
|
90
|
+
return f"{self.__class__.__name__}({info_str})"
|
|
91
|
+
|
|
92
|
+
def __eq__(self, other):
|
|
93
|
+
if isinstance(other, Layout):
|
|
94
|
+
return self._blocks == other._blocks and self.page_data == other.page_data
|
|
95
|
+
else:
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
def __add__(self, other):
|
|
99
|
+
if isinstance(other, Layout):
|
|
100
|
+
if self.page_data == other.page_data:
|
|
101
|
+
return self.__class__(
|
|
102
|
+
self._blocks + other._blocks, page_data=self.page_data
|
|
103
|
+
)
|
|
104
|
+
elif self.page_data == {} or other.page_data == {}:
|
|
105
|
+
return self.__class__(
|
|
106
|
+
self._blocks + other._blocks,
|
|
107
|
+
page_data=self.page_data or other.page_data,
|
|
108
|
+
)
|
|
109
|
+
else:
|
|
110
|
+
raise ValueError(
|
|
111
|
+
f"Incompatible page_data for two innputs: {self.page_data} vs {other.page_data}."
|
|
112
|
+
)
|
|
113
|
+
elif isinstance(other, list):
|
|
114
|
+
return self.__class__(self._blocks + other, page_data=self.page_data)
|
|
115
|
+
else:
|
|
116
|
+
raise ValueError(
|
|
117
|
+
f"Invalid input type for other {other.__class__.__name__}."
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def insert(self, key, value):
|
|
121
|
+
self._blocks.insert(key, value)
|
|
122
|
+
|
|
123
|
+
def copy(self):
|
|
124
|
+
return self.__class__(copy(self._blocks), page_data=self.page_data)
|
|
125
|
+
|
|
126
|
+
def relative_to(self, other):
|
|
127
|
+
return self.__class__(
|
|
128
|
+
[ele.relative_to(other) for ele in self], page_data=self.page_data
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def condition_on(self, other):
|
|
132
|
+
return self.__class__(
|
|
133
|
+
[ele.condition_on(other) for ele in self], page_data=self.page_data
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
def is_in(self, other, soft_margin={}, center=False):
|
|
137
|
+
return self.__class__(
|
|
138
|
+
[ele.is_in(other, soft_margin, center) for ele in self],
|
|
139
|
+
page_data=self.page_data,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def sort(self, key=None, reverse=False, inplace=False) -> Optional["Layout"]:
|
|
143
|
+
"""Sort the list of blocks based on the given
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
key ([type], optional): key specifies a function of one argument that
|
|
147
|
+
is used to extract a comparison key from each list element.
|
|
148
|
+
Defaults to None.
|
|
149
|
+
reverse (bool, optional): reverse is a boolean value. If set to True,
|
|
150
|
+
then the list elements are sorted as if each comparison were reversed.
|
|
151
|
+
Defaults to False.
|
|
152
|
+
inplace (bool, optional): whether to perform the sort inplace. If set
|
|
153
|
+
to False, it will return another object instance with _block sorted in
|
|
154
|
+
the order. Defaults to False.
|
|
155
|
+
|
|
156
|
+
Examples::
|
|
157
|
+
>>> import layoutparser as lp
|
|
158
|
+
>>> i = lp.Interval(4, 5, axis="y")
|
|
159
|
+
>>> l = lp.Layout([i, i.shift(2)])
|
|
160
|
+
>>> l.sort(key=lambda x: x.coordinates[1], reverse=True)
|
|
161
|
+
|
|
162
|
+
"""
|
|
163
|
+
if not inplace:
|
|
164
|
+
return self.__class__(
|
|
165
|
+
sorted(self._blocks, key=key, reverse=reverse), page_data=self.page_data
|
|
166
|
+
)
|
|
167
|
+
else:
|
|
168
|
+
self._blocks.sort(key=key, reverse=reverse)
|
|
169
|
+
|
|
170
|
+
def filter_by(self, other, soft_margin={}, center=False):
|
|
171
|
+
"""
|
|
172
|
+
Return a `Layout` object containing the elements that are in the `other` object.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
other (:obj:`BaseCoordElement`):
|
|
176
|
+
The block to filter the current elements.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
:obj:`Layout`:
|
|
180
|
+
A new layout object after filtering.
|
|
181
|
+
"""
|
|
182
|
+
return self.__class__(
|
|
183
|
+
[ele for ele in self if ele.is_in(other, soft_margin, center)],
|
|
184
|
+
page_data=self.page_data,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
def shift(self, shift_distance):
|
|
188
|
+
"""
|
|
189
|
+
Shift all layout elements by user specified amounts on x and y axis respectively. If shift_distance is one
|
|
190
|
+
numeric value, the element will by shifted by the same specified amount on both x and y axis.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
shift_distance (:obj:`numeric` or :obj:`Tuple(numeric)` or :obj:`List[numeric]`):
|
|
194
|
+
The number of pixels used to shift the element.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
:obj:`Layout`:
|
|
198
|
+
A new layout object with all the elements shifted in the specified values.
|
|
199
|
+
"""
|
|
200
|
+
return self.__class__(
|
|
201
|
+
[ele.shift(shift_distance) for ele in self], page_data=self.page_data
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
def pad(self, left=0, right=0, top=0, bottom=0, safe_mode=True):
|
|
205
|
+
"""Pad all layout elements on the four sides of the polygon with the user-defined pixels. If
|
|
206
|
+
safe_mode is set to True, the function will cut off the excess padding that falls on the negative
|
|
207
|
+
side of the coordinates.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
left (:obj:`int`, `optional`, defaults to 0): The number of pixels to pad on the upper side of the polygon.
|
|
211
|
+
right (:obj:`int`, `optional`, defaults to 0): The number of pixels to pad on the lower side of the polygon.
|
|
212
|
+
top (:obj:`int`, `optional`, defaults to 0): The number of pixels to pad on the left side of the polygon.
|
|
213
|
+
bottom (:obj:`int`, `optional`, defaults to 0): The number of pixels to pad on the right side of the polygon.
|
|
214
|
+
safe_mode (:obj:`bool`, `optional`, defaults to True): A bool value to toggle the safe_mode.
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
:obj:`Layout`:
|
|
218
|
+
A new layout object with all the elements padded in the specified values.
|
|
219
|
+
"""
|
|
220
|
+
return self.__class__(
|
|
221
|
+
[ele.pad(left, right, top, bottom, safe_mode) for ele in self],
|
|
222
|
+
page_data=self.page_data,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
def scale(self, scale_factor):
|
|
226
|
+
"""
|
|
227
|
+
Scale all layout element by a user specified amount on x and y axis respectively. If scale_factor is one
|
|
228
|
+
numeric value, the element will by scaled by the same specified amount on both x and y axis.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
scale_factor (:obj:`numeric` or :obj:`Tuple(numeric)` or :obj:`List[numeric]`): The amount for downscaling or upscaling the element.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
:obj:`Layout`:
|
|
235
|
+
A new layout object with all the elements scaled in the specified values.
|
|
236
|
+
"""
|
|
237
|
+
return self.__class__(
|
|
238
|
+
[ele.scale(scale_factor) for ele in self], page_data=self.page_data
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
def crop_image(self, image):
|
|
242
|
+
return [ele.crop_image(image) for ele in self]
|
|
243
|
+
|
|
244
|
+
def get_texts(self):
|
|
245
|
+
"""
|
|
246
|
+
Iterate through all the text blocks in the list and append their ocr'ed text results.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
:obj:`List[str]`: A list of text strings of the text blocks in the list of layout elements.
|
|
250
|
+
"""
|
|
251
|
+
|
|
252
|
+
return [ele.text for ele in self if hasattr(ele, "text")]
|
|
253
|
+
|
|
254
|
+
def get_info(self, attr_name):
|
|
255
|
+
"""Given user-provided attribute name, check all the elements in the list and return the corresponding
|
|
256
|
+
attribute values.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
attr_name (:obj:`str`): The text string of certain attribute name.
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
:obj:`List`:
|
|
263
|
+
The list of the corresponding attribute value (if exist) of each element in the list.
|
|
264
|
+
"""
|
|
265
|
+
return [getattr(ele, attr_name) for ele in self if hasattr(ele, attr_name)]
|
|
266
|
+
|
|
267
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
268
|
+
"""Generate a dict representation of the layout object with
|
|
269
|
+
the page_data and all the blocks in its dict representation.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
:obj:`Dict`:
|
|
273
|
+
The dictionary representation of the layout object.
|
|
274
|
+
"""
|
|
275
|
+
return {"page_data": self.page_data, "blocks": [ele.to_dict() for ele in self]}
|
|
276
|
+
|
|
277
|
+
def get_homogeneous_blocks(self) -> List[BaseLayoutElement]:
|
|
278
|
+
"""Convert all elements into blocks of the same type based
|
|
279
|
+
on the type casting rule::
|
|
280
|
+
|
|
281
|
+
Interval < Rectangle < Quadrilateral < TextBlock
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
List[BaseLayoutElement]:
|
|
285
|
+
A list of base layout elements of the maximal compatible
|
|
286
|
+
type
|
|
287
|
+
"""
|
|
288
|
+
|
|
289
|
+
# Detect the maximal compatible type
|
|
290
|
+
has_textblock = False
|
|
291
|
+
max_coord_level = -1
|
|
292
|
+
for ele in self:
|
|
293
|
+
|
|
294
|
+
if isinstance(ele, TextBlock):
|
|
295
|
+
has_textblock = True
|
|
296
|
+
block = ele.block
|
|
297
|
+
else:
|
|
298
|
+
block = ele
|
|
299
|
+
|
|
300
|
+
max_coord_level = max(
|
|
301
|
+
max_coord_level, BASECOORD_ELEMENT_INDEXMAP[block._name]
|
|
302
|
+
)
|
|
303
|
+
target_coord_name = ALL_BASECOORD_ELEMENTS[max_coord_level]._name
|
|
304
|
+
|
|
305
|
+
if has_textblock:
|
|
306
|
+
new_blocks = []
|
|
307
|
+
for ele in self:
|
|
308
|
+
if isinstance(ele, TextBlock):
|
|
309
|
+
ele = copy(ele)
|
|
310
|
+
if ele.block._name != target_coord_name:
|
|
311
|
+
ele.block = getattr(ele.block, f"to_{target_coord_name}")()
|
|
312
|
+
else:
|
|
313
|
+
if ele._name != target_coord_name:
|
|
314
|
+
ele = getattr(ele, f"to_{target_coord_name}")()
|
|
315
|
+
ele = TextBlock(block)
|
|
316
|
+
new_blocks.append(ele)
|
|
317
|
+
else:
|
|
318
|
+
new_blocks = [
|
|
319
|
+
getattr(ele, f"to_{target_coord_name}")()
|
|
320
|
+
if ele._name != target_coord_name
|
|
321
|
+
else ele
|
|
322
|
+
for ele in self
|
|
323
|
+
]
|
|
324
|
+
|
|
325
|
+
return new_blocks
|
|
326
|
+
|
|
327
|
+
def to_dataframe(self, enforce_same_type=False) -> pd.DataFrame:
|
|
328
|
+
"""Convert the layout object into the dataframe.
|
|
329
|
+
Warning: the page data won't be exported.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
enforce_same_type (:obj:`bool`, optional):
|
|
333
|
+
If true, it will convert all the contained blocks to
|
|
334
|
+
the maximal compatible data type.
|
|
335
|
+
Defaults to False.
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
pd.DataFrame:
|
|
339
|
+
The dataframe representation of layout object
|
|
340
|
+
"""
|
|
341
|
+
if enforce_same_type:
|
|
342
|
+
blocks = self.get_homogeneous_blocks()
|
|
343
|
+
else:
|
|
344
|
+
blocks = self
|
|
345
|
+
|
|
346
|
+
df = pd.DataFrame([ele.to_dict() for ele in blocks])
|
|
347
|
+
|
|
348
|
+
return df
|