在Carla中构建自动驾驶:使用PID控制和ROS2进行路径跟踪
- 机器人软件开发
- 什么是 P、PI 和 PID 控制器?
- 比例 (P) 控制器
- 比例积分 (PI) 控制器
- 比例-积分-微分 (PID) 控制器
- 横向控制简介
- CARLA ROS2 集成
- 纵向控制
- 横向控制
- 关键要点
- 结论
- 引用
机器人软件开发
机器人技术是一个多学科领域,涉及机械设计和计算机科学的各个领域,例如计算机体系结构、网络和数据库。由于它集成了不同的领域,它通常被视为系统工程。机器人技术的核心软件组件是数学密集型的,必须经过高度优化才能保持低延迟。机器人技术的主要软件组件是:
- 知觉
- 规划
- 控制
无论其环境如何,通用的 autonomous stack 将如下所示(见下图),至少在其初始迭代中是这样。该堆栈由 4 个子堆栈组成,即传感器堆栈、感知堆栈、规划和控制堆栈。传感器堆栈由前后两个摄像头以及车辆顶部的 LiDAR 组成。感知堆栈包括定位和地图构建(我们在整个系列中一直在讨论这一点),高清地图对于导航也非常重要,但已经提出了一些方法,可以即时创建高清地图,最后是用于检测地标和了解可驾驶区域的对象检测和分割。
规划模块分为三个部分。众所周知,对于许多用例来说,拥有全局和本地规划器可能就足够了,但是在处理不同的场景时,机器人需要有一个行为树。让我们以自动驾驶汽车为例。可能存在诸如遵循红绿灯、停车或越野驾驶等场景,在这些场景中,不同的任务需要不同的规划者。最后是控件 — 通常,经过良好调整的 PID 控制器用于执行器信号。它通常分为两个步骤:用于航点跟踪的横向控制和用于速度管理的纵向控制。在本文中,我们将更详细地探讨控制部分。
如果我们要在 CARLA 中设计一辆基本的自动驾驶汽车,我们将如何做到这一点?强调 “in CARLA” 因为它将位于模拟器内部,这使得在结构化环境中工作变得更加容易。此外,我们可以利用 CARLA 提供的所有 API 优势来获得大量预先计算的估计值。下面的系统架构显示了如何使用 CARLA 和 ROS 2 创建自主路径跟踪车辆。
AV系统
我们首先启动 CARLA 客户端和 CARLA ROS 桥。这两个模块进行通信以将数据从 CARLA 发送到 ROS 2。最初,这将提供有关 CARLA 世界、天气和状态的信息。接下来,需要启动 CARLA ros-bridge 生成车辆节点,以便在 CARLA 世界中生成车辆,这将通过 ROS 2 主题提供有关车辆、传感器数据和交通的大量信息。之后,启动Lanelet 和 Point Cloud 地图,让我们可以看到可驾驶区域。
Lanelet 地图和点云地图可以了解周围环境和可驾驶区域。在我们了解之后,我们可动 waypoint generator。它采用车辆的起点和目标位置来生成最短路径,该路径由航点组成。路径点本质上是定义全局路径的一系列 3D 点。车辆的任务是沿着这些航路点到达其目标。为此,使用了 PID 控制器,该控制器采用航路点坐标和车辆的里程计(每个时间戳的当前位置和方向)来生成执行器信号。里程计数据也来自 CARLA。
从 ROS 2 的角度来看,系统是这样工作的:节点是工作流的核心。它订阅了开始和结束位置主题,以及为车辆生成的航路点,并使用车辆的里程计来计算控制命令。主题接收这些控制命令并移动车辆。整个系统在 RViz2 中可视化。carla vehicle ctrl
/carla/ego_vehicle/vehicle_control_cmd
由于 CARLA 已经处理了里程计和规划,我们将主要专注于车辆控制。为此,我们决定使用 PID 控制器,但可以探索更高级的控制器。以下是我们将涵盖的主题:
- 什么是 P、PI 和 PID 控制器?
- 实现 PID 控制 python。
- 什么是纵向控制和横向控制?
什么是 P、PI 和 PID 控制器?
如前所述,我们将使用 PID 控制器通过生成必要的执行器信号,使车辆沿所需轨迹移动。但在此之前,“执行器信号”实际上是什么意思?执行器是将电能转化为物理运动的机器人部件,通常是机器人关节中的电机或液压系统。控制器的工作是产生适量的电能以实现所需的运动。例如,如果你有一辆车,希望它从一个地方行驶到另一个地方,或者如果你有一个机械臂,希望它从桌子上抓一个苹果,规划器会给你所需的物理运动——它可以表示为速度曲线、轨迹,或两者兼而有之。控制者的工作是遵循建议的路径。
现在我们了解了目的,让我们来探索一下控制器是如何实现的。有不同类型的控制器可用,例如 PID 控制器、模糊逻辑控制器、模型预测控制 (MPC)、非线性模型预测控制 (NLMPC)、神经网络控制器、基于 RL 的控制器等。但在本文中,我们将重点介绍 PID 控制器,它是最简单但最高效的控制器之一。传说中,经过适当调整的 PID 控制器仍然可以让所有现代控制器物有所值。尽管它是最古老的控制器方法之一(已有 100 多年的历史),但它仍在工业和学术界使用。
PID 控制器中的 P、I 和 D 分别代表比例、积分和导数。每个术语都有助于控制机器人。让我们用一个例子来分解一下:假设您有一辆自动驾驶汽车,想在保持 50 m/s 速度的同时从家到办公室。以恒定速度自动驾驶车辆是一项称为巡航控制的高级驾驶员辅助系统 (ADAS) 功能。
从控制的角度来看,目标是通过根据当前车速和目标速度之间的差异不断调整油门来实现这一目标。从图示上讲,它可以表示如下:
在高层次上,你可以把它想象成一个系统,你提供所需的速度,它返回系统响应——实际的速度(见上面的图 a)。控制器的目标是使实际速度等于所需的速度,这意味着它们之间的差异最终应该达到零。
如果我们放大系统(如图 a),您将看到(图 b)控制器和车辆(作为工艺/工厂)。在这里,控制器的工作是将所需速度和实际速度作为输入,并计算它们之间的差值以产生节流值。向 PID 控制器提供飞行器的响应称为闭环反馈。
现在我们已经全面了解了 PID 控制器的作用,让我们纠缠 PID 控制器并了解 P、I 和 D 项的贡献。
比例 (P) 控制器
P 控制器解决当前速度和所需速度之间的差值。例如,如果当前速度为 0 且目标为 50 m/s,则需要增加节流阀以缩小该间隙。误差越大,应用的调节就越多,这意味着调节与误差成正比,表示为:
在数学上,这可以写成
下面的框图表示反馈控制系统中使用的比例 (P) 控制器,用于调节车辆的速度。将所需速度输入到系统中,并与实际速度进行比较以确定误差。该误差被馈送到 P 控制器中,该控制器根据误差的成比例增益调整车辆的油门。输出是车辆的实际速度,它被反馈到系统中进行连续调整,保持所需的速度。循环通过反馈闭合,确保实时校正飞行器的速度。
下面是简单 P 控制器的 python 代码
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import Slider# Define PID Controller class with resistance
class PIDControllerWithResistance:def __init__(self, Kp, set_point=0, resistance_factor=0.1):self.Kp = Kpself.set_point = set_pointself.resistance_factor = resistance_factor # Resistance to throttle (e.g., air resistance, friction)def update(self, current_value, dt):# Apply the same PID control logic but factor in resistanceerror = self.set_point - current_valueoutput = self.Kp * errorreturn output - self.resistance_factor * current_value # Reduce output by a resistance factor# Simulation parameters
dt = 0.1 # Time step
time = np.arange(0, 50, dt) # Simulation time# Initialize the PID controller with disturbance (resistance)
pid_with_resistance = PIDControllerWithResistance(Kp=1.0, set_point=50, resistance_factor=0.2)# Initial conditions
speed = 0
throttle_with_resistance = []
speed_record_with_resistance = []# Simulate the system with resistance
for t in time:control = pid_with_resistance.update(speed, dt)speed += control * dt # Speed is affected by throttle control and resistancethrottle_with_resistance.append(control)speed_record_with_resistance.append(speed)# Plot setup
fig, ax = plt.subplots()
plt.subplots_adjust(left=0.1, bottom=0.3)l, = plt.plot(time, speed_record_with_resistance, label="Speed Output (With Resistance)")
plt.axhline(pid_with_resistance.set_point, color='r', linestyle='--', label='Set Point')
plt.xlabel('Time [s]')
plt.ylabel('Speed [m/s]')
plt.title('PID Cruise Control with Resistance')
plt.legend()
plt.show()
结果:
此代码做了一些假设:
- 我们假设油门和车速之间的关系是线性的,这意味着增加油门会增加速度。这就是为什么我们简单地做 .通过将 乘以 ,我们确保速度在每个小的时间间隔内逐渐变化,模拟逐渐变化。
speed += control * dt
control
dt
- 为简单起见,我们假设在这个模型中,阻力与速度成线性比例。
我们首先定义 class,它接受
value、set-point 和 resistance 值。在 update 函数中,我们计算误差并将其乘以
得到节流。我们还包括 .PIDControllerWithResistance
self.set_point - current_value
output - self.resistance_factor * current_value
- 将阻力系数乘以当前速度 () 可创建一个更真实的模型,其中阻力随速度的增加而增加。
current_value
- 通过从输出节气门值中减去这个阻力分量,我们模拟了需要更多的节气门来保持更高的速度,从而使车辆更难以更高的速度加速。
我们从 0 到 50 循环遍历时间戳,保持 0.1,并调用控制器的 update 函数来获得结果速度。然而,正如你所看到的,仅使用比例控制是不够的。尽管速度接近设定点,但它从未完全达到设定点。稳态和设定点之间的这种差值称为 。此时,误差变得如此之小,以至于即使将其乘以
也会导致最小的变化,而速度几乎保持不变。dt
Steady State Error
比例积分 (PI) 控制器
正如我们所看到的,响应速度接近设定点,但并没有完全达到它。为了解决这个问题,我们可以在 Proportional 控制器旁边添加一个 Integral 控制器。Integral 控制器查看过去的错误并随着时间的推移累积它们。这种正误差的累积有助于将响应推向更接近设定点。
在数学上,这可以写成
微分 (D) 控制器现已与 PI 控制器并联添加,形成比例-积分-微分 (PID) 控制器。该系统用于反馈回路中,以调节车辆的速度,通过考虑现在、过去和预测的未来误差来提高精度。将所需速度与实际速度进行比较以计算误差。该误差被馈送到三个控制器中:比例 (P) 控制器,对当前误差做出反应,积分 (I) 控制器,解释随时间累积的误差,以及导数 (D) 控制器,通过考虑误差的变化率来预测未来的误差。这些控制器的输出组合在一起以调整车辆的油门,并将实际速度反馈到系统中进行连续校正,确保以更高的精度和稳定性保持所需的速度。
该方程式在编写时考虑了离散时间。 值通常是通过反复试验找到的,尽管有一些算法可用于调整增益值。表示导数收益。
以下是 PID 控制器的 python 代码
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import Slider# Define PID Controller class with resistance
class PIDControllerWithResistance:def __init__(self, Kp, Ki, Kd, set_point=0, resistance_factor=0.1):self.Kp = Kpself.Ki = Kiself.Kd = Kdself.set_point = set_pointself.prev_error = 0self.integral = 0self.resistance_factor = resistance_factor # Resistance to throttle (e.g., air resistance, friction)def update(self, current_value, dt):# Apply the same PID control logic but factor in resistanceerror = self.set_point - current_valueself.integral += error * dtderivative = (error - self.prev_error) / dtoutput = self.Kp * error + self.Ki * self.integral + self.Kd * derivativeself.prev_error = errorreturn output - self.resistance_factor * current_value # Reduce output by a resistance factor# Simulation parameters
dt = 0.1 # Time step
time = np.arange(0, 50, dt) # Simulation time# Initialize the PID controller with disturbance (resistance)
pid_with_resistance = PIDControllerWithResistance(Kp=1.0, Ki=0.05, Kd=0.01, set_point=50, resistance_factor=0.05)# Initial conditions
speed = 0
throttle_with_resistance = []
speed_record_with_resistance = []# Simulate the system with resistance
for t in time:control = pid_with_resistance.update(speed, dt)speed += control * dt # Speed is affected by throttle control and resistancethrottle_with_resistance.append(control)speed_record_with_resistance.append(speed)# Plot setup
fig, ax = plt.subplots()
plt.subplots_adjust(left=0.1, bottom=0.3)l, = plt.plot(time, speed_record_with_resistance, label="Velocity Output (With Resistance)")
plt.axhline(pid_with_resistance.set_point, color='r', linestyle='--', label='Set Point')
plt.xlabel('Time [s]')
plt.ylabel('Velocity [m/s]')
plt.title('PID Cruise Control with Resistance')
plt.legend()
plt.show()
该代码与 PI 控制器代码非常相似,只是在 PI 控制器输出中添加了导数项。它的计算公式为 。derivative = (error - self.prev_error) / dt
总结 PID 控制器,P 查看当前错误,I 查看过去的错误,D 预测未来的错误。它们共同调节输出响应以达到所需的结果。
横向控制简介
当将机器人从一个位置移动到另一个位置时,仅使用速度控制(通常称为纵向控制)不足以确保准确的路径跟踪。虽然纵向控制控制机器人的速度,但规划器通常会生成一个由从机器人当前位置到目标的 3D 航路点组成的轨迹。为了有效地遵循这一轨迹,机器人还需要横向控制,调整其转向角度或方向以保持所需的路径。横向控制对于处理偏差、确保平稳导航和实现精确定位至关重要,尤其是在复杂环境中移动时。
有各种类型的侧向控制器旨在处理此任务,每种类型都适用于不同的应用和条件。这些控制器可以大致分为几何控制器和动态控制器。
- 几何控制器:
- Pure Pursuit (胡萝卜追随)
- 赤 柱
- PID (PID)
- 动态控制器:
- MPC 控制器
- 其他控制系统
- 滑动模式、反馈线性化等
CARLA ROS2 集成
现在我们已经了解了 PID 控制器,让我们使用它在 CARLA 中移动我们的车辆。只有两个主文件用于执行此作,并且 ;我们将一一介绍它们。vehicle_ctrl.py
lat_lon_ctrl.py
首先了解 中的纵向和横向控制代码。下面的代码取自 CARLA 本身,略有修改。lat_lon_ctrl.py
from collections import deque
import math
import numpy as np
import carla
from agents.tools.misc import get_speed...class PIDLongitudinalController():"""PIDLongitudinalController implements longitudinal control using a PID."""def __init__(self, vehicle, K_P=1.0, K_I=0.0, K_D=0.0, dt=0.03):"""Constructor method.:param vehicle: actor to apply to local planner logic onto:param K_P: Proportional term:param K_D: Differential term:param K_I: Integral term:param dt: time differential in seconds"""self._vehicle = vehicleself._k_p = K_Pself._k_i = K_Iself._k_d = K_Dself._dt = dtself._error_buffer = deque(maxlen=10)def run_step(self, target_speed, debug=False):"""Execute one step of longitudinal control to reach a given target speed.:param target_speed: target speed in Km/h:param debug: boolean for debugging:return: throttle control"""current_speed = get_speed(self._vehicle)if debug:print('Current speed = {}'.format(current_speed))return self._pid_control(target_speed, current_speed)def _pid_control(self, target_speed, current_speed):"""Estimate the throttle/brake of the vehicle based on the PID equations:param target_speed: target speed in Km/h:param current_speed: current speed of the vehicle in Km/h:return: throttle/brake control"""error = target_speed - current_speedself._error_buffer.append(error)if len(self._error_buffer) >= 2:_de = (self._error_buffer[-1] - self._error_buffer[-2]) / self._dt_ie = sum(self._error_buffer) * self._dtelse:_de = 0.0_ie = 0.0return np.clip((self._k_p * error) + (self._k_d * _de) + (self._k_i * _ie), -1.0, 1.0)def change_parameters(self, K_P, K_I, K_D, dt):"""Changes the PID parameters"""self._k_p = K_Pself._k_i = K_Iself._k_d = K_Dself._dt = dtclass PIDLateralController():"""PIDLateralController implements lateral control using a PID."""def __init__(self, vehicle, offset=0, K_P=1.0, K_I=0.0, K_D=0.0, dt=0.03):"""Constructor method.:param vehicle: actor to apply to local planner logic onto:param offset: distance to the center line. If might cause issues if the valueis large enough to make the vehicle invade other lanes.:param K_P: Proportional term:param K_D: Differential term:param K_I: Integral term:param dt: time differential in seconds"""self._vehicle = vehicleself._k_p = K_Pself._k_i = K_Iself._k_d = K_Dself._dt = dtself._offset = offsetself._e_buffer = deque(maxlen=10)def run_step(self, waypoint):"""Execute one step of lateral control to steerthe vehicle towards a certain waypoin.:param waypoint: target waypoint:return: steering control in the range [-1, 1] where:-1 maximum steering to left+1 maximum steering to right"""return self._pid_control(waypoint, self._vehicle.get_transform())def _pid_control(self, waypoint, vehicle_transform):"""Estimate the steering angle of the vehicle based on the PID equations:param waypoint: target waypoint:param vehicle_transform: current transform of the vehicle:return: steering control in the range [-1, 1]"""# Get the ego's location and forward vectorego_loc = vehicle_transform.locationv_vec = vehicle_transform.get_forward_vector()v_vec = np.array([v_vec.x, v_vec.y, 0.0])# Get the vector vehicle-target_wpif self._offset != 0:# Displace the wp to the sidew_tran = waypointr_vec = w_tran.get_right_vector()w_loc = w_tran.location + carla.Location(x=self._offset*r_vec.x,y=self._offset*r_vec.y)else:w_loc = waypoint.locationw_vec = np.array([w_loc.x - ego_loc.x,w_loc.y - ego_loc.y,0.0])wv_linalg = np.linalg.norm(w_vec) * np.linalg.norm(v_vec)if wv_linalg == 0:_dot = 1else:_dot = math.acos(np.clip(np.dot(w_vec, v_vec) / (wv_linalg), -1.0, 1.0))_cross = np.cross(v_vec, w_vec)if _cross[2] < 0:_dot *= -1.0self._e_buffer.append(_dot)if len(self._e_buffer) >= 2:_de = (self._e_buffer[-1] - self._e_buffer[-2]) / self._dt_ie = sum(self._e_buffer) * self._dtelse:_de = 0.0_ie = 0.0return np.clip((self._k_p * _dot) + (self._k_d * _de) + (self._k_i * _ie), -1.0, 1.0)def change_parameters(self, K_P, K_I, K_D, dt):"""Changes the PID parameters"""self._k_p = K_Pself._k_i = K_Iself._k_d = K_Dself._dt = dt
纵向控制
PIDLongitudinalController
class 包含纵向控制的代码,我们首先初始化 PID 增益、Carla Vehicle 实例和误差缓冲区(用于 D-Control)。 是从外部函数迭代调用 PID 更新的函数的函数。真正的魔力在于函数内部。实现非常简单,类似于上面的 PID 实现。我们首先计算误差,填充列表。如果有 2 个以上的元素,那么我们计算导数误差 () 和积分误差 (),最后通过将其从 -1 裁剪到 1 来返回总控制输出为 。run_step
_pid_control
_pid_control
error = target_speed - current_speed
self._error_buffer
self._error_buffer
_de
_ie
np.clip((self._k_p * error) + (self._k_d * _de) + (self._k_i * _ie), -1.0, 1.0)
横向控制
横向控制有点棘手。在跳入代码之前,我们先看看横向误差是如何计算的。误差表示为无人机前向矢量和航路点矢量之间的有符号角。角度的标志是根据车辆是在所需路径的左侧还是右侧来决定。角度应为正 (向右) 或负 (向左)。
在自动驾驶汽车的横向控制中,主要目标是使车辆保持在正确的轨迹或路径上。车辆的横向控制通常与最小化与该所需路径的偏差有关,这是通过控制转向角来实现的。您提供的代码使用 PID (Proportional, Integral, Derivative) 控制器根据此偏差调整车辆的转向角。
让我们来看看代码。 是定义横向控制的位置。以下是误差角的计算方法,PIDLateralController
1. 车辆的前向矢量 (v_vec):
正向向量表示无人机当前移动的方向,该方向源自无人机的变换 ()。它是一个 3D 向量,但由于车辆在 2D 平面上移动,因此仅使用 x 和 y 分量。vehicle_transform.get_forward_vector()
2. 航点矢量 ():w_vec
航路点向量表示从机体的当前位置 () 到目标航路点 () 的方向:ego_loc
w_loc
3. 点积和角度:
机体航向与目标航点之间的误差由机体的前向矢量与航点矢量之间的角度来测量。这是使用点积和这些向量的大小计算的:
此角度 () 表示以弧度为单位的横向误差。_dot
4. Sign 的叉积:
为了确定无人机是在所需路径的左侧还是右侧(即误差的符号),计算前向矢量和航点矢量之间的叉积:
叉积的第三个分量(z 分量)的符号确定角度应该是正 (向右) 还是负 (向左)。如果 z 分量为负,则角度反转:
因此,横向误差 () 是一个有符号角度,表示无人机需要转向多少才能与航点对齐。_dot
PID 控制应用
错误 () 被传递到 PID 控制器中,PID 控制器使用以下公式:_dot
现在我们已经完成了 PID 控制,让我们看看如何使用 ROS 2 运行它。
现在让我们来看看 .vehicle_ctrl.py
class CarlaVehicleControl(Node):def __init__(self):super().__init__('carla_route_planner')# Subscribersself.initialpose_sub = self.create_subscription(PoseWithCovarianceStamped,'/initialpose',self.initialpose_callback,10)self.goal_pose_sub = self.create_subscription(PoseStamped,'/goal_pose',self.goal_pose_callback,10)self.waypt_sub = self.create_subscription(Path, '/carla/ego_vehicle/waypoints', self.waypoints_callback, 10)# Subscriber to the /carla/ego_vehicle/odometry topicself.odom_sub = self.create_subscription(Odometry,'/carla/ego_vehicle/odometry',self.odometry_callback,10)self.vehicle_control_publisher = self.create_publisher(CarlaEgoVehicleControl, '/carla/ego_vehicle/vehicle_control_cmd', 10)# Initialize Carla client and mapself.client = Client('localhost', 2000)self.client.set_timeout(10.0)# Get the current worldself.world = self.client.get_world()# Check if Town01 is already loadedif 'Town01' not in self.world.get_map().name:print("Town01 is not loaded. Loading Town01...")self.world = self.client.load_world('Town01')print("Done!")else:print("Town01 is already loaded.")self.map = self.world.get_map()# Initialize GlobalRoutePlannerself.route_planner = GlobalRoutePlanner(self.map, 2.0)# Get all actors (vehicles, pedestrians, etc.) in the worldself.actors = self.world.get_actors()# Filter to get only the vehicles get the 0-th veh as there is only one vehself.vehicle = self.actors.filter('vehicle.*')[0]# Placeholders for start and end posesself.start_pose = Noneself.end_pose = Noneself.waypoints_list = []self.odom = None# TF2 listener and buffer# self.tf_buffer = Buffer()# self.tf_listener = TransformListener(self.tf_buffer, self)# self.vehicle_loc = Nonedef odometry_callback(self, msg):self.get_logger().info(f"Received odometry data: {msg.pose.pose.position.x}, {msg.pose.pose.position.y}, {msg.pose.pose.position.z}")# Extract position and orientation from Odometry messagex = msg.pose.pose.position.xy = -msg.pose.pose.position.yz = msg.pose.pose.position.zprint(" ^^^^ ODOM XYZ: ", x,y,z )orientation_q = msg.pose.pose.orientationroll, pitch, yaw = euler_from_quaternion([orientation_q.x, orientation_q.y, orientation_q.z, orientation_q.w])# Create a carla.Location objectlocation = carla.Location(x=x, y=y, z=z)# Create a carla.Rotation objectrotation = carla.Rotation(roll=roll, pitch=pitch, yaw=yaw)# Create a carla.Transform objecttransform = carla.Transform(location, rotation)self.odom = transformdef waypoints_callback(self, msg):# self.waypoints_list.clear() # Clear the list before storing new waypoints# Iterate through all the waypoints in the Path messagefor pose in msg.poses:# Extract the position from the posex = pose.pose.position.xy = -pose.pose.position.yz = pose.pose.position.z# Extract the orientation (quaternion) from the poseorientation_q = pose.pose.orientationroll, pitch, yaw = euler_from_quaternion([orientation_q.x, orientation_q.y, orientation_q.z, orientation_q.w])# Create a carla.Location objectlocation = carla.Location(x=x, y=y, z=z)# Create a carla.Rotation objectrotation = carla.Rotation(roll=roll, pitch=pitch, yaw=yaw)# Create a carla.Transform objecttransform = carla.Transform(location, rotation)# Store the Waypoint in the global listself.waypoints_list.append(transform)self.get_logger().info(f"Stored {len(self.waypoints_list)} waypoints as carla.libcarla.Waypoint objects.")def create_ctrl_msg(self, throttle, steer, brake):control_msg = CarlaEgoVehicleControl()control_msg.throttle = throttlecontrol_msg.steer = steercontrol_msg.brake = brakereturn control_msgdef initialpose_callback(self, msg):self.get_logger().info("Received initialpose")self.start_pose = msg.pose.posedef goal_pose_callback(self, msg):self.get_logger().info("Received goal_pose")self.end_pose = msg.pose# Clear the waypoints list for the new goalself.waypoints_list.clear()def get_transform(self, vehicle_location, angle, d=6.4):a = math.radians(angle)location = carla.Location(d * math.cos(a), d * math.sin(a), 2.0) + vehicle_locationreturn carla.Transform(location, carla.Rotation(yaw=180 + angle, pitch=-15))def setup_PID(self, vehicle):"""This function creates a PID controller for the vehicle passed to it """args_lateral_dict = {'K_P': 0.5, # Reduced proportional gain for smoother steering'K_D': 0.1, # Small derivative gain to dampen oscillations'K_I': 0.01, # Small integral gain to correct for long-term drift'dt': 0.05}args_long_dict = {'K_P': 0.2, # Slightly lower gain for acceleration control'K_D': 0.3, # Moderate derivative gain'K_I': 0.01, # Small integral gain'dt': 0.05}PID= VehiclePIDController(vehicle,args_lateral=args_lateral_dict,args_longitudinal=args_long_dict)return PIDdef find_dist_veh(self, vehicle_loc,target):dist = math.sqrt( (target.location.x - vehicle_loc.x)**2 + \(target.location.y - vehicle_loc.y)**2 )return distdef drive_through_plan(self, planned_route, vehicle, speed, PID):"""This function drives throught the planned_route with the speed passed in the argument"""i=0waypt_cnt = len(planned_route)-1target=planned_route[0]cnt = 0while True:self.world.get_spectator().set_transform(self.get_transform(vehicle.get_location() +carla.Location(z=1, x=0.5), vehicle.get_transform().rotation.yaw-180))# vehicle_loc = vehicle.get_location()vehicle_loc = self.odom.locationdistance_v = self.find_dist_veh(vehicle_loc,target)control = PID.run_step(speed,target)# vehicle.apply_control(control)ctrl_msg = self.create_ctrl_msg(control.throttle,control.steer,control.brake)self.vehicle_control_publisher.publish(ctrl_msg)if i==(len(planned_route)-1):print("last waypoint reached")breakif (distance_v<3.5):control = PID.run_step(speed,target)# vehicle.apply_control(control)ctrl_msg = self.create_ctrl_msg(control.throttle,control.steer,control.brake)self.vehicle_control_publisher.publish(ctrl_msg)i=i+1target=planned_route[i]if cnt%5==0:print("=----------------------------------------------------------")print(f"\n{GREEN} ***** from current loc to {i}/{waypt_cnt} waypoint distance: {distance_v}{RESET}\n")print("ROS2 vehilce location: ", self.odom.location)print("CARLA vehilce location: ", vehicle.get_location())print("target location: ", target.location)rclpy.spin_once(self)# time.sleep(0.1) # Add a slight delay to reduce control frequency# time.sleep(1) # Add a 1-second delay# print("throttle: ", control.throttle)cnt+=1control = PID.run_step(0,planned_route[len(planned_route)-1])# vehicle.apply_control(control)ctrl_msg = self.create_ctrl_msg(control.throttle,control.steer,control.brake)self.vehicle_control_publisher.publish(ctrl_msg)def run(self):desired_velocity=10 #Km/hwhile rclpy.ok():rclpy.spin_once(self)if self.start_pose is None or self.end_pose is None:self.get_logger().info(f'Start pose: {self.start_pose}, End pose: {self.end_pose}')elif not self.waypoints_list:self.get_logger().info('Waiting for waypoints to be generated...')else:# Delay to ensure waypoints are populatedself.get_logger().info('Waiting a bit for waypoints to be fully populated...')time.sleep(1) # Add a 1-second delayself.get_logger().info(f'Generated {len(self.waypoints_list)} waypoints from start to end pose')# calculating life time of the markertotal_dist = self.find_dist_veh(self.waypoints_list[0].location, self.waypoints_list[len(self.waypoints_list)-1])marker_life_time = (total_dist/desired_velocity) * 3.6# Draw waypoints on the Carla mapfor w in self.waypoints_list:# print("self.waypoints_list: ",w.location)self.world.debug.draw_string(w.location, 'O', draw_shadow=False,color=Color(r=255, g=0, b=0), life_time=5000000.0,persistent_lines=True)# drive the vehiclePID=self.setup_PID(self.vehicle)self.drive_through_plan(self.waypoints_list,self.vehicle,desired_velocity,PID)# After processing, break the loop if neededbreakdef main(args=None):rclpy.init(args=args)route_planner = CarlaVehicleControl()route_planner.run()rclpy.shutdown()
如果您一直在关注 robotics 系列,上面的代码应该很容易理解。代码的核心在于 and 函数。run()
drive_through_plan()
- 调用 main 函数时,回调将转到该函数。您会注意到我们没有使用 ,而是使用 .这是因为 Continuous 旋转以收集所有已发布的数据。然而,我们不能仅仅为了这个目的而旋转——我们还需要根据输入来移动车辆。如果我们只关注这一点,我们可能会错过已发布的数据,导致车辆失控并可能撞墙。
这可以通过以下方式处理:run()
rclpy.spin()
rclpy.spin_once(self)
rclpy.spin()
- 在单独的线程中运行这两个任务。
- 运行 with 来收集已发布的数据,在循环中时,定期使用 来更新 subscriber 变量。
rclpy.spin_once()
while rclpy.ok()
drive_through_plan
rclpy.spin_once()
- 工作原理的解释如下:它首先设置一个目标轨迹点并进入一个 while 循环。此循环一直持续到飞机到达其目标位置。在循环中,它首先计算车辆位置与目标航点之间的距离,然后使用 PID 控制器根据所需的速度和目标航点计算转向和油门值。这些值将应用于车辆。此过程将重复,直到机体与目标航点之间的距离小于 3.5。此时,它会重新计算油门和转向值,应用它们,并更新目标航点。如果迭代到达最后一个航点,则车辆会根据所需的速度 0 制动并应用控制。此外,在该条件中,它会执行以更新里程计值。
drive_through_plan
if cnt%5==0:
rclpy.spin_once(self)
一切就绪后,剩下的工作就是运行代码并观察它的运行情况。
# in a new terminal, run carla first
# ./CarlaUE4.sh # or ./CarlaUE4.sh -prefernvidia # $ ~/carla_simulator/PythonAPI/util/config.py --map Town01
$CARLA_ROOT/CarlaUE4.sh -quality-level=Low -prefernvidia -nosound# in a new terminal, get inside the `carla-ros-bridge/colcon_ws` folder and source the workspace; launch the `carla ros-bridge`
cd ~/carla-ros-bridge/colcon_ws && source install/setup.bash
ros2 launch carla_ros_bridge carla_ros_bridge.launch.py synchronous_mode:=True town:=Town01 # <town number, eg: 03># in a new terminal, launch the objects.json; launch ros-bridge
# cd ~/carla-ros-bridge/colcon_ws
cd ~/carla-ros-bridge/colcon_ws && source install/setup.bashros2 launch carla_spawn_objects carla_example_ego_vehicle.launch.py spawn_sensors_only:=False objects_definition_file:=<absolute path to>/src/vehicle_ctrl/vehicle_ctrl/config/objects.json# load the town1 lanelet map
python src/vehicle_ctrl/vehicle_ctrl/map.py# in new terminal, launch the rviz2 [set the global frame to map in rviz2]
rviz2 -d /src/vehicle_ctrl/rviz2/carla_map_spawn_anywherev2.rviz# in a new terminal, get inside the `carla-ros-bridge/colcon_ws` folder and source the workspace; waypoint publisher
cd ~/carla-ros-bridge/colcon_ws && source install/setup.bash
ros2 launch carla_waypoint_publisher carla_waypoint_publisher.launch.py# goal remap
python src/vehicle_ctrl/vehicle_ctrl/remap_goal.py # waypoint following using carls ros-bridge
python src/vehicle_ctrl/vehicle_ctrl/simple_ctrl.py
相关文章:
在Carla中构建自动驾驶:使用PID控制和ROS2进行路径跟踪
机器人软件开发什么是 P、PI 和 PID 控制器?比例 (P) 控制器比例积分 (PI) 控制器比例-积分-微分 (PID) 控制器横向控制简介CARLA ROS2 集成纵向控制横向控制关键要点结论引用 机器人软件开发 …...
Windows和 macOS 上安装 `nvm` 和 Node.js 16.16.0 的详细教程。
Windows和 macOS 上安装 nvm 和 Node.js 16.16.0 的详细教程。 --- ### 1. 安装 nvm(Node Version Manager) nvm 是一个 Node.js 版本管理工具,可以轻松安装和切换不同版本的 Node.js。 #### Windows 安装 nvm 1. **下载 nvm 安装包**&#x…...
day11 python超参数调整
模型组成:模型 算法 实例化设置的外参(超参数) 训练得到的内参调参评估:调参通常需要进行两次评估。若不使用交叉验证,需手动划分验证集和测试集;但许多调参方法自带交叉验证功能,实际中可省略…...
Linux C++ xercesc xml 怎么判断路径下有没有对应的节点
在Linux环境下使用Xerces-C库处理XML文件时,判断路径下是否存在对应的节点可以通过以下几个步骤实现: 加载XML文档 首先,你需要加载XML文档。这可以通过创建一个xercesc::DOMParser对象并使用它的parse方法来实现。 #include <xercesc/…...
罗技K580蓝牙键盘连接mac pro
罗技K580蓝牙键盘,满足了我们的使用需求。最棒的是,它能够同时连接两个设备,通过按F11和F12键进行切换,简直不要太方便! 连接电脑 💻 USB连接 1、打开键盘:双手按住凹槽两边向前推࿰…...
Socket-UDP
Socket(套接字 )是计算机网络中用于实现进程间通信的重要编程接口,是对 TCP/IP 协议的封装 ,可看作是不同主机上应用进程之间双向通信端点的抽象。以下是详细介绍: 作用与地位 作为应用层与传输层、网络层协议间的中…...
【游戏ai】从强化学习开始自学游戏ai-2 使用IPPO自博弈对抗pongv3环境
文章目录 前言一、环境设计二、动作设计三、状态设计四、神经网路设计五、效果展示其他问题总结 前言 本学期的大作业,要求完成多智能体PPO的乒乓球对抗环境,这里我使用IPPO的方法来实现。 正好之前做过这个单个PPO与pong环境内置的ai对抗的训练&#…...
LeRobot 项目部署运行逻辑(三)——机器人及舵机配置
Lerobot 目前的机器人硬件以舵机类型为主,并未配置机器人正逆运动学及运动学,遥操作映射以舵机关节角度为主 因此,需要在使用前需要对舵机各项参数及初始位置进行配置 目录 1 Mobile ALOHA 配置 2 Dynamixel 配置 2.1 配置软件 2.2 SDK …...
Ubuntu20.04安装NVIDIA Warp
Ubuntu20.04安装NVIDIA Warp 安装测试 Warp的gitee网址 Warp的github网址 写在前面:建议安装前先参考readme文件自检系统驱动和cuda是否支持,个人实测建议是python3.9,但python3.8.20也可以使用。 写在前面:后续本人可能会使用这…...
电子病历高质量语料库构建方法与架构项目(临床情景理解模块篇)
引言 随着人工智能技术在医疗健康领域的广泛应用,电子病历(Electronic Medical Records,EMR)作为临床医疗数据的重要载体,已成为医学研究和临床决策支持的关键资源。电子病历高质量语料库的构建为医疗人工智能模型的训练和应用提供了基础支撑,其中临床情境理解模块是连接…...
WPF性能优化举例
WPF性能优化集锦 一、UI渲染性能优化 1. 虚拟化技术 ListView/GridView虚拟化: <ListView VirtualizingStackPanel.IsVirtualizing="True"VirtualizingStackPanel.VirtualizationMode="Recycling"ScrollViewer.IsDeferredScrollingEnabled=…...
【CUDA pytorch】
ev win10 3050ti 联想笔记本 nvcc --version 得到 PS C:\Users\25515> nvcc --version nvcc: NVIDIA (R) Cuda compiler driver Copyright (c) 2005-2022 NVIDIA Corporation Built on Tue_May__3_19:00:59_Pacific_Daylight_Time_2022 Cuda compilation tools, release …...
mac下载homebrew 安装和使用git
mac下载homebrew 安装和使用git 本人最近从windows换成mac,记录一下用homebrew安装git的过程 打开终端 command 空格,搜索终端 安装homebrew 在终端中输入下面命令,来安装homebrew /bin/bash -c "$(curl -fsSL https://raw.githu…...
Elasticsearch入门速通01:核心概念与选型指南
一、Elasticsearch 是什么? 一句话定义: 开源分布式搜索引擎,擅长处理海量数据的实时存储、搜索与分析,是ELK技术栈(ElasticsearchKibanaBeatsLogstash)的核心组件。 核心能力: 近实时搜索&…...
应对过度处方挑战:为药物推荐任务微调大语言模型(Xiangnan He)
Abstract 药物推荐系统因其有潜力根据患者的临床数据提供个性化且有效的药物组合,在医疗保健领域备受关注。然而,现有方法在适应不同的电子健康记录(EHR)系统以及有效利用非结构化数据方面面临挑战,导致其泛化能力有限…...
41 python http之requests 库
Python 的requests库就像你的 "接口助手",用几行代码就能发送 HTTP 请求,自动处理复杂的网络交互,让你告别手动拼接 URL 和解析响应的痛苦! 一、快速入门:3 步搞定基本请求 1.1 安装库:一键开启助手功能 pip install requests 1.2 发送 GET 请求 import r…...
百度网盘golang实习面经
goroutine内存泄漏的情况?如何避免? goroutine内存泄漏基本上是因为异常导致阻塞, 可以导致阻塞的情况 1 死锁, goroutine 等待的锁发生了死锁情况 2 chan没有正常被关闭,导致读取读chan的goroutine阻塞 如何避免 1 避免死锁 2 正常关闭 3 使用context管…...
super_small_toy_tpu
super_small_toy_tpu 小狼http://blog.csdn.net/xiaolangyangyang 1、基础框图 2、源码下载: GitHub - dldldlfma/super_small_toy_tpu 3、安装iverilog、vvp、gtkwave windows安装:https://bleyer.org/icarus/ ubuntu安装:sudo ap…...
Redis缓存穿透、缓存击穿与缓存雪崩:如何在.NET Core中解决
在高并发的互联网系统中,缓存技术作为优化系统性能的重要手段,已被广泛应用。然而,缓存系统本身也存在一些常见的问题,尤其是 缓存穿透、缓存击穿 和 缓存雪崩。这些问题如果处理不当,可能导致系统性能严重下降&#x…...
驱动车辆诊断测试创新 | 支持诊断测试的模拟器及数据文件转换生成
一 背景和挑战 | 背景: 随着汽车功能的日益丰富,ECU和域控制器的复杂性大大增加,导致测试需求大幅上升,尤其是在ECU的故障诊断和性能验证方面。然而,传统的实车测试方法难以满足高频率迭代和验证需求,不仅…...
VS Code技巧2:识别FreeCAD对象
在使用VS Code阅读FreeCAD代码或者FreeCAD的工作台代码时,VS Code无法识别FreeCAD对象,会提示Import “FreeCAD” could not be resolved: 问题解决如下几步即可。 第一步:确认 FreeCAD 的 Python 环境路径 在FreeCAD的Python控制…...
泰迪杯特等奖案例学习资料:基于多模态融合与边缘计算的智能温室环境调控系统
(第十二届泰迪杯数据挖掘挑战赛特等奖案例解析) 一、案例背景与核心挑战 1.1 应用场景与行业痛点 在现代设施农业中,温室环境调控直接影响作物产量与品质。传统温室管理存在以下问题: 环境参数耦合性高:温度、湿度、光照、CO₂浓度等参数相互影响,人工调控易顾此失彼。…...
猿人学web端爬虫攻防大赛赛题第13题——入门级cookie
1. F12开发者模式 刷新第一页,仔细研究发现里面有三次请求名为13的请求,根据题目提示cookie关键字,所以主要留意请求和响应的cookie值。 三次请求都带了sessionid,说明存在session(后面写代码要用session来写&#x…...
机器指标监控技术方案
文章目录 机器指标监控技术方案架构图组件简介Prometheus 简介核心特性适用场景 Grafana 简介核心特性适用场景 Alertmanager 简介核心特性适用场景 数据采集机器Node ExporterMySQL ExporterRedis ExporterES ExporterRocketMQ ExporterSpringcloud ExporterNacos 数据存储短期…...
数据库设计理论:从需求分析到实现的全流程解析
引言 在当今信息爆炸的时代,数据已成为企业和组织最宝贵的资产之一。如何有效地组织、存储和管理这些数据,是数据库设计需要解决的核心问题。一个优秀的数据库设计能够提高系统性能,确保数据一致性,降低维护成本,而糟…...
一文详解 Linux下的开源打印系统CUPS(Common UNIX Printing System)
文章目录 前言一、CUPS 简介二、CUPS 常用指令解析2.1 安装 CUPS2.2 启动/重启服务2.3 添加打印机(核心操作)2.4 设置默认打印机2.5 打印文件2.6 查看打印任务2.7 取消打印任务2.8 查看、移除已添加的打印机 三、调试与常见问题3.1 日志查看3.2 驱动问题…...
uniapp打包apk详细教程
目录 1.打apk包前提条件 2.获取uni-app标识 3.进入dcloud开发者后台 4.开始打包 1.打apk包前提条件 1.在HBuilderX.exe软化中,登录自己的账号 2.在dcloud官网,同样登录自己的账号。没有可以免费注册。 2.获取uni-app标识 获取方法:点…...
C++初阶-string类2
目录 1.迭代器 1.1普通迭代器的使用 1.2string::begin 1.3string::end 1.4const迭代器的使用 1.5泛型迭代器和const反向迭代器 1.6string::rbegin 1.6string::rend 1.7string::cbegin、string::cend、string::crbegin、string::crend 与begin/end、rbegin/rend的区别 …...
Qt QComboBox 下拉复选多选(multicombobox)
Qt QComboBox 下拉复选多选(multicombobox),备忘,待更多测试 【免费】QtQComboBox下拉复选多选(multicombobox)资源-CSDN文库...
逻辑回归之参数选择:从理论到实践
在机器学习的广阔领域中,逻辑回归作为一种经典的有监督学习算法,常用于解决分类问题。它以其简单易懂的原理和高效的计算性能,在实际应用中备受青睐。然而,要充分发挥逻辑回归的优势,参数选择是关键环节。本文将结合信…...
10、属性和数据处理---c++17
一、[[fallthrought]] 用途:在 switch 语句中标记某个分支 (case) 故意不写 break,明确告知编译器“执行穿透”是有意为之。 仅在需要向下穿透时使用,且应添加注释说明原因 #include<cstdio> #include<iostream> using namesp…...
conda管理python环境
安装conda 使用anaconda官网安装地址:https://www.anaconda.com/download/success 配置镜像环境 conda config --add channels Index of /anaconda/pkgs/main/ | 清华大学开源软件镜像站 | Tsinghua Open Source Mirror conda config --add channels Index of /an…...
【Python学习路线】零基础到项目实战系统
目录 🌟 前言技术背景与价值当前技术痛点解决方案概述目标读者说明 🧠 一、技术原理剖析核心概念图解核心作用讲解关键技术模块说明技术选型对比 💻 二、实战演示环境配置要求核心代码实现运行结果验证 ⚡ 三、性能对比测试方法论量化数据对比…...
C/C++核心机制深度解析:指针、结构体与动态内存管理(面试精要)
C/C核心机制深度解析:指针、结构体与动态内存管理(面试精要) 引言 在系统级编程领域,C/C语言凭借对硬件的直接操作能力和高效的内存管理机制,长期占据主导地位。面试中,指针、结构体和动态内存管理作为三…...
宇树科技举办“人型机器人格斗大赛”
2025 年 5 月至 6 月,一场全球瞩目的科技盛宴 —— 全球首场 “人形机器人格斗大赛”,将由杭州宇树科技盛大举办。届时,观众将迎来机器人格斗领域前所未有的视觉震撼。 为打造最强参赛阵容,宇树科技技术团队在过去数周里…...
getattr 的作用
getattr 是 Python 内置的一个函数,用于“动态地”获取对象的属性。**它允许你在运行时通过属性名称(字符串形式)来访问对象的属性,而不用在代码中直接硬编码属性名。**下面详细介绍该方法的用法和注意事项: ────…...
腾讯云服务器性能提升全栈指南(2025版)
腾讯云服务器性能提升全栈指南(2025版) 一、硬件选型与资源优化 1. 实例规格精准匹配 腾讯云服务器提供计算型CVM、内存型MEM、大数据型Hadoop等12种实例类型。根据业务特性选择: • 高并发Web应用:推荐SA3实例࿰…...
Kotlin与Jetpack Compose的详细使用指南
Kotlin与Jetpack Compose的详细使用指南,综合最新技术实践和官方文档整理: 一、环境配置与基础架构 项目创建 在Android Studio中选择Empty Compose Activity模板,默认生成包含Composable预览的MainActivity2要求Kotlin版本≥1.8.0&…...
潇洒郎: 100% 成功搭建Docker私有镜像仓库并管理、删除镜像
1、Registry Web管理界面 2、拉取Registry-Web镜像 创建配置文件 tee /opt/zwx-registry/web-config.yml <<-EOF registry:url: http://172.28.73.90:8010/v2name: registryreadonly: falseauth:enabled: false EOF 拉取docker-registry-web镜像并绑定Registry仓库 …...
【Spring Boot 注解】@ConfigurationProperties
文章目录 ConfigurationProperties注解一、简介二、依赖引入三、基本用法四、主要特性五、激活方式六,优点七、与 Value 对比 ConfigurationProperties注解 一、简介 ConfigurationProperties 是 Spring Boot 提供的一个强大注解,用于将外部配置&#…...
阿里云服务迁移实战: 06-切换DNS
概述 按前面的步骤,所有服务迁移完毕之后,最后就剩下 DNS 解析修改了。 修改解析 在域名解析处,修改域名的解析地址即可。 如果 IP 已经过户到了新账号,则不需要修改解析。 何确保业务稳定 域名解析更换时,由于 D…...
Java实现归并排序算法
1. 归并排序原理图解 归并排序是一种分治算法,其核心思想是将数组分成两半,分别对这两半进行排序,然后将排序后的两半合并。以下是归并排序的步骤: 1. 分治: - 将数组分成两半。 - 递归地对每半部分进行归并排序。 2. …...
Vue 项目中运行 `npm run dev` 时发生的过程
步骤1:找到「任务说明书」(package.json) 当你输入 npm run dev,系统首先会去查项目的 「任务说明书」(即 package.json 文件),看看 dev 这个任务具体要做什么。 示例代码(package.json 片段)…...
Python3(19)数据结构
在 Python 编程中,数据结构是组织和存储数据的重要方式,合理选择和使用数据结构能显著提升程序的效率和可读性。这篇博客通过丰富的代码示例深入学习 Python3 的数据结构知识,方便日后复习回顾。 一、列表(List) 1.1…...
macOS 安装了Docker Desktop版终端docker 命令没办法使用
macOS 安装了Docker Desktop版终端docker 命令没办法使用 1、检查Docker Desktop能否正常运行。 确保Docker Desktop能正常运行。 2、检查环境变量是否添加 1、添加环境变量 如果环境变量中没有包含Docker的路径,你可以手动添加。首先,找到Docker的…...
VR 汽车线束培训:探索高效学习新路径
在汽车线束生产领域,VR 汽车线束培训对于新员工的成长至关重要,它是一个关键环节,直接影响着生产效率和产品质量。传统的培训方式,通常是新员工在老员工的指导下,通过实际操作来学习线束装配流程。这种方式不仅耗费大量…...
k8s术语之Deployment
Deployment为Pod和Replica Set(下一代Replication Controller)提供声明式更新 您只需要在Deployment中描述您想要的目标状态是什么,Deployment controller就会帮您将Pod和ReplicaSet的实际状态改变到您的目标状态。您可以定义一个全新的Deployment Controller的职责…...
对js的Date二次封装,继承了原Date的所有方法,增加了自己扩展的方法,可以实现任意时间往前往后推算多少小时、多少天、多少周、多少月;
封装js时间工具 概述 该方法继承了 js 中 Date的所有方法;同时扩展了一部分自用方法: 1、任意时间 往前推多少小时,天,月,周;参数1、2必填,参数3可选beforeDate(num,formatter,dateVal); befo…...
17、商品管理:魔药商店运营——React 19 CRUD实现
一、魔药商店的炼金基石 1. 魔药配方契约(数据模型设计) // 预言池契约(Supabase Schema) interface Potion { id: uuid, name: string, effect: healing | transformation | attack, stock: number, moonSensitive: boo…...
2025-04-30 AIGC-如何做短片视频
摘要: 2025-04-30 AIGC-如何做短片视频 如何做短片视频: 一、画图修图 1.保存视频(无水保存) 2.文案提取(提取文案) 3. DeepSeek(提示词) 4.小梦Ai(图片视频) 5.修图Ai 6.扩图Ai 7.养生…...