iDP3的训练与部署代码解析:从数据可视化vis_dataset.py、训练脚本train.py到部署脚本deploy.py

前言

本文是接上文《iDP3的Learning代码解析:逐步分解人形策略斯坦福iDP3的数据集、模型、动作预测策略代码(包含2D和3D两个版本)》而来的

如此文《斯坦福通用人形策略iDP3——同一套策略控制各种机器人:改进3D扩散策略,不再依赖相机校准和点云分割(含DP3的详解)》的开头所说,我司正在借助iDP3做通用化改写,使得一套策略控制各种机器人

而关于iDP3的介绍「如作者所说,iDP3 是适用于任何机器人的通用 3D 视觉运动策略,且可以在没有相机校准和点云分割的情况下使用 iDP3」在之前的文章都详细分析过了「详见《此文

故本文侧重介绍

  1. iDP3的预处理、训练、部署,比如iDP3 三个核心脚本包括:
    vis_dataset.sh、train_policy.sh、deploy_policy.sh
    分别代表了可视化、训练、部署,且他们分别作为对应 py 脚本的参数设置前置环节
  2. 对应的py 脚本

且如此文前言部分的最后所说,本文依然会一如既往的保持以下几点措施

  1. 每一段待解读的代码,尽可能控制在10行以内,因为按我的经验,超过10行 看着就累了
  2. 即便有解读,贴的代码 也要逐行都有对应的注释
  3. 对于较长的代码文件,我会特意在分析代码文件之前,贴一下对应的代码结构截图
  4. 每个章节的代码文件名称都加上了对应的一句话说明

最后,本文的写就有「我司机器人方向技术合伙人姚博士」的重要贡献,且本文之外,建议进一步学习(你会得到进一步的巩固):此课程的《第十二次课 基于iDP3的umi-dexcap通用化部署(我司独创工作:对iDP3的通用化改写)》,我自己也认真看了两遍

目录

前言

第一部分 iDP3的数据可视化:vis_dataset.sh、vis_dataset.py

1.1 scripts/vis_dataset.sh

1.2 Improved-3D-Diffusion-Policy/vis_dataset.py

第二部分 iDP3的训练:train_policy.sh、train.py

2.1 scripts/train_policy.sh

2.2 Improved-3D-Diffusion-Policy/train.py:调用对应的yaml文件

2.3 管理配置库Hydra与训练流程小结

2.3.1 管理配置库Hydra:动态配置Python项目

2.3.2 训练流程:train_policy.py 从 idp3.yaml 中获取类,并实例化idp3_workspace 

第三部分 iDP3的部署:deploy_policy.sh、deploy.py

3.1 scripts/deploy_policy.sh

3.2 Improved-3D-Diffusion-Policy/deploy.py:部署全流程

3.2.1 各种库函数的应用:含通讯模块zenoh

3.2.2 类 GR1DexEnvInference:用于机器人部署

3.2.3 主函数main

3.3 Realsense L515 测试脚本


第一部分 iDP3的数据可视化:vis_dataset.sh、vis_dataset.py

1.1 scripts/vis_dataset.sh

这个 vis_dataset.sh 脚本用于可视化3D扩散策略模型的训练数据,或者如姚博士所说vis_dataset.sh 脚本调用 vis_dataset.py 文件以可视化 2D/3D 数据集

因此不需要额外写脚本,可以直接借助开源的代码直接可视化训练数据

运行方法非常简单,首先下载开源的样例数据集 training_data_example.zip 并解压「 如此文《斯坦福通用人形策略iDP3——同一套策略控制各种机器人:改进3D扩散策略,不再依赖相机校准和点云分割(含DP3的详解)》第二部分的开头所述,iDP3的训练数据示例training_data_example.zip见:training_data_example.zip」,然后对应修改脚本中的 dataset_path 为解压后文件夹位置,并更改 --vis_cloud=1,至于vis_dataset.py 中需要对应修改的部分会在下一节中讲解

具体而言

  1. 脚本的开头提供了一个示例命令,展示了如何使用该脚本
    # bash scripts/vis_dataset.sh
  2. 首先,脚本定义了数据集路径 `dataset_path`——注意,凡是这种路径设置 你都要改成你本地电脑上的实际路径,指向训练数据的示例目录
    dataset_path=/home/ze/projects/Improved-3D-Diffusion-Policy/training_data_example
  3. 接着,设置了一个变量 `vis_cloud`,用于控制是否可视化点云数据
    vis_cloud=0      # 是否可视化点云数据的标志,0 表示不可视化
  4. 然后,脚本切换到 Improved-3D-Diffusion-Policy 目录
    cd Improved-3D-Diffusion-Policy      # 切换到项目目录
    并运行 `vis_dataset.py` 脚本——下一节将详细介绍该脚本
    该脚本接受多个参数,包括数据集路径、是否使用图像、是否可视化点云、是否使用点云颜色以及是否对数据进行下采样
    # 运行 vis_dataset.py 脚本,指定数据集路径
    python vis_dataset.py --dataset_path $dataset_path \  
                        --use_img 1 \                   # 使用图像进行可视化
                        --vis_cloud ${vis_cloud} \      # 是否可视化点云数据
                        --use_pc_color 0 \              # 不使用点云颜色
                        --downsample 1 \                # 对数据进行下采样
    通过以上这些参数,`vis_dataset.py` 脚本可以根据指定的配置对数据集进行可视化,帮助用户更好地理解和分析训练数据

最终,运行效果如下图所示(来自合伙人姚博士)

25年2.27日,我自己也实际运行了下这套数据可视化的代码,没有问题


且值得一提的是,copilot确实好用,全程在VS code专业版的copilot agent模式下:下指令就行了(配套的 Claude 3.7),​电脑没有的环境自动帮我安装,需要修改的路径、代码、命令直接帮我改..

1.2 Improved-3D-Diffusion-Policy/vis_dataset.py

这个 vis_dataset.py 脚本用于可视化3D扩散策略模型的训练数据,或者如姚博士所说,vis_dataset.py 主要作用在于点云数据的可视化,并可以做一些简单的预处理

