|
| 1 | +""" |
| 2 | +离散曲线相关函数 |
| 3 | +""" |
| 4 | + |
| 5 | +import math |
| 6 | +from typing import Optional, Tuple |
| 7 | + |
| 8 | +import numpy as np |
| 9 | +import numpy.typing as npt |
| 10 | + |
| 11 | +from numpy_additional_funcs import normalize_angle |
| 12 | + |
| 13 | + |
| 14 | +def find_match_point( |
| 15 | + xy_coordinate: tuple[float, float], |
| 16 | + reference_line_nodes: npt.NDArray[np.float_], |
| 17 | + *, |
| 18 | + start_index: int = 0, |
| 19 | +) -> Tuple[int, float]: |
| 20 | + """ |
| 21 | + 曲线由若干离散点组成的点集表达,遍历曲线外某一点到该点集中每一点的距离,取距离最小的为匹配点,返回该点在点集中的索引和最小距离 |
| 22 | + :param xy_coordinate: (x, y) 点的坐标 |
| 23 | + :param reference_line_nodes: [[x0, y0], [x1, y1], ...] 构成参考线曲线的点集 |
| 24 | + :param start_index: 从点集中的该索引位置开始匹配 |
| 25 | + :return: (match_point_index, min_distance) 匹配点在曲线点集中的索引、到匹配点的距离 |
| 26 | + """ |
| 27 | + |
| 28 | + x, y = xy_coordinate |
| 29 | + reference_line_length = reference_line_nodes.shape[0] |
| 30 | + increase_count = 0 # 用 increase_count 记录 distance 连续增大的次数,避免多个局部最小值的干扰 |
| 31 | + min_distance_square = float("inf") |
| 32 | + match_point_index = start_index # 若最后仍没有找到匹配索引,则说明起始索引已经是最佳匹配,直接返回 |
| 33 | + |
| 34 | + if start_index == 0: |
| 35 | + # 首次运行情况 |
| 36 | + increase_count_limit = reference_line_length // 3 |
| 37 | + direction_flag = 1 # 正向遍历 |
| 38 | + elif start_index < 0: |
| 39 | + raise ValueError("index < 0") |
| 40 | + else: |
| 41 | + # 非首次运行情况 |
| 42 | + increase_count_limit = 5 |
| 43 | + |
| 44 | + # 上个周期匹配点坐标 |
| 45 | + pre_match_point = [ |
| 46 | + reference_line_nodes[start_index, 0], |
| 47 | + reference_line_nodes[start_index, 1], |
| 48 | + ] |
| 49 | + # 以上个周期匹配点、上个周期匹配点的前一个点之间的连线的方向,近似表示切向 |
| 50 | + d_x = pre_match_point[0] - reference_line_nodes[start_index - 1, 0] |
| 51 | + d_y = pre_match_point[1] - reference_line_nodes[start_index - 1, 1] |
| 52 | + pre_match_point_theta = np.arctan2(d_y, d_x) |
| 53 | + |
| 54 | + # 上个匹配点在曲线上的切向向量 |
| 55 | + pre_match_point_direction = np.array( |
| 56 | + [ |
| 57 | + normalize_angle(np.cos(pre_match_point_theta)), |
| 58 | + normalize_angle(np.sin(pre_match_point_theta)), |
| 59 | + ] |
| 60 | + ) |
| 61 | + |
| 62 | + # 计算上个匹配点指向当前 (x, y) 的向量 |
| 63 | + pre_match_to_xy_v = np.array([x - pre_match_point[0], y - pre_match_point[1]]) |
| 64 | + |
| 65 | + # 计算 pre_match_to_xy_v 在 pre_match_point_direction 上的投影,用于判断遍历方向 |
| 66 | + direction_flag = np.dot( |
| 67 | + pre_match_to_xy_v, pre_match_point_direction |
| 68 | + ) # 大于零正反向遍历,反之,反方向遍历 |
| 69 | + |
| 70 | + if direction_flag > 0: # 正向遍历 |
| 71 | + search_range = (start_index, reference_line_length, 1) |
| 72 | + else: # 反向遍历 |
| 73 | + search_range = (start_index, -1, -1) |
| 74 | + |
| 75 | + # 确定匹配点 |
| 76 | + for i in range(*search_range): |
| 77 | + reference_line_node_x = reference_line_nodes[i][0] |
| 78 | + reference_line_node_y = reference_line_nodes[i][1] |
| 79 | + # 计算 (x, y) 与 (reference_line_node_x, reference_line_node_y) 之间的距离 |
| 80 | + distance_square = (reference_line_node_x - x) ** 2 + ( |
| 81 | + reference_line_node_y - y |
| 82 | + ) ** 2 |
| 83 | + if distance_square < min_distance_square: |
| 84 | + min_distance_square = distance_square # 保留最小值 |
| 85 | + match_point_index = i |
| 86 | + increase_count = 0 |
| 87 | + else: |
| 88 | + increase_count += 1 |
| 89 | + if increase_count >= increase_count_limit: |
| 90 | + break |
| 91 | + |
| 92 | + return match_point_index, math.sqrt(min_distance_square) |
| 93 | + |
| 94 | + |
| 95 | +def get_projection_point( |
| 96 | + xy_coordinate: tuple[float, float], |
| 97 | + reference_line_nodes: npt.NDArray[np.float_], |
| 98 | + match_point_index: Optional[int] = None, |
| 99 | +) -> tuple[float, float, float, float]: |
| 100 | + """ |
| 101 | + TODO 此函数待验证 |
| 102 | + 获取某点在一条由离散点表示的参考线上的投影点信息 \n |
| 103 | + ref: https://www.bilibili.com/video/BV1EM4y137Jv |
| 104 | + :param xy_coordinate: (x, y) 笛卡尔坐标系下点的坐标 |
| 105 | + :param reference_line_nodes: [[x0, y0, heading0, kappa0], ...] 参考线上的若干离散点 |
| 106 | + :param match_point_index: [可选参数] 匹配点在参考线点集中的索引 |
| 107 | + :return: 匹配点的 (x, y, theta, kappa) |
| 108 | + """ |
| 109 | + |
| 110 | + x, y = xy_coordinate |
| 111 | + |
| 112 | + # d_v 是匹配点(x_match, y_match)指向待投影点(x,y)的向量(x-x_match, y-y_match) |
| 113 | + # tau 是匹配点的单位切向量(cos(theta_match), sin(theta_match))' |
| 114 | + # (x_r, y_r)' 约等于 (x_match, y_match)' + (d_v . tau) * tau |
| 115 | + # kappa_r 约等于 kappa_match,投影点曲率 |
| 116 | + # theta_r 约等于 theta_match + k_m * (d_v . tau),投影点切线与坐标轴夹角 |
| 117 | + |
| 118 | + if match_point_index is None: |
| 119 | + match_point_index, _ = find_match_point( |
| 120 | + xy_coordinate, |
| 121 | + reference_line_nodes[:, 0:2], |
| 122 | + start_index=0, |
| 123 | + ) |
| 124 | + |
| 125 | + # 通过匹配点确定投影点 |
| 126 | + x_match, y_match, theta_match, kappa_match = reference_line_nodes[match_point_index] |
| 127 | + d_v = np.array([x - x_match, y - y_match]) # 匹配点指向待投影点的向量 |
| 128 | + tau = np.array([np.cos(theta_match), np.sin(theta_match)]) # 匹配点的单位切向量 |
| 129 | + ds = np.dot(d_v, tau) |
| 130 | + r_m_v = np.array([x_match, y_match]) |
| 131 | + |
| 132 | + # 计算投影点的位置信息 |
| 133 | + x_r, y_r = r_m_v + ds * tau # 计算投影点坐标 |
| 134 | + theta_r = normalize_angle(theta_match + kappa_match * ds) # 计算投影点在参考线上切线与 x 轴的夹角 |
| 135 | + kappa_r = kappa_match # 投影点在参考线处的曲率 |
| 136 | + |
| 137 | + return x_r, y_r, theta_r, kappa_r |
| 138 | + |
| 139 | + |
| 140 | +def calculate_heading_kappa(path_points: npt.NDArray[np.float_]): |
| 141 | + """ |
| 142 | + 计算曲线上每个点的切向角 theta(与直角坐标轴x轴之间的角度)和曲率 kappa \n |
| 143 | + ref: https://github.com/6Lackiu/EMplanner_Carla/blob/4cb40d5ca04af8c49f3f7dd6b6966fa70bb7dc2d/planner/planning_utils.py#L185 |
| 144 | + :param path_points: 曲线上每一点的坐标 [(x0, y0), (x1, y1), ...] |
| 145 | + :return: [[theta0, kappa0], [theta1, kappa1], ...] |
| 146 | + """ |
| 147 | + # 原理: |
| 148 | + # theta = arctan(d_y/d_x) |
| 149 | + # kappa = d_theta / d_s |
| 150 | + # d_s = (d_x^2 + d_y^2)^0.5 |
| 151 | + |
| 152 | + # 用割线斜率近似表示切线斜率,参考数值微分知识 |
| 153 | + # 采用中点欧拉法来计算每个点处的切线方向角: |
| 154 | + # 当前点与前一个点连成的线段的方向角、和当前点与下一点连成线段的方向角求平均值,作为该点的切线方向 |
| 155 | + |
| 156 | + # TODO 将填补差分的功能提取至单独的函数中 |
| 157 | + |
| 158 | + points_array = np.array(path_points) |
| 159 | + d_xy_ = points_array[1:, :] - points_array[:-1, :] # 一阶差分,此种写法比 np.diff() 性能高得多 |
| 160 | + d_xy = np.empty_like(points_array) # 定义变量,预分配内存 |
| 161 | + # 由于 n 个点差分得到的只有 n-1 个差分结果,所以要在首尾添加重复单元来近似求每个节点的 dx、dy |
| 162 | + d_xy[0, :] = d_xy_[:, :][0] |
| 163 | + d_xy[-1, :] = d_xy_[:, :][-1] |
| 164 | + d_xy[1:-1, :] = (d_xy_[1:, :] + d_xy_[:-1, :]) / 2 |
| 165 | + |
| 166 | + # 计算切线方向角 theta |
| 167 | + theta = np.arctan2(d_xy[:, 1], d_xy[:, 0]) # np.arctan2 会将角度限制在 (-pi, pi)之间 |
| 168 | + |
| 169 | + d_theta_ = theta[1:] - theta[:-1] # 差分,这种写法比np.diff()性能高得多 |
| 170 | + d_theta = np.empty_like(theta) |
| 171 | + d_theta[0] = d_theta_[0] |
| 172 | + d_theta[-1] = d_theta_[-1] |
| 173 | + # d_theta[1:-1] = (d_theta_[1:] + d_theta_[:-1]) / 2 # 准确值,但有多值性风险 |
| 174 | + d_theta[1:-1] = np.sin( |
| 175 | + (d_theta_[1:] + d_theta_[:-1]) / 2 |
| 176 | + ) # 认为 d_theta 是个小量,用 sin(d_theta) 代替 d_theta,避免多值性 |
| 177 | + |
| 178 | + # 计算曲率 kappa |
| 179 | + d_s = np.sqrt(d_xy[:, 0] ** 2 + d_xy[:, 1] ** 2) |
| 180 | + kappa = d_theta / d_s |
| 181 | + |
| 182 | + result = np.vstack((theta, kappa)).T |
| 183 | + return result |
| 184 | + |
| 185 | + |
| 186 | +def enhance_reference_line( |
| 187 | + reference_line_node_list: npt.NDArray[np.float_], |
| 188 | +) -> npt.NDArray[np.float_]: |
| 189 | + """ |
| 190 | + 将仅包含坐标信息的参考线增强为包含坐标、切向角、曲率的参考线 \n |
| 191 | + :param reference_line_node_list: [[x0, y0], [x1, y1], ...] 参考线上的若干离散点 |
| 192 | + :return: [[x0, y0, heading0, kappa0], ...] |
| 193 | + """ |
| 194 | + |
| 195 | + heading_kappa = calculate_heading_kappa(reference_line_node_list) |
| 196 | + return np.hstack((reference_line_node_list, heading_kappa)) |
| 197 | + |
| 198 | + |
| 199 | +def calculate_curvature(points: np.ndarray) -> float: |
| 200 | + """ |
| 201 | + 曲率半径计算函数 \n |
| 202 | + ref: https://github.com/Pjer-zhang/PJCurvature \n |
| 203 | + :param points: 三个点的坐标 |
| 204 | + :return: 曲率半径 |
| 205 | + """ |
| 206 | + |
| 207 | + x = points[:, 0] |
| 208 | + y = points[:, 1] |
| 209 | + |
| 210 | + t_a = np.linalg.norm([x[1] - x[0], y[1] - y[0]]) |
| 211 | + t_b = np.linalg.norm([x[2] - x[1], y[2] - y[1]]) |
| 212 | + |
| 213 | + m = np.array([[1, -t_a, t_a**2], [1, 0, 0], [1, t_b, t_b**2]]) |
| 214 | + |
| 215 | + a = np.matmul(np.linalg.inv(m), x) |
| 216 | + b = np.matmul(np.linalg.inv(m), y) |
| 217 | + |
| 218 | + kappa = 2 * (a[2] * b[1] - b[2] * a[1]) / (a[1] ** 2.0 + b[1] ** 2.0) ** 1.5 |
| 219 | + |
| 220 | + return kappa |
0 commit comments