mujoco_py/modder.py (287 lines of code) (raw):
"""
Utilites for changing textures and materials after creating a
MuJoCo simulation. This allows for super fast scene generation.
"""
from collections import defaultdict
import numpy as np
from mujoco_py import cymj
class BaseModder():
def __init__(self, sim, random_state=None):
self.sim = sim
if random_state is None:
self.random_state = np.random.RandomState()
elif isinstance(random_state, int):
# random_state assumed to be an int
self.random_state = np.random.RandomState(random_state)
else:
self.random_state = random_state
@property
def model(self):
# Available for quick convenience access
return self.sim.model
class LightModder(BaseModder):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def set_pos(self, name, value):
lightid = self.get_lightid(name)
assert lightid > -1, "Unkwnown light %s" % name
value = list(value)
assert len(value) == 3, "Expected 3-dim value, got %s" % value
self.model.light_pos[lightid] = value
def set_dir(self, name, value):
lightid = self.get_lightid(name)
assert lightid > -1, "Unkwnown light %s" % name
value = list(value)
assert len(value) == 3, "Expected 3-dim value, got %s" % value
self.model.light_dir[lightid] = value
def set_active(self, name, value):
lightid = self.get_lightid(name)
assert lightid > -1, "Unkwnown light %s" % name
self.model.light_active[lightid] = value
def set_specular(self, name, value):
lightid = self.get_lightid(name)
assert lightid > -1, "Unkwnown light %s" % name
value = list(value)
assert len(value) == 3, "Expected 3-dim value, got %s" % value
self.model.light_specular[lightid] = value
def set_ambient(self, name, value):
lightid = self.get_lightid(name)
assert lightid > -1, "Unkwnown light %s" % name
value = list(value)
assert len(value) == 3, "Expected 3-dim value, got %s" % value
self.model.light_ambient[lightid] = value
def set_diffuse(self, name, value):
lightid = self.get_lightid(name)
assert lightid > -1, "Unkwnown light %s" % name
value = list(value)
assert len(value) == 3, "Expected 3-dim value, got %s" % value
self.model.light_diffuse[lightid] = value
def set_castshadow(self, name, value):
lightid = self.get_lightid(name)
assert lightid > -1, "Unkwnown light %s" % name
self.model.light_castshadow[lightid] = value
def get_lightid(self, name):
return self.model.light_name2id(name)
class CameraModder(BaseModder):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def set_fovy(self, name, value):
camid = self.get_camid(name)
assert 0 < value < 180
assert camid > -1, "Unknown camera %s" % name
self.model.cam_fovy[camid] = value
def get_quat(self, name):
camid = self.get_camid(name)
assert camid > -1, "Unknown camera %s" % name
return self.model.cam_quat[camid]
def set_quat(self, name, value):
value = list(value)
assert len(value) == 4, (
"Expectd value of length 3, instead got %s" % value)
camid = self.get_camid(name)
assert camid > -1, "Unknown camera %s" % name
self.model.cam_quat[camid] = value
def get_pos(self, name):
camid = self.get_camid(name)
assert camid > -1, "Unknown camera %s" % name
return self.model.cam_pos[camid]
def set_pos(self, name, value):
value = list(value)
assert len(value) == 3, (
"Expected value of length 3, instead got %s" % value)
camid = self.get_camid(name)
assert camid > -1
self.model.cam_pos[camid] = value
def get_camid(self, name):
return self.model.camera_name2id(name)
class MaterialModder(BaseModder):
"""
Modify material properties of a model. Example use:
sim = MjSim(...)
modder = MaterialModder(sim)
modder.set_specularity('some_geom', 0.5)
modder.rand_all('another_geom')
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def set_specularity(self, name, value):
assert 0 <= value <= 1.0
mat_id = self.get_mat_id(name)
self.model.mat_specular[mat_id] = value
def set_shininess(self, name, value):
assert 0 <= value <= 1.0
mat_id = self.get_mat_id(name)
self.model.mat_shininess[mat_id] = value
def set_reflectance(self, name, value):
assert 0 <= value <= 1.0
mat_id = self.get_mat_id(name)
self.model.mat_reflectance[mat_id] = value
def set_texrepeat(self, name, repeat_x, repeat_y):
mat_id = self.get_mat_id(name)
# ensure the following is set to false, so that repeats are
# relative to the extent of the body.
self.model.mat_texuniform[mat_id] = 0
self.model.mat_texrepeat[mat_id, :] = [repeat_x, repeat_y]
def rand_all(self, name):
self.rand_specularity(name)
self.rand_shininess(name)
self.rand_reflectance(name)
def rand_specularity(self, name):
value = 0.1 + 0.2 * self.random_state.uniform()
self.set_specularity(name, value)
def rand_shininess(self, name):
value = 0.1 + 0.5 * self.random_state.uniform()
self.set_shininess(name, value)
def rand_reflectance(self, name):
value = 0.1 + 0.5 * self.random_state.uniform()
self.set_reflectance(name, value)
def rand_texrepeat(self, name, max_repeat=5):
repeat_x = self.random_state.randint(0, max_repeat) + 1
repeat_y = self.random_state.randint(0, max_repeat) + 1
self.set_texrepeat(name, repeat_x, repeat_y)
def get_mat_id(self, name):
""" Returns the material id based on the geom name. """
geom_id = self.model.geom_name2id(name)
return self.model.geom_matid[geom_id]
class TextureModder(BaseModder):
"""
Modify textures in model. Example use:
sim = MjSim(...)
modder = TextureModder(sim)
modder.whiten_materials() # ensures materials won't impact colors
modder.set_checker('some_geom', (255, 0, 0), (0, 0, 0))
modder.rand_all('another_geom')
Note: in order for the textures to take full effect, you'll need to set
the rgba values for all materials to [1, 1, 1, 1], otherwise the texture
colors will be modulated by the material colors. Call the
`whiten_materials` helper method to set all material colors to white.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.textures = [Texture(self.model, i)
for i in range(self.model.ntex)]
self._build_tex_geom_map()
# These matrices will be used to rapidly synthesize
# checker pattern bitmaps
self._cache_checker_matrices()
def get_texture(self, name):
if name == 'skybox':
tex_id = -1
for i in range(self.model.ntex):
# TODO: Don't hardcode this
skybox_textype = 2
if self.model.tex_type[i] == skybox_textype:
tex_id = i
assert tex_id >= 0, "Model has no skybox"
else:
geom_id = self.model.geom_name2id(name)
mat_id = self.model.geom_matid[geom_id]
assert mat_id >= 0, "Geom has no assigned material"
tex_id = self.model.mat_texid[mat_id]
assert tex_id >= 0, "Material has no assigned texture"
texture = self.textures[tex_id]
return texture
def get_checker_matrices(self, name):
if name == 'skybox':
return self._skybox_checker_mat
else:
geom_id = self.model.geom_name2id(name)
return self._geom_checker_mats[geom_id]
def set_checker(self, name, rgb1, rgb2):
bitmap = self.get_texture(name).bitmap
cbd1, cbd2 = self.get_checker_matrices(name)
rgb1 = np.asarray(rgb1).reshape([1, 1, -1])
rgb2 = np.asarray(rgb2).reshape([1, 1, -1])
bitmap[:] = rgb1 * cbd1 + rgb2 * cbd2
self.upload_texture(name)
return bitmap
def set_gradient(self, name, rgb1, rgb2, vertical=True):
"""
Creates a linear gradient from rgb1 to rgb2.
Args:
- rgb1 (array): start color
- rgb2 (array): end color
- vertical (bool): if True, the gradient in the positive
y-direction, if False it's in the positive x-direction.
"""
# NOTE: MuJoCo's gradient uses a sigmoid. Here we simplify
# and just use a linear gradient... We could change this
# to just use a tanh-sigmoid if needed.
bitmap = self.get_texture(name).bitmap
h, w = bitmap.shape[:2]
if vertical:
p = np.tile(np.linspace(0, 1, h)[:, None], (1, w))
else:
p = np.tile(np.linspace(0, 1, w), (h, 1))
for i in range(3):
bitmap[..., i] = rgb2[i] * p + rgb1[i] * (1.0 - p)
self.upload_texture(name)
return bitmap
def set_rgb(self, name, rgb):
bitmap = self.get_texture(name).bitmap
bitmap[..., :] = np.asarray(rgb)
self.upload_texture(name)
return bitmap
def set_noise(self, name, rgb1, rgb2, fraction=0.9):
"""
Args:
- name (str): name of geom
- rgb1 (array): background color
- rgb2 (array): color of random noise foreground color
- fraction (float): fraction of pixels with foreground color
"""
bitmap = self.get_texture(name).bitmap
h, w = bitmap.shape[:2]
mask = self.random_state.uniform(size=(h, w)) < fraction
bitmap[..., :] = np.asarray(rgb1)
bitmap[mask, :] = np.asarray(rgb2)
self.upload_texture(name)
return bitmap
def randomize(self):
for name in self.sim.model.geom_names:
self.rand_all(name)
def rand_all(self, name):
choices = [
self.rand_checker,
self.rand_gradient,
self.rand_rgb,
self.rand_noise,
]
choice = self.random_state.randint(len(choices))
return choices[choice](name)
def rand_checker(self, name):
rgb1, rgb2 = self.get_rand_rgb(2)
return self.set_checker(name, rgb1, rgb2)
def rand_gradient(self, name):
rgb1, rgb2 = self.get_rand_rgb(2)
vertical = bool(self.random_state.uniform() > 0.5)
return self.set_gradient(name, rgb1, rgb2, vertical=vertical)
def rand_rgb(self, name):
rgb = self.get_rand_rgb()
return self.set_rgb(name, rgb)
def rand_noise(self, name):
fraction = 0.1 + self.random_state.uniform() * 0.8
rgb1, rgb2 = self.get_rand_rgb(2)
return self.set_noise(name, rgb1, rgb2, fraction)
def upload_texture(self, name):
"""
Uploads the texture to the GPU so it's available in the rendering.
"""
texture = self.get_texture(name)
if not self.sim.render_contexts:
cymj.MjRenderContextOffscreen(self.sim)
for render_context in self.sim.render_contexts:
render_context.upload_texture(texture.id)
def whiten_materials(self, geom_names=None):
"""
Helper method for setting all material colors to white, otherwise
the texture modifications won't take full effect.
Args:
- geom_names (list): list of geom names whose materials should be
set to white. If omitted, all materials will be changed.
"""
geom_names = geom_names or []
if geom_names:
for name in geom_names:
geom_id = self.model.geom_name2id(name)
mat_id = self.model.geom_matid[geom_id]
self.model.mat_rgba[mat_id, :] = 1.0
else:
self.model.mat_rgba[:] = 1.0
def get_rand_rgb(self, n=1):
def _rand_rgb():
return np.array(self.random_state.uniform(size=3) * 255,
dtype=np.uint8)
if n == 1:
return _rand_rgb()
else:
return tuple(_rand_rgb() for _ in range(n))
def _build_tex_geom_map(self):
# Build a map from tex_id to geom_ids, so we can check
# for collisions.
self._geom_ids_by_tex_id = defaultdict(list)
for geom_id in range(self.model.ngeom):
mat_id = self.model.geom_matid[geom_id]
if mat_id >= 0:
tex_id = self.model.mat_texid[mat_id]
if tex_id >= 0:
self._geom_ids_by_tex_id[tex_id].append(geom_id)
def _cache_checker_matrices(self):
"""
Cache two matrices of the form [[1, 0, 1, ...],
[0, 1, 0, ...],
...]
and [[0, 1, 0, ...],
[1, 0, 1, ...],
...]
for each texture. To use for fast creation of checkerboard patterns
"""
self._geom_checker_mats = []
for geom_id in range(self.model.ngeom):
mat_id = self.model.geom_matid[geom_id]
tex_id = self.model.mat_texid[mat_id]
texture = self.textures[tex_id]
h, w = texture.bitmap.shape[:2]
self._geom_checker_mats.append(self._make_checker_matrices(h, w))
# add skybox
skybox_tex_id = -1
for tex_id in range(self.model.ntex):
skybox_textype = 2
if self.model.tex_type[tex_id] == skybox_textype:
skybox_tex_id = tex_id
if skybox_tex_id >= 0:
texture = self.textures[skybox_tex_id]
h, w = texture.bitmap.shape[:2]
self._skybox_checker_mat = self._make_checker_matrices(h, w)
else:
self._skybox_checker_mat = None
def _make_checker_matrices(self, h, w):
re = np.r_[((w + 1) // 2) * [0, 1]]
ro = np.r_[((w + 1) // 2) * [1, 0]]
cbd1 = np.expand_dims(np.row_stack(((h + 1) // 2) * [re, ro]), -1)[:h, :w]
cbd2 = np.expand_dims(np.row_stack(((h + 1) // 2) * [ro, re]), -1)[:h, :w]
return cbd1, cbd2
# From mjtTexture
MJT_TEXTURE_ENUM = ['2d', 'cube', 'skybox']
class Texture():
"""
Helper class for operating on the MuJoCo textures.
"""
__slots__ = ['id', 'type', 'height', 'width', 'tex_adr', 'tex_rgb']
def __init__(self, model, tex_id):
self.id = tex_id
self.type = MJT_TEXTURE_ENUM[model.tex_type[tex_id]]
self.height = model.tex_height[tex_id]
self.width = model.tex_width[tex_id]
self.tex_adr = model.tex_adr[tex_id]
self.tex_rgb = model.tex_rgb
@property
def bitmap(self):
size = self.height * self.width * 3
data = self.tex_rgb[self.tex_adr:self.tex_adr + size]
return data.reshape((self.height, self.width, 3))