Skip to content

Commit aef7439

Browse files
committed
✨ add Cropper support
1 parent a780b53 commit aef7439

File tree

13 files changed

+190
-15
lines changed

13 files changed

+190
-15
lines changed

.pylintrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ indent-after-paren=4
249249
indent-string=' '
250250

251251
# Maximum number of characters on a single line.
252-
max-line-length=100
252+
max-line-length=120
253253

254254
# Maximum number of lines in a module.
255255
max-module-lines=1000

mindee/client.py

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from typing import BinaryIO, Dict, Optional, Type
33

44
from mindee.documents import (
5+
CropperV1,
56
CustomV1,
67
FinancialV1,
78
InvoiceV3,
@@ -53,19 +54,33 @@ def parse(
5354
include_words: bool = False,
5455
close_file: bool = True,
5556
page_options: Optional[PageOptions] = None,
57+
cropper: bool = False,
5658
) -> PredictResponse[TypeDocument]:
5759
"""
5860
Call prediction API on the document and parse the results.
5961
6062
:param document_class: The document class to use.
61-
The response object will be instanced based on this parameter.
62-
:param endpoint_name: For custom documents, the "API name" field
63-
in the "Settings" page of the API Builder.
64-
:param account_name: API username, the endpoint owner
65-
:param include_words: Include all the words of the document in the response
63+
The response object will be instantiated based on this parameter.
64+
65+
:param endpoint_name: For custom endpoints, the "API name" field in the "Settings" page of the API Builder.
66+
Do not set for standard (off the shelf) endpoints.
67+
68+
:param account_name: For custom endpoints, your account or organization username on the API Builder.
69+
This is normally not required unless you have a custom endpoint which has the
70+
same name as standard (off the shelf) endpoint.
71+
Do not set for standard (off the shelf) endpoints.
72+
73+
:param include_words: Whether to include the full text for each page.
74+
This performs a full OCR operation on the server and will increase response time.
75+
6676
:param close_file: Whether to ``close()`` the file after parsing it.
6777
Set to ``False`` if you need to access the file after this operation.
68-
:param page_options: Options for preparing multipage documents.
78+
79+
:param page_options: If set, remove pages from the document as specified.
80+
This is done before sending the file to the server and is useful to avoid page limitations.
81+
82+
:param cropper: Whether to include cropper results for each page.
83+
This performs a cropping operation on the server and will increase response time.
6984
"""
7085
bound_classname = get_bound_classname(document_class)
7186
if bound_classname != CustomV1.__name__:
@@ -108,14 +123,17 @@ def parse(
108123
page_options.on_min_pages,
109124
page_options.page_indexes,
110125
)
111-
return self._make_request(document_class, doc_config, include_words, close_file)
126+
return self._make_request(
127+
document_class, doc_config, include_words, close_file, cropper
128+
)
112129

113130
def _make_request(
114131
self,
115132
document_class: TypeDocument,
116133
doc_config: DocumentConfig,
117134
include_words: bool,
118135
close_file: bool,
136+
cropper: bool,
119137
) -> PredictResponse[TypeDocument]:
120138
if get_bound_classname(document_class) != doc_config.document_class.__name__:
121139
raise RuntimeError("Document class mismatch!")
@@ -125,6 +143,7 @@ def _make_request(
125143
self.input_doc,
126144
include_words=include_words,
127145
close_file=close_file,
146+
cropper=cropper,
128147
)
129148

130149
dict_response = response.json()
@@ -228,6 +247,15 @@ def _init_default_endpoints(self) -> None:
228247
)
229248
],
230249
),
250+
(OTS_OWNER, CropperV1.__name__): DocumentConfig(
251+
document_type="cropper_v1",
252+
document_class=CropperV1,
253+
endpoints=[
254+
StandardEndpoint(
255+
url_name="cropper", version="1", api_key=self.api_key
256+
)
257+
],
258+
),
231259
}
232260

