Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

:arrow_up better custom line items support #182

Merged
merged 6 commits into from
Oct 20, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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