第 9 章:从 Stroke 到 Path:Sketch 系列思想与贝塞尔生成
1. 开篇段落
在第 5 章中,我们通过传统算法将像素图像“描摹”成了矢量图,那是基于视觉结果的逆向工程。然而,要构建一个真正理解并能像人类一样创作的 SVG-MLLM,仅仅“看图说话”是不够的。我们需要模型掌握绘画的过程(Drawing Process)。
人类画图不是像素的堆砌,而是笔触(Stroke)的时序组合:落笔、滑动、抬笔、移动、再落笔。Google 的 SketchRNN 是将这种时序思想引入矢量生成的开山之作。然而,SketchRNN 生成的仅仅是密集的折线(Polyline),而现代 SVG 的灵魂在于贝塞尔曲线(Bézier Curves)。
本章将完成从“粗糙草图”到“精细矢量”的跨越。我们将深入剖析如何对“画笔的移动”进行序列建模,如何将离散的点序列参数化为 SVG 标准的 <path> 指令,以及如何利用结构先验(如对称性、重复性)来约束生成结果。这是让大模型输出不仅“长得像”,而且“结构美”的关键一步。
本章学习目标:
- 序列建模思维:理解 SketchRNN 的五元组数据格式及其对 SVG 序列化生成的启示。
- 贝塞尔几何:掌握三次贝塞尔曲线的控制点机制,以及它们如何决定曲线的形态与平滑度。
- 拟合策略:学习从 Stroke(点序列)到 Path(曲线参数)的转化算法(如 Schneider 算法与深度拟合)。
- 监督权衡:深刻理解“参数空间监督”与“渲染空间监督”在训练中的博弈与结合。
2. 文字论述
9.1 Stroke-based 建模:模仿人类的绘画直觉
传统的图像生成模型(如 GAN、Diffusion)通常一次性输出整张图的像素。这种方式忽略了图形的构成逻辑。Stroke-based 建模主张:图形是动作的痕迹。
2.1.1 核心数据结构:SketchRNN 五元组
为了让神经网络学习绘画,我们需要定义一种通用的数据格式。最经典的是 SketchRNN 提出的“五元组”格式 $( \Delta x, \Delta y, p_1, p_2, p_3 )$。
- $\Delta x, \Delta y$(Offset 偏移量):
- 模型不预测绝对坐标 $(x, y)$,而是预测相对于上一个点的移动距离。
- 原因:这赋予了模型平移不变性。无论你在画布的左上角还是右下角画一个圆,其 $\Delta x, \Delta y$ 序列模式是几乎一样的。这极大地降低了学习难度。
- $p_1, p_2, p_3$(Pen State 笔状态,One-hot 编码):
- $p_1=1$ (Pen Down / Draw):笔尖接触纸面并移动。这是构成线条的实际部分。
- $p_2=1$ (Pen Up / Lift):笔尖离开纸面,移动到新位置。这对应 SVG 中的
M (MoveTo) 指令,用于切断线条,开始新的笔画。
- $p_3=1$ (End of Drawing):绘画结束。对应 SVG 文件的结束或
<EOF> Token。
ASCII 图示:笔触序列的数据流
Frame: t=0 t=1 t=2 t=3 t=4
Pos: (0,0) -> (10,10) ---> (20,20) ---> (50,50) ---> (End)
Action: Start Draw Draw Lift Stop
------------------------------------------------------------------
dx: 0 10 10 30 0
dy: 0 10 10 30 0
p1: 1 1 0 0 0
p2: 0 0 1 0 0
p3: 0 0 0 1 1
SVG: M L 10 10 L 20 20 M 50 50 (Done)
Rule of Thumb (建模法则):
在构建 SVG-MLLM 的 Tokenizer 时,相对坐标通常优于绝对坐标,因为它们不仅易于泛化,还能通过限制数值范围(如 -128 到 127)来压缩词表大小。但相对坐标容易产生累积误差(Drifting),需要配合周期性的绝对坐标校正(Anchor Points)。
9.2 从 Polyline 到 Bézier:跨越“离散”与“连续”的鸿沟
SketchRNN 生成的结果本质上是密集的折线(Polyline)。虽然视觉上像曲线,但数据层面非常低效且难以编辑。SVG 的核心优势在于使用参数化的曲线。
- Stroke (Polyline): 数据量大,放大后有棱角,无法体现“曲率”语义。
- SVG Path (Bézier): 数据极简,无限分辨率,蕴含了“切线”、“平滑”等几何语义。
要训练一个优秀的 SVG-MLLM,我们不能止步于生成 Polyline,必须让模型学会输出 <path d="M ... C ...">。
9.3 贝塞尔参数化:控制点空间的几何直觉
理解贝塞尔曲线是理解 SVG 生成模型的前提。SVG 主要使用三次贝塞尔曲线(Cubic Bézier),由四个点定义:$P_0$(起点)、$P_1$(控制点1)、$P_2$(控制点2)、$P_3$(终点)。
2.3.1 控制点的物理意义
不要把控制点仅仅看作坐标。它们具有明确的物理意义:
- 方向(切线):曲线在 $P_0$ 处的切线方向,就是向量 $\vec{P_0 P_1}$ 的方向。
- 力度(速度):$\vec{P_0 P_1}$ 的长度决定了曲线向该方向延伸的“惯性”大小。长度越长,曲线在该方向贴合得越久。
ASCII 图示:控制点如何影响曲线形态
P1 (Handle 1) P2 (Handle 2)
+ +
| (Pulling Up) /
| / (Pulling Right-Up)
| /
P0 (Start) .................... P3 (End)
\ /
\ /
\__ Actual Curve is __/
pulled by P1, P2
2.3.2 连续性(Smoothness)
在生成多段连续的贝塞尔曲线时,前一段的 $P_2$、公共端点 $P_{join}$、后一段的 $P_1$ 必须共线,才能保证 $G^1$ 连续(光滑无折角)。这不仅是几何约束,也是训练模型时用于设计 Loss 的重要先验。
9.4 由 Stroke 拟合到 Path(The Fitting Strategy)
在训练数据准备阶段,或者在两阶段生成的模型中,我们经常需要把 Stroke(点序列)转化为 Path(贝塞尔)。主要有两种路径:
9.4.1 传统算法:Schneider’s Algorithm
这是图形学中最经典的拟合算法,也是许多 SVG 矢量化工具的内核。
- 尝试拟合:试图用一条贝塞尔曲线拟合整段点集。
- 计算误差:找出离拟合曲线最远的数据点,计算距离 $d_{max}$。
- 递归切分:如果 $d_{max} > \text{threshold}$,则在误差最大点将数据切分为两段,分别递归执行步骤 1。
- 拐点检测:在拟合前,必须先利用角度变化率检测出尖锐的拐角(Corner),并在该处强制打断,否则会产生严重的振铃效应。
在 MLLM 时代,我们可以让模型端到端学习。
- 输入:文本提示 或 图像特征。
- 输出:直接预测 SVG 命令 Token(
M, C, L, z)和坐标数值。
- 难点:模型很难凭空学会“$P_1, P_2, P_joint$ 三点共线”这种硬约束,导致生成的 SVG 经常出现看起来很怪异的“结节”或“尖刺”。
Rule of Thumb (拟合策略):
对于初学者或 MVP 系统,建议采用 Prediction-then-Fitting 策略。即让模型先生成密集的 Stroke(这很容易收敛),然后用确定性的 Schneider 算法后处理成 SVG。直接让模型预测贝塞尔控制点(End-to-End)通常需要极大量的数据和复杂的 Loss 设计。
9.5 条件生成与结构先验
SVG 生成不仅仅是画线条,更是画“有意义的结构”。
- 文本条件 (Text-to-SVG):
利用 CLIP 等预训练模型的 Text Encoder,将文本 Embedding 注入到 SVG Decoder 的 Cross-Attention 层。这要求数据集具备高质量的
<SVG, Caption> 对(详见第 4 章数据工程)。
- 结构先验 (Inductive Bias):
很多 SVG 图标具有对称性。可以在模型架构中引入对称性约束,例如只生成左半边,强行镜像得到右半边。虽然这降低了通用性,但能显著提高特定领域(如人脸、徽章)的生成质量。
9.6 渲染监督 vs 结构监督:训练的核心博弈
这是本章乃至整个 SVG 生成领域最深刻的矛盾:我们该优化“参数”还是优化“像素”?
9.6.1 结构监督 (Parameter/Sequence Loss)
即直接计算预测坐标 $(x, y)$ 与真值 $(\hat{x}, \hat{y})$ 的距离(MSE 或 Cross-Entropy)。
- 优点:强制模型学习 SVG 的语法结构,拓扑关系明确。
- 缺点(多义性):SVG 的非唯一性。画同一个圆,可以用 4 段贝塞尔曲线,也可以用 8 段;可以顺时针画,也可以逆时针画。如果模型预测了正确的圆,但控制点分布与真值不同,Loss 会非常大。这会误导模型的梯度下降方向。
9.6.2 渲染监督 (Rendering/Pixel Loss)
将生成的 SVG 栅格化成图像,计算图像与真值图像的差异。
- 优点:关注视觉结果。只要画出来的图是对的,不管你用了多少个点,Loss 都很低。这解决了多义性问题。
- 缺点:
- 梯度阻断:标准光栅化器不可微(详见第 7 章)。
- 几何垃圾:模型为了凑像素,可能会画出极度扭曲、自交但视觉上重叠的线条。这些 SVG 在编辑软件中打开是不可用的。
Rule of Thumb (混合监督):
现代 SOTA 方法(如 DeepSVG, Im2Vec)通常采用 Hybrid Loss:
\(L_{total} = \lambda_1 L_{seq} + \lambda_2 L_{render} + \lambda_3 L_{aux}\)
- $L_{seq}$:保证语法合法性。
- $L_{render}$:保证视觉相似性。
- $L_{aux}$:辅助几何约束(如惩罚过短线段、惩罚过大曲率、惩罚自交)。
3. 本章小结
本章我们深入了 SVG 生成的“里子”。我们发现,要让机器像人一样画画,必须从像素思维转变为序列动作思维。
- Stroke 建模:通过 $(\Delta x, \Delta y, p)$ 五元组,我们捕捉了绘画的动态过程。
- 贝塞尔拟合:通过 Schneider 算法或端到端学习,我们将离散动作升维为数学上光滑的连续曲线。
- 监督难题:我们揭示了“参数监督”与“渲染监督”的矛盾——前者太死板,后者太自由。构建高质量 SVG-MLLM 的秘诀,往往在于如何设计巧妙的 Loss 函数,在“视觉相似”与“拓扑整洁”之间找到平衡点。
4. 练习题
基础题(熟悉材料)
- 坐标转换:给定起始点绝对坐标 $(100, 100)$,以及笔触序列 $\Delta = [(10, 0), (0, 10), (-10, 0), (0, -10)]$,请画出其轨迹,并计算终点的绝对坐标。这是一个什么形状?
- 贝塞尔几何:一条三次贝塞尔曲线,如果 $P_0=(0,0), P_1=(10,0), P_2=(20,0), P_3=(30,0)$。请问这条曲线在视觉上是什么样子?为什么?
- Pen State 理解:在 SketchRNN 格式中,如果连续出现三个点的状态都是 $p_2=1$(Pen Up),这在实际 SVG 渲染中意味着什么?这种情况合理吗?
- 拟合算法:在使用 Schneider 算法时,如果我们将“最大允许误差”阈值设置得非常大(例如无穷大),拟合出来的结果会变成什么样?
挑战题(包括开放性思考)
- Loss 设计:假设你只使用渲染监督(Pixel Loss)来训练 SVG 生成器。训练初期,Loss 下降很快,但后期生成的 SVG 线条出现了严重的“抖动”和很多微小的碎片线段。请分析原因,并提出两种基于几何的辅助 Loss 来解决这个问题。
- 多义性困境:给定一个简单的正方形图像作为输入,你希望模型生成对应的 SVG。
- 模型 A 顺时针生成了 4 条边。
- 模型 B 逆时针生成了 4 条边。
- 模型 C 用一条很粗的描边线画了一个矩形。
- 如果是基于 Sequence Cross-Entropy Loss 训练,且数据集里这三种画法都存在,模型会发生什么现象?(提示:考虑“平均化”效应)。
- Tokenization 优化:浮点数坐标(如
12.345)直接 Token 化会产生大量 Token。请设计一种量化(Quantization)方案,既能大幅缩短 Context Length,又能保证生成图像在 256x256 画布上的视觉误差肉眼不可见。
点击查看练习题提示与答案思路
**提示与思路:**
1. **答案**:轨迹是 `(100,100)->(110,100)->(110,110)->(100,110)->(100,100)`。这是一个边长为 10 的正方形。终点回到了 `(100, 100)`。
2. **答案**:这是一条直线。因为控制点 $P_1, P_2$ 都在 $P_0$ 到 $P_3$ 的连线上,切线方向从未改变,没有“拉力”将曲线拉离直线路径。
3. **答案**:意味着笔抬起来在空中移动了三次,但没有画任何东西。这在 SVG 中对应连续的 `M` 指令。这通常是不合理的冗余数据,应当在数据清洗阶段去除,合并为一次 `M` 移动。
4. **答案**:算法将只用**一条**三次贝塞尔曲线去拟合所有的点。无论原本形状多复杂(比如一个五角星),都会被强制拟合成一条单一的、可能极度扭曲的曲线,丢失所有细节。
5. **思路**:
* **原因**:Pixel Loss 只看结果,不看过程。模型发现用大量细碎的线段去“堆凑”像素,比预测一条完美的曲线更容易“骗过”像素对比。
* **解决**:
1. **Sparsity Loss (L0/L1)**:惩罚指令的数量,鼓励用最少的命令画图。
2. **Smoothness Loss**:惩罚曲率的二阶导数,或者惩罚控制点与基线的距离。
6. **思路**:
* **现象**:模型会学习到这些模式的**概率平均**。结果可能是画出的正方形既不顺时针也不逆时针,或者线条在中间断开,或者四个角都在抖动。这就是著名的“多模态平均问题”(The Mode Averaging Problem)。
* **对策**:使用 Mixture Density Network (MDN) 或 VAE/Diffusion,让模型学习分布而非单一均值。
7. **思路**:
* **方案**:将画布离散化为 $[0, 255]$ 或 $[0, 1023]$ 的整数网格。
* **Token表**:创建 `` 到 `` 的特殊 Token。
* **理由**:对于 256px 的图,1px 的精度通常足够肉眼观看。这能将每个坐标从“数个字符 Token”压缩为“1个整数 Token”。
</details>
---
## 5. 常见陷阱与错误 (Gotchas)
### 5.1 闭合路径的诅咒 (The Curse of Closed Paths)
* **现象**:生成的图形(如圆形、方形)在终点处经常“差一点点”没闭合,或者多出了一条难看的尾巴。
* **原因**:序列生成模型不知道“回到起点”是一个硬约束。
* **调试/解决**:
* **Post-processing**:如果检测到终点距离起点很近(如 < 5px),强制把最后一条指令改为 `Z` (ClosePath) 指令,并修正坐标。
* **Token 层面**:专门引入 `` Token,训练模型显式预测闭合动作。
### 5.2 坐标系混淆
* **现象**:SVG 画布通常是 y 轴向下为正。但某些数学库或数据集中,y 轴向上为正。
* **后果**:生成的图像不仅是上下颠倒的,而且由于贝塞尔控制点的手性(Chirality)翻转,曲线的弯曲方向可能会变得怪异(如“打结”)。
* **Checklist**:在数据预处理阶段,务必统一所有 SVG 的 ViewBox 和坐标系方向。
### 5.3 梯度消失与长序列
* **现象**:处理复杂 SVG(如地图、复杂插画)时,模型容易忽略后面的细节,或者在长序列生成中途崩盘。
* **原因**:SVG 的 Path Data 可能非常长(数千个 Token)。RNN 肯定忘,即便是 Transformer 也有长度限制。
* **技巧**:**层级化表示(Hierarchical Representation)**。
* 不要把整个 SVG 压成一串 Path 命令。
* 先生成 Group/Object 级别的 Token(如 ``, ``, ``)。
* 再分别对每个 Object 进行解码。这大大缩短了单一序列的长度。
### 5.4 0-长度线段与 NaN
* **现象**:训练过程中 Loss 突然变成 NaN。
* **原因**:模型预测出的 $P_0$ 和 $P_3$ 重合,或者控制点重合,导致后续计算曲率或法线时出现除以零(Divide by Zero)错误。
* **防御**:在计算 Loss 的代码中,务必加入 `epsilon`(如 `1e-6`)进行除法保护。在数据清洗时,移除所有长度为 0 的线段。