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.
@@ -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