Skip to content

Commit

Permalink
[visualization] Add frame (aka triad) visualization function
Browse files Browse the repository at this point in the history
  • Loading branch information
jwnimmer-tri committed Apr 7, 2023
1 parent 3f9800b commit e112f3f
Show file tree
Hide file tree
Showing 5 changed files with 196 additions and 68 deletions.
11 changes: 11 additions & 0 deletions bindings/pydrake/visualization/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ drake_pybind_library(
"_meldis.py",
"_model_visualizer.py",
"_plotting.py",
"_triad.py",
"_video.py",
"_visualization_extra.py",
],
Expand Down Expand Up @@ -162,6 +163,16 @@ drake_py_unittest(
],
)

drake_py_unittest(
name = "triad_test",
data = [
"//multibody/benchmarks/acrobot:models",
],
deps = [
":visualization",
],
)

drake_py_unittest(
name = "video_test",
deps = [
Expand Down
79 changes: 11 additions & 68 deletions bindings/pydrake/visualization/_model_visualizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@
VisualizationConfig,
ApplyVisualizationConfig,
)
from pydrake.visualization._triad import (
AddFrameTriadIllustration,
)


class RunResult(Enum):
Expand Down Expand Up @@ -63,9 +66,9 @@ class ModelVisualizer:

def __init__(self, *,
visualize_frames=False,
triad_length=0.5,
triad_radius=0.01,
triad_opacity=1,
triad_length=0.3,
triad_radius=0.005,
triad_opacity=0.9,
publish_contacts=True,
browser_new=False,
pyplot=False,
Expand Down Expand Up @@ -248,7 +251,7 @@ def Finalize(self, position=None):
raise RuntimeError("Finalize has already been called.")

if self._visualize_frames:
# Find all the frames and draw them using _add_triad().
# Find all the frames and draw them.
# The frames are drawn using the parsed length.
# The world frame is drawn thicker than the rest.
inspector = self._builder.scene_graph().model_inspector()
Expand All @@ -257,8 +260,10 @@ def Finalize(self, position=None):
radius = self._triad_radius * (
3 if frame_id == world_id else 1
)
self._add_triad(
frame_id,
AddFrameTriadIllustration(
plant=self._builder.plant(),
scene_graph=self._builder.scene_graph(),
frame_id=frame_id,
length=self._triad_length,
radius=radius,
opacity=self._triad_opacity,
Expand Down Expand Up @@ -485,68 +490,6 @@ def _set_slider_values(self, slider_values):
if old_name in current_names:
self._meshcat.SetSliderValue(old_name, old_value)

def _add_triad(
self,
frame_id,
*,
length,
radius,
opacity,
X_FT=RigidTransform(),
name="frame",
):
"""
Adds illustration geometry representing the coordinate frame, with
the x-axis drawn in red, the y-axis in green and the z-axis in blue.
The axes point in +x, +y and +z directions, respectively.
Based on [code permalink](https://github.com/RussTedrake/manipulation/blob/5e59811/manipulation/scenarios.py#L367-L414).# noqa
Args:
frame_id: a geometry::frame_id registered with scene_graph.
length: the length of each axis in meters.
radius: the radius of each axis in meters.
opacity: the opacity of the coordinate axes, between 0 and 1.
X_FT: a RigidTransform from the triad frame T to the frame_id frame F
name: the added geometry will have names name + " x-axis", etc.
"""
# x-axis
X_TG = RigidTransform(
RotationMatrix.MakeYRotation(np.pi / 2),
[length / 2.0, 0, 0],
)
geom = GeometryInstance(
X_FT.multiply(X_TG), Cylinder(radius, length), name + " x-axis"
)
geom.set_illustration_properties(
MakePhongIllustrationProperties([1, 0, 0, opacity])
)
self._builder.scene_graph().RegisterGeometry(
self._builder.plant().get_source_id(), frame_id, geom)

# y-axis
X_TG = RigidTransform(
RotationMatrix.MakeXRotation(np.pi / 2),
[0, length / 2.0, 0],
)
geom = GeometryInstance(
X_FT.multiply(X_TG), Cylinder(radius, length), name + " y-axis"
)
geom.set_illustration_properties(
MakePhongIllustrationProperties([0, 1, 0, opacity])
)
self._builder.scene_graph().RegisterGeometry(
self._builder.plant().get_source_id(), frame_id, geom)

# z-axis
X_TG = RigidTransform([0, 0, length / 2.0])
geom = GeometryInstance(
X_FT.multiply(X_TG), Cylinder(radius, length), name + " z-axis"
)
geom.set_illustration_properties(
MakePhongIllustrationProperties([0, 0, 1, opacity])
)
self._builder.scene_graph().RegisterGeometry(
self._builder.plant().get_source_id(), frame_id, geom)

def _add_traffic_cone(self):
"""Adds a traffic cone to the scene, indicating a parsing error."""
base_width = 0.4
Expand Down
79 changes: 79 additions & 0 deletions bindings/pydrake/visualization/_triad.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import numpy as np

from pydrake.common.eigen_geometry import AngleAxis
from pydrake.geometry import (
Cylinder,
FrameId,
GeometryInstance,
MakePhongIllustrationProperties,
SceneGraph,
)
from pydrake.math import RigidTransform
from pydrake.multibody.plant import MultibodyPlant
from pydrake.multibody.tree import Body


def AddFrameTriadIllustration(
*,
scene_graph: SceneGraph,
body: Body = None,
frame_id: FrameId = None,
plant: MultibodyPlant = None,
name: str = "frame",
length: float = 0.3,
radius: float = 0.005,
opacity: float = 0.9,
X_FT: RigidTransform = RigidTransform(),
):
"""
Adds illustration geometry representing the given frame using an RGB triad,
with the x-axis drawn in red, the y-axis in green and the z-axis in blue.
The given frame can be either a geometry FrameId or a multibody Body.
Args:
scene_graph: the SceneGraph where geometry will be added.
body: when provided, illustrates the frame of the given body;
either body= or frame_id= must be provided, but not both.
frame_id: when provided, illustrates the given geometry.FrameId
registered with the given plant and scene_graph;
either body= or frame_id= must be provided, but not both.
plant: MultibodyPlant associated with the given frame_id;
required if and only if a frame_id= is being used (not body=).
name: the added geometries will have names "{name} x-axis", etc.
length: the length of each axis in meters.
radius: the radius of each axis in meters.
opacity: the opacity each axis, between 0.0 and 1.0.
X_FT: rigid transform relating frame T (the triad geometry) and
frame F (the given frame_id= or body= frame).
Returns:
The newly-added geometry ids for (x, y, z) respectively.
"""
if sum([body is not None, frame_id is not None]) != 1:
raise ValueError("Must provide either body= xor frame_id=")
if body is not None:
if plant is not None:
if plant is not body.GetParentPlant():
raise ValueError(
"Mismatched body= and plant=; remove the plant= arg")
else:
plant = body.GetParentPlant()
frame_id = plant.GetBodyFrameIdOrThrow(body.index())
source_id = plant.get_source_id()
eye = np.eye(3)
result = []
for i, char in enumerate(("x", "y", "z")):
geom_name = f"{name} {char}-axis"
# p_TG centers the cylinder halfway along the i'th axis.
p_TG = 0.5 * length * eye[i]
# R_TG rotates the canonical cylinder (aligned with +z) to align with
# the i'th axis instead. When i == 2, it spins the cylinder around the
# z axis, but this is effectively a no-op.
R_TG = AngleAxis(angle=np.pi/2, axis=eye[1-i])
X_FG = X_FT @ RigidTransform(R_TG, p_TG)
geom = GeometryInstance(X_FG, Cylinder(radius, length), geom_name)
phong = MakePhongIllustrationProperties(np.append(eye[i], [opacity]))
geom.set_illustration_properties(phong)
geometry_id = scene_graph.RegisterGeometry(source_id, frame_id, geom)
result.append(geometry_id)
return tuple(result)
1 change: 1 addition & 0 deletions bindings/pydrake/visualization/_visualization_extra.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Meldis,
)
from ._model_visualizer import (
AddFrameTriadIllustration,
ModelVisualizer,
)
from ._plotting import (
Expand Down
94 changes: 94 additions & 0 deletions bindings/pydrake/visualization/test/triad_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import unittest

import numpy as np

from pydrake.geometry import Meshcat, Rgba
from pydrake.math import RigidTransform, RotationMatrix
from pydrake.multibody.plant import MultibodyPlant
from pydrake.planning import RobotDiagramBuilder
import pydrake.visualization as mut


class TestTriad(unittest.TestCase):

def setUp(self):
# Create a simple robot.
builder = RobotDiagramBuilder()
builder.parser().AddModels(
url="package://drake/multibody/benchmarks/acrobot/acrobot.sdf")
robot = builder.Build()
self._plant = robot.plant()
self._scene_graph = robot.scene_graph()

def test_via_body(self):
# Illustrate the Link2 body frame.
x, y, z = mut.AddFrameTriadIllustration(
scene_graph=self._scene_graph,
body=self._plant.GetBodyByName("Link2"),
name="foo",
length=0.2,
radius=0.001,
opacity=0.5,
X_FT=RigidTransform())

# Check the geometry pose of each axis.
inspect = self._scene_graph.model_inspector()
np.testing.assert_equal(
inspect.GetPoseInFrame(x).GetAsMatrix34(),
RigidTransform(
RotationMatrix.MakeYRotation(np.pi/2),
[0.1, 0, 0]).GetAsMatrix34())
np.testing.assert_equal(
inspect.GetPoseInFrame(y).GetAsMatrix34(),
RigidTransform(
RotationMatrix.MakeXRotation(np.pi/2),
[0, 0.1, 0]).GetAsMatrix34())
np.testing.assert_equal(
inspect.GetPoseInFrame(z).GetAsMatrix34(),
RigidTransform(
RotationMatrix.MakeZRotation(np.pi/2),
[0, 0, 0.1]).GetAsMatrix34())

# Check the geometry details of each axis.
for i, char in enumerate(("x", "y", "z")):
geom_id = [x, y, z][i]
frame_name = inspect.GetName(inspect.GetFrameId(geom_id))
self.assertEqual(frame_name, "acrobot::Link2")
self.assertEqual(inspect.GetName(geom_id), f"foo {char}-axis")
self.assertEqual(inspect.GetShape(geom_id).length(), 0.2)
self.assertEqual(inspect.GetShape(geom_id).radius(), 0.001)
props = inspect.GetIllustrationProperties(geom_id)
self.assertEqual(props.GetProperty("phong", "diffuse"),
Rgba(r=(i == 0), g=(i == 1), b=(i == 2), a=0.5))

def test_via_frame_id(self):
# Illustrate the Link2 geometry frame.
body = self._plant.GetBodyByName("Link2")
expected_frame = self._plant.GetBodyFrameIdIfExists(body.index())
geom_ids = mut.AddFrameTriadIllustration(
plant=self._plant,
scene_graph=self._scene_graph,
frame_id=expected_frame)

# Check that all three geoms were added.
self.assertEqual(len(geom_ids), 3)
inspect = self._scene_graph.model_inspector()
for geom_id in geom_ids:
actual_frame = inspect.GetFrameId(geom_id)
self.assertEqual(actual_frame, expected_frame)

def test_redundant_plant_arg(self):
# Pass a plant= even though it's not needed.
# It's sliently cross-checked and accepted.
mut.AddFrameTriadIllustration(
plant=self._plant,
scene_graph=self._scene_graph,
body=self._plant.GetBodyByName("Link2"))

def test_wrong_plant_arg(self):
# Pass the wrong plant= value and get rejected.
with self.assertRaisesRegex(ValueError, "Mismatched"):
mut.AddFrameTriadIllustration(
plant=MultibodyPlant(0.0),
scene_graph=self._scene_graph,
body=self._plant.GetBodyByName("Link2"))

0 comments on commit e112f3f

Please sign in to comment.