Advertisement

基于 Python 的无人驾驶仿真模拟系统

阅读量:

在本博客中将分享一个基于Python语言开发的无人驾驶仿真平台,该平台不仅通过势场法实现了目标导航与障碍避让,而且使用Tkinter库开发了一个具有直观界面的可视化界面,能够实现包括目标跟踪、路径规划以及动态障碍物避让等功能模块:

  • 动态障碍物模拟:本系统采用动态障碍物模型,在画面范围内生成随机移动的目标物体,并能在碰到画面边缘后反弹返回。
    • 路径轨迹记录功能:系统通过路径跟踪模块持续采集并追踪分析小车运行轨迹数据信息。
    • 电池电量实时监控系统:该模块能够精确监测小车电池能量状态,并在剩余电量降至临界值时系统会自动暂停仿真并对情况进行警报提示。
    • 日志采集与存储模块:采用日志模块持续采集数据信息,并将重要数据参数按时间段存入指定文件目录中供后续分析查询。
    • 系统仿真模式切换功能:支持启动'标准工作状态'与'应急避障模式'两种运行场景配置,在不同工作状态下系统的避障策略存在显著差异。
    • 活动区域边界限制机制:AWS系统会主动识别并限制车辆进入不允许区域,在接近限定范围边缘时实施精准反射返回操作以保证安全运行。

本文将详细介绍各模块的设计思想和实现方法,并展示最终代码。


1. 项目背景与需求

在无人驾驶领域中,避障、目标导航以及安全边界控制等都是关键问题。为了使新手无需实际硬件设备即可体验相关技术,在本项目中我们通过仿真模拟展示了无人驾驶小车在真实场景中的行为模式。

在本项目中我们利用Python的Turtle模块进行图形仿真并结合Tkinter构建具有交互功能的界面,在线提供实时参数调节日志记录与状态显示功能;特别地我们对小车超出活动区域的情况进行了反弹机制的设计从而确保车辆始终处于规定边界范围内运行。


2. 技术方案概述

2.1 仿真核心——势场法

势场法(Potential Field Method)是一种主要应用于机器人导航系统的避障与路径规划算法,在此领域中具有重要地位

  • 引力场:目标点具有引力效应并被其吸引。
  • 排斥力场:当障碍物存在时会产生排斥力,并且其强度与其到障碍物的距离呈反比关系,在距离越近时排斥力越大。
  • 矢量控制:通过将引力场效应与排斥力场进行矢量相加来确定小车的行驶方向,并通过连续调整行驶方向来实现避障与导航功能。

在代码实现中,在计算小车的动力学模型时,在程序流程中我们对目标向量以及所有障碍物施加对应的排斥力向量并进行加权求和运算;通过归一化处理后的斥力场总和来确定小车的理想运动方向。为了确保运动轨迹的平滑性,在规划路径时采用分阶段调整策略以规避剧烈转向的情况

2.2 可视化 UI —— Tkinter 与 Turtle 的结合

为了便于用户进行调试与仿真过程的监控,并将Turtle的绘图功能集成至Tkinter窗口中,并加入了相应的控制模块

  • 操作按钮:提供启动/停止/重置仿真的功能。
    • 参数调节条:支持实时可调的小车速度参数(如速度大小)、传感器的工作范围(如检测距离)、最大转向幅度(如转弯半径)以及排斥力系数(如引力衰减)。
    • 模式切换功能:用户可通过下拉菜单选择"常规运行状态"或"紧急运行状态"两种工作模式。
    • 日志面板:系统将实时显示仿真运行的关键数据及事件记录内容。

2.3 边界控制 —— 保证小车在活动区域内

在实际场景中无人驾驶车辆的主要关注点是其在限定区域内行驶的能力为此我们开发了一个自适应反弹定位函数 bounce_position 该函数能够实时监测并根据当前行驶状态自动计算出最合适的反射参数从而确保车辆不会越界操作过程中通过这种自适应机制实现无人驾驶车辆能够有效避免越界问题的同时还能模仿真实环境中的安全边界控制


3. 代码实现详解

下面简要介绍代码中的关键部分:

3.1 辅助函数

