yuma-upright 0.1.0__tar.gz
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.
- yuma_upright-0.1.0/LICENSE +21 -0
- yuma_upright-0.1.0/MANIFEST.in +2 -0
- yuma_upright-0.1.0/PKG-INFO +307 -0
- yuma_upright-0.1.0/README.md +273 -0
- yuma_upright-0.1.0/pyproject.toml +48 -0
- yuma_upright-0.1.0/setup.cfg +4 -0
- yuma_upright-0.1.0/src/yuma_upright/__init__.py +40 -0
- yuma_upright-0.1.0/src/yuma_upright/core.py +428 -0
- yuma_upright-0.1.0/src/yuma_upright/exceptions.py +14 -0
- yuma_upright-0.1.0/src/yuma_upright/utils.py +111 -0
- yuma_upright-0.1.0/src/yuma_upright.egg-info/PKG-INFO +307 -0
- yuma_upright-0.1.0/src/yuma_upright.egg-info/SOURCES.txt +14 -0
- yuma_upright-0.1.0/src/yuma_upright.egg-info/dependency_links.txt +1 -0
- yuma_upright-0.1.0/src/yuma_upright.egg-info/requires.txt +8 -0
- yuma_upright-0.1.0/src/yuma_upright.egg-info/top_level.txt +1 -0
- yuma_upright-0.1.0/tests/test_basic.py +43 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Your Name
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: yuma_upright
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: OCR-driven automatic image uprighting (arbitrary-angle deskew) for cropped text regions such as ID card fields, form labels, and document crops.
|
|
5
|
+
Author-email: Your Name <you@example.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/yourusername/yuma_upright
|
|
8
|
+
Project-URL: Repository, https://github.com/yourusername/yuma_upright
|
|
9
|
+
Project-URL: Issues, https://github.com/yourusername/yuma_upright/issues
|
|
10
|
+
Keywords: ocr,deskew,image-rotation,easyocr,computer-vision,document-processing,image-preprocessing,id-card,opencv
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Image Processing
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Requires-Python: >=3.8
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: opencv-python-headless>=4.5
|
|
27
|
+
Requires-Dist: numpy>=1.20
|
|
28
|
+
Requires-Dist: easyocr>=1.7
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: build; extra == "dev"
|
|
31
|
+
Requires-Dist: twine; extra == "dev"
|
|
32
|
+
Requires-Dist: pytest; extra == "dev"
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# yuma_upright
|
|
36
|
+
|
|
37
|
+
OCR-driven automatic image **uprighting** (arbitrary-angle deskew) for
|
|
38
|
+
cropped text regions — ID card fields, form labels, single words/lines cut
|
|
39
|
+
out of a larger document, etc.
|
|
40
|
+
|
|
41
|
+
Unlike classic "deskew" tools that only fix a few degrees of scanner skew,
|
|
42
|
+
`yuma_upright` reads the actual text with OCR and rotates the image by
|
|
43
|
+
**whatever angle is needed** — 15°, 90°, 180°, 233°, anything — so the text
|
|
44
|
+
ends up horizontal, left-to-right, right-side up.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Install
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install yuma_upright
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`easyocr` (and its own dependency, `torch`) will be installed automatically.
|
|
55
|
+
The first time you construct an `Uprighter`, EasyOCR will download its
|
|
56
|
+
model weights (~a few hundred MB) to `~/.EasyOCR/`.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## ⚠️ Important: this expects a cropped image
|
|
61
|
+
|
|
62
|
+
`yuma_upright` OCRs the **whole image you give it** and anchors the
|
|
63
|
+
rotation on the single highest-confidence word it finds. That means:
|
|
64
|
+
|
|
65
|
+
- **Best case:** you feed it a tightly cropped single field/word — a name
|
|
66
|
+
field, a PAN/ID number, a label — with the field's own detector (e.g.
|
|
67
|
+
your RT-DETR model). This is what the library is designed for and it
|
|
68
|
+
works reliably.
|
|
69
|
+
- **Bad case:** you feed it a whole, uncropped, multi-field document. It
|
|
70
|
+
will pick *one* word anywhere on the page (whichever OCRs best) and
|
|
71
|
+
rotate the **entire image** to align just that word — which may be wrong
|
|
72
|
+
for the rest of the page if different fields are rotated differently, or
|
|
73
|
+
it may simply pick a word you didn't intend to anchor on.
|
|
74
|
+
|
|
75
|
+
**If you already have your own detector** (like the RT-DETR model you
|
|
76
|
+
mentioned) that finds field bounding boxes, run detection first, crop to
|
|
77
|
+
the box you trust, *then* pass that crop to `yuma_upright` — or pass the
|
|
78
|
+
box directly via the `box=` parameter (see below) so `yuma_upright` skips
|
|
79
|
+
its own full-image search and anchors on exactly the region you specify.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Quick start
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from yuma_upright import Uprighter
|
|
87
|
+
|
|
88
|
+
# Construct once, reuse across many images (loads the OCR model once)
|
|
89
|
+
up = Uprighter(gpu=False) # set gpu=True if you have CUDA available
|
|
90
|
+
|
|
91
|
+
result = up.upright("pan_number_crop.jpg")
|
|
92
|
+
|
|
93
|
+
result.image # np.ndarray (BGR) — the upright image
|
|
94
|
+
result.angle_deg # float — rotation actually applied, in degrees
|
|
95
|
+
result.axis_word_text # str — the OCR word used to anchor the rotation
|
|
96
|
+
result.ocr_words # list[str] — every word OCR found in the crop
|
|
97
|
+
result.debug # dict — details of the 180° disambiguation pass
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
import cv2
|
|
102
|
+
cv2.imwrite("upright.jpg", result.image)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### One-off convenience call (no need to manage an instance)
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from yuma_upright import upright_image
|
|
109
|
+
|
|
110
|
+
result = upright_image("pan_number_crop.jpg", gpu=False)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Accepted input types
|
|
114
|
+
|
|
115
|
+
`image` can be any of:
|
|
116
|
+
- a file path (`str` or `pathlib.Path`)
|
|
117
|
+
- raw encoded image `bytes` (e.g. from an upload)
|
|
118
|
+
- an already-decoded `numpy.ndarray` (BGR, RGBA/BGRA, or grayscale)
|
|
119
|
+
- a `PIL.Image.Image`
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## API reference
|
|
124
|
+
|
|
125
|
+
### `class Uprighter`
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
Uprighter(
|
|
129
|
+
languages: list[str] = ["en"],
|
|
130
|
+
gpu: bool = True,
|
|
131
|
+
blur_threshold: float = 15.0,
|
|
132
|
+
ocr_min_conf: float = 0.0,
|
|
133
|
+
reader=None,
|
|
134
|
+
)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
| Parameter | Meaning |
|
|
138
|
+
|-------------------|---------|
|
|
139
|
+
| `languages` | EasyOCR language codes. |
|
|
140
|
+
| `gpu` | Whether EasyOCR should use CUDA. |
|
|
141
|
+
| `blur_threshold` | Laplacian-variance floor; images below this are rejected as too blurry to OCR reliably. Lower it if your inputs are naturally soft; raise it to reject marginal images earlier. |
|
|
142
|
+
| `ocr_min_conf` | Minimum per-word OCR confidence to keep. Default `0.0` (blur gate does the filtering instead). |
|
|
143
|
+
| `reader` | Pass an already-built `easyocr.Reader` to reuse across multiple `Uprighter` instances / avoid re-loading weights. |
|
|
144
|
+
|
|
145
|
+
### `Uprighter.upright(image, box=None, resolve_ambiguity=True, invert_direction=False) -> UprightResult`
|
|
146
|
+
|
|
147
|
+
| Parameter | Meaning |
|
|
148
|
+
|----------------------|---------|
|
|
149
|
+
| `image` | Any supported input type (see above). Should be a single cropped field/region — see the warning above. |
|
|
150
|
+
| `box` | **(Roadmap feature, partially available now)** Skip full-image OCR search and anchor the rotation on a specific region instead. Accepts either an axis-aligned `(x1, y1, x2, y2)` box, or a 4-point polygon `[[x,y],[x,y],[x,y],[x,y]]` (top-left, top-right, bottom-right, bottom-left — same convention OCR/detector boxes typically use). Use this if you already have a detector (e.g. your own RT-DETR model) telling you exactly which region to trust as the orientation reference. |
|
|
151
|
+
| `resolve_ambiguity` | Whether to run the 180°-disambiguation OCR pass. `True` by default. Turn off for speed if you don't care about upside-down results. |
|
|
152
|
+
| `invert_direction` | If your outputs consistently rotate the "wrong way" for your data/box convention, set `True` to flip the sign of the computed angle without touching anything else. See "Rotation direction" below. |
|
|
153
|
+
|
|
154
|
+
Returns an `UprightResult`:
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
@dataclass
|
|
158
|
+
class UprightResult:
|
|
159
|
+
image: np.ndarray # rotated (upright) BGR image
|
|
160
|
+
angle_deg: float # final rotation actually applied
|
|
161
|
+
raw_angle_deg: float # angle before 180° disambiguation
|
|
162
|
+
axis_word_text: str | None # OCR text used as the axis anchor
|
|
163
|
+
orientation_used: str | None # "left-right-ends" or "top-bottom-ends"
|
|
164
|
+
ocr_words: list[str] # all words seen during OCR
|
|
165
|
+
debug: dict # 180°-disambiguation candidate details
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### `upright_image(image, gpu=True, languages=None, **kwargs) -> UprightResult`
|
|
169
|
+
|
|
170
|
+
Module-level convenience wrapper around a lazily-shared `Uprighter`
|
|
171
|
+
instance. Good for scripts / one-offs. For processing many images,
|
|
172
|
+
construct your own `Uprighter` once and call `.upright()` in a loop instead
|
|
173
|
+
— it avoids the overhead of re-checking whether the shared instance's
|
|
174
|
+
config matches on every call.
|
|
175
|
+
|
|
176
|
+
### Exceptions
|
|
177
|
+
|
|
178
|
+
All in `yuma_upright.exceptions`, all subclasses of `UprightError`:
|
|
179
|
+
|
|
180
|
+
- `NoTextFoundError` — OCR found nothing usable in the image.
|
|
181
|
+
- `ImageTooBlurryError` — image (or `box` region) failed the blur gate.
|
|
182
|
+
- `InvalidImageInputError` — the `image` argument couldn't be interpreted.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## How it works (short version)
|
|
187
|
+
|
|
188
|
+
1. OCR finds words + their 4-point boxes.
|
|
189
|
+
2. The best word is picked (highest confidence; ambiguous-looking words
|
|
190
|
+
like "SOS"/"NO" are deprioritized since they look the same rotated
|
|
191
|
+
180°).
|
|
192
|
+
3. Each word box has two edge-pairs — `(top, bottom)` and `(left, right)`.
|
|
193
|
+
Whichever pair is *shorter* marks the two ends of the word; their
|
|
194
|
+
midpoints define the reading direction, regardless of the word's
|
|
195
|
+
rotation.
|
|
196
|
+
4. The angle between that direction and horizontal gives the rotation
|
|
197
|
+
needed.
|
|
198
|
+
5. Because geometry alone can't tell which end is the *start* of the word,
|
|
199
|
+
the result can be off by exactly 180°. This is resolved by rotating a
|
|
200
|
+
crop both ways, OCR-ing each, and keeping the orientation with higher
|
|
201
|
+
mean OCR confidence (upside-down text reliably OCRs worse).
|
|
202
|
+
|
|
203
|
+
## Rotation direction
|
|
204
|
+
|
|
205
|
+
The sign convention assumes EasyOCR's box point order (top-left,
|
|
206
|
+
top-right, bottom-right, bottom-left) and a standard image coordinate
|
|
207
|
+
system (y grows downward). If you're feeding in `box=` polygons from a
|
|
208
|
+
different detector/convention and results consistently rotate opposite to
|
|
209
|
+
what you expect, pass `invert_direction=True` rather than modifying the
|
|
210
|
+
angle math yourself.
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## About the RT-DETR field-detection model
|
|
215
|
+
|
|
216
|
+
The original prototype this library is based on used a custom-trained
|
|
217
|
+
RT-DETR model (`best_pan_details.pt`) to find PAN card fields (name,
|
|
218
|
+
father's name, PAN number, DOB) before cropping and uprighting each one.
|
|
219
|
+
|
|
220
|
+
**That detector is intentionally NOT part of this package.** It's a
|
|
221
|
+
domain-specific (PAN card) object-detection model with its own weights
|
|
222
|
+
file, license considerations, and inference dependencies (`ultralytics`) —
|
|
223
|
+
bundling it would make `yuma_upright` heavy and narrow instead of a general
|
|
224
|
+
OCR-uprighting utility. Recommended pattern if you have your own detector
|
|
225
|
+
(RT-DETR, YOLO, or anything else):
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
from ultralytics import RTDETR
|
|
229
|
+
from yuma_upright import Uprighter
|
|
230
|
+
|
|
231
|
+
detector = RTDETR("best_pan_details.pt")
|
|
232
|
+
up = Uprighter(gpu=True)
|
|
233
|
+
|
|
234
|
+
results = detector.predict(image_rgb, conf=0.45, iou=0.45)
|
|
235
|
+
for box in results[0].boxes:
|
|
236
|
+
x1, y1, x2, y2 = box.xyxy[0].cpu().numpy().astype(int)
|
|
237
|
+
field_crop = original_bgr[y1:y2, x1:x2]
|
|
238
|
+
result = up.upright(field_crop)
|
|
239
|
+
# result.image is now the upright crop for this field
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
If there's demand, a separate optional package (e.g. `yuma_upright[pan]`
|
|
243
|
+
or a standalone `yuma_upright_pan_detector`) could wrap this pattern —
|
|
244
|
+
open an issue if you want that.
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## Roadmap / planned improvements
|
|
249
|
+
|
|
250
|
+
- [x] Accept a caller-supplied bounding box / polygon (`box=`) to anchor
|
|
251
|
+
rotation on a known region instead of always searching the whole
|
|
252
|
+
image (basic support shipped in `0.1.0`).
|
|
253
|
+
- [ ] Accept multiple `box=` regions + a policy for combining/voting on
|
|
254
|
+
their angles (useful for multi-line fields).
|
|
255
|
+
- [ ] Optional non-OCR 180° disambiguation (e.g. a lightweight upright/
|
|
256
|
+
upside-down image classifier) as a faster alternative to the current
|
|
257
|
+
double-OCR pass.
|
|
258
|
+
- [ ] Batch API (`upright_many([...])`) that shares one OCR reader
|
|
259
|
+
efficiently across a list of crops.
|
|
260
|
+
- [ ] Pluggable OCR backend (currently hard-wired to EasyOCR).
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## Development / publishing this package to PyPI
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
# from the repo root (where pyproject.toml lives)
|
|
268
|
+
python -m pip install --upgrade build twine
|
|
269
|
+
|
|
270
|
+
# 1. Build sdist + wheel into dist/
|
|
271
|
+
python -m build
|
|
272
|
+
|
|
273
|
+
# 2. (Recommended) upload to TestPyPI first and verify install works
|
|
274
|
+
python -m twine upload --repository testpypi dist/*
|
|
275
|
+
pip install --index-url https://test.pypi.org/simple/ yuma_upright
|
|
276
|
+
|
|
277
|
+
# 3. Upload to real PyPI
|
|
278
|
+
python -m twine upload dist/*
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Notes:
|
|
282
|
+
- You need a PyPI account and an API token (Account settings → API
|
|
283
|
+
tokens). Use `__token__` as the username and the token (starting
|
|
284
|
+
`pypi-...`) as the password when `twine` prompts, or store it in
|
|
285
|
+
`~/.pypirc`:
|
|
286
|
+
|
|
287
|
+
```ini
|
|
288
|
+
[pypi]
|
|
289
|
+
username = __token__
|
|
290
|
+
password = pypi-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
291
|
+
|
|
292
|
+
[testpypi]
|
|
293
|
+
username = __token__
|
|
294
|
+
password = pypi-yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
- `yuma_upright` as a name must be unique on PyPI — check
|
|
298
|
+
`https://pypi.org/project/yuma_upright/` before your first upload; if
|
|
299
|
+
it's taken you'll need to rename the `name` field in `pyproject.toml`
|
|
300
|
+
(the importable Python package name doesn't have to match).
|
|
301
|
+
- Bump `version` in `pyproject.toml` (and `__version__` in
|
|
302
|
+
`src/yuma_upright/__init__.py`) before every re-upload — PyPI does not
|
|
303
|
+
allow re-uploading the same version number, even after deleting it.
|
|
304
|
+
|
|
305
|
+
## License
|
|
306
|
+
|
|
307
|
+
MIT — see `LICENSE`.
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# yuma_upright
|
|
2
|
+
|
|
3
|
+
OCR-driven automatic image **uprighting** (arbitrary-angle deskew) for
|
|
4
|
+
cropped text regions — ID card fields, form labels, single words/lines cut
|
|
5
|
+
out of a larger document, etc.
|
|
6
|
+
|
|
7
|
+
Unlike classic "deskew" tools that only fix a few degrees of scanner skew,
|
|
8
|
+
`yuma_upright` reads the actual text with OCR and rotates the image by
|
|
9
|
+
**whatever angle is needed** — 15°, 90°, 180°, 233°, anything — so the text
|
|
10
|
+
ends up horizontal, left-to-right, right-side up.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install yuma_upright
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`easyocr` (and its own dependency, `torch`) will be installed automatically.
|
|
21
|
+
The first time you construct an `Uprighter`, EasyOCR will download its
|
|
22
|
+
model weights (~a few hundred MB) to `~/.EasyOCR/`.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## ⚠️ Important: this expects a cropped image
|
|
27
|
+
|
|
28
|
+
`yuma_upright` OCRs the **whole image you give it** and anchors the
|
|
29
|
+
rotation on the single highest-confidence word it finds. That means:
|
|
30
|
+
|
|
31
|
+
- **Best case:** you feed it a tightly cropped single field/word — a name
|
|
32
|
+
field, a PAN/ID number, a label — with the field's own detector (e.g.
|
|
33
|
+
your RT-DETR model). This is what the library is designed for and it
|
|
34
|
+
works reliably.
|
|
35
|
+
- **Bad case:** you feed it a whole, uncropped, multi-field document. It
|
|
36
|
+
will pick *one* word anywhere on the page (whichever OCRs best) and
|
|
37
|
+
rotate the **entire image** to align just that word — which may be wrong
|
|
38
|
+
for the rest of the page if different fields are rotated differently, or
|
|
39
|
+
it may simply pick a word you didn't intend to anchor on.
|
|
40
|
+
|
|
41
|
+
**If you already have your own detector** (like the RT-DETR model you
|
|
42
|
+
mentioned) that finds field bounding boxes, run detection first, crop to
|
|
43
|
+
the box you trust, *then* pass that crop to `yuma_upright` — or pass the
|
|
44
|
+
box directly via the `box=` parameter (see below) so `yuma_upright` skips
|
|
45
|
+
its own full-image search and anchors on exactly the region you specify.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Quick start
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from yuma_upright import Uprighter
|
|
53
|
+
|
|
54
|
+
# Construct once, reuse across many images (loads the OCR model once)
|
|
55
|
+
up = Uprighter(gpu=False) # set gpu=True if you have CUDA available
|
|
56
|
+
|
|
57
|
+
result = up.upright("pan_number_crop.jpg")
|
|
58
|
+
|
|
59
|
+
result.image # np.ndarray (BGR) — the upright image
|
|
60
|
+
result.angle_deg # float — rotation actually applied, in degrees
|
|
61
|
+
result.axis_word_text # str — the OCR word used to anchor the rotation
|
|
62
|
+
result.ocr_words # list[str] — every word OCR found in the crop
|
|
63
|
+
result.debug # dict — details of the 180° disambiguation pass
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
import cv2
|
|
68
|
+
cv2.imwrite("upright.jpg", result.image)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### One-off convenience call (no need to manage an instance)
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from yuma_upright import upright_image
|
|
75
|
+
|
|
76
|
+
result = upright_image("pan_number_crop.jpg", gpu=False)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Accepted input types
|
|
80
|
+
|
|
81
|
+
`image` can be any of:
|
|
82
|
+
- a file path (`str` or `pathlib.Path`)
|
|
83
|
+
- raw encoded image `bytes` (e.g. from an upload)
|
|
84
|
+
- an already-decoded `numpy.ndarray` (BGR, RGBA/BGRA, or grayscale)
|
|
85
|
+
- a `PIL.Image.Image`
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## API reference
|
|
90
|
+
|
|
91
|
+
### `class Uprighter`
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
Uprighter(
|
|
95
|
+
languages: list[str] = ["en"],
|
|
96
|
+
gpu: bool = True,
|
|
97
|
+
blur_threshold: float = 15.0,
|
|
98
|
+
ocr_min_conf: float = 0.0,
|
|
99
|
+
reader=None,
|
|
100
|
+
)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
| Parameter | Meaning |
|
|
104
|
+
|-------------------|---------|
|
|
105
|
+
| `languages` | EasyOCR language codes. |
|
|
106
|
+
| `gpu` | Whether EasyOCR should use CUDA. |
|
|
107
|
+
| `blur_threshold` | Laplacian-variance floor; images below this are rejected as too blurry to OCR reliably. Lower it if your inputs are naturally soft; raise it to reject marginal images earlier. |
|
|
108
|
+
| `ocr_min_conf` | Minimum per-word OCR confidence to keep. Default `0.0` (blur gate does the filtering instead). |
|
|
109
|
+
| `reader` | Pass an already-built `easyocr.Reader` to reuse across multiple `Uprighter` instances / avoid re-loading weights. |
|
|
110
|
+
|
|
111
|
+
### `Uprighter.upright(image, box=None, resolve_ambiguity=True, invert_direction=False) -> UprightResult`
|
|
112
|
+
|
|
113
|
+
| Parameter | Meaning |
|
|
114
|
+
|----------------------|---------|
|
|
115
|
+
| `image` | Any supported input type (see above). Should be a single cropped field/region — see the warning above. |
|
|
116
|
+
| `box` | **(Roadmap feature, partially available now)** Skip full-image OCR search and anchor the rotation on a specific region instead. Accepts either an axis-aligned `(x1, y1, x2, y2)` box, or a 4-point polygon `[[x,y],[x,y],[x,y],[x,y]]` (top-left, top-right, bottom-right, bottom-left — same convention OCR/detector boxes typically use). Use this if you already have a detector (e.g. your own RT-DETR model) telling you exactly which region to trust as the orientation reference. |
|
|
117
|
+
| `resolve_ambiguity` | Whether to run the 180°-disambiguation OCR pass. `True` by default. Turn off for speed if you don't care about upside-down results. |
|
|
118
|
+
| `invert_direction` | If your outputs consistently rotate the "wrong way" for your data/box convention, set `True` to flip the sign of the computed angle without touching anything else. See "Rotation direction" below. |
|
|
119
|
+
|
|
120
|
+
Returns an `UprightResult`:
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
@dataclass
|
|
124
|
+
class UprightResult:
|
|
125
|
+
image: np.ndarray # rotated (upright) BGR image
|
|
126
|
+
angle_deg: float # final rotation actually applied
|
|
127
|
+
raw_angle_deg: float # angle before 180° disambiguation
|
|
128
|
+
axis_word_text: str | None # OCR text used as the axis anchor
|
|
129
|
+
orientation_used: str | None # "left-right-ends" or "top-bottom-ends"
|
|
130
|
+
ocr_words: list[str] # all words seen during OCR
|
|
131
|
+
debug: dict # 180°-disambiguation candidate details
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### `upright_image(image, gpu=True, languages=None, **kwargs) -> UprightResult`
|
|
135
|
+
|
|
136
|
+
Module-level convenience wrapper around a lazily-shared `Uprighter`
|
|
137
|
+
instance. Good for scripts / one-offs. For processing many images,
|
|
138
|
+
construct your own `Uprighter` once and call `.upright()` in a loop instead
|
|
139
|
+
— it avoids the overhead of re-checking whether the shared instance's
|
|
140
|
+
config matches on every call.
|
|
141
|
+
|
|
142
|
+
### Exceptions
|
|
143
|
+
|
|
144
|
+
All in `yuma_upright.exceptions`, all subclasses of `UprightError`:
|
|
145
|
+
|
|
146
|
+
- `NoTextFoundError` — OCR found nothing usable in the image.
|
|
147
|
+
- `ImageTooBlurryError` — image (or `box` region) failed the blur gate.
|
|
148
|
+
- `InvalidImageInputError` — the `image` argument couldn't be interpreted.
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## How it works (short version)
|
|
153
|
+
|
|
154
|
+
1. OCR finds words + their 4-point boxes.
|
|
155
|
+
2. The best word is picked (highest confidence; ambiguous-looking words
|
|
156
|
+
like "SOS"/"NO" are deprioritized since they look the same rotated
|
|
157
|
+
180°).
|
|
158
|
+
3. Each word box has two edge-pairs — `(top, bottom)` and `(left, right)`.
|
|
159
|
+
Whichever pair is *shorter* marks the two ends of the word; their
|
|
160
|
+
midpoints define the reading direction, regardless of the word's
|
|
161
|
+
rotation.
|
|
162
|
+
4. The angle between that direction and horizontal gives the rotation
|
|
163
|
+
needed.
|
|
164
|
+
5. Because geometry alone can't tell which end is the *start* of the word,
|
|
165
|
+
the result can be off by exactly 180°. This is resolved by rotating a
|
|
166
|
+
crop both ways, OCR-ing each, and keeping the orientation with higher
|
|
167
|
+
mean OCR confidence (upside-down text reliably OCRs worse).
|
|
168
|
+
|
|
169
|
+
## Rotation direction
|
|
170
|
+
|
|
171
|
+
The sign convention assumes EasyOCR's box point order (top-left,
|
|
172
|
+
top-right, bottom-right, bottom-left) and a standard image coordinate
|
|
173
|
+
system (y grows downward). If you're feeding in `box=` polygons from a
|
|
174
|
+
different detector/convention and results consistently rotate opposite to
|
|
175
|
+
what you expect, pass `invert_direction=True` rather than modifying the
|
|
176
|
+
angle math yourself.
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## About the RT-DETR field-detection model
|
|
181
|
+
|
|
182
|
+
The original prototype this library is based on used a custom-trained
|
|
183
|
+
RT-DETR model (`best_pan_details.pt`) to find PAN card fields (name,
|
|
184
|
+
father's name, PAN number, DOB) before cropping and uprighting each one.
|
|
185
|
+
|
|
186
|
+
**That detector is intentionally NOT part of this package.** It's a
|
|
187
|
+
domain-specific (PAN card) object-detection model with its own weights
|
|
188
|
+
file, license considerations, and inference dependencies (`ultralytics`) —
|
|
189
|
+
bundling it would make `yuma_upright` heavy and narrow instead of a general
|
|
190
|
+
OCR-uprighting utility. Recommended pattern if you have your own detector
|
|
191
|
+
(RT-DETR, YOLO, or anything else):
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
from ultralytics import RTDETR
|
|
195
|
+
from yuma_upright import Uprighter
|
|
196
|
+
|
|
197
|
+
detector = RTDETR("best_pan_details.pt")
|
|
198
|
+
up = Uprighter(gpu=True)
|
|
199
|
+
|
|
200
|
+
results = detector.predict(image_rgb, conf=0.45, iou=0.45)
|
|
201
|
+
for box in results[0].boxes:
|
|
202
|
+
x1, y1, x2, y2 = box.xyxy[0].cpu().numpy().astype(int)
|
|
203
|
+
field_crop = original_bgr[y1:y2, x1:x2]
|
|
204
|
+
result = up.upright(field_crop)
|
|
205
|
+
# result.image is now the upright crop for this field
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
If there's demand, a separate optional package (e.g. `yuma_upright[pan]`
|
|
209
|
+
or a standalone `yuma_upright_pan_detector`) could wrap this pattern —
|
|
210
|
+
open an issue if you want that.
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## Roadmap / planned improvements
|
|
215
|
+
|
|
216
|
+
- [x] Accept a caller-supplied bounding box / polygon (`box=`) to anchor
|
|
217
|
+
rotation on a known region instead of always searching the whole
|
|
218
|
+
image (basic support shipped in `0.1.0`).
|
|
219
|
+
- [ ] Accept multiple `box=` regions + a policy for combining/voting on
|
|
220
|
+
their angles (useful for multi-line fields).
|
|
221
|
+
- [ ] Optional non-OCR 180° disambiguation (e.g. a lightweight upright/
|
|
222
|
+
upside-down image classifier) as a faster alternative to the current
|
|
223
|
+
double-OCR pass.
|
|
224
|
+
- [ ] Batch API (`upright_many([...])`) that shares one OCR reader
|
|
225
|
+
efficiently across a list of crops.
|
|
226
|
+
- [ ] Pluggable OCR backend (currently hard-wired to EasyOCR).
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## Development / publishing this package to PyPI
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
# from the repo root (where pyproject.toml lives)
|
|
234
|
+
python -m pip install --upgrade build twine
|
|
235
|
+
|
|
236
|
+
# 1. Build sdist + wheel into dist/
|
|
237
|
+
python -m build
|
|
238
|
+
|
|
239
|
+
# 2. (Recommended) upload to TestPyPI first and verify install works
|
|
240
|
+
python -m twine upload --repository testpypi dist/*
|
|
241
|
+
pip install --index-url https://test.pypi.org/simple/ yuma_upright
|
|
242
|
+
|
|
243
|
+
# 3. Upload to real PyPI
|
|
244
|
+
python -m twine upload dist/*
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Notes:
|
|
248
|
+
- You need a PyPI account and an API token (Account settings → API
|
|
249
|
+
tokens). Use `__token__` as the username and the token (starting
|
|
250
|
+
`pypi-...`) as the password when `twine` prompts, or store it in
|
|
251
|
+
`~/.pypirc`:
|
|
252
|
+
|
|
253
|
+
```ini
|
|
254
|
+
[pypi]
|
|
255
|
+
username = __token__
|
|
256
|
+
password = pypi-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
257
|
+
|
|
258
|
+
[testpypi]
|
|
259
|
+
username = __token__
|
|
260
|
+
password = pypi-yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
- `yuma_upright` as a name must be unique on PyPI — check
|
|
264
|
+
`https://pypi.org/project/yuma_upright/` before your first upload; if
|
|
265
|
+
it's taken you'll need to rename the `name` field in `pyproject.toml`
|
|
266
|
+
(the importable Python package name doesn't have to match).
|
|
267
|
+
- Bump `version` in `pyproject.toml` (and `__version__` in
|
|
268
|
+
`src/yuma_upright/__init__.py`) before every re-upload — PyPI does not
|
|
269
|
+
allow re-uploading the same version number, even after deleting it.
|
|
270
|
+
|
|
271
|
+
## License
|
|
272
|
+
|
|
273
|
+
MIT — see `LICENSE`.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "yuma_upright"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "OCR-driven automatic image uprighting (arbitrary-angle deskew) for cropped text regions such as ID card fields, form labels, and document crops."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Your Name", email = "you@example.com" }
|
|
14
|
+
]
|
|
15
|
+
keywords = [
|
|
16
|
+
"ocr", "deskew", "image-rotation", "easyocr", "computer-vision",
|
|
17
|
+
"document-processing", "image-preprocessing", "id-card", "opencv"
|
|
18
|
+
]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Development Status :: 3 - Alpha",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"License :: OSI Approved :: MIT License",
|
|
23
|
+
"Operating System :: OS Independent",
|
|
24
|
+
"Programming Language :: Python :: 3",
|
|
25
|
+
"Programming Language :: Python :: 3.8",
|
|
26
|
+
"Programming Language :: Python :: 3.9",
|
|
27
|
+
"Programming Language :: Python :: 3.10",
|
|
28
|
+
"Programming Language :: Python :: 3.11",
|
|
29
|
+
"Programming Language :: Python :: 3.12",
|
|
30
|
+
"Topic :: Scientific/Engineering :: Image Processing",
|
|
31
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
32
|
+
]
|
|
33
|
+
dependencies = [
|
|
34
|
+
"opencv-python-headless>=4.5",
|
|
35
|
+
"numpy>=1.20",
|
|
36
|
+
"easyocr>=1.7",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[project.optional-dependencies]
|
|
40
|
+
dev = ["build", "twine", "pytest"]
|
|
41
|
+
|
|
42
|
+
[project.urls]
|
|
43
|
+
Homepage = "https://github.com/yourusername/yuma_upright"
|
|
44
|
+
Repository = "https://github.com/yourusername/yuma_upright"
|
|
45
|
+
Issues = "https://github.com/yourusername/yuma_upright/issues"
|
|
46
|
+
|
|
47
|
+
[tool.setuptools.packages.find]
|
|
48
|
+
where = ["src"]
|