路径规划:人工势场法

发布于 2024-09-12
更新于 2025-02-26

人工势场法在轨迹规划中的应用:以三连杆机械臂为例

演示效果图演示效果图

1. 引言

轨迹规划是机器人运动控制中的核心问题之一,常见的方法包括人工势场法(Artificial Potential Field, APF)、动态规划、快速探索随机树(RRT)等。其中,人工势场法因其计算简单、直观易理解而广泛应用于机器人路径规划。

本文将详细介绍人工势场法的基本原理,并以三连杆机械臂为实例,使用 MATLAB 实现轨迹规划的完整代码。

2. 人工势场法原理

人工势场法的核心思想是将机器人视为一个受力物体,环境中的目标点施加吸引力,障碍物施加斥力,使得机器人在势场的引导下朝向目标,同时避开障碍物。

2.1 势场函数

人工势场由两部分组成: 1. 吸引势场:用于引导机器人朝向目标点; 2. 斥力势场:用于使机器人远离障碍物。

2.1.1 吸引势场

吸引势场通常采用二次函数形式:

$$ U_{att}(q) = \frac{1}{2} k_{att} \|q - q_{goal}\|^2 $$

吸引力计算如下:

$$ F_{att}(q) = -\nabla U_{att}(q) = -k_{att} (q - q_{goal}) $$

其中, $k_{att}$ 是吸引势场的增益系数,$q$ 为机器人当前位置,$q_{goal}$ 为目标位置。

2.1.2 斥力势场

斥力势场通常采用指数形式或双曲线形式,这里采用如下表达式:

$$ U_{rep}(q) = \begin{cases} \frac{1}{2} k_{rep} \left( \frac{1}{d(q, q_{obs})} - \frac{1}{d_0} \right)^2, & d(q, q_{obs}) \leq d_0 \\ 0, & d(q, q_{obs}) > d_0 \end{cases} $$

对应的斥力计算如下:

$$ F_{rep}(q) = \begin{cases} k_{rep} \left( \frac{1}{d(q, q_{obs})} - \frac{1}{d_0} \right) \frac{1}{d(q, q_{obs})^2} \frac{q - q_{obs}}{\|q - q_{obs}\|}, & d(q, q_{obs}) \leq d_0 \\ 0, & d(q, q_{obs}) > d_0 \end{cases} $$

其中,$d(q, q_{obs})$ 为机器人到障碍物的欧几里得距离,$d_0$ 为斥力作用的阈值,$k_{rep}$ 为斥力势场的增益系数。

2.2 运动方向

机器人在势场中的受力为吸引力与斥力之和:

$$ F(q) = F_{att}(q) + F_{rep}(q) $$

机器人沿着总力方向移动,从而实现避障和目标跟踪。

3. 三连杆机械臂轨迹规划示例

3.1 案例数学建模

三连杆机械臂具有三个旋转关节,关节角度变量: $\mathbf{q} = [\theta_1, \theta_2, \theta_3]^T$ ,定义机械臂的运动学方程如下:

$$ \begin{cases} x = L_1\cos\theta_1 + L_2\cos(\theta_1+\theta_2) + L_3\cos(\theta_1+\theta_2+\theta_3) \\ y = L_1\sin\theta_1 + L_2\sin(\theta_1+\theta_2) + L_3\sin(\theta_1+\theta_2+\theta_3) \end{cases} $$

其中,$L_1, L_2, L_3$ 分别为三个连杆的长度,案例中给定 $ L_1=1, \quad L_2=1, \quad L_3=0.5 $。 初始关节角度和目标关节角度分别为:

  • 初始关节角度: $ \mathbf{q} = [q_1, q_2, q_3]^T = [0, 0, 0]^T$
  • 目标关节角度: $ \mathbf{q}_{\text{goal}} = [q_{1,\text{goal}}, q_{2,\text{goal}}, q_{3,\text{goal}}]^T = \left[\frac{\pi}{4}, \frac{\pi}{6}, -\frac{\pi}{6}\right]^T$

计算目标末端执行器在笛卡尔空间中的位置。正向运动学公式如下:

