ocr-harness 1.0.3__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.
- consts.py +5 -0
- evaluation/__init__.py +13 -0
- evaluation/evaluation_analysis.py +237 -0
- evaluation/evaluation_pipeline.py +483 -0
- evaluation/metrics.py +265 -0
- evaluation/ocr_ground_truth.py +42 -0
- ocr_backbone/__init__.py +16 -0
- ocr_backbone/bounding_box.py +74 -0
- ocr_backbone/image_preprocessing.py +181 -0
- ocr_backbone/input_image.py +24 -0
- ocr_backbone/ocr_abstract.py +212 -0
- ocr_backbone/ocr_config.py +218 -0
- ocr_backbone/ocr_result.py +44 -0
- ocr_backbone/polygon.py +92 -0
- ocr_harness-1.0.3.dist-info/METADATA +322 -0
- ocr_harness-1.0.3.dist-info/RECORD +40 -0
- ocr_harness-1.0.3.dist-info/WHEEL +5 -0
- ocr_harness-1.0.3.dist-info/licenses/LICENSE +21 -0
- ocr_harness-1.0.3.dist-info/top_level.txt +7 -0
- ocr_modules/__init__.py +40 -0
- ocr_modules/easyocr_module.py +64 -0
- ocr_modules/paddleocr_module.py +137 -0
- ocr_modules/pytesseract_module.py +75 -0
- scripts/__init__.py +1 -0
- scripts/draw_bboxes.py +107 -0
- scripts/run_evaluation.py +120 -0
- tagging_tool/__init__.py +0 -0
- tagging_tool/__main__.py +58 -0
- tagging_tool/app.py +271 -0
- utils/__init__.py +1 -0
- utils/binarize.py +15 -0
- utils/callable_descriptors.py +121 -0
- utils/dataset_utils.py +271 -0
- utils/image_utils.py +49 -0
- utils/json_utils.py +30 -0
- utils/lazy_import.py +36 -0
- utils/logging_config.py +21 -0
- utils/serialize_utils.py +211 -0
- utils/statistics.py +93 -0
- utils/text_utils.py +27 -0
consts.py
ADDED
evaluation/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Evaluation pipeline, metrics, and ground-truth types.
|
|
2
|
+
|
|
3
|
+
The re-export below exposes the package's public ground-truth type
|
|
4
|
+
and, as a necessary side effect, ensures every ``SerializableClass``
|
|
5
|
+
subclass in this package registers itself in
|
|
6
|
+
``SerializableClass._registry`` (via ``__init_subclass__``) the moment
|
|
7
|
+
any consumer touches the package. Add a line here when introducing a
|
|
8
|
+
new ``SerializableClass`` subclass under ``evaluation/``.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from evaluation.ocr_ground_truth import OCRGroundTruth
|
|
12
|
+
|
|
13
|
+
__all__ = ["OCRGroundTruth"]
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""Plotting and analysis utilities for OCR evaluation results.
|
|
2
|
+
|
|
3
|
+
Loads aggregate and per-image results produced by the evaluation
|
|
4
|
+
pipeline and generates comparative visualisations across OCR modules.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import matplotlib.pyplot as plt
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
from evaluation.evaluation_pipeline import ALL_TAGS_KEY, AggregateResult
|
|
14
|
+
from evaluation.metrics import Metric
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def plot_metric_comparison(
|
|
20
|
+
aggregate: AggregateResult,
|
|
21
|
+
stat: str = "mean",
|
|
22
|
+
ci_level: str = "95",
|
|
23
|
+
save_path: str | Path | None = None,
|
|
24
|
+
tag: str = ALL_TAGS_KEY,
|
|
25
|
+
) -> plt.Figure:
|
|
26
|
+
"""Bar chart comparing all modules across every metric.
|
|
27
|
+
|
|
28
|
+
Each metric gets a group of bars, one per module. When ``stat`` is
|
|
29
|
+
``mean``, asymmetric error bars are drawn from the percentile-based
|
|
30
|
+
confidence interval stored under the requested ``ci_level``.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
aggregate: An :class:`AggregateResult` (e.g. from
|
|
34
|
+
``AggregateResult.from_path(...)`` or
|
|
35
|
+
:class:`EvaluationResult.aggregate` of a live run).
|
|
36
|
+
stat: Which statistic to plot (mean, median, min, max).
|
|
37
|
+
ci_level: Confidence interval key to use for error bars
|
|
38
|
+
(default ``"95"``).
|
|
39
|
+
save_path: If given, save the figure to this path.
|
|
40
|
+
tag: Which tag's slice to plot. Defaults to ``ALL_TAGS_KEY``
|
|
41
|
+
(aggregated across all images).
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
The matplotlib Figure.
|
|
45
|
+
|
|
46
|
+
Raises:
|
|
47
|
+
KeyError: If ``tag`` is not present in ``aggregate``.
|
|
48
|
+
"""
|
|
49
|
+
view = aggregate.for_tag(tag)
|
|
50
|
+
|
|
51
|
+
x = np.arange(len(view.metrics))
|
|
52
|
+
width = 0.8 / len(view.labels)
|
|
53
|
+
|
|
54
|
+
fig, ax = plt.subplots(figsize=(max(10, len(view.metrics) * 2), 6))
|
|
55
|
+
|
|
56
|
+
for i, label in enumerate(view.labels):
|
|
57
|
+
values = np.array([view.data[label][m][stat] for m in view.metrics])
|
|
58
|
+
yerr = None
|
|
59
|
+
if stat == "mean":
|
|
60
|
+
ci_bounds = [view.data[label][m].get("ci", {}).get(ci_level) for m in view.metrics]
|
|
61
|
+
if all(b is not None for b in ci_bounds):
|
|
62
|
+
lower = np.array([values[j] - b[0] for j, b in enumerate(ci_bounds)])
|
|
63
|
+
upper = np.array([b[1] - values[j] for j, b in enumerate(ci_bounds)])
|
|
64
|
+
yerr = np.array([lower, upper])
|
|
65
|
+
ax.bar(x + i * width, values, width, label=label, yerr=yerr, capsize=3)
|
|
66
|
+
|
|
67
|
+
ax.set_xticks(x + width * (len(view.labels) - 1) / 2)
|
|
68
|
+
ax.set_xticklabels([Metric._registry[m].display_name for m in view.metrics], rotation=30, ha="right")
|
|
69
|
+
ax.set_ylabel(stat.capitalize())
|
|
70
|
+
ax.set_title(f"OCR Module Comparison ({stat})")
|
|
71
|
+
ax.legend()
|
|
72
|
+
fig.tight_layout()
|
|
73
|
+
|
|
74
|
+
if save_path:
|
|
75
|
+
fig.savefig(save_path, dpi=150)
|
|
76
|
+
|
|
77
|
+
return fig
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def plot_radar(
|
|
81
|
+
aggregate: AggregateResult,
|
|
82
|
+
stat: str = "mean",
|
|
83
|
+
save_path: str | Path | None = None,
|
|
84
|
+
tag: str = ALL_TAGS_KEY,
|
|
85
|
+
) -> plt.Figure:
|
|
86
|
+
"""Radar (spider) chart comparing modules across all metrics.
|
|
87
|
+
|
|
88
|
+
All values are clipped to [0, 1] for display so the chart axes
|
|
89
|
+
remain comparable.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
aggregate: An :class:`AggregateResult` (e.g. from
|
|
93
|
+
``AggregateResult.from_path(...)`` or
|
|
94
|
+
:class:`EvaluationResult.aggregate` of a live run).
|
|
95
|
+
stat: Which statistic to plot (mean, median, min, max).
|
|
96
|
+
save_path: If given, save the figure to this path.
|
|
97
|
+
tag: Which tag's slice to plot. Defaults to ``ALL_TAGS_KEY``
|
|
98
|
+
(aggregated across all images).
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
The matplotlib Figure.
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
KeyError: If ``tag`` is not present in ``aggregate``.
|
|
105
|
+
"""
|
|
106
|
+
view = aggregate.for_tag(tag)
|
|
107
|
+
n_metrics = len(view.metrics)
|
|
108
|
+
|
|
109
|
+
angles = np.linspace(0, 2 * np.pi, n_metrics, endpoint=False).tolist()
|
|
110
|
+
angles.append(angles[0])
|
|
111
|
+
|
|
112
|
+
fig, ax = plt.subplots(figsize=(8, 8), subplot_kw={"polar": True})
|
|
113
|
+
|
|
114
|
+
for label in view.labels:
|
|
115
|
+
values = [min(view.data[label][m][stat], 1.0) for m in view.metrics]
|
|
116
|
+
values.append(values[0])
|
|
117
|
+
ax.plot(angles, values, linewidth=2, label=label)
|
|
118
|
+
ax.fill(angles, values, alpha=0.15)
|
|
119
|
+
|
|
120
|
+
ax.set_xticks(angles[:-1])
|
|
121
|
+
ax.set_xticklabels([])
|
|
122
|
+
|
|
123
|
+
label_radius = 1.18
|
|
124
|
+
display_names = [Metric._registry[m].display_name for m in view.metrics]
|
|
125
|
+
for angle, name in zip(angles[:-1], display_names, strict=True):
|
|
126
|
+
angle_deg = np.degrees(angle)
|
|
127
|
+
rotation = angle_deg - 90 if angle_deg <= 180 else angle_deg + 90
|
|
128
|
+
ha = "center"
|
|
129
|
+
if not (np.isclose(angle, 0) or np.isclose(angle, np.pi)):
|
|
130
|
+
ha = "left" if 0 < angle < np.pi else "right"
|
|
131
|
+
ax.text(
|
|
132
|
+
angle,
|
|
133
|
+
label_radius,
|
|
134
|
+
name,
|
|
135
|
+
size=9,
|
|
136
|
+
ha=ha,
|
|
137
|
+
va="center",
|
|
138
|
+
rotation=rotation,
|
|
139
|
+
rotation_mode="anchor",
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
ax.set_ylim(0, 1.05)
|
|
143
|
+
ax.set_title(f"OCR Radar Chart ({stat})", y=1.08)
|
|
144
|
+
ax.legend(loc="upper right", bbox_to_anchor=(1.3, 1.1))
|
|
145
|
+
|
|
146
|
+
if save_path:
|
|
147
|
+
fig.savefig(save_path, dpi=150, bbox_inches="tight")
|
|
148
|
+
|
|
149
|
+
return fig
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def plot_stat_range(
|
|
153
|
+
aggregate: AggregateResult,
|
|
154
|
+
save_path: str | Path | None = None,
|
|
155
|
+
tag: str = ALL_TAGS_KEY,
|
|
156
|
+
) -> plt.Figure:
|
|
157
|
+
"""Box-style range plot showing min, mean, and max per metric per module.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
aggregate: An :class:`AggregateResult` (e.g. from
|
|
161
|
+
``AggregateResult.from_path(...)`` or
|
|
162
|
+
:class:`EvaluationResult.aggregate` of a live run).
|
|
163
|
+
save_path: If given, save the figure to this path.
|
|
164
|
+
tag: Which tag's slice to plot. Defaults to ``ALL_TAGS_KEY``
|
|
165
|
+
(aggregated across all images).
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
The matplotlib Figure.
|
|
169
|
+
|
|
170
|
+
Raises:
|
|
171
|
+
KeyError: If ``tag`` is not present in ``aggregate``.
|
|
172
|
+
"""
|
|
173
|
+
view = aggregate.for_tag(tag)
|
|
174
|
+
|
|
175
|
+
fig, axes = plt.subplots(1, len(view.metrics), figsize=(4 * len(view.metrics), 5), sharey=False)
|
|
176
|
+
if len(view.metrics) == 1:
|
|
177
|
+
axes = [axes]
|
|
178
|
+
|
|
179
|
+
for ax, metric in zip(axes, view.metrics, strict=True):
|
|
180
|
+
means = [view.data[label][metric]["mean"] for label in view.labels]
|
|
181
|
+
mins = [view.data[label][metric]["min"] for label in view.labels]
|
|
182
|
+
maxs = [view.data[label][metric]["max"] for label in view.labels]
|
|
183
|
+
|
|
184
|
+
x = np.arange(len(view.labels))
|
|
185
|
+
ax.errorbar(
|
|
186
|
+
x,
|
|
187
|
+
means,
|
|
188
|
+
yerr=[np.array(means) - np.array(mins), np.array(maxs) - np.array(means)],
|
|
189
|
+
fmt="o",
|
|
190
|
+
capsize=5,
|
|
191
|
+
capthick=2,
|
|
192
|
+
)
|
|
193
|
+
ax.set_xticks(x)
|
|
194
|
+
ax.set_xticklabels(view.labels, rotation=45, ha="right", fontsize=8)
|
|
195
|
+
ax.set_title(Metric._registry[metric].display_name, fontsize=10)
|
|
196
|
+
|
|
197
|
+
fig.suptitle("Metric Ranges (min / mean / max)", fontsize=13)
|
|
198
|
+
fig.tight_layout()
|
|
199
|
+
|
|
200
|
+
if save_path:
|
|
201
|
+
fig.savefig(save_path, dpi=150)
|
|
202
|
+
|
|
203
|
+
return fig
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def summary_table(aggregate: AggregateResult, stat: str = "mean", tag: str = ALL_TAGS_KEY) -> str:
|
|
207
|
+
"""Return a formatted text table of aggregate results.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
aggregate: An :class:`AggregateResult` (e.g. from
|
|
211
|
+
``AggregateResult.from_path(...)`` or
|
|
212
|
+
:class:`EvaluationResult.aggregate` of a live run).
|
|
213
|
+
stat: Which statistic to tabulate.
|
|
214
|
+
tag: Which tag's slice to tabulate. Defaults to ``ALL_TAGS_KEY``
|
|
215
|
+
(aggregated across all images).
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
A multi-line string table suitable for printing.
|
|
219
|
+
|
|
220
|
+
Raises:
|
|
221
|
+
KeyError: If ``tag`` is not present in ``aggregate``.
|
|
222
|
+
"""
|
|
223
|
+
view = aggregate.for_tag(tag)
|
|
224
|
+
|
|
225
|
+
col_width = max(len(Metric._registry[m].display_name) for m in view.metrics) + 2
|
|
226
|
+
label_width = max(len(label) for label in view.labels) + 2
|
|
227
|
+
|
|
228
|
+
header = " " * label_width + "".join(Metric._registry[m].display_name.rjust(col_width) for m in view.metrics)
|
|
229
|
+
lines = [header, "-" * len(header)]
|
|
230
|
+
|
|
231
|
+
for label in view.labels:
|
|
232
|
+
row = label.ljust(label_width) + "".join(
|
|
233
|
+
f"{view.data[label][m][stat]:.4f}".rjust(col_width) for m in view.metrics
|
|
234
|
+
)
|
|
235
|
+
lines.append(row)
|
|
236
|
+
|
|
237
|
+
return "\n".join(lines)
|