Skip to main content

yuma_upright

OCR-driven automatic image uprighting (arbitrary-angle deskew) for cropped text regions — ID card fields, form labels, single words/lines cut out of a larger document, etc.

Unlike classic "deskew" tools that only fix a few degrees of scanner skew, yuma_upright reads the actual text with OCR and rotates the image by whatever angle is needed — 15°, 90°, 180°, 233°, anything — so the text ends up horizontal, left-to-right, right-side up.


Install

pip install yuma_upright

easyocr (and its own dependency, torch) will be installed automatically. The first time you construct an Uprighter, EasyOCR will download its model weights (~a few hundred MB) to ~/.EasyOCR/.


⚠️ Important: this expects a cropped image

yuma_upright OCRs the whole image you give it and anchors the rotation on the single highest-confidence word it finds. That means:

  • Best case: you feed it a tightly cropped single field/word — a name field, a PAN/ID number, a label — with the field's own detector (e.g. your RT-DETR model). This is what the library is designed for and it works reliably.
  • Bad case: you feed it a whole, uncropped, multi-field document. It will pick one word anywhere on the page (whichever OCRs best) and rotate the entire image to align just that word — which may be wrong for the rest of the page if different fields are rotated differently, or it may simply pick a word you didn't intend to anchor on.

If you already have your own detector (like the RT-DETR model you mentioned) that finds field bounding boxes, run detection first, crop to the box you trust, then pass that crop to yuma_upright — or pass the box directly via the box= parameter (see below) so yuma_upright skips its own full-image search and anchors on exactly the region you specify.


Quick start

from yuma_upright import Uprighter

# Construct once, reuse across many images (loads the OCR model once)
up = Uprighter(gpu=False)  # set gpu=True if you have CUDA available

result = up.upright("pan_number_crop.jpg")

result.image             # np.ndarray (BGR) — the upright image
result.angle_deg         # float — rotation actually applied, in degrees
result.axis_word_text    # str — the OCR word used to anchor the rotation
result.ocr_words         # list[str] — every word OCR found in the crop
result.debug             # dict — details of the 180° disambiguation pass
import cv2
cv2.imwrite("upright.jpg", result.image)

One-off convenience call (no need to manage an instance)

from yuma_upright import upright_image

result = upright_image("pan_number_crop.jpg", gpu=False)

Accepted input types

image can be any of:

  • a file path (str or pathlib.Path)
  • raw encoded image bytes (e.g. from an upload)
  • an already-decoded numpy.ndarray (BGR, RGBA/BGRA, or grayscale)
  • a PIL.Image.Image

API reference

class Uprighter

Uprighter(
    languages: list[str] = ["en"],
    gpu: bool = True,
    blur_threshold: float = 15.0,
    ocr_min_conf: float = 0.0,
    reader=None,
)
Parameter Meaning
languages EasyOCR language codes.
gpu Whether EasyOCR should use CUDA.
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.
ocr_min_conf Minimum per-word OCR confidence to keep. Default 0.0 (blur gate does the filtering instead).
reader Pass an already-built easyocr.Reader to reuse across multiple Uprighter instances / avoid re-loading weights.

Uprighter.upright(image, box=None, resolve_ambiguity=True, invert_direction=False) -> UprightResult

Parameter Meaning
image Any supported input type (see above). Should be a single cropped field/region — see the warning above.
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.
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.
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.

Returns an UprightResult:

@dataclass
class UprightResult:
    image: np.ndarray             # rotated (upright) BGR image
    angle_deg: float              # final rotation actually applied
    raw_angle_deg: float          # angle before 180° disambiguation
    axis_word_text: str | None    # OCR text used as the axis anchor
    orientation_used: str | None  # "left-right-ends" or "top-bottom-ends"
    ocr_words: list[str]          # all words seen during OCR
    debug: dict                   # 180°-disambiguation candidate details

upright_image(image, gpu=True, languages=None, **kwargs) -> UprightResult

Module-level convenience wrapper around a lazily-shared Uprighter instance. Good for scripts / one-offs. For processing many images, construct your own Uprighter once and call .upright() in a loop instead — it avoids the overhead of re-checking whether the shared instance's config matches on every call.

Exceptions

All in yuma_upright.exceptions, all subclasses of UprightError:

  • NoTextFoundError — OCR found nothing usable in the image.
  • ImageTooBlurryError — image (or box region) failed the blur gate.
  • InvalidImageInputError — the image argument couldn't be interpreted.

How it works (short version)

  1. OCR finds words + their 4-point boxes.
  2. The best word is picked (highest confidence; ambiguous-looking words like "SOS"/"NO" are deprioritized since they look the same rotated 180°).
  3. Each word box has two edge-pairs — (top, bottom) and (left, right). Whichever pair is shorter marks the two ends of the word; their midpoints define the reading direction, regardless of the word's rotation.
  4. The angle between that direction and horizontal gives the rotation needed.
  5. Because geometry alone can't tell which end is the start of the word, the result can be off by exactly 180°. This is resolved by rotating a crop both ways, OCR-ing each, and keeping the orientation with higher mean OCR confidence (upside-down text reliably OCRs worse).

