如何画 贝赛尔曲线 以及 样条曲线?


贝赛尔曲线 的定义,这里就不多做说明了。 样条曲线, 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的内置方法,画图

看起来,两种方法,非常吻合。