贝赛尔曲线 的定义,这里就不多做说明了。 样条曲线, B-SPLINE , 每一段 都可以分解成 一个三阶贝赛尔曲线。
在QT 里, cubicto 可以生成三阶贝赛尔曲线。 所以,如果可以找到样条曲线的每一段上的起点p1和终点p4,再加上线上的两个点p2,p3,可以反求出4个控制点 p1,pa,pb,p4, 就可以用 QPainterPath 生成 path.
上代码
import random
from typing import Generator, List
from PySide6 import QtCore as qc
from PySide6 import QtWidgets as qw
from PySide6 import QtGui as qg
class spline:
class s_point:
def __init__(self, x: float = 0, y: float = 0, z: float = 0) -> None:
self.x = x
self.y = y
self.z = z
def __str__(self) -> str:
return f""
def __repr__(self) -> str:
return self.__str__()
def __sub__(self, o: "spline.s_point") -> "spline.s_point":
if isinstance(o, spline.s_point):
r = spline.s_point()
r.x = self.x - o.x
r.y = self.y - o.y
r.z = self.y - o.z
return r
else:
raise NotImplementedError
def __add__(self, o: "spline.s_point") -> "spline.s_point":
if isinstance(o, spline.s_point):
r = spline.s_point()
r.x = self.x + o.x
r.y = self.y + o.y
r.z = self.z + o.z
return r
else:
raise NotImplementedError
def __mul__(self, o: "spline.s_point") -> "spline.s_point":
if isinstance(o, (int, float)):
r = spline.s_point()
r.x = self.x * o
r.y = self.y * o
r.z = self.z * o
return r
else:
raise NotImplementedError
def __eq__(self, o: "spline.s_point") -> bool:
if isinstance(o, spline.s_point):
tor = 3e-7
if abs(self.x - o.x) + abs(self.y - o.y) + abs(self.z - o.z) <= tor:
return True
else:
return False
else:
raise NotImplementedError
def dis(self):
return self.x**2 + self.y**2 + self.z**2
def __init__(self, keyP: List[float], pts: "List[spline.s_point]") -> None:
self.key_pt: List[float] = keyP
self.pts: "List[spline.s_point]" = pts
self.len_pts = len(pts)
self.len_keys = len(keyP)
assert len(pts) == len(keyP) - 4, f"节点数({len(keyP)}) = 控制点数({len(pts)}) + 4"
def Xik(self, i, k, u) -> float:
"""
### X(i,k,u) = [u- u(i)] / [u(i+k)-u(i)]
### N(i,k,u) = X(i,k,u)*N(i,k-1,u) + [1 - X(i+1,k,u)]*N(i+1,k-1,u)
"""
i1 = i
i2 = i + k
u1 = self.key_pt[i1]
u2 = self.key_pt[i2]
if u2 == u1:
return 0
else:
return (u - u1) / (u2 - u1)
def Nik(self, i, k, u) -> float:
"""
### X(i,k,u) = [u - u(i)] / [u(i+k) - u(i)]
### N(i,k,u) = X(i,k,u) * N(i,k-1,u) + [1 - X(i+1,k,u)] * N(i+1,k-1,u)
"""
if k < 0:
raise ValueError(k, "k < 0 error")
elif k == 0:
u1 = self.key_pt[i]
u2 = self.key_pt[i + 1]
if u1 <= u < u2:
return 1
else:
return 0
else:
r1 = self.Nik(i, k - 1, u)
r2 = self.Nik(i + 1, k - 1, u)
x1 = self.Xik(i, k, u)
x2 = 1 - self.Xik(i + 1, k, u)
return x1 * r1 + x2 * r2
def cn(self, u) -> "spline.s_point":
"""
### C(k,u)=sum( [ N(i,k,u) * point(i) for i in range( len(point) ) ] )
k==3
"""
lst_n = [self.Nik(i, 3, u) for i in range(self.len_pts)]
if sum(lst_n) < 0.1:
lst_n[-1] = 1
ps: List[spline.s_point] = [self.pts[i] * lst_n[i] for i in range(self.len_pts)]
r = ps[0]
for i in ps[1:]:
r += i
return r
def toBsr(self) -> Generator["list[spline.s_point]", None, None]:
"""三阶贝赛尔点集"""
uu1 = self.key_pt[:-1]
uu2 = self.key_pt[1:]
for i in range(0, self.len_keys - 1):
u1 = uu1[i]
u2 = uu2[i]
r = [u1, 0.75 * u1 + 0.25 * u2, 0.25 * u1 + 0.75 * u2, u2]
p1, p2, p3, p4 = [self.cn(i) for i in r]
y1 = (
(p2 + p4 * 0.25**3 * -1 + p1 * 0.75**3 * -1)
* (1 / 3)
* (1 / 0.75)
* (1 / 0.25)
)
y2 = (
(p3 + p4 * 0.75**3 * -1 + p1 * 0.25**3 * -1)
* (1 / 3)
* (1 / 0.75)
* (1 / 0.25)
)
pa = y1 * 1.5 + y2 * -0.5
pb = y2 * 1.5 + y1 * -0.5
yield [p1, pa, pb, p4]
class test_mw(qw.QGraphicsView):
def __init__(self, parent=None):
super(test_mw, self).__init__(parent)
self.scene: mgs = mgs(self)
self.setScene(self.scene)
self.setStyleSheet("background:rgb(0, 0, 0)")
class mgs(qw.QGraphicsScene):
def __init__(self, parent=None):
super(mgs, self).__init__(parent)
self.sc = 10
self.penSPLINE_1 = qg.QPen()
self.penSPLINE_1.setWidth(1)
self.penSPLINE_1.setColor(qg.QColor(200, 0, 30))
self.penSPLINE_2 = qg.QPen()
self.penSPLINE_2.setWidth(1)
self.penSPLINE_2.setColor(qg.QColor(20, 240, 70))
class test_qd(qw.QDialog):
def __init__(self) -> None:
super().__init__()
ly = qw.QVBoxLayout()
self.setLayout(ly)
self.bt1 = qw.QPushButton()
ly.addWidget(self.bt1)
self.gv = test_mw()
ly.addWidget(self.gv)
self.bt1.clicked.connect(self.on_b1_clicked)
self.resize(1550, 1000)
self.tm = qc.QTimer()
self.tm.setInterval(2000)
self.tm.timeout.connect(self.on_b1_clicked)
self.tm.start()
def on_b1_clicked(self):
self.gv.scene.clear()
self.gv.update()
self.tm.setInterval(2000)
ss = self.get_sp()
ppt = None
p0 = ss.cn(0)
ppt = self.pointPth(ppt, p0.x, p0.y, r=5)
for i in range(1000 + 1):
t = i / 1000
p = ss.cn(t)
if (p.x - p0.x) ** 2 + (p.y - p0.y) ** 2 > 35:
ppt = self.pointPth(ppt, p.x, p.y, r=5)
p0 = p
p0 = ss.cn(1)
ppt = self.pointPth(ppt, p0.x, p0.y, r=5)
self.gv.scene.addPath(ppt, self.gv.scene.penSPLINE_1)
self.gv.scene.update()
ppt = None
pts = ss.toBsr()
for p1, p2, p3, p4 in pts:
ppt = self.bsrPth(ppt, p1, p2, p3, p4)
self.gv.scene.addPath(ppt, self.gv.scene.penSPLINE_2)
self.gv.scene.update()
def bsr(self, p1: spline.s_point, p2: spline.s_point, t: float) -> spline.s_point:
ret = spline.s_point()
ret.x = p1.x * (1 - t) + p2.x * t
ret.y = p1.y * (1 - t) + p2.y * t
ret.z = p1.z * (1 - t) + p2.z * t
return ret
def bsr2(self, pp: List[spline.s_point], t: float) -> spline.s_point:
ll = len(pp)
if ll == 1:
return pp[0]
elif ll == 2:
return self.bsr(pp[0], pp[1], t)
elif ll > 2:
return self.bsr(self.bsr2(pp[:-1], t), self.bsr2(pp[1:], t), t)
else:
raise ValueError("pp 长度不够")
def pointPth(
self, p: qg.QPainterPath, x: float, y: float, scale: float = 1, r: float = 1
):
if p is None:
pth = qg.QPainterPath()
else:
pth = p
pth.moveTo(x * scale, y * scale)
pth.addEllipse(qc.QPoint(x * scale, y * scale), r, r)
return pth
def bsrPth(
self, p: qg.QPainterPath, p1, p2, p3, p4, scale: float = 1, r: float = 1
):
if p is None:
pth = qg.QPainterPath()
else:
pth = p
pth.moveTo(p1.x * scale, p1.y * scale)
pth.cubicTo(
p2.x * scale,
p2.y * scale,
p3.x * scale,
p3.y * scale,
p4.x * scale,
p4.y * scale,
)
return pth
def get_sp(self):
ns = random.randint(10, 20)
utmp = []
while len(utmp) < ns:
tmpu = random.randint(1, 99) / 100
if tmpu in utmp:
continue
utmp.append(tmpu)
utmp.sort()
utmp = [0, 0, 0, 0] + utmp + [1, 1, 1, 1]
tn1 = 100
tn2 = 900
tx = [random.randint(tn1, tn2) for i in range(len(utmp) - 4)]
ty = [random.randint(tn1, tn2) for i in range(len(utmp) - 4)]
tx.sort()
ty.sort()
spts = [spline.s_point(tx[i], ty[i]) for i in range(len(tx))]
# 样条曲线
sp = spline(utmp, spts)
return sp
if __name__ == "__main__":
app = qw.QApplication()
qd = test_qd()
qd.on_b1_clicked()
qd.show()
app.exec()
exit()
结果图如下
红色的圆圈,是用样条曲线的公式,取点,按一点间隔,画图
绿色的中心线,是把样条曲线分段,每段转换成三阶贝赛尔曲线,调用QT的内置方法,画图
看起来,两种方法,非常吻合。