求解两个闭合边界环对应赛道中心线的方法及优化咨询
赛道中心线求解问题与方案演进
问题背景
假设在二维x,y空间中存在两个闭合环,分别代表赛道的左右边界。将两个环的起点连接得到的线段为起终点线,该线段的中点即为赛道中心线的起点。
从起终点线沿行驶方向延伸,每个环由长度随机但波动较小的线段组成,每个环的线段数量高达2~4万条。
需要解决的问题是求解出一个闭合环,作为两个边界环对应的赛道中心线。
初始实现方案与存在缺陷
最初考虑到各小段线段长度接近,可以每次取两个环当前对应线段中更短的那个作为步长,在更长的线段上截取等长位置,计算两个等步长位置的中点作为中心线上的点,更新两条边界的当前位置后重复该过程,在步长更短的边界之间来回切换推进。
使用Python实现该逻辑,输入为存储两个环x、y坐标(x对应*.PositionX,y对应*.PositionZ)的CSV文件,参考代码如下:
import traceback import pandas as pd import numpy as np from matplotlib import pyplot as plt fDict = {"Bernese Alps":{"fName":"2021-09-12202910_FM7_DASH","left":4,"right":6}, "Maple Valley":{"fName":"2021-08-30171416_FM7_DASH","left":4,"right":1},} track = "Maple Valley" df = pd.read_csv(fDict[track]["fName"]+".csv") # 对应左右赛道边界的圈数编号 lapRight = fDict[track]["right"] lapLeft = fDict[track]["left"] # 提取左右边界的坐标序列 rightPos = df.loc[df.LapNumber==lapRight,['PositionX','PositionZ']] leftPos = df.loc[df.LapNumber==lapLeft,['PositionX','PositionZ']] # 计算相邻点的位移向量,闭合环首段使用首尾点差值 rightDiff = rightPos.diff() rightDiff.iloc[0] = rightPos.iloc[0]-rightPos.iloc[-1] leftDiff = leftPos.diff() leftDiff.iloc[0] = leftPos.iloc[0]-leftPos.iloc[-1] # 计算相邻点位移的模长 rightDiffMag = (rightDiff.PositionX.copy()*0+np.linalg.norm(rightDiff,axis=1)).rename({'PositionX':'PositionMag'}) leftDiffMag = (leftDiff.PositionX.copy()*0+np.linalg.norm(leftDiff,axis=1)).rename({'PositionX':'PositionMag'}) # 计算单位位移向量 rightDiffNormed = rightDiff.div(rightDiffMag,'index') leftDiffNormed = leftDiff.div(leftDiffMag,'index') # 中点计算工具方法 def right_to_left(rightX,rightZ,leftX,leftZ): rtl_diff = np.array([leftX-rightX,leftZ-rightZ]) rtl_mag = np.sqrt(sum(rtl_diff**2)) rtl_dir = rtl_diff/rtl_mag return rtl_dir,rtl_mag def midPoint(rightX,rightZ,leftX,leftZ): rtl_dir,rtl_mag = right_to_left(rightX,rightZ,leftX,leftZ) mid_vec = rtl_dir*rtl_mag*.5 return rightX+mid_vec[0],rightZ+mid_vec[1] # 初始化边界、中心线坐标存储容器 rights = [[rightPos.iloc[0].values[0]],[rightPos.iloc[0].values[1]]] lefts = [[leftPos.iloc[0].values[0]],[leftPos.iloc[0].values[1]]] midP = midPoint(rights[0][0],rights[1][0],lefts[0][0],lefts[1][0]) mids = [[midP[0]],[midP[1]]] # 初始化步长余量、索引跟踪变量 partials = {'left':0.0,'right':0.0} indicies = {'left':1,'right':1} print(len(leftPos),len(rightPos)) key = input("PAUSED - 'Y' TO CONTINUE 'N' TO ABORT: ") if key.lower() == 'y': Done = False try: while not Done: rightIdx = rightPos.index[indicies['right']] leftIdx = leftPos.index[indicies['left']] # 下一个点的索引,闭合环末尾跳转到起点 rightIdx_fwd = rightPos.index[0] if indicies['right']>=len(rightPos)-1 else rightPos.index[indicies['right']+1] leftIdx_fwd = rightPos.index[0] if indicies['left']>=len(leftPos)-1 else leftPos.index[indicies['left']+1] # 左边界剩余步长更长时,按右边界剩余步长推进 if leftDiffMag.loc[leftIdx]-partials['left'] >= rightDiffMag.loc[rightIdx]-partials['right']: active_side = 'left' mag = rightDiffMag.loc[rightIdx]-partials['right'] if not mag+partials['left'] >= leftDiffMag.loc[leftIdx_fwd]: partials['left']+= mag else: partials['left']=0.0 indicies['left']+=1 partials['right']=0.0 indicies['right']+=1 # 右边界剩余步长更长时,按左边界剩余步长推进 else: active_side = 'right' mag = leftDiffMag.loc[leftIdx]-partials['left'] if not mag+partials['right'] >= rightDiffMag.loc[rightIdx_fwd]: partials['right']+= mag else: partials['right']=0.0 indicies['right']+=1 partials['left']=0.0 indicies['left']+=1 # 计算更新后的边界点、中点 rightUpdate = rightPos.loc[rightIdx]+mag*rightDiffNormed.loc[rightIdx_fwd] leftUpdate = leftPos.loc[leftIdx]+mag*leftDiffNormed.loc[leftIdx_fwd] midP = midPoint(*rightUpdate.values,*leftUpdate.values) rights[0].append(rightUpdate.PositionX) rights[1].append(rightUpdate.PositionZ) lefts[0].append(leftUpdate.PositionX) lefts[1].append(leftUpdate.PositionZ) mids[0].append(midP[0]) mids[1].append(midP[1]) # 两侧边界都遍历完成时结束循环 if indicies['right']>=len(rightPos)-1 and indicies['left']>=len(leftPos)-1: Done=True except Exception as err: traceback.print_exception(None, err, err.__traceback__) print(len(rights[0]),len(lefts[0]),len(mids[0])) # 异常时输出结果可视化 axi = plt.subplot() axi.plot(*rights,ls='-',marker='.',c='r') axi.plot(*lefts,ls='-',marker='.',c='b') axi.plot(*mids,ls='-',marker='.',c='k') axi.set_aspect('equal') fig,axes = plt.subplots(2) for i in range(2): axes[i].plot(rights[i],c='r') axes[i].plot(lefts[i],c='b') axes[i].plot(mids[i],c='k') finally: plt.show() else: print("Processing was ABORTED!")
该方案存在以下问题:
- 运行速度慢
- 弯道区域两个边界的曲率差异会导致中心线向单侧偏移
- 中心线输出存在锯齿状噪声(后续排查发现部分噪声来自实现错误,但偏移问题为方法本身缺陷)
后续补充排查发现外边界更新存在过冲异常,解释了部分中心线锯齿问题,但中心线单侧偏移仍是当前方法的固有缺陷,相关效果示意图如下:





优化需求与方案演进
希望找到可行的中心线求解方法,额外需求包括:可便捷获取中心线的法向量及法向量与两侧边界的交点,支持输出步长标准化的中心线。
曾考虑过一种思路:已知行驶方向和起点中心点后,固定中心线步长,以当前中心点为圆心、步长为半径作半圆,求解与两个外边界相交的最短切线作为下一步的方向。
Voronoi图求解方案
这类问题属于GIS领域的常见需求,例如求解河流两侧边界对应的河道中心线,目前已知的可行方案是生成Voronoi图,其生成的多边形顶点和边可自然对应中心线。
参照scipy.spatial.voronoi_plot_2d的官方文档做了快速测试,即使输入超过4.7万个点也能正常运行,效果示意图如下:


现存待解决问题
- 需要筛选出位于两个外边界之间的Voronoi多边形顶点
- 该方法在赛道自相交区域效果较差,可能需要先检测自相交区域,将赛道拆分后再拼接结果

- 起终点线区域也存在少量输出异常

内容的提问来源于stack exchange,提问作者Dizzixx
相关产品推荐
相关产品推荐