我们实现了一系列向量运算、角度计算和边界检测的辅助函数,例如:

  • normalize_vector(v) :该函数将输入的二维向量进行归一化处理。
    • angle_difference(a, b) :此函数旨在计算两个方向之间的最小夹角。
    • bounce_position(x, y, xmin, xmax, ymin, ymax) :此函数用于检测给定坐标的越界情况,并返回调整后的坐标位置及反弹方向。

这些函数为后续的势场计算和边界控制提供了基础。

3.2 仿真核心类 Simulation

Simulation 类实现了整个仿真过程,并包含以下核心要素:小车定位、目标追踪、障碍物避让、轨迹规划以及传感器数据处理五个环节。具体流程如下:

  1. 初始化操作:通过Turtle实现创建一个小车(代号:蓝色三角形)、目标(代号:绿色圆点)、一组动态障碍物(代号:红色方块)以及用于记录运动轨迹的记录模块。
  2. 动态障碍物运动控制:通过move_obstacles()函数实现障碍物在规定区域内随机分布并移动,在碰到边界后反弹返回。
  3. 势场算法计算会得到目标吸引与障碍物排斥的结果:在simulation_step()中执行势场算法计算会得到目标吸引与障碍物排斥的结果,并据此计算出合适转向角后调整行驶方向。
  4. 车辆越界检测机制:通过keep_car_in_bounds()函数确保车辆不会越界行驶。
  5. 能量管理与数据存储流程:每次运行步骤都会消耗一定电量水平;当电能检测降至阈值时会触发停止仿真流程,并将关键数据信息存储至日志模块中。

3.3 UI 界面 Application

通过Tkinter开发一个主界面窗口,在其中集成Turtle绘图板,并将其右侧配置为控制面板。用户可以通过一系列按钮、滑块以及下拉菜单即时更改仿真参数,并在界面的底部部分专门展示了详细的日志记录信息。

4. 最终代码展示

rich_simulation_ui_bound.py

import tkinter as tk
import turtle
import math
import random

------------------------------

辅助函数

------------------------------

def distance(p1, p2):
"""计算 p1 和 p2 两点之间的欧几里得距离。"""
return math.hypot(p1[0] - p2[0], p1[1] - p2[1])

def normalize_vector(v):
"""归一化二维向量 v = (x, y) 。"""
mag = math.hypot(v[0], v[1])
if mag == 0:
return (0, 0)
return (v[0] / mag, v[1] / mag)

def vector_add(v1, v2):
return (v1[0] + v2[0], v1[1] + v2[1])

def vector_scale(v, s):
return (v[0] * s, v[1] * s)

def vector_to_angle(v):
"""将二维向量转换为角度(度制)。"""
return math.degrees(math.atan2(v[1], v[0]))

def angle_difference(a, b):
"""计算两个角度之间的最小差值。"""
diff = (a - b + 180) % 360 - 180
return diff

def bounce_position(x, y, xmin, xmax, ymin, ymax):
"""
如果 (x, y) 超出边界,则返回反弹后的坐标,
并指示是否需要反转水平方向或垂直方向。
"""
flip_x = False
flip_y = False
if x < xmin:
x = xmin + (xmin - x)
flip_x = True
elif x > xmax:
x = xmax - (x - xmax)
flip_x = True
if y < ymin:
y = ymin + (ymin - y)
flip_y = True
elif y > ymax:
y = ymax - (y - ymax)
flip_y = True
return x, y, flip_x, flip_y

------------------------------

仿真核心类

------------------------------

class Simulation:
def init(self, canvas, screen, control_panel, log_widget):
self.canvas = canvas
self.screen = screen
self.control_panel = control_panel
self.log_widget = log_widget

参数设置

self.SENSOR_RANGE = 120 # 障碍斥力作用范围
self.CAR_SPEED = 4 # 小车每步前进距离
self.MAX_TURN_ANGLE = 5 # 最大转向角度
self.REPULSION_WEIGHT = 1.5 # 正常模式斥力权重
self.EMERGENCY_REPULSION = 3.0 # 紧急模式斥力权重

self.running = False # 仿真是否在运行
self.mode = "正常模式" # 默认模式
self.battery = 100 # 电池电量百分比

