wormocr 1.0.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.
- wormocr/__init__.py +18 -0
- wormocr/__main__.py +4 -0
- wormocr/api.py +229 -0
- wormocr/benchmark/__init__.py +5 -0
- wormocr/benchmark/report.py +403 -0
- wormocr/checkpoints.py +55 -0
- wormocr/cli.py +308 -0
- wormocr/config.py +220 -0
- wormocr/data/__init__.py +5 -0
- wormocr/data/dataset.py +110 -0
- wormocr/data/fonts.py +173 -0
- wormocr/data/handwriting.py +304 -0
- wormocr/data/lexicon.py +105 -0
- wormocr/data/lexicon_medical.py +48 -0
- wormocr/data/mixed.py +211 -0
- wormocr/data/synthetic.py +445 -0
- wormocr/detection/__init__.py +5 -0
- wormocr/detection/lines.py +382 -0
- wormocr/evaluation/__init__.py +5 -0
- wormocr/evaluation/errors.py +145 -0
- wormocr/evaluation/metrics.py +105 -0
- wormocr/evaluation/robustness.py +43 -0
- wormocr/exceptions.py +25 -0
- wormocr/export/__init__.py +5 -0
- wormocr/export/onnx_export.py +75 -0
- wormocr/hub.py +157 -0
- wormocr/inference/__init__.py +5 -0
- wormocr/inference/decode.py +113 -0
- wormocr/inference/engine.py +122 -0
- wormocr/models/__init__.py +5 -0
- wormocr/models/crnn.py +236 -0
- wormocr/preprocessing.py +135 -0
- wormocr/py.typed +0 -0
- wormocr/runtime.py +56 -0
- wormocr/training/__init__.py +5 -0
- wormocr/training/loop.py +645 -0
- wormocr/vocabulary.py +69 -0
- wormocr/web/__init__.py +5 -0
- wormocr/web/server.py +505 -0
- wormocr/web/static/app.js +514 -0
- wormocr/web/static/index.html +141 -0
- wormocr/web/static/style.css +379 -0
- wormocr-1.0.0.dist-info/METADATA +377 -0
- wormocr-1.0.0.dist-info/RECORD +48 -0
- wormocr-1.0.0.dist-info/WHEEL +5 -0
- wormocr-1.0.0.dist-info/entry_points.txt +2 -0
- wormocr-1.0.0.dist-info/licenses/LICENSE +21 -0
- wormocr-1.0.0.dist-info/top_level.txt +1 -0
wormocr/models/crnn.py
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""CNN (optionally depthwise-separable) + BiGRU + CTC log-probabilities.
|
|
2
|
+
|
|
3
|
+
Width is downsampled by `width_stride` (2 or 4). Height 32 is reduced to a
|
|
4
|
+
short feature column and flattened. Stride 4 halves the sequence the GRU must
|
|
5
|
+
read and makes CTC alignment converge much sooner; older checkpoints that do
|
|
6
|
+
not store the field load as stride 2.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
|
|
13
|
+
import torch
|
|
14
|
+
from torch import nn
|
|
15
|
+
|
|
16
|
+
from wormocr.exceptions import ConfigError
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class Spec:
|
|
21
|
+
kind: str
|
|
22
|
+
out_channels: int
|
|
23
|
+
kernel: tuple[int, int]
|
|
24
|
+
stride: tuple[int, int]
|
|
25
|
+
padding: tuple[int, int]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def build_specs(channels: list[int], width_stride: int = 2) -> list[Spec]:
|
|
29
|
+
if len(channels) != 4:
|
|
30
|
+
raise ConfigError("the recognizer expects four channel stages")
|
|
31
|
+
if int(width_stride) not in (2, 4):
|
|
32
|
+
raise ConfigError("model.width_stride must be 2 or 4")
|
|
33
|
+
first, second, third, fourth = (int(value) for value in channels)
|
|
34
|
+
second_pool = (2, 2) if int(width_stride) == 4 else (2, 1)
|
|
35
|
+
return [
|
|
36
|
+
Spec("conv", first, (3, 3), (1, 1), (1, 1)),
|
|
37
|
+
Spec("pool", 0, (2, 2), (2, 2), (0, 0)),
|
|
38
|
+
Spec("conv", second, (3, 3), (1, 1), (1, 1)),
|
|
39
|
+
Spec("pool", 0, second_pool, second_pool, (0, 0)),
|
|
40
|
+
Spec("conv", third, (3, 3), (1, 1), (1, 1)),
|
|
41
|
+
Spec("pool", 0, (2, 1), (2, 1), (0, 0)),
|
|
42
|
+
Spec("conv", fourth, (3, 3), (1, 1), (1, 1)),
|
|
43
|
+
Spec("pool", 0, (2, 1), (2, 1), (0, 0)),
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _spatial(size: int, kernel: int, stride: int, padding: int) -> int:
|
|
48
|
+
return (int(size) + 2 * padding - kernel) // stride + 1
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def output_width(width: int, specs: list[Spec]) -> int:
|
|
52
|
+
size = int(width)
|
|
53
|
+
for spec in specs:
|
|
54
|
+
size = _spatial(size, spec.kernel[1], spec.stride[1], spec.padding[1])
|
|
55
|
+
if size <= 0:
|
|
56
|
+
return 0
|
|
57
|
+
return size
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ConvBlock(nn.Module):
|
|
61
|
+
def __init__(
|
|
62
|
+
self,
|
|
63
|
+
in_channels: int,
|
|
64
|
+
out_channels: int,
|
|
65
|
+
kernel: tuple[int, int],
|
|
66
|
+
stride: tuple[int, int],
|
|
67
|
+
padding: tuple[int, int],
|
|
68
|
+
separable: bool,
|
|
69
|
+
) -> None:
|
|
70
|
+
super().__init__()
|
|
71
|
+
if separable:
|
|
72
|
+
self.conv: nn.Module = nn.Sequential(
|
|
73
|
+
nn.Conv2d(
|
|
74
|
+
in_channels,
|
|
75
|
+
in_channels,
|
|
76
|
+
kernel,
|
|
77
|
+
stride=stride,
|
|
78
|
+
padding=padding,
|
|
79
|
+
groups=in_channels,
|
|
80
|
+
bias=False,
|
|
81
|
+
),
|
|
82
|
+
nn.Conv2d(in_channels, out_channels, kernel_size=1, bias=False),
|
|
83
|
+
)
|
|
84
|
+
else:
|
|
85
|
+
self.conv = nn.Conv2d(
|
|
86
|
+
in_channels,
|
|
87
|
+
out_channels,
|
|
88
|
+
kernel,
|
|
89
|
+
stride=stride,
|
|
90
|
+
padding=padding,
|
|
91
|
+
bias=False,
|
|
92
|
+
)
|
|
93
|
+
self.bn = nn.BatchNorm2d(out_channels)
|
|
94
|
+
self.act = nn.ReLU(inplace=False)
|
|
95
|
+
|
|
96
|
+
def forward(self, images: torch.Tensor) -> torch.Tensor:
|
|
97
|
+
return self.act(self.bn(self.conv(images)))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class Encoder(nn.Module):
|
|
101
|
+
def __init__(self, in_channels: int, specs: list[Spec], separable: bool) -> None:
|
|
102
|
+
super().__init__()
|
|
103
|
+
self.specs = specs
|
|
104
|
+
blocks: list[nn.Module] = []
|
|
105
|
+
channels = in_channels
|
|
106
|
+
for spec in specs:
|
|
107
|
+
if spec.kind == "conv":
|
|
108
|
+
blocks.append(
|
|
109
|
+
ConvBlock(
|
|
110
|
+
channels,
|
|
111
|
+
spec.out_channels,
|
|
112
|
+
spec.kernel,
|
|
113
|
+
spec.stride,
|
|
114
|
+
spec.padding,
|
|
115
|
+
separable=separable and channels > 1,
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
channels = spec.out_channels
|
|
119
|
+
elif spec.kind == "pool":
|
|
120
|
+
blocks.append(
|
|
121
|
+
nn.MaxPool2d(
|
|
122
|
+
kernel_size=spec.kernel,
|
|
123
|
+
stride=spec.stride,
|
|
124
|
+
padding=spec.padding,
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
else:
|
|
128
|
+
raise ConfigError(f"unknown encoder spec {spec.kind}")
|
|
129
|
+
self.blocks = nn.ModuleList(blocks)
|
|
130
|
+
|
|
131
|
+
def forward(self, images: torch.Tensor) -> torch.Tensor:
|
|
132
|
+
features = images
|
|
133
|
+
for block in self.blocks:
|
|
134
|
+
features = block(features)
|
|
135
|
+
return features
|
|
136
|
+
|
|
137
|
+
def output_width(self, width: int) -> int:
|
|
138
|
+
return output_width(width, self.specs)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class CRNN(nn.Module):
|
|
142
|
+
"""Variable-width line recognizer. Forward returns log-probabilities (B, T, C)."""
|
|
143
|
+
|
|
144
|
+
def __init__(
|
|
145
|
+
self,
|
|
146
|
+
num_classes: int,
|
|
147
|
+
in_channels: int = 1,
|
|
148
|
+
image_height: int = 32,
|
|
149
|
+
channels: list[int] | None = None,
|
|
150
|
+
separable: bool = True,
|
|
151
|
+
rnn_hidden: int = 256,
|
|
152
|
+
rnn_layers: int = 2,
|
|
153
|
+
rnn_dropout: float = 0.1,
|
|
154
|
+
proj_size: int = 256,
|
|
155
|
+
width_stride: int = 2,
|
|
156
|
+
) -> None:
|
|
157
|
+
super().__init__()
|
|
158
|
+
if num_classes < 2:
|
|
159
|
+
raise ConfigError("num_classes must include the blank and at least one character")
|
|
160
|
+
stage_channels = list(channels or [32, 64, 128, 192])
|
|
161
|
+
self.image_height = int(image_height)
|
|
162
|
+
self.num_classes = int(num_classes)
|
|
163
|
+
self.width_stride = int(width_stride)
|
|
164
|
+
self.encoder = Encoder(in_channels, build_specs(stage_channels, self.width_stride), separable)
|
|
165
|
+
with torch.no_grad():
|
|
166
|
+
probe = torch.zeros(1, in_channels, self.image_height, 32)
|
|
167
|
+
encoded = self.encoder(probe)
|
|
168
|
+
if encoded.shape[-2] < 1 or encoded.shape[-1] < 1:
|
|
169
|
+
raise ConfigError(
|
|
170
|
+
f"encoder collapsed a {self.image_height}px-tall input; "
|
|
171
|
+
"choose a taller image_height that is a multiple of 16"
|
|
172
|
+
)
|
|
173
|
+
feature_height = int(encoded.shape[-2])
|
|
174
|
+
feature_channels = int(encoded.shape[1])
|
|
175
|
+
feature_size = feature_channels * feature_height
|
|
176
|
+
self.feature_size = feature_size
|
|
177
|
+
self.proj = nn.Sequential(
|
|
178
|
+
nn.Linear(feature_size, proj_size),
|
|
179
|
+
nn.ReLU(inplace=False),
|
|
180
|
+
)
|
|
181
|
+
dropout = float(rnn_dropout) if rnn_layers > 1 else 0.0
|
|
182
|
+
self.rnn = nn.GRU(
|
|
183
|
+
input_size=proj_size,
|
|
184
|
+
hidden_size=int(rnn_hidden),
|
|
185
|
+
num_layers=int(rnn_layers),
|
|
186
|
+
batch_first=True,
|
|
187
|
+
bidirectional=True,
|
|
188
|
+
dropout=dropout if rnn_layers > 1 else 0.0,
|
|
189
|
+
)
|
|
190
|
+
self.classifier = nn.Linear(int(rnn_hidden) * 2, self.num_classes)
|
|
191
|
+
nn.init.zeros_(self.classifier.bias)
|
|
192
|
+
# Class 0 is the CTC blank. A negative bias stops a fresh model from
|
|
193
|
+
# taking the blank at every frame before it has aligned a character.
|
|
194
|
+
self.classifier.bias.data[0] = -2.0
|
|
195
|
+
|
|
196
|
+
def output_lengths(self, widths: torch.Tensor) -> torch.Tensor:
|
|
197
|
+
lengths = [self.encoder.output_width(int(width)) for width in widths.detach().cpu().tolist()]
|
|
198
|
+
return torch.tensor(lengths, dtype=torch.long)
|
|
199
|
+
|
|
200
|
+
def forward(self, images: torch.Tensor) -> torch.Tensor:
|
|
201
|
+
if images.ndim != 4:
|
|
202
|
+
raise ValueError("images must have shape (batch, channels, height, width)")
|
|
203
|
+
if images.shape[2] != self.image_height:
|
|
204
|
+
raise ValueError(
|
|
205
|
+
f"expected image height {self.image_height}, got {images.shape[2]}"
|
|
206
|
+
)
|
|
207
|
+
features = self.encoder(images)
|
|
208
|
+
batch, channels, height, time = features.shape
|
|
209
|
+
sequence = features.permute(0, 3, 1, 2).contiguous().view(batch, time, channels * height)
|
|
210
|
+
sequence = self.proj(sequence)
|
|
211
|
+
sequence, _hidden = self.rnn(sequence)
|
|
212
|
+
return self.classifier(sequence).float().log_softmax(dim=-1)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def build_model(config: dict, num_classes: int) -> CRNN:
|
|
216
|
+
model_cfg = config["model"]
|
|
217
|
+
return CRNN(
|
|
218
|
+
num_classes=num_classes,
|
|
219
|
+
in_channels=int(model_cfg.get("in_channels", 1)),
|
|
220
|
+
image_height=int(model_cfg["image_height"]),
|
|
221
|
+
channels=[int(value) for value in model_cfg["channels"]],
|
|
222
|
+
separable=bool(model_cfg["separable"]),
|
|
223
|
+
rnn_hidden=int(model_cfg["rnn_hidden"]),
|
|
224
|
+
rnn_layers=int(model_cfg["rnn_layers"]),
|
|
225
|
+
rnn_dropout=float(model_cfg.get("rnn_dropout", 0.0)),
|
|
226
|
+
proj_size=int(model_cfg.get("proj_size", model_cfg["rnn_hidden"])),
|
|
227
|
+
width_stride=int(model_cfg.get("width_stride", 2)),
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def count_parameters(module: nn.Module) -> int:
|
|
232
|
+
return sum(parameter.numel() for parameter in module.parameters())
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def state_dict_num_bytes(module: nn.Module) -> int:
|
|
236
|
+
return sum(parameter.numel() * parameter.element_size() for parameter in module.parameters())
|
wormocr/preprocessing.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Image loading and line normalization.
|
|
2
|
+
|
|
3
|
+
The recognizer consumes grayscale tensors of a fixed height and variable width,
|
|
4
|
+
normalized to [-1, 1]. Padding on the right uses the image border so a short
|
|
5
|
+
line in a batch is not given a false edge.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
import torch
|
|
14
|
+
from PIL import Image, UnidentifiedImageError
|
|
15
|
+
|
|
16
|
+
from wormocr.exceptions import RecognitionError
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def to_grayscale(image: Image.Image) -> Image.Image:
|
|
20
|
+
if image.mode == "RGBA":
|
|
21
|
+
background = Image.new("RGBA", image.size, (255, 255, 255, 255))
|
|
22
|
+
image = Image.alpha_composite(background, image)
|
|
23
|
+
return image.convert("L")
|
|
24
|
+
if image.mode != "L":
|
|
25
|
+
return image.convert("L")
|
|
26
|
+
return image
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def border_color(image: Image.Image) -> int:
|
|
30
|
+
array = np.asarray(image.convert("L"))
|
|
31
|
+
if array.size == 0:
|
|
32
|
+
return 255
|
|
33
|
+
border = np.concatenate(
|
|
34
|
+
[array[:, 0], array[:, -1], array[0, :], array[-1, :]]
|
|
35
|
+
)
|
|
36
|
+
return int(np.median(border))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def resize_to_height(image: Image.Image, height: int) -> Image.Image:
|
|
40
|
+
if height < 8:
|
|
41
|
+
raise ValueError("image height must be at least 8")
|
|
42
|
+
width, source_height = image.size
|
|
43
|
+
if width < 1 or source_height < 1:
|
|
44
|
+
raise ValueError("image is empty")
|
|
45
|
+
if source_height == height:
|
|
46
|
+
return image
|
|
47
|
+
new_width = max(1, int(round(width * (height / float(source_height)))))
|
|
48
|
+
return image.resize((new_width, height), Image.Resampling.BILINEAR)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _normalize_pixel(value: float) -> float:
|
|
52
|
+
return (value / 255.0 - 0.5) / 0.5
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def prepare_line(
|
|
56
|
+
image: Image.Image,
|
|
57
|
+
height: int = 32,
|
|
58
|
+
align: int = 2,
|
|
59
|
+
max_width: int = 2048,
|
|
60
|
+
min_width: int = 8,
|
|
61
|
+
) -> tuple[torch.Tensor, float]:
|
|
62
|
+
"""Return a float tensor of shape (1, height, width) and its pad value."""
|
|
63
|
+
if align < 1:
|
|
64
|
+
raise ValueError("align must be positive")
|
|
65
|
+
gray = to_grayscale(image)
|
|
66
|
+
if gray.width < 1 or gray.height < 1:
|
|
67
|
+
raise ValueError("image is empty")
|
|
68
|
+
resized = resize_to_height(gray, height)
|
|
69
|
+
if resized.width > max_width:
|
|
70
|
+
resized = resized.resize((max_width, height), Image.Resampling.BILINEAR)
|
|
71
|
+
color = border_color(resized)
|
|
72
|
+
if resized.width < min_width:
|
|
73
|
+
canvas = Image.new("L", (min_width, height), color=color)
|
|
74
|
+
canvas.paste(resized, (0, 0))
|
|
75
|
+
resized = canvas
|
|
76
|
+
if resized.width % align != 0:
|
|
77
|
+
padded_width = resized.width + (align - resized.width % align)
|
|
78
|
+
canvas = Image.new("L", (padded_width, height), color=color)
|
|
79
|
+
canvas.paste(resized, (0, 0))
|
|
80
|
+
resized = canvas
|
|
81
|
+
color = border_color(resized)
|
|
82
|
+
array = np.asarray(resized, dtype=np.float32)
|
|
83
|
+
tensor = torch.from_numpy(array).unsqueeze(0)
|
|
84
|
+
tensor = (tensor / 255.0 - 0.5) / 0.5
|
|
85
|
+
return tensor, float(_normalize_pixel(color))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def image_from_array(array: np.ndarray) -> Image.Image:
|
|
89
|
+
if array.ndim not in (2, 3):
|
|
90
|
+
raise RecognitionError("expected an HxW or HxWx3 image array")
|
|
91
|
+
values = array
|
|
92
|
+
if np.issubdtype(values.dtype, np.floating):
|
|
93
|
+
values = np.clip(values * 255.0, 0, 255).astype(np.uint8)
|
|
94
|
+
else:
|
|
95
|
+
values = values.astype(np.uint8)
|
|
96
|
+
if values.ndim == 2:
|
|
97
|
+
return Image.fromarray(values, mode="L")
|
|
98
|
+
channels = values.shape[2]
|
|
99
|
+
if channels == 3:
|
|
100
|
+
return Image.fromarray(values, mode="RGB")
|
|
101
|
+
if channels == 4:
|
|
102
|
+
return Image.fromarray(values, mode="RGBA")
|
|
103
|
+
raise RecognitionError(f"unsupported channel count: {channels}")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def load_image(source: str | Path) -> Image.Image:
|
|
107
|
+
path = Path(source)
|
|
108
|
+
if not path.exists():
|
|
109
|
+
raise RecognitionError(f"image not found: {path}")
|
|
110
|
+
if path.suffix.lower() == ".pdf":
|
|
111
|
+
raise RecognitionError("use the document loader for PDF files")
|
|
112
|
+
try:
|
|
113
|
+
with Image.open(path) as handle:
|
|
114
|
+
return handle.convert("RGBA") if handle.mode == "RGBA" else handle.convert("RGB")
|
|
115
|
+
except UnidentifiedImageError as exc:
|
|
116
|
+
raise RecognitionError(f"cannot read image: {path}") from exc
|
|
117
|
+
except OSError as exc:
|
|
118
|
+
raise RecognitionError(f"cannot read image: {path}") from exc
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def coerce_image(source: str | Path | Image.Image | np.ndarray) -> Image.Image:
|
|
122
|
+
if isinstance(source, Image.Image):
|
|
123
|
+
return source
|
|
124
|
+
if isinstance(source, np.ndarray):
|
|
125
|
+
return image_from_array(source)
|
|
126
|
+
if isinstance(source, (str, Path)):
|
|
127
|
+
return load_image(source)
|
|
128
|
+
raise RecognitionError(
|
|
129
|
+
"image must be a file path, a PIL image, or a numpy array"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def looks_like_line(image: Image.Image, max_height: int = 96) -> bool:
|
|
134
|
+
width, height = image.size
|
|
135
|
+
return height <= max_height and width >= height
|
wormocr/py.typed
ADDED
|
File without changes
|
wormocr/runtime.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Device selection and a small environment report."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import platform
|
|
7
|
+
import shutil
|
|
8
|
+
|
|
9
|
+
import torch
|
|
10
|
+
|
|
11
|
+
from wormocr.exceptions import WormOCRError
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def resolve_device(name: str | None) -> torch.device:
|
|
15
|
+
requested = (name or "auto").strip().lower()
|
|
16
|
+
if requested == "auto":
|
|
17
|
+
return torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
|
18
|
+
if requested.startswith("cuda") and not torch.cuda.is_available():
|
|
19
|
+
raise WormOCRError(
|
|
20
|
+
"CUDA was requested, but this PyTorch installation cannot see a GPU. "
|
|
21
|
+
f"torch {torch.__version__} is the build in use. Run with --device cpu."
|
|
22
|
+
)
|
|
23
|
+
try:
|
|
24
|
+
return torch.device(requested)
|
|
25
|
+
except (TypeError, RuntimeError) as exc:
|
|
26
|
+
raise WormOCRError(f"unknown device {name!r}") from exc
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def environment_report() -> dict:
|
|
30
|
+
report: dict = {
|
|
31
|
+
"python": platform.python_version(),
|
|
32
|
+
"platform": platform.platform(),
|
|
33
|
+
"cpu": platform.processor() or os.environ.get("PROCESSOR_IDENTIFIER", "unknown"),
|
|
34
|
+
"cpu_count": os.cpu_count(),
|
|
35
|
+
"torch": torch.__version__,
|
|
36
|
+
"cuda_available": bool(torch.cuda.is_available()),
|
|
37
|
+
"cuda_build": torch.version.cuda,
|
|
38
|
+
"torch_threads": int(torch.get_num_threads()),
|
|
39
|
+
"mkldnn": bool(torch.backends.mkldnn.is_available()),
|
|
40
|
+
}
|
|
41
|
+
if torch.cuda.is_available():
|
|
42
|
+
report["gpu"] = torch.cuda.get_device_name(0)
|
|
43
|
+
try:
|
|
44
|
+
import onnxruntime
|
|
45
|
+
|
|
46
|
+
report["onnxruntime"] = onnxruntime.__version__
|
|
47
|
+
except ImportError:
|
|
48
|
+
report["onnxruntime"] = None
|
|
49
|
+
try:
|
|
50
|
+
import onnx
|
|
51
|
+
|
|
52
|
+
report["onnx"] = onnx.__version__
|
|
53
|
+
except ImportError:
|
|
54
|
+
report["onnx"] = None
|
|
55
|
+
report["tesseract"] = shutil.which("tesseract")
|
|
56
|
+
return report
|