☰
人工势场法船舶避碰MATLAB仿真:从势场设计到调参实战
2026/10/15 22:26:28 网站建设 项目流程

1. 为什么船舶避碰要把"最短路径"放在一边:先看清人工势场法的位置

做船舶自动避碰仿真的人,大多先接触过A*、Dijkstra、RRT这类的路径搜索算法。它们的特点是先在环境地图上算出一条路径,再让船沿着走。刚开始我也是一样的思路,觉得避碰就是“找一条不撞的路”。但真正跑到水面场景就发现问题了:船舶不是扫地机器人,更不是无人机,它惯性大、转向迟、刹车基本等于倒车,而且所有其他船舶都在移动。一条预先算好的、带拐角的折线路径,往往还没走完,态势就已经变了。这也是我把目光转向人工势场法的直接原因——它不是一个“算路径”的算法,而是一个“实时算受力”的算法。把目标点等效成吸引力、把障碍物和其他船等效成排斥力,每时每刻根据当前态势重新计算合力方向,再去驱动船舶运动。整个过程没有离线规划,没有阶梯性重规划,天然适配船舶这种连续运动对象。

人工势场法(APF,Artificial Potential Field)最早用在移动机器人和机械臂避障领域,后来被引入船舶操纵避碰研究。它最大的特点是直观:你不需要理解复杂的优化理论,也不需要维护一棵搜索树。你只需要定义好“引力场”和“斥力场”,然后把两股力量叠加起来,合力的方向就是当前应该前进的方向。算法的实时性非常高,一个控制周期内可以计算成百上千次,这对船舶避碰这种需要持续响应的场景来说非常关键。

当然,直接拿机器人的一套用到船上肯定是不行的。船舶运动模型和陆地机器人的运动模型差异太大,而且海上的避碰还要考虑规则约束。比如两船对遇的时候,按照惯例需要各自向右转向,如果单纯的势场力算出来是往左转,哪怕局部最优也是不合适的。这就是本篇要记录的核心问题:如何把人工势场法从“概念上适用”变成“工程上能用”。我在这篇文章里会把完整的MATLAB实现思路、核心代码骨架、参数调整经验,以及几个容易被忽略的坑全部展开,供做相关仿真和控制算法验证的同行参考。

1.1 搜索算法在船舶场景里的三个不自在

先说搜索算法的别扭之处。第一,船舶运动受操纵性限制,路径上每个拐点都需要转换为一系列转向指令。A*算出来的路径往往是折线,折线顶点处要求瞬间改向,但船舶航向变化是有时间常数和最大转向速度约束的,这就需要对路径做额外平滑。平滑本身不难,难的是平滑后的路径可能重新进入障碍物安全范围,又得重新调整。

第二,动态障碍物的处理很麻烦。搜索类算法处理静态地图比较顺手,但海上的其他船舶是运动的,每时每刻都在改变位置。你规划的路径在5分钟后可能已经被另一艘船“占住”了,需要反复重规划。每次重规划都伴随计算开销和转向动作,会让整个系统显得很“急躁”。

第三,搜索算法对地图栅格化敏感。栅格大小直接影响计算量和路径质量。海上环境障碍物稀疏但面积大,栅格取太大,路径会贴着危险区域走;栅格取太小,搜索时间飙升。这种“成本-分辨率”矛盾在人工势场法里几乎不存在,因为势场是在连续空间里计算的,不需要离散化地图。

1.2 连续空间里“受力前进”的直觉建模

人工势场法的核心概念其实很生活化。你想象一根橡皮筋拴在你和目标点之间,它拉着你往目标方向走。你再想象每个障碍物都有点“脾气”,靠近它就会有一种无形的推力把你推开。橡皮筋的拉力加上各个障碍物的推力,合成一个最终的合力方向,船就往那个方向走。

在数学上,引力场我用经典的二次势场:

U_att = 0.5 * k_att * d_goal^2

其中d_goal是当前位置到目标点的距离,k_att引力系数。对势场求梯度取负号就得到引力向量,方向指向目标点,大小与距离成正比,也就是离目标越远,引力越强。这个设计符合直觉:目标很遥远时,你需要一个明确的前进方向;接近目标时,引力渐弱,避免过冲。

