indico_toolkit 6.14.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.
- indico_toolkit/__init__.py +24 -0
- indico_toolkit/association/__init__.py +15 -0
- indico_toolkit/association/association.py +65 -0
- indico_toolkit/association/extracted_tokens.py +106 -0
- indico_toolkit/association/line_items.py +189 -0
- indico_toolkit/association/positioning.py +301 -0
- indico_toolkit/association/splitting.py +41 -0
- indico_toolkit/auto_populate/__init__.py +3 -0
- indico_toolkit/auto_populate/populator.py +362 -0
- indico_toolkit/auto_populate/types.py +66 -0
- indico_toolkit/auto_review/__init__.py +6 -0
- indico_toolkit/auto_review/auto_review_functions.py +123 -0
- indico_toolkit/auto_review/auto_reviewer.py +62 -0
- indico_toolkit/client.py +35 -0
- indico_toolkit/errors.py +32 -0
- indico_toolkit/etloutput/__init__.py +210 -0
- indico_toolkit/etloutput/cell.py +47 -0
- indico_toolkit/etloutput/errors.py +16 -0
- indico_toolkit/etloutput/etloutput.py +110 -0
- indico_toolkit/etloutput/range.py +30 -0
- indico_toolkit/etloutput/table.py +44 -0
- indico_toolkit/etloutput/token.py +25 -0
- indico_toolkit/indico_wrapper/__init__.py +15 -0
- indico_toolkit/indico_wrapper/dataset.py +114 -0
- indico_toolkit/indico_wrapper/doc_extraction.py +79 -0
- indico_toolkit/indico_wrapper/download.py +193 -0
- indico_toolkit/indico_wrapper/indico_wrapper.py +119 -0
- indico_toolkit/indico_wrapper/modelop.py +200 -0
- indico_toolkit/indico_wrapper/reviewer.py +111 -0
- indico_toolkit/indico_wrapper/workflow.py +231 -0
- indico_toolkit/metrics/__init__.py +11 -0
- indico_toolkit/metrics/compare_ground_truth.py +123 -0
- indico_toolkit/metrics/compare_models.py +180 -0
- indico_toolkit/metrics/metrics.py +373 -0
- indico_toolkit/metrics/plotting.py +102 -0
- indico_toolkit/ocr/__init__.py +9 -0
- indico_toolkit/ocr/customocr_object.py +42 -0
- indico_toolkit/ocr/ondoc_object.py +93 -0
- indico_toolkit/ocr/standard_object.py +54 -0
- indico_toolkit/pipelines/__init__.py +3 -0
- indico_toolkit/pipelines/file_processing.py +157 -0
- indico_toolkit/polling/__init__.py +8 -0
- indico_toolkit/polling/autoreview.py +204 -0
- indico_toolkit/polling/downstream.py +145 -0
- indico_toolkit/polling/queries.py +68 -0
- indico_toolkit/py.typed +0 -0
- indico_toolkit/results/__init__.py +104 -0
- indico_toolkit/results/document.py +78 -0
- indico_toolkit/results/errors.py +4 -0
- indico_toolkit/results/model.py +60 -0
- indico_toolkit/results/normalization.py +159 -0
- indico_toolkit/results/predictionlist.py +395 -0
- indico_toolkit/results/predictions/__init__.py +88 -0
- indico_toolkit/results/predictions/box.py +58 -0
- indico_toolkit/results/predictions/citation.py +47 -0
- indico_toolkit/results/predictions/classification.py +50 -0
- indico_toolkit/results/predictions/documentextraction.py +160 -0
- indico_toolkit/results/predictions/extraction.py +35 -0
- indico_toolkit/results/predictions/formextraction.py +121 -0
- indico_toolkit/results/predictions/group.py +29 -0
- indico_toolkit/results/predictions/prediction.py +41 -0
- indico_toolkit/results/predictions/span.py +43 -0
- indico_toolkit/results/predictions/summarization.py +103 -0
- indico_toolkit/results/predictions/unbundling.py +51 -0
- indico_toolkit/results/result.py +152 -0
- indico_toolkit/results/review.py +32 -0
- indico_toolkit/results/utilities.py +73 -0
- indico_toolkit/retry.py +94 -0
- indico_toolkit/snapshots/__init__.py +3 -0
- indico_toolkit/snapshots/snapshot.py +340 -0
- indico_toolkit/structure/__init__.py +3 -0
- indico_toolkit/structure/create_structure.py +232 -0
- indico_toolkit/structure/queries.py +119 -0
- indico_toolkit/structure/utils.py +16 -0
- indico_toolkit/types/__init__.py +12 -0
- indico_toolkit/types/classification.py +92 -0
- indico_toolkit/types/extractions.py +227 -0
- indico_toolkit/types/predictions.py +28 -0
- indico_toolkit/types/workflow_object.py +133 -0
- indico_toolkit-6.14.0.dist-info/LICENSE +21 -0
- indico_toolkit-6.14.0.dist-info/METADATA +144 -0
- indico_toolkit-6.14.0.dist-info/RECORD +83 -0
- indico_toolkit-6.14.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
from math import sqrt
|
|
2
|
+
from typing import List
|
|
3
|
+
|
|
4
|
+
from ..errors import ToolkitInputError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Positioning:
|
|
8
|
+
"""
|
|
9
|
+
Class to help identify relative positions in a document using bounding box data.
|
|
10
|
+
|
|
11
|
+
Positions are expected to contain, at a minimum, the following top-level
|
|
12
|
+
keys: "bbTop", "bbBot", "bbLeft", "bbRight", "page_num".
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self):
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
def positioned_above(
|
|
19
|
+
self, above_pos: dict, below_pos: dict, must_be_same_page: bool = True
|
|
20
|
+
) -> bool:
|
|
21
|
+
"""
|
|
22
|
+
Check if the location of one box is above another
|
|
23
|
+
Args:
|
|
24
|
+
above_pos (dict): the position expected to be above
|
|
25
|
+
below_pos (dict): to position expected to be below
|
|
26
|
+
must_be_same_page (bool, optional): required to be on same page. Defaults to
|
|
27
|
+
True.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
bool: True if above_pos is above below_pos
|
|
31
|
+
"""
|
|
32
|
+
if below_pos["page_num"] < above_pos["page_num"]:
|
|
33
|
+
is_above = False
|
|
34
|
+
elif below_pos["page_num"] != above_pos["page_num"] and must_be_same_page:
|
|
35
|
+
is_above = False
|
|
36
|
+
elif self.xaxis_overlap(above_pos, below_pos):
|
|
37
|
+
if (
|
|
38
|
+
below_pos["page_num"] == above_pos["page_num"]
|
|
39
|
+
) and not self.yaxis_above(above_pos, below_pos):
|
|
40
|
+
is_above = False
|
|
41
|
+
else:
|
|
42
|
+
is_above = True
|
|
43
|
+
else:
|
|
44
|
+
is_above = False
|
|
45
|
+
return is_above
|
|
46
|
+
|
|
47
|
+
def positioned_above_overlap(
|
|
48
|
+
self, above_pos: dict, below_pos: dict, min_overlap_percent: float = None
|
|
49
|
+
) -> bool:
|
|
50
|
+
"""
|
|
51
|
+
Check if the location of one box is on the same page and above another and if
|
|
52
|
+
the lower box's overlap is at least the given percentage.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
above_pos (dict): the position expected to be above
|
|
56
|
+
below_pos (dict): the position expected to be below
|
|
57
|
+
min_overlap_percent (float, optional): the minimum amount of overlap needed.
|
|
58
|
+
Defaults to None.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
bool: True if above_pos is above below_pos and below_pos' amount of overlap
|
|
62
|
+
is at least min_overlap_percent
|
|
63
|
+
"""
|
|
64
|
+
is_above = False
|
|
65
|
+
is_min_overlap = True
|
|
66
|
+
if below_pos["page_num"] != above_pos["page_num"]:
|
|
67
|
+
raise ToolkitInputError("Predictions are not on the same page!")
|
|
68
|
+
if self.xaxis_overlap(above_pos, below_pos) and self.yaxis_above(
|
|
69
|
+
above_pos, below_pos
|
|
70
|
+
):
|
|
71
|
+
is_above = True
|
|
72
|
+
overlap_amount = self.get_horizontal_overlap(above_pos, below_pos)
|
|
73
|
+
if min_overlap_percent and overlap_amount < min_overlap_percent:
|
|
74
|
+
is_min_overlap = False
|
|
75
|
+
return is_above and is_min_overlap
|
|
76
|
+
|
|
77
|
+
def positioned_on_same_level(
|
|
78
|
+
self, pos1: dict, pos2: dict, must_be_same_page: bool = True
|
|
79
|
+
) -> bool:
|
|
80
|
+
"""
|
|
81
|
+
Two box positions are located on the same level/row, i.e. yaxes overlap
|
|
82
|
+
Args:
|
|
83
|
+
pos1 (dict): first position
|
|
84
|
+
pos2 (dict): second position
|
|
85
|
+
must_be_same_page (bool, optional): required to be on same page. Defaults to
|
|
86
|
+
True.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
bool: True if positions on same level, else False
|
|
90
|
+
"""
|
|
91
|
+
if must_be_same_page and not self.on_same_page(pos1, pos2):
|
|
92
|
+
same_level = False
|
|
93
|
+
elif self.yaxis_overlap(pos1, pos2):
|
|
94
|
+
same_level = True
|
|
95
|
+
else:
|
|
96
|
+
same_level = False
|
|
97
|
+
return same_level
|
|
98
|
+
|
|
99
|
+
def get_min_distance(
|
|
100
|
+
self, pos1: dict, pos2: dict, page_height: int = None
|
|
101
|
+
) -> float:
|
|
102
|
+
"""
|
|
103
|
+
Get the minimum distance between any two corners of two bounding boxes via the
|
|
104
|
+
pythagorean formula.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
page_height (int, optional): If you want to measure distances across pages,
|
|
108
|
+
set the OCR page height otherwise locations on separate pages will raise
|
|
109
|
+
an exception. Defaults to None.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
float: minimum distance
|
|
113
|
+
"""
|
|
114
|
+
add_page_height = False
|
|
115
|
+
page_difference = abs(pos1["page_num"] - pos2["page_num"])
|
|
116
|
+
if page_difference > 0:
|
|
117
|
+
if not page_height:
|
|
118
|
+
raise ToolkitInputError(
|
|
119
|
+
"Predictions are not on the same page! Must enter a page height"
|
|
120
|
+
)
|
|
121
|
+
else:
|
|
122
|
+
add_page_height = True
|
|
123
|
+
distances = []
|
|
124
|
+
corners = [
|
|
125
|
+
("bbRight", "bbTop"),
|
|
126
|
+
("bbRight", "bbBot"),
|
|
127
|
+
("bbLeft", "bbTop"),
|
|
128
|
+
("bbLeft", "bbBot"),
|
|
129
|
+
]
|
|
130
|
+
for p1 in corners:
|
|
131
|
+
for p2 in corners:
|
|
132
|
+
distance = self._distance_between_points(
|
|
133
|
+
(pos1[p1[0]], pos1[p1[1]]), (pos2[p2[0]], pos2[p2[1]])
|
|
134
|
+
)
|
|
135
|
+
distances.append(distance)
|
|
136
|
+
min_distance = min(distances)
|
|
137
|
+
if add_page_height:
|
|
138
|
+
min_distance += page_height * page_difference
|
|
139
|
+
return min_distance
|
|
140
|
+
|
|
141
|
+
def get_horizontal_overlap(self, pos1: dict, pos2: dict) -> float:
|
|
142
|
+
"""
|
|
143
|
+
Get the amount of horizontal overlap between two bounding boxes.
|
|
144
|
+
Returns:
|
|
145
|
+
float: percentage of pos2 that horizontally overlaps with pos1
|
|
146
|
+
"""
|
|
147
|
+
page_difference = abs(pos1["page_num"] - pos2["page_num"])
|
|
148
|
+
if page_difference > 0:
|
|
149
|
+
raise ToolkitInputError("Predictions are not on the same page!")
|
|
150
|
+
if self.xaxis_overlap(pos1, pos2):
|
|
151
|
+
horizontal_overlap_distance = abs(
|
|
152
|
+
max(pos1["bbLeft"], pos2["bbLeft"])
|
|
153
|
+
- min(pos1["bbRight"], pos2["bbRight"])
|
|
154
|
+
)
|
|
155
|
+
position_width = abs(pos2["bbLeft"] - pos2["bbRight"])
|
|
156
|
+
return horizontal_overlap_distance / position_width
|
|
157
|
+
else:
|
|
158
|
+
return 0.0
|
|
159
|
+
|
|
160
|
+
def get_vertical_overlap(self, pos1: dict, pos2: dict) -> float:
|
|
161
|
+
"""
|
|
162
|
+
Get the amount of vertical overlap between two bounding boxes.
|
|
163
|
+
Returns:
|
|
164
|
+
float: percentage of pos2 that vertically overlaps with pos1
|
|
165
|
+
"""
|
|
166
|
+
page_difference = abs(pos1["page_num"] - pos2["page_num"])
|
|
167
|
+
if page_difference > 0:
|
|
168
|
+
raise ToolkitInputError("Predictions are not on the same page!")
|
|
169
|
+
if self.yaxis_overlap(pos1, pos2):
|
|
170
|
+
vertical_overlap_distance = abs(
|
|
171
|
+
max(pos1["bbTop"], pos2["bbTop"]) - min(pos1["bbBot"], pos2["bbBot"])
|
|
172
|
+
)
|
|
173
|
+
position_height = abs(pos2["bbTop"] - pos2["bbBot"])
|
|
174
|
+
return vertical_overlap_distance / position_height
|
|
175
|
+
else:
|
|
176
|
+
return 0.0
|
|
177
|
+
|
|
178
|
+
def get_tokens_within_bounds(
|
|
179
|
+
self, bbox: dict, ocr_tokens: List[dict], include_overlap: bool = False
|
|
180
|
+
) -> List[dict]:
|
|
181
|
+
"""
|
|
182
|
+
Args:
|
|
183
|
+
bbox (dict): dict with target box dimensions and page number
|
|
184
|
+
bbox = {
|
|
185
|
+
bbTop: int
|
|
186
|
+
bbBot: int
|
|
187
|
+
bbLeft: int
|
|
188
|
+
bbRight: int
|
|
189
|
+
page_num: int
|
|
190
|
+
}
|
|
191
|
+
ocr_tokens (List[dict]): on-doc OCR token output from raw or OnDoc class
|
|
192
|
+
include_overlap (bool, optional): Determines whether to include tokens
|
|
193
|
+
partially inside bbox. Defaults to False.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
List[dict]: list of OCR tokens that fall within the specified bounding box
|
|
197
|
+
"""
|
|
198
|
+
if "position" not in ocr_tokens[0] or "page_num" not in ocr_tokens[0]:
|
|
199
|
+
raise ToolkitInputError(
|
|
200
|
+
"Token list argument is missing required key(s): "
|
|
201
|
+
"page_num and/or position"
|
|
202
|
+
)
|
|
203
|
+
if include_overlap:
|
|
204
|
+
return [
|
|
205
|
+
token
|
|
206
|
+
for token in ocr_tokens
|
|
207
|
+
if self.on_same_page(bbox, token)
|
|
208
|
+
and self.yaxis_overlap(bbox, token["position"])
|
|
209
|
+
and self.xaxis_overlap(bbox, token["position"])
|
|
210
|
+
]
|
|
211
|
+
else:
|
|
212
|
+
return [
|
|
213
|
+
token
|
|
214
|
+
for token in ocr_tokens
|
|
215
|
+
if self.on_same_page(bbox, token)
|
|
216
|
+
and token["position"]["bbLeft"] > bbox["bbLeft"]
|
|
217
|
+
and token["position"]["bbRight"] < bbox["bbRight"]
|
|
218
|
+
and token["position"]["bbTop"] > bbox["bbTop"]
|
|
219
|
+
and token["position"]["bbBot"] < bbox["bbBot"]
|
|
220
|
+
]
|
|
221
|
+
|
|
222
|
+
@staticmethod
|
|
223
|
+
def get_vertical_min_distance(
|
|
224
|
+
above_pos: dict, below_pos: dict, page_height: int = None
|
|
225
|
+
) -> float:
|
|
226
|
+
"""
|
|
227
|
+
Get the vertical minimum distance between two bounding boxes
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
above_pos (dict): the position expected to be above
|
|
231
|
+
below_pos (dict): to position expected to be below
|
|
232
|
+
page_height (int, optional): If you want to measure distances across pages,
|
|
233
|
+
set the OCR page height otherwise locations on separate pages will raise
|
|
234
|
+
an exception. Defaults to None.
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
float: minimum distance
|
|
238
|
+
"""
|
|
239
|
+
add_page_height = False
|
|
240
|
+
page_difference = abs(above_pos["page_num"] - below_pos["page_num"])
|
|
241
|
+
if page_difference > 0:
|
|
242
|
+
if not page_height:
|
|
243
|
+
raise ToolkitInputError(
|
|
244
|
+
"Predictions are not on the same page! Must enter a page height"
|
|
245
|
+
)
|
|
246
|
+
else:
|
|
247
|
+
add_page_height = True
|
|
248
|
+
min_distance = below_pos["bbTop"] - above_pos["bbBot"]
|
|
249
|
+
|
|
250
|
+
if add_page_height:
|
|
251
|
+
min_distance += page_height * page_difference
|
|
252
|
+
return min_distance
|
|
253
|
+
|
|
254
|
+
@staticmethod
|
|
255
|
+
def get_horizontal_min_distance(pos1: dict, pos2: dict) -> float:
|
|
256
|
+
"""
|
|
257
|
+
Get the horizontal minimum distance between two bounding boxes
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
float: minimum distance
|
|
261
|
+
"""
|
|
262
|
+
page_difference = abs(pos1["page_num"] - pos2["page_num"])
|
|
263
|
+
if page_difference > 0:
|
|
264
|
+
raise ToolkitInputError(
|
|
265
|
+
"Predictions are not on the same page! Must enter a page height"
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
min_distance_1 = abs(pos1["bbLeft"] - pos2["bbRight"])
|
|
269
|
+
min_distance_2 = abs(pos1["bbRight"] - pos2["bbLeft"])
|
|
270
|
+
return min(min_distance_1, min_distance_2)
|
|
271
|
+
|
|
272
|
+
@staticmethod
|
|
273
|
+
def _distance_between_points(point1: tuple, point2: tuple) -> float:
|
|
274
|
+
x = (point1[0] - point2[0]) ** 2
|
|
275
|
+
y = (point1[1] - point2[1]) ** 2
|
|
276
|
+
return sqrt(x + y)
|
|
277
|
+
|
|
278
|
+
@staticmethod
|
|
279
|
+
def manhattan_distance_between_points(point1: tuple, point2: tuple) -> float:
|
|
280
|
+
x = abs(point1[0] - point2[0])
|
|
281
|
+
y = abs(point1[1] - point2[1])
|
|
282
|
+
return x + y
|
|
283
|
+
|
|
284
|
+
@staticmethod
|
|
285
|
+
def yaxis_overlap(x: dict, y: dict):
|
|
286
|
+
"""
|
|
287
|
+
Two locations overlap on the yaxis, i.e. same "row"
|
|
288
|
+
"""
|
|
289
|
+
return y["bbBot"] > x["bbTop"] and y["bbTop"] < x["bbBot"]
|
|
290
|
+
|
|
291
|
+
@staticmethod
|
|
292
|
+
def xaxis_overlap(x: dict, y: dict):
|
|
293
|
+
return x["bbLeft"] < y["bbRight"] and y["bbLeft"] < x["bbRight"]
|
|
294
|
+
|
|
295
|
+
@staticmethod
|
|
296
|
+
def yaxis_above(above_pos: dict, below_pos: dict):
|
|
297
|
+
return above_pos["bbBot"] < below_pos["bbTop"]
|
|
298
|
+
|
|
299
|
+
@staticmethod
|
|
300
|
+
def on_same_page(pos1: dict, pos2: dict):
|
|
301
|
+
return pos1["page_num"] == pos2["page_num"]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from copy import deepcopy
|
|
3
|
+
from itertools import zip_longest
|
|
4
|
+
from typing import Any, Dict, Iterator
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def split_prediction(
|
|
8
|
+
prediction: Dict[str, Any], separator: str = r"\s*\n\s*"
|
|
9
|
+
) -> Iterator[Dict[str, Any]]:
|
|
10
|
+
"""
|
|
11
|
+
Split a single `prediction` into mutiple predictions along a `separator` regex
|
|
12
|
+
and yield new predictions having updated text and spans. By default, split on
|
|
13
|
+
newlines (including adjacent whitespace).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
# re.split (with group capture) returns a sequence of text values and delimiters:
|
|
17
|
+
# ["some", "\n", "separated", "\n", "text"]
|
|
18
|
+
# The text values are the even elements:
|
|
19
|
+
# ["some", "separated", "text"]
|
|
20
|
+
# The delimeters are the odd elements:
|
|
21
|
+
# ["\n", "\n"]
|
|
22
|
+
# zip_longest pairs them together without losing the last text value:
|
|
23
|
+
# [("some", "\n"), ("separated", "\n"), ("text", None)]
|
|
24
|
+
texts_and_delimiters = re.split(f"({separator})", prediction["text"])
|
|
25
|
+
texts = texts_and_delimiters[::2]
|
|
26
|
+
delimiters = texts_and_delimiters[1::2]
|
|
27
|
+
text_and_delimiter_pairs = zip_longest(texts, delimiters)
|
|
28
|
+
start = prediction["start"]
|
|
29
|
+
|
|
30
|
+
for text, delimiter in text_and_delimiter_pairs:
|
|
31
|
+
end = start + len(text)
|
|
32
|
+
|
|
33
|
+
yield {
|
|
34
|
+
**deepcopy(prediction),
|
|
35
|
+
"text": text,
|
|
36
|
+
"start": start,
|
|
37
|
+
"end": end,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if delimiter is not None:
|
|
41
|
+
start = end + len(delimiter)
|