6 色阶分层
✅ 已验证 · 代码 code/cel.py(分档复用 segment.tiers / segment.pick_flat)
层次是这么来的
人类画的插画靠明确的几档色阶构成层次,不是连续渐变。实测参考画和 bicubic 放大的 origin 在同一块 1:1 特写上量 8 级量化色数,前者更少却更有层次。所以每个部件要切成几档:固有色、阴影 1、阴影 2、高光……
分档
cel.levels(rgb, mask, k=None):
segment.tiers()在部件内做 KMeans,按亮度排序segment.pick_flat()把面积最大的那档定为Flat(不是最亮的——白裙的本色就是最亮档)- 比 Flat 暗的档叫
Shadow1 / Shadow2 …,比它亮的叫Light1 / Light2 …
档数 k 由 pick_k() 按部件的亮度跨度(3–97 分位)自适应,2–5 档。
嵌套,不是并列
这一条是关键。每一档的 mask 不是「只属于这档的像素」,而是「这档及所有比它更深的像素」:
Flat = 整个部件
Shadow1 = 所有比 Flat 暗的像素 (包含 Shadow2)
Shadow2 = 所有比 Shadow1 暗的像素 (包含 Shadow3)
叠层时上层盖下层,每一档只管「比上一档再深一点的那块」。
为什么不并列?并列切法的各档各自独立矢量化,曲线不会正好贴合,档与档之间会漏缝。 嵌套则永远是上层盖下层,没有缝。这也正是画师的图层叠法。
清理
KMeans 分出来的 mask 有碎点和针孔,矢量化之后是几百个 2px 的小环。clean_mask() 做闭运算 + 开运算,去掉小于 min_px=24 的连通域,填掉小于 24 的洞。
矢量化
cel.vectorize() 把每一档变成曲线(第 7 章)。Flat 用对整幅解的 α(另一侧是别的部件),其余档用 domain=部件 限定在部件内解(另一侧是同部件的更浅档)。Flat 还向外扩 expand=0.6 像素盖住接缝(第 14 章)。
实测
origin 的 15 个部件:
| 部件 | 档 | 环 | 控制点 |
|---|---|---|---|
| Hair_Front | 5 | 124 | 4254 |
| Dress_Frill | 5 | 126 | 4148 |
| Hair_Back | 5 | 61 | 2831 |
| Dress_Skirt | 5 | 43 | 1848 |
| … | |||
| 合计 | 594 | 20,495 |
耗时 26.7s(裸 python)。
档数不是越多越好
试过给脸加档:
| Skin_Face | k=4 | k=8 | k=16 |
|---|---|---|---|
| 只铺色阶的 PSNR | 18.89 | 20.08 | 20.80 |
4 倍的档数只换来 1.9 dB。色阶的作用是给结构(眼睛在哪、阴影形状是什么),连续变化的部分交给笔画。这也是为什么第 17 章的规划器起点是「色阶铺完的画面」。
只铺色阶的 1x 模拟。眼睛被压成色块是预期的——这是「铺平涂」阶段,画师的平涂稿也长这样。