5 亚像素边界:从抗锯齿边解 α
✅ 已验证 · 代码 code/contour.py 的 alpha_from_image()
丢掉的信息在哪
origin 是一张抗锯齿的图。在两个部件交界的像素上,颜色是两侧颜色的线性混合:
c = α · C_p + (1 − α) · C_q
α 就是这个像素有多大比例属于部件 p——它编码着边界的亚像素位置。第 4 章的分割把每个像素硬判给一个部件,α 在那一刻被扔了。而它正是「边平不平」的全部依据:一条从二值 mask 里提出来的边,位置误差可达 ±0.5 像素,放大 8 倍就是 ±4 像素的抖动。
解 α
把 c 投影到 C_p − C_q 这条直线上:
α = ((c − C_q) · (C_p − C_q)) / |C_p − C_q|²
两个关键细节:
C_p、C_q 不能用固有色。 有明暗时,固有色离该处的实际颜色很远,解出来的 α 会系统性偏。做法是各取本侧最近的干净像素:把 mask 腐蚀掉 band=2 层,剩下的核心区用最近邻向外推成整幅的场。所以 C_p / C_q 是随位置变的。
两侧颜色太接近时退回二值。 |C_p − C_q| < min_sep(12) 时 α 病态,硬解会把噪声放大成边界抖动。这些地方 α 直接取 0 或 1。
只在边界带(mask 膨胀 2 层 ∖ 腐蚀 2 层)里解,其余地方 α = mask。
实测
对 Hair_Front(90,473 px):
| 解出分数覆盖的像素 | 5,896(6.5% 的部件像素是亚像素边) |
| 边界带 α 分布 p10 / p50 / p90 | 0.05 / 0.30 / 0.89 |
| 面积:硬 mask → 分数 | 90,473 → 88,135(−2.58%) |
面积缩小是对的——硬 mask 把任何标签为 p 的像素都算作整个像素,哪怕它只有 30% 属于 p。
它值多少
第 7 章的自检里,同一套矢量化,喂二值 mask 和喂 α 场的差别:
| 输入 | K90(8x) | aliased% | 形状偏差 |
|---|---|---|---|
| 二值 mask(最佳参数) | 0.0448 | 2.8 | 0.0715 |
| α 场 | 0.0422 | 2.6 | 0.0485 |
形状准确度好 1.5 倍。这是 origin 里唯一残存的亚像素信息,不用就没了。
也用于色阶边界
同一个函数加 domain= 参数,可以在部件内部解色阶之间的 α:这时「另一侧」是同部件里更浅或更深的那档,而不是隔壁部件。搞混的话色阶边会去跟部件边对齐,位置整体偏掉。第 6 章的 cel.vectorize() 就是这么调的。