斥力场常规写法是:

U_rep = 0.5 * k_rep * (1/d - 1/d0)^2 (当 d <= d0)

其中d是船到障碍物的距离,d0是斥力影响半径。这个势场的特点是:船越靠近障碍物,斥力增长速度不是线性而是急剧上升,几乎不可能“硬闯”进障碍物内部。工程实现上,我一般不直接用这个二次形式,而是用它的梯度形式,得到一个带方向的斥力向量,这样方便直接与引力向量相加。

1.3 这套逻辑在避碰算法里的真实定位

如果把避碰算法看成工具箱,APF不是用来替代防撞逻辑的,而是用来解决“局部实时导向”的。目前的工程实践中,很多船舶自动避碰系统采用“分层的架构”:上一层是态势识别和避碰决策,比如判断当前是对遇、交叉还是追越局面,给出大致避让意图;下一层是执行层,需要把意图转化为具体的转向和变速指令。APF特别适合放在执行层偏下的位置,作为连续变化的导引力。它能够把“远离目标点”和“避开障碍船”这两个相互拉扯的目标,融合成一个连续的控制信号,不会出现决策层面的跳变。

这个定位也决定了APF的优化目标不是“全局最优路径”,而是“当前时刻最合理的运动趋势”。它是一种贪婪式的、反应式的算法。正因为它不追求最优,所以它的计算代价低、响应速度快、实现门槛也低,作为避碰仿真的第一版实现,几乎是性价比最高的选择。

2. 船舶运动模型和势场的对接:先把动力学这块地基打牢

在把APF受力写进MATLAB之前,我踩过一个很大的坑:直接用一个点质量模型去算力的方向,然后告诉船“你往这个方向走”。结果看起来很“顺”,仿真图上避让轨迹非常完美,但一旦把模型换成带转动惯量和纵距特性的船舶模型,控制效果马上就变了,甚至出现大幅振荡。原因很简单:人工势场法输出的是一个期望方向,但船舶并不能像航模一样瞬间转头,它必须经过舵角、转艏、航向变化这一连串过程。所以,势场设计必须和船舶运动模型放在一起考虑。

2.1 船舶状态向量选取:二维位置加转艏率

我在仿真里采用的船舶运动状态包括五个量:横向位置x、纵向位置y、航向角ψ、转艏率r、速度u。这里x-y方向我直接映射到仿真平面,航向角从x轴正方向量起,逆时针为正。虽然真实导航中一般用经纬度和真北航向,但MATLAB仿真阶段用平面坐标就够了,算法逻辑才是核心。

运动学方程如下:

dx/dt = u * cos(ψ)
dy/dt = u * sin(ψ)
dψ/dt = r

其中u代表船舶对地速度的大小。之所以没有直接加一个“横向速度”,是因为大多数船舶在常规操纵下,横漂很小,简化为欠驱动模型已经足够说明问题。

动力学部分我采用一阶KT响应模型:

dr/dt = (K * δ - r) / T
du/dt = (u_cmd - u) / T_u

这里的δ是舵角指令,K是转向增益,T是转艏惯性时间常数。工程上,可以根据船长和船型大致估算,也可以用实船试验数据拟合。仿真中我设置的典型值是K=0.15,T=20,舵角限幅±35度,最大转艏率每秒3度。这些值不是拍脑袋定的,而是参考了常规民船操纵性试验的量级,具体项目需要根据实际船舶参数替换。

2.2 从合力向量到操舵指令:坐标转换的关键

APF输出的合力是一个二维向量,要变成操舵指令,不能直接用atan2求出一个期望航向然后硬追。因为在船速基本不变的情况下,持续追某个航向角,如果误差角很小,舵角指令会一直在正负之间抖。我的做法是先算出期望航向,再算期望航向与实际航向的差,然后用一个带死区的比例控制产生转向速率指令,最后转换为舵角。

伪逻辑大概是:

期望航向 psi_d = atan2(F_y, F_x)
航向误差 e_psi = psi_d - psi
转向速率需求 r_d = clamp(K_psi * e_psi, -max_turn_rate, max_turn_rate)
舵角 δ = clamp(K_r * (r_d - r), -35, 35)

