Skip to content

Commit 45fb1e6

Browse files
yuvaltassaalimuldal
authored andcommitted
Add a .matrix property to mujoco.Camera that exposes the camera matrix
PiperOrigin-RevId: 330349436 Change-Id: Ib5295f0e3501b5d5c39e45c3eb2b09822322c311
1 parent 57475de commit 45fb1e6

3 files changed

Lines changed: 95 additions & 1 deletion

File tree

dm_control/mujoco/engine.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -686,6 +686,46 @@ def scene(self):
686686
"""Returns the `mujoco.MjvScene` instance used by the camera."""
687687
return self._scene
688688

689+
@property
690+
def matrix(self):
691+
"""Returns the 3x4 camera matrix.
692+
693+
For a description of the camera matrix see, e.g.,
694+
https://en.wikipedia.org/wiki/Camera_matrix.
695+
For a usage example, see the associated test.
696+
"""
697+
camera_id = self._render_camera.fixedcamid
698+
if camera_id == -1:
699+
# If the camera is a 'free' camera, we get its position and orientation
700+
# from the scene data structure. It is a stereo camera, so we average over
701+
# the left and right channels. Note: we call `self.update()` in order to
702+
# ensure that the contents of `scene.camera` are correct.
703+
self.update()
704+
pos = np.mean(self.scene.camera.pos, axis=0)
705+
z = -np.mean(self.scene.camera.forward, axis=0)
706+
y = np.mean(self.scene.camera.up, axis=0)
707+
rot = np.vstack((np.cross(y, z), y, z))
708+
fov = self._physics.model.vis.global_.fovy
709+
else:
710+
pos = self._physics.data.cam_xpos[camera_id]
711+
rot = self._physics.data.cam_xmat[camera_id].reshape(3, 3).T
712+
fov = self._physics.model.cam_fovy[camera_id]
713+
714+
# Translation matrix (4x4).
715+
translation = np.eye(4)
716+
translation[0:3, 3] = -pos
717+
# Rotation matrix (4x4).
718+
rotation = np.eye(4)
719+
rotation[0:3, 0:3] = rot
720+
# Focal transformation matrix (3x4).
721+
focal_scaling = (1./np.tan(np.deg2rad(fov)/2)) * self.height / 2.0
722+
focal = np.diag([-focal_scaling, focal_scaling, 1.0, 0])[0:3, :]
723+
# Image matrix (3x3).
724+
image = np.eye(3)
725+
image[0, 2] = (self.width - 1) / 2.0
726+
image[1, 2] = (self.height - 1) / 2.0
727+
return image.dot(focal).dot(rotation).dot(translation)
728+
689729
def update(self, scene_option=None):
690730
"""Updates geometry used for rendering.
691731

dm_control/mujoco/engine_test.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,60 @@ def testCameraSelection(self, coordinates, expected_selection):
191191
selected = camera.select(coordinates)
192192
self.assertEqual(expected_selection, selected[:2])
193193

194+
@parameterized.parameters(
195+
dict(camera_id='cam0', height=200, width=300),
196+
dict(camera_id=1, height=300, width=200),
197+
dict(camera_id=-1, height=400, width=400),
198+
)
199+
def testCameraMatrix(self, camera_id, height, width):
200+
"""Tests the camera_matrix() method.
201+
202+
Creates a model with two cameras and two small geoms. We render the scene
203+
with one of the cameras and check that the geom locations, projected into
204+
pixel space, are correct, using segmenation rendering.
205+
xyz2pixels() shows how the transformation is used. For a description
206+
of the camera matrix see https://en.wikipedia.org/wiki/Camera_matrix.
207+
208+
Args:
209+
camera_id: One of the two cameras. Can be either integer or String.
210+
height: The height of the image (pixels).
211+
width: The width of the image (pixels).
212+
"""
213+
214+
def xyz2pixels(x, y, z, camera_matrix):
215+
"""Transforms from world coordinates to pixel coordinates."""
216+
xs, ys, s = camera_matrix.dot(np.array([x, y, z, 1.0]))
217+
return xs/s, ys/s
218+
219+
two_geoms_and_two_cameras = """
220+
<mujoco>
221+
<visual>
222+
<global fovy="55"/>
223+
</visual>
224+
<worldbody>
225+
<light name="top" pos="0 0 1"/>
226+
<geom name="red" pos=".2 0 0" size=".005" rgba="1 0 0 1"/>
227+
<geom name="green" pos=".2 .2 .1" size=".005" rgba="0 1 0 1"/>
228+
<camera name="cam0" pos="1 .5 1" zaxis="1 .5 1" fovy="20"/>
229+
<camera name="cam1" pos=".1 .1 1" xyaxes="1 1 0 -1 0 0"/>
230+
</worldbody>
231+
</mujoco>
232+
"""
233+
physics = engine.Physics.from_xml_string(two_geoms_and_two_cameras)
234+
camera = engine.Camera(physics, width=width, height=height,
235+
camera_id=camera_id)
236+
camera_matrix = camera.matrix # Get camera matrix.
237+
pixels = camera.render(segmentation=True) # Render a segmentation frame.
238+
for geom_id in [0, 1]:
239+
# Compute the location of the geom in pixel space using the camera matrix.
240+
x, y = xyz2pixels(*physics.data.geom_xpos[geom_id], camera_matrix)
241+
row = int(round(y))
242+
column = int(round(x))
243+
# Compare segmentation values of nearest pixel to corresponding geom.
244+
[obj_id, obj_type] = pixels[row, column, :]
245+
self.assertEqual(obj_type, enums.mjtObj.mjOBJ_GEOM)
246+
self.assertEqual(obj_id, geom_id)
247+
194248
def testMovableCameraSetGetPose(self):
195249
height, width = 240, 320
196250

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ def is_excluded(s):
177177

178178
setup(
179179
name='dm_control',
180-
version='0.0.329943179',
180+
version='0.0.330349436',
181181
description='Continuous control environments and MuJoCo Python bindings.',
182182
author='DeepMind',
183183
license='Apache License, Version 2.0',

0 commit comments

Comments
 (0)