ocr-foundry 0.2.0__py3-none-any.whl
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.
- khocr_gen/__init__.py +11 -0
- khocr_gen/_pil_compat.py +25 -0
- khocr_gen/_rust_accel.py +204 -0
- khocr_gen/augmentation.py +1193 -0
- khocr_gen/benchmark.py +888 -0
- khocr_gen/benchmark_cmd.py +103 -0
- khocr_gen/cli.py +246 -0
- khocr_gen/combine.py +214 -0
- khocr_gen/combine_cmd.py +125 -0
- khocr_gen/config.py +1474 -0
- khocr_gen/config_loader.py +157 -0
- khocr_gen/corpus.py +621 -0
- khocr_gen/data_generator.py +1160 -0
- khocr_gen/errors.py +33 -0
- khocr_gen/evaluate_cmd.py +50 -0
- khocr_gen/evaluation.py +259 -0
- khocr_gen/fonts.py +316 -0
- khocr_gen/generate.py +227 -0
- khocr_gen/layout.py +1028 -0
- khocr_gen/layout_cmd.py +371 -0
- khocr_gen/layout_degradation.py +128 -0
- khocr_gen/layout_mesh.py +200 -0
- khocr_gen/layout_shaping.py +264 -0
- khocr_gen/layout_transform.py +238 -0
- khocr_gen/layout_warp.py +473 -0
- khocr_gen/line_height.py +127 -0
- khocr_gen/lmdb_pack.py +132 -0
- khocr_gen/logging.py +25 -0
- khocr_gen/normalizer.py +221 -0
- khocr_gen/parallel.py +366 -0
- khocr_gen/provenance.py +328 -0
- khocr_gen/rendering.py +1607 -0
- khocr_gen/text_support.py +429 -0
- khocr_gen/verify.py +471 -0
- khocr_gen/viewer.py +172 -0
- ocr_foundry-0.2.0.dist-info/METADATA +174 -0
- ocr_foundry-0.2.0.dist-info/RECORD +40 -0
- ocr_foundry-0.2.0.dist-info/WHEEL +4 -0
- ocr_foundry-0.2.0.dist-info/entry_points.txt +3 -0
- ocr_foundry-0.2.0.dist-info/licenses/LICENSE +21 -0
khocr_gen/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""OCR Foundry - synthetic OCR training data generator."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
__version__ = version("ocr-foundry")
|
|
7
|
+
except PackageNotFoundError:
|
|
8
|
+
try:
|
|
9
|
+
__version__ = version("khocr-gen")
|
|
10
|
+
except PackageNotFoundError:
|
|
11
|
+
__version__ = "0.2.0"
|
khocr_gen/_pil_compat.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""PIL compatibility shims for resampling across Pillow versions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from PIL import Image
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _resolve_resample(name: str) -> Any:
|
|
11
|
+
resampling = getattr(Image, "Resampling", None)
|
|
12
|
+
if resampling is not None:
|
|
13
|
+
value = getattr(resampling, name, None)
|
|
14
|
+
if value is not None:
|
|
15
|
+
return value
|
|
16
|
+
|
|
17
|
+
legacy_value = getattr(Image, name, None)
|
|
18
|
+
if legacy_value is not None:
|
|
19
|
+
return legacy_value
|
|
20
|
+
|
|
21
|
+
raise AttributeError(f"PIL.Image does not define resample mode '{name}'")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
RESAMPLE_BILINEAR: Any = _resolve_resample("BILINEAR")
|
|
25
|
+
RESAMPLE_BICUBIC: Any = _resolve_resample("BICUBIC")
|
khocr_gen/_rust_accel.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Rust acceleration backend for khocr-gen.
|
|
2
|
+
|
|
3
|
+
This module wraps the native `_rust_accel` extension (built via maturin/PyO3).
|
|
4
|
+
If the native extension is unavailable, Python augmentation and text splitting fallbacks are used.
|
|
5
|
+
|
|
6
|
+
Usage::
|
|
7
|
+
|
|
8
|
+
from khocr_gen._rust_accel import (
|
|
9
|
+
apply_sauvola, apply_blur,
|
|
10
|
+
split_text_spans,
|
|
11
|
+
estimate_bg,
|
|
12
|
+
HAS_RUST_ACCEL,
|
|
13
|
+
)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import logging
|
|
19
|
+
from typing import TYPE_CHECKING, Any
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from collections.abc import Callable
|
|
25
|
+
|
|
26
|
+
_LOGGER = logging.getLogger("khocr_gen._rust_accel")
|
|
27
|
+
|
|
28
|
+
# Try to load the native Rust extension
|
|
29
|
+
|
|
30
|
+
_native: Any = None
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
import _rust_accel as _native
|
|
34
|
+
|
|
35
|
+
HAS_RUST_ACCEL = True
|
|
36
|
+
except ImportError:
|
|
37
|
+
HAS_RUST_ACCEL = False
|
|
38
|
+
_LOGGER.info("Rust extension not available, using pure-Python fallbacks.")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _log_available() -> None:
|
|
42
|
+
if HAS_RUST_ACCEL:
|
|
43
|
+
try:
|
|
44
|
+
ver: str = getattr(_native, "__version__", "unknown")
|
|
45
|
+
except Exception:
|
|
46
|
+
ver = "unknown"
|
|
47
|
+
_LOGGER.info("Rust acceleration enabled (native _rust_accel v%s)", ver)
|
|
48
|
+
else:
|
|
49
|
+
_LOGGER.info("Rust acceleration NOT available.")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# Augmentation functions
|
|
53
|
+
|
|
54
|
+
# Each entry: (function_name, has_rgb_variant)
|
|
55
|
+
_AUG_FUNCTIONS: list[tuple[str, bool]] = [
|
|
56
|
+
("apply_sauvola", False),
|
|
57
|
+
("apply_geo_warp", False),
|
|
58
|
+
("apply_vertical_crop", False),
|
|
59
|
+
("apply_blur", False),
|
|
60
|
+
("apply_salt_pepper", False),
|
|
61
|
+
("apply_background_texture", False),
|
|
62
|
+
("apply_jpeg_compression", False),
|
|
63
|
+
("apply_rotation", True),
|
|
64
|
+
("apply_lowdpi", False),
|
|
65
|
+
("apply_oversample", False),
|
|
66
|
+
("apply_perspective", False),
|
|
67
|
+
("apply_elastic", False),
|
|
68
|
+
("apply_random_crop", False),
|
|
69
|
+
("apply_online_blur", False),
|
|
70
|
+
("apply_online_noise", False),
|
|
71
|
+
("apply_reverse", True),
|
|
72
|
+
("apply_brightness_contrast", True),
|
|
73
|
+
("apply_pixelation", False),
|
|
74
|
+
("apply_gradient_illumination", False),
|
|
75
|
+
("apply_morphological", False),
|
|
76
|
+
("apply_anisotropic_dilation", False),
|
|
77
|
+
("apply_hsv", True), # RGB-only in Rust
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _get_rust_aug_fn(name: str) -> Callable[..., Any] | None:
|
|
82
|
+
"""Return the native Rust augmentation function by name, or None."""
|
|
83
|
+
if not HAS_RUST_ACCEL:
|
|
84
|
+
return None
|
|
85
|
+
assert _native is not None
|
|
86
|
+
return getattr(_native, f"{name}_rust", None)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _make_aug_fallback(name: str) -> Callable[..., Any]:
|
|
90
|
+
"""Create a fallback that delegates to the Python augmentation module."""
|
|
91
|
+
|
|
92
|
+
def _fallback(img: np.ndarray, intensity: float, **kwargs: Any) -> np.ndarray:
|
|
93
|
+
from khocr_gen.augmentation import AUG_METHODS
|
|
94
|
+
|
|
95
|
+
fn = AUG_METHODS.get(name)
|
|
96
|
+
if fn is None:
|
|
97
|
+
return img
|
|
98
|
+
return fn(img, intensity, **kwargs)
|
|
99
|
+
|
|
100
|
+
_fallback.__name__ = name
|
|
101
|
+
_fallback.__qualname__ = name
|
|
102
|
+
return _fallback
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _export_aug_functions() -> None:
|
|
106
|
+
"""Populate the module globals with augmentation functions.
|
|
107
|
+
|
|
108
|
+
For each augmentation method, the Rust variant is preferred when available;
|
|
109
|
+
otherwise the pure-Python fallback is used.
|
|
110
|
+
|
|
111
|
+
Grayscale-only methods dispatch directly.
|
|
112
|
+
Methods with an RGB variant (`brightness_contrast`, `hsv`) are wrapped to handle both grayscale and RGB input arrays.
|
|
113
|
+
"""
|
|
114
|
+
g = globals()
|
|
115
|
+
|
|
116
|
+
for name, has_rgb in _AUG_FUNCTIONS:
|
|
117
|
+
rust_fn = _get_rust_aug_fn(name)
|
|
118
|
+
rust_rgb_fn: Callable[..., Any] | None = (
|
|
119
|
+
_get_rust_aug_fn(f"{name}_rgb") if has_rgb else None
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
if has_rgb and HAS_RUST_ACCEL and (rust_fn is not None or rust_rgb_fn is not None):
|
|
123
|
+
# Wrap to auto-detect channel count and dispatch.
|
|
124
|
+
# Rust RGB functions return flat (h, w*3) 2D arrays; reshape to (h, w, 3).
|
|
125
|
+
_gray_fn = rust_fn
|
|
126
|
+
_rgb_fn = rust_rgb_fn
|
|
127
|
+
|
|
128
|
+
# Capture the fallback factory by value so we can safely delete it later
|
|
129
|
+
_fallback_factory = _make_aug_fallback
|
|
130
|
+
|
|
131
|
+
def _make_rgb_wrapper(
|
|
132
|
+
gray_fn: Callable[..., Any] | None,
|
|
133
|
+
rgb_fn: Callable[..., Any] | None,
|
|
134
|
+
fallback_name: str,
|
|
135
|
+
fallback_factory: Callable[..., Any],
|
|
136
|
+
) -> Callable[..., Any]:
|
|
137
|
+
def _wrapper(img: np.ndarray, intensity: float, **kwargs: Any) -> np.ndarray:
|
|
138
|
+
if img.ndim == 3 and img.shape[2] == 3:
|
|
139
|
+
if rgb_fn is not None:
|
|
140
|
+
h, w = img.shape[:2]
|
|
141
|
+
result = rgb_fn(img, intensity)
|
|
142
|
+
# Rust RGB fns return flat (h, w*3); reshape to (h, w, 3)
|
|
143
|
+
if result.ndim == 2 and result.shape[1] == w * 3:
|
|
144
|
+
return result.reshape(h, w, 3)
|
|
145
|
+
return result
|
|
146
|
+
if gray_fn is not None:
|
|
147
|
+
channels = [
|
|
148
|
+
gray_fn(np.ascontiguousarray(img[:, :, c]), intensity)
|
|
149
|
+
for c in range(3)
|
|
150
|
+
]
|
|
151
|
+
return np.stack(channels, axis=-1)
|
|
152
|
+
return fallback_factory(fallback_name)(img, intensity)
|
|
153
|
+
if gray_fn is not None:
|
|
154
|
+
return gray_fn(img, intensity)
|
|
155
|
+
return fallback_factory(fallback_name)(img, intensity)
|
|
156
|
+
|
|
157
|
+
return _wrapper
|
|
158
|
+
|
|
159
|
+
g[name] = _make_rgb_wrapper(_gray_fn, _rgb_fn, name, _fallback_factory)
|
|
160
|
+
elif rust_fn is not None:
|
|
161
|
+
g[name] = rust_fn
|
|
162
|
+
else:
|
|
163
|
+
g[name] = _make_aug_fallback(name)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
_export_aug_functions()
|
|
167
|
+
|
|
168
|
+
# Clean up the helpers so they don't pollute the module namespace.
|
|
169
|
+
# _make_aug_fallback and _AUG_FUNCTIONS are retained for closures referencing them.
|
|
170
|
+
del _export_aug_functions
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# Text span splitting
|
|
174
|
+
|
|
175
|
+
if HAS_RUST_ACCEL:
|
|
176
|
+
assert _native is not None
|
|
177
|
+
split_text_spans = _native.split_text_spans # type: ignore[assignment]
|
|
178
|
+
else:
|
|
179
|
+
|
|
180
|
+
def split_text_spans(text: str) -> list[tuple[str, str]]: # type: ignore[no-redef]
|
|
181
|
+
"""Split text into contiguous script spans (pure-Python fallback)."""
|
|
182
|
+
from khocr_gen.rendering import ImageRenderer
|
|
183
|
+
|
|
184
|
+
return ImageRenderer._split_text_into_spans(text)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# Utilities
|
|
188
|
+
|
|
189
|
+
if HAS_RUST_ACCEL:
|
|
190
|
+
assert _native is not None
|
|
191
|
+
estimate_bg = _native.estimate_bg_rust # type: ignore[assignment]
|
|
192
|
+
else:
|
|
193
|
+
|
|
194
|
+
def estimate_bg(img: np.ndarray) -> int: # type: ignore[no-redef]
|
|
195
|
+
"""Estimate background colour from image border (pure-Python fallback)."""
|
|
196
|
+
from khocr_gen.augmentation import _estimate_bg
|
|
197
|
+
|
|
198
|
+
return int(_estimate_bg(img))
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# Module-level init
|
|
202
|
+
|
|
203
|
+
_log_available()
|
|
204
|
+
del _log_available
|