ponychart-classifier 0.1.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.
- ponychart_classifier/__init__.py +27 -0
- ponychart_classifier/inference.py +114 -0
- ponychart_classifier/model.onnx +60784 -119
- ponychart_classifier/model_spec.py +38 -0
- ponychart_classifier/py.typed +0 -0
- ponychart_classifier/thresholds.json +8 -0
- ponychart_classifier/training/__init__.py +141 -0
- ponychart_classifier/training/constants.py +70 -0
- ponychart_classifier/training/dataset.py +265 -0
- ponychart_classifier/training/device.py +37 -0
- ponychart_classifier/training/export.py +46 -0
- ponychart_classifier/training/log_helpers.py +25 -0
- ponychart_classifier/training/model.py +156 -0
- ponychart_classifier/training/sampling.py +154 -0
- ponychart_classifier/training/splitting.py +142 -0
- ponychart_classifier/training/training.py +380 -0
- ponychart_classifier-0.1.0.dist-info/METADATA +194 -0
- ponychart_classifier-0.1.0.dist-info/RECORD +20 -0
- ponychart_classifier-0.1.0.dist-info/WHEEL +5 -0
- ponychart_classifier-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""PonyChart classifier -- inference constants and prediction utilities."""
|
|
2
|
+
|
|
3
|
+
from .inference import PonyChartClassifier, predict, preload
|
|
4
|
+
from .model_spec import (
|
|
5
|
+
CLASS_NAMES,
|
|
6
|
+
IMAGENET_MEAN,
|
|
7
|
+
IMAGENET_STD,
|
|
8
|
+
INPUT_SIZE,
|
|
9
|
+
MAX_K,
|
|
10
|
+
NUM_CLASSES,
|
|
11
|
+
PRE_RESIZE,
|
|
12
|
+
select_predictions,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"CLASS_NAMES",
|
|
17
|
+
"IMAGENET_MEAN",
|
|
18
|
+
"IMAGENET_STD",
|
|
19
|
+
"INPUT_SIZE",
|
|
20
|
+
"MAX_K",
|
|
21
|
+
"NUM_CLASSES",
|
|
22
|
+
"PRE_RESIZE",
|
|
23
|
+
"PonyChartClassifier",
|
|
24
|
+
"predict",
|
|
25
|
+
"preload",
|
|
26
|
+
"select_predictions",
|
|
27
|
+
]
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""High-level inference API for PonyChart character classification."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import cv2 as cv
|
|
11
|
+
import numpy as np
|
|
12
|
+
import onnxruntime as ort
|
|
13
|
+
|
|
14
|
+
from .model_spec import (
|
|
15
|
+
CLASS_NAMES,
|
|
16
|
+
IMAGENET_MEAN,
|
|
17
|
+
IMAGENET_STD,
|
|
18
|
+
INPUT_SIZE,
|
|
19
|
+
PRE_RESIZE,
|
|
20
|
+
select_predictions,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
_IMAGENET_MEAN = np.array(IMAGENET_MEAN, dtype=np.float32)
|
|
24
|
+
_IMAGENET_STD = np.array(IMAGENET_STD, dtype=np.float32)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _package_dir() -> str:
|
|
28
|
+
return os.path.dirname(__file__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class PonyChartClassifier:
|
|
32
|
+
"""Lazy-loading ONNX classifier for PonyChart images."""
|
|
33
|
+
|
|
34
|
+
def __init__(self) -> None:
|
|
35
|
+
self._loaded = False
|
|
36
|
+
self._session: Any = None
|
|
37
|
+
self._classes: list[str] = list(CLASS_NAMES)
|
|
38
|
+
self._thresholds: dict[str, float] = {}
|
|
39
|
+
|
|
40
|
+
def load(self) -> None:
|
|
41
|
+
"""Load the ONNX model and thresholds. Safe to call multiple times."""
|
|
42
|
+
if self._loaded:
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
d = _package_dir()
|
|
46
|
+
model_path = os.path.join(d, "model.onnx")
|
|
47
|
+
th_path = os.path.join(d, "thresholds.json")
|
|
48
|
+
self._session = ort.InferenceSession(
|
|
49
|
+
model_path, providers=["CPUExecutionProvider"]
|
|
50
|
+
)
|
|
51
|
+
with open(th_path, encoding="utf-8") as f:
|
|
52
|
+
self._thresholds = json.load(f)
|
|
53
|
+
self._loaded = True
|
|
54
|
+
|
|
55
|
+
def _preprocess(self, bgr: np.ndarray[Any, Any]) -> np.ndarray[Any, Any]:
|
|
56
|
+
"""BGR image -> NCHW float32 tensor (matching training transforms)."""
|
|
57
|
+
resized = cv.resize(bgr, (PRE_RESIZE, PRE_RESIZE), interpolation=cv.INTER_AREA)
|
|
58
|
+
offset = (PRE_RESIZE - INPUT_SIZE) // 2
|
|
59
|
+
cropped = resized[offset : offset + INPUT_SIZE, offset : offset + INPUT_SIZE]
|
|
60
|
+
rgb = cv.cvtColor(cropped, cv.COLOR_BGR2RGB).astype(np.float32) / 255.0
|
|
61
|
+
normalized = (rgb - _IMAGENET_MEAN) / _IMAGENET_STD
|
|
62
|
+
# HWC -> CHW -> NCHW
|
|
63
|
+
return normalized.transpose(2, 0, 1)[np.newaxis, ...].astype(np.float32)
|
|
64
|
+
|
|
65
|
+
def predict(
|
|
66
|
+
self, img_path: str, min_k: int = 1, max_k: int = 3
|
|
67
|
+
) -> tuple[list[str], dict[str, float]]:
|
|
68
|
+
"""Predict characters in a PonyChart image.
|
|
69
|
+
|
|
70
|
+
Returns ``(picked_names, scores)`` where *picked_names* is a list of
|
|
71
|
+
selected character names and *scores* maps every class name to its
|
|
72
|
+
sigmoid probability.
|
|
73
|
+
"""
|
|
74
|
+
self.load()
|
|
75
|
+
img = cv.imread(img_path, cv.IMREAD_COLOR)
|
|
76
|
+
if img is None:
|
|
77
|
+
raise RuntimeError(f"Cannot read image: {img_path}")
|
|
78
|
+
|
|
79
|
+
input_tensor = self._preprocess(img)
|
|
80
|
+
input_name: str = self._session.get_inputs()[0].name
|
|
81
|
+
logits = self._session.run(None, {input_name: input_tensor})[0]
|
|
82
|
+
probs = 1.0 / (1.0 + np.exp(-logits[0]))
|
|
83
|
+
|
|
84
|
+
scores = {self._classes[i]: float(probs[i]) for i in range(len(self._classes))}
|
|
85
|
+
thresholds = [self._thresholds.get(c, 0.5) for c in self._classes]
|
|
86
|
+
indices = select_predictions(list(probs), thresholds, min_k=min_k, max_k=max_k)
|
|
87
|
+
picked = [self._classes[i] for i in indices]
|
|
88
|
+
return picked, scores
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
_default_classifier = PonyChartClassifier()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def predict(
|
|
95
|
+
img_path: str, min_k: int = 1, max_k: int = 3
|
|
96
|
+
) -> tuple[list[str], dict[str, float]]:
|
|
97
|
+
"""Predict characters using the default classifier instance."""
|
|
98
|
+
return _default_classifier.predict(img_path, min_k=min_k, max_k=max_k)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def preload() -> None:
|
|
102
|
+
"""Pre-load the ONNX model to catch dependency issues early."""
|
|
103
|
+
try:
|
|
104
|
+
_default_classifier.load()
|
|
105
|
+
except ImportError as e:
|
|
106
|
+
msg = "onnxruntime failed to load."
|
|
107
|
+
if sys.platform == "win32" and "DLL load failed" in str(e):
|
|
108
|
+
msg += (
|
|
109
|
+
"\nPossible cause: missing Microsoft Visual C++ Redistributable."
|
|
110
|
+
"\nDownload from https://aka.ms/vs/17/release/vc_redist.x64.exe"
|
|
111
|
+
)
|
|
112
|
+
else:
|
|
113
|
+
msg += "\nPlease install: pip install onnxruntime"
|
|
114
|
+
raise RuntimeError(msg) from e
|