Go2 DDS bridge is added.
This commit is contained in:
parent
a5749756c4
commit
030326f0b4
|
@ -0,0 +1,354 @@
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import torch
|
||||||
|
import torch.nn as nn
|
||||||
|
import torch.nn.functional as F
|
||||||
|
|
||||||
|
import onnxruntime
|
||||||
|
from models import model_utils
|
||||||
|
|
||||||
|
class ActorStochasticMLP(nn.Module):
|
||||||
|
def __init__(self):
|
||||||
|
super(ActorStochasticMLP, self).__init__()
|
||||||
|
|
||||||
|
self.hidden_size = 256
|
||||||
|
obs_dim = 49
|
||||||
|
action_dim = 12
|
||||||
|
self.gru = nn.GRU(obs_dim, self.hidden_size, batch_first=True)
|
||||||
|
self.layer_dims = [obs_dim + self.hidden_size] + [256, 128, 64] + [action_dim]
|
||||||
|
|
||||||
|
init_ = lambda m: model_utils.init(m, nn.init.orthogonal_, lambda x: nn.init.
|
||||||
|
constant_(x, 0), np.sqrt(2))
|
||||||
|
|
||||||
|
modules = []
|
||||||
|
for i in range(len(self.layer_dims) - 1):
|
||||||
|
modules.append(nn.Linear(self.layer_dims[i], self.layer_dims[i + 1]))
|
||||||
|
if i < len(self.layer_dims) - 2:
|
||||||
|
modules.append(model_utils.get_activation_func('elu'))
|
||||||
|
modules.append(torch.nn.LayerNorm(self.layer_dims[i+1]))
|
||||||
|
else:
|
||||||
|
modules.append(model_utils.get_activation_func('identity'))
|
||||||
|
|
||||||
|
self.mu_net = nn.Sequential(*modules)
|
||||||
|
|
||||||
|
logstd = -1.0
|
||||||
|
|
||||||
|
self.logstd = torch.nn.Parameter(torch.ones(action_dim, dtype=torch.float32) * logstd)
|
||||||
|
|
||||||
|
self.action_dim = action_dim
|
||||||
|
self.obs_dim = obs_dim
|
||||||
|
|
||||||
|
self.min_logstd = -1.427
|
||||||
|
|
||||||
|
print(self.mu_net)
|
||||||
|
print(self.logstd)
|
||||||
|
|
||||||
|
def forward(self, obs, l):
|
||||||
|
out, time_latent = self.gru(obs, l)
|
||||||
|
x = torch.cat([obs, out], dim=-1)
|
||||||
|
|
||||||
|
mu = self.mu_net(x)
|
||||||
|
|
||||||
|
return mu, time_latent
|
||||||
|
|
||||||
|
class Policy:
|
||||||
|
def __init__(self, checkpoint_path):
|
||||||
|
self.agent = ActorStochasticMLP()
|
||||||
|
agent_init, _, _, self.obs_rms, _, _ = torch.load(checkpoint_path, map_location="cpu")
|
||||||
|
self.agent.load_state_dict(agent_init.state_dict())
|
||||||
|
onnx_file_name = checkpoint_path.replace(".pt", ".onnx")
|
||||||
|
self.actor_hidden_in = np.zeros((1, 256), dtype=np.float32)
|
||||||
|
|
||||||
|
dummy_input = torch.randn(1, 49)
|
||||||
|
with torch.no_grad():
|
||||||
|
torch_out, torch_hidden_out = self.agent(dummy_input, l = torch.tensor(self.actor_hidden_in))
|
||||||
|
|
||||||
|
torch.onnx.export(
|
||||||
|
self.agent, # The model being converted
|
||||||
|
(dummy_input, torch.tensor(self.actor_hidden_in)), # An example input for the model
|
||||||
|
onnx_file_name, # Output file name
|
||||||
|
export_params=True, # Store trained parameter weights inside the model file
|
||||||
|
opset_version=11, # ONNX version (opset) to export to, adjust as needed
|
||||||
|
do_constant_folding=True, # Whether to perform constant folding optimization
|
||||||
|
input_names=['input', 'hidden_in'], # Name of the input in the ONNX graph (can be customized)
|
||||||
|
output_names=['action', 'hidden_out'], # Name of the output (assuming get_action and get_value are key outputs)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.ort_session = onnxruntime.InferenceSession(onnx_file_name, providers=["CPUExecutionProvider"])
|
||||||
|
ort_inputs = {'input': dummy_input.numpy(), 'hidden_in': self.actor_hidden_in}
|
||||||
|
ort_outs = self.ort_session.run(None, ort_inputs)
|
||||||
|
action, hidden_out = ort_outs[0], ort_outs[1]
|
||||||
|
np.testing.assert_allclose(torch_out.numpy(), action, rtol=1e-03, atol=1e-05)
|
||||||
|
np.testing.assert_allclose(torch_hidden_out.numpy(), hidden_out, rtol=1e-03, atol=1e-05)
|
||||||
|
print("Exported model has been tested with ONNXRuntime, and the result looks good!")
|
||||||
|
|
||||||
|
def __call__(self, obs, info):
|
||||||
|
obs = torch.cat(
|
||||||
|
[torch.zeros((1, 3)), torch.tensor(obs[np.newaxis]), torch.zeros((1, 9))],
|
||||||
|
dim = -1
|
||||||
|
)
|
||||||
|
obs = self.obs_rms.normalize(obs)[:, 3:-9].numpy().astype(np.float32)
|
||||||
|
ort_inputs = {'input': obs, 'hidden_in': self.actor_hidden_in}
|
||||||
|
ort_outs = self.ort_session.run(None, ort_inputs)
|
||||||
|
self.actor_hidden_in = ort_outs[1]
|
||||||
|
return ort_outs[0]
|
||||||
|
|
||||||
|
class CommandInterface:
|
||||||
|
def __init__(self, limits=None):
|
||||||
|
self.limits = limits
|
||||||
|
self.x_vel_cmd, self.y_vel_cmd, self.yaw_vel_cmd = 0.0, 0.0, 0.0
|
||||||
|
|
||||||
|
def get_command(self):
|
||||||
|
command = np.zeros((3,))
|
||||||
|
command[0] = self.x_vel_cmd
|
||||||
|
command[1] = self.y_vel_cmd
|
||||||
|
command[2] = self.yaw_vel_cmd
|
||||||
|
return command, False
|
||||||
|
|
||||||
|
|
||||||
|
class NeuroDiffAgent:
|
||||||
|
def __init__(self, command_profile, robot):
|
||||||
|
self.robot = robot
|
||||||
|
self.command_profile = command_profile
|
||||||
|
# self.lcm_bridge = LCMBridgeClient(robot_name=self.robot_name)
|
||||||
|
self.sim_dt = 0.001
|
||||||
|
self.decimation = 20
|
||||||
|
self.dt = self.sim_dt * self.decimation
|
||||||
|
self.timestep = 0
|
||||||
|
|
||||||
|
self.device = "cpu"
|
||||||
|
|
||||||
|
joint_names = [
|
||||||
|
"FL_hip_joint",
|
||||||
|
"FL_thigh_joint",
|
||||||
|
"FL_calf_joint",
|
||||||
|
"FR_hip_joint",
|
||||||
|
"FR_thigh_joint",
|
||||||
|
"FR_calf_joint",
|
||||||
|
"RL_hip_joint",
|
||||||
|
"RL_thigh_joint",
|
||||||
|
"RL_calf_joint",
|
||||||
|
"RR_hip_joint",
|
||||||
|
"RR_thigh_joint",
|
||||||
|
"RR_calf_joint",
|
||||||
|
]
|
||||||
|
|
||||||
|
policy_joint_names = joint_names
|
||||||
|
|
||||||
|
unitree_joint_names = [
|
||||||
|
"FR_hip_joint",
|
||||||
|
"FR_thigh_joint",
|
||||||
|
"FR_calf_joint",
|
||||||
|
"FL_hip_joint",
|
||||||
|
"FL_thigh_joint",
|
||||||
|
"FL_calf_joint",
|
||||||
|
"RR_hip_joint",
|
||||||
|
"RR_thigh_joint",
|
||||||
|
"RR_calf_joint",
|
||||||
|
"RL_hip_joint",
|
||||||
|
"RL_thigh_joint",
|
||||||
|
"RL_calf_joint",
|
||||||
|
]
|
||||||
|
|
||||||
|
policy_to_unitree_map = []
|
||||||
|
unitree_to_policy_map = []
|
||||||
|
for i, policy_joint_name in enumerate(policy_joint_names):
|
||||||
|
id = np.where([name == policy_joint_name for name in unitree_joint_names])[0][0]
|
||||||
|
policy_to_unitree_map.append((i, id))
|
||||||
|
self.policy_to_unitree_map = np.array(policy_to_unitree_map).astype(np.uint32)
|
||||||
|
|
||||||
|
for i, unitree_joint_name in enumerate(unitree_joint_names):
|
||||||
|
id = np.where([name == unitree_joint_name for name in policy_joint_names])[0][0]
|
||||||
|
unitree_to_policy_map.append((i, id))
|
||||||
|
self.unitree_to_policy_map = np.array(unitree_to_policy_map).astype(np.uint32)
|
||||||
|
|
||||||
|
default_joint_angles = {
|
||||||
|
"FL_hip_joint": 0.1,
|
||||||
|
"RL_hip_joint": 0.1,
|
||||||
|
"FR_hip_joint": -0.1,
|
||||||
|
"RR_hip_joint": -0.1,
|
||||||
|
"FL_thigh_joint": 0.8,
|
||||||
|
"RL_thigh_joint": 1.0,
|
||||||
|
"FR_thigh_joint": 0.8,
|
||||||
|
"RR_thigh_joint": 1.0,
|
||||||
|
"FL_calf_joint": -1.5,
|
||||||
|
"RL_calf_joint": -1.5,
|
||||||
|
"FR_calf_joint": -1.5,
|
||||||
|
"RR_calf_joint": -1.5
|
||||||
|
}
|
||||||
|
self.default_dof_pos = np.array(
|
||||||
|
[
|
||||||
|
default_joint_angles[name]
|
||||||
|
for name in joint_names
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.default_dof_pos = self.default_dof_pos
|
||||||
|
|
||||||
|
self.p_gains = np.zeros(12)
|
||||||
|
self.d_gains = np.zeros(12)
|
||||||
|
for i in range(12):
|
||||||
|
self.p_gains[i] = 20.0
|
||||||
|
self.d_gains[i] = 0.5
|
||||||
|
|
||||||
|
print(f"p_gains: {self.p_gains}")
|
||||||
|
|
||||||
|
self.commands = np.zeros(3)
|
||||||
|
self.actions = np.zeros((1, 12))
|
||||||
|
self.last_actions = np.zeros((1,12))
|
||||||
|
self.gravity_vector = np.zeros(3)
|
||||||
|
self.dof_pos = np.zeros(12)
|
||||||
|
self.dof_vel = np.zeros(12)
|
||||||
|
self.pos = np.zeros(3)
|
||||||
|
self.body_linear_vel = np.zeros(3)
|
||||||
|
self.body_angular_vel = np.zeros(3)
|
||||||
|
self.joint_pos_target = np.zeros(12)
|
||||||
|
self.joint_vel_target = np.zeros(12)
|
||||||
|
self.prev_joint_acc = None
|
||||||
|
self.torques = np.zeros(12)
|
||||||
|
self.contact_state = np.ones(4)
|
||||||
|
self.foot_contact_forces_mag = np.zeros(4)
|
||||||
|
self.prev_foot_contact_forces_mag = np.zeros(4)
|
||||||
|
self.test = 0
|
||||||
|
|
||||||
|
self.gait_indices = np.zeros(1, dtype=np.float32)
|
||||||
|
self.clock_inputs = np.zeros(4, dtype=np.float32)
|
||||||
|
|
||||||
|
def wait_for_state(self):
|
||||||
|
# return self.lcm_bridge.getStates(timeout=2)
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_obs(self):
|
||||||
|
cmds, reset_timer = self.command_profile.get_command()
|
||||||
|
self.commands[:] = cmds
|
||||||
|
|
||||||
|
# self.state = self.wait_for_state()
|
||||||
|
joint_state = self.robot.getJointStates()
|
||||||
|
if joint_state is not None:
|
||||||
|
self.gravity_vector = self.robot.getGravityInBody()
|
||||||
|
self.prev_dof_pos = self.dof_pos.copy()
|
||||||
|
self.dof_pos = np.array(joint_state['q'])[self.unitree_to_policy_map[:, 1]]
|
||||||
|
self.prev_dof_vel = self.dof_vel.copy()
|
||||||
|
self.dof_vel = np.array(joint_state['dq'])[self.unitree_to_policy_map[:, 1]]
|
||||||
|
self.body_angular_vel = self.robot.getIMU()["gyro"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.foot_contact_forces_mag = self.robot.getFootContact()
|
||||||
|
self.body_linear_vel = self.robot.getLinVel()
|
||||||
|
self.pos, _ = self.robot.getPose()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
ob = np.concatenate(
|
||||||
|
(
|
||||||
|
self.body_angular_vel * 0.25,
|
||||||
|
self.commands * np.array([2.0, 2.0, 0.25]),
|
||||||
|
self.gravity_vector[:, 0],
|
||||||
|
self.dof_pos * 1.0,
|
||||||
|
#((self.dof_pos - self.prev_dof_pos) / self.dt) * 0.05,
|
||||||
|
self.dof_vel * 0.05,
|
||||||
|
self.last_actions[0],
|
||||||
|
self.clock_inputs
|
||||||
|
),
|
||||||
|
axis=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
#return torch.tensor(ob, device=self.device).float()
|
||||||
|
return ob
|
||||||
|
|
||||||
|
def publish_action(self, action, hard_reset=False):
|
||||||
|
# command_for_robot = UnitreeLowCommand()
|
||||||
|
#self.joint_pos_target = (
|
||||||
|
# action[0, :12].detach().cpu().numpy() * 0.25
|
||||||
|
#).flatten()
|
||||||
|
self.joint_pos_target = (
|
||||||
|
action[0, :12] * 0.25
|
||||||
|
).flatten()
|
||||||
|
self.joint_pos_target += self.default_dof_pos
|
||||||
|
self.joint_vel_target = np.zeros(12)
|
||||||
|
# command_for_robot.q_des = self.joint_pos_target
|
||||||
|
# command_for_robot.dq_des = self.joint_vel_target
|
||||||
|
# command_for_robot.kp = self.p_gains
|
||||||
|
# command_for_robot.kd = self.d_gains
|
||||||
|
# command_for_robot.tau_ff = np.zeros(12)
|
||||||
|
if hard_reset:
|
||||||
|
command_for_robot.id = -1
|
||||||
|
|
||||||
|
self.torques = (self.joint_pos_target - self.dof_pos) * self.p_gains + (
|
||||||
|
self.joint_vel_target - self.dof_vel
|
||||||
|
) * self.d_gains
|
||||||
|
# self.lcm_bridge.sendCommands(command_for_robot)
|
||||||
|
self.robot.setCommands(self.joint_pos_target[self.policy_to_unitree_map[:, 1]],
|
||||||
|
self.joint_vel_target[self.policy_to_unitree_map[:, 1]],
|
||||||
|
self.p_gains[self.policy_to_unitree_map[:, 1]],
|
||||||
|
self.d_gains[self.policy_to_unitree_map[:, 1]],
|
||||||
|
np.zeros(12))
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
self.actions = torch.zeros((1, 12))
|
||||||
|
self.time = time.time()
|
||||||
|
self.timestep = 0
|
||||||
|
return self.get_obs()
|
||||||
|
|
||||||
|
def step(self, actions, hard_reset=False):
|
||||||
|
self.last_actions = self.actions[:]
|
||||||
|
self.actions = actions
|
||||||
|
self.publish_action(self.actions, hard_reset=hard_reset)
|
||||||
|
# time.sleep(max(self.dt - (time.time() - self.time), 0))
|
||||||
|
# if self.timestep % 100 == 0:
|
||||||
|
# print(f"frq: {1 / (time.time() - self.time)} Hz")
|
||||||
|
self.time = time.time()
|
||||||
|
#obs = self.get_obs()
|
||||||
|
|
||||||
|
joint_acc = np.abs(self.prev_dof_vel - self.dof_vel) / self.dt
|
||||||
|
if self.prev_joint_acc is None:
|
||||||
|
self.prev_joint_acc = np.zeros_like(joint_acc)
|
||||||
|
joint_jerk = np.abs(self.prev_joint_acc - joint_acc) / self.dt
|
||||||
|
self.prev_joint_acc = joint_acc.copy()
|
||||||
|
|
||||||
|
# clock accounting
|
||||||
|
frequencies = 3.
|
||||||
|
phases = 0.5
|
||||||
|
offsets = 0.
|
||||||
|
bounds = 0
|
||||||
|
self.gait_indices = np.remainder(
|
||||||
|
self.gait_indices + self.dt * frequencies, 1.0
|
||||||
|
)
|
||||||
|
|
||||||
|
self.foot_indices = [
|
||||||
|
self.gait_indices + phases + offsets + bounds,
|
||||||
|
self.gait_indices + offsets,
|
||||||
|
self.gait_indices + bounds,
|
||||||
|
self.gait_indices + phases,
|
||||||
|
]
|
||||||
|
|
||||||
|
self.clock_inputs[0] = np.sin(2 * np.pi * self.foot_indices[0])
|
||||||
|
self.clock_inputs[1] = np.sin(2 * np.pi * self.foot_indices[1])
|
||||||
|
self.clock_inputs[2] = np.sin(2 * np.pi * self.foot_indices[2])
|
||||||
|
self.clock_inputs[3] = np.sin(2 * np.pi * self.foot_indices[3])
|
||||||
|
|
||||||
|
foot_contact_rate = np.abs(self.foot_contact_forces_mag - self.prev_foot_contact_forces_mag)
|
||||||
|
self.prev_foot_contact_forces_mag = self.foot_contact_forces_mag.copy()
|
||||||
|
|
||||||
|
infos = {
|
||||||
|
"joint_pos": self.dof_pos[np.newaxis, :],
|
||||||
|
"joint_vel": self.dof_vel[np.newaxis, :],
|
||||||
|
"joint_pos_target": self.joint_pos_target[np.newaxis, :],
|
||||||
|
"joint_vel_target": self.joint_vel_target[np.newaxis, :],
|
||||||
|
"body_linear_vel": self.body_linear_vel[np.newaxis, :],
|
||||||
|
"body_angular_vel": self.body_angular_vel[np.newaxis, :],
|
||||||
|
"body_pos": self.pos[np.newaxis, :].copy(),
|
||||||
|
"contact_state": self.contact_state[np.newaxis, :],
|
||||||
|
"body_linear_vel_cmd": self.commands[np.newaxis, 0:2],
|
||||||
|
"body_angular_vel_cmd": self.commands[np.newaxis, 2:],
|
||||||
|
"torques": self.torques,
|
||||||
|
"foot_contact_forces_mag": self.foot_contact_forces_mag.copy(),
|
||||||
|
"joint_acc": joint_acc[np.newaxis, :],
|
||||||
|
"joint_jerk": joint_jerk[np.newaxis, :],
|
||||||
|
"foot_contact_rate": foot_contact_rate[np.newaxis, :],
|
||||||
|
}
|
||||||
|
|
||||||
|
self.timestep += 1
|
||||||
|
return None, None, None, infos
|
|
@ -0,0 +1,329 @@
|
||||||
|
# Copyright 2008 Willow Garage, Inc.
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
#
|
||||||
|
# * Redistributions of source code must retain the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer.
|
||||||
|
#
|
||||||
|
# * Redistributions in binary form must reproduce the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer in the
|
||||||
|
# documentation and/or other materials provided with the distribution.
|
||||||
|
#
|
||||||
|
# * Neither the name of the Willow Garage, Inc. nor the names of its
|
||||||
|
# contributors may be used to endorse or promote products derived from
|
||||||
|
# this software without specific prior written permission.
|
||||||
|
#
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||||
|
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
# POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
Serialization of sensor_msgs.PointCloud2 messages.
|
||||||
|
|
||||||
|
Author: Tim Field
|
||||||
|
ROS 2 port by Sebastian Grans
|
||||||
|
File originally ported from:
|
||||||
|
https://github.com/ros/common_msgs/blob/f48b00d43cdb82ed9367e0956db332484f676598/
|
||||||
|
sensor_msgs/src/sensor_msgs/point_cloud2.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import array
|
||||||
|
from collections import namedtuple
|
||||||
|
import sys
|
||||||
|
from typing import Iterable, List, NamedTuple, Optional
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
try:
|
||||||
|
from numpy.lib.recfunctions import (structured_to_unstructured, unstructured_to_structured)
|
||||||
|
except ImportError:
|
||||||
|
from sensor_msgs_py.numpy_compat import (structured_to_unstructured,
|
||||||
|
unstructured_to_structured)
|
||||||
|
|
||||||
|
from sensor_msgs.msg import PointCloud2, PointField
|
||||||
|
from std_msgs.msg import Header
|
||||||
|
|
||||||
|
|
||||||
|
_DATATYPES = {}
|
||||||
|
_DATATYPES[PointField.INT8] = np.dtype(np.int8)
|
||||||
|
_DATATYPES[PointField.UINT8] = np.dtype(np.uint8)
|
||||||
|
_DATATYPES[PointField.INT16] = np.dtype(np.int16)
|
||||||
|
_DATATYPES[PointField.UINT16] = np.dtype(np.uint16)
|
||||||
|
_DATATYPES[PointField.INT32] = np.dtype(np.int32)
|
||||||
|
_DATATYPES[PointField.UINT32] = np.dtype(np.uint32)
|
||||||
|
_DATATYPES[PointField.FLOAT32] = np.dtype(np.float32)
|
||||||
|
_DATATYPES[PointField.FLOAT64] = np.dtype(np.float64)
|
||||||
|
|
||||||
|
DUMMY_FIELD_PREFIX = 'unnamed_field'
|
||||||
|
|
||||||
|
|
||||||
|
def read_points(
|
||||||
|
cloud: PointCloud2,
|
||||||
|
field_names: Optional[List[str]] = None,
|
||||||
|
skip_nans: bool = False,
|
||||||
|
uvs: Optional[Iterable] = None,
|
||||||
|
reshape_organized_cloud: bool = False) -> np.ndarray:
|
||||||
|
"""
|
||||||
|
Read points from a sensor_msgs.PointCloud2 message.
|
||||||
|
|
||||||
|
:param cloud: The point cloud to read from sensor_msgs.PointCloud2.
|
||||||
|
:param field_names: The names of fields to read. If None, read all fields.
|
||||||
|
(Type: Iterable, Default: None)
|
||||||
|
:param skip_nans: If True, then don't return any point with a NaN value.
|
||||||
|
(Type: Bool, Default: False)
|
||||||
|
:param uvs: If specified, then only return the points at the given
|
||||||
|
coordinates. (Type: Iterable, Default: None)
|
||||||
|
:param reshape_organized_cloud: Returns the array as an 2D organized point cloud if set.
|
||||||
|
:return: Structured NumPy array containing all points.
|
||||||
|
"""
|
||||||
|
assert isinstance(cloud, PointCloud2), \
|
||||||
|
'Cloud is not a sensor_msgs.msg.PointCloud2'
|
||||||
|
|
||||||
|
# Cast bytes to numpy array
|
||||||
|
points = np.ndarray(
|
||||||
|
shape=(cloud.width * cloud.height, ),
|
||||||
|
dtype=dtype_from_fields(cloud.fields, point_step=cloud.point_step),
|
||||||
|
buffer=cloud.data)
|
||||||
|
|
||||||
|
# Keep only the requested fields
|
||||||
|
if field_names is not None:
|
||||||
|
assert all(field_name in points.dtype.names for field_name in field_names), \
|
||||||
|
'Requests field is not in the fields of the PointCloud!'
|
||||||
|
# Mask fields
|
||||||
|
points = points[list(field_names)]
|
||||||
|
|
||||||
|
# Swap array if byte order does not match
|
||||||
|
if bool(sys.byteorder != 'little') != bool(cloud.is_bigendian):
|
||||||
|
points = points.byteswap(inplace=True)
|
||||||
|
|
||||||
|
# Check if we want to drop points with nan values
|
||||||
|
if skip_nans and not cloud.is_dense:
|
||||||
|
# Init mask which selects all points
|
||||||
|
not_nan_mask = np.ones(len(points), dtype=bool)
|
||||||
|
for field_name in points.dtype.names:
|
||||||
|
# Only keep points without any non values in the mask
|
||||||
|
not_nan_mask = np.logical_and(
|
||||||
|
not_nan_mask, ~np.isnan(points[field_name]))
|
||||||
|
# Select these points
|
||||||
|
points = points[not_nan_mask]
|
||||||
|
|
||||||
|
# Select points indexed by the uvs field
|
||||||
|
if uvs is not None:
|
||||||
|
# Don't convert to numpy array if it is already one
|
||||||
|
if not isinstance(uvs, np.ndarray):
|
||||||
|
uvs = np.fromiter(uvs, int)
|
||||||
|
# Index requested points
|
||||||
|
points = points[uvs]
|
||||||
|
|
||||||
|
# Cast into 2d array if cloud is 'organized'
|
||||||
|
if reshape_organized_cloud and cloud.height > 1:
|
||||||
|
points = points.reshape(cloud.width, cloud.height)
|
||||||
|
|
||||||
|
return points
|
||||||
|
|
||||||
|
|
||||||
|
def read_points_numpy(
|
||||||
|
cloud: PointCloud2,
|
||||||
|
field_names: Optional[List[str]] = None,
|
||||||
|
skip_nans: bool = False,
|
||||||
|
uvs: Optional[Iterable] = None,
|
||||||
|
reshape_organized_cloud: bool = False) -> np.ndarray:
|
||||||
|
"""
|
||||||
|
Read equally typed fields from sensor_msgs.PointCloud2 message as a unstructured numpy array.
|
||||||
|
|
||||||
|
This method is better suited if one wants to perform math operations
|
||||||
|
on e.g. all x,y,z fields.
|
||||||
|
But it is limited to fields with the same dtype as unstructured numpy arrays
|
||||||
|
only contain one dtype.
|
||||||
|
|
||||||
|
:param cloud: The point cloud to read from sensor_msgs.PointCloud2.
|
||||||
|
:param field_names: The names of fields to read. If None, read all fields.
|
||||||
|
(Type: Iterable, Default: None)
|
||||||
|
:param skip_nans: If True, then don't return any point with a NaN value.
|
||||||
|
(Type: Bool, Default: False)
|
||||||
|
:param uvs: If specified, then only return the points at the given
|
||||||
|
coordinates. (Type: Iterable, Default: None)
|
||||||
|
:param reshape_organized_cloud: Returns the array as an 2D organized point cloud if set.
|
||||||
|
:return: Numpy array containing all points.
|
||||||
|
"""
|
||||||
|
assert all(cloud.fields[0].datatype == field.datatype for field in cloud.fields[1:]), \
|
||||||
|
'All fields need to have the same datatype. Use `read_points()` otherwise.'
|
||||||
|
structured_numpy_array = read_points(
|
||||||
|
cloud, field_names, skip_nans, uvs, reshape_organized_cloud)
|
||||||
|
return structured_to_unstructured(structured_numpy_array)
|
||||||
|
|
||||||
|
|
||||||
|
def read_points_list(
|
||||||
|
cloud: PointCloud2,
|
||||||
|
field_names: Optional[List[str]] = None,
|
||||||
|
skip_nans: bool = False,
|
||||||
|
uvs: Optional[Iterable] = None) -> List[NamedTuple]:
|
||||||
|
"""
|
||||||
|
Read points from a sensor_msgs.PointCloud2 message.
|
||||||
|
|
||||||
|
This function returns a list of namedtuples. It operates on top of the
|
||||||
|
read_points method. For more efficient access use read_points directly.
|
||||||
|
|
||||||
|
:param cloud: The point cloud to read from. (Type: sensor_msgs.PointCloud2)
|
||||||
|
:param field_names: The names of fields to read. If None, read all fields.
|
||||||
|
(Type: Iterable, Default: None)
|
||||||
|
:param skip_nans: If True, then don't return any point with a NaN value.
|
||||||
|
(Type: Bool, Default: False)
|
||||||
|
:param uvs: If specified, then only return the points at the given
|
||||||
|
coordinates. (Type: Iterable, Default: None]
|
||||||
|
:return: List of namedtuples containing the values for each point
|
||||||
|
"""
|
||||||
|
assert isinstance(cloud, PointCloud2), \
|
||||||
|
'cloud is not a sensor_msgs.msg.PointCloud2'
|
||||||
|
|
||||||
|
if field_names is None:
|
||||||
|
field_names = [f.name for f in cloud.fields]
|
||||||
|
|
||||||
|
Point = namedtuple('Point', field_names)
|
||||||
|
|
||||||
|
return [Point._make(p) for p in read_points(cloud, field_names,
|
||||||
|
skip_nans, uvs)]
|
||||||
|
|
||||||
|
|
||||||
|
def dtype_from_fields(fields: Iterable[PointField], point_step: Optional[int] = None) -> np.dtype:
|
||||||
|
"""
|
||||||
|
Convert a Iterable of sensor_msgs.msg.PointField messages to a np.dtype.
|
||||||
|
|
||||||
|
:param fields: The point cloud fields.
|
||||||
|
(Type: iterable of sensor_msgs.msg.PointField)
|
||||||
|
:param point_step: Point step size in bytes. Calculated from the given fields by default.
|
||||||
|
(Type: optional of integer)
|
||||||
|
:returns: NumPy datatype
|
||||||
|
"""
|
||||||
|
# Create a lists containing the names, offsets and datatypes of all fields
|
||||||
|
field_names = []
|
||||||
|
field_offsets = []
|
||||||
|
field_datatypes = []
|
||||||
|
for i, field in enumerate(fields):
|
||||||
|
# Datatype as numpy datatype
|
||||||
|
datatype = _DATATYPES[field.datatype]
|
||||||
|
# Name field
|
||||||
|
if field.name == '':
|
||||||
|
name = f'{DUMMY_FIELD_PREFIX}_{i}'
|
||||||
|
else:
|
||||||
|
name = field.name
|
||||||
|
# Handle fields with count > 1 by creating subfields with a suffix consiting
|
||||||
|
# of "_" followed by the subfield counter [0 -> (count - 1)]
|
||||||
|
assert field.count > 0, "Can't process fields with count = 0."
|
||||||
|
for a in range(field.count):
|
||||||
|
# Add suffix if we have multiple subfields
|
||||||
|
if field.count > 1:
|
||||||
|
subfield_name = f'{name}_{a}'
|
||||||
|
else:
|
||||||
|
subfield_name = name
|
||||||
|
assert subfield_name not in field_names, 'Duplicate field names are not allowed!'
|
||||||
|
field_names.append(subfield_name)
|
||||||
|
# Create new offset that includes subfields
|
||||||
|
field_offsets.append(field.offset + a * datatype.itemsize)
|
||||||
|
field_datatypes.append(datatype.str)
|
||||||
|
|
||||||
|
# Create dtype
|
||||||
|
dtype_dict = {
|
||||||
|
'names': field_names,
|
||||||
|
'formats': field_datatypes,
|
||||||
|
'offsets': field_offsets
|
||||||
|
}
|
||||||
|
if point_step is not None:
|
||||||
|
dtype_dict['itemsize'] = point_step
|
||||||
|
return np.dtype(dtype_dict)
|
||||||
|
|
||||||
|
|
||||||
|
def create_cloud(
|
||||||
|
header: Header,
|
||||||
|
fields: Iterable[PointField],
|
||||||
|
points: Iterable) -> PointCloud2:
|
||||||
|
"""
|
||||||
|
Create a sensor_msgs.msg.PointCloud2 message.
|
||||||
|
|
||||||
|
:param header: The point cloud header. (Type: std_msgs.msg.Header)
|
||||||
|
:param fields: The point cloud fields.
|
||||||
|
(Type: iterable of sensor_msgs.msg.PointField)
|
||||||
|
:param points: The point cloud points. List of iterables, i.e. one iterable
|
||||||
|
for each point, with the elements of each iterable being the
|
||||||
|
values of the fields for that point (in the same order as
|
||||||
|
the fields parameter)
|
||||||
|
:return: The point cloud as sensor_msgs.msg.PointCloud2
|
||||||
|
"""
|
||||||
|
# Check if input is numpy array
|
||||||
|
if isinstance(points, np.ndarray):
|
||||||
|
# Check if this is an unstructured array
|
||||||
|
if points.dtype.names is None:
|
||||||
|
assert all(fields[0].datatype == field.datatype for field in fields[1:]), \
|
||||||
|
'All fields need to have the same datatype. Pass a structured NumPy array \
|
||||||
|
with multiple dtypes otherwise.'
|
||||||
|
# Convert unstructured to structured array
|
||||||
|
points = unstructured_to_structured(
|
||||||
|
points,
|
||||||
|
dtype=dtype_from_fields(fields))
|
||||||
|
else:
|
||||||
|
assert points.dtype == dtype_from_fields(fields), \
|
||||||
|
'PointFields and structured NumPy array dtype do not match for all fields! \
|
||||||
|
Check their field order, names and types.'
|
||||||
|
else:
|
||||||
|
# Cast python objects to structured NumPy array (slow)
|
||||||
|
points = np.array(
|
||||||
|
# Points need to be tuples in the structured array
|
||||||
|
list(map(tuple, points)),
|
||||||
|
dtype=dtype_from_fields(fields))
|
||||||
|
|
||||||
|
# Handle organized clouds
|
||||||
|
assert len(points.shape) <= 2, \
|
||||||
|
'Too many dimensions for organized cloud! \
|
||||||
|
Points can only be organized in max. two dimensional space'
|
||||||
|
height = 1
|
||||||
|
width = points.shape[0]
|
||||||
|
# Check if input points are an organized cloud (2D array of points)
|
||||||
|
if len(points.shape) == 2:
|
||||||
|
height = points.shape[1]
|
||||||
|
|
||||||
|
# Convert numpy points to array.array
|
||||||
|
memory_view = memoryview(points)
|
||||||
|
casted = memory_view.cast('B')
|
||||||
|
array_array = array.array('B')
|
||||||
|
array_array.frombytes(casted)
|
||||||
|
|
||||||
|
# Put everything together
|
||||||
|
cloud = PointCloud2(
|
||||||
|
header=header,
|
||||||
|
height=height,
|
||||||
|
width=width,
|
||||||
|
is_dense=False,
|
||||||
|
is_bigendian=sys.byteorder != 'little',
|
||||||
|
fields=fields,
|
||||||
|
point_step=points.dtype.itemsize,
|
||||||
|
row_step=(points.dtype.itemsize * width))
|
||||||
|
# Set cloud via property instead of the constructor because of the bug described in
|
||||||
|
# https://github.com/ros2/common_interfaces/issues/176
|
||||||
|
cloud.data = array_array
|
||||||
|
return cloud
|
||||||
|
|
||||||
|
|
||||||
|
def create_cloud_xyz32(header: Header, points: Iterable) -> PointCloud2:
|
||||||
|
"""
|
||||||
|
Create a sensor_msgs.msg.PointCloud2 message with (x, y, z) fields.
|
||||||
|
|
||||||
|
:param header: The point cloud header. (Type: std_msgs.msg.Header)
|
||||||
|
:param points: The point cloud points. (Type: Iterable)
|
||||||
|
:return: The point cloud as sensor_msgs.msg.PointCloud2.
|
||||||
|
"""
|
||||||
|
fields = [PointField(name='x', offset=0,
|
||||||
|
datatype=PointField.FLOAT32, count=1),
|
||||||
|
PointField(name='y', offset=4,
|
||||||
|
datatype=PointField.FLOAT32, count=1),
|
||||||
|
PointField(name='z', offset=8,
|
||||||
|
datatype=PointField.FLOAT32, count=1)]
|
||||||
|
return create_cloud(header, fields, points)
|
|
@ -9,6 +9,85 @@ from rclpy.qos import QoSProfile
|
||||||
from rclpy.executors import MultiThreadedExecutor
|
from rclpy.executors import MultiThreadedExecutor
|
||||||
from geometry_msgs.msg import TransformStamped
|
from geometry_msgs.msg import TransformStamped
|
||||||
from scipy.spatial.transform import Rotation as R
|
from scipy.spatial.transform import Rotation as R
|
||||||
|
from sensor_msgs.msg import PointCloud2
|
||||||
|
from sensor_msgs_py import point_cloud2
|
||||||
|
from sensor_msgs.msg import Image, CameraInfo
|
||||||
|
from cv_bridge import CvBridge
|
||||||
|
import cv2
|
||||||
|
from Go2Py.utils.point_cloud2 import read_points_numpy
|
||||||
|
import rclpy
|
||||||
|
from rclpy.node import Node
|
||||||
|
from nav_msgs.msg import Odometry
|
||||||
|
|
||||||
|
import rclpy
|
||||||
|
from rclpy.node import Node
|
||||||
|
from nav_msgs.msg import Odometry
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from scipy.spatial.transform import Rotation as R
|
||||||
|
|
||||||
|
class ROS2OdometryReader(Node):
|
||||||
|
"""
|
||||||
|
A class for subscribing to and retrieving Odometry messages.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
odom_topic (str): The topic name for the odometry data.
|
||||||
|
node_name (str): A unique name for this node.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
odom (Odometry): The latest odometry message received from the topic.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, odom_topic, node_name):
|
||||||
|
super().__init__(f'{node_name}_odom_reader')
|
||||||
|
self.odom = None # Stores the latest odometry message
|
||||||
|
|
||||||
|
# Create a subscription to the specified odometry topic
|
||||||
|
self.odom_subscriber = self.create_subscription(
|
||||||
|
Odometry,
|
||||||
|
odom_topic,
|
||||||
|
self.odom_callback,
|
||||||
|
10
|
||||||
|
)
|
||||||
|
|
||||||
|
def odom_callback(self, msg):
|
||||||
|
"""
|
||||||
|
Callback function that stores the latest odometry message.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
msg (Odometry): The odometry message received.
|
||||||
|
"""
|
||||||
|
self.odom = msg
|
||||||
|
|
||||||
|
def get_odometry(self):
|
||||||
|
"""
|
||||||
|
Returns the latest odometry message received from the topic.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Odometry: The latest odometry message.
|
||||||
|
"""
|
||||||
|
return self.odom
|
||||||
|
|
||||||
|
def get_pose(self):
|
||||||
|
if self.odom is not None:
|
||||||
|
position = self.odom.pose.pose.position
|
||||||
|
orientation = self.odom.pose.pose.orientation
|
||||||
|
t = np.array([position.x, position.y, position.z])
|
||||||
|
q = np.array([orientation.x, orientation.y, orientation.z, orientation.w])
|
||||||
|
Rot = R.from_quat(q).as_matrix()
|
||||||
|
T = np.eye(4)
|
||||||
|
T[:3, :3] = Rot
|
||||||
|
T[:3, -1] = t
|
||||||
|
return T
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""
|
||||||
|
Destroys the node, cleaning up resources.
|
||||||
|
"""
|
||||||
|
self.destroy_node()
|
||||||
|
|
||||||
def ros2_init(args=None):
|
def ros2_init(args=None):
|
||||||
rclpy.init(args=args)
|
rclpy.init(args=args)
|
||||||
|
|
||||||
|
@ -49,10 +128,50 @@ class ROS2ExecutorManager:
|
||||||
if self.executor_thread:
|
if self.executor_thread:
|
||||||
self.executor_thread.join()
|
self.executor_thread.join()
|
||||||
|
|
||||||
class ROS2TFInterface(Node):
|
# class ROS2TFInterface(Node):
|
||||||
|
|
||||||
|
# def __init__(self, parent_name, child_name, node_name):
|
||||||
|
# super().__init__(f'{node_name}_tf2_listener')
|
||||||
|
# self.parent_name = parent_name
|
||||||
|
# self.child_name = child_name
|
||||||
|
# self.tfBuffer = tf2_ros.Buffer()
|
||||||
|
# self.listener = tf2_ros.TransformListener(self.tfBuffer, self)
|
||||||
|
# self.T = None
|
||||||
|
# self.stamp = None
|
||||||
|
# self.running = True
|
||||||
|
# self.thread = threading.Thread(target=self.update_loop)
|
||||||
|
# self.thread.start()
|
||||||
|
# self.trans = None
|
||||||
|
|
||||||
|
# def update_loop(self):
|
||||||
|
# while self.running:
|
||||||
|
# try:
|
||||||
|
# self.trans = self.tfBuffer.lookup_transform(self.parent_name, self.child_name, rclpy.time.Time(), rclpy.time.Duration(seconds=0.1))
|
||||||
|
# except (tf2_ros.LookupException, tf2_ros.ConnectivityException, tf2_ros.ExtrapolationException) as e:
|
||||||
|
# print(e)
|
||||||
|
# time.sleep(0.01)
|
||||||
|
|
||||||
|
# def get_pose(self):
|
||||||
|
# if self.trans is None:
|
||||||
|
# return None
|
||||||
|
# else:
|
||||||
|
# translation = [self.trans.transform.translation.x, self.trans.transform.translation.y, self.trans.transform.translation.z]
|
||||||
|
# rotation = [self.trans.transform.rotation.x, self.trans.transform.rotation.y, self.trans.transform.rotation.z, self.trans.transform.rotation.w]
|
||||||
|
# self.T = np.eye(4)
|
||||||
|
# self.T[0:3, 0:3] = R.from_quat(rotation).as_matrix()
|
||||||
|
# self.T[:3, 3] = translation
|
||||||
|
# self.stamp = self.trans.header.stamp.nanosec * 1e-9 + self.trans.header.stamp.sec
|
||||||
|
# return self.T
|
||||||
|
|
||||||
|
# def close(self):
|
||||||
|
# self.running = False
|
||||||
|
# self.thread.join()
|
||||||
|
# self.destroy_node()
|
||||||
|
|
||||||
|
|
||||||
|
class ROS2TFInterface(Node):
|
||||||
def __init__(self, parent_name, child_name, node_name):
|
def __init__(self, parent_name, child_name, node_name):
|
||||||
super().__init__(f'{node_name}_tf2_listener')
|
super().__init__(f"{node_name}_tf2_listener")
|
||||||
self.parent_name = parent_name
|
self.parent_name = parent_name
|
||||||
self.child_name = child_name
|
self.child_name = child_name
|
||||||
self.tfBuffer = tf2_ros.Buffer()
|
self.tfBuffer = tf2_ros.Buffer()
|
||||||
|
@ -60,31 +179,137 @@ class ROS2TFInterface(Node):
|
||||||
self.T = None
|
self.T = None
|
||||||
self.stamp = None
|
self.stamp = None
|
||||||
self.running = True
|
self.running = True
|
||||||
self.thread = threading.Thread(target=self.update_loop)
|
|
||||||
self.thread.start()
|
|
||||||
self.trans = None
|
self.trans = None
|
||||||
|
|
||||||
def update_loop(self):
|
|
||||||
while self.running:
|
|
||||||
try:
|
|
||||||
self.trans = self.tfBuffer.lookup_transform(self.parent_name, self.child_name, rclpy.time.Time(), rclpy.time.Duration(seconds=0.1))
|
|
||||||
except (tf2_ros.LookupException, tf2_ros.ConnectivityException, tf2_ros.ExtrapolationException) as e:
|
|
||||||
print(e)
|
|
||||||
time.sleep(0.01)
|
|
||||||
|
|
||||||
def get_pose(self):
|
def get_pose(self):
|
||||||
if self.trans is None:
|
try:
|
||||||
|
self.trans = self.tfBuffer.lookup_transform(
|
||||||
|
self.parent_name,
|
||||||
|
self.child_name,
|
||||||
|
rclpy.time.Time(),
|
||||||
|
rclpy.time.Duration(seconds=0.1),
|
||||||
|
)
|
||||||
|
except (
|
||||||
|
tf2_ros.LookupException,
|
||||||
|
tf2_ros.ConnectivityException,
|
||||||
|
tf2_ros.ExtrapolationException,
|
||||||
|
) as e:
|
||||||
return None
|
return None
|
||||||
else:
|
|
||||||
translation = [self.trans.transform.translation.x, self.trans.transform.translation.y, self.trans.transform.translation.z]
|
translation = [
|
||||||
rotation = [self.trans.transform.rotation.x, self.trans.transform.rotation.y, self.trans.transform.rotation.z, self.trans.transform.rotation.w]
|
self.trans.transform.translation.x,
|
||||||
self.T = np.eye(4)
|
self.trans.transform.translation.y,
|
||||||
self.T[0:3, 0:3] = R.from_quat(rotation).as_matrix()
|
self.trans.transform.translation.z,
|
||||||
self.T[:3, 3] = translation
|
]
|
||||||
self.stamp = self.trans.header.stamp.nanosec * 1e-9 + self.trans.header.stamp.sec
|
rotation = [
|
||||||
return self.T
|
self.trans.transform.rotation.x,
|
||||||
|
self.trans.transform.rotation.y,
|
||||||
|
self.trans.transform.rotation.z,
|
||||||
|
self.trans.transform.rotation.w,
|
||||||
|
]
|
||||||
|
self.T = np.eye(4)
|
||||||
|
self.T[0:3, 0:3] = R.from_quat(rotation).as_matrix()
|
||||||
|
self.T[:3, 3] = translation
|
||||||
|
self.stamp = (
|
||||||
|
self.trans.header.stamp.nanosec * 1e-9 + self.trans.header.stamp.sec
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.T
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
self.running = False
|
self.running = False
|
||||||
self.thread.join()
|
self.destroy_node()
|
||||||
|
|
||||||
|
|
||||||
|
class PoindCloud2Bridge(Node):
|
||||||
|
def __init__(self, topic_name):
|
||||||
|
super().__init__("point_cloud_listener")
|
||||||
|
self.subscription = self.create_subscription(
|
||||||
|
PointCloud2, topic_name, self.listener_callback, 1
|
||||||
|
)
|
||||||
|
self.data = None
|
||||||
|
self.new_frame_flag = False
|
||||||
|
self.points = None
|
||||||
|
|
||||||
|
self._points = None
|
||||||
|
self.points = None
|
||||||
|
self.ready = False
|
||||||
|
self.running = True
|
||||||
|
|
||||||
|
def listener_callback(self, msg):
|
||||||
|
# Read the x, y, z fields from the PointCloud2 message
|
||||||
|
self._points = read_points_numpy(
|
||||||
|
msg,
|
||||||
|
field_names=("x", "y", "z"),
|
||||||
|
skip_nans=True,
|
||||||
|
reshape_organized_cloud=True,
|
||||||
|
)
|
||||||
|
# self._points = point_cloud2.read_points(
|
||||||
|
# msg,
|
||||||
|
# field_names=("x", "y", "z"),
|
||||||
|
# skip_nans=True,
|
||||||
|
# )
|
||||||
|
# _points = np.reshape(_points, (-1, 16, 3))
|
||||||
|
# self._points = np.reshape(_points, (-1, 16, 3))
|
||||||
|
self.new_frame_flag = True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
return self._points
|
||||||
|
|
||||||
|
class ROS2CameraReader(Node):
|
||||||
|
"""
|
||||||
|
A class for interacting with a ROS2 camera topics.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
image_topic (str): The topic name for the image stream.
|
||||||
|
camera_info_topic (str, optional): The topic name for the camera information.
|
||||||
|
K (numpy.ndarray, optional): The intrinsic camera matrix for the raw (distorted) images.
|
||||||
|
D (numpy.ndarray, optional): The distortion coefficients.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
color_frame (numpy.ndarray): The latest image frame received from the topic.
|
||||||
|
camera_info (CameraInfo): The latest camera information received from the topic.
|
||||||
|
K (numpy.ndarray): The intrinsic camera matrix.
|
||||||
|
D (numpy.ndarray): The distortion coefficients.
|
||||||
|
"""
|
||||||
|
def __init__(self, image_topic, node_name, camera_info_topic=None, K=None, D=None):
|
||||||
|
super().__init__(f'{node_name}_camera_reader')
|
||||||
|
self.bridge = CvBridge()
|
||||||
|
self.color_frame = None
|
||||||
|
self.camera_info = None
|
||||||
|
self.K = K
|
||||||
|
self.D = D
|
||||||
|
self.node_name = node_name
|
||||||
|
|
||||||
|
self.image_subscriber = self.create_subscription(Image, image_topic, self.image_callback, 10)
|
||||||
|
if camera_info_topic:
|
||||||
|
self.camera_info_subscriber = self.create_subscription(CameraInfo, camera_info_topic, self.camera_info_callback, 10)
|
||||||
|
|
||||||
|
def image_callback(self, msg):
|
||||||
|
self.color_frame = self.bridge.imgmsg_to_cv2(msg, desired_encoding='passthrough')
|
||||||
|
|
||||||
|
def camera_info_callback(self, msg):
|
||||||
|
self.camera_info = msg
|
||||||
|
self.K = np.array(msg.k).reshape((3, 3))
|
||||||
|
self.D = np.array(msg.d)
|
||||||
|
|
||||||
|
def get_image(self):
|
||||||
|
"""
|
||||||
|
Returns the latest image frame received from the topic.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
numpy.ndarray: The latest image frame received from the topic.
|
||||||
|
"""
|
||||||
|
return self.color_frame
|
||||||
|
|
||||||
|
def get_intrinsics(self):
|
||||||
|
"""
|
||||||
|
Returns the intrinsic camera matrix.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
numpy.ndarray: The intrinsic camera matrix.
|
||||||
|
"""
|
||||||
|
return {'K': self.K, 'D': self.D}
|
||||||
|
def close(self):
|
||||||
self.destroy_node()
|
self.destroy_node()
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,43 @@
|
||||||
|
module Go2DDSMsgs {
|
||||||
|
struct PointCloud {
|
||||||
|
sequence<float> x;
|
||||||
|
sequence<float> y;
|
||||||
|
sequence<float> z;
|
||||||
|
string timestamp;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DepthImage {
|
||||||
|
uint16 width;
|
||||||
|
uint16 height;
|
||||||
|
sequence<uint16> data;
|
||||||
|
string timestamp;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct RGBImage {
|
||||||
|
uint16 width;
|
||||||
|
uint16 height;
|
||||||
|
sequence<uint8> data;
|
||||||
|
string timestamp;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Pose {
|
||||||
|
double quat[4];
|
||||||
|
double trans[3];
|
||||||
|
string timestamp;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HighCommand {
|
||||||
|
double vx;
|
||||||
|
double vy;
|
||||||
|
double omega;
|
||||||
|
string timestamp;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Imu {
|
||||||
|
double accel[3];
|
||||||
|
double gyro[3];
|
||||||
|
double quat[4];
|
||||||
|
string timestamp;
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
|
@ -0,0 +1,8 @@
|
||||||
|
DDSMsgs
|
||||||
|
|
||||||
|
DepthImage
|
||||||
|
HighCommand
|
||||||
|
Imu
|
||||||
|
PointCloud
|
||||||
|
Pose
|
||||||
|
RGBImage
|
|
@ -0,0 +1,79 @@
|
||||||
|
"""
|
||||||
|
Generated by Eclipse Cyclone DDS idlc Python Backend
|
||||||
|
Cyclone DDS IDL version: v0.10.5
|
||||||
|
Module: Go2DDSMsgs
|
||||||
|
IDL file: DDSMsgs.idl
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from enum import auto
|
||||||
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
import cyclonedds.idl as idl
|
||||||
|
import cyclonedds.idl.annotations as annotate
|
||||||
|
import cyclonedds.idl.types as types
|
||||||
|
|
||||||
|
# root module import for resolving types
|
||||||
|
import Go2DDSMsgs
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
@annotate.final
|
||||||
|
@annotate.autoid("sequential")
|
||||||
|
class PointCloud(idl.IdlStruct, typename="Go2DDSMsgs.PointCloud"):
|
||||||
|
x: types.sequence[types.float32]
|
||||||
|
y: types.sequence[types.float32]
|
||||||
|
z: types.sequence[types.float32]
|
||||||
|
timestamp: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
@annotate.final
|
||||||
|
@annotate.autoid("sequential")
|
||||||
|
class DepthImage(idl.IdlStruct, typename="Go2DDSMsgs.DepthImage"):
|
||||||
|
width: types.uint16
|
||||||
|
height: types.uint16
|
||||||
|
data: types.sequence[types.uint16]
|
||||||
|
timestamp: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
@annotate.final
|
||||||
|
@annotate.autoid("sequential")
|
||||||
|
class RGBImage(idl.IdlStruct, typename="Go2DDSMsgs.RGBImage"):
|
||||||
|
width: types.uint16
|
||||||
|
height: types.uint16
|
||||||
|
data: types.sequence[types.uint8]
|
||||||
|
timestamp: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
@annotate.final
|
||||||
|
@annotate.autoid("sequential")
|
||||||
|
class Pose(idl.IdlStruct, typename="Go2DDSMsgs.Pose"):
|
||||||
|
quat: types.array[types.float64, 4]
|
||||||
|
trans: types.array[types.float64, 3]
|
||||||
|
timestamp: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
@annotate.final
|
||||||
|
@annotate.autoid("sequential")
|
||||||
|
class HighCommand(idl.IdlStruct, typename="Go2DDSMsgs.HighCommand"):
|
||||||
|
vx: types.float64
|
||||||
|
vy: types.float64
|
||||||
|
omega: types.float64
|
||||||
|
timestamp: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
@annotate.final
|
||||||
|
@annotate.autoid("sequential")
|
||||||
|
class Imu(idl.IdlStruct, typename="Go2DDSMsgs.Imu"):
|
||||||
|
accel: types.array[types.float64, 3]
|
||||||
|
gyro: types.array[types.float64, 3]
|
||||||
|
quat: types.array[types.float64, 4]
|
||||||
|
timestamp: str
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
"""
|
||||||
|
Generated by Eclipse Cyclone DDS idlc Python Backend
|
||||||
|
Cyclone DDS IDL version: v0.10.5
|
||||||
|
Module: Go2DDSMsgs
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from ._DDSMsgs import DepthImage, HighCommand, Imu, PointCloud, Pose, RGBImage
|
||||||
|
__all__ = ["DepthImage", "HighCommand", "Imu", "PointCloud", "Pose", "RGBImage", ]
|
|
@ -0,0 +1,116 @@
|
||||||
|
import sys
|
||||||
|
sys.path.append('..')
|
||||||
|
|
||||||
|
from Go2Py.utils.ros2 import PoindCloud2Bridge, ros2_init, ros2_close, ROS2ExecutorManager, ROS2CameraReader
|
||||||
|
from Go2Py.utils.ros2 import ROS2OdometryReader
|
||||||
|
from Go2Py.robot.interface import GO2Real
|
||||||
|
from dds_telemetry import Go2DDSServer
|
||||||
|
import time
|
||||||
|
import numpy as np
|
||||||
|
import time
|
||||||
|
import cv2
|
||||||
|
|
||||||
|
import pickle
|
||||||
|
RECORD_DATASET = False
|
||||||
|
DATASET_LENGTH = 120
|
||||||
|
ros2_init()
|
||||||
|
map_bridge = PoindCloud2Bridge('/Laser_map')
|
||||||
|
fast_lio_odom = ROS2OdometryReader('/Odometry', 'odometry_subscriber')
|
||||||
|
rgb_camera_bridge = ROS2CameraReader('/camera/color/image_raw', 'rgb_reader')
|
||||||
|
depth_camera_bridge = ROS2CameraReader('/camera/depth/image_rect_raw', 'depth_reader')
|
||||||
|
|
||||||
|
ros2_exec_manager = ROS2ExecutorManager()
|
||||||
|
ros2_exec_manager.add_node(map_bridge)
|
||||||
|
ros2_exec_manager.add_node(fast_lio_odom)
|
||||||
|
ros2_exec_manager.add_node(rgb_camera_bridge)
|
||||||
|
ros2_exec_manager.add_node(depth_camera_bridge)
|
||||||
|
ros2_exec_manager.start()
|
||||||
|
|
||||||
|
# The extrinsics of the MID360 LiDAR with respect to the robot body
|
||||||
|
body_T_lidar = np.array([
|
||||||
|
[ 0.9743701, 0.0, 0.2249511, 0.1870 ],
|
||||||
|
[ 0.0, 1.0, 0.0, 0.0 ],
|
||||||
|
[-0.2249511, 0.0, 0.9743701, 0.0803 ],
|
||||||
|
[ 0.0, 0.0, 0.0, 1.0 ]
|
||||||
|
])
|
||||||
|
|
||||||
|
robot = GO2Real(mode='highlevel')
|
||||||
|
dds_server = Go2DDSServer(robot_name='go2')
|
||||||
|
|
||||||
|
def GetLidarPose():
|
||||||
|
lio_T_lidar = fast_lio_odom.get_pose()
|
||||||
|
if lio_T_lidar is not None:
|
||||||
|
lio_T_body = lio_T_lidar @ np.linalg.inv(body_T_lidar)
|
||||||
|
return lio_T_body
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def GetLidarMap():
|
||||||
|
pcd = map_bridge._points
|
||||||
|
if pcd is not None:
|
||||||
|
return pcd
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
print('Waitng for the ROS2 bridges to start up ...')
|
||||||
|
time.sleep(5)
|
||||||
|
print('Running the bridge loop')
|
||||||
|
lio_pcd = None
|
||||||
|
map_update_counter = 0
|
||||||
|
|
||||||
|
rgb_imgs = []
|
||||||
|
depth_imgs = []
|
||||||
|
lio_Ts_robot = []
|
||||||
|
pcd_maps = []
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
tic = time.time()
|
||||||
|
lio_T_robot = GetLidarPose()
|
||||||
|
if lio_T_robot is not None:
|
||||||
|
dds_server.sendLidarOdom(lio_T_robot)
|
||||||
|
if RECORD_DATASET:
|
||||||
|
lio_Ts_robot.append(lio_T_robot)
|
||||||
|
|
||||||
|
|
||||||
|
if map_update_counter%10==0:
|
||||||
|
lio_pcd = GetLidarMap()
|
||||||
|
if lio_pcd is not None:
|
||||||
|
dds_server.sendMap(lio_pcd)
|
||||||
|
if RECORD_DATASET:
|
||||||
|
pcd_maps.append(lio_pcd.copy())
|
||||||
|
|
||||||
|
map_update_counter += 1
|
||||||
|
|
||||||
|
depth = depth_camera_bridge.get_image()
|
||||||
|
rgb = rgb_camera_bridge.get_image()
|
||||||
|
if depth is not None and rgb is not None:
|
||||||
|
depth = cv2.resize(depth, (320, 240))
|
||||||
|
rgb = cv2.resize(rgb, (320, 240))
|
||||||
|
# print('sending image')
|
||||||
|
dds_server.sendDepth(depth)
|
||||||
|
dds_server.sendRGB(rgb)
|
||||||
|
|
||||||
|
if RECORD_DATASET:
|
||||||
|
rgb_imgs.append(rgb.copy())
|
||||||
|
depth_imgs.append(depth.copy())
|
||||||
|
|
||||||
|
|
||||||
|
if time.time()-start_time >= DATASET_LENGTH:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Forward the rgb, depth, lio, lio_T_lidar to the computer through DDS
|
||||||
|
toc = time.time()
|
||||||
|
command = dds_server.getHighCmd()
|
||||||
|
if command is not None:
|
||||||
|
print('Recievied a command from the client. Forwarding to the robot...')
|
||||||
|
robot.setCommandsHigh(command.vx, command.vy, command.omega)
|
||||||
|
|
||||||
|
print(f'{(toc-tic):0.3f}')
|
||||||
|
while(time.time()-tic) < 0.1:
|
||||||
|
time.sleep(0.001)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,111 @@
|
||||||
|
import socket
|
||||||
|
import struct
|
||||||
|
import numpy as np
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
from cyclonedds.domain import DomainParticipant
|
||||||
|
from cyclonedds.topic import Topic
|
||||||
|
from cyclonedds.sub import DataReader
|
||||||
|
from cyclonedds.pub import Publisher, DataWriter
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from Go2DDSMsgs import PointCloud, RGBImage, DepthImage, Pose, HighCommand
|
||||||
|
from scipy.spatial.transform import Rotation as R
|
||||||
|
|
||||||
|
|
||||||
|
def get_last_msg(reader, topic_type):
|
||||||
|
""" """
|
||||||
|
last_msg = reader.take()
|
||||||
|
|
||||||
|
if last_msg:
|
||||||
|
while True:
|
||||||
|
a = reader.take()
|
||||||
|
if not a:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
last_msg = a
|
||||||
|
if last_msg:
|
||||||
|
msg = last_msg[0]
|
||||||
|
if type(msg) == topic_type:
|
||||||
|
return msg
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
class Go2DDSServer:
|
||||||
|
def __init__(self, robot_name):
|
||||||
|
self.robot_name = robot_name
|
||||||
|
self.domain = DomainParticipant()
|
||||||
|
self.rgb_topic = Topic(self.domain, f'{self.robot_name}_rgb', RGBImage)
|
||||||
|
self.depth_topic = Topic(self.domain, f'{self.robot_name}_depth', DepthImage)
|
||||||
|
self.map_topic = Topic(self.domain, f'{self.robot_name}_lio_map', PointCloud)
|
||||||
|
self.lidar_odom_topic = Topic(self.domain, f'{self.robot_name}_lio_odom', Pose)
|
||||||
|
self.high_cmd_topic = Topic(self.domain, f'{self.robot_name}_high_cmd', HighCommand)
|
||||||
|
self.rgb_publisher = Publisher(self.domain)
|
||||||
|
self.depth_publisher = Publisher(self.domain)
|
||||||
|
self.map_publisher = Publisher(self.domain)
|
||||||
|
self.lidar_odom_publisher = Publisher(self.domain)
|
||||||
|
self.high_cmd_reader = DataReader(self.domain, self.high_cmd_topic)
|
||||||
|
self.rgb_writer = DataWriter(self.rgb_publisher, self.rgb_topic)
|
||||||
|
self.depth_writer = DataWriter(self.depth_publisher, self.depth_topic)
|
||||||
|
self.map_writer = DataWriter(self.map_publisher, self.map_topic)
|
||||||
|
self.lidar_odom_writer = DataWriter(self.lidar_odom_publisher, self.lidar_odom_topic)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def sendRGB(self, rgb):
|
||||||
|
self.rgb_writer.write(RGBImage(data=rgb.reshape(-1).tolist(), width=rgb.shape[1], height=rgb.shape[0], timestamp=''))
|
||||||
|
|
||||||
|
def sendDepth(self, depth):
|
||||||
|
self.depth_writer.write(DepthImage(data=depth.reshape(-1).tolist(), width=depth.shape[1], height=depth.shape[0], timestamp=''))
|
||||||
|
|
||||||
|
def sendMap(self, pcd):
|
||||||
|
map_pcd_mgs = PointCloud(x = pcd[:,0].tolist(),
|
||||||
|
y = pcd[:,1].tolist(),
|
||||||
|
z = pcd[:,2].tolist(),
|
||||||
|
timestamp=''
|
||||||
|
)
|
||||||
|
self.map_writer.write(map_pcd_mgs)
|
||||||
|
|
||||||
|
def sendLidarOdom(self, lio_T_body):
|
||||||
|
q = R.from_matrix(lio_T_body[:3,:3]).as_quat()
|
||||||
|
t = lio_T_body[:3,3]
|
||||||
|
pose_msg = Pose(quat=q.tolist(), trans=t.tolist(), timestamp='')
|
||||||
|
self.lidar_odom_writer.write(pose_msg)
|
||||||
|
|
||||||
|
def getHighCmd(self):
|
||||||
|
return get_last_msg(self.high_cmd_reader, HighCommand)
|
||||||
|
|
||||||
|
|
||||||
|
class Go2DDSClient:
|
||||||
|
def __init__(self, robot_name):
|
||||||
|
self.robot_name = robot_name
|
||||||
|
self.domain = DomainParticipant()
|
||||||
|
self.rgb_topic = Topic(self.domain, f'{self.robot_name}_rgb', RGBImage)
|
||||||
|
self.depth_topic = Topic(self.domain, f'{self.robot_name}_depth', DepthImage)
|
||||||
|
self.map_topic = Topic(self.domain, f'{self.robot_name}_lio_map', PointCloud)
|
||||||
|
self.lidar_odom_topic = Topic(self.domain, f'{self.robot_name}_lio_odom', Pose)
|
||||||
|
self.high_cmd_topic = Topic(self.domain, f'{self.robot_name}_high_cmd', HighCommand)
|
||||||
|
self.rgb_reader = DataReader(self.domain, self.rgb_topic)
|
||||||
|
self.depth_reader = DataReader(self.domain, self.depth_topic)
|
||||||
|
self.map_reader = DataReader(self.domain, self.map_topic)
|
||||||
|
self.lidar_odom_reader = DataReader(self.domain, self.lidar_odom_topic)
|
||||||
|
self.high_cmd_writer = DataWriter(self.domain, self.high_cmd_topic)
|
||||||
|
|
||||||
|
def getRGB(self):
|
||||||
|
return get_last_msg(self.rgb_reader, RGBImage)
|
||||||
|
|
||||||
|
def getDepth(self):
|
||||||
|
return get_last_msg(self.depth_reader, DepthImage)
|
||||||
|
|
||||||
|
def getMap(self):
|
||||||
|
return get_last_msg(self.map_reader, PointCloud)
|
||||||
|
|
||||||
|
def getLidarOdom(self):
|
||||||
|
return get_last_msg(self.lidar_odom_reader, Pose)
|
||||||
|
|
||||||
|
def sendHighCmd(self, vx, vy, omega):
|
||||||
|
self.high_cmd_writer.write(HighCommand(vx=vx, vy=vy, omega=omega, timestamp=''))
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
from Go2Py.robot.fsm import FSM
|
||||||
|
from Go2Py.robot.safety import SafetyHypervisor
|
||||||
|
from Go2Py.robot.interface import GO2Real
|
||||||
|
from Go2Py.control.neuro_diff import NeuroDiffAgent
|
||||||
|
|
||||||
|
import torch
|
||||||
|
import numpy as np
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
class NeuroDiffController:
|
||||||
|
def __init__(self, robot, remote, checkpoint):
|
||||||
|
self.remote = remote
|
||||||
|
self.robot = robot
|
||||||
|
self.policy = Policy(checkpoint)
|
||||||
|
self.command_profile = CommandInterface()
|
||||||
|
self.agent = NeuroDiffSimAgent(self.command_profile, self.robot)
|
||||||
|
self.hist_data = {}
|
||||||
|
|
||||||
|
def init(self):
|
||||||
|
self.obs = self.agent.reset()
|
||||||
|
self.policy_info = {}
|
||||||
|
self.command_profile.yaw_vel_cmd = 0.0
|
||||||
|
self.command_profile.x_vel_cmd = 0.0
|
||||||
|
self.command_profile.y_vel_cmd = 0.0
|
||||||
|
|
||||||
|
def update(self, robot, remote):
|
||||||
|
if not hasattr(self, "obs"):
|
||||||
|
self.init()
|
||||||
|
commands = getRemote(remote)
|
||||||
|
self.command_profile.yaw_vel_cmd = 0.0
|
||||||
|
self.command_profile.x_vel_cmd = 0.0
|
||||||
|
self.command_profile.y_vel_cmd = 0.0
|
||||||
|
|
||||||
|
self.obs = self.agent.get_obs()
|
||||||
|
action = self.policy(self.obs, self.policy_info)
|
||||||
|
_, self.ret, self.done, self.info = self.agent.step(action)
|
||||||
|
for key, value in self.info.items():
|
||||||
|
if key in self.hist_data:
|
||||||
|
self.hist_data[key].append(value)
|
||||||
|
else:
|
||||||
|
self.hist_data[key] = [value]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
robot = GO2Real(mode='lowlevel')
|
||||||
|
safety_hypervisor = SafetyHypervisor(robot)
|
||||||
|
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
print(robot.getJointStates())
|
||||||
|
|
||||||
|
robot.close()
|
|
@ -0,0 +1,26 @@
|
||||||
|
from cyclonedds.topic import Topic
|
||||||
|
from cyclonedds.pub import DataWriter
|
||||||
|
|
||||||
|
from cyclonedds.domain import DomainParticipant
|
||||||
|
from cyclonedds.topic import Topic
|
||||||
|
from cyclonedds.sub import DataReader
|
||||||
|
from cyclonedds.util import duration
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from Go2DDSMsgs import PointCloud, RGBImage, DepthImage, Pose, HighCommand
|
||||||
|
# from scipy.spatial.transform import Rotation as R
|
||||||
|
|
||||||
|
participant = DomainParticipant()
|
||||||
|
topic = Topic(participant, 'test_topic', RGBImage)
|
||||||
|
writer = DataWriter(participant, topic)
|
||||||
|
|
||||||
|
# domain = DomainParticipant()
|
||||||
|
# rgb_topic = Topic(domain, 'go2_rgb', RGBImage)
|
||||||
|
# rgb_publisher = Publisher(domain)
|
||||||
|
# rgb_writer = DataWriter(rgb_publisher, rgb_topic)
|
||||||
|
|
||||||
|
|
||||||
|
rgb = np.random.randint(0, 255, (10, 10, 3)).astype(np.uint8)
|
||||||
|
|
||||||
|
rgb_msg = RGBImage(data=[0, 1, 2], timestamp='')
|
||||||
|
writer.write(rgb_msg)
|
Loading…
Reference in New Issue