Skip to content

Commit

Permalink
:arrow_up better custom line items support (#182)
Browse files Browse the repository at this point in the history
  • Loading branch information
sebastianMindee authored Oct 20, 2023
1 parent 9f706aa commit f541bfc
Show file tree
Hide file tree
Showing 13 changed files with 364 additions and 119 deletions.
File renamed without changes.
47 changes: 47 additions & 0 deletions docs/guide/custom_v1.md → docs/extras/guide/custom_v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,53 @@ print(str(result.document.inference.prediction.fields["my-field"]))
print(str(result.document.inference.prediction.classifications["my-classification"]))
```


# 🧪 Custom Line Items

> **⚠️ Warning**: Custom Line Items are an **experimental** feature, results may vary.

Though not supported directly in the API, sometimes you might need to reconstitute line items by hand.
The library provides a tool for this very purpose:

## columns_to_line_items()
The **columns_to_line_items()** function can be called from the document and page level prediction objects.

It takes the following arguments:

* **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.
* **field_names** (`List[str]`): a list of fields to retrieve the values from
* **height_tolerance** (`float`): Optional, the height tolerance used to build the line. It helps when the height of a line can vary unexpectedly.

Example use:

```python
# document-level
response.document.inference.prediction.columns_to_line_items(
anchor_names,
field_names,
0.011 # optional, defaults to 0.01
)

# page-level
response.document.pages[0].prediction.columns_to_line_items(
anchor_names,
field_names,
0.011 # optional, defaults to 0.01
)
```

It returns a list of [CustomLineV1](#CustomlineV1) objects.

## CustomlineV1

`CustomlineV1` represents a line as it has been read from column fields. It has the following attributes:

* **row_number** (`int`): Number of a given line. Starts at 1.
* **fields** (`Dict[str, ListFieldValueV1]`[]): List of the fields associated with the line, indexed by their column name.
* **bbox** (`BBox`): Simple bounding box of the current line representing the 4 minimum & maximum coordinates as `float` values.


# Questions?

[Join our Slack](https://join.slack.com/t/mindee-community/shared_invite/zt-1jv6nawjq-FDgFcF2T5CmMmRpl9LLptw)
55 changes: 55 additions & 0 deletions examples/custom_line_items_reconstruction.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import os

from mindee import Client, product
from mindee.parsing.common.predict_response import PredictResponse

CUSTOM_ENDPOINT_NAME = os.getenv("CUSTOM_ENDPOINT_NAME", "my-endpoint-name")
CUSTOM_ACCOUNT_NAME = os.getenv("CUSTOM_ACCOUNT_NAME", "my-account-name")
CUSTOM_VERSION = os.getenv("CUSTOM_VERSION", "1")
CUSTOM_DOCUMENT_PATH = os.getenv("CUSTOM_DOCUMENT_PATH", "path/to/your/file.ext")

# This example assumes you are following the associated tutorial:
# https://developers.mindee.com/docs/extracting-line-items-tutorial#line-reconstruction-code
anchors = ["category"]
columns = ["category", "previous_year_actual", "year_actual", "year_projection"]


def get_field_content(line, field) -> str:
if field in line.fields:
return str(line.fields[field].content)
return ""


def print_line(line) -> None:
category = get_field_content(line, "category")
previous_year_actual = get_field_content(line, "previous_year_actual")
year_actual = get_field_content(line, "year_actual")
year_projection = get_field_content(line, "year_projection")
# here ljust() fills the rest of the given size with spaces
string_line = (
category.ljust(20, " ")
+ previous_year_actual.ljust(10, " ")
+ year_projection.ljust(10, " ")
+ year_actual
)

print(string_line)


client = Client()

custom_endpoint = client.create_endpoint(
CUSTOM_ENDPOINT_NAME, CUSTOM_ACCOUNT_NAME, CUSTOM_VERSION
)
input_doc = client.source_from_path(CUSTOM_DOCUMENT_PATH)


response: PredictResponse[product.CustomV1] = client.parse(
product.CustomV1, input_doc, endpoint=custom_endpoint
)
line_items = response.document.inference.prediction.columns_to_line_items(
anchors, columns
)

for line in line_items:
print_line(line)
28 changes: 28 additions & 0 deletions mindee/geometry/bbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,31 @@ def get_bbox(points: Points) -> BBox:
x_min = min(v[0] for v in points)
x_max = max(v[0] for v in points)
return BBox(x_min, y_min, x_max, y_max)


def merge_bbox(bbox_1: BBox, bbox_2: BBox) -> BBox:
"""Merges two BBox."""
return BBox(
min(bbox_1.x_min, bbox_2.x_min),
min(bbox_1.y_min, bbox_2.y_min),
max(bbox_1.x_max, bbox_2.x_max),
max(bbox_1.y_max, bbox_2.y_max),
)


def extend_bbox(bbox: BBox, points: Points) -> BBox:
"""
Given a BBox and a sequence of points, calculate the surrounding bbox that encompasses all.
:param bbox: initial BBox to extend.
:param points: Sequence of points to process. Accepts polygons and similar
"""
all_points = []
for point in points:
all_points.append(point)

y_min = min(v[1] for v in all_points)
y_max = max(v[1] for v in all_points)
x_min = min(v[0] for v in all_points)
x_max = max(v[0] for v in all_points)
return merge_bbox(bbox, BBox(x_min, y_min, x_max, y_max))
32 changes: 16 additions & 16 deletions mindee/geometry/quadrilateral.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,6 @@ def centroid(self) -> Point:
return get_centroid(self)


def get_bounding_box(points: Points) -> Quadrilateral:
"""
Given a sequence of points, calculate a bounding box that encompasses all points.
:param points: Polygon to process.
:return: A bounding box that encompasses all points.
"""
x_min, y_min, x_max, y_max = get_bbox(points)
return Quadrilateral(
Point(x_min, y_min),
Point(x_max, y_min),
Point(x_max, y_max),
Point(x_min, y_max),
)


def quadrilateral_from_prediction(prediction: Sequence[list]) -> Quadrilateral:
"""
Transform a prediction into a Quadrilateral.
Expand All @@ -54,3 +38,19 @@ def quadrilateral_from_prediction(prediction: Sequence[list]) -> Quadrilateral:
Point(prediction[2][0], prediction[2][1]),
Point(prediction[3][0], prediction[3][1]),
)


def get_bounding_box(points: Points) -> Quadrilateral:
"""
Given a sequence of points, calculate a bounding box that encompasses all points.
:param points: Polygon to process.
:return: A bounding box that encompasses all points.
"""
x_min, y_min, x_max, y_max = get_bbox(points)
return Quadrilateral(
Point(x_min, y_min),
Point(x_max, y_min),
Point(x_max, y_max),
Point(x_min, y_max),
)
2 changes: 1 addition & 1 deletion mindee/parsing/custom/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from mindee.parsing.custom.classification import ClassificationFieldV1
from mindee.parsing.custom.line_items import LineV1, get_line_items
from mindee.parsing.custom.line_items import CustomLineV1, get_line_items
from mindee.parsing.custom.list import ListFieldV1, ListFieldValueV1
Loading

0 comments on commit f541bfc

Please sign in to comment.