Skip to content

Commit 935339b

Browse files
committed
Add discrete_curve_funcs and numpy_additional_funcs
1 parent cc6581d commit 935339b

File tree

3 files changed

+264
-29
lines changed

3 files changed

+264
-29
lines changed

AuxiliaryFunctions/curvature.py

Lines changed: 0 additions & 29 deletions
This file was deleted.
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
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
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"""
2+
一些补充numpy功能的自实现函数
3+
"""
4+
5+
import math
6+
7+
import numpy as np
8+
9+
10+
def normalize_angle(angle: float) -> float:
11+
"""
12+
将角度转换到 [-pi, pi] 范围中 \n
13+
:param angle: 弧度制角度
14+
:return: [-pi, pi] 范围内的弧度制角度
15+
"""
16+
17+
return np.arctan2(np.sin(angle), np.cos(angle))
18+
19+
20+
def find_array_nearest(array_1d, value, mode: int = 1, *, sorter=None) -> int:
21+
"""
22+
在一维数组中搜索与给定值最接近的值的索引 \n
23+
ref: https://stackoverflow.com/questions/2566412
24+
:param array_1d: 一维数组
25+
:param value: 待搜索的值
26+
:param mode: 默认模式(mode!=0)可靠性高;快速模式(mode==0)要求数组有序或传入排序器
27+
:param sorter: 数组排序器(仅在对非有序数组使用快速模式时需要)
28+
:return: index
29+
"""
30+
31+
array = np.array(array_1d)
32+
if mode:
33+
# 普通模式,速度慢,但数组无需排序
34+
index = np.argmin(abs(array - value))
35+
else:
36+
# 快速模式,速度约为普通模式的3倍,要求数组有序,某些情况下可能会出错
37+
index = np.searchsorted(array, value, side="left", sorter=sorter)
38+
if index > 0 and (
39+
index == len(array)
40+
or math.fabs(value - array[index - 1]) < math.fabs(value - array[index])
41+
):
42+
index -= 1
43+
44+
return int(index)

0 commit comments

Comments
 (0)