233261
def add_endpoint(

mindee/documents/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from mindee.documents import us
2+
from mindee.documents.cropper import CropperV1, TypeCropperV1
23
from mindee.documents.custom import CustomV1, TypeCustomV1
34
from mindee.documents.financial import FinancialV1, TypeFinancialV1
45
from mindee.documents.invoice import InvoiceV3, TypeInvoiceV3

mindee/documents/base.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from mindee.endpoints import Endpoint
66
from mindee.fields.orientation import OrientationField
7+
from mindee.fields.position import PositionField
78
from mindee.input.sources import InputSource
89

910
TypeApiPrediction = Dict[str, Any]
@@ -40,6 +41,8 @@ class Document:
4041
# orientation is only present on page-level, not document-level
4142
orientation: Optional[OrientationField] = None
4243
"""Page orientation"""
44+
cropper: List[PositionField]
45+
"""Cropper results"""
4346

4447
def __init__(
4548
self,
@@ -56,6 +59,7 @@ def __init__(
5659
self.type = document_type
5760

5861
if page_n is not None:
62+
self._set_extras(api_prediction["extras"])
5963
self.orientation = OrientationField(
6064
api_prediction["orientation"], page_n=page_n
6165
)
@@ -66,6 +70,7 @@ def request(
6670
input_source: InputSource,
6771
include_words: bool = False,
6872
close_file: bool = True,
73+
cropper: bool = False,
6974
):
7075
"""
7176
Make request to prediction endpoint.
@@ -74,8 +79,19 @@ def request(
7479
:param endpoints: Endpoints config
7580
:param include_words: Include Mindee vision words in http_response
7681
:param close_file: Whether to `close()` the file after parsing it.
82+
:param cropper: Including Mindee cropper results.
7783
"""
78-
return endpoints[0].predict_req_post(input_source, include_words, close_file)
84+
return endpoints[0].predict_req_post(
85+
input_source, include_words, close_file, cropper=cropper
86+
)
87+
88+
def _set_extras(self, extras: TypeApiPrediction):
89+
self.cropper = []
90+
try:
91+
for crop in extras["cropper"]["cropping"]:
92+
self.cropper.append(PositionField(crop))
93+
except KeyError:
94+
pass
7995

8096
def _build_from_api_prediction(
8197
self, api_prediction: TypeApiPrediction, page_n: Optional[int] = None
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .cropper_v1 import CropperV1, TypeCropperV1
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
from typing import List, Optional, TypeVar
2+
3+
from mindee.documents.base import Document, TypeApiPrediction, clean_out_string
4+
from mindee.fields.position import PositionField
5+
6+
7+
class CropperV1(Document):
8+
cropping: List[PositionField]
9+
"""List of all detected cropped elements in the image"""
10+
11+
def __init__(
12+
self,
13+
api_prediction: TypeApiPrediction,
14+
input_source=None,
15+
page_n: Optional[int] = None,
16+
document_type="cropper",
17+
):
18+
"""
19+
Custom document object.
20+
21+
:param document_type: Document type
22+
:param api_prediction: Raw prediction from HTTP response
23+
:param input_source: Input object
24+
:param page_n: Page number for multi pages pdf input
25+
"""
26+
super().__init__(
27+
input_source=input_source,
28+
document_type=document_type,
29+
api_prediction=api_prediction,
30+
page_n=page_n,
31+
)
32+
self._build_from_api_prediction(api_prediction["prediction"], page_n=page_n)
33+
34+
def _build_from_api_prediction(
35+
self, api_prediction: TypeApiPrediction, page_n: Optional[int] = None
36+
) -> None:
37+
"""Build the document from an API response JSON."""
38+
self.cropping = []
39+
40+
# cropping is only present on pages
41+
if page_n is None:
42+
return
43+
44+
for crop in api_prediction["cropping"]:
45+
self.cropping.append(PositionField(prediction=crop))
46+
47+
def _checklist(self) -> None:
48+
pass
49+
50+
def __str__(self):
51+
cropping = "\n ".join([str(crop) for crop in self.cropping])
52+
return clean_out_string(
53+
"----- Cropper Data -----\n"
54+
f"Filename: {self.filename or ''}\n"
55+
f"Cropping: {cropping}\n"
56+
"------------------------"
57+
)
58+
59+
60+
TypeCropperV1 = TypeVar("TypeCropperV1", bound=CropperV1)

mindee/documents/financial/financial_v1.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from mindee.fields.payment_details import PaymentDetails
1212
from mindee.fields.tax import TaxField
1313
from mindee.fields.text import TextField
14+
from mindee.input.sources import InputSource
1415

1516

1617
class FinancialV1(Document):
@@ -150,9 +151,10 @@ def __str__(self) -> str:
150151
@staticmethod
151152
def request(
152153
endpoints: List[Endpoint],
153-
input_source,
154+
input_source: InputSource,
154155
include_words: bool = False,
155156
close_file: bool = True,
157+
cropper: bool = False,
156158
):
157159
"""
158160
Make request to prediction endpoint.
@@ -161,14 +163,15 @@ def request(
161163
:param endpoints: Endpoints config
162164
:param include_words: Include Mindee vision words in http_response
163165
:param close_file: Whether to `close()` the file after parsing it.
166+
:param cropper: Including Mindee cropper results.
164167
"""
165168
if "pdf" in input_source.file_mimetype:
166169
# invoices is index 0, receipts 1 (this should be cleaned up)
167170
index = 0
168171
else:
169172
index = 1
170173
return endpoints[index].predict_req_post(
171-
input_source, include_words, close_file
174+
input_source, include_words, close_file, cropper=cropper
172175
)
173176

174177
def _checklist(self) -> None:

mindee/documents/us/bank_check/bank_check_v1.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,7 @@ def _build_from_api_prediction(
7373
]
7474

7575
def __str__(self) -> str:
76-
payees = ", ".join(
77-
[payee.value if payee.value is not None else "" for payee in self.payees]
78-
)
76+
payees = ", ".join([str(payee) for payee in self.payees])
7977
return clean_out_string(
8078
"----- US Bank Check V1 -----\n"
8179
f"Filename: {self.filename or ''}\n"

mindee/endpoints.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,25 +84,32 @@ def predict_req_post(
8484
input_source: InputSource,
8585
include_words: bool = False,
8686
close_file: bool = True,
87+
cropper: bool = False,
8788
) -> requests.Response:
8889
"""
8990
Make a request to POST a document for prediction.
9091
9192
:param input_source: Input object
9293
:param include_words: Include raw OCR words in the response
9394
:param close_file: Whether to `close()` the file after parsing it.
95+
:param cropper: Including Mindee cropping results.
9496
:return: requests response
9597
"""
9698
files = {"document": input_source.read_contents(close_file)}
9799
data = {}
98100
if include_words:
99101
data["include_mvision"] = "true"
100102

103+
params = {}
104+
if cropper:
105+
params["cropper"] = "true"
106+
101107
response = requests.post(
102108
f"{self._url_root}/predict",
103109
files=files,
104110
headers=self.base_headers,
105111
data=data,
112+
params=params,
106113
timeout=self.timeout,
107114
)
108115
return response

mindee/fields/position.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,15 @@
1212

1313
class PositionField(BaseField):
1414
value: Optional[Polygon] = None
15+
"""Polygon of cropped area, identical to the ``polygon`` property."""
1516
polygon: Optional[Polygon] = None
17+
"""Polygon of cropped area"""
1618
quadrangle: Optional[Quadrilateral] = None
19+
"""Quadrangle of cropped area (does not exceed the canvas)"""
1720
rectangle: Optional[Quadrilateral] = None
21+
"""Oriented rectangle of cropped area (may exceed the canvas)"""
1822
bounding_box: Optional[Quadrilateral] = None
23+
"""Straight rectangle of cropped area (does not exceed the canvas)"""
1924

2025
def __init__(
2126
self,
@@ -57,3 +62,8 @@ def get_polygon(key: str) -> Optional[Polygon]:
5762
self.polygon = get_polygon("polygon")
5863

5964
self.value = self.polygon
65+
66+
def __str__(self) -> str:
67+
if self.polygon is None:
68+
return ""
69+
return f"Polygon with {len(self.polygon)} points."

0 commit comments

Comments
 (0)