Skip to content

Commit f0e56c3

Browse files
siegelncopybara-github
authored andcommitted
Implement all-player tracking cameras for soccer.
The current soccer cameras either have a fixed viewpoint or follow a single entity at a fixed distance. This CL implements all-player tracking cameras, which zooms in when players are close together and zooms out when they're farther apart. This provides a more detailed view without leaving anything out. There isn't a builtin Mujoco camera that does this, so this CL implements camera movement using composer. To avoid performance impact on experiments that aren't logging video, no tracking cameras are used by default. PiperOrigin-RevId: 304198694 Change-Id: I4ad8a6cfdc709d4e79b18c19b0e53d4339d9b6f4
1 parent 90a1f48 commit f0e56c3

4 files changed

Lines changed: 188 additions & 11 deletions

File tree

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# Lint as: python3
2+
#
3+
# Copyright 2019 The dm_control Authors.
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
# ============================================================================
17+
18+
"""Cameras for recording soccer videos."""
19+
20+
from dm_control.mujoco import engine
21+
import numpy as np
22+
23+
24+
class MultiplayerTrackingCamera(object):
25+
"""Camera that smoothly tracks multiple entities."""
26+
27+
def __init__(
28+
self,
29+
min_distance,
30+
distance_factor,
31+
smoothing_update_speed,
32+
azimuth=90,
33+
elevation=-45,
34+
width=1920,
35+
height=1080,
36+
):
37+
"""Construct a new MultiplayerTrackingcamera.
38+
39+
The target lookat point is the centroid of all tracked entities.
40+
Target camera distance is set to min_distance + distance_factor * d_max,
41+
where d_max is the maximum distance of any entity to the lookat point.
42+
43+
Args:
44+
min_distance: minimum camera distance.
45+
distance_factor: camera distance multiplier (see above).
46+
smoothing_update_speed: exponential filter parameter to smooth camera
47+
movement. 1 means no filter; smaller values mean less change per step.
48+
azimuth: constant azimuth to use for camera.
49+
elevation: constant elevation to use for camera.
50+
width: width to use for rendered video.
51+
height: height to use for rendered video.
52+
"""
53+
self._min_distance = min_distance
54+
self._distance_factor = distance_factor
55+
if smoothing_update_speed < 0 or smoothing_update_speed > 1:
56+
raise ValueError("Filter speed must be in range [0, 1].")
57+
self._smoothing_update_speed = smoothing_update_speed
58+
self._azimuth = azimuth
59+
self._elevation = elevation
60+
self._width = width
61+
self._height = height
62+
self._camera = None
63+
64+
@property
65+
def camera(self):
66+
return self._camera
67+
68+
def render(self):
69+
"""Render the current frame."""
70+
if self._camera is None:
71+
raise ValueError(
72+
"Camera has not been initialized yet."
73+
" render can only be called after physics has been compiled."
74+
)
75+
return self._camera.render()
76+
77+
def after_compile(self, physics):
78+
"""Instantiate the camera and ensure rendering buffer is large enough."""
79+
buffer_height = max(self._height, physics.model.vis.global_.offheight)
80+
buffer_width = max(self._height, physics.model.vis.global_.offwidth)
81+
physics.model.vis.global_.offheight = buffer_height
82+
physics.model.vis.global_.offwidth = buffer_width
83+
self._camera = engine.MovableCamera(
84+
physics, height=self._height, width=self._width)
85+
86+
def _get_target_camera_pose(self, entity_positions):
87+
"""Returns the pose that the camera should be pulled toward.
88+
89+
Args:
90+
entity_positions: list of numpy arrays representing current positions of
91+
the entities to be tracked.
92+
Returns: mujoco.engine.Pose representing the target camera pose.
93+
"""
94+
stacked_positions = np.stack(entity_positions)
95+
centroid = np.mean(stacked_positions, axis=0)
96+
radii = np.linalg.norm(stacked_positions - centroid, axis=1)
97+
assert len(radii) == len(entity_positions)
98+
camera_distance = self._min_distance + self._distance_factor * np.max(radii)
99+
return engine.Pose(
100+
lookat=centroid,
101+
distance=camera_distance,
102+
azimuth=self._azimuth,
103+
elevation=self._elevation,
104+
)
105+
106+
def initialize_episode(self, entity_positions):
107+
"""Begin the episode with the camera set to its target pose."""
108+
target_pose = self._get_target_camera_pose(entity_positions)
109+
self._camera.set_pose(*target_pose)
110+
111+
def after_step(self, entity_positions):
112+
"""Move camera toward its target poses."""
113+
target_pose = self._get_target_camera_pose(entity_positions)
114+
cur_pose = self._camera.get_pose()
115+
smoothing_update_speed = self._smoothing_update_speed
116+
filtered_pose = [
117+
target_val * smoothing_update_speed + \
118+
current_val * (1 - smoothing_update_speed)
119+
for target_val, current_val in zip(target_pose, cur_pose)
120+
]
121+
self._camera.set_pose(*filtered_pose)

‎dm_control/locomotion/soccer/task.py‎

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# Lint as: python3
2+
#
13
# Copyright 2019 The dm_control Authors.
24
#
35
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -40,16 +42,19 @@ def _disable_geom_contacts(entities):
4042
class Task(composer.Task):
4143
"""A task where two teams of walkers play soccer."""
4244