定义仿真区域边界

self.xmin, self.xmax = -380, 380
self.ymin, self.ymax = -280, 280

创建仿真对象

self.car = turtle.RawTurtle(self.screen)
self.car.shape("triangle")
self.car.color("blue")
self.car.penup()
self.car.speed(0)
self.car.setheading(90)
self.car.goto(-300, -250)

self.tracer = turtle.RawTurtle(self.screen)
self.tracer.hideturtle()
self.tracer.penup()
self.tracer.speed(0)
self.tracer.color("blue")
self.tracer.goto(self.car.position())
self.tracer.pendown()

self.target = turtle.RawTurtle(self.screen)
self.target.shape("circle")
self.target.color("green")
self.target.penup()
self.target.speed(0)
self.target.goto(300, 250)

self.NUM_OBSTACLES = 8
self.obstacles = []
self.obstacle_dirs = []
for _ in range(self.NUM_OBSTACLES):
obs = turtle.RawTurtle(self.screen)
obs.shape("square")
obs.color("red")
obs.penup()
obs.speed(0)
x = random.randint(-250, 250)
y = random.randint(-150, 150)
obs.goto(x, y)
obs.shapesize(stretch_wid=2, stretch_len=2)
self.obstacles.append(obs)
angle = random.uniform(0, 360)
self.obstacle_dirs.append(normalize_vector((math.cos(math.radians(angle)), math.sin(math.radians(angle)))))

self.desired_ray = turtle.RawTurtle(self.screen)
self.desired_ray.hideturtle()
self.desired_ray.speed(0)
self.desired_ray.penup()

self.info_label = tk.Label(self.control_panel, text="Simulation Paused", fg="red", font=("Arial", 12))
self.info_label.pack(side=tk.TOP, pady=5)
self.battery_label = tk.Label(self.control_panel, text="Battery: 100%", font=("Arial", 12))
self.battery_label.pack(side=tk.TOP, pady=5)

def log(self, message):
"""在日志区域添加信息。"""
self.log_widget.insert(tk.END, message + "\n")
self.log_widget.see(tk.END)

def reset(self):
"""重置仿真。"""
self.car.clear()
self.target.clear()
for obs in self.obstacles:
obs.clear()
self.desired_ray.clear()
self.tracer.clear()

self.car.reset()
self.car.shape("triangle")
self.car.color("blue")
self.car.penup()
self.car.speed(0)
self.car.setheading(90)
self.car.goto(-300, -250)

self.tracer.reset()
self.tracer.hideturtle()
self.tracer.penup()
self.tracer.speed(0)
self.tracer.color("blue")
self.tracer.goto(self.car.position())
self.tracer.pendown()

self.target.reset()
self.target.shape("circle")
self.target.color("green")
self.target.penup()
self.target.speed(0)
self.target.goto(300, 250)

for i, obs in enumerate(self.obstacles):
obs.reset()
obs.shape("square")
obs.color("red")
obs.penup()
obs.speed(0)
x = random.randint(-250, 250)
y = random.randint(-150, 150)
obs.goto(x, y)
obs.shapesize(stretch_wid=2, stretch_len=2)
angle = random.uniform(0, 360)
self.obstacle_dirs[i] = normalize_vector((math.cos(math.radians(angle)), math.sin(math.radians(angle))))
self.battery = 100
self.battery_label.config(text="Battery: 100%")
self.log("Simulation reset.")
self.screen.update()

def move_obstacles(self):
"""更新动态障碍物位置。"""
for i, obs in enumerate(self.obstacles):
x, y = obs.xcor(), obs.ycor()
dx, dy = self.obstacle_dirs[i]
speed = 2
new_x = x + dx * speed
new_y = y + dy * speed
new_x, new_y, flip_x, flip_y = bounce_position(new_x, new_y,
self.xmin + 20, self.xmax - 20,
self.ymin + 20, self.ymax - 20)
if flip_x or flip_y:
dx = -dx if flip_x else dx
dy = -dy if flip_y else dy
self.obstacle_dirs[i] = (dx, dy)
obs.goto(new_x, new_y)

