从贝塞尔曲线反推控制点

由之前的文章我们可以得到贝塞尔曲线的方程,今天要通过贝塞尔曲线(三次)重新推出控制点。

需求

在得到并对贝塞尔曲线做完处理后,为了让浏览器重新渲染贝塞尔曲线,必须通过贝塞尔曲线重新取得控制点坐标。

准备条件

了解 SVG 的 path 中 C/c 相关指令的用法,还有相对位置等一些概念。最好能提前获得贝塞尔曲线的表达式。

解决方案

假设我们已经得到了贝塞尔曲线的表达式: ¯P3=(1t)3¯P0+3t(1t)2¯P1+3t2(1t)¯P2+t3¯P3

其中, 是三次贝塞尔曲线上的点,¯P0¯P1¯P2¯P3 分别是贝塞尔曲线的控制点,因为 ¯P0¯P3 本身就是贝塞尔曲线的两个端点,所以它们的坐标是事先知道的,我们的目标是要求出 ¯P1¯P2。注意到,贝塞尔曲线的点是 t 由 0 逐渐增加到 1 的过程中采样得到的(数学上需要对 t 取极限,但计算机是离散的,所以称为采样),因为项目中,我是通过原控制点得到贝塞尔曲线后,对曲线做形变处理,然后再反推控制点,所以我只需要在原来的贝塞尔曲线表达式中分别取 t=1323 ( t 的取值可以是 0 到 1 之间任意实数,当然不能是 0 和 1,不然就和端点重合了),就可以得到两个 ¯P3 点的坐标,形变处理完后,我同样近似地认为这两个点是新曲线中 t 取 1323 的坐标点。这样,我们相当于知道了四个点的坐标,对于一个二元一次方程,我们由这四个点可以得到两组方程,最终一定可以把 ¯P1¯P2 解出来。对于原表达式不知道的情况,可以根据端点坐标来近似取点,具体可以看参考链接。

step1

对于 (1) 式,令 t=13,我们得到: ¯P3=(23)3¯P0+(23)2¯P1+3(13)223¯P2+(13)3¯P3

¯P3827¯P0127¯P3=49¯P1+29¯P2

因为 ¯P3¯P0¯P3 的坐标是事先已知的,所以可以设 ¯P3827¯P0127¯P3¯B。我们设 ¯P1¯P2 的坐标分别为 (x1, y1)、(x2, y2),¯B 的坐标为(xbyb),由 (2) 式可以得到如下方程: 49x1+29x2=xb

49y1+29y2=yb

step2

按照 step1 的思路,令 t=23,可以得到: ¯P3127¯P0827¯P3=29¯P1+49¯P2

¯P3127¯P0827¯P3¯C,设 ¯C 的坐标为 (xc, yc),同样的可以得到另一组方程: 29x1+49x2=xc

29y1+49y2=yc

step3

联立 step1 和 step2 的方程组,最终可以吧 ¯P1¯P2 的坐标求出来。 x1=3xb32xc

y1=3yb32yc

x2=3xc32xb

y2=3yc32yb

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
* 计算第二,第三个控制点的坐标
**/
void get_control_points(Point &p1, Point &thirdOne, Point &thirdTwo,
Point &p4, Point &p2, Point &p3) {
double xb1, yb1, xb2, yb2; // 计算的中间变量
double f1 = 0.037037037037037037037; // (1/3)^3
double f2 = 0.296296296296296296296; // (2/3)^3
double x2, y2, x3, y3; // 返回的p2、p3的坐标
xb1 = thirdOne.x - f2 * p1.x - f1 * p4.x;
yb1 = thirdOne.y - f2 * p1.y - f1 * p4.y;
xb2 = thirdTwo.x - f1 * p1.x - f2 * p4.x;
yb2 = thirdTwo.y - f1 * p1.y - f2 * p4.y;
x2 = 3 * xb1 - 3 / (double)2 * xb2;
y2 = 3 * yb1 - 3 / (double)2 * yb2;
x3 = 3 * xb2 - 3 / (double)2 * xb1;
y3 = 3 * yb2 - 3 / (double)2 * yb1;
p2.x = (int)x2;
p2.y = (int)y2;
p3.x = (int)x3;
p3.y = (int)y3;
}

/**
* points里面,除了贝塞尔曲线前后的端点外,还包括曲线上1/3和2/3位置的点。
* 返回贝塞尔曲线的控制点
*/
vector<Point> regain_new_points(vector<Point>& points) {
vector<Point> newPoints;

for (int i = 0; i < points.size()-1; i+=3) {
Point p2, p3;
get_control_points(points[i], points[i+1], points[i+2],
points[i+3], p2, p3);
if (i == 0) {
newPoints.push_back(points[i]);
}
newPoints.push_back(p2);
newPoints.push_back(p3);
newPoints.push_back(points[i+3]);
}
return newPoints;
}

测试结果

test
test

右图的脸为原 svg 在浏览器中的展示效果,左图的脸部轮廓白点为根据 svg 的控制点绘制出贝塞尔曲线后,再根据贝塞尔曲线反推的控制点坐标,而左图的脸部曲线则是根据这些控制点绘制出的贝塞尔曲线。从效果上看,两张人脸的轮廓基本很相似。

接下来,我对贝塞尔曲线做了平移、缩放和旋转,看看重新推出的控制点以及贝塞尔曲线的情况:

平移操作

translate
translate

缩放操作

scale
scale

旋转操作

rotate
rotate

看得出,这些基本的线性变换都不会导致贝塞尔曲线「失真」:-),这是个好消息。

参考

Algorithm for deriving control points of a bezier curve from points along that curve?