关键参数基本都在 vis_dataset.sh 中定义了,需要改动的仅以下两点:

  1. 点云图像保存位置,因为 dataset_path 被设置为了绝对路径,因此需要相应修改:
    save_dir = f"{dataset_path}/{episode_idx}"      # 设置当前集的保存目录
  2. 点云视频保存位置,对应修改:
            if vis_cloud:
                # 将图像序列转换为视频
                os.system(f"ffmpeg -r 10 -i {save_dir}/%d.png -vcodec mpeg4 -y {dataset_path}/{episode_idx}.mp4")

运行后,可生成

对原代码文件我也详细介绍下,具体而言

  1. 脚本首先导入必要的模块,包括 os、argparse 和 numpy
    import zarr                       # 导入 zarr 库,用于处理 zarr 格式的数据
    import cv2                        # 导入 OpenCV 库,用于图像处理
    from termcolor import cprint      # 导入 cprint 函数,用于彩色打印
    import time                       # 导入 time 模块,用于时间相关操作
    from tqdm import tqdm             # 导入 tqdm 库,用于显示进度条
    import visualizer                 # 导入自定义的 visualizer 模块
    import os                         # 导入 os 模块,用于操作系统相关功能
    import argparse                   # 导入 argparse 模块,用于解析命令行参数
    import numpy as np                # 导入 numpy 库,并重命名为 np
  2. 然后,使用 argparse 模块解析命令行参数
    定义了几个参数:dataset_path(数据集路径)、use_img(是否使用图像)、vis_cloud(是否可视化点云)、use_pc_color(是否使用点云颜色)和 downsample(是否对数据进行下采样)
    # 创建 ArgumentParser 对象
    parser = argparse.ArgumentParser()
    
    # 添加命令行参数
    parser.add_argument("--dataset_path", type=str, default="data/box_zarr")
    parser.add_argument("--use_img", type=int, default=0)
    parser.add_argument("--vis_cloud", type=int, default=0)
    parser.add_argument("--use_pc_color", type=int, default=0)
    parser.add_argument("--downsample", type=int, default=0)
  3. 解析参数后,脚本打开指定路径的数据集,并打印数据集的结构树
    # 解析命令行参数
    args = parser.parse_args()
    use_img = args.use_img                # 是否使用图像
    dataset_path = args.dataset_path      # 数据集路径
    vis_cloud = args.vis_cloud            # 是否可视化点云
    use_pc_color = args.use_pc_color      # 是否使用点云颜色
    downsample = args.downsample          # 是否对数据进行下采样
    
    # 打开 zarr 格式的数据集
    with zarr.open(dataset_path) as zf:
        print(zf.tree())                  # 打印数据集的结构树
    
  4. 接着,根据参数 use_img 决定是否加载图像数据,并加载所有的点云数据和元数据中的每个 episode 的结束位置

    本质上输入是img、point_cloud、state,输出是action

    # 获取数据
    if use_img:
        all_img = zf['data/img']                  # 获取所有图像数据
    all_point_cloud = zf['data/point_cloud']      # 获取所有点云数据
    all_episode_ends = zf['meta/episode_ends']    # 获取所有 episode 的结束位置
  5. 脚本遍历每个 episode,根据 `episode_ends` 将数据分割成不同的 episode
    如果是第一个 episode,则从头开始截取数据
    # 根据 episode_ends 将数据分割成不同的 episode
    for episode_idx, episode_end in enumerate(all_episode_ends):
        if episode_idx == 0:
            if use_img:
                img_episode = all_img[:episode_end]              # 获取第一个 episode 的图像数据
            point_cloud_episode = all_point_cloud[:episode_end]  # 获取第一个 episode 的点云数据
    否则,从上一个 episode 的结束位置开始截取数据
      else:
            if use_img:
                # 获取后续 episode 的图像数据
                img_episode = all_img[all_episode_ends[episode_idx-1]:episode_end]  
    
            # 获取后续 episode 的点云数据
            point_cloud_episode = all_point_cloud[all_episode_ends[episode_idx-1]:episode_end]
    然后,创建保存可视化结果的目录,并打印当前正在回放的 episode
        # 设置保存可视化结果的目录    
        save_dir = f"visualizations/{dataset_path}/{episode_idx}"  
        if vis_cloud:
            os.makedirs(save_dir, exist_ok=True)          # 创建保存目录
        cprint(f"replay episode {episode_idx}", "green")  # 打印当前正在回放的 episode
  6. 在每个 episode 中,脚本遍历每一帧的数据
        # 回放图像
        for i in range(point_cloud_episode.shape[0]):
            
            pc = point_cloud_episode[i]      # 获取当前帧的点云数据
    如果启用了下采样,则随机选择4096个点进行下采样
            # 下采样
            if downsample:
                num_points = 4096      # 下采样后的点数
                idx = np.random.choice(pc.shape[0], num_points, replace=False)  # 随机选择点
                pc = pc[idx]           # 获取下采样后的点云数据
    如果使用图像,则将图像转换为 RGB 格式并显示
            if use_img:
                img = img_episode[i]      # 获取当前帧的图像数据
                img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)  # 将图像转换为 RGB 格式
                cv2.imshow('img', img)    # 显示图像
                cv2.waitKey(1)            # 等待1毫秒
                time.sleep(0.05)          # 暂停0.05秒
    对于点云数据,如果启用了点云可视化,则根据参数决定是否使用点云颜色,并将点云数据保存为图像文件
            # if vis_cloud and i >= 50:
            if vis_cloud:
                if not use_pc_color:
                    pc = pc[:, :3]      # 如果不使用点云颜色,只保留前三列
                visualizer.visualize_pointcloud(pc, img_path=f"{save_dir}/{i}.png")  # 可视化点云并保存为图像文件
                print(f"vis cloud saved to {save_dir}/{i}.png")  # 打印保存信息
    
            print(f"frame {i}/{point_cloud_episode.shape[0]}")   # 打印当前帧的信息
  7. 最后,如果启用了点云可视化,脚本会将保存的图像文件转换为视频文件,并保存到指定目录中
       if vis_cloud:
            # 转换为视频
            os.system(f"ffmpeg -r 10 -i {save_dir}/%d.png -vcodec mpeg4 -y visualizations/{dataset_path}/{episode_idx}.mp4")      # 将图像文件转换为视频文件
    通过这些步骤,脚本可以根据配置对数据集进行可视化,帮助用户更好地理解和分析训练数据

