跳到主要内容基于 IsaacLab 从零训练机器人行走 | 极客日志PythonAI算法
基于 IsaacLab 从零训练机器人行走
IsaacLab 环境配置与机器人强化学习训练流程。涵盖 Ubuntu 系统搭建、CUDA 驱动及 PyTorch 安装,详细讲解 IsaacSim 资产导入与机器人注册方法。通过修改配置文件定义奖励函数与终止条件,完成 PPO 算法训练脚本编写。最后提供命令行启动训练与加载模型测试的具体步骤,适用于双足或四足机器人的运动控制开发。
极光16 浏览 环境配置
硬件与系统要求
- 操作系统: Ubuntu 22.04 LTS
- 显卡: NVIDIA RTX 4080 或以上
系统安装与驱动
首先确保系统为 Ubuntu 22.04 LTS。若需自行安装,建议 /home 与 /usr 分区容量均不少于 200GB。
安装 NVIDIA 驱动时,根据显卡型号选择对应版本,推荐 550.xxx 系列。安装完成后在终端输入 nvidia-smi,若显示 GPU 信息则代表成功。
Thu Jun 5 15:49:51 2025 +-----------------------------------------------------------------------------------------+
| NVIDIA-SMI 550.54.14 Driver Version: 550.54.14 CUDA Version: 12.4 |
...
接下来安装 CUDA 和 cuDNN。根据驱动信息显示的 CUDA 版本(如 12.4),下载对应的 Toolkit 安装包。以 Ubuntu 22.04 为例,执行以下命令:
wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/cuda-ubuntu2204.pin
sudo mv cuda-ubuntu2204.pin /etc/apt/preferences.d/cuda-repository-pin-600
wget https://developer.download.nvidia.com/compute/cuda/12.4.0/local_installers/cuda-repo-ubuntu2204-12-4-local_12.4.0-550.54.14-1_amd64.deb
sudo dpkg -i cuda-repo-ubuntu2204-12-4-local_12.4.0-550.54.14-1_amd64.deb
sudo cp /var/cuda-repo-ubuntu2204-12-4-local/cuda-*-keyring.gpg /usr/share/keyrings/
sudo apt-get update
sudo apt-get -y install cuda-toolkit-12-4
安装后设置环境变量并验证:
echo 'export PATH=/usr/local/cuda-12.4/bin:$PATH' >> ~/.bashrc
echo 'export LD_LIBRARY_PATH=/usr/local/cuda-12.4/lib64:$LD_LIBRARY_PATH' >> ~/.bashrc
echo 'export CUDA_HOME=/usr/local/cuda-12.4' >> ~/.bashrc
source ~/.bashrc
nvcc --version
cuDNN 需选择与 CUDA 版本匹配的版本(如 CUDA 12.4 对应 cuDNN 8.9.2)。下载 deb 包后执行安装:
sudo dpkg -i cudnn-local-repo-ubuntu2204-xxxxxxxxxxxxxx.deb
sudo cp /var/cudnn-local-repo-ubuntu2204-9.10.1/cudnn-*-keyring.gpg /usr/share/keyrings/
apt-get update
apt-get -y install cudnn-cuda-12
sudo
sudo
Python 环境与 Isaac Lab 安装
推荐使用 Anaconda 管理环境。创建虚拟环境并安装 PyTorch(CUDA 12.4):
conda create -n env_isaaclab python=3.10
conda activate env_isaaclab
pip install --upgrade pip
conda install pytorch==2.4.0 torchvision==0.19.0 torchaudio==2.4.0 pytorch-cuda=12.4-c pytorch -c nvidia
接着安装 Isaac Sim 及 Isaac Lab。Isaac Sim 首次运行需接受 EULA 协议。
pip install 'isaacsim[all,extscache]==4.5.0' --extra-index-url https://pypi.nvidia.com
isaacsim
资产包可从官网下载(约 92GB),替换默认路径下的资产目录。最后克隆 Isaac Lab 代码库并安装依赖:
git clone [email protected]:isaac-sim/IsaacLab.git
cd IsaacLab
./isaaclab.sh --install
./isaaclab.sh -p scripts/tutorials/00_sim/create_empty.py
机器人注册
获取 USD 文件
将 SolidWorks 导出的 URDF 文件导入 Isaac Sim,生成 USD 文件。注意检查 STL 路径格式,若使用 package://... 不可行,建议替换为相对或绝对路径。移动机器人需勾选可动基座。
配置文件编写
进入 Isaac Lab 任务配置目录,复制现有机器人配置文件夹并重命名(例如 op3)。
1. 机器人参数 (op3.py)
定义机器人的初始状态、关节名称及执行器参数。注意替换为你的实际关节名。
import isaaclab.sim as sim_utils
from isaaclab.actuators import ImplicitActuatorCfg
from isaaclab.assets.articulation import ArticulationCfg
from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR
OP3_CFG = ArticulationCfg(
spawn=sim_utils.UsdFileCfg(
usd_path="xxx/op3.usd",
activate_contact_sensors=True,
rigid_props=sim_utils.RigidBodyPropertiesCfg(
disable_gravity=False,
retain_accelerations=False,
linear_damping=0.0,
angular_damping=0.0,
max_linear_velocity=20.0,
max_angular_velocity=20.0,
max_depenetration_velocity=10.0,
),
articulation_props=sim_utils.ArticulationRootPropertiesCfg(
enabled_self_collisions=True,
solver_position_iteration_count=4,
solver_velocity_iteration_count=4,
),
),
init_state=ArticulationCfg.InitialStateCfg(
pos=(0.0, 0.0, 0.28),
joint_pos={
"head_pan": 0.0,
"head_tilt": 0.0,
".*_hip_yaw": 0.0,
".*_hip_roll": 0.0,
".*_hip_pitch": 0.0,
".*_knee": 0.0,
".*_ank_pitch": 0.0,
".*_ank_roll": 0.0,
".*_sho_pitch": 0.0,
".*_sho_roll": 0.0,
".*_el": 0.0,
},
joint_vel={".*": 0.0},
),
soft_joint_pos_limit_factor=0.7,
actuators={
"legs": ImplicitActuatorCfg(
joint_names_expr=[".*_hip_yaw", ".*_hip_roll", ".*_hip_pitch", ".*_knee", ".*_ank_pitch", ".*_ank_roll"],
effort_limit_sim=20,
velocity_limit_sim=20.0,
stiffness={".*_hip_yaw": 100.0, ".*_hip_roll": 100.0, ".*_hip_pitch": 100.0, ".*_knee": 100.0, ".*_ank_pitch": 100.0, ".*_ank_roll": 100.0,},
damping={".*_hip_yaw": 20.0, ".*_hip_roll": 20.0, ".*_hip_pitch": 20.0, ".*_knee": 20.0, ".*_ank_pitch": 20.0, ".*_ank_roll": 20.0,},
),
"arms": ImplicitActuatorCfg(
joint_names_expr=[".*_sho_pitch", ".*_sho_roll", ".*_el"],
effort_limit_sim=20,
velocity_limit_sim=20.0,
stiffness={".*_sho_pitch": 100.0, ".*_sho_roll": 100.0, ".*_el": 100.0,},
damping={".*_sho_pitch": 20.0, ".*_sho_roll": 20.0, ".*_el": 20.0,},
),
"head": ImplicitActuatorCfg(
joint_names_expr=["head_pan", "head_tilt"],
effort_limit_sim=20,
velocity_limit_sim=20.0,
stiffness={"head_pan": 100.0, "head_tilt": 100.0,},
damping={"head_pan": 20.0, "head_tilt": 20.0,},
),
},
)
2. 高度检查脚本 (check_op3.py)
用于确认机器人落地后的实际基座高度,以便调整初始位置。
import argparse
import torch
from isaaclab.app import AppLauncher
from isaaclab.assets import Articulation
from isaaclab.sim import SimulationContext
from op3 import OP3_CFG
def design_scene(sim):
cfg = sim_utils.GroundPlaneCfg()
cfg.func("/World/defaultGroundPlane", cfg)
cfg = sim_utils.DomeLightCfg(intensity=2000.0, color=(0.75, 0.75, 0.75))
cfg.func("/World/Light", cfg)
origins = torch.tensor([[0.0, -1.0, 0.0], [0.0, 0.0, 0.0], [0.0, 1.0, 0.0]]).to(device=sim.device)
hr2 = Articulation(OP3_CFG.replace(prim_path="/World/G1"))
robots = [hr2]
return robots, origins
def run_simulator(sim, robots, origins):
sim_dt = sim.get_physics_dt()
count = 0
while simulation_app.is_running():
if count % 1000 == 0:
count = 0
for index, robot in enumerate(robots):
joint_pos, joint_vel = robot.data.default_joint_pos, robot.data.default_joint_vel
robot.write_joint_state_to_sim(joint_pos, joint_vel)
root_state = robot.data.default_root_state.clone()
root_state[:, :3] += origins[index]
robot.write_root_pose_to_sim(root_state[:, :7])
robot.write_root_velocity_to_sim(root_state[:, 7:])
robot.reset()
for robot in robots:
robot.set_joint_position_target(robot.data.default_joint_pos.clone())
robot.write_data_to_sim()
sim.step()
count += 1
for robot in robots:
robot.update(sim_dt)
root_pos = robot.data.root_pos_w
print(f"Robot height (z): {root_pos[:, 2].item():.3f}")
if __name__ == "__main__":
main()
3. 速度环境配置 (op3_velocity_env_cfg.py)
这是核心配置文件,定义了观测、动作、奖励及终止条件。主要修改点包括替换机器人名称、关节链接名及物理材质参数。
import math
from dataclasses import MISSING
import isaaclab.sim as sim_utils
from isaaclab.assets import ArticulationCfg, AssetBaseCfg
from isaaclab.envs import ManagerBasedRLEnvCfg
from isaaclab.managers import CurriculumTermCfg as CurrTerm
from isaaclab.managers import EventTermCfg as EventTerm
from isaaclab.managers import ObservationGroupCfg as ObsGroup
from isaaclab.managers import ObservationTermCfg as ObsTerm
from isaaclab.managers import RewardTermCfg as RewTerm
from isaaclab.managers import SceneEntityCfg
from isaaclab.managers import TerminationTermCfg as DoneTerm
from isaaclab.scene import InteractiveSceneCfg
from isaaclab.sensors import ContactSensorCfg, RayCasterCfg, patterns
from isaaclab.terrains import TerrainImporterCfg
from isaaclab.utils import configclass
from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR, ISAACLAB_NUCLEUS_DIR
from isaaclab.utils.noise import AdditiveUniformNoiseCfg as Unoise
import isaaclab_tasks.manager_based.locomotion.velocity.mdp as mdp
from isaaclab.terrains.config.rough import ROUGH_TERRAINS_CFG
from isaaclab.managers import ActionTermCfg
@configclass
class MySceneCfg(InteractiveSceneCfg):
terrain = TerrainImporterCfg(
prim_path="/World/ground",
terrain_type="generator",
terrain_generator=ROUGH_TERRAINS_CFG,
max_init_terrain_level=5,
collision_group=-1,
physics_material=sim_utils.RigidBodyMaterialCfg(
friction_combine_mode="multiply",
restitution_combine_mode="multiply",
static_friction=1.0,
dynamic_friction=1.0,
),
visual_material=sim_utils.MdlFileCfg(
mdl_path=f"{ISAACLAB_NUCLEUS_DIR}/Materials/TilesMarbleSpiderWhiteBrickBondHoned/TilesMarbleSpiderWhiteBrickBondHoned.mdl",
project_uvw=True,
texture_scale=(0.25, 0.25),
),
debug_vis=False,
)
robot: ArticulationCfg = MISSING
height_scanner = RayCasterCfg(
prim_path="{ENV_REGEX_NS}/Robot/robotis_op3/base",
offset=RayCasterCfg.OffsetCfg(pos=(0.0, 0.0, 20.0)),
attach_yaw_only=True,
pattern_cfg=patterns.GridPatternCfg(resolution=0.1, size=[1.6, 1.0]),
debug_vis=False,
mesh_prim_paths=["/World/ground"],
)
contact_forces = ContactSensorCfg(prim_path="{ENV_REGEX_NS}/Robot/robotis_op3/.*", history_length=3, track_air_time=True)
sky_light = AssetBaseCfg(
prim_path="/World/skyLight",
spawn=sim_utils.DomeLightCfg(
intensity=750.0,
texture_file=f"{ISAAC_NUCLEUS_DIR}/Materials/Textures/Skies/PolyHaven/kloofendal_43d_clear_puresky_4k.hdr",
),
)
@configclass
class CommandsCfg:
base_velocity = mdp.UniformVelocityCommandCfg(
asset_name="robot",
resampling_time_range=(10.0, 10.0),
rel_standing_envs=0.02,
rel_heading_envs=1.0,
heading_command=True,
heading_control_stiffness=0.5,
debug_vis=True,
ranges=mdp.UniformVelocityCommandCfg.Ranges(
lin_vel_x=(-1.0, 1.0),
lin_vel_y=(-1.0, 1.0),
ang_vel_z=(-1.0, 1.0),
heading=(-math.pi, math.pi),
),
)
@configclass
class ActionsCfg:
joint_pos = mdp.JointPositionActionCfg(asset_name="robot", joint_names=[".*"], scale=0.5, use_default_offset=True)
@configclass
class ObservationsCfg:
@configclass
class PolicyCfg(ObsGroup):
base_lin_vel = ObsTerm(func=mdp.base_lin_vel, noise=Unoise(n_min=-0.1, n_max=0.1))
base_ang_vel = ObsTerm(func=mdp.base_ang_vel, noise=Unoise(n_min=-0.2, n_max=0.2))
projected_gravity = ObsTerm(func=mdp.projected_gravity, noise=Unoise(n_min=-0.05, n_max=0.05))
velocity_commands = ObsTerm(func=mdp.generated_commands, params={"command_name": "base_velocity"})
joint_pos = ObsTerm(func=mdp.joint_pos_rel, noise=Unoise(n_min=-0.01, n_max=0.01))
joint_vel = ObsTerm(func=mdp.joint_vel_rel, noise=Unoise(n_min=-1.5, n_max=1.5))
actions = ObsTerm(func=mdp.last_action)
height_scan = ObsTerm(
func=mdp.height_scan,
params={"sensor_cfg": SceneEntityCfg("height_scanner")},
noise=Unoise(n_min=-0.1, n_max=0.1),
clip=(-1.0, 1.0),
)
def __post_init__(self):
self.enable_corruption = True
self.concatenate_terms = True
policy: PolicyCfg = PolicyCfg()
@configclass
class EventCfg:
physics_material = EventTerm(
func=mdp.randomize_rigid_body_material,
mode="startup",
params={"asset_cfg": SceneEntityCfg("robot", body_names=".*"), "static_friction_range": (0.8, 0.8), "dynamic_friction_range": (0.6, 0.6), "restitution_range": (0.0, 0.0), "num_buckets": 64,},
)
add_base_mass = EventTerm(
func=mdp.randomize_rigid_body_mass,
mode="startup",
params={"asset_cfg": SceneEntityCfg("robot", body_names="base"), "mass_distribution_params": (-5.0, 5.0), "operation": "add",},
)
reset_base = EventTerm(
func=mdp.reset_root_state_uniform,
mode="reset",
params={"pose_range": {"x": (-0.5, 0.5), "y": (-0.5, 0.5), "yaw": (-3.14, 3.14)}, "velocity_range": {"x": (-0.5, 0.5), "y": (-0.5, 0.5), "z": (-0.5, 0.5), "roll": (-0.5, 0.5), "pitch": (-0.5, 0.5), "yaw": (-0.5, 0.5),},},
)
push_robot = EventTerm(
func=mdp.push_by_setting_velocity,
mode="interval",
interval_range_s=(10.0, 15.0),
params={"velocity_range": {"x": (-0.5, 0.5), "y": (-0.5, 0.5)}},
)
@configclass
class RewardsCfg:
track_lin_vel_xy_exp = RewTerm(func=mdp.track_lin_vel_xy_exp, weight=1.0, params={"command_name": "base_velocity", "std": math.sqrt(0.25)})
track_ang_vel_z_exp = RewTerm(func=mdp.track_ang_vel_z_exp, weight=0.5, params={"command_name": "base_velocity", "std": math.sqrt(0.25)})
lin_vel_z_l2 = RewTerm(func=mdp.lin_vel_z_l2, weight=-2.0)
dof_torques_l2 = RewTerm(func=mdp.joint_torques_l2, weight=-1.0e-5)
feet_air_time = RewTerm(func=mdp.feet_air_time, weight=0.125, params={"sensor_cfg": SceneEntityCfg("contact_forces", body_names=".*ank_roll_link"), "command_name": "base_velocity", "threshold": 0.5,})
@configclass
class TerminationsCfg:
time_out = DoneTerm(func=mdp.time_out, time_out=True)
base_contact = DoneTerm(func=mdp.illegal_contact, params={"sensor_cfg": SceneEntityCfg("contact_forces", body_names="base"), "threshold": 1.0,})
@configclass
class LocomotionVelocityRoughEnvCfg(ManagerBasedRLEnvCfg):
scene: MySceneCfg = MySceneCfg(num_envs=4096, env_spacing=2.5)
observations: ObservationsCfg = ObservationsCfg()
actions: ActionsCfg = ActionsCfg()
commands: CommandsCfg = CommandsCfg()
rewards: RewardsCfg = RewardsCfg()
terminations: TerminationsCfg = TerminationsCfg()
events: EventCfg = EventCfg()
curriculum: CurriculumCfg = CurriculumCfg()
def __post_init__(self):
self.decimation = 4
self.episode_length_s = 20.0
self.sim.dt = 0.005
self.sim.render_interval = self.decimation
self.sim.physics_material = self.scene.terrain.physics_material
4. 复杂地面环境 (rough_env_cfg.py)
针对崎岖地形优化奖励函数,鼓励步态交替并惩罚脚部滑动。
from .op3_velocity_env_cfg import LocomotionVelocityRoughEnvCfg
from op3 import OP3_CFG
@configclass
class OP3Rewards(RewardsCfg):
termination_penalty = RewTerm(func=mdp.is_terminated, weight=-200.0)
track_lin_vel_xy_exp = RewTerm(func=mdp.track_lin_vel_xy_yaw_frame_exp, weight=1.0, params={"command_name": "base_velocity", "std": 0.5})
track_ang_vel_z_exp = RewTerm(func=mdp.track_ang_vel_z_world_exp, weight=1.0, params={"command_name": "base_velocity", "std": 0.5})
feet_air_time = RewTerm(func=mdp.feet_air_time_positive_biped, weight=0.25, params={"command_name": "base_velocity", "sensor_cfg": SceneEntityCfg("contact_forces", body_names=".*ank_roll_link"), "threshold": 0.4,})
feet_slide = RewTerm(func=mdp.feet_slide, weight=-0.25, params={"sensor_cfg": SceneEntityCfg("contact_forces", body_names=".*ank_roll_link"), "asset_cfg": SceneEntityCfg("robot", body_names=".*ank_roll_link"),})
dof_pos_limits = RewTerm(func=mdp.joint_pos_limits, weight=-1.0, params={"asset_cfg": SceneEntityCfg("robot", joint_names=".*_ank_roll")})
joint_deviation_hip = RewTerm(func=mdp.joint_deviation_l1, weight=-0.2, params={"asset_cfg": SceneEntityCfg("robot", joint_names=[".*_hip_roll", ".*_hip_pitch", ".*_hip_yaw"])},)
@configclass
class OP3RoughEnvCfg(LocomotionVelocityRoughEnvCfg):
rewards: OP3Rewards = OP3Rewards()
def __post_init__(self):
super().__post_init__()
self.scene.robot = OP3_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot")
if self.scene.height_scanner:
self.scene.height_scanner.prim_path = "{ENV_REGEX_NS}/Robot/robotis_op3/base"
self.events.push_robot = None
self.events.add_base_mass = None
self.events.reset_robot_joints.params["position_range"] = (1.0, 1.0)
self.events.base_external_force_torque.params["asset_cfg"].body_names = [".*base"]
self.terminations.base_contact.params["sensor_cfg"].body_names = ".*base"
self.rewards.flat_orientation_l2.weight = -1.0
self.commands.base_velocity.ranges.lin_vel_x = (0.5, 1.0)
self.commands.base_velocity.ranges.lin_vel_y = (0.0, 0.0)
5. 平地环境 (flat_env_cfg.py)
from .rough_env_cfg import OP3RoughEnvCfg
@configclass
class OP3FlatEnvCfg(OP3RoughEnvCfg):
def __post_init__(self):
super().__post_init__()
self.scene.terrain.terrain_type = "plane"
self.scene.terrain.terrain_generator = None
self.scene.height_scanner = None
self.observations.policy.height_scan = None
self.curriculum.terrain_levels = None
self.rewards.feet_air_time.weight = 1.0
self.rewards.feet_air_time.params["threshold"] = 0.6
6. 环境注册 (__init__.py)
在 __init__.py 中注册 Gym 环境,区分训练模式(Train)和测试模式(Play)。
import gymnasium as gym
from . import agents
gym.register(id="Rough-OP3-train", entry_point="isaaclab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={"env_cfg_entry_point": f"{__name__}.rough_env_cfg:OP3RoughEnvCfg", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:OP3RoughPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_rough_ppo_cfg.yaml",})
gym.register(id="Flat-OP3-train", entry_point="isaaclab.envs:ManagerBasedRLEnv", disable_env_checker=True, kwargs={"env_cfg_entry_point": f"{__name__}.flat_env_cfg:OP3FlatEnvCfg", "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:OP3FlatPPORunnerCfg", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_flat_ppo_cfg.yaml",})
7. 训练参数 (rsl_rl_ppo_cfg.py)
from isaaclab.utils import configclass
from isaaclab_rl.rsl_rl import RslRlOnPolicyRunnerCfg, RslRlPpoActorCriticCfg, RslRlPpoAlgorithmCfg
@configclass
class OP3RoughPPORunnerCfg(RslRlOnPolicyRunnerCfg):
num_steps_per_env = 24
max_iterations = 3000
save_interval = 50
experiment_name = "OP3_rough"
empirical_normalization = False
policy = RslRlPpoActorCriticCfg(init_noise_std=1.0, actor_hidden_dims=[512, 256, 128], critic_hidden_dims=[512, 256, 128], activation="elu")
algorithm = RslRlPpoAlgorithmCfg(value_loss_coef=1.0, use_clipped_value_loss=True, clip_param=0.2, entropy_coef=0.01, learning_rate=1.0e-3, gamma=0.99, lam=0.95)
训练与测试
启动训练
完成所有配置后,进入 Isaac Lab 目录执行训练命令。这里以平坦地形训练为例:
./isaaclab.sh -p scripts/reinforcement_learning/rsl_rl/train.py --task Flat-OP3-train --max_iterations 3000 --seed 42 --headless
如需查看帮助参数,可运行 -h。支持多 GPU 分布式训练及 TensorBoard 日志记录。
模型测试
训练完成后,在 ~/isaaclab/logs/rsl_rl/OP3_flat 目录下会生成 .pt 权重文件。选取最后一次保存的模型进行回放测试:
./isaaclab.sh -p scripts/reinforcement_learning/rsl_rl/play.py --task Flat-OP3-Play --checkpoint /home/user/isaaclab/logs/rsl_rl/OP3_flat/2025-06-05_19-31-40/model_2999.pt
此时应能看到机器人在仿真环境中按照训练策略行走。若表现不佳,可调整奖励权重或增加训练迭代次数。
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- RSA密钥对生成器
生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
- Mermaid 预览与可视化编辑
基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online
- 随机西班牙地址生成器
随机生成西班牙地址(支持马德里、加泰罗尼亚、安达卢西亚、瓦伦西亚筛选),支持数量快捷选择、显示全部与下载。 在线工具,随机西班牙地址生成器在线工具,online
- Gemini 图片去水印
基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
- curl 转代码
解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online