这样的链路把APF的“力”变成了真正可控的“舵”。在后面章节的MATLAB主循环里,我也会按这个链路写代码。

2.3 速度控制:让避让不止于转向

很多时候,单纯转向不足以避开高速接近的来船。特别是交叉相遇局面,如果两船速度都很快,只转向可能仍会逼近最小会遇距离。APF的天然优势是可以同时输出方向和速度,它的合力大小本身就能反映“危险程度”。

我的处理方式:期望速度v_cmd按合力大小做缩放,再加船本体的操纵性限制。具体来说,合力越小说明环境越宽松,船恢复定速巡航速度;合力越大说明周围障碍物多或距离近,船自动减速以争取反应时间。速度回路同样是一阶惯性环节,限制了加速度。这样仿真画面更接近真实船舶变速变距的行为,也为后续加入碰撞风险指标验证留了口子。

3. 势场设计细节:不是简单加两个力就完事

很多初学者上来就把引力势和斥力势一加,然后发现船不是绕不过去,就是在障碍物附近抖成筛子。原因在于势场函数的形式、参数大小、以及影响范围之间必须匹配。这里我把设计上的关键细节拆开讲。

3.1 引力项改进:远距离限制最大引力

教科书版本U_att = 0.5 * k_att * d^2的问题是:当目标点很远时,引力会大到把斥力完全压过,船会直直冲进障碍物区域。实际工程上,我建议给引力加一个饱和限幅:

F_att = k_att * d,如果F_att > F_att_max,则F_att = F_att_max

这相当于引力在远距离时是一个固定大小、指向目标点的向量,只有在接近目标时才线性衰减。加了这层之后,障碍物斥力才有机会在远距离就改变船的航向,而不是被引力按在地上摩擦。

3.2 斥力项改进:安全半径与影响半径分开

斥力设计有两个半径要区分:一个是障碍物本体的安全半径,另一个是斥力影响半径。很多实现直接把障碍物轮廓点当作质点,导致船必须离坐标点很远才开始感觉到斥力,这种延迟极易引发碰撞。安全半径应当包含船本身长度的一半和障碍物尺寸的一半,再加上一个操作裕度。我习惯设置安全距离为:

d_safe = R_obstacle + 2 * L_ship + margin

其中R_obstacle是障碍物等效半径,L_ship是本船船长,margin可以根据仿真精度设定。这里要把船长放进去,因为避碰系统感知到的表面危险是船体边缘,而计算用的位置点通常取在船舶重心,两者差着半个船长。

影响半径则决定了船在多远处开始“感觉”到障碍物。它太大,会让船在宽旷水域就无故绕路;太小,又会在临近时才剧烈反应。我通常取安全距离的5到8倍,同时根据相对速度动态调整。

3.3 相对速度修正:给势场装上“预判”

静态障碍物的斥力公式只和距离有关,意味着即使来船速度很快,斥力也只在距离足够近时才爆发。这在实际仿真里会导致紧急避让动作太晚。我的处理方式是在斥力表达式中加入接近率修正:如果船与障碍物之间的相对距离在快速减小,那么等效影响半径就自动放大。

具体实现上,我先计算相对速度向量v_rel,求它沿距离方向的分量d_dot。如果d_dot小于0,说明两者在相互接近,就把当前影响半径扩大:

d0_eff = d0 + k_velocity * max(0, -d_dot)

这个扩大的影响半径在下一个控制周期又恢复。它不改变斥力曲线的形式,只是把“感知范围”动态拉大,既保留了APF连续性的优点,又避免了纯距离式势场的“事后诸葛”。

这个技巧对动态船舶模型特别有效,因为它把时间维度隐式地放进了势场计算里。虽然不能保证全局最优,但在工程仿真中,它显著降低了对遇和高速交叉场景下的最小会遇距离。

4. MATLAB主循环与代码骨架:把整套逻辑跑起来

