Robowflex  v0.1
Making MoveIt Easy
robot.py
Go to the documentation of this file.
1 ## @package robot
2 # Functions for loading and animating robots in Blender.
3 
4 import bpy
5 import os
6 import subprocess
7 import math
8 import mathutils
9 from urdf_parser_py import urdf as URDF
10 
11 import robowflex_visualization as rv
12 
13 
14 ## @brief Controllable URDF described robot.
15 # This class loads a URDF from the package resource URI under a Blender
16 # collection.
17 #
18 class Robot:
19  ## @var name
20  # Name of Robot and Blender collection.
21  ## @var collection
22  # Blender collection containing loaded robot geometry.
23  ## @var urdf
24  # Absolute path to the URDF file.
25  ## @var xml
26  # Raw XML of the URDF file.
27  ## @var robot
28  # Parsed URDF file.
29 
30  ## @brief Constructor. Loads robot and creates a collection.
31  #
32  # @param name Name of Blender collection to put Robot geometry in.
33  # @param urdf URDF package resource URI to load.
34  # @param make_pretty Calls prettify functions after load.
35  #
36  def __init__(self, name, urdf, make_pretty = True):
37  self.namename = name
38 
39  self.collectioncollection = rv.utils.get_collection(name)
40  if not self.collectioncollection:
41  self.collectioncollection = rv.utils.make_collection(name)
42  self.load_urdfload_urdf(urdf)
43  if make_pretty:
44  self.prettifyprettify()
45 
46  else:
47  self.load_urdfload_urdf(urdf, False)
48 
49  self.clear_animation_dataclear_animation_data()
50 
51  ## @brief Loads the URDF as a COLLADA mesh, as well as loading the XML.
52  #
53  # @param urdf URDF package resource URI to load.
54  # @param load Load the actual COLLADA mesh, not just URDF info.
55  #
56  def load_urdf(self, urdf, load = True):
57  self.urdf_pathurdf_path = rv.utils.resolve_path(urdf)
58  xml_output = os.path.join("/tmp",
59  os.path.basename(self.urdf_pathurdf_path) + ".xml")
60  dae_output = os.path.join("/tmp",
61  os.path.basename(self.urdf_pathurdf_path) + ".dae")
62 
63  subprocess.check_output([
64  "rosrun",
65  "xacro",
66  "xacro",
67  self.urdf_pathurdf_path,
68  "-o",
69  xml_output,
70  ])
71 
72  self.urdf_xmlurdf_xml = open(xml_output, 'r').read()
73  self.robotrobot = URDF.Robot.from_xml_string(self.urdf_xmlurdf_xml)
74 
75  if load:
76  # Don't regenerate URDF COLLADA if it already exists.
77  if not os.path.exists(dae_output):
78  subprocess.check_output([
79  "rosrun",
80  "collada_urdf",
81  "urdf_to_collada",
82  xml_output,
83  dae_output,
84  ])
85 
86  # Import and move into new collection.
87  bpy.ops.wm.collada_import(filepath = dae_output)
88  rv.utils.move_selected_to_collection(self.namename)
89  rv.utils.deselect_all()
90 
91  ## @brief Gets a Blender object corresponding to a link on the robot.
92  #
93  # @param link_name Name of the link to find in the robot.
94  #
95  def get_link(self, link_name):
96  return rv.utils.find_object_in_collection(self.namename, link_name)
97 
98  ## @brief Get the XML description in the URDF of a link.
99  #
100  # @param link_name Name of the link to find in the robot.
101  #
102  def get_link_xml(self, link_name):
103  for link in self.robotrobot.links:
104  if link.name == link_name:
105  return link
106 
107  return None
108 
109  ## @brief Get the XML description in the URDF of a joint.
110  #
111  # @param joint_name Name of the joint to find in the robot.
112  #
113  def get_joint_xml(self, joint_name):
114  for joint in self.robotrobot.joints:
115  if joint.name == joint_name:
116  return joint
117 
118  return None
119 
120  ## @brief Set the value of a 1-DoF joint in the URDF.
121  #
122  # Works by finding the child link of the joint and setting its relative
123  # transformation in Blender according to the axis of movement. Basically
124  # reimplementing forward kinematics.
125  #
126  # @param joint_name Name of the joint to set in the robot.
127  # @param value Value of joint to set.
128  # @param interpolate If true, will attempt to make quaternions compatible
129  # with current pose.
130  #
131  def set_joint(self, joint_name, value, interpolate = True):
132  joint_xml = self.get_joint_xmlget_joint_xml(joint_name)
133  parent_xml = self.get_link_xmlget_link_xml(joint_xml.parent)
134  link_xml = self.get_link_xmlget_link_xml(joint_xml.child)
135  link = self.get_linkget_link(joint_xml.child)
136 
137  link.rotation_mode = "QUATERNION"
138 
139  parent_l = mathutils.Matrix.Identity(4)
140  if parent_xml.visual:
141  parent_l = rv.utils.get_tf_origin_xml(parent_xml.visual)
142  parent_l = parent_l.inverted()
143 
144  origin_l = mathutils.Matrix.Identity(4)
145  if link_xml.visual:
146  origin_l = rv.utils.get_tf_origin_xml(link_xml.visual)
147 
148  origin_j = rv.utils.get_tf_origin_xml(joint_xml)
149 
150  if joint_xml.type == "prismatic":
151  rm = mathutils.Matrix.Translation(
152  mathutils.Vector(joint_xml.axis) * value)
153 
154  else:
155  rotation = mathutils.Quaternion(joint_xml.axis, value)
156  rm = rotation.to_matrix()
157  rm.resize_4x4()
158 
159  output = parent_l @ origin_j @ rm @ origin_l
160 
161  prior = link.rotation_quaternion.copy()
162 
163  link.location = output.to_translation()
164  link.rotation_quaternion = output.to_quaternion()
165 
166  if interpolate:
167  link.rotation_quaternion.make_compatible(prior)
168 
169  ## @brief Set the value of a 6-DoF joint in the URDF.
170  #
171  # Assumes input transform is of the form:
172  # {
173  # 'translation' : [x, y, z],
174  # 'rotation' : [x, y, z, w]
175  # }
176  #
177  # @param joint_name Name of the joint to set in the robot.
178  # @param tf YAML of the transform.
179  # @param interpolate If true, will attempt to make quaternions compatible
180  # with current pose.
181  #
182  def set_joint_tf(self, joint_name, tf, interpolate = True):
183  # Check for "virtual_joint", which isn't in the URDF
184  if joint_name == "virtual_joint":
185  root = self.get_rootget_root().name
186  link_xml = self.get_link_xmlget_link_xml(root)
187  link = self.get_linkget_link(root)
188  else:
189  joint_xml = self.get_joint_xmlget_joint_xml(joint_name)
190  link_xml = self.get_link_xmlget_link_xml(joint_xml.child)
191  link = self.get_linkget_link(joint_xml.child)
192 
193  link.rotation_mode = "QUATERNION"
194  link.location = tf['translation']
195 
196  q = mathutils.Quaternion([tf['rotation'][3]] + tf['rotation'][:3])
197  if interpolate:
198  q.make_compatible(link.rotation_quaternion)
199 
200  link.rotation_quaternion = q
201 
202  ## @brief Adds a keyframe to a joint in the URDF at a frame in the
203  # animation timeline.
204  #
205  # Adds keyframe actually to child link (same as set_joint()). Uses current
206  # transform of the link. Only keyframes important data path for the joint
207  # (e.g., rotation for rotation joints).
208  #
209  # @param joint_name Joint to add keyframe to.
210  # @param frame Frame to add keyframe at.
211  #
212  def add_keyframe(self, joint_name, frame):
213  if joint_name == "virtual_joint":
214  root = self.get_rootget_root().name
215  link_xml = self.get_link_xmlget_link_xml(root)
216  link = self.get_linkget_link(root)
217  else:
218  joint_xml = self.get_joint_xmlget_joint_xml(joint_name)
219  link_xml = self.get_link_xmlget_link_xml(joint_xml.child)
220  link = self.get_linkget_link(joint_xml.child)
221 
222  link.keyframe_insert(data_path = "location", frame = frame)
223  link.keyframe_insert(data_path = "rotation_quaternion", frame = frame)
224 
225  ## @brief Adds keyframes to animate a moveit_msgs::RobotTrajectoryMsg.
226  #
227  # @param path_file YAML file for moveit_msgs::RobotTrajectoryMsg.
228  # @param fps Frames-per-second to animate the path at.
229  # @param start Frame to begin the animation at.
230  # @param reverse If true, load path from end-to-start rather than start-to-end.
231  # @param interpolate If true, interpolates quaternion from previous state.
232  #
233  def animate_path(self,
234  path_file,
235  fps = 60.,
236  start = 30,
237  reverse = False,
238  interpolate = False):
239  path = rv.utils.read_YAML_data(path_file)
240 
241  trajectory = path["joint_trajectory"]
242  names = trajectory["joint_names"]
243 
244  if "multi_dof_joint_trajectory" in path:
245  mdof_trajectory = path["multi_dof_joint_trajectory"]
246  mdof_names = mdof_trajectory["joint_names"]
247 
248  last_time = float(trajectory["points"][-1]["time_from_start"])
249  last_frame = start
250 
251  # Add keyframe at start
252  for name in names:
253  self.add_keyframeadd_keyframe(name, start)
254 
255  for i in range(len(trajectory["points"])):
256  point = trajectory["points"][i]
257 
258  time = float(point["time_from_start"])
259  if reverse:
260  time = last_time - time
261 
262  frame = start + time * fps
263  if frame > last_frame:
264  last_frame = frame
265 
266  for value, name in zip(point["positions"], names):
267  self.set_jointset_joint(name, value, interpolate or i > 0)
268  self.add_keyframeadd_keyframe(name, frame)
269 
270  if "multi_dof_joint_trajectory" in path:
271  mdof_point = mdof_trajectory["points"][i]
272  for tf, name in zip(mdof_point["transforms"], mdof_names):
273  self.set_joint_tfset_joint_tf(name, tf, interpolate or i > 0)
274  self.add_keyframeadd_keyframe(name, frame)
275 
276  return math.ceil(last_frame)
277 
278  ## @brief Sets the robot's state from a file with a moveit_msgs::RobotState
279  #
280  # @param state_file File containing the state.
281  def set_state(self, state_file):
282  state = rv.utils.read_YAML_data(state_file)
283  joint_state = state["joint_state"]
284 
285  names = joint_state["name"]
286  position = joint_state["position"]
287 
288  for name, value in zip(names, position):
289  self.set_jointset_joint(name, value)
290 
291  ## @brief Attaches an object to a link of the robot.
292  #
293  # @param link_name Name of link to attach item to.
294  # @param item Blender object to attach to link.
295  # @param frame Animation frame to parent in.
296  #
297  def attach_object(self, link_name, item, frame):
298  rv.utils.parent_object(self.get_linkget_link(link_name), item, frame)
299 
300  ## @brief Detaches an object to a link of the robot. Object must have been
301  # previously parented.
302  #
303  # @param link_name Name of link to attach item to.
304  # @param item Blender object to attach to link.
305  # @param frame Animation frame to parent in.
306  #
307  def detach_object(self, link_name, item, frame):
308  rv.utils.unparent_object(self.get_linkget_link(link_name), item, frame)
309 
310  ## @brief Cleans up robot mesh geometry and applies modifiers to improve
311  # rendering aesthetics.
312  #
313  def prettify(self):
314  for link_xml in self.robotrobot.links:
315  # Loads original mesh.
316  self.load_link_meshload_link_mesh(link_xml.name)
317 
318  link = self.get_linkget_link(link_xml.name)
319  rv.utils.remove_doubles(link)
320  # rv.utils.remove_inner_faces(link)
321  rv.utils.apply_edge_split(link)
322  rv.utils.apply_smooth_shade(link)
323 
324  ## @brief Loads the mesh data for a specific link on the robot.
325  #
326  # If the mesh is originally a COLLADA mesh, loads and replaces current
327  # mesh data. This provides better textures if they exist, as collada_urdf
328  # does not preserve them when converting the robot.
329  #
330  # @param link_name Name of the link to the load the mesh for.
331  #
332  def load_link_mesh(self, link_name):
333  link_xml = self.get_link_xmlget_link_xml(link_name)
334 
335  name = self.namename + "_temp"
336  collection = rv.utils.make_collection(name)
337 
338  if link_xml.visuals:
339  visual = link_xml.visuals[0]
340  geometry = visual.geometry
341 
342  # Only replace geometry if COLLADA
343  if hasattr(geometry, 'filename') and ".dae" in geometry.filename:
344  meshes, top = rv.primitives.add_mesh(
345  {"resource": geometry.filename})
346 
347  rv.utils.deselect_all()
348  bpy.context.view_layer.objects.active = meshes[0]
349  for mesh in meshes:
350  mesh.select_set(True)
351  bpy.ops.object.join()
352  rv.utils.move_selected_to_collection(name)
353  rv.utils.deselect_all()
354 
355  old = self.get_linkget_link(link_name)
356  old.data = meshes[0].data
357 
358  rv.utils.remove_collection(name)
359 
360  ## @brief Clear all animation data for this robot.
362  for link_xml in self.robotrobot.links:
363  link = self.get_linkget_link(link_xml.name)
364  if link:
365  link.animation_data_clear()
366 
367  ## @brief Get the root link of this robot
368  def get_root(self):
369  children = []
370  for joint in self.robotrobot.joints:
371  xml = self.get_joint_xmlget_joint_xml(joint.name)
372  children.append(xml.child)
373 
374  for link in self.robotrobot.links:
375  if link.name not in children:
376  return link
Controllable URDF described robot.
Definition: robot.py:18
def set_joint(self, joint_name, value, interpolate=True)
Set the value of a 1-DoF joint in the URDF.
Definition: robot.py:131
def set_state(self, state_file)
Sets the robot's state from a file with a moveit_msgs::RobotState.
Definition: robot.py:281
def get_joint_xml(self, joint_name)
Get the XML description in the URDF of a joint.
Definition: robot.py:113
def get_root(self)
Get the root link of this robot.
Definition: robot.py:368
def detach_object(self, link_name, item, frame)
Detaches an object to a link of the robot.
Definition: robot.py:307
def clear_animation_data(self)
Clear all animation data for this robot.
Definition: robot.py:361
def get_link(self, link_name)
Gets a Blender object corresponding to a link on the robot.
Definition: robot.py:95
def add_keyframe(self, joint_name, frame)
Adds a keyframe to a joint in the URDF at a frame in the animation timeline.
Definition: robot.py:212
def __init__(self, name, urdf, make_pretty=True)
Constructor.
Definition: robot.py:36
def prettify(self)
Cleans up robot mesh geometry and applies modifiers to improve rendering aesthetics.
Definition: robot.py:313
def set_joint_tf(self, joint_name, tf, interpolate=True)
Set the value of a 6-DoF joint in the URDF.
Definition: robot.py:182
def get_link_xml(self, link_name)
Get the XML description in the URDF of a link.
Definition: robot.py:102
name
Name of Robot and Blender collection.
Definition: robot.py:37
def load_link_mesh(self, link_name)
Loads the mesh data for a specific link on the robot.
Definition: robot.py:332
def load_urdf(self, urdf, load=True)
Loads the URDF as a COLLADA mesh, as well as loading the XML.
Definition: robot.py:56
def attach_object(self, link_name, item, frame)
Attaches an object to a link of the robot.
Definition: robot.py:297
def animate_path(self, path_file, fps=60., start=30, reverse=False, interpolate=False)
Adds keyframes to animate a moveit_msgs::RobotTrajectoryMsg.
Definition: robot.py:238
collection
Blender collection containing loaded robot geometry.
Definition: robot.py:39