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.
@@ -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,2 @@
1
+ include README.md
2
+ include LICENSE
@@ -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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+