这一部分直接给代码骨架。为了保持文章节奏清楚,我拆成几个函数模块:势场计算函数、船舶模型更新函数、主仿真循环。代码用MATLAB语法,在R2020之后的版本可以直接跑通。模型中参数需要按实际需求调整。

4.1 数据结构与初始化

我习惯把所有仿真参数放进一个结构体,避免到处写魔法数字。初始化的核心字段包括:船舶初始位置、初始航向、目标点位置、障碍物列表、控制参数。障碍物列表是一个矩阵,每一行代表一个障碍物,前两列是位置,第三列是安全半径;动态障碍物还额外加一列速度向量。

% 仿真参数结构体 param.k_att = 1.0; param.F_att_max = 2.5; param.k_rep = 8.0; param.safe_margin = 30; % 米 param.infl_base = 120; % 基础影响半径 param.k_tc = 4.0; % 接近率修正系数 param.dt = 0.5; % 仿真步长,秒 param.sim_T = 300; % 仿真时长,秒 % 本船初始状态:x, y, psi, r, u X0 = [0; 0; pi/6; 0; 7.0]; % 目标点 goal = [500; 400]; % 静态障碍物: [x, y, safe_radius] obstacles_static = [150, 160, 25; 320, 220, 30]; % 相对船初始状态: [x, y, psi, r, u] target0 = [180, 90, -pi/4, 0, 6.0];

4.2 势场计算函数

核心函数接收当前位置、目标点、障碍物列表和参数,返回合力向量,以及期望航向和期望速度。为了让代码更清晰,我分成引力和斥力两个局部计算块。

function [F, psi_d, v_cmd] = potential_field(X, goal, obst, param) % 引力计算,带饱和限幅 d_vec = goal(1:2) - X(1:2); d_g = norm(d_vec); if d_g > 1e-6 F_att_dir = d_vec / d_g; else F_att_dir = [1; 0]; end F_att = min(param.k_att * d_g, param.F_att_max) * F_att_dir; % 斥力计算 F_rep = [0; 0]; for i = 1:size(obst, 1) o_vec = X(1:2) - obst(i, 1:2)'; dist = norm(o_vec); d_safe = obst(i, 3) + param.safe_margin; d_infl = param.infl_base; % 动态接近率修正 if size(obst, 2) >= 5 v_rel = obst(i, 4:5) - X(5:6); d_dot = (o_vec' * v_rel) / max(dist, 1e-4); if d_dot < 0 d_infl = d_infl + param.k_tc * min(-d_dot, 10); end end if dist - d_safe < d_infl && dist > d_safe o_dir = o_vec / dist; rep_mag = param.k_rep * (1/(dist-d_safe) - 1/d_infl) ... * (1/(dist-d_safe)^2); F_rep = F_rep + rep_mag * o_dir; elseif dist <= d_safe % 进入安全圈,强制大斥力 o_dir = o_vec / max(dist, 1e-4); F_rep = F_rep + 100 * o_dir; end end F = F_att + F_rep; % 期望航向和速度 psi_d = atan2(F(2), F(1)); Fmag = norm(F); v_cmd = min(param.vmax, param.vbase + param.k_v * (Fmag - param.F_nominal)); end

4.3 船舶模型更新

船舶模型用欧拉积分就可以,步长0.5秒足够。真正要注意的是舵角和转向速率的限幅,必须放在每个积分步里,否则仿真会在短时间内积累出不可能的操纵动作。

function X_new = ship_update(X, delta_cmd, u_cmd, param) x = X(1); y = X(2); psi = X(3); r = X(4); u = X(5); % 一阶KT模型 r_dot = (param.K * delta_cmd - r) / param.T_turn; u_dot = (u_cmd - u) / param.T_u; r_new = r + r_dot * param.dt; u_new = u + u_dot * param.dt; % 限幅 r_new = clamp(r_new, -param.r_max, param.r_max); u_new = clamp(u_new, param.vmin, param.vmax); psi_new = psi + r_new * param.dt; x_new = x + u_new * cos(psi_new) * param.dt; y_new = y + u_new * sin(psi_new) * param.dt; X_new = [x_new; y_new; psi_new; r_new; u_new]; end function y = clamp(x, lo, hi) y = min(max(x, lo), hi); end

