|
| 1 | +""" |
| 2 | +Clothoid Path Planner |
| 3 | +Author: Daniel Ingram (daniel-s-ingram) |
| 4 | + Atsushi Sakai (AtsushiSakai) |
| 5 | +Reference paper: Fast and accurate G1 fitting of clothoid curves |
| 6 | +https://www.researchgate.net/publication/237062806 |
| 7 | +""" |
| 8 | + |
| 9 | +from collections import namedtuple |
| 10 | +import matplotlib.pyplot as plt |
| 11 | +import numpy as np |
| 12 | +import scipy.integrate as integrate |
| 13 | +from scipy.optimize import fsolve |
| 14 | +from math import atan2, cos, hypot, pi, sin |
| 15 | +from matplotlib import animation |
| 16 | + |
| 17 | +Point = namedtuple("Point", ["x", "y"]) |
| 18 | + |
| 19 | +show_animation = True |
| 20 | + |
| 21 | + |
| 22 | +def generate_clothoid_paths(start_point, start_yaw_list, |
| 23 | + goal_point, goal_yaw_list, |
| 24 | + n_path_points): |
| 25 | + """ |
| 26 | + Generate clothoid path list. This function generate multiple clothoid paths |
| 27 | + from multiple orientations(yaw) at start points to multiple orientations |
| 28 | + (yaw) at goal point. |
| 29 | +
|
| 30 | + :param start_point: Start point of the path |
| 31 | + :param start_yaw_list: Orientation list at start point in radian |
| 32 | + :param goal_point: Goal point of the path |
| 33 | + :param goal_yaw_list: Orientation list at goal point in radian |
| 34 | + :param n_path_points: number of path points |
| 35 | + :return: clothoid path list |
| 36 | + """ |
| 37 | + clothoids = [] |
| 38 | + for start_yaw in start_yaw_list: |
| 39 | + for goal_yaw in goal_yaw_list: |
| 40 | + clothoid = generate_clothoid_path(start_point, start_yaw, |
| 41 | + goal_point, goal_yaw, |
| 42 | + n_path_points) |
| 43 | + clothoids.append(clothoid) |
| 44 | + return clothoids |
| 45 | + |
| 46 | + |
| 47 | +def generate_clothoid_path(start_point, start_yaw, |
| 48 | + goal_point, goal_yaw, n_path_points): |
| 49 | + """ |
| 50 | + Generate a clothoid path list. |
| 51 | +
|
| 52 | + :param start_point: Start point of the path |
| 53 | + :param start_yaw: Orientation at start point in radian |
| 54 | + :param goal_point: Goal point of the path |
| 55 | + :param goal_yaw: Orientation at goal point in radian |
| 56 | + :param n_path_points: number of path points |
| 57 | + :return: a clothoid path |
| 58 | + """ |
| 59 | + dx = goal_point.x - start_point.x |
| 60 | + dy = goal_point.y - start_point.y |
| 61 | + r = hypot(dx, dy) |
| 62 | + |
| 63 | + phi = atan2(dy, dx) |
| 64 | + phi1 = normalize_angle(start_yaw - phi) |
| 65 | + phi2 = normalize_angle(goal_yaw - phi) |
| 66 | + delta = phi2 - phi1 |
| 67 | + |
| 68 | + try: |
| 69 | + # Step1: Solve g function |
| 70 | + A = solve_g_for_root(phi1, phi2, delta) |
| 71 | + |
| 72 | + # Step2: Calculate path parameters |
| 73 | + L = compute_path_length(r, phi1, delta, A) |
| 74 | + curvature = compute_curvature(delta, A, L) |
| 75 | + curvature_rate = compute_curvature_rate(A, L) |
| 76 | + except Exception as e: |
| 77 | + print(f"Failed to generate clothoid points: {e}") |
| 78 | + return None |
| 79 | + |
| 80 | + # Step3: Construct a path with Fresnel integral |
| 81 | + points = [] |
| 82 | + for s in np.linspace(0, L, n_path_points): |
| 83 | + try: |
| 84 | + x = start_point.x + s * X(curvature_rate * s ** 2, curvature * s, |
| 85 | + start_yaw) |
| 86 | + y = start_point.y + s * Y(curvature_rate * s ** 2, curvature * s, |
| 87 | + start_yaw) |
| 88 | + points.append(Point(x, y)) |
| 89 | + except Exception as e: |
| 90 | + print(f"Skipping failed clothoid point: {e}") |
| 91 | + |
| 92 | + return points |
| 93 | + |
| 94 | + |
| 95 | +def X(a, b, c): |
| 96 | + return integrate.quad(lambda t: cos((a/2)*t**2 + b*t + c), 0, 1)[0] |
| 97 | + |
| 98 | + |
| 99 | +def Y(a, b, c): |
| 100 | + return integrate.quad(lambda t: sin((a/2)*t**2 + b*t + c), 0, 1)[0] |
| 101 | + |
| 102 | + |
| 103 | +def solve_g_for_root(theta1, theta2, delta): |
| 104 | + initial_guess = 3*(theta1 + theta2) |
| 105 | + return fsolve(lambda A: Y(2*A, delta - A, theta1), [initial_guess]) |
| 106 | + |
| 107 | + |
| 108 | +def compute_path_length(r, theta1, delta, A): |
| 109 | + return r / X(2*A, delta - A, theta1) |
| 110 | + |
| 111 | + |
| 112 | +def compute_curvature(delta, A, L): |
| 113 | + return (delta - A) / L |
| 114 | + |
| 115 | + |
| 116 | +def compute_curvature_rate(A, L): |
| 117 | + return 2 * A / (L**2) |
| 118 | + |
| 119 | + |
| 120 | +def normalize_angle(angle_rad): |
| 121 | + return (angle_rad + pi) % (2 * pi) - pi |
| 122 | + |
| 123 | + |
| 124 | +def get_axes_limits(clothoids): |
| 125 | + x_vals = [p.x for clothoid in clothoids for p in clothoid] |
| 126 | + y_vals = [p.y for clothoid in clothoids for p in clothoid] |
| 127 | + |
| 128 | + x_min = min(x_vals) |
| 129 | + x_max = max(x_vals) |
| 130 | + y_min = min(y_vals) |
| 131 | + y_max = max(y_vals) |
| 132 | + |
| 133 | + x_offset = 0.1*(x_max - x_min) |
| 134 | + y_offset = 0.1*(y_max - y_min) |
| 135 | + |
| 136 | + x_min = x_min - x_offset |
| 137 | + x_max = x_max + x_offset |
| 138 | + y_min = y_min - y_offset |
| 139 | + y_max = y_max + y_offset |
| 140 | + |
| 141 | + return x_min, x_max, y_min, y_max |
| 142 | + |
| 143 | + |
| 144 | +def draw_clothoids(start, goal, num_steps, clothoidal_paths, |
| 145 | + save_animation=False): |
| 146 | + |
| 147 | + fig = plt.figure(figsize=(10, 10)) |
| 148 | + x_min, x_max, y_min, y_max = get_axes_limits(clothoidal_paths) |
| 149 | + axes = plt.axes(xlim=(x_min, x_max), ylim=(y_min, y_max)) |
| 150 | + |
| 151 | + axes.plot(start.x, start.y, 'ro') |
| 152 | + axes.plot(goal.x, goal.y, 'ro') |
| 153 | + lines = [axes.plot([], [], 'b-')[0] for _ in range(len(clothoidal_paths))] |
| 154 | + |
| 155 | + def animate(i): |
| 156 | + for line, clothoid_path in zip(lines, clothoidal_paths): |
| 157 | + x = [p.x for p in clothoid_path[:i]] |
| 158 | + y = [p.y for p in clothoid_path[:i]] |
| 159 | + line.set_data(x, y) |
| 160 | + |
| 161 | + return lines |
| 162 | + |
| 163 | + anim = animation.FuncAnimation( |
| 164 | + fig, |
| 165 | + animate, |
| 166 | + frames=num_steps, |
| 167 | + interval=25, |
| 168 | + blit=True |
| 169 | + ) |
| 170 | + if save_animation: |
| 171 | + anim.save('clothoid.gif', fps=30, writer="imagemagick") |
| 172 | + plt.show() |
| 173 | + |
| 174 | + |
| 175 | +def main(): |
| 176 | + start_point = Point(0, 0) |
| 177 | + start_orientation_list = [0.0] |
| 178 | + goal_point = Point(10, 0) |
| 179 | + goal_orientation_list = np.linspace(-pi, pi, 75) |
| 180 | + num_path_points = 100 |
| 181 | + clothoid_paths = generate_clothoid_paths( |
| 182 | + start_point, start_orientation_list, |
| 183 | + goal_point, goal_orientation_list, |
| 184 | + num_path_points) |
| 185 | + if show_animation: |
| 186 | + draw_clothoids(start_point, goal_point, |
| 187 | + num_path_points, clothoid_paths, |
| 188 | + save_animation=False) |
| 189 | + |
| 190 | + |
| 191 | +if __name__ == "__main__": |
| 192 | + main() |
0 commit comments