Skip to content

Commit b9be223

Browse files
⬆️ better custom line items support (#182)
1 parent ec198d1 commit b9be223

File tree

13 files changed

+364
-119
lines changed

13 files changed

+364
-119
lines changed
File renamed without changes.

docs/guide/custom_v1.md renamed to docs/extras/guide/custom_v1.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,53 @@ print(str(result.document.inference.prediction.fields["my-field"]))
8585
print(str(result.document.inference.prediction.classifications["my-classification"]))
8686
```
8787

88+
89+
# 🧪 Custom Line Items
90+
91+
> **⚠️ Warning**: Custom Line Items are an **experimental** feature, results may vary.
92+
93+
94+
Though not supported directly in the API, sometimes you might need to reconstitute line items by hand.
95+
The library provides a tool for this very purpose:
96+
97+
## columns_to_line_items()
98+
The **columns_to_line_items()** function can be called from the document and page level prediction objects.
99+
100+
It takes the following arguments:
101+
102+
* **anchor_names** (`List[str]`): a list of the names of possible anchor (field) candidate for the horizontal placement a line. If all provided anchors are invalid, the `LineItemV1` won't be built.
103+
* **field_names** (`List[str]`): a list of fields to retrieve the values from
104+
* **height_tolerance** (`float`): Optional, the height tolerance used to build the line. It helps when the height of a line can vary unexpectedly.
105+
106+
Example use:
107+
108+
```python
109+
# document-level
110+
response.document.inference.prediction.columns_to_line_items(
111+
anchor_names,
112+
field_names,
113+
0.011 # optional, defaults to 0.01
114+
)
115+
116+
# page-level
117+
response.document.pages[0].prediction.columns_to_line_items(
118+
anchor_names,
119+
field_names,
120+
0.011 # optional, defaults to 0.01
121+
)
122+
```
123+
124+
It returns a list of [CustomLineV1](#CustomlineV1) objects.
125+
126+
## CustomlineV1
127+
128+
`CustomlineV1` represents a line as it has been read from column fields. It has the following attributes:
129+
130+
* **row_number** (`int`): Number of a given line. Starts at 1.
131+
* **fields** (`Dict[str, ListFieldValueV1]`[]): List of the fields associated with the line, indexed by their column name.
132+
* **bbox** (`BBox`): Simple bounding box of the current line representing the 4 minimum & maximum coordinates as `float` values.
133+
134+
88135
# Questions?
89136

90137
[Join our Slack](https://join.slack.com/t/mindee-community/shared_invite/zt-1jv6nawjq-FDgFcF2T5CmMmRpl9LLptw)
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import os
2+
3+
from mindee import Client, product
4+
from mindee.parsing.common.predict_response import PredictResponse
5+
6+
CUSTOM_ENDPOINT_NAME = os.getenv("CUSTOM_ENDPOINT_NAME", "my-endpoint-name")
7+
CUSTOM_ACCOUNT_NAME = os.getenv("CUSTOM_ACCOUNT_NAME", "my-account-name")
8+
CUSTOM_VERSION = os.getenv("CUSTOM_VERSION", "1")
9+
CUSTOM_DOCUMENT_PATH = os.getenv("CUSTOM_DOCUMENT_PATH", "path/to/your/file.ext")
10+
11+
# This example assumes you are following the associated tutorial:
12+
# https://developers.mindee.com/docs/extracting-line-items-tutorial#line-reconstruction-code
13+
anchors = ["category"]
14+
columns = ["category", "previous_year_actual", "year_actual", "year_projection"]
15+
16+
17+
def get_field_content(line, field) -> str:
18+
if field in line.fields:
19+
return str(line.fields[field].content)
20+
return ""
21+
22+
23+
def print_line(line) -> None:
24+
category = get_field_content(line, "category")
25+
previous_year_actual = get_field_content(line, "previous_year_actual")
26+
year_actual = get_field_content(line, "year_actual")
27+
year_projection = get_field_content(line, "year_projection")
28+
# here ljust() fills the rest of the given size with spaces
29+
string_line = (
30+
category.ljust(20, " ")
31+
+ previous_year_actual.ljust(10, " ")
32+
+ year_projection.ljust(10, " ")
33+
+ year_actual
34+
)
35+
36+
print(string_line)
37+
38+
39+
client = Client()
40+
41+
custom_endpoint = client.create_endpoint(
42+
CUSTOM_ENDPOINT_NAME, CUSTOM_ACCOUNT_NAME, CUSTOM_VERSION
43+
)
44+
input_doc = client.source_from_path(CUSTOM_DOCUMENT_PATH)
45+
46+
47+
response: PredictResponse[product.CustomV1] = client.parse(
48+
product.CustomV1, input_doc, endpoint=custom_endpoint
49+
)
50+
line_items = response.document.inference.prediction.columns_to_line_items(
51+
anchors, columns
52+
)
53+
54+
for line in line_items:
55+
print_line(line)

mindee/geometry/bbox.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,31 @@ def get_bbox(points: Points) -> BBox:
4343
x_min = min(v[0] for v in points)
4444
x_max = max(v[0] for v in points)
4545
return BBox(x_min, y_min, x_max, y_max)
46+
47+
48+
def merge_bbox(bbox_1: BBox, bbox_2: BBox) -> BBox:
49+
"""Merges two BBox."""
50+
return BBox(
51+
min(bbox_1.x_min, bbox_2.x_min),
52+
min(bbox_1.y_min, bbox_2.y_min),
53+
max(bbox_1.x_max, bbox_2.x_max),
54+
max(bbox_1.y_max, bbox_2.y_max),
55+
)
56+
57+
58+
def extend_bbox(bbox: BBox, points: Points) -> BBox:
59+
"""
60+
Given a BBox and a sequence of points, calculate the surrounding bbox that encompasses all.
61+
62+
:param bbox: initial BBox to extend.
63+
:param points: Sequence of points to process. Accepts polygons and similar
64+
"""
65+
all_points = []
66+
for point in points:
67+
all_points.append(point)
68+
69+
y_min = min(v[1] for v in all_points)
70+
y_max = max(v[1] for v in all_points)
71+
x_min = min(v[0] for v in all_points)
72+
x_max = max(v[0] for v in all_points)
73+
return merge_bbox(bbox, BBox(x_min, y_min, x_max, y_max))

mindee/geometry/quadrilateral.py

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,22 +24,6 @@ def centroid(self) -> Point:
2424
return get_centroid(self)
2525

2626

27-
def get_bounding_box(points: Points) -> Quadrilateral:
28-
"""
29-
Given a sequence of points, calculate a bounding box that encompasses all points.
30-
31-
:param points: Polygon to process.
32-
:return: A bounding box that encompasses all points.
33-
"""
34-
x_min, y_min, x_max, y_max = get_bbox(points)
35-
return Quadrilateral(
36-
Point(x_min, y_min),
37-
Point(x_max, y_min),
38-
Point(x_max, y_max),
39-
Point(x_min, y_max),
40-
)
41-
42-
4327
def quadrilateral_from_prediction(prediction: Sequence[list]) -> Quadrilateral:
4428
"""
4529
Transform a prediction into a Quadrilateral.
@@ -54,3 +38,19 @@ def quadrilateral_from_prediction(prediction: Sequence[list]) -> Quadrilateral:
5438
Point(prediction[2][0], prediction[2][1]),
5539
Point(prediction[3][0], prediction[3][1]),
5640
)
41+
42+
43+
def get_bounding_box(points: Points) -> Quadrilateral:
44+
"""
45+
Given a sequence of points, calculate a bounding box that encompasses all points.
46+
47+
:param points: Polygon to process.
48+
:return: A bounding box that encompasses all points.
49+
"""
50+
x_min, y_min, x_max, y_max = get_bbox(points)
51+
return Quadrilateral(
52+
Point(x_min, y_min),
53+
Point(x_max, y_min),
54+
Point(x_max, y_max),
55+
Point(x_min, y_max),
56+
)

mindee/parsing/custom/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
from mindee.parsing.custom.classification import ClassificationFieldV1
2-
from mindee.parsing.custom.line_items import LineV1, get_line_items
2+
from mindee.parsing.custom.line_items import CustomLineV1, get_line_items
33
from mindee.parsing.custom.list import ListFieldV1, ListFieldValueV1

0 commit comments

Comments
 (0)