Download learned_cost.py from ChrisMoe/template: direct link, hf CLI and curl.
- Browser
- Download file 2.63 kB
-
https://huggingface.co/ChrisMoe/template/resolve/main/learned_cost.py
- Command line
-
hf download hf://ChrisMoe/template/learned_cost.py
-
curl -L -o learned_cost.py https://huggingface.co/ChrisMoe/template/resolve/main/learned_cost.py
2.63 kB
| """Learned stroke cost for HanziFlow. | |
| Needs: numpy, onnxruntime, and the three files from the training notebook: | |
| stroke_encoder.onnx, template_embeddings.npz, calibration.json | |
| Strokes must be in the Make Me a Hanzi frame (1024 box, y up) and resampled to 32 points, | |
| exactly as in the matcher notebook (prep()). | |
| """ | |
| import json | |
| import numpy as np | |
| import onnxruntime as ort | |
| class LearnedCost: | |
| def __init__(self, onnx_path="stroke_encoder.onnx", npz_path="template_embeddings.npz", calib_path="calibration.json"): | |
| self.sess = ort.InferenceSession(onnx_path, providers=["CPUExecutionProvider"]) | |
| z = np.load(npz_path) | |
| self.chars = [str(c) for c in z["chars"]] | |
| self.templates = {c: z[f"emb_{i}"] for i, c in enumerate(self.chars)} # (n_strokes, 64) per character | |
| cal = json.load(open(calib_path)) | |
| self.scale, self.tau, self.good = cal["scale"], cal["tau"], cal["good"] | |
| self.alpha = cal.get("alpha_recommended", 0.5) | |
| def features(S): | |
| # (n, 32, 2) points -> (n, 32, 6); identical to stroke_features() in the training notebook | |
| S = np.asarray(S, np.float32) | |
| x = (S[..., 0] - 512) / 512 | |
| y = (S[..., 1] - 388) / 512 | |
| g = np.gradient(S, axis=-2) | |
| nrm = np.linalg.norm(g, axis=-1) + 1e-6 | |
| rx = (S[..., 0] - S[..., :1, 0]) / 512 | |
| ry = (S[..., 1] - S[..., :1, 1]) / 512 | |
| return np.stack([x, y, g[..., 0] / nrm, g[..., 1] / nrm, rx, ry], axis=-1).astype(np.float32) | |
| def embed(self, S): | |
| return self.sess.run(None, {"stroke_features": self.features(S)})[0] | |
| def costs(self, char, U): | |
| """Learned cost of every drawn stroke (rows) against every template stroke of `char` (columns). | |
| Direction-agnostic: a backwards stroke is flagged by the geometric part of the matcher, not here.""" | |
| te = self.templates[char] | |
| fwd = np.linalg.norm(self.embed(U)[:, None, :] - te[None], axis=-1) | |
| bwd = np.linalg.norm(self.embed(np.ascontiguousarray(U[:, ::-1]))[:, None, :] - te[None], axis=-1) | |
| return np.minimum(fwd, bwd) / self.scale | |
| def cost_fn(self, char, geometric_cost_matrix, alpha=None): | |
| """Drop-in `cost_fn` for align_full(): blends the geometric cost with the learned one. | |
| alpha = 0 -> geometric only, 1 -> learned only, 0.5 -> half and half (recommended).""" | |
| a = self.alpha if alpha is None else alpha | |
| if a == 0: | |
| return None | |
| def fn(U, T): | |
| C_geo, rev = geometric_cost_matrix(U, T) | |
| return (1 - a) * C_geo + a * self.costs(char, U), rev | |
| return fn | |