第二部分 iDP3的训练:train_policy.sh、train.py

2.1 scripts/train_policy.sh

这个 train_policy.sh 脚本用于训练一个3D扩散策略模型

  1. 脚本的开头提供了两个示例命令,展示了如何使用该脚本
    脚本接受三个参数:算法名称 (`alg_name`)、任务名称 (`task_name`) 、附加信息 (`addition_info`)

    比如下面
    第一条示例命令是,基于iDP3算法的gr1和灵巧手3D任务
    第二条示例命令是,基于DP的gr1和灵巧手2D图像任务
    # 示例命令:
    
    #   bash scripts/train_policy.sh idp3 gr1_dex-3d 0913_example
    #   bash scripts/train_policy.sh dp_224x224_r3m gr1_dex-image 0913_example
  2. 首先,脚本定义了数据集路径 `dataset_path`,并设置了一些默认参数,如调试模式 (`DEBUG`) 和 `wandb` 模式 (`wandb_mode`)
    # 数据集路径
    dataset_path=/home/ze/projects/Improved-3D-Diffusion-Policy/training_data_example  
    
    DEBUG=False              # 调试模式
    wandb_mode=offline       # wandb 模式
  3. 接着,它从命令行参数中获取算法名称、任务名称和附加信息,并使用这些信息生成实验名称 (`exp_name`) 和运行目录 (`run_dir`)
    alg_name=${1}               # 从命令行参数获取算法名称
    task_name=${2}              # 从命令行参数获取任务名称
    config_name=${alg_name}     # 配置名称与算法名称相同
    addition_info=${3}          # 从命令行参数获取附加信息
    seed=0                      # 随机种子
    exp_name=${task_name}-${alg_name}-${addition_info}      # 生成实验名称
    run_dir="data/outputs/${exp_name}_seed${seed}"          # 生成运行目录
  4. 脚本还设置了 GPU 的 ID,并打印出使用的 GPU ID
    gpu_id=0          # GPU ID
    echo -e "\033[33mgpu id (to use): ${gpu_id}\033[0m"  # 打印使用的 GPU ID
    如果调试模式 (`DEBUG`) 为真,则不保存检查点,并打印调试模式的提示信息;
    if [ $DEBUG = True ]; then       # 如果调试模式为真
        save_ckpt=False              # 不保存检查点
    
        # wandb_mode=online          # 注释掉的 wandb 在线模式
        echo -e "\033[33mDebug mode!\033[0m"      # 打印调试模式提示信息
        echo -e "\033[33mDebug mode!\033[0m"      # 打印调试模式提示信息
        echo -e "\033[33mDebug mode!\033[0m"      # 打印调试模式提示信息
    否则,保存检查点,并打印训练模式的提示信息
    else             # 如果调试模式为假
        save_ckpt=True                           # 保存检查点
        echo -e "\033[33mTrain mode\033[0m"      # 打印训练模式提示信息
  5. 然后,脚本切换到 Improved-3D-Diffusion-Policy 目录,并设置一些环境变量,如 `HYDRA_FULL_ERROR` 和 `CUDA_VISIBLE_DEVICES`
    cd Improved-3D-Diffusion-Policy      # 切换到项目目录
    
    export HYDRA_FULL_ERROR=1                  # 设置环境变量,显示完整的 Hydra 错误信息
    export CUDA_VISIBLE_DEVICES=${gpu_id}      # 设置可见的 GPU 设备
  6. 最后,脚本运行 `train.py` 脚本——下一节即将介绍该脚本,并传递多个参数,具体如下所示
    # 运行 train.py 脚本,指定配置文件
    python train.py --config-name=${config_name}.yaml \  
                                task=${task_name} \          # 设置任务名称
                                hydra.run.dir=${run_dir} \   # 设置运行目录
                                training.debug=$DEBUG \      # 设置调试模式
                                training.seed=${seed} \      # 设置随机种子
                                training.device="cuda:0" \   # 设置训练设备为 GPU
                                exp_name=${exp_name} \       # 设置实验名称
                                logging.mode=${wandb_mode} \          # 设置日志模式
                                checkpoint.save_ckpt=${save_ckpt} \   # 设置是否保存检查点
                                task.dataset.zarr_path=$dataset_path  # 设置数据集路径
    这些参数用于配置和运行模型的训练过程

2.2 Improved-3D-Diffusion-Policy/train.py:调用对应的yaml文件

