基于OSQP的二次规划

1. 问题建模

  假设已知一条参考轨迹为 r 0 , r 1 , . . . , r i , . . . , r n r_0,r_1,...,r_i,...,r_n r0,r1,...,ri,...,rn,这条轨迹可以任意生成,因为其中可能有直角拐点或者锐角拐点,机器人无法直接沿着该轨迹移动。因此,需要生成一条满足机器人运动学约束、尽可能平滑的实际可行的轨迹 x 0 , x 1 , . . . , x i , . . . x n x_0,x_1,...,x_i,...x_n x0,x1,...,xi,...xn,这也是我们需要优化的变量,这里轨迹 { r i } \{r_i\} { ri} { x i } \{x_i\} { xi}具有相同的长度。由此建立下面的代价函数( cost   function \textbf{cost function} cost function ):

J = w x ∑ i = 0 n − 1 ( x i − r i ) 2 + w x ′ ∑ i = 0 n − 1 ( x i ′ ) 2 + w x ′ ′ ∑ i = 0 n − 1 ( x i ′ ′ ) 2 + w x ′ ′ ′ ∑ i = 0 n − 1 ( x i ′ ′ ′ ) 2 \mathcal{J}=w_x\sum^{n-1}_{i=0}(x_i-r_i)^2+w_{x'}\sum^{n-1}_{i=0}(x'_i)^2+w_{x''}\sum^{n-1}_{i=0}(x''_i)^2+w_{x'''}\sum^{n-1}_{i=0}(x'''_i)^2 J=wxi=0n1(xiri)2+wxi=0n1(xi)2+wxi=0n1(xi)2+wxi=0n1(xi)2

  • w x ∑ i = 0 n − 1 ( x i − r i ) 2 w_x\sum^{n-1}_{i=0}(x_i-r_i)^2 wxi=0n1(xiri)2:最小化生成轨迹和参考轨迹之间的距离,也即让生成轨迹尽可能的沿着参考轨迹,其中 w x w_x wx为权重。
  • w x ′ ∑ i = 0 n − 1 ( x i ′ ) 2 w_{x'}\sum^{n-1}_{i=0}(x'_i)^2 wxi=0n1(xi)2:最小化轨迹的一阶导,其中 w x ′ w_x' wx为权重。通俗点讲, { x } \{x\} { x}为轨迹时, { x ′ } \{x'\} { x}为速度,这一项的目的是最小化速度。就像开车的时候,沿直线运动轨迹最短,能耗最小,乱打方向盘会让轨迹扭曲,加大能耗和移动时间。
  • w x ∑ i = 0 n − 1 ( x i − r i ) 2 w_x\sum^{n-1}_{i=0}(x_i-r_i)^2 wxi=0n1(xiri)2:最小化轨迹的二阶导,其中 w x ′ ′ w_x'' wx为权重。通俗点讲,就是最小化加速度。
  • w x ∑ i = 0 n − 1 ( x i − r i ) 2 w_x\sum^{n-1}_{i=0}(x_i-r_i)^2 wxi=0n1(xiri)2:最小化轨迹的三阶导,其中 w x ′ ′ ′ w_x''' wx为权重。通俗点讲,就是最小化加加速度(专业术语应该叫$\textbf{Jerk})。

2. QP问题

  常见格式如下:
m i n 1 2 x T P x + q x l ≤ A x ≤ u \begin{aligned} min \quad \frac{1}{2} x^T\mathcal{P}x+\mathcal{q}x \\ l\leq Ax \leq u \end{aligned} min21xTPx+qxlAxu

  其中 P {P} P是一个 n x n nxn nxn矩阵, x x x n n n维向量, q q q m x n mxn mxn的矩阵。

  为什么要转换成QP格式?

  因为QP问题一定是可求解的,而且目前各种求解器都支持求解QP问题,速度相对于非线性优化问题快了很多很多很多。

3. 格式转换

  下面将我们的问题转换成QP格式,主要就是构建 P \mathcal{P} P矩阵和 q \mathcal{q} q矩阵,下面的内容主要参考这篇文章规划控制:Piecewise Jerk Path Optimizer的python实现。这里将参考文章种的一维拓展到二维,即 x = { x 0 , x 1 , . . . , x n } x=\{x_0,x_1, ..., x_n\} x={ x0,x1,...,xn},其中每个状态 x i = { p x i , p y i , v x i , v y i , a x i , a y i } x_i=\{p_{xi},p_{yi},v_{xi},v_{yi},a_{xi},a_{yi}\} xi={ pxi,pyi,vxi,vyi,axi,ayi},分别对应在 x y xy xy轴上的位置 p x p_x px p y p_y py、速度 v x v_x vx v y v_y vy和加速度 a x a_x ax a y a_y ay

  为了简化QP问题的规模,这里假设优化变量 x x x包含 n n n个状态(分别对应参考轨迹 r r r的长度),且满足如下关系

  • x i ′ = x i + 1 − x i Δ t x'_i=\frac{x_{i+1}-x_i}{\Delta{t}} xi=Δtxi+1xi
  • x i ′ ′ = x i + 1 ′ − x i ′ Δ t x''_i=\frac{x'_{i+1}-x'_i}{\Delta{t}} xi=Δtxi+1xi
  • x i ′ ′ ′ = x i + 1 ′ ′ − x i ′ ′ Δ t x'''_i=\frac{x''_{i+1}-x''_i}{\Delta{t}} xi=Δtxi+1xi

  将上面最后一个公式带入代价函数 J \mathcal{J} J中,并将其展开可得:
J = w x ∑ i = 0 n − 1 [ ( x i ) 2 + ( r i ) 2 − 2 x i r i ] + w x ′ ∑ i = 0 n − 1 ( x i ′ ) 2 + w x ′ ′ ∑ i = 0 n − 1 ( x i ′ ′ ) 2 + w x ′ ′ ′ Δ s 2 ∑ i = 0 n − 2 [ ( x i + 1 ′ ′ ) 2 + ( x i ′ ′ ) 2 − 2 x i + 1 ′ ′ x i ′ ′ ] \mathcal{J}=w_x\sum^{n-1}_{i=0}[(x_i)^2+(r_i)^2-2x_ir_i]+w_{x'}\sum^{n-1}_{i=0}(x'_i)^2+w_{x''}\sum^{n-1}_{i=0}(x''_i)^2+\frac{w_{x'''}}{\Delta s^2}\sum^{n-2}_{i=0}[(x''_{i+1})^2+(x''_i)^2-2x''_{i+1}x''_i] J=wxi=0n1[(xi)2+(ri)22xiri]+wxi=0n1(xi)2+wxi=0n1(xi)2+Δs2wxi=0n2[(xi+1)2+(xi)22xi+1xi]

去掉其中的常数项(和优化变量 x x x无关的项)可得:

J = w x ∑ i = 0 n − 1 [ ( x i ) 2 − 2 x i r i ] + w x ′ ∑ i = 0 n − 1 ( x i ′ ) 2 + ( w x ′ ′ + 2 w x ′ ′ ′ Δ s 2 ) ∑ i = 0 n − 1 ( x i ′ ′ ) 2 − w x ′ ′ ′ Δ s 2 ( x 0 ′ ′ + x n − 1 ′ ′ ) − 2 w x ′ ′ ′ Δ s 2 ∑ i = 0 n − 2 ( x i + 1 ′ ′ x i ′ ′ ) \mathcal{J}=w_x\sum^{n-1}_{i=0}[(x_i)^2-2x_ir_i]+w_{x'}\sum^{n-1}_{i=0}(x'_i)^2+(w_{x''}+\frac{2w_{x'''}}{\Delta s^2})\sum^{n-1}_{i=0}(x''_i)^2-\frac{w_{x'''}}{\Delta s^2}(x''_0+x''_{n-1})-\frac{2w_{x'''}}{\Delta s^2}\sum^{n-2}_{i=0}(x''_{i+1}x''_i) J=wxi=0n1[(xi)22xiri]+wxi=0n1(xi)2+(wx+Δs22wx)i=0n1(xi)2Δs2wx(x0+xn1)Δs22wxi=0n2(xi+1xi)