4.4 控制链路与主循环

主循环里做的事情是:把当前状态喂给势场函数,得到期望航向,计算航向误差,生成转向速率需求,再换算成舵角指令,最后更新船舶状态。

X = X0; obst = [obstacles_static, zeros(size(obstacles_static,1), 2)]; for t = 0:param.dt:param.sim_T % 第一步:势场计算 [F, psi_d, v_cmd] = potential_field(X, goal, obst, param); % 第二步:转向控制 e_psi = wrapToPi(psi_d - X(3)); r_demand = clamp(param.k_psi * e_psi, -param.r_max, param.r_max); delta_cmd = clamp(param.k_r * (r_demand - X(4)), -35, 35); % 第三步:更新船舶状态 X = ship_update(X, delta_cmd, v_cmd, param); % 记录并绘图 hist_x(t_end) = X(1); hist_y(t_end) = X(2); end

这里wrapToPi是MATLAB自带的角度归一化函数,非常关键。如果不做角度归一化,当航向误差跨过-π和π边界时会产生巨大的转舵指令,船会突然原地打转。

4.5 可视化与结果验证

我习惯在仿真过程中实时绘制本船位置、目标点、障碍物范围、合力方向和航迹。绘制代码不复杂,但非常有效,因为APF参数调整几乎离不开可视化。每改一个参数后的力方向变化,用箭头画出来比盯数据表直观得多。

figure(1); plot(hist_x, hist_y, 'b-'); hold on; plot(goal(1), goal(2), 'g*', 'MarkerSize', 12); plot(obs_x, obs_y, 'r+', 'MarkerSize', 10); viscircles([obs_x, obs_y], obs_r); quiver(X(1), X(2), F(1), F(2), 'r', 'LineWidth', 1.5);

这样一路调下来,看航迹是否平滑、是否穿越障碍物安全圈、是否在目标点附近收敛,基本能判断参数方向对不对。

5. 调参踩坑实录:这五个问题比算法本身更花时间

从代码能跑到结果合理,中间隔了一整个“调参地狱”。我把自己实际遇到的情况按现象、原因、修改手段梳理出来,这部分对做类似仿真的人应该最有参考价值。

5.1 船在障碍物附近“抖成筛子”

初期仿真中,我经常看到船靠近障碍物时来回摆动航向,轨迹像一条波浪线。根本原因有两点。一是斥力系数k_rep调得过大,只要稍微越过一个临界位置,斥力大幅变化,合力方向随之剧烈摆动。二是控制链路里航向误差到转向速率的系数k_psi太大,导致系统接近刚性,任何一个小的角度误差都会放大成大舵角。

我的经验是:先调控制链路的系数,保证船追一个固定航向时稳定不振荡,再调势场参数。否则两个环节相互作用,问题很难定位。具体来说,k_psi我从1.0开始,以0.5的间隔往上加,直到出现振荡就退回来。

5.2 局部极小值问题:船在障碍物前“劝不动”

APF最著名的缺陷就是局部极小值。当目标引力恰好与障碍物斥力大小相等、方向相反时,合力为零,船会停在原地。在船舶场景里,这比在机器人场景更危险,因为船舶还有惯性,它可能会停在障碍物附近“思考人生”,而这时航迹实际上还在漂移。

我用了几种缓解手段。第一种是给期望航向叠加一个缓慢变化的小波动作业:

psi_d = psi_apf + A * sin(omega * t)

这个扰动幅度通常控制在5度以内,频率很低,作用只是破坏力的完全对称,不至于显著影响整体路径。第二种手段是设置一个“等待计时器”,如果合力的模连续N个周期小于某个阈值,就进入绕行策略,强制沿一个固定方向偏航一个预设角度,直到合力方向发生明显改变。

5.3 仿真轨迹看着合理,但是违反避碰规则

纯APF实现的典型问题是“只避让,不辨规则”。对遇局面下,两船理论上应该各自向右转向避让,但APF可能计算出向左转更近、或者因为障碍物布局导致船选择向左让。仿真结果显示避碰成功,实际操作根本不能这么干。