这个 Python 脚本用于训练一个3D扩散策略模型

  1. 脚本首先导入必要的模块
    """
    用法:
    训练:
    python train.py --config-name=train_diffusion_lowdim_workspace
    """
    
    # 导入 os 模块,用于操作系统相关功能
    import os  
    
    # 导入 BaseWorkspace 类
    from diffusion_policy_3d.workspace.base_workspace import BaseWorkspace  
    
    # 导入 pathlib 模块,用于路径操作
    import pathlib  
    
    # 导入 OmegaConf 模块,用于配置解析
    from omegaconf import OmegaConf  
    
    # 导入 hydra 模块,用于配置管理
    import hydra  
    
    # 导入 cprint 函数,用于彩色打印
    from termcolor import cprint  
    
    # 导入 sys 模块,用于系统相关功能
    import sys
    并设置标准输出和标准错误为行缓冲模式,以便实时输出日志信息
    # 使用行缓冲模式设置 stdout 和 stderr
    sys.stdout = open(sys.stdout.fileno(), mode='w', buffering=1)
    sys.stderr = open(sys.stderr.fileno(), mode='w', buffering=1)
  2. 接着,通过设置环境变量 `WANDB_SILENT` 为 `True` 来静默 `wandb` 的输出
    # 设置环境变量,静默 wandb 输出
    os.environ['WANDB_SILENT'] = "True"
  3. 然后,脚本使用 OmegaConf 注册了一个新的解析器 eval,允许在配置文件中执行任意的 Python 代码
    # 允许在配置中执行任意 Python 代码
    OmegaConf.register_new_resolver("eval", eval, replace=True)
  4. 接下来,脚本使用 hydra.main 装饰器定义了 main 函数,并指定了配置文件的路径
    @hydra.main(
        config_path=str(pathlib.Path(__file__).parent.joinpath(
            'diffusion_policy_3d', 'config'))      # 配置文件路径

    上面这段代码相当于指定配置文件的路径,配置文件存放在 diffusion_policy_3d/config 文件夹下——在此文《iDP3的Learning代码解析:逐步分解iDP3的数据集、模型、动作预测策略代码(包含2D和3D两个版本)》的「1.2 diffusion_policy_3d/config」已介绍
    其配合 --config-name 参数,加载特定的配置文件

  5. main 函数接受一个 OmegaConf 对象 cfg 作为参数,并立即解析配置文件中的所有解析器。然后,通过 hydra.utils.get_class 获取配置中指定的类,并实例化一个 workspace 对象
    def main(cfg: OmegaConf):
        # 立即解析配置,以便所有 ${now:} 解析器使用相同的时间
        OmegaConf.resolve(cfg)
    
        cls = hydra.utils.get_class(cfg._target_)      # 获取目标类
        workspace: BaseWorkspace = cls(cfg)            # 实例化工作空间
  6. 最后,调用 workspace.run() 方法运行训练过程
        workspace.run()      # 运行工作空间
  7. 如果脚本作为主程序运行,则调用 main 函数,开始训练过程
    if __name__ == "__main__":
        main()      # 调用 main 函数

2.3 管理配置库Hydra与训练流程小结

2.3.1 管理配置库Hydra:动态配置Python项目

为方便大家理解,特地再解释一下Hydra库

  1. 首先,iDP3 无论是 train 还是 deploy 均使用了 Hydra 这一个python 库
    Hydra 是一个用于管理复杂配置的开源框架,特别适用于需要动态配置的 Python 项目。它的主要功能是允许用户通过多种方式(如配置文件、命令行参数、环境变量等)管理和合成配置
    而 iDP3 中就是使用 Hydra 管理 yaml 配置文件
  2. Hydra 使用 YAML 语言书写配置文件。YAML是一种简洁的数据序列化格式,常用于配置文件、数据交换、日志记录等场景。通常把需要的配置写在 config.yaml 中

    在运行 application 时候,config.yaml 会自动加载
    也可以在  application 中通过命令行覆盖 config.yaml 中的值

2.3.2 训练流程:train_policy.py 从 idp3.yaml 中获取类,并实例化idp3_workspace 

最后总结一下

  1. train_policy.sh 传参了 policy、task 以及 addition_info,并设定了 DEBUG 模式
    bash scripts/train_policy.sh idp3 ur5e_leap 1228_example
  2. 此后,train_policy.py 从 idp3.yaml 中获取类,并实例化工作空间 idp3_workspace.py
    同时,hydra.main 主函数装饰器在运行  idp3_workspace.py 时候,config 会自动加载文件夹下的所有 yaml 配置文件,包括 task 和 idp3
    相当于先 idp3.yaml 然后再 idp3_workspace.py 及 gr1_dex-3d.yaml

而idp3.yaml 设置了绝大部分关键的策略配置,全逻辑可以总结为:

  1. 任务定义: 训练一个扩散模型,用于点云数据的序列生成 ->
  2. 数据编码: 使用PointNet对点云数据进行多阶段特征提取 ->
  3. 扩散过程: 基于DDIMScheduler调度生成点云动作序列 ->
  4. 训练优化: 使用AdamW优化器,通过余弦调度策略逐步降低学习率 ->
  5. 结果管理: 配置检查点保存和日志记录,确保实验过程可控

更具体的idp3.yaml介绍,详见此文《iDP3的Learning代码解析:逐步分解iDP3的数据集、模型、动作预测策略代码(包含2D和3D两个版本)》的「1.2.3 config/idp3.yaml:相当于配置文件」

第三部分 iDP3的部署:deploy_policy.sh、deploy.py

3.1 scripts/deploy_policy.sh

这个 deploy_policy.sh 脚本用于部署一个3D扩散策略模型。脚本的开头提供了两个示例命令,展示了如何使用该脚本。脚本接受三个参数:算法名称 (`alg_name`)、任务名称 (`task_name`) 和附加信息 (`addition_info`)

简言之,该脚本先后涉及:参数输入 -> 实验配置 -> 环境变量设置 -> 执行文件

具体而言

  1. 首先,脚本定义了数据集路径 `dataset_path`
    # Examples:
    
    #   bash scripts/deploy_policy.sh idp3 gr1_dex-3d 0913_example
    #   bash scripts/deploy_policy.sh dp_224x224_r3m gr1_dex-image 0913_example
    
    # $1: alg_name - 算法名称,例如 "idp3" 或 "dp_224x224_r3m"
    # $2: task_name - 任务名称,例如 "gr1_dex-3d" 或 "gr1_dex-image"
    # $3: addition_info - 附加信息,用于标记实验,例如 "0913_example"
    
    dataset_path=/home/ze/projects/Improved-3D-Diffusion-Policy/training_data_example
    并设置了一些默认参数——相当于实验配置,如调试模式 (`DEBUG`) 和是否保存检查点 (`save_ckpt`)
    DEBUG=False
    save_ckpt=True
  2. 接着,设定实验名称与目录
    即它从命令行参数中获取算法名称、任务名称和附加信息,并使用这些信息生成实验名称 (`exp_name`) 和运行目录 (`run_dir`)
    alg_name=${1}
    task_name=${2}
    config_name=${alg_name}
    addition_info=${3}
    seed=0
    exp_name=${task_name}-${alg_name}-${addition_info}
    run_dir="data/outputs/${exp_name}_seed${seed}"
    脚本还设置了 GPU 的 ID——相当于GPU设置,并打印出使用的 GPU ID
    gpu_id=0
    echo -e "\033[33mgpu id (to use): ${gpu_id}\033[0m"
  3. 然后,它切换到 Improved-3D-Diffusion-Policy 目录
    cd Improved-3D-Diffusion-Policy
    并设置一些环境变量,如 `HYDRA_FULL_ERROR` 和 `CUDA_VISIBLE_DEVICES`
    export HYDRA_FULL_ERROR=1 
    export CUDA_VISIBLE_DEVICES=${gpu_id}
  4. 最后,脚本运行 `deploy.py` 脚本——下节即将介绍,并传递多个参数,包括配置文件名称、任务名称、运行目录、调试模式、随机种子、设备、实验名称、日志模式、是否保存检查点以及数据集路径
    # 使用指定的配置文件运行 deploy.py 脚本
    python deploy.py --config-name=${config_name}.yaml \  
                                task=${task_name} \          # 设置任务名称
                                hydra.run.dir=${run_dir} \   # 设置运行目录
                                training.debug=$DEBUG \      # 设置调试模式
                                training.seed=${seed} \      # 设置随机种子
                                training.device="cuda:0" \   # 设置训练设备为 GPU
                                exp_name=${exp_name} \  # 设置实验名称
                                logging.mode=${wandb_mode} \          # 设置日志模式
                                checkpoint.save_ckpt=${save_ckpt} \   # 设置是否保存检查点
                                task.dataset.zarr_path=$dataset_path  # 设置数据集路径
    这些参数用于配置和运行模型的部署过程

如姚博士所说,需要注意以下几点

  1. wandb_mode 未在脚本中定义:需要在执行脚本前定义 wandb_mode 变量,否则会报错
  2. 路径校验:确保 dataset_path 和 Improved-3D-Diffusion-Policy 目录存在
  3. GPU 可见性:确保系统中存在指定的 GPU 设备

3.2 Improved-3D-Diffusion-Policy/deploy.py:部署全流程

作为机器人系统控制和推理部署脚本,主要基于扩散策略模型(Diffusion Policy Model)进行推理和决策,包括以下4个核心功能:

  1. 机器人控制:通过 UpperBodyCommunication 和 HandCommunication 来控制机器人的上半身关节和机械手
  2. 环境感知:使用 RealSense 相机获取深度图像、RGB图像和点云数据,提供给深度学习模型作为输入
  3. 动作推理:加载预训练的策略模型,根据实时环境输入推理出下一步的控制动作
  4. 数据记录:在执行任务的同时记录传感器数据和机器人关节状态,以便后续分析或调试

3.2.1 各种库函数的应用:含通讯模块zenoh

首先,导入机器人控制、相机控制、图像处理和扩散策略操作所需的模块和库,其中

  1. 首先
    import sys
    # 使用行缓冲模式设置 stdout 和 stderr
    sys.stdout = open(sys.stdout.fileno(), mode='w', buffering=1)
    sys.stderr = open(sys.stderr.fileno(), mode='w', buffering=1)
  2. 其次
    import hydra          # 导入 hydra 库,用于配置管理
    import time           # 导入 time 库,用于时间相关操作
    from omegaconf import OmegaConf          # 导入 OmegaConf 库,用于配置解析
    import pathlib        # 导入 pathlib 库,用于路径操作
    
    # 导入 BaseWorkspace 类
    from diffusion_policy_3d.workspace.base_workspace import BaseWorkspace  
    
    # 导入动作工具模块并重命名为 action_util    
    import diffusion_policy_3d.common.gr1_action_util as action_util      
    
    # 导入旋转工具模块并重命名为 rotation_util  
    import diffusion_policy_3d.common.rotation_util as rotation_util
    
    import tqdm           # 导入 tqdm 库,用于显示进度条
    import torch          # 导入 torch 库,用于深度学习
    import os             # 导入 os 库,用于操作系统相关功能
    os.environ['WANDB_SILENT'] = "True"      # 设置环境变量,静默 wandb 输出
    
    # 允许在配置中执行任意 Python 代码
    OmegaConf.register_new_resolver("eval", eval, replace=True)
  3. 最后
     # 导入 MultiRealSense 类
    from diffusion_policy_3d.common.multi_realsense import MultiRealSense 
    
    # 设置 zenoh 路径
    zenoh_path="/home/gr1p24ap0049/projects/gr1-dex-real/teleop-zenoh"  
     
    sys.path.append(zenoh_path)       # 将 zenoh 路径添加到系统路径中
    from communication import *       # 导入 communication 模块中的所有内容
    from retarget import ArmRetarget  # 导入 ArmRetarget 类
    
    import numpy as np      # 导入 numpy 库并重命名为 np
    import torch            # 再次导入 torch 库(可能是重复导入)
    from termcolor import cprint      # 导入 cprint 函数,用于彩色打印
    其中,zenoh 是仿人机器人通讯(UpperBody 及 Hand )相关模块(Humanoid-Teleoperation
    如合伙人姚博士所说,此模块是 iDP3 复现使用和外界通讯的关键脚本,如果想替换对应硬件,则需要更改此脚本

3.2.2 类 GR1DexEnvInference:用于机器人部署

这段代码定义了一个名为 GR1DexEnvInference 的类,用于在机器人本地计算机上运行的部署环境。该类主要负责初始化机器人环境、执行动作、获取观测数据、重置环境。其中:

  1. __init__ 方法初始化了机器人环境的各个组件
    class GR1DexEnvInference:
        """
        部署在机器人的本地计算机上运行
        """
        def __init__(self, obs_horizon=2, action_horizon=8, device="gpu",
                    use_point_cloud=True, use_image=True, img_size=224,
                     num_points=4096,
                     use_waist=False):
            
            # 观测/动作
            self.use_point_cloud = use_point_cloud      # 是否使用点云
            self.use_image = use_image            # 是否使用图像
            
            self.use_waist = use_waist            # 是否使用腰部
    比如包括相机
            # 相机
            self.camera = MultiRealSense(use_front_cam=True,  # 默认使用单个相机,但也支持多相机
                                front_num_points=num_points,
                                img_size=img_size)
    时间跨度
            # 时间跨度
            self.obs_horizon = obs_horizon            # 观测时间跨度
            self.action_horizon = action_horizon      # 动作时间跨度
    推理设备
    
            # 推理设备
            if device == "gpu":
                self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")  # 使用 GPU 或 CPU
            else:
                self.device = torch.device("cpu")  # 使用 CPU
            
    
    通信接口
            # 机器人通信
            self.upbody_comm = UpperBodyCommunication()   # 上半身通信
            self.hand_comm = HandCommunication()          # 手部通信
            self.arm_solver = ArmRetarget("AVP")          # 手臂重定向器
  2. step 方法执行一系列动作,并收集相应的观测数据
    简言之,它将动作发送到机器人,并从相机和传感器获取数据,最后返回一个包含观测数据的字典
    具体而言

    首先,方法遍历 action_list 中的每个动作,并将其转换为32个关节的格式
        def step(self, action_list):
            
            for action_id in range(self.action_horizon):
                act = action_list[action_id]      # 获取当前动作
                self.action_array.append(act)     # 将动作添加到动作数组中
                act = action_util.joint25_to_joint32(act)      # 将25关节动作转换为32关节动作
    然后,将动作分为位置和手部位置两个部分。如果不使用腰部,则将位置的前六个值设为零
                filtered_act = act.copy()          # 复制动作
                filtered_pos = filtered_act[:-12]  # 获取过滤后的位置
                filtered_handpos = filtered_act[-12:]  # 获取过滤后的手部位置
                if not self.use_waist:
                    filtered_pos[0:6] = 0.         # 如果不使用腰部,将腰部位置设为0
    接着,方法通过 upbody_comm 和 hand_comm 分别设置上半身和手部的位置
                self.upbody_comm.set_pos(filtered_pos)    # 设置上半身位置
                self.hand_comm.send_hand_cmd(filtered_handpos[6:], filtered_handpos[:6])      # 发送手部命令
    在每个动作步骤中,方法还会从相机获取点云、颜色和深度数据,并将这些数据存储在相应的数组中
                cam_dict = self.camera()  # 获取相机数据
                self.cloud_array.append(cam_dict['point_cloud'])  # 添加点云数据到数组中
                self.color_array.append(cam_dict['color'])        # 添加颜色数据到数组中
                self.depth_array.append(cam_dict['depth'])        # 添加深度数据到数组中
    同时,方法尝试获取手部的关节位置,如果失败,则使用默认值。然后,将上半身和手部的关节位置合并,并存储在环境关节位置数组中
                try:
                    hand_qpos = self.hand_comm.get_qpos()  # 获取手部位置
                except:
    
                    # 获取手部位置失败,使用默认值
                    cprint("fail to fetch hand qpos. use default.", "red")      
                    hand_qpos = np.ones(12)           # 默认手部位置为全1
    
                 # 获取环境位置
                env_qpos = np.concatenate([self.upbody_comm.get_pos(), hand_qpos]) 
                self.env_qpos_array.append(env_qpos)  # 添加环境位置到数组中
    最后,方法将最近的观测数据堆叠起来,形成一个包含代理位置、点云和图像的字典 obs_dict。这些数据被转换为 PyTorch 张量,并根据需要移动到指定的设备(如 GPU)
            agent_pos = np.stack(self.env_qpos_array[-self.obs_horizon:], axis=0)  # 获取最近的观测位置
        
            # 获取最近的点云数据
            obs_cloud = np.stack(self.cloud_array[-self.obs_horizon:], axis=0)    
            # 获取最近的图像数据
            obs_img = np.stack(self.color_array[-self.obs_horizon:], axis=0)      
                
            obs_dict = {
                # 将观测位置转换为张量
                'agent_pos': torch.from_numpy(agent_pos).unsqueeze(0).to(self.device),  
            }
    
            if self.use_point_cloud:
                # 如果用点云数据,则将点云数据转换为张量
                obs_dict['point_cloud'] = torch.from_numpy(obs_cloud).unsqueeze(0).to(self.device)  
    
            if self.use_image:
                # 如果用图像数据,则将图像数据转换为张量
                obs_dict['image'] = torch.from_numpy(obs_img).permute(0, 3, 1, 2).unsqueeze(0)  
    最终,方法返回这个包含观测数据的字典
            return obs_dict  # 返回观测字典
  3. reset 方法 重置机器人和环境,包括相机和传感器,并返回初始观测数据
    该方法接受一个布尔参数 first_init,用于指示是否是第一次初始化
    def reset(self, first_init=True):
    首先,方法初始化了一些缓冲区,包括颜色数组、深度数组、点云数组、环境关节位置数组和动作数组
            # 初始化缓冲区
            self.color_array, self.depth_array, self.cloud_array = [], [], []  # 初始化颜色、深度和点云数组
            self.env_qpos_array = []        # 初始化环境关节位置数组
            self.action_array = []          # 初始化动作数组
    接着,定义了两个初始关节位置数组 qpos_init1 和 qpos_init2,以及一个手部初始位置数组 hand_init
            # 位置初始化
            qpos_init1 = np.array([-np.pi / 12, 0, 0, -1.6, 0, 0, 0, 
                -np.pi / 12, 0, 0, -1.6, 0, 0, 0])          # 初始关节位置1
            qpos_init2 = np.array([-np.pi / 12, 0, 1.5, -1.6, 0, 0, 0, 
                    -np.pi / 12, 0, -1.5, -1.6, 0, 0, 0])   # 初始关节位置2
            hand_init = np.ones(12)          # 初始手部位置为全1
            # hand_init = np.ones(12) * 0    # 初始手部位置为全0(注释掉)
    如果 first_init 为真,方法会将 qpos_init2 连接起来,并通过 upbody_comm 和hand_comm 分别设置上半身和手部的位置
            if first_init:
                # ======== 初始化 ==========
                upbody_initpos = np.concatenate([qpos_init2])      # 连接初始关节位置2
                self.upbody_comm.init_set_pos(upbody_initpos)      # 设置上半身初始位置
                self.hand_comm.send_hand_cmd(hand_init[6:], hand_init[:6])  # 发送手部初始命令
    然后,将 qpos_init1 连接起来,并再次设置上半身的位置
            upbody_initpos = np.concatenate([qpos_init1])  # 连接初始关节位置1
            self.upbody_comm.init_set_pos(upbody_initpos)  # 设置上半身初始位置
            q_14d = upbody_initpos.copy()  # 复制上半身初始位置
                
            body_action = np.zeros(6)      # 初始化身体动作为全0
    接着,方法使用逆运动学(IK)求解器 arm_solver 对齐末端执行器(EEF)的位置,并设置上半身的位置
            # 末端执行器位置对齐
            arm_pos, arm_rot_quat = action_util.init_arm_pos, action_util.init_arm_quat  # 获取手臂初始位置和旋转四元数
            q_14d = self.arm_solver.ik(q_14d, arm_pos, arm_rot_quat)  # 通过逆运动学计算手臂位置
            self.upbody_comm.init_set_pos(q_14d)      # 设置上半身位置
    在完成这些初始化步骤后,方法暂停两秒钟,并打印 "Robot ready!" 以指示机器人已准备好
            time.sleep(2)  # 等待2秒
            
            print("Robot ready!")  # 打印机器人准备好信息
    然后,方法从相机获取颜色、深度和点云数据,并将这些数据存储在相应的数组中
            # ======== 初始化相机 ==========
            cam_dict = self.camera()      # 获取相机数据
            self.color_array.append(cam_dict['color'])      # 添加颜色数据到数组中
            self.depth_array.append(cam_dict['depth'])      # 添加深度数据到数组中
            self.cloud_array.append(cam_dict['point_cloud'])  # 添加点云数据到数组中
    接着,方法尝试获取手部的关节位置,如果失败,则使用默认值
            try:
                hand_qpos = self.hand_comm.get_qpos()      # 获取手部位置
            except:
                cprint("fail to fetch hand qpos. use default.", "red")  # 获取手部位置失败,使用默认值
                hand_qpos = np.ones(12)      # 默认手部位置为全1
    然后,将上半身和手部的关节位置合并,并存储在环境关节位置数组中
            env_qpos = np.concatenate([self.upbody_comm.get_pos(), hand_qpos])  # 获取环境位置
            self.env_qpos_array.append(env_qpos)      # 添加环境位置到数组中
                            
            self.q_14d = q_14d                  # 设置上半身位置
            self.body_action = body_action      # 设置身体动作
    最后,方法将最近的观测数据堆叠起来,形成一个包含代理位置、点云和图像的字典 obs_dict
    这些数据被转换为 PyTorch 张量,并根据需要移动到指定的设备(如 GPU)

    最终,方法返回这个包含观测数据的字典

通过以上这些方法,可以控制机器人执行任务并收集相关数据

3.2.3 主函数main

简言之,这是整个程序的核心入口,用于初始化配置、加载模型和环境,执行推理控制循环,并记录运行数据

  1. 初始化配置:使用 Hydra 动态加载配置文件和策略模型,解析任务相关的参数;设置随机种子,确保推理结果可重复
  2. 加载模型和环境:创建环境对象,设定观测和动作的时间步长;加载策略模型,用于从观测数据中生成动作
  3. 执行推理控制循环:根据不同的任务类型(grasp, pour, wipe)设定推理执行的步数,初始化环境 (GR1DexEnvInference) 并重置机器人状态,循环执行推理和动作控制,调用策略模型生成动作,执行动作并更新环境观察值,持续到设定的 roll_out_length 为止
  4. 记录运行数据(可选):如果 record_data=True,将采集到的数据保存到 HDF5 文件中,并支持用户选择文件重命名

具体而言

  1. 首先,做Hydra 配置管理
    Hydra 用于从配置文件中加载参数
    config_path 指定了配置文件的路径,位于 diffusion_policy_3d/config 目录下——同样的,在此文《iDP3的Learning代码解析:逐步分解人形策略斯坦福iDP3的数据集、模型、动作预测策略代码(包含2D和3D两个版本)》的「1.2 diffusion_policy_3d/config」已介绍
    函数的参数 cfg 是 OmegaConf 对象,包含了从配置文件加载的所有配置信息
    @hydra.main(
        config_path=str(pathlib.Path(__file__).parent.joinpath(
            'diffusion_policy_3d','config'))      # 配置文件路径
    )
    
    def main(cfg: OmegaConf):
        # 使用 Hydra 管理配置,加载指定路径下的配置文件
        # config_path:配置文件所在目录,定义了程序的各类参数
        # cfg:OmegaConf 对象,包含从配置文件加载的所有参数
  2. 设置随机种子和解析配置
    torch.manual_seed(42):设置 PyTorch 的随机种子,保证实验结果具有可重复性
        torch.manual_seed(42)  # 设置随机种子
    OmegaConf.resolve(cfg):解析配置文件中的动态变量(如 ${now:}、${eval:}),并替换为实际值
        # 立即解析配置,以便所有 ${now:} 解析器使用相同的时间
        OmegaConf.resolve(cfg)
  3. 加载工作区对象
    cfg._target_:配置文件中定义的类路径(字符串形式),通过 hydra.utils.get_class 动态加载相应的类
        cls = hydra.utils.get_class(cfg._target_)      # 获取目标类
    workspace:BaseWorkspace 类的实例,封装了与策略模型加载和环境交互相关的逻辑
        workspace: BaseWorkspace = cls(cfg)            # 实例化工作空间
  4. 判断输入类型
    根据 workspace 的类名判断当前任务类型:如果是 DPWorkspace,则任务需要图像输入,不使用点云;如果是其他类型,则任务需要点云输入,不使用图像
        if workspace.__class__.__name__ == 'DPWorkspace':
            use_image = True      # 使用图像
            use_point_cloud = False      # 不使用点云
        else:
            use_image = False     # 不使用图像
            use_point_cloud = True       # 使用点云
  5. 加载策略模型和推理参数
        # 获取策略模型
        policy = workspace.get_model()
        action_horizon = policy.horizon - policy.n_obs_steps + 1   # 动作时间跨度
    其中
    policy:从 workspace 加载策略模型,用于根据观察值生成动作
    action_horizon:实际执行的动作时间步长
    policy.horizon:策略模型的预测时间范围
    policy.n_obs_steps:策略模型需要的观察步数
  6. 设置任务参数
    任务类型包括 pour(倒液体)、grasp(抓取)、wipe(擦拭)
    根据任务类型设置 roll_out_length,即推理和控制的总步数
        # 任务配置
        roll_out_length_dict = {
            "pour": 300,
            "grasp": 1000,
            "wipe": 300,
        }
    
        # task = "wipe"
        task = "grasp"          # 任务类型——grasp
    
        # task = "pour"
        roll_out_length = roll_out_length_dict[task]      # 任务执行长度
        
        img_size = 224          # 图像大小
        num_points = 4096       # 点云数量
        use_waist = True        # 是否使用腰部
        first_init = True       # 是否第一次初始化
        record_data = True      # 是否记录数据
  7. 初始化环境
        env = GR1DexEnvInference(obs_horizon=2, action_horizon=action_horizon, device="cpu",
                                 use_point_cloud=use_point_cloud,
                                 use_image=use_image,
                                 img_size=img_size,
                                 num_points=num_points,
                                 use_waist=use_waist)    # 初始化环境
    
        
        obs_dict = env.reset(first_init=first_init)      # 重置环境
    其中
    obs_horizon=2:设置观察的时间步长
    action_horizon=action_horizon:设置动作的时间步长
    device="cpu":使用 CPU 进行推理
    use_point_cloud 和 use_image:控制是否使用点云或图像输入

    最后,调用 reset 方法初始化机器人和传感器数据,返回初始观测字典 obs_dict
  8. 推理和控制循环(主循环)
        step_count = 0  # 步数计数器
        
        while step_count < roll_out_length:
            with torch.no_grad():              # 禁用梯度计算
                action = policy(obs_dict)[0]      # 获取动作
    
                # 将动作转换为 numpy 数组
                action_list = [act.numpy() for act in action]      
            
            obs_dict = env.step(action_list)      # 执行动作
            step_count += action_horizon       # 更新步数计数器
            print(f"step: {step_count}")       # 打印当前步数
    相当于
    每次迭代中,调用策略模型生成动作
    使用 env.step 执行动作,并采集新的观测数据
    将步数增加 action_horizon,直到达到 roll_out_length
    禁用梯度计算:torch.no_grad() 减少内存消耗,提高推理速度
  9. 数据记录
        if record_data:
            # 导入 h5py 库
            import h5py  
    
            root_dir = "/home/gr1p24ap0049/projects/gr1-learning-real/"  # 根目录
            save_dir = root_dir + "deploy_dir"        # 保存目录
            os.makedirs(save_dir, exist_ok=True)      # 创建保存目录
            
            record_file_name = f"{save_dir}/demo.h5"      # 记录文件名
            color_array = np.array(env.color_array)       # 颜色数组
            depth_array = np.array(env.depth_array)       # 深度数组
            cloud_array = np.array(env.cloud_array)       # 点云数组
            qpos_array = np.array(env.qpos_array)         # 关节位置数组
    
            with h5py.File(record_file_name, "w") as f:   # 创建 h5 文件
                f.create_dataset("color", data=np.array(color_array))    # 创建颜色数据集
                f.create_dataset("depth", data=np.array(depth_array))    # 创建深度数据集
                f.create_dataset("cloud", data=np.array(cloud_array))    # 创建点云数据集
                f.create_dataset("qpos", data=np.array(qpos_array))      # 创建关节位置数据集
    以上相当于
    使用 h5py 将采集的数据保存到 HDF5 文件中
    数据包括 RGB 图像、深度图、点云和机器人关节状态
    默认保存到 demo.h5
    如果用户选择重命名,会更改文件名
  10. 文件重命名
            choice = input("whether to rename: y/n")  # 是否重命名文件
            if choice == "y":
                renamed = input("file rename:")       # 输入新文件名
    
                # 重命名文件
                os.rename(src=record_file_name, dst=record_file_name.replace("demo.h5", renamed+'.h5'))      
                new_name = record_file_name.replace("demo.h5", renamed+'.h5')   # 新文件名
                cprint(f"save data at step: {roll_out_length} in {new_name}", "yellow")  # 
            else:
                cprint(f"save data at step: {roll_out_length} in {record_file_name}", "yellow")
    以上相当于
    提示用户是否需要重命名文件
    如果选择 y,则输入新的文件名,完成重命名
    输出保存文件的最终路径

3.3 Realsense L515 测试脚本

iDP3 采用了 realsense L515 激光雷达相机,但是并没有相关测试脚本,这一点 UMI 做的相对舒服一些,方便问题排查,因此姚博士在复现过程中写了一些测试脚本,开源方便大家复现少点麻烦

首先安装 python api,此处注意务必采用推荐的 pyrealsense2==2.54.2.5684 版本,因为 L515 作为一款老产品新版本未经过适配测试

pip install pyrealsense2==2.54.2.5684
pip install opencv-python

然后,在 idp3 环境中运行以下脚本,此脚本根据 realsense 官方文档进行了修改,设置了三个窗口显示:

  1. RGB图像
  2. 伪彩色深度图像
  3. 原始灰度深度图像

以下是部分代码「完整代码将放在七月官网首页的具身智能复现实战训练营 第二期中」

  1. 创建三个窗口
    cv2.namedWindow("RealSense Color", cv2.WINDOW_AUTOSIZE)
    cv2.namedWindow("RealSense Depth (Color Map)", cv2.WINDOW_AUTOSIZE)
    cv2.namedWindow("RealSense Depth (Raw)", cv2.WINDOW_AUTOSIZE)
    其中
    RealSense Color: 显示RGB图像
    RealSense Depth (Color Map): 显示伪彩色处理后的深度图像
    RealSense Depth (Raw): 显示原始深度图像
  2. 其次
    
    try:
        while True:
            # 等待获取新的一帧
            frames = pipeline.wait_for_frames()
     
            # 获取深度和颜色图像帧
            depth_frame = frames.get_depth_frame()
            color_frame = frames.get_color_frame()
     
            if not depth_frame or not color_frame:
                continue
     
            # 转换图像为numpy数组
            depth_image = np.asanyarray(depth_frame.get_data())
            color_image = np.asanyarray(color_frame.get_data())
  3. 伪彩色深度图像
    
            # 对深度图像进行伪彩色处理,使其更容易查看
            depth_colormap = cv2.applyColorMap(cv2.convertScaleAbs(depth_image, alpha=0.03), cv2.COLORMAP_JET)
  4. 显示RGB图像、伪彩色深度图像和原始深度图像
    
            # 使用cv2.imshow()来显示RGB图像、伪彩色深度图像和原始深度图像
            cv2.imshow("RealSense Color", color_image)
            cv2.imshow("RealSense Depth (Color Map)", depth_colormap)
            cv2.imshow("RealSense Depth (Raw)", depth_image)
  5. 最后
            # 等待按键,按“q”退出
            key = cv2.waitKey(1)
            if key == ord('q'):
                break
    finally:
        # 停止pipeline
        pipeline.stop()
        # 关闭所有OpenCV窗口
        cv2.destroyAllWindows()

运行该程序后弹出三个窗口,以通过按 “q” 键退出程序(下图来自合伙人姚博士)

至于更多我司对iDP3通用化的改写,后续发布或见七月官网首页的具身智能复现实战训练营

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

v_JULY_v

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值