You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

求解两个闭合边界环对应赛道中心线的方法及优化咨询

赛道中心线求解问题与方案演进

问题背景

假设在二维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图
前序方法存在问题的区域,Voronoi法输出的中心线效果更优
优化可视化结果,橙色点为计算得到的Voronoi顶点

现存待解决问题

  • 需要筛选出位于两个外边界之间的Voronoi多边形顶点
  • 该方法在赛道自相交区域效果较差,可能需要先检测自相交区域,将赛道拆分后再拼接结果
    自相交区域的求解异常
  • 起终点线区域也存在少量输出异常
    起终点线区域的异常输出

内容的提问来源于stack exchange,提问作者Dizzixx

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.03 19:24:03