$$ \begin{aligned} x_{\text{target}} &= L_1 \cos(q_{1,\text{goal}}) + L_2 \cos(q_{1,\text{goal}} + q_{2,\text{goal}}) + L_3 \cos(q_{1,\text{goal}} + q_{2,\text{goal}} + q_{3,\text{goal}}) \\ y_{\text{target}} &= L_1 \sin(q_{1,\text{goal}}) + L_2 \sin(q_{1,\text{goal}} + q_{2,\text{goal}}) + L_3 \sin(q_{1,\text{goal}} + q_{2,\text{goal}} + q_{3,\text{goal}}) \end{aligned} $$

代入具体数值:

$$ \begin{aligned} x_{\text{target}} &= 1 \cdot \cos\left(\frac{\pi}{4}\right) + 1 \cdot \cos\left(\frac{\pi}{4} + \frac{\pi}{6}\right) + 0.5 \cdot \cos\left(\frac{\pi}{4} + \frac{\pi}{6} - \frac{\pi}{6}\right) \\ &= \cos\left(\frac{\pi}{4}\right) + \cos\left(\frac{5\pi}{12}\right) + 0.5 \cdot \cos\left(\frac{\pi}{4}\right) \\ &= \frac{\sqrt{2}}{2} + \cos\left(\frac{5\pi}{12}\right) + 0.5 \cdot \frac{\sqrt{2}}{2} \\ &= \frac{\sqrt{2}}{2} + \cos\left(\frac{5\pi}{12}\right) + \frac{\sqrt{2}}{4} \\ &= \frac{3\sqrt{2}}{4} + \cos\left(\frac{5\pi}{12}\right) \end{aligned} $$
$$ \begin{aligned} y_{\text{target}} &= 1 \cdot \sin\left(\frac{\pi}{4}\right) + 1 \cdot \sin\left(\frac{\pi}{4} + \frac{\pi}{6}\right) + 0.5 \cdot \sin\left(\frac{\pi}{4} + \frac{\pi}{6} - \frac{\pi}{6}\right) \\ &= \sin\left(\frac{\pi}{4}\right) + \sin\left(\frac{5\pi}{12}\right) + 0.5 \cdot \sin\left(\frac{\pi}{4}\right) \\ &= \frac{\sqrt{2}}{2} + \sin\left(\frac{5\pi}{12}\right) + 0.5 \cdot \frac{\sqrt{2}}{2} \\ &= \frac{\sqrt{2}}{2} + \sin\left(\frac{5\pi}{12}\right) + \frac{\sqrt{2}}{4} \\ &= \frac{3\sqrt{2}}{4} + \sin\left(\frac{5\pi}{12}\right) \end{aligned} $$

因此,目标末端执行器的位置为:

$$ \mathbf{x}_{\text{target}} = \left[ x_{\text{target}}, y_{\text{target}} \right]^T $$

定义吸引力和斥力的参数:

  • 吸引力比例系数: $ k_{\text{att}} = 1$
  • 斥力比例系数: $ k_{\text{rep}} = 0.1$
  • 斥力有效范围: $ d_0 = 0.5$

障碍物在笛卡尔坐标系中的位置为:

$$ \mathbf{o} = [1.2, 1.2]^T $$

定义迭代参数:

  • 最大迭代次数: $ \text{max\_iters} = 200$
  • 步长因子: $ \alpha = 0.1$
  • 阻尼因子: $ \lambda = 0.01$

在每次迭代中,更新关节角度以使末端执行器接近目标并避开障碍物。使用正向运动学计算当前关节角度下的末端执行器位置:

$$ \begin{aligned} x_0 &= 0 \\ y_0 &= 0 \\ x_1 &= L_1 \cos(q_1) \\ y_1 &= L_1 \sin(q_1) \\ x_2 &= x_1 + L_2 \cos(q_1 + q_2) \\ y_2 &= y_1 + L_2 \sin(q_1 + q_2) \\ x_{\text{end}} &= x_2 + L_3 \cos(q_1 + q_2 + q_3) \\ y_{\text{end}} &= y_2 + L_3 \sin(q_1 + q_2 + q_3) \end{aligned} $$

因此,末端执行器的位置为:

