| """位图 → 64×64 归一化。 |
| |
| 输入:HWDB GNT 原始灰度位图(uint8,255=白底,0=黑笔画,任意宽高) |
| 输出:归一化的 64×64 uint8 灰度图,**保持原始规约**(255=白底,0=黑笔画) |
| |
| 为什么保持白底黑字:推理时手机端把用户笔迹渲染成"白纸黑笔"喂模型,训练数据 |
| 和推理输入分布对齐;若要反转让前景=高值(MNIST 风格),在 dataloader 用 |
| `255 - x` 即可。 |
| |
| 流程: |
| 1. 阈值化找笔画像素的最小外接矩形(笔画 = 像素值低) |
| 2. 等比缩放,长边对齐到 56 像素 |
| 3. 居中放到 64×64 画布(白底,周围 4 像素白边距) |
| """ |
|
|
| from __future__ import annotations |
|
|
| import numpy as np |
| from PIL import Image |
|
|
|
|
| CANVAS_SIZE = 64 |
| CONTENT_SIZE = 56 |
| MARGIN = (CANVAS_SIZE - CONTENT_SIZE) // 2 |
| FG_THRESHOLD = 220 |
|
|
|
|
| def normalize(bitmap: np.ndarray) -> np.ndarray: |
| """归一化到 64×64 uint8,白底黑字(255=白,0=黑)。""" |
| if bitmap.ndim != 2: |
| raise ValueError(f"期望 2D 位图,得到 shape={bitmap.shape}") |
|
|
| |
| fg = bitmap < FG_THRESHOLD |
| if not fg.any(): |
| |
| return np.full((CANVAS_SIZE, CANVAS_SIZE), 255, dtype=np.uint8) |
| ys, xs = np.where(fg) |
| y0, y1 = ys.min(), ys.max() + 1 |
| x0, x1 = xs.min(), xs.max() + 1 |
| cropped = bitmap[y0:y1, x0:x1] |
|
|
| |
| h, w = cropped.shape |
| scale = CONTENT_SIZE / max(h, w) |
| new_h = max(1, int(round(h * scale))) |
| new_w = max(1, int(round(w * scale))) |
| pil = Image.fromarray(cropped, mode="L") |
| pil = pil.resize((new_w, new_h), Image.BILINEAR) |
| resized = np.asarray(pil, dtype=np.uint8) |
|
|
| |
| canvas = np.full((CANVAS_SIZE, CANVAS_SIZE), 255, dtype=np.uint8) |
| off_y = (CANVAS_SIZE - new_h) // 2 |
| off_x = (CANVAS_SIZE - new_w) // 2 |
| canvas[off_y:off_y + new_h, off_x:off_x + new_w] = resized |
| return canvas |
|
|
|
|
| if __name__ == "__main__": |
| |
| src = np.full((100, 80), 255, dtype=np.uint8) |
| src[20:80, 15:65] = 50 |
| out = normalize(src) |
| print(f"input shape: {src.shape}, output shape: {out.shape}") |
| print(f"output range: [{out.min()}, {out.max()}]") |
| print(f"前景像素数(< {FG_THRESHOLD}): {(out < FG_THRESHOLD).sum()}") |
|
|