43-
def __init__(self,
44-
players,
45-
arena,
46-
ball=None,
47-
initializer=None,
48-
observables=None,
49-
disable_walker_contacts=False,
50-
nconmax_per_player=200,
51-
njmax_per_player=200,
52-
control_timestep=0.025):
45+
def __init__(
46+
self,
47+
players,
48+
arena,
49+
ball=None,
50+
initializer=None,
51+
observables=None,
52+
disable_walker_contacts=False,
53+
nconmax_per_player=200,
54+
njmax_per_player=200,
55+
control_timestep=0.025,
56+
tracking_cameras=(),
57+
):
5358
"""Construct an instance of soccer.Task.
5459
5560
This task implements the high-level game logic of multi-agent MuJoCo soccer.
@@ -77,6 +82,8 @@ def __init__(self,
7782
player. It may be necessary to increase this value if you encounter
7883
errors due to `mjWARN_CNSTRFULL`.
7984
control_timestep: control timestep of the agent.
85+
tracking_cameras: a sequence of `camera.MultiplayerTrackingCamera`
86+
instances to track the players and ball.
8087
"""
8188
self.arena = arena
8289
self.players = players
@@ -99,6 +106,8 @@ def __init__(self,
99106
# Add per-walkers observables.
100107
self._observables(self, player)
101108

109+
self._tracking_cameras = tracking_cameras
110+
102111
self.set_timesteps(
103112
physics_timestep=0.005, control_timestep=control_timestep)
104113
self.root_entity.mjcf_model.size.nconmax = nconmax_per_player * len(players)
@@ -120,12 +129,33 @@ def _throw_in(self, physics, random_state, ball):
120129
physics, velocity=np.zeros(3), angular_velocity=np.zeros(3))
121130
ball.initialize_entity_trackers()
122131

132+
def _tracked_entity_positions(self, physics):
133+
"""Return a list of the positions of the ball and all players."""
134+
ball_pos, unused_ball_quat = self.ball.get_pose(physics)
135+
entity_positions = [ball_pos]
136+
for player in self.players:
137+
walker_pos, unused_walker_quat = player.walker.get_pose(physics)
138+
entity_positions.append(walker_pos)
139+
return entity_positions
140+
141+
def after_compile(self, physics, random_state):
142+
super(Task, self).after_compile(physics, random_state)
143+
for camera in self._tracking_cameras:
144+
camera.after_compile(physics)
145+
146+
def after_step(self, physics, random_state):
147+
super(Task, self).after_step(physics, random_state)
148+
for camera in self._tracking_cameras:
149+
camera.after_step(self._tracked_entity_positions(physics))
150+
123151
def initialize_episode_mjcf(self, random_state):
124152
self.arena.initialize_episode_mjcf(random_state)
125153

126154
def initialize_episode(self, physics, random_state):
127155
self.arena.initialize_episode(physics, random_state)
128156
self._initializer(self, physics, random_state)
157+
for camera in self._tracking_cameras:
158+
camera.initialize_episode(self._tracked_entity_positions(physics))
129159

130160
@property
131161
def root_entity(self):

‎dm_control/locomotion/soccer/task_test.py‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from dm_control import composer
2828
from dm_control import mjcf
2929
from dm_control.locomotion import soccer
30+
from dm_control.locomotion.soccer import camera
3031
from dm_control.locomotion.soccer import initializers
3132
from dm_control.mujoco.wrapper import mjbindings
3233
import numpy as np
@@ -424,6 +425,31 @@ def _initial_configuration(physics, unused_random_state):
424425

425426
self.assertEqual(timestep.discount, expected_terminal_discount)
426427

428+
@parameterized.named_parameters(("reset_only", False), ("step", True))
429+
def test_render(self, take_step):
430+
height = 100
431+
width = 150
432+
tracking_cameras = []
433+
for min_distance in [1, 1, 2]:
434+
tracking_cameras.append(
435+
camera.MultiplayerTrackingCamera(
436+
min_distance=min_distance,
437+
distance_factor=1,
438+
smoothing_update_speed=0.1,
439+
width=width,
440+
height=height,
441+
))
442+
env = _env(_home_team(1) + _away_team(1), tracking_cameras=tracking_cameras)
443+
env.reset()
444+
if take_step:
445+
actions = [np.zeros(s.shape, s.dtype) for s in env.action_spec()]
446+
env.step(actions)
447+
rendered_frames = [cam.render() for cam in tracking_cameras]
448+
for frame in rendered_frames:
449+
assert frame.shape == (height, width, 3)
450+
self.assertTrue(np.array_equal(rendered_frames[0], rendered_frames[1]))
451+
self.assertFalse(np.array_equal(rendered_frames[1], rendered_frames[2]))
452+
427453

428454
class UniformInitializerTest(parameterized.TestCase):
429455

‎setup.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ def find_data_files(package_dir, patterns):
166166

167167
setup(
168168
name='dm_control',
169-
version='0.0.303130558',
169+
version='0.0.304198694',
170170
description='Continuous control environments and MuJoCo Python bindings.',
171171
author='DeepMind',
172172
license='Apache License, Version 2.0',

0 commit comments

Comments
 (0)