我的解决思路是在势场计算之前,加入一个规则强制偏置:根据来船相对方位和航向,判断当前是哪种会遇局面,如果是交叉和对遇局面,则把奖励方向强制设置为来船右侧。实现上不是直接否决APF结果,而是在计算合力后,加入一个“规则向量”,优先级最高:

F_rule = w_rule * dir_rule

dir_rule根据局面不同取单位向量。这样整体输出会带着规则约束的偏向,而不是纯粹由几何力决定。这个改造也是APF从“能用”走向“能实际部署”的关键一步。

5.4 多个动态障碍物同时进入影响范围时的相互拉扯

APF对每个障碍物独立计算斥力再累加,这意味着如果两艘来船从不同方向逼近,本船的合力方向可能被两边同时拉动,最终进入一个尴尬的中庸方向,既不朝目标前进,也没有远离任何一方。实际操作中,这种情形在密集水域很常见。

我采用的处理方法是“降权合并”:不是简单累加所有斥力,而是先按距离从近到远排序,取距离最近的两三个障碍物参与合力计算,其余暂时忽略。这在一定程度上牺牲了全局性,但换来的是意图明确的避让动作。比起让算法被远处的弱斥力干扰,我更愿意让它在关键障碍物上做出果断决策。

5.5 速度控制与转向控制耦合导致的航迹振荡

修改了期望速度v_cmd之后,船的速度变化会改变运动学方程中u的大小,进而影响下一步的航向积分。如果速度变化太快,航向响应还跟不上的话,航迹会出现一种“蛇形走位”。这是控制链路内部耦合的问题。我在速度环上加了一阶低通滤波,让速度变化率保持在每秒0.2米以内,同时在转向环中引入r_d的平滑处理,两边的响应速度都不宜太快。船毕竟是船,动作越猛,实际效果越差。

6. 三类典型场景验证:静态避障、对遇与交叉会遇

这一部分用仿真结果说明算法在不同场景下的表现。由于我这里是文字版,不放完整曲线图,但会列出我在记录中的实测指标和现象,方便对照复现。

6.1 静态障碍物避让

单静态障碍物场景中,本船从起点向目标点直航,障碍物位于路径偏离约30米的位置。加入APF后,船在距离影响半径约120米处开始感知斥力,航向逐渐偏转;最近通过距离我记录为约46米,而设置的安全距离是55米,说明偏航幅度略小。我调整k_rep从8增加到14后,最近通过距离提升到62米,航迹整体更“绕”,但代价是总航程增加约5%。这个取舍在工程上需要评估安全裕度和燃油经济性之间的关系。

6.2 对遇局面

对遇场景是本船和目标船航向相反、正对而行,初始相对距离约600米,横向错位很小。在没有规则修正的初版代码里,APF给出的避让方向偏向几何上的“近侧”,结果两船从同一侧通过,虽然安全但违反会遇习惯。加入规则偏置后,本船向右转向,两船最终对向错开通过。我记录的最近会遇距离从初版的约38米提升到规则修正后的约72米,碰撞风险明显下降。

6.3 交叉相遇局面

交叉场景我设置目标船航向与本船成约70度夹角,从右前方接近。这种情况下,规则要求本船作为直航船实际上要让路或者变速避让。APF在距离足够远时引力占优,船仍朝目标走;当目标船进入影响半径后,斥力和规则偏置叠加,本船主动减速并向右转向,等待目标船通过后恢复航向。这个过程在图表中会看到航迹短暂外凸,然后回到目标线。速度曲线则明显出现一段凹陷,符合实际操纵预期。

6.4 现有局限和后续改进方向

这段验证下来,我的整体判断是:APF适合作为船舶自动避碰的底层导引算法,尤其适合与规则约束、动态障碍预测结合使用。它不足以单独应对复杂的多船会遇和极端拥挤水域,但它提供了一个连续、稳定、可解释的控制基础。后续如果再往下做,我认为值得尝试的方向是把APF输出作为优化问题的初值,或者把规则约束直接建模成异构势场的一部分。就现阶段而言,这套MATLAB实现足以支撑你跑通“感知-决策-操纵”的完整链路,也为后续换用更复杂的模型留足了接口。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询