其中
x = [ x 0 x 1 ⋮ x n − 1 x 0 ′ x 1 ′ ⋮ x n − 1 ′ x 0 ′ ′ x 1 ′ ′ ⋮ x n − 1 ′ ′ ] = [ p x 0 x x 1 ⋮ p x n − 1 p y 0 p y 1 ⋮ p y n − 1 v x 0 v x 1 ⋮ v x n − 1 v y 0 v y 1 ⋮ v y n − 1 a x 0 a x 1 ⋮ a x n − 1 a y 0 a y 1 ⋮ a y n − 1 ] ∈ R 6 n × 1 x=\begin{bmatrix} x_0 \\[4pt] x_1\\[4pt]\vdots \\[4pt]x_{n-1}\\[4pt]x'_0\\[4pt]x'_1\\[4pt]\vdots\\[4pt]x'_{n-1}\\[4pt]x''_0\\[4pt]x''_1\\[4pt]\vdots\\[4pt]x''_{n-1} \end{bmatrix} = \begin{bmatrix} p_{x_0} \\[4pt] x_{x_1}\\[4pt]\vdots \\[4pt]p_{x_{n-1}}\\[4pt]p_{y_0}\\[4pt]p_{y_1}\\[4pt]\vdots\\[4pt]p_{y_n-1}\\[4pt]v_{x_0}\\[4pt]v_{x_1}\\[4pt]\vdots\\[4pt]v_{x_{n-1}}\\[4pt]v_{y_0}\\[4pt]v_{y_1}\\[4pt]\vdots\\[4pt]v_{y_{n-1}}\\[4pt]a_{x_0}\\[4pt]a_{x_1}\\[4pt]\vdots\\[4pt]a_{x_{n-1}}\\[4pt]a_{y_0}\\[4pt]a_{y_1}\\[4pt]\vdots\\[4pt]a_{y_{n-1}} \end{bmatrix} \in R^{6n \times 1} x=x0x1xn1x0x1xn1x0x1xn1=px0xx1pxn1py0py1pyn1vx0vx1vxn1vy0vy1vyn1ax0ax1axn1ay0ay1ayn1R6n×1

构建 P P P q q q矩阵

