-
Notifications
You must be signed in to change notification settings - Fork 559
/
frame_helpers.py
312 lines (249 loc) · 12.3 KB
/
frame_helpers.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
# Copyright (c) 2023 Boston Dynamics, Inc. All rights reserved.
#
# Downloading, reproducing, distributing or otherwise using the SDK Software
# is subject to the terms and conditions of the Boston Dynamics Software
# Development Kit License (20191101-BDSDK-SL).
from bosdyn.api import geometry_pb2
from . import math_helpers
VISION_FRAME_NAME = "vision"
BODY_FRAME_NAME = "body"
# Note that the frame name for a gravity aligned body frame is "flat_body" to create a shorter
# string identifier.
GRAV_ALIGNED_BODY_FRAME_NAME = "flat_body"
ODOM_FRAME_NAME = "odom"
SEED_FRAME_NAME = "seed"
GROUND_PLANE_FRAME_NAME = "gpe"
HAND_FRAME_NAME = "hand"
UNKNOWN_FRAME_NAME = "unknown"
RAYCAST_FRAME_NAME = "walkto_raycast_intersection"
TOOL_FRAME_NAME = "tool"
DESIRED_TOOL_FRAME_NAME = "desired_tool"
TASK_FRAME_NAME = "task"
DESIRED_TOOL_AT_END_FRAME_NAME = "desired_tool_at_end"
MEASURED_TOOL_AT_START_FRAME_NAME = "measured_tool_at_start"
GAZE_TARGET_FRAME_NAME = "gaze_target"
FRONT_LEFT_FOOT_FRAME_NAME = "fl_foot"
FRONT_RIGHT_FOOT_FRAME_NAME = "fr_foot"
HIND_LEFT_FOOT_FRAME_NAME = "hl_foot"
HIND_RIGHT_FOOT_FRAME_NAME = "hr_foot"
FOOT_FRAME_NAMES = [
FRONT_LEFT_FOOT_FRAME_NAME, FRONT_RIGHT_FOOT_FRAME_NAME, HIND_LEFT_FOOT_FRAME_NAME,
HIND_RIGHT_FOOT_FRAME_NAME
]
WR1_FRAME_NAME = "arm0.link_wr1"
WAYPOINT_FRAME_NAME = "waypoint"
class Error(Exception):
pass
class ValidateFrameTreeError(Error):
pass
class ValidateFrameTreeUnknownFrameError(ValidateFrameTreeError):
pass
class ValidateFrameTreeCycleError(ValidateFrameTreeError):
pass
class ValidateFrameTreeDisjointError(ValidateFrameTreeError):
pass
def validate_frame_tree_snapshot(frame_tree_snapshot):
"""Validates that a FrameTreeSnapshot is well-formed.
A FrameTreeSnapshot is expected to be a single tree, but poorly written
services can misuse the syntax to construct other data structures. The
syntax prevents DAGs from forming, but the data structure could
Valid FrameTrees must be a single-rooted tree. However, the general format of
repeated edges may not actually be valid - there could be cycles, disjoint
trees, or missing edges in the actual data structure.
Inputs:
frame_tree_snapshot: A snapshot of the data.
Returns:
True if valid
Raises:
ValidateFrameTreeError in a number of cases: Empty tree, invalid frame names in the tree, missing transforms
relating the two nodes, cycles in the tree, the tree is actually a DAG, and disconnected trees.
"""
if not frame_tree_snapshot:
raise ValueError('No frame_tree_snapshot')
# For every entry in the map, see if we can walk to the root without
# cycles. The root should also be identical for each node
def _walk_up_tree(frame_name):
cur_frame_name = frame_name
visited_frames = set()
visited_frames.add(cur_frame_name)
while True:
edge = frame_tree_snapshot.child_to_parent_edge_map.get(cur_frame_name)
if not edge:
raise ValidateFrameTreeUnknownFrameError()
if not edge.parent_frame_name:
# At the root of the tree
break
if edge.parent_frame_name in visited_frames:
raise ValidateFrameTreeCycleError()
visited_frames.add(edge.parent_frame_name)
cur_frame_name = edge.parent_frame_name
return cur_frame_name
root = None
if not frame_tree_snapshot.child_to_parent_edge_map:
raise ValidateFrameTreeError("Empty edges in FrameTreeSnapshot")
for frame_name in frame_tree_snapshot.child_to_parent_edge_map:
if not frame_name:
raise ValidateFrameTreeError("Empty child frame name")
cur_root = _walk_up_tree(frame_name)
if not root:
root = cur_root
else:
if not cur_root == root:
raise ValidateFrameTreeDisjointError()
return True
def get_a_tform_b(frame_tree_snapshot, frame_a, frame_b, validate=True):
"""Get the SE(3) pose representing the transform between frame_a and frame_b.
Using frame_tree_snapshot, find the math_helpers.SE3Pose to transform geometry from
frame_a's representation to frame_b's.
Args:
frame_tree_snapshot (dict) dictionary representing the child_to_parent_edge_map
frame_a (string)
frame_b (string)
validate (bool) if the FrameTreeSnapshot should be checked for a valid tree structure
Returns:
math_helpers.SE3Pose between frame_a and frame_b if they exist in the tree. None otherwise.
"""
if validate:
validate_frame_tree_snapshot(frame_tree_snapshot)
if frame_a not in frame_tree_snapshot.child_to_parent_edge_map:
return None
if frame_b not in frame_tree_snapshot.child_to_parent_edge_map:
return None
def _list_parent_edges(leaf_frame):
parent_edges = []
cur_frame = leaf_frame
while True:
parent_edge = frame_tree_snapshot.child_to_parent_edge_map.get(cur_frame)
if not parent_edge.parent_frame_name:
break
parent_edges.append(parent_edge)
cur_frame = parent_edge.parent_frame_name
return parent_edges
inverse_edges = _list_parent_edges(frame_a)
forward_edges = _list_parent_edges(frame_b)
# Possible optimization: Nearest common ancestor pruning.
def _accumulate_transforms(parent_edges):
ret = math_helpers.SE3Pose.from_identity()
for parent_edge in parent_edges:
ret = math_helpers.SE3Pose.from_proto(parent_edge.parent_tform_child) * ret
return ret
frame_a_tform_root_frame = _accumulate_transforms(inverse_edges).inverse()
root_frame_tform_frame_b = _accumulate_transforms(forward_edges)
return frame_a_tform_root_frame * root_frame_tform_frame_b
def get_se2_a_tform_b(frame_tree_snapshot, frame_a, frame_b, validate=True):
"""Get the SE(2) pose representing the transform between frame_a and frame_b.
Using frame_tree_snapshot, find the math_helpers.SE2Pose to transform geometry from
frame_a's representation to frame_b's.
Args:
frame_tree_snapshot (dict) dictionary representing the child_to_parent_edge_map
frame_a (string)
frame_b (string)
validate (bool) if the FrameTreeSnapshot should be checked for a valid tree structure
Returns:
math_helpers.SE2Pose between frame_a and frame_b if they exist in the tree and
frame_a is a gravity aligned frame. None otherwise.
"""
# Validate that the transform is in a gravity aligned frame based on the string name.
if not is_gravity_aligned_frame_name(frame_a):
# Frame A is not gravity aligned, and therefore a_tform_b cannot be converted into
# an SE(2) pose because it will lose height information.
return None
# Get the SE(3) pose from the frame tree snapshot representing the desired transform a_tform_b
se3_a_tform_b = get_a_tform_b(frame_tree_snapshot, frame_a, frame_b, validate)
if se3_a_tform_b is None:
# Failed to find the transformation between frames a and b in the frame tree snapshot.
return None
return se3_a_tform_b.get_closest_se2_transform()
def express_se2_velocity_in_new_frame(frame_tree_snapshot, frame_b, frame_c, vel_of_a_in_b,
validate=True):
"""Convert the SE2 Velocity in frame b to a SE2 Velocity in frame c using
the frame tree snapshot.
Args:
frame_tree_snapshot (dict) dictionary representing the child_to_parent_edge_map
frame_b (string)
frame_c (string)
vel_of_a_in_b (SE2Velocity proto) SE2 Velocity in frame_b
validate (bool) if the FrameTreeSnapshot should be checked for a valid tree structure
Returns:
math_helpers.SE2Velocity velocity_of_a_in_c in frame_c if the frames exist in the tree. None otherwise.
"""
# Find the SE(3) pose in the frame tree snapshot that represents c_tform_b.
se3_c_tform_b = get_a_tform_b(frame_tree_snapshot, frame_c, frame_b, validate)
if se3_c_tform_b is None:
# If the SE3Pose for c_tform_b does not exist in the frame tree snapshot,
# then we cannot transform the velocity.
return None
# Check that the frame name of frame_c is considered to be a gravity aligned frame.
if not is_gravity_aligned_frame_name(frame_c):
# Frame C is not gravity aligned, and therefore c_tform_b cannot be converted into
# an SE(2) pose because it will lose height information.
return None
# Find the closest SE(2) pose for the c_tform_b SE(3) pose found from the snapshot.
se2_c_tform_b = se3_c_tform_b.get_closest_se2_transform()
# Transform the velocity into the new frame to get vel_of_a_in_c.
c_adjoint_b = se2_c_tform_b.to_adjoint_matrix()
vel_of_a_in_c = math_helpers.transform_se2velocity(c_adjoint_b, vel_of_a_in_b)
return vel_of_a_in_c
def express_se3_velocity_in_new_frame(frame_tree_snapshot, frame_b, frame_c, vel_of_a_in_b,
validate=True):
"""Convert the SE(3) Velocity in frame b to an SE(3) Velocity in frame c using
the frame tree snapshot.
Args:
frame_tree_snapshot (dict) dictionary representing the child_to_parent_edge_map
frame_b (string)
frame_c (string)
vel_of_a_in_b (SE3Velocity proto) SE(3) Velocity in frame_b
validate (bool) if the FrameTreeSnapshot should be checked for a valid tree structure
Returns:
math_helpers.SE3Velocity velocity_of_a_in_c in frame_c if the frames exist in the tree. None otherwise.
"""
# Find the SE(3) pose in the frame tree snapshot that represents c_tform_b.
se3_c_tform_b = get_a_tform_b(frame_tree_snapshot, frame_c, frame_b, validate)
if se3_c_tform_b is None:
# If the SE3Pose for c_tform_b does not exist in the frame tree snapshot,
# then we cannot transform the velocity.
return None
# Transform the velocity into the new frame to get vel_of_a_in_c.
c_adjoint_b = se3_c_tform_b.to_adjoint_matrix()
vel_of_a_in_c = math_helpers.transform_se3velocity(c_adjoint_b, vel_of_a_in_b)
return vel_of_a_in_c
def get_odom_tform_body(frame_tree_snapshot):
"""Get the transformation between "odom" frame and "body" frame from the FrameTreeSnapshot."""
return get_a_tform_b(frame_tree_snapshot, ODOM_FRAME_NAME, BODY_FRAME_NAME)
def get_vision_tform_body(frame_tree_snapshot):
"""Get the transformation between "vision" frame and "body" frame from the FrameTreeSnapshot."""
return get_a_tform_b(frame_tree_snapshot, VISION_FRAME_NAME, BODY_FRAME_NAME)
class GenerateTreeError(Error):
pass
class ChildFrameInTree(GenerateTreeError):
pass
def add_edge_to_tree(frame_tree_snapshot, parent_tform_child, parent_frame_name, child_frame_name):
"""Appends a child/parent and the transform to the FrameTreeSnapshot.
Args:
frame_tree_snapshot (dict) dictionary representing the child_to_parent_edge_map
parent_tform_child (SE3Pose proto)
parent_frame_name (string)
child_frame_name (string)
"""
if child_frame_name in frame_tree_snapshot:
raise ChildFrameInTree
# Can add additional validation checks, such as if the parent frame will make a cycle,
# or if this will be completely disconnected
frame_tree_snapshot[child_frame_name] = geometry_pb2.FrameTreeSnapshot.ParentEdge(
parent_frame_name=parent_frame_name, parent_tform_child=parent_tform_child)
return frame_tree_snapshot
def get_frame_names(frame_tree_snapshot):
"""Returns a list of all known child or parent frames in the FrameTreeSnapshot."""
frame_names = []
for child_frame in frame_tree_snapshot.child_to_parent_edge_map:
if child_frame not in frame_names:
frame_names.append(child_frame)
parent_frame = frame_tree_snapshot.child_to_parent_edge_map[child_frame].parent_frame_name
if parent_frame not in frame_names:
frame_names.append(parent_frame)
return [frame_name for frame_name in frame_names if frame_name != ""]
def is_gravity_aligned_frame_name(frame_name):
"""Checks if the string frame name is a known gravity aligned frame."""
if frame_name == VISION_FRAME_NAME or frame_name == GRAV_ALIGNED_BODY_FRAME_NAME or frame_name == ODOM_FRAME_NAME:
return True
return False