$$ \mathbf{x}_{\text{end}} = \left[ x_{\text{end}}, y_{\text{end}} \right]^T $$

吸引力力由目标位置和当前位置之间的差值决定:

$$ \mathbf{F}_{\text{att}} = -k_{\text{att}} (\mathbf{x}_{\text{end}} - \mathbf{x}_{\text{target}}) $$

斥力(障碍物势场梯度)仅当末端执行器接近障碍物时生效。如果末端执行器与障碍物的距离小于 $d_0$,则计算斥力:

$$ d_{\text{obs}} = \|\mathbf{x}_{\text{end}} - \mathbf{o}\| $$

如果 $ d_{\text{obs}} < d_0$:

$$ \mathbf{F}_{\text{rep}} = k_{\text{rep}} \left( \frac{1}{d_{\text{obs}}} - \frac{1}{d_0} \right) \frac{1}{d_{\text{obs}}^3} (\mathbf{x}_{\text{end}} - \mathbf{o}) $$

否则:

$$ \mathbf{F}_{\text{rep}} = [0, 0]^T $$

总合力为吸引力和斥力之和:

$$ \mathbf{F} = \mathbf{F}_{\text{att}} + \mathbf{F}_{\text{rep}} $$

雅可比矩阵的计算

雅可比矩阵 J 表示末端执行器位置对关节角度的偏导数,即:

$$ J = \begin{bmatrix} \frac{\partial x}{\partial \theta_1} & \frac{\partial x}{\partial \theta_2} & \frac{\partial x}{\partial \theta_3} \\ \frac{\partial y}{\partial \theta_1} & \frac{\partial y}{\partial \theta_2} & \frac{\partial y}{\partial \theta_3} \end{bmatrix} $$

对于三连杆机械臂,末端执行器的位置坐标 (x, y) 可以表示为:

$$x = l_1\cos(\theta_1) + l_2\cos(\theta_1 + \theta_2) + l_3\cos(\theta_1 + \theta_2 + \theta_3)$$
$$y = l_1\sin(\theta_1) + l_2\sin(\theta_1 + \theta_2) + l_3\sin(\theta_1 + \theta_2 + \theta_3)$$

其中: - $l_1, l_2, l_3$ 是三个连杆的长度 - $\theta_1, \theta_2, \theta_3$ 是三个关节角度

对这些方程求偏导,可以得到雅可比矩阵的各个元素:

  1. 对于第一个关节 $\theta_1$:
$$\frac{\partial x}{\partial \theta_1} = -l_1\sin(\theta_1) - l_2\sin(\theta_1 + \theta_2) - l_3\sin(\theta_1 + \theta_2 + \theta_3)$$
$$\frac{\partial y}{\partial \theta_1} = l_1\cos(\theta_1) + l_2\cos(\theta_1 + \theta_2) + l_3\cos(\theta_1 + \theta_2 + \theta_3)$$
  1. 对于第二个关节 $\theta_2$:
$$\frac{\partial x}{\partial \theta_2} = -l_2\sin(\theta_1 + \theta_2) - l_3\sin(\theta_1 + \theta_2 + \theta_3)$$
$$\frac{\partial y}{\partial \theta_2} = l_2\cos(\theta_1 + \theta_2) + l_3\cos(\theta_1 + \theta_2 + \theta_3)$$
  1. 对于第三个关节 $\theta_3$:
$$\frac{\partial x}{\partial \theta_3} = -l_3\sin(\theta_1 + \theta_2 + \theta_3)$$
$$\frac{\partial y}{\partial \theta_3} = l_3\cos(\theta_1 + \theta_2 + \theta_3)$$

这样计算得到的雅可比矩阵可以用于:

  1. 将末端执行器的速度转换为关节角速度
  2. 将末端执行器的力转换为关节力矩
  3. 在路径规划中进行速度和位置控制

这里的雅可比矩阵 $J$ (2x3) 描述了末端执行器速度与关节速度之间的关系:

$$ J = \begin{bmatrix} -\sum_{i=1}^{3} L_i \sin(q_1 + \cdots + q_i) & -\sum_{i=2}^{3} L_i \sin(q_1 + \cdots + q_i) & -L_3 \sin(q_1 + q_2 + q_3) \\ \sum_{i=1}^{3} L_i \cos(q_1 + \cdots + q_i) & \sum_{i=2}^{3} L_i \cos(q_1 + \cdots + q_i) & L_3 \cos(q_1 + q_2 + q_3) \end{bmatrix} $$

具体展开为:

$$ J = \begin{bmatrix} -L_1 \sin(q_1) - L_2 \sin(q_1 + q_2) - L_3 \sin(q_1 + q_2 + q_3) & -L_2 \sin(q_1 + q_2) - L_3 \sin(q_1 + q_2 + q_3) & -L_3 \sin(q_1 + q_2 + q_3) \\ L_1 \cos(q_1) + L_2 \cos(q_1 + q_2) + L_3 \cos(q_1 + q_2 + q_3) & L_2 \cos(q_1 + q_2) + L_3 \cos(q_1 + q_2 + q_3) & L_3 \cos(q_1 + q_2 + q_3) \end{bmatrix} $$

为了稳定求解,使用奇异值分解(SVD)来计算雅可比矩阵的伪逆:

$$ [U, S, V] = \text{svd}(J J' + \lambda^2 I) $$

其中 $ I$ 是单位矩阵,$ \lambda$ 是阻尼因子。

伪逆矩阵 $ J^\dagger$ 可以表示为:

$$ J^\dagger = V \cdot \text{diag}\left(\frac{1}{s_i + \epsilon}\right) U' $$

其中 $ s_i$ 是 $ S$ 的对角元素,$ \epsilon$ 是一个小常数以避免除以零。

最终的关节角度变化 $ \Delta \mathbf{q}$ (阻尼伪逆控制律)为:

$$ \mathbf{J}^+=\mathbf{J}^T(\mathbf{J}\mathbf{J}^T + \lambda^2\mathbf{I})^{-1} \\ \Delta\mathbf{q} = \alpha \mathbf{J}^T(\mathbf{J}\mathbf{J}^T + \lambda^2\mathbf{I})^{-1}\mathbf{F}_{total} $$

参数说明: - $\alpha=0.1$:步长因子 - $\lambda=0.01$:阻尼因子(防止矩阵奇异)

更新关节角度:

$$ \mathbf{q} = \mathbf{q} + \Delta \mathbf{q} $$

3.2 MATLAB 代码实现

  1. 初始化:设置目标位形 $\mathbf{q}_{goal}$,计算目标位置 $\mathbf{p}_{goal}$
  2. 迭代过程:
plaintext
while ||Δp|| > ε and iter < max_iters do
      1. 计算当前末端位置 p_e
      2. 计算合力 F = F_att + F_rep
      3. 计算雅可比矩阵 J(q)
      4. 求阻尼伪逆 J⁺ = J^T(JJ^T + λ²I)^-1
      5. 更新关节角度:q ← q + αJ⁺F
      6. 可视化检测
   end while
参数 作用域 调节效果
k_att 吸引力场强度 增大加速收敛,但可能引发振荡
k_rep 斥力场强度 增大增强避障,但可能造成局部极小
λ 阻尼因子 增大提高稳定性,降低收敛速度
α 步长因子 影响收敛速度和超调量

下面的 MATLAB 代码实现了人工势场法在三连杆机械臂轨迹规划中的应用。

matlab
clc; clear; close all;

% 机械臂参数
L1 = 1; L2 = 1; L3 = 0.5;
q = [0; 0; 0];                % 初始关节角度
q_goal = [pi/4; pi/6; -pi/6]; % 目标关节角度

% 计算目标末端位置(笛卡尔空间)
target_x = L1*cos(q_goal(1)) + L2*cos(q_goal(1)+q_goal(2)) + L3*cos(q_goal(1)+q_goal(2)+q_goal(3));
target_y = L1*sin(q_goal(1)) + L2*sin(q_goal(1)+q_goal(2)) + L3*sin(q_goal(1)+q_goal(2)+q_goal(3));
target = [target_x; target_y];

% 势场参数
k_att = 1; 
k_rep = 0.1; 
d0 = 0.5;

% 障碍物位置(笛卡尔坐标)
obstacle = [1.2, 1.2];

% 迭代参数
max_iters = 200;
alpha = 0.05;     % 步长因子
lambda = 0.05;   % 阻尼因子(关键修改)

% GIF 文件设置
gif_filename = 'robot_arm_motion.gif';
delay_time = 0.1;  % 每帧间隔时间

figure;
for iter = 1:max_iters
    % 计算各关节位置
    x0 = 0; y0 = 0;
    x1 = L1*cos(q(1));
    y1 = L1*sin(q(1));
    x2 = x1 + L2*cos(q(1)+q(2));
    y2 = y1 + L2*sin(q(1)+q(2));
    x_end = x2 + L3*cos(q(1)+q(2)+q(3));
    y_end = y2 + L3*sin(q(1)+q(2)+q(3));
    end_effector = [x_end; y_end];

    % 计算吸引力(目标势场梯度)
    F_att = -k_att * (end_effector - target);

    % 计算斥力(障碍物势场梯度)
    d_obs = norm(end_effector - obstacle');
    if d_obs < d0
        F_rep = k_rep * (1/d_obs - 1/d0) * (1/d_obs^3) * (end_effector - obstacle');
    else
        F_rep = [0; 0];
    end

    % 总合力
    F = F_att + F_rep;

    % 计算雅可比矩阵 J (2x3)(基于当前关节角度 q)
    J = [ -L1*sin(q(1)) - L2*sin(q(1)+q_goal(2)) - L3*sin(q(1)+q_goal(2)+q_goal(3)), ...
       -L2*sin(q(1)+q_goal(2)) - L3*sin(q(1)+q_goal(2)+q_goal(3)), ...
       -L3*sin(q(1)+q_goal(2)+q_goal(3));
        L1*cos(q(1)) + L2*cos(q(1)+q_goal(2)) + L3*cos(q(1)+q_goal(2)+q_goal(3)), ...
        L2*cos(q(1)+q_goal(2)) + L3*cos(q(1)+q_goal(2)+q_goal(3)), ...
        L3*cos(q(1)+q_goal(2)+q_goal(3))];

    % 伪逆计算(带SVD稳定性处理)✅
    [U,S,V] = svd(J*J' + lambda^2*eye(2));
    inv_term = V * diag(1./(diag(S)+eps)) * U'; % 添加eps避免除以零
    dq = alpha * J' * inv_term * F;
    % 更新关节角度
    q = q + dq;


    % 可视化机械臂和环境
    clf;
    hold on;
    plot([x0, x1, x2, x_end], [y0, y1, y2, y_end], 'ro-', 'LineWidth',2);
    plot(obstacle(1), obstacle(2), 'ks', 'MarkerSize', 10, 'MarkerFaceColor', 'k');
    plot(target(1), target(2), 'g*', 'MarkerSize', 10);
    axis equal;
    axis([-1 3 -1 3]);
    grid on;
    title(sprintf('Iteration: %d', iter));
    drawnow;

    % 保存 GIF
    frame = getframe(gcf);
    img = frame2im(frame);
    [A, map] = rgb2ind(img, 256);
    if iter == 1
        imwrite(A, map, gif_filename, 'gif', 'LoopCount', Inf, 'DelayTime', delay_time);
    else
        imwrite(A, map, gif_filename, 'gif', 'WriteMode', 'append', 'DelayTime', delay_time);
    end

    pause(0.1);

    % 判断是否已接近目标
    if norm(end_effector - target) < 0.01
        break;
    end
end

4.补充:python 实现

python的实现效果似乎更好,更清晰:

python
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from matplotlib.animation import PillowWriter
import matplotlib.patches as patches
import time

class ThreeLinkArm:
    def __init__(self, link_lengths=[1.2, 1.0, 0.8]):
        """初始化三连杆机械臂
        Args:
            link_lengths: 三个连杆的长度列表,默认为[1.2, 1.0, 0.8]
        """
        self.link_lengths = link_lengths
        self.num_links = len(link_lengths)  # 连杆数量
        self.joints = np.zeros((self.num_links + 1, 2))  # 存储所有关节的坐标,包括基座和末端
        self.angles = np.zeros(self.num_links)  # 存储各关节角度
        self.update_joints()

    def update_joints(self):
        """根据关节角度更新各关节位置"""
        self.joints[0] = [0, 0]  # 基座位置
        cumulative_angle = 0
        for i in range(self.num_links):
            cumulative_angle += self.angles[i]
            x = self.joints[i][0] + self.link_lengths[i] * np.cos(cumulative_angle)
            y = self.joints[i][1] + self.link_lengths[i] * np.sin(cumulative_angle)
            self.joints[i+1] = [x, y]

    def get_end_effector(self):
        """获取末端执行器位置"""
        return self.joints[-1]

    def set_angles(self, angles):
        """设置关节角度"""
        self.angles = np.array(angles)
        self.update_joints()

    def forward_kinematics(self, angles):
        """前向运动学:给定角度计算末端位置"""
        x = 0
        y = 0
        cumulative_angle = 0
        for i in range(self.num_links):
            cumulative_angle += angles[i]
            x += self.link_lengths[i] * np.cos(cumulative_angle)
            y += self.link_lengths[i] * np.sin(cumulative_angle)
        return np.array([x, y])

    def jacobian(self):
        """计算雅可比矩阵"""
        J = np.zeros((2, self.num_links))
        for i in range(self.num_links):
            x_partial = 0
            y_partial = 0
            for j in range(i, self.num_links):
                angle_sum = np.sum(self.angles[:j+1])
                x_partial -= self.link_lengths[j] * np.sin(angle_sum)
                y_partial += self.link_lengths[j] * np.cos(angle_sum)
            J[0, i] = x_partial
            J[1, i] = y_partial
        return J

def artificial_potential_field(arm, target, obstacles, k_att=1.0, k_rep=10.0, d0=0.5):
    """计算基于人工势场的合力
    Args:
        arm: 机械臂对象
        target: 目标位置坐标 [x, y]
        obstacles: 障碍物列表,每个元素为 [x, y, radius]
        k_att: 引力系数
        k_rep: 斥力系数
        d0: 障碍物影响阈值距离
    Returns:
        total_force: 合力向量 [Fx, Fy]
    """
    current_pos = arm.get_end_effector()

    # 计算引力:与目标点的距离成正比
    att_vec = k_att * (np.array(target) - current_pos)

    # 计算斥力:当距离障碍物小于阈值d0时产生
    rep_vec = np.zeros(2)
    for obs in obstacles:
        obs_pos = np.array(obs[:2])
        obs_radius = obs[2]
        # 计算到障碍物边缘的距离向量
        dist_vec = current_pos - obs_pos
        # `np.linalg.norm(dist_vec)` 计算这个向量的模长(欧几里得距离)
        # `- obs_radius` 减去障碍物的半径
        dist = np.linalg.norm(dist_vec) - obs_radius # 到障碍物边缘的实际距离
        dist = max(dist, 0.01)  # 防止距离为0导致除法错误

        if dist < d0:  # 只有在影响范围内才计算斥力
            rep_magnitude = k_rep * (1/dist - 1/d0) * (1/dist**2)
            rep_vec += rep_magnitude * (dist_vec/dist)

    total_force = att_vec + rep_vec
    return total_force


def plan_path(arm, start_angles, target_position, obstacles, max_steps=2000, dt=0.01):
    """使用人工势场法规划路径
    Args:
        arm: 机械臂对象
        start_angles: 初始关节角度
        target_position: 目标位置 [x, y]
        obstacles: 障碍物列表
        max_steps: 最大迭代步数
        dt: 时间步长
    Returns:
        path: 关节角度路径列表
        positions: 末端执行器位置列表
    """
    arm.set_angles(start_angles)

    path = [arm.angles.copy()]
    positions = [arm.get_end_effector().copy()]

    for step in range(max_steps):
        current_pos = arm.get_end_effector()
        # 检查是否到达目标(末端位置与目标距离小于阈值)
        if np.linalg.norm(np.array(target_position) - current_pos) < 0.05:
            print(f"目标达成!在第 {step} 步")
            break

        # 计算人工势场力
        force = artificial_potential_field(arm, target_position, obstacles)
        # 通过雅可比矩阵将末端力转换为关节角度变化
        J = arm.jacobian()
        try:
            J_inv = np.linalg.pinv(J) # 计算雅可比矩阵的伪逆(Moore-Penrose伪逆),用于将末端力转换为关节力矩
            angle_changes = J_inv @ force # 将末端力转换为关节角度变化量(Δθ = α * J⁺ * F,α为缩放因子)
            angle_changes = np.clip(angle_changes, -0.1, 0.1) # 对关节角度变化量进行限幅,防止单步变化过大(数值稳定性)
            arm.angles += dt * angle_changes # 根据时间步长 dt 更新关节角度(θ_{k+1} = θ_k + dt * Δθ)
            arm.update_joints() # 更新机械臂关节的几何位置(前向运动学)

            path.append(arm.angles.copy()) # 记录当前关节角度和末端位置,用于路径可视化
            positions.append(arm.get_end_effector().copy())
        except np.linalg.LinAlgError:
            print("雅可比矩阵求逆出错,可能处于奇异点") # 处理雅可比矩阵奇异的情况(如机械臂处于奇异构型)
            break

    return path, positions

def visualize_arm_motion(arm, path, positions, obstacles, target, save_animation=False):
    """可视化机械臂运动过程
    Args:
        arm: 机械臂对象
        path: 关节角度路径列表
        positions: 末端执行器位置列表
        obstacles: 障碍物列表
        target: 目标位置
        save_animation: 是否保存动画为GIF文件
    """
    # 创建画布和坐标轴
    fig, ax = plt.subplots(figsize=(10, 8))

    # 设置坐标轴范围和网格
    ax.set_xlim(-1, 4)
    ax.set_ylim(-1, 2)
    ax.set_aspect('equal')  # 保持横纵比例相等
    ax.grid(True)

    # 绘制目标点(绿色圆形)
    target_point = plt.Circle(target, 0.1, color='green', alpha=0.7)
    ax.add_patch(target_point)
    ax.text(target[0]+0.1, target[1]+0.1, "目标", fontsize=12)

    # 绘制障碍物(红色圆形)
    for obs in obstacles:
        obstacle = plt.Circle(obs[:2], obs[2], color='red', alpha=0.5)
        ax.add_patch(obstacle)

    # 绘制末端执行器的运动轨迹
    positions_arr = np.array(positions)
    ax.plot(positions_arr[:, 0], positions_arr[:, 1], 'b--', alpha=0.3, label='末端轨迹')

    # 创建机械臂连杆和关节的绘图对象
    arm_links = [plt.Line2D([], [], color='black', lw=3) for _ in range(arm.num_links)]
    arm_joints = [plt.Circle((0, 0), 0.05, color='blue', zorder=3) for _ in range(arm.num_links + 1)]

    for link in arm_links:
        ax.add_line(link)
    for joint in arm_joints:
        ax.add_patch(joint)

    end_effector = plt.Circle((0, 0), 0.08, color='purple', zorder=3)
    ax.add_patch(end_effector)

    # 添加文本信息
    time_text = ax.text(0.02, 0.95, '', transform=ax.transAxes)
    angle_text = ax.text(0.02, 0.90, '', transform=ax.transAxes)

    ax.legend(loc='upper right')
    ax.set_title('三连杆机械臂人工势场法路径规划')

    def init():
        for link in arm_links:
            link.set_data([], [])
        return arm_links + arm_joints + [end_effector, time_text, angle_text]

    def animate(i):
        arm.set_angles(path[i])
        # 更新连杆数据
        for j in range(arm.num_links):
            x_data = [arm.joints[j][0], arm.joints[j+1][0]]
            y_data = [arm.joints[j][1], arm.joints[j+1][1]]
            arm_links[j].set_data(x_data, y_data)
        # 更新关节位置
        for j in range(arm.num_links + 1):
            arm_joints[j].center = arm.joints[j]
        end_effector.center = arm.joints[-1]
        time_text.set_text(f'步骤: {i}/{len(path)-1}')
        angle_text.set_text(f'角度: [{arm.angles[0]:.2f}, {arm.angles[1]:.2f}, {arm.angles[2]:.2f}]')
        return arm_links + arm_joints + [end_effector, time_text, angle_text]

    ani = FuncAnimation(fig, animate, frames=len(path), init_func=init, interval=20, blit=True) 


    if save_animation:
        ani.save('arm_motion.gif', writer=PillowWriter(fps=30))  # 使用 PillowWriter 保存为 GIF

    plt.tight_layout()
    plt.show()

    return ani

def main():
    """主函数:设置参数并运行仿真"""
    # 设置中文字体
    plt.rcParams['font.sans-serif'] = ['SimSun']

    # 创建机械臂实例
    arm = ThreeLinkArm(link_lengths=[1.2, 1.0, 0.8])

    # 设置初始角度(所有关节角度为0)
    start_angles = [0.0, 0.0, 0.0]

    # 设置目标位置坐标
    target = [2, 1.5]

    # 设置障碍物位置和大小:[x坐标, y坐标, 半径]
    obstacles = [
        [0.7, 0.3, 0.1],
        [0.8, 1.2, 0.2],
        [1.2, 0.8, 0.2],
        [2.5, 0.5, 0.1]
    ]

    print("正在使用人工势场法规划路径...")
    path, positions = plan_path(arm, start_angles, target, obstacles, max_steps=2000, dt=0.01)

    print("正在可视化机械臂运动...")
    visualize_arm_motion(arm, path, positions, obstacles, target, save_animation=True)

    print("完成!")

if __name__ == "__main__":
    main()

5.人工势场法的优缺点

优点:

  1. 计算简单:不需要复杂的路径搜索算法,仅通过计算势场梯度进行导航,适用于实时应用。
  2. 直观易理解:物理意义明确,吸引力和斥力的概念直观易懂,易于调试和优化。
  3. 高效性:计算复杂度较低,适用于低计算资源的嵌入式系统。
  4. 适用于动态环境:可以根据障碍物或目标点的变化即时调整轨迹,不需要重新规划整个路径。

缺点:

  1. 局部极小问题:在势场中可能会陷入局部最小值,导致机器人无法继续前进。
  2. 目标不可达问题:如果目标位于障碍物的凹陷区域,可能导致无法到达目标点。
  3. 震荡问题:当机器人接近障碍物或目标时,可能会产生来回摆动的现象,特别是在势场设计不合理时。
  4. 参数敏感性:吸引力和斥力的参数(增益系数、作用范围等)需要仔细调节,否则可能导致路径不稳定或不可行。

实践过程中的关键点
首先是避免局部极小问题。可以引入随机扰动(如模拟退火或随机势场扰动)来帮助机器人跳出局部最小值;采用虚拟势场路径启发式方法,比如在目标方向添加小的外力来引导机器人前进;还能结合其他路径规划算法(如 A* 、RRT)用于全局优化,再用人工势场法进行局部避障。
其次是改善震荡问题。可以采用梯度下降 + 惯性项,在运动过程中增加一个速度记忆项,使其运动更加平滑;设计更平滑的势场函数,避免在边界附近出现过大的梯度变化;采用变步长方法,当接近目标或障碍物时减小步长,提高稳定性。
再者是合理设置参数。吸引力系数不能过大,否则会导致运动不稳定,过小则可能影响收敛速度;斥力作用范围需要适中,过大会影响目标吸引,过小可能导致碰撞风险;斥力强度需要根据障碍物的形状和密度调整,避免远处障碍物影响过大。
最后是考虑机械臂的运动学约束。对于机器人机械臂,需结合雅可比矩阵进行运动变换,保证轨迹在可行工作空间内;机械臂的奇异位形(Singularity)可能导致雅可比矩阵不可逆,此时应采取正则化处理或调整运动策略。

6. 结论

本文介绍了人工势场法的基本原理,并通过 MATLAB 实现了其在三连杆机械臂轨迹规划中的应用。人工势场法在机器人导航和轨迹规划中是一种高效且直观的方法,但其局部最小值、震荡等问题需要结合其他优化方法进行改进。实际应用中,合理调节参数、结合全局路径规划方法、优化势场函数形态,可以有效提升算法的稳定性和实用性。