在本篇博客中,我将分享一个基于 Python 的无人驾驶仿真模拟系统,该系统不仅实现了基于势场法的目标导航与障碍避让,还通过 Tkinter 构建了可视化的用户界面,实现了如下多项功能:
- 动态障碍物:障碍物在画布上随机运动,并在碰到边界时反弹。
- 路径轨迹记录:小车运动过程中实时绘制轨迹,便于观察运动轨迹。
- 电量监控:模拟小车电池电量的消耗,当电量不足时自动暂停仿真并给出提示。
- 日志记录:通过日志区域实时记录仿真运行的关键信息。
- 仿真模式切换:提供“正常模式”和“紧急模式”,不同模式下小车的避障行为不同。
- 活动区域边界限制:确保小车始终在预定活动区域内运动,当小车接近边界时自动“反弹”。
本文将详细介绍各模块的设计思想和实现方法,并展示最终代码。
1. 项目背景与需求
在无人驾驶领域,避障、目标导航以及安全边界控制等都是非常重要的问题。为了让初学者能够在不需要实际硬件的前提下体验这些技术,我们可以通过仿真模拟来演示无人驾驶小车在真实场景中的行为。
在本项目中,我们使用 Python 的 Turtle 模块进行图形仿真,结合 Tkinter 构建交互式 UI,提供实时调参、日志记录与状态显示。特别是,针对小车可能超出活动区域的问题,我们设计了“反弹”机制来保证小车始终在规定边界内运行。
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 类封装了整个仿真过程,包括小车、目标、障碍物、轨迹绘制和传感器射线的实现。主要流程如下:
- 初始化对象:使用 Turtle 创建小车(蓝色三角形)、目标(绿色圆点)、多个动态障碍物(红色方块)以及记录轨迹的笔。
- 动态障碍物运动:调用
move_obstacles()函数,让障碍物在预定区域内随机移动,并在碰壁时反弹。 - 势场法计算:在
simulation_step()中计算目标吸引力与障碍物斥力,并通过逐步转向使小车朝期望方向运动。 - 边界检测:调用
keep_car_in_bounds()确保小车不会超出预定的活动区域。 - 电量监控与日志记录:每一步都会消耗电量,电量低时自动暂停仿真,并将关键信息记录到日志区域。
3.3 UI 界面 Application
利用 Tkinter,我们构建了一个主窗口,将 Turtle 画布嵌入其中,并在右侧设置了控制面板。用户可以通过按钮、滑块和下拉菜单实时调节仿真参数,同时在底部查看详细的日志信息。
4. 最终代码展示
完整代码如下(保存为 rich_simulation_ui_bound.py 并在 Python 3 环境下运行):
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 diffdef 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:
returnself.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
returnif distance((self.car.xcor(), self.car.ycor()), target_pos) < 20:
self.info_label.config(text="到达目标!", fg="green")
self.log("Target reached.")
self.running = False
returnif 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. 总结
在这篇博客中,我们详细介绍了如何利用 Python 的 Turtle 模块结合 Tkinter 实现一个功能丰富的无人驾驶仿真系统。通过势场法实现目标导航与障碍避让,再加上动态障碍物、轨迹记录、电量监控以及活动区域边界控制等功能,不仅让初学者可以直观地体验无人驾驶的原理,同时也为后续深入研究路径规划与自动控制算法提供了一个很好的起点。
希望这篇博客对你有所帮助,如果你有任何问题或改进建议,欢迎在评论区留言交流!