P p = [ w x 0 … 0 0 w x … 0 ⋮ ⋮ ⋱ ⋮ 0 0 … w x ] ∈ R n × n , P v = [ w x ′ 0 … 0 0 w x ′ … 0 ⋮ ⋮ ⋱ ⋮ 0 0 … w x ′ ] ∈ R n × n P_p=\begin{bmatrix} w_x &0& \ldots&0\\0&w_x&\ldots&0\\\vdots&\vdots&\ddots&\vdots\\0&0&\ldots&w_x \end{bmatrix}\in R^{n \times n} , P_{v}=\begin{bmatrix} w_{x'} &0& \ldots&0\\0&w_{x'}&\ldots&0\\\vdots&\vdots&\ddots&\vdots\\0&0&\ldots&w_{x'} \end{bmatrix}\in R^{n \times n} Pp=wx000wx000wxRn×nPv=wx000wx000wxRn×n

P a = [ w x ′ ′ + w x ′ ′ ′ Δ s 2 0 … 0 0 − 2 w x ′ ′ ′ Δ s 2 w x ′ ′ + 2 w x ′ ′ ′ Δ s 2 … 0 0 ⋮ ⋮ ⋱ ⋮ ⋮ 0 0 … w x ′ ′ + 2 w x ′ ′ ′ Δ s 2 0 0 0 … − 2 w x ′ ′ ′ Δ s 2 w x ′ ′ + w x ′ ′ ′ Δ s 2 ] ∈ R n × n P_{a}=\begin{bmatrix} w_{x''}+\frac{w_{x'''}}{\Delta s^2} &0& \ldots&0&0\\[8pt]-\frac{2w_{x'''}}{\Delta s^2}&w_{x''}+\frac{2w_{x'''}}{\Delta s^2}&\ldots&0&0\\[8pt]\vdots&\vdots&\ddots&\vdots&\vdots\\[8pt]0&0&\ldots&w_{x''}+\frac{2w_{x'''}}{\Delta s^2}&0\\[8pt]0&0&\ldots&-\frac{2w_{x'''}}{\Delta s^2}&w_{x''}+\frac{w_{x'''}}{\Delta s^2} \end{bmatrix}\in R^{n \times n} Pa=wx+Δs2wxΔs22wx000wx+Δs22wx0000wx+Δs22wxΔs22wx000wx+Δs2wxRn×n

P = [ P p P p P v P v P a P a ] ∈ R 6 n × 6 n P=\begin{bmatrix} P_p & & & \\ & P_p \\ && P_v \\ &&& P_v \\ &&&& P_a \\ &&&&& P_a \\ \end{bmatrix}\in R^{6n \times 6n} P=PpPpPvPvPaPaR6n×6n

q = − [ w x p x 0 w x x x 1 ⋮ w x p x n − 1 w x p y 0 w x p y 1 ⋮ w x p y n − 1 w x ′ v x 0 w x ′ v x 1 ⋮ w x ′ v x n − 1 w x ′ v y 0 w x ′ v y 1 ⋮ w x ′ v y n − 1 w x ′ ′ a x 0 w x ′ ′ a x 1 ⋮ w x ′ ′ a x n − 1 w x ′ ′ a y 0 w x ′ ′ a y 1 ⋮ w x ′ ′ a y n − 1 ] ∈ R 1 × 6 n q=- \begin{bmatrix} w_xp_{x_0} \\[4pt] w_xx_{x_1}\\[4pt]\vdots \\[4pt]w_xp_{x_{n-1}}\\[4pt]w_xp_{y_0}\\[4pt]w_xp_{y_1}\\[4pt]\vdots\\[4pt]w_xp_{y_n-1}\\[4pt]w_{x'}v_{x_0}\\[4pt]w_{x'}v_{x_1}\\[4pt]\vdots\\[4pt]w_{x'}v_{x_{n-1}}\\[4pt]w_{x'}v_{y_0}\\[4pt]w_{x'}v_{y_1}\\[4pt]\vdots\\[4pt]w_{x'}v_{y_{n-1}}\\[4pt]w_{x''}a_{x_0}\\[4pt]w_{x''}a_{x_1}\\[4pt]\vdots\\[4pt]w_{x''}a_{x_{n-1}}\\[4pt]w_{x''}a_{y_0}\\[4pt]w_{x''}a_{y_1}\\[4pt]\vdots\\[4pt]w_{x''}a_{y_{n-1}} \end{bmatrix} \in R^{1 \times 6n} q=wxpx0wxxx1wxpxn1wxpy0wxpy1wxpyn1wxvx0wxvx1wxvxn1wxvy0wxvy1wxvyn1wxax0wxax1wxaxn1wxay0wxay1wxayn1R1×6n

等式约束

  为了让生成的轨迹满足机器人的移动状态,需要将机器人的运动模型转换成对应的等式约束,即 l = A x = u l =Ax = u l=Ax=u。这里考虑如下运动模型:
x i + 1 = x i + x i ′ Δ t x i + 1 ′ = x i ′ + x i ′ ′ Δ t x i + 1 ′ ′ = x i ′ ′ + x i ′ ′ ′ Δ t \begin{aligned} x_{i+1}=x_i + x'_i\Delta t \\ x'_{i+1}=x'_i + x''_i\Delta t \\ x''_{i+1}=x''_i + x'''_i\Delta t \\ \end{aligned} xi+1=xi+xiΔtxi+1=xi+xiΔtxi+1=xi+xiΔt

转换成对应矩阵形式,此处以2维空间为例,即 x = { x 0 , x 1 , . . . , x n } x=\{x_0,x_1, ..., x_n\} x={ x0,x1,...,xn},其中每个状态 x i = { p x i , p y i , v x i , v y i , a x i , a y i } x_i=\{p_{xi},p_{yi},v_{xi},v_{yi},a_{xi},a_{yi}\} xi={ pxi,pyi,vxi,vyi,axi,ayi},分别对应在 x y xy xy轴上的位置 p x p_x px p y p_y py、速度 v x v_x vx v y v_y vy和加速度 a x a_x ax a y a_y ay

A I = [ 1 − 1 0 … 0 0 1 − 1 … 0 ⋮ ⋮ ⋱ ⋱ ⋮ 0 0 … 1 − 1 ] ∈ R ( n − 1 ) × n A_{I}=\begin{bmatrix} 1 &-1& 0&\ldots&0 \\0& 1&-1&\ldots&0 \\\vdots&\vdots&\ddots&\ddots&\vdots \\0&0&\ldots&1&-1 \end{bmatrix}\in R^{(n-1) \times n} AI=100110011001R(n1)×n

A t = [ Δ t 0 0 … 0 0 Δ t 0 … 0 ⋮ ⋮ ⋱ ⋱ ⋮ 0 0 … Δ t 0 ] ∈ R ( n − 1 ) × n A_{t}=\begin{bmatrix} \Delta{t} &0& 0&\ldots&0 \\0&\Delta{t} &0&\ldots&0 \\\vdots&\vdots&\ddots&\ddots&\vdots \\0&0&\ldots&\Delta{t} &0 \end{bmatrix}\in R^{(n-1) \times n} At=Δt000Δt000Δt000R(n1)×n

A z = [ 0 0 0 … 0 0 0 0 … 0 ⋮ ⋮ ⋱ ⋱ ⋮ 0 0 … 0 0 ] ∈ R ( n − 1 ) × n A_{z}=\begin{bmatrix}0 &0& 0&\ldots&0 \\0&0 &0&\ldots&0 \\\vdots&\vdots&\ddots&\ddots&\vdots \\0&0&\ldots&0 &0 \end{bmatrix}\in R^{(n-1) \times n} Az=000000000000R(n1)×n

A e q = [ A I A z A t A z A z A z A z A I A z A t A z A z ] ∈ R ( 2 n − 2 ) × 6 n A_{eq}=\begin{bmatrix} A_I &A_z & A_t &A_z &A_z &A_z\\ A_z&A_{I} &A_z & A_{t} &A_z &A_z \end{bmatrix}\in R^{(2n-2) \times 6n} Aeq=[AIAzAzAIAtAzAzAtAzAzAzAz]R(2n2)×6n

l e q = u e q = [ 0 0 ⋮ 0 ] ∈ R 6 n × 1 l_{eq} = u_{eq}=\begin{bmatrix} 0 \\ 0 \\ \vdots \\ 0 \end{bmatrix}\in R^{6n \times 1} leq=ueq=000R6n×1

4. 使用OSQP求解问题

  效果如下,红色点是设置的路径点,灰色是b样条拟合的路径,绿色是优化的结果。

  直接上代码

import osqp
import numpy as np
import time
from scipy.interpolate import *
from scipy import sparse
import matplotlib.pyplot as plt


class PathOptimizer:
    def __init__(self, points, ts, acc, d='2d'):
        self.ctrl_points = points
        self.ctrl_ts = ts
        self.acc_max = acc
        self.dimension = d

        # 时间分配
        self.seg_length = self.acc_max * self.ctrl_ts * self.ctrl_ts
        self.total_length = 0
        for i in range(self.ctrl_points.shape[0] - 1):
            self.total_length += np.linalg.norm(self.ctrl_points[i + 1, :] - self.ctrl_points[i, :])
        self.total_time = self.total_length/self.seg_length * self.ctrl_ts * 1.2

        # 生成控制点B样条函数
        time_list = np.linspace(0, self.total_time, self.ctrl_points.shape[0])
        self.px_spline = InterpolatedUnivariateSpline(time_list, self.ctrl_points[:, 0])
        self.py_spline = InterpolatedUnivariateSpline(time_list, self.ctrl_points[:, 1])

        # 对B样条优化参考轨迹进行均匀采样
        self.seg_time_list = np.arange(0, self.total_time, self.ctrl_ts)
        self.px = self.px_spline(self.seg_time_list)
        self.py = self.py_spline(self.seg_time_list)

        # 优化问题权重参数
        self.weight_pos = 5
        self.weight_vel = 1
        self.weight_acc = 1
        self.weight_jerk = 0.1

        start_time = time.perf_counter()
        self.smooth_path = self.osqpSmooth()
        print('OSQP in ' + self.dimension + ' Cost time: ' + str(time.perf_counter() - start_time))
        pass

    def osqpSmooth(self):
        px0, py0 = self.px, self.py
        vx0 = vy0 = vz0 = np.zeros(self.px.shape)
        ax0 = ay0 = az0 = np.zeros(self.px.shape)

        # x(0), x(1), ..., x(n - 1), x'(0), x'(1), ..., x'(n - 1), x"(0), x"(1), ..., x"(n - 1)
        x0_total = np.hstack((px0, py0, vx0, vy0, ax0, ay0))


        n = int(x0_total.shape[0] / 3)   # 每种级别状态有几个变量,即x共n个,x'共n个,x"也是n个
        Q_x = self.weight_pos * np.eye(n)
        Q_dx = self.weight_vel * np.eye(n)
        Q_zeros = np.zeros((n, n))
        w_ddl = self.weight_acc
        w_dddl = self.weight_jerk

        if n % 2 != 0:
            print('x0 input error.')
            return
        n_part = int(n/2)   # 每部分有n_part个数据,x_x(0), x_x(1), ..., x_x(n-1), x_y(0), x_y(1), ..., x_y(n-1), ...

        Q_ddx_part = (w_ddl + 2 * w_dddl / self.ctrl_ts / self.ctrl_ts) * np.eye(n_part) \
                     - 2 * w_dddl / self.ctrl_ts / self.ctrl_ts * np.eye(n_part, k=-1)
        Q_ddx_part[0][0] = w_ddl + w_dddl / self.ctrl_ts / self.ctrl_ts
        Q_ddx_part[n_part-1][n_part-1] = w_ddl + w_dddl / self.ctrl_ts / self.ctrl_ts

        np_zeros = np.zeros(Q_ddx_part.shape)
        Q_ddx_l = np.vstack((Q_ddx_part, np_zeros))
        Q_ddx_r = np.vstack((np_zeros, Q_ddx_part))
        Q_ddx = np.hstack((Q_ddx_l, Q_ddx_r))


        Q_total = sparse.csc_matrix(np.block([[Q_x, Q_zeros, Q_zeros],
                                              [Q_zeros, Q_dx, Q_zeros],
                                              [Q_zeros, Q_zeros, Q_ddx]
                                              ]))

        p_total = - x0_total
        p_total[:n] = self.weight_pos * p_total[:n]

        # 动力学模型,所以是等式约束
        # x(i+1) = x(i) + x'(i)△t
        # x'(i+1) = x'(i) + x"(i)△t
        AI_part = np.eye(n_part-1, n_part) - np.eye(n_part-1, n_part, k=1)  # 计算同阶变量之间插值 (n-1) x n维
        AT_part = self.ctrl_ts * np.eye(n_part-1, n_part)   # 时间矩阵 (n-1) x n维
        AZ_part = np.zeros([n_part-1, n_part])  # 全0矩阵 (n-1) x n维

        # 起点为第一个点
        A_init = np.zeros([4, x0_total.shape[0]])
        A_init[0, 0] = A_init[1, n_part] = 1
        A_init[2, n_part-1] = A_init[3, 2*n_part-1] = 1

        A_l_init = A_u_init = np.array([x0_total[0], x0_total[n_part],
                                        x0_total[n_part-1], x0_total[2*n_part-1]])

        A_eq = sparse.csc_matrix(np.block([
            [A_init],
            [AI_part, AZ_part, AT_part, AZ_part, AZ_part, AZ_part],
            [AZ_part, AI_part, AZ_part, AT_part, AZ_part, AZ_part],
            [AZ_part, AZ_part, AI_part, AZ_part, AT_part, AZ_part],
            [AZ_part, AZ_part, AZ_part, AI_part, AZ_part, AT_part]
        ]))
        A_leq = A_ueq = np.zeros(A_eq.shape[0])
        A_leq[:4] = A_ueq[:4] = A_l_init


        # Create an OSQP object
        prob = osqp.OSQP()
        prob.setup(Q_total, p_total, A_eq, A_leq, A_ueq, alpha=1.0)
        res = prob.solve()

        return res.x[:]


class PathGenerator:
    def __init__(self):
        self.ctrl_points = np.array([[0.5, 0.5],
                                     [0.5, 1],
                                     [1.5, 1.],
                                     [2., 2.],
                                     [2.5, 2.5]])

        self.ctrl_ts = 0.1  # 控制时间s
        self.acc_max = 2    # 米/秒

        self.ref_path = PathOptimizer(self.ctrl_points, self.ctrl_ts, self.acc_max)


if __name__ == '__main__':
    pg = PathGenerator()

    fig = plt.figure()

    ax = plt.axes()
    ax.scatter(pg.ctrl_points[:, 0], pg.ctrl_points[:, 1], color='red')
    ax.plot(pg.ref_path.px, pg.ref_path.py, color='gray')

    seg_num = pg.ref_path.px.shape[0]
    ax.plot(pg.ref_path.smooth_path[:seg_num], pg.ref_path.smooth_path[seg_num:2*seg_num], color='green')

    plt.show()

猜你喜欢

转载自blog.csdn.net/qq_16775293/article/details/121849740