def keep_car_in_bounds(self):
"""确保小车始终在活动区域内。"""
x, y = self.car.xcor(), self.car.ycor()
new_x, new_y, flip_x, flip_y = bounce_position(x, y, self.xmin, self.xmax, self.ymin, self.ymax)
if flip_x or flip_y:
self.car.goto(new_x, new_y)
current_heading = self.car.heading()
if flip_x:
current_heading = 180 - current_heading
if flip_y:
current_heading = - current_heading
self.car.setheading(current_heading)
self.log(f"Car bounced at boundary to {new_x:.1f}, {new_y:.1f}")

def simulation_step(self):
"""单步仿真逻辑。"""
if not self.running:
return

self.move_obstacles()

car_pos = (self.car.xcor(), self.car.ycor())
current_heading = self.car.heading()

target_pos = (self.target.xcor(), self.target.ycor())
target_vector = (target_pos[0] - car_pos[0], target_pos[1] - car_pos[1])
target_vector_norm = normalize_vector(target_vector)

repulsion = (0, 0)
for obs in self.obstacles:
obs_pos = (obs.xcor(), obs.ycor())
d = distance(car_pos, obs_pos)
if d < self.SENSOR_RANGE and d > 0:
strength = (self.SENSOR_RANGE - d) / self.SENSOR_RANGE
dir_vector = normalize_vector((car_pos[0] - obs_pos[0], car_pos[1] - obs_pos[1]))
repulsion = vector_add(repulsion, vector_scale(dir_vector, strength))
if self.mode == "紧急模式":
repulsion = vector_scale(repulsion, self.EMERGENCY_REPULSION)
else:
repulsion = vector_scale(repulsion, self.REPULSION_WEIGHT)

desired_vector = vector_add(target_vector_norm, repulsion)
desired_vector = normalize_vector(desired_vector)
desired_angle = vector_to_angle(desired_vector)

self.desired_ray.clear()
self.desired_ray.goto(car_pos)
self.desired_ray.pendown()
self.desired_ray.color("orange")
ray_end = (car_pos[0] + desired_vector[0] * self.SENSOR_RANGE,
car_pos[1] + desired_vector[1] * self.SENSOR_RANGE)
self.desired_ray.goto(ray_end)
self.desired_ray.penup()

angle_diff = angle_difference(desired_angle, current_heading)
if abs(angle_diff) > self.MAX_TURN_ANGLE:
if angle_diff > 0:
self.car.left(self.MAX_TURN_ANGLE)
else:
self.car.right(self.MAX_TURN_ANGLE)
else:
self.car.setheading(desired_angle)

self.car.forward(self.CAR_SPEED)
self.tracer.goto(self.car.position())

self.keep_car_in_bounds()

self.battery -= 0.05 * self.CAR_SPEED
if self.battery < 0:
self.battery = 0
self.battery_label.config(text=f"Battery: {int(self.battery)}%")
if self.battery <= 5:
self.log("Battery low. Simulation paused.")
self.info_label.config(text="Battery Low!", fg="orange")
self.running = False
return

if distance((self.car.xcor(), self.car.ycor()), target_pos) < 20:
self.info_label.config(text="到达目标!", fg="green")
self.log("Target reached.")
self.running = False
return

if int(self.battery * 100) % 50 == 0:
self.log(f"Pos: {self.car.position()}, Heading: {self.car.heading():.1f}")

self.screen.update()
self.screen.ontimer(self.simulation_step, 20)

def start(self):
if not self.running:
self.running = True
self.info_label.config(text="Simulation Running", fg="blue")
self.log("Simulation started.")
self.simulation_step()

def pause(self):
self.running = False
self.info_label.config(text="Simulation Paused", fg="red")
self.log("Simulation paused.")

def set_mode(self, mode_value):
self.mode = mode_value
self.log(f"Mode changed to: {mode_value}")

------------------------------

主应用程序(Tkinter UI)

------------------------------

class Application(tk.Tk):
def init(self):
super().init()
self.title("无人驾驶仿真模拟 - 丰富功能版(限区域活动)")
self.geometry("1100x750")

self.main_frame = tk.Frame(self)
self.main_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True)