Rotation direction

The sign convention assumes EasyOCR's box point order (top-left, top-right, bottom-right, bottom-left) and a standard image coordinate system (y grows downward). If you're feeding in box= polygons from a different detector/convention and results consistently rotate opposite to what you expect, pass invert_direction=True rather than modifying the angle math yourself.


About the RT-DETR field-detection model

The original prototype this library is based on used a custom-trained RT-DETR model (best_pan_details.pt) to find PAN card fields (name, father's name, PAN number, DOB) before cropping and uprighting each one.

That detector is intentionally NOT part of this package. It's a domain-specific (PAN card) object-detection model with its own weights file, license considerations, and inference dependencies (ultralytics) — bundling it would make yuma_upright heavy and narrow instead of a general OCR-uprighting utility. Recommended pattern if you have your own detector (RT-DETR, YOLO, or anything else):

from ultralytics import RTDETR
from yuma_upright import Uprighter

detector = RTDETR("best_pan_details.pt")
up = Uprighter(gpu=True)

results = detector.predict(image_rgb, conf=0.45, iou=0.45)
for box in results[0].boxes:
    x1, y1, x2, y2 = box.xyxy[0].cpu().numpy().astype(int)
    field_crop = original_bgr[y1:y2, x1:x2]
    result = up.upright(field_crop)
    # result.image is now the upright crop for this field

If there's demand, a separate optional package (e.g. yuma_upright[pan] or a standalone yuma_upright_pan_detector) could wrap this pattern — open an issue if you want that.


Roadmap / planned improvements

  • Accept a caller-supplied bounding box / polygon (box=) to anchor rotation on a known region instead of always searching the whole image (basic support shipped in 0.1.0).
  • Accept multiple box= regions + a policy for combining/voting on their angles (useful for multi-line fields).
  • Optional non-OCR 180° disambiguation (e.g. a lightweight upright/ upside-down image classifier) as a faster alternative to the current double-OCR pass.
  • Batch API (upright_many([...])) that shares one OCR reader efficiently across a list of crops.
  • Pluggable OCR backend (currently hard-wired to EasyOCR).

Development / publishing this package to PyPI

# from the repo root (where pyproject.toml lives)
python -m pip install --upgrade build twine

# 1. Build sdist + wheel into dist/
python -m build

# 2. (Recommended) upload to TestPyPI first and verify install works
python -m twine upload --repository testpypi dist/*
pip install --index-url https://test.pypi.org/simple/ yuma_upright

# 3. Upload to real PyPI
python -m twine upload dist/*

Notes:

  • You need a PyPI account and an API token (Account settings → API tokens). Use __token__ as the username and the token (starting pypi-...) as the password when twine prompts, or store it in ~/.pypirc:

    [pypi]
    username = __token__
    password = pypi-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    
    [testpypi]
    username = __token__
    password = pypi-yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
    
  • yuma_upright as a name must be unique on PyPI — check https://pypi.org/project/yuma_upright/ before your first upload; if it's taken you'll need to rename the name field in pyproject.toml (the importable Python package name doesn't have to match).

  • Bump version in pyproject.toml (and __version__ in src/yuma_upright/__init__.py) before every re-upload — PyPI does not allow re-uploading the same version number, even after deleting it.

License

MIT — see LICENSE.

Metadata

Release files for yuma-upright 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for yuma-upright 0.1.0
File Size Uploaded Provenance
yuma_upright-0.1.0.tar.gz 20.2 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for yuma-upright 0.1.0
File Interpreter ABI Platform
yuma_upright-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 36.5 kB

Release files / yuma_upright-0.1.0.tar.gz

Download URL yuma_upright-0.1.0.tar.gz
Size 20.2 kB
Tags Source
SHA-256 checksum
How to use checksums
3aa9866333e376c0092dc60197249ced3757590e0731ef6a7b9cf5b150f11e0f
BLAKE2b-256 checksum
How to use checksums
934e225814a56272d8670fc74fb833978feb1a2293282330e3c70d637b60d7ac
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.12.10

Release files / yuma_upright-0.1.0-py3-none-any.whl

Download URL yuma_upright-0.1.0-py3-none-any.whl
Size 16.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
bbff8e763622d05f0427e0207cf31c1e6d93613592ab7ee7692c622526648a3c
BLAKE2b-256 checksum
How to use checksums
26a2ecdb8f62505b3419a67959ff9a6163cf09ca8496ec1f83b65278079b54b1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.12.10

Security for yuma-upright 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page