self.canvas = tk.Canvas(self.main_frame, width=800, height=600, bg="white")
self.canvas.pack(side=tk.LEFT, padx=10, pady=10)

self.control_panel = tk.Frame(self.main_frame)
self.control_panel.pack(side=tk.RIGHT, fill=tk.Y, padx=10, pady=10)

self.log_text = tk.Text(self, height=8)
self.log_text.pack(side=tk.BOTTOM, fill=tk.X, padx=10, pady=5)

self.screen = turtle.TurtleScreen(self.canvas)
self.screen.bgcolor("lightgray")
self.screen.tracer(0)

self.simulation = Simulation(self.canvas, self.screen, self.control_panel, self.log_text)
self.create_controls()

def create_controls(self):
self.start_button = tk.Button(self.control_panel, text="Start", command=self.simulation.start, width=15)
self.start_button.pack(pady=5)

self.pause_button = tk.Button(self.control_panel, text="Pause", command=self.simulation.pause, width=15)
self.pause_button.pack(pady=5)

self.reset_button = tk.Button(self.control_panel, text="Reset", command=self.reset_simulation, width=15)
self.reset_button.pack(pady=5)

self.mode_label = tk.Label(self.control_panel, text="Mode:")
self.mode_label.pack(pady=5)
self.mode_var = tk.StringVar(value="正常模式")
self.mode_menu = tk.OptionMenu(self.control_panel, self.mode_var, "正常模式", "紧急模式", command=self.change_mode)
self.mode_menu.config(width=12)
self.mode_menu.pack(pady=5)

self.speed_label = tk.Label(self.control_panel, text="Car Speed:")
self.speed_label.pack(pady=5)
self.speed_slider = tk.Scale(self.control_panel, from_=1, to=10, orient=tk.HORIZONTAL, command=self.update_speed)
self.speed_slider.set(self.simulation.CAR_SPEED)
self.speed_slider.pack(pady=5)

self.sensor_range_label = tk.Label(self.control_panel, text="Sensor Range:")
self.sensor_range_label.pack(pady=5)
self.sensor_range_slider = tk.Scale(self.control_panel, from_=50, to=200, orient=tk.HORIZONTAL, command=self.update_sensor_range)
self.sensor_range_slider.set(self.simulation.SENSOR_RANGE)
self.sensor_range_slider.pack(pady=5)

self.turn_angle_label = tk.Label(self.control_panel, text="Max Turn Angle:")
self.turn_angle_label.pack(pady=5)
self.turn_angle_slider = tk.Scale(self.control_panel, from_=1, to=15, orient=tk.HORIZONTAL, command=self.update_turn_angle)
self.turn_angle_slider.set(self.simulation.MAX_TURN_ANGLE)
self.turn_angle_slider.pack(pady=5)

self.repulsion_label = tk.Label(self.control_panel, text="Repulsion Weight:")
self.repulsion_label.pack(pady=5)
self.repulsion_slider = tk.Scale(self.control_panel, from_=0, to=5, resolution=0.1, orient=tk.HORIZONTAL, command=self.update_repulsion)
self.repulsion_slider.set(self.simulation.REPULSION_WEIGHT)
self.repulsion_slider.pack(pady=5)

def update_speed(self, val):
self.simulation.CAR_SPEED = float(val)

def update_sensor_range(self, val):
self.simulation.SENSOR_RANGE = float(val)

def update_turn_angle(self, val):
self.simulation.MAX_TURN_ANGLE = float(val)

def update_repulsion(self, val):
self.simulation.REPULSION_WEIGHT = float(val)
self.simulation.log(f"Repulsion weight updated to {val}")

def change_mode(self, mode_value):
self.simulation.set_mode(mode_value)
if mode_value == "紧急模式":
self.repulsion_slider.config(state=tk.DISABLED)
else:
self.repulsion_slider.config(state=tk.NORMAL)

def reset_simulation(self):
self.simulation.pause()
self.simulation.reset()
self.screen.update()

if name == "main":
app = Application()
app.mainloop()

5. 总结

本文博客中

期待这篇博客能为您带来帮助。
如有任何疑问或建议,请您在评论区留言。

全部评论 (0)

还没有任何评论哟~