egogym 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. baselines/pi_policy.py +110 -0
  2. baselines/rum/__init__.py +1 -0
  3. baselines/rum/loss_fns/__init__.py +37 -0
  4. baselines/rum/loss_fns/abstract_loss_fn.py +13 -0
  5. baselines/rum/loss_fns/diffusion_policy_loss_fn.py +114 -0
  6. baselines/rum/loss_fns/rvq_loss_fn.py +104 -0
  7. baselines/rum/loss_fns/vqbet_loss_fn.py +202 -0
  8. baselines/rum/models/__init__.py +1 -0
  9. baselines/rum/models/bet/__init__.py +3 -0
  10. baselines/rum/models/bet/bet.py +347 -0
  11. baselines/rum/models/bet/gpt.py +277 -0
  12. baselines/rum/models/bet/tokenized_bet.py +454 -0
  13. baselines/rum/models/bet/utils.py +124 -0
  14. baselines/rum/models/bet/vqbet.py +410 -0
  15. baselines/rum/models/bet/vqvae/__init__.py +3 -0
  16. baselines/rum/models/bet/vqvae/residual_vq.py +346 -0
  17. baselines/rum/models/bet/vqvae/vector_quantize_pytorch.py +1194 -0
  18. baselines/rum/models/bet/vqvae/vqvae.py +313 -0
  19. baselines/rum/models/bet/vqvae/vqvae_utils.py +30 -0
  20. baselines/rum/models/custom.py +33 -0
  21. baselines/rum/models/encoders/__init__.py +0 -0
  22. baselines/rum/models/encoders/abstract_base_encoder.py +70 -0
  23. baselines/rum/models/encoders/identity.py +45 -0
  24. baselines/rum/models/encoders/timm_encoders.py +82 -0
  25. baselines/rum/models/policies/diffusion_policy.py +881 -0
  26. baselines/rum/models/policies/open_loop.py +122 -0
  27. baselines/rum/models/policies/simple_open_loop.py +108 -0
  28. baselines/rum/molmo/server.py +144 -0
  29. baselines/rum/policy.py +293 -0
  30. baselines/rum/utils/__init__.py +212 -0
  31. baselines/rum/utils/action_transforms.py +22 -0
  32. baselines/rum/utils/decord_transforms.py +135 -0
  33. baselines/rum/utils/rpc.py +249 -0
  34. baselines/rum/utils/schedulers.py +71 -0
  35. baselines/rum/utils/trajectory_vis.py +128 -0
  36. baselines/rum/utils/zmq_utils.py +281 -0
  37. baselines/rum_policy.py +108 -0
  38. egogym/__init__.py +8 -0
  39. egogym/assets/constants.py +1804 -0
  40. egogym/components/__init__.py +1 -0
  41. egogym/components/object.py +94 -0
  42. egogym/egogym.py +106 -0
  43. egogym/embodiments/__init__.py +10 -0
  44. egogym/embodiments/arms/__init__.py +4 -0
  45. egogym/embodiments/arms/arm.py +65 -0
  46. egogym/embodiments/arms/droid.py +49 -0
  47. egogym/embodiments/grippers/__init__.py +4 -0
  48. egogym/embodiments/grippers/floating_gripper.py +58 -0
  49. egogym/embodiments/grippers/rum.py +6 -0
  50. egogym/embodiments/robot.py +95 -0
  51. egogym/evaluate.py +216 -0
  52. egogym/managers/__init__.py +2 -0
  53. egogym/managers/objects_managers.py +30 -0
  54. egogym/managers/textures_manager.py +21 -0
  55. egogym/misc/molmo_client.py +49 -0
  56. egogym/misc/molmo_server.py +197 -0
  57. egogym/policies/__init__.py +1 -0
  58. egogym/policies/base_policy.py +13 -0
  59. egogym/scripts/analayze.py +834 -0
  60. egogym/scripts/plot.py +87 -0
  61. egogym/scripts/plot_correlation.py +392 -0
  62. egogym/scripts/plot_correlation_hardcoded.py +338 -0
  63. egogym/scripts/plot_failure.py +248 -0
  64. egogym/scripts/plot_failure_hardcoded.py +195 -0
  65. egogym/scripts/plot_failure_vlm.py +257 -0
  66. egogym/scripts/plot_failure_vlm_hardcoded.py +177 -0
  67. egogym/scripts/plot_line.py +303 -0
  68. egogym/scripts/plot_line_hardcoded.py +285 -0
  69. egogym/scripts/plot_pi0_bars.py +169 -0
  70. egogym/tasks/close.py +84 -0
  71. egogym/tasks/open.py +85 -0
  72. egogym/tasks/pick.py +121 -0
  73. egogym/utils.py +969 -0
  74. egogym/wrappers/__init__.py +20 -0
  75. egogym/wrappers/episode_monitor.py +282 -0
  76. egogym/wrappers/unprivileged_chatgpt.py +163 -0
  77. egogym/wrappers/unprivileged_gemini.py +157 -0
  78. egogym/wrappers/unprivileged_molmo.py +88 -0
  79. egogym/wrappers/unprivileged_moondream.py +121 -0
  80. egogym-0.1.0.dist-info/METADATA +52 -0
  81. egogym-0.1.0.dist-info/RECORD +83 -0
  82. egogym-0.1.0.dist-info/WHEEL +5 -0
  83. egogym-0.1.0.dist-info/top_level.txt +2 -0
egogym/utils.py ADDED
@@ -0,0 +1,969 @@
1
+ import math
2
+ import numpy as np
3
+ import torch
4
+ import hydra
5
+ from scipy.spatial.transform import Rotation as R
6
+ import os
7
+ import xml.etree.ElementTree as ET
8
+ import gdown
9
+ import zipfile
10
+ import random
11
+ from PIL import Image
12
+ import importlib.util
13
+ import sys
14
+ from pathlib import Path
15
+
16
+
17
+ def load_policy_from_file(policy_path, config_overrides=None):
18
+ policy_path = Path(policy_path)
19
+
20
+ spec = importlib.util.spec_from_file_location("policy_module", policy_path)
21
+ policy_module = importlib.util.module_from_spec(spec)
22
+ sys.modules["policy_module"] = policy_module
23
+ spec.loader.exec_module(policy_module)
24
+
25
+ config = policy_module.get_config()
26
+
27
+ if config_overrides:
28
+ for key, value in config_overrides.items():
29
+ config[key] = value
30
+
31
+ policy_class = None
32
+ for attr_name in dir(policy_module):
33
+ attr = getattr(policy_module, attr_name)
34
+ if isinstance(attr, type) and attr_name.endswith('Policy') and attr_name != 'BasePolicy':
35
+ if hasattr(attr, '__module__') and attr.__module__ == 'policy_module':
36
+ policy_class = attr
37
+ break
38
+
39
+ if policy_class is None:
40
+ raise ValueError(f"No policy class found in {policy_path}")
41
+
42
+ return policy_class(config=config)
43
+
44
+
45
+ def maybe_download():
46
+ objects_path = os.path.join(os.path.dirname(__file__), "assets/objects")
47
+ if not os.path.exists(objects_path):
48
+ print("Downloading Objects...")
49
+ download_and_extract_zip(
50
+ gdrive_link="https://drive.google.com/file/d/1wDczBM6H3rQvFUGKe_vQINE5Bm2fBlU4",
51
+ output_dir=os.path.join(os.path.dirname(__file__), "assets"),
52
+ )
53
+ return objects_path
54
+
55
+ def get_textures(root_folder=None):
56
+ results = []
57
+ if root_folder is None:
58
+ root_folder = os.path.join(os.path.dirname(__file__), "assets/textures")
59
+
60
+ for dirpath, _, filenames in os.walk(root_folder):
61
+ for filename in filenames:
62
+ if filename.endswith('.png'):
63
+ full_path = os.path.join(dirpath, filename)
64
+ rel_path = os.path.relpath(full_path, root_folder)
65
+ results.append(rel_path)
66
+
67
+ return results
68
+
69
+ def get_objects(root_folder=None):
70
+ results = []
71
+ if root_folder is None:
72
+ root_folder = maybe_download()
73
+
74
+ for dirpath, _, filenames in os.walk(root_folder):
75
+ if "model.xml" in filenames:
76
+ full_path = os.path.join(dirpath, "model.xml")
77
+ parent_folder = os.path.basename(dirpath)
78
+ results.append((parent_folder, full_path))
79
+
80
+ results.append(("cabinet", "procedurally_generated"))
81
+ results.append(("drawer", "procedurally_generated"))
82
+
83
+ return results
84
+
85
+ def make_objects_manager(objects_set, np_random, shuffle=True):
86
+ from egogym.managers import ObjectsManager
87
+ objects = []
88
+ available_objects = get_objects()
89
+ available_objects = {obj[0]: obj[1] for obj in available_objects}
90
+ for obj_name in objects_set:
91
+ if obj_name in available_objects:
92
+ objects.append((obj_name, available_objects[obj_name]))
93
+ return ObjectsManager(objects, np_random, shuffle=shuffle)
94
+
95
+ def make_textures_manager(textures_set, np_random):
96
+ from egogym.managers import TexturesManager
97
+ textures = []
98
+ available_textures = get_textures(root_folder=os.path.join(os.path.dirname(__file__), "assets/textures"))
99
+ for tex_path in textures_set:
100
+ for available_tex_name in available_textures:
101
+ if tex_path in available_tex_name:
102
+ textures.append(tex_path)
103
+ return TexturesManager(textures, np_random)
104
+
105
+ def include_in_scene(scene_xml_content, include_file_path):
106
+ root = ET.fromstring(scene_xml_content)
107
+ new_include = ET.Element("include", file=include_file_path)
108
+ includes = root.findall("include")
109
+ if includes:
110
+ index = list(root).index(includes[-1]) + 1
111
+ root.insert(index, new_include)
112
+ else:
113
+ root.insert(0, new_include)
114
+ return ET.tostring(root, encoding="unicode")
115
+
116
+ def download_and_extract_zip(gdrive_link, output_dir):
117
+ os.makedirs(output_dir, exist_ok=True)
118
+ file_id = gdrive_link.split("/d/")[1].split("/")[0]
119
+ download_url = f"https://drive.google.com/uc?id={file_id}"
120
+ zip_path = os.path.join(output_dir, "temp_download.zip")
121
+
122
+ gdown.download(download_url, zip_path, quiet=False)
123
+
124
+ with zipfile.ZipFile(zip_path, "r") as zip_ref:
125
+ zip_ref.extractall(output_dir)
126
+
127
+ os.remove(zip_path)
128
+
129
+ def euler_to_mat_xyz(rx, ry, rz):
130
+ cx = torch.cos(rx)
131
+ sx = torch.sin(rx)
132
+ cy = torch.cos(ry)
133
+ sy = torch.sin(ry)
134
+ cz = torch.cos(rz)
135
+ sz = torch.sin(rz)
136
+ B = rx.shape[0]
137
+ Rmats = torch.empty(B, 3, 3, device=rx.device)
138
+ Rmats[:, 0, 0] = cy * cz
139
+ Rmats[:, 0, 1] = -cy * sz
140
+ Rmats[:, 0, 2] = sy
141
+ Rmats[:, 1, 0] = cx * sz + cz * sx * sy
142
+ Rmats[:, 1, 1] = cx * cz - sx * sy * sz
143
+ Rmats[:, 1, 2] = -cy * sx
144
+ Rmats[:, 2, 0] = sx * sz - cx * cz * sy
145
+ Rmats[:, 2, 1] = cz * sx + cx * sy * sz
146
+ Rmats[:, 2, 2] = cx * cy
147
+ return Rmats
148
+
149
+ def make_pose(pos, rot):
150
+ pose = np.eye(4)
151
+ pose[:3, 3] = pos
152
+ pose[:3, :3] = rot.as_matrix()
153
+ return pose
154
+
155
+ def get_pos_rot(data, name, type):
156
+ type_lower = type.lower()
157
+ if type_lower == "body-control":
158
+ pos = data.body(name).xpos.copy()
159
+ rot_mat = data.body(name).xmat.copy().reshape((3, 3))
160
+ return pos, rot_mat
161
+ if type_lower == "body":
162
+ pos = data.body(name).xipos.copy()
163
+ rot_mat = data.body(name).xmat.copy().reshape((3, 3))
164
+ return pos, rot_mat
165
+ elif type_lower == "site":
166
+ pos = data.site(name).xpos.copy()
167
+ rot_mat = data.site(name).xmat.copy().reshape((3, 3))
168
+ return pos, rot_mat
169
+ elif type_lower == "geom":
170
+ pos = data.geom(name).xpos.copy()
171
+ rot_mat = data.geom(name).xmat.copy().reshape((3, 3))
172
+ return pos, rot_mat
173
+ elif type_lower == "camera":
174
+ cam_id = data.model.camera(name).id
175
+ pos = data.cam_xpos[cam_id].copy()
176
+ rot_mat = data.cam_xmat[cam_id].copy().reshape((3, 3))
177
+ return pos, rot_mat
178
+ else:
179
+ raise ValueError(
180
+ f'type_ must be one of "geom", "body", or "site", got "{type}".'
181
+ )
182
+
183
+ def get_pose(data, name, type):
184
+ pos, rot_mat = get_pos_rot(data, name, type)
185
+ pose = np.eye(4)
186
+ pose[:3, 3] = pos
187
+ pose[:3, :3] = rot_mat
188
+ return pose
189
+
190
+ def position_sampler(
191
+ np_random,
192
+ num_samples,
193
+ box_half_ranges,
194
+ min_distance,
195
+ max_total_tries=100,
196
+ start_position=None,
197
+ thickness=0.1,
198
+ ):
199
+ hx, hy = map(float, box_half_ranges)
200
+ samples = []
201
+ samples_arr = []
202
+ cell = float(min_distance) / np.sqrt(2.0)
203
+ ox, oy = -hx, -hy
204
+ grid = {}
205
+
206
+ def to_cell_idx(p):
207
+ return (
208
+ int(np.floor((p[0] - ox) / cell)),
209
+ int(np.floor((p[1] - oy) / cell)),
210
+ )
211
+
212
+ def is_far_enough(p):
213
+ cx, cy = to_cell_idx(p)
214
+ for dx in (-1, 0, 1):
215
+ for dy in (-1, 0, 1):
216
+ key = (cx + dx, cy + dy)
217
+ j = grid.get(key)
218
+ if j is None:
219
+ continue
220
+ q = samples_arr[j]
221
+ if (p[0] - q[0]) ** 2 + (p[1] - q[1]) ** 2 < min_distance**2:
222
+ return False
223
+ return True
224
+
225
+ tries = 0
226
+ while len(samples) < num_samples and tries < max_total_tries:
227
+ tries += 1
228
+ px = np_random.uniform(-hx, hx)
229
+ py = np_random.uniform(-hy, hy)
230
+ p = (px, py)
231
+ if not samples_arr:
232
+ samples.append([px, py])
233
+ samples_arr.append(np.array(p, dtype=float))
234
+ grid[to_cell_idx(p)] = 0
235
+ continue
236
+ if is_far_enough(p):
237
+ idx = len(samples)
238
+ samples.append([px, py])
239
+ samples_arr.append(np.array(p, dtype=float))
240
+ grid[to_cell_idx(p)] = idx
241
+
242
+ if start_position is None:
243
+ return samples
244
+
245
+ A = np.array(start_position, dtype=float)
246
+ blocked = [False] * len(samples)
247
+ eps = 1e-9
248
+
249
+ S = np.array(samples, dtype=float)
250
+ for i in range(len(samples)):
251
+ B = S[i]
252
+ AB = B - A
253
+ ab2 = float(AB[0] * AB[0] + AB[1] * AB[1])
254
+ if ab2 <= eps:
255
+ blocked[i] = False
256
+ continue
257
+ for j in range(len(samples)):
258
+ if j == i:
259
+ continue
260
+ P = S[j]
261
+ t = float(np.dot(P - A, AB) / ab2)
262
+ if t <= eps or t >= 1.0 - eps:
263
+ continue
264
+ C = A + t * AB
265
+ if float(np.sum((P - C) ** 2)) <= thickness * thickness:
266
+ blocked[i] = True
267
+ break
268
+
269
+ if blocked[0]:
270
+ for i in range(1, len(blocked)):
271
+ if not blocked[i]:
272
+ samples[0], samples[i] = samples[i], samples[0]
273
+ blocked[0], blocked[i] = blocked[i], blocked[0]
274
+ break
275
+ return samples
276
+
277
+
278
+ def add_xml_to_scene(original_xml_string, new_xml_string):
279
+ """
280
+ Merges new_xml_string into original_xml_string by adding elements from corresponding sections.
281
+ Elements from new XML's worldbody go under original's worldbody, assets under assets, etc.
282
+ """
283
+
284
+ original_root = ET.fromstring(original_xml_string)
285
+ new_root = ET.fromstring(new_xml_string)
286
+
287
+ for new_section in new_root:
288
+ section_name = new_section.tag
289
+
290
+ original_section = original_root.find(section_name)
291
+
292
+ if original_section is not None:
293
+ for child in new_section:
294
+ original_section.append(child)
295
+ else:
296
+ original_root.append(new_section)
297
+
298
+ return ET.tostring(original_root, encoding="unicode")
299
+
300
+ def make_handle_xml(
301
+ handle_type="pullbar",
302
+ handle_name="handle",
303
+ position=(0, 0, 0),
304
+ euler_orientation=(0, 0, 0),
305
+ handle_radius=0.009,
306
+ handle_length=0.05,
307
+ handle_material="M_hinge_metal",
308
+ mass=0.1,
309
+ friction_geom="0 0 0",
310
+ hinge_side="left",
311
+ np_random=None,
312
+ ):
313
+ pos_x, pos_y, pos_z = position
314
+ euler_x, euler_y, euler_z = euler_orientation
315
+
316
+ if handle_type == "knob":
317
+ if handle_name == "handle":
318
+ knob_geom_name = "handle"
319
+ knob_geom_viz_name = "handle_viz"
320
+ else:
321
+ knob_geom_name = f"knob_geo_{hinge_side}"
322
+ knob_geom_viz_name = f"knob_geom_viz_{hinge_side}"
323
+ friction_geom = "5 5 5"
324
+ handle_body_xml = f"""
325
+ <body name="{handle_name}_body" pos="{pos_x + (-0.02 if hinge_side == "left" else 0.02)} {pos_y + 0.07} {pos_z}" euler="{euler_x} {euler_y} 0">
326
+ <body name="knob">
327
+ <geom name="{knob_geom_name}" mesh="knob" class="visual" material="{handle_material}"/>
328
+ <geom name="{knob_geom_viz_name}" mesh="knob_collision_0" rgba="0.12319198224928918 0.8185897489755214 0.9473380251850289 1" class="collision" friction="{friction_geom}" mass="{mass}"/>
329
+ <geom mesh="knob_collision_1" rgba="0.9952048472348035 0.9587846043226812 0.44813684566262046 1" class="collision" friction="{friction_geom}" mass="{mass}"/>
330
+ <geom mesh="knob_collision_2" rgba="0.5562189501844684 0.3964830683252689 0.24776822693559275 1" class="collision" friction="{friction_geom}" mass="{mass}"/>
331
+ <geom mesh="knob_collision_3" rgba="0.5665588588501549 0.4670123537790013 0.1620234740959262 1" class="collision" friction="{friction_geom}" mass="{mass}"/>
332
+ <geom mesh="knob_collision_4" rgba="0.6170081600309119 0.6175499636469116 0.07870231532672867 1" class="collision" friction="{friction_geom}" mass="{mass}"/>
333
+ <geom mesh="knob_collision_5" rgba="0.5008884031488108 0.5029776170199916 0.7335611007545255 1" class="collision" friction="{friction_geom}" mass="{mass}"/>
334
+ <geom mesh="knob_collision_6" rgba="0.6962939670098736 0.9990710674845018 0.1259585452380786 1" class="collision" friction="{friction_geom}" mass="{mass}"/>
335
+ <geom mesh="knob_collision_7" rgba="0.25804299152558585 0.3180969182215061 0.9745095357400368 1" class="collision" friction="{friction_geom}" mass="{mass}"/>
336
+ <geom mesh="knob_collision_8" rgba="0.012626418834853048 0.6472133449453128 0.6250494987727182 1" class="collision" friction="{friction_geom}" mass="{mass}"/>
337
+ <geom mesh="knob_collision_9" rgba="0.5557254592689047 0.6025541199284764 0.902390111497001 1" class="collision" friction="{friction_geom}" mass="{mass}"/>
338
+ </body>
339
+ </body>"""
340
+ scale = np_random.uniform(0.012, 0.0135)
341
+ base_path = os.path.join(os.path.dirname(__file__), "assets/meshes")
342
+ mesh_asset_xml = f"""
343
+ <mesh file="{base_path}/knob/knob.obj" scale="{scale} {scale} {scale}"/>
344
+ <mesh file="{base_path}/knob/knob_collision_0.obj" scale="{scale} {scale} {scale}"/>
345
+ <mesh file="{base_path}/knob/knob_collision_1.obj" scale="{scale} {scale} {scale}"/>
346
+ <mesh file="{base_path}/knob/knob_collision_2.obj" scale="{scale} {scale} {scale}"/>
347
+ <mesh file="{base_path}/knob/knob_collision_3.obj" scale="{scale} {scale} {scale}"/>
348
+ <mesh file="{base_path}/knob/knob_collision_4.obj" scale="{scale} {scale} {scale}"/>
349
+ <mesh file="{base_path}/knob/knob_collision_5.obj" scale="{scale} {scale} {scale}"/>
350
+ <mesh file="{base_path}/knob/knob_collision_6.obj" scale="{scale} {scale} {scale}"/>
351
+ <mesh file="{base_path}/knob/knob_collision_7.obj" scale="{scale} {scale} {scale}"/>
352
+ <mesh file="{base_path}/knob/knob_collision_8.obj" scale="{scale} {scale} {scale}"/>
353
+ <mesh file="{base_path}/knob/knob_collision_9.obj" scale="{scale} {scale} {scale}"/>
354
+ """
355
+
356
+ elif handle_type == "pullbar":
357
+ friction_geom = "5 5 5"
358
+ handle_body_xml = f"""
359
+ <body name="{handle_name}_body" pos="{pos_x} {pos_y} {pos_z}" euler="{euler_x} {euler_y} {euler_z}">
360
+ <geom material="{handle_material}" pos="0 0 0" size="{handle_radius} {handle_radius} {handle_length}" type="box" />
361
+ <geom name="{handle_name}" material="{handle_material}" pos="0 0 0" size="{handle_radius} {handle_radius} {handle_length}" type="box"/>
362
+ <geom material="{handle_material}" pos="-0.04 0 {handle_length - 0.03}" euler="0 1.57 0" size="{handle_radius / 1.2} {0.04} {0.021}" type="cylinder" mass="{mass / 10}"/>
363
+ <geom material="{handle_material}" pos="-0.04 0 -{handle_length - 0.03}" euler="0 1.57 0" size="{handle_radius / 1.2} {0.04} {0.021}" type="cylinder" mass="{mass / 10}"/>
364
+ <geom material="{handle_material}" pos="-0.04 0 {handle_length - 0.03}" euler="0 1.57 0" size="{handle_radius / 1.2} {0.04} {0.021}" type="cylinder" conaffinity="0" contype="0"/>
365
+ <geom material="{handle_material}" pos="-0.04 0 -{handle_length - 0.03}" euler="0 1.57 0" size="{handle_radius / 1.2} {0.04} {0.021}" type="cylinder" conaffinity="0" contype="0"/>
366
+ </body>"""
367
+ mesh_asset_xml = ""
368
+
369
+ else:
370
+ friction_geom = "5 5 5"
371
+ handle_body_xml = f"""
372
+ <body name="{handle_name}_body" pos="{pos_x} {pos_y} {pos_z}" euler="{euler_x} {euler_y} {euler_z}">
373
+ <geom material="{handle_material}" pos="0 0 0" size="{handle_radius} {handle_length}" type="cylinder" friction="{friction_geom}"/>
374
+ <geom name="{handle_name}" material="{handle_material}" pos="0 0 0" size="{handle_radius} {handle_length}" type="cylinder" mass="{mass}" friction="{friction_geom}"/>
375
+ <geom material="{handle_material}" pos="-0.04 0 {handle_length - 0.03}" euler="0 1.57 0" size="{handle_radius / 1.2} {0.04} {0.021}" type="cylinder" mass="{mass / 10}"/>
376
+ <geom material="{handle_material}" pos="-0.04 0 -{handle_length - 0.03}" euler="0 1.57 0" size="{handle_radius / 1.2} {0.04} {0.021}" type="cylinder" mass="{mass / 10}"/>
377
+ <geom material="{handle_material}" pos="-0.04 0 {handle_length - 0.03}" euler="0 1.57 0" size="{handle_radius / 1.2} {0.04} {0.021}" type="cylinder" conaffinity="0" contype="0"/>
378
+ <geom material="{handle_material}" pos="-0.04 0 -{handle_length - 0.03}" euler="0 1.57 0" size="{handle_radius / 1.2} {0.04} {0.021}" type="cylinder" conaffinity="0" contype="0"/>
379
+ </body>"""
380
+ mesh_asset_xml = ""
381
+
382
+ return handle_body_xml, mesh_asset_xml
383
+
384
+
385
+ def make_cabinet_xml(np_random=None):
386
+ if np_random is None:
387
+ np_random = np.random
388
+
389
+ hinge_side = np_random.choice(["left", "right"])
390
+ holder_type = np_random.choice(["handle", "pullbar"])
391
+ double_doors = np_random.choice([0, 1])
392
+
393
+ if double_doors:
394
+ width = np_random.uniform(0.4, 0.8)
395
+ else:
396
+ width = np_random.uniform(0.2, 0.5)
397
+
398
+ height = np_random.uniform(0.4, 0.8)
399
+ depth = np_random.uniform(0.1, 0.3)
400
+ handle_position = np_random.uniform(0.5, 0.8)
401
+ handle_length = np_random.uniform(0.05, 0.1)
402
+ handle_radius = np_random.uniform(0.004, 0.006)
403
+
404
+ wall_thickness = 0.01
405
+ door_gap = 0.00
406
+ add_wall = 1
407
+ handle_offset_y = 0.05
408
+ handle_offset_z = 0.00
409
+ door_mass = 0.001
410
+ friction_geom = "3 3 3" if holder_type == "knob" else "0 0 0"
411
+ door_gap_between = 0.005
412
+ texrepeat_xy = (5, 5)
413
+ paint_rgba = (1.0, 1.0, 1.0, 1.0)
414
+ collision_rgba = (0.3, 0.3, 1.0, 0.5)
415
+
416
+ base_path = os.path.join(os.path.dirname(__file__), "assets/textures")
417
+ door_texture_png = np_random.choice([f"{base_path}/door/{i}.png" for i in range(2)])
418
+ cabinet_texture_png = np_random.choice([f"{base_path}/door/{i}.png" for i in range(2)])
419
+ handle_texture_png = np_random.choice([f"{base_path}/handle/{i}.png" for i in range(4)])
420
+ floor_texture_png = np_random.choice([f"{base_path}/tiles/{i}.png" for i in range(3)])
421
+ wall_texture_png = np_random.choice([f"{base_path}/wall/{i}.png" for i in range(6)])
422
+
423
+ hx = width / 2.0
424
+ hy = depth / 2.0
425
+ hz = height / 2.0
426
+ t = wall_thickness
427
+
428
+ interior_width = width - 2 * t
429
+ door_thickness_y = t
430
+
431
+ if double_doors:
432
+ door_half_x = (hx - t / 2.0) / 2.0
433
+ else:
434
+ door_half_x = hx - t / 2.0
435
+
436
+ door_half_y = door_thickness_y / 2.0
437
+ door_half_z = hz - t / 2.0
438
+
439
+ side_x = hx - t / 2.0
440
+ top_z = hz - t / 2.0
441
+ bottom_z = -hz + t / 2.0
442
+ back_y = hy - t / 2.0
443
+
444
+ side_half_x = t / 2.0
445
+ side_half_y = (depth - t) / 2.0
446
+ side_half_z = hz
447
+
448
+ tb_half_x = interior_width / 2.0
449
+ tb_half_y = (depth - t) / 2.0
450
+ tb_half_z = t / 2.0
451
+
452
+ back_half_x = hx - t / 2.0
453
+ back_half_y = t / 2.0
454
+ back_half_z = hz - t / 2.0
455
+
456
+ repx, repy = texrepeat_xy
457
+ wood_texrepeat = f"{repx} {repy}"
458
+
459
+ paint_r, paint_g, paint_b, paint_a = paint_rgba
460
+ col_r, col_g, col_b, col_a = collision_rgba
461
+
462
+ wall_thickness_back = 0.02
463
+ wall_y_pos = hy + wall_thickness_back / 2.0
464
+
465
+ wall_section = ""
466
+ if add_wall:
467
+ wall_section = f"""
468
+ <!-- Large wall behind cabinet -->
469
+ <body name="front_wall">
470
+ <geom name="wall" pos="0 {wall_y_pos} 0" size="0 0 0.05" euler="1.57 0 0" type="plane" material="M_wall"/>
471
+ </body>
472
+ """
473
+
474
+ def build_holder(
475
+ hinge_side,
476
+ holder_type,
477
+ handle_x,
478
+ handle_y,
479
+ handle_z,
480
+ friction_geom,
481
+ door_mass,
482
+ handle_radius,
483
+ handle_length,
484
+ handle_name,
485
+ ):
486
+ # Use the new reusable handle function
487
+ handle_body_xml, mesh_asset_xml = make_handle_xml(
488
+ handle_type=holder_type,
489
+ handle_name=handle_name,
490
+ position=(handle_x, handle_y, handle_z),
491
+ euler_orientation=(0, 0, -1.57),
492
+ handle_radius=handle_radius,
493
+ handle_length=handle_length,
494
+ handle_material="M_hinge_metal",
495
+ mass=0.1,
496
+ friction_geom=friction_geom,
497
+ hinge_side=hinge_side,
498
+ np_random=np_random,
499
+ )
500
+
501
+ return handle_body_xml, mesh_asset_xml
502
+
503
+ def make_single_door(
504
+ name_suffix, x_offset, hinge_side, joint_name, handle_name, door_width_half
505
+ ):
506
+ hinge_sign = +1.0 if hinge_side.lower() == "right" else -1.0
507
+
508
+ # For double doors, adjust hinge position so doors are flush with cabinet sides and add gap
509
+ if double_doors:
510
+ door_side_offset = hx - t / 2.0
511
+ gap = door_gap_between / 2.0
512
+ if hinge_side.lower() == "left":
513
+ door_root_x = -door_side_offset - gap
514
+ else:
515
+ door_root_x = door_side_offset + gap
516
+ else:
517
+ door_root_x = hinge_sign * side_x
518
+
519
+ door_root_z = 0.0
520
+ door_center_x = -hinge_sign * door_width_half
521
+ door_center_y = -(t / 2.0)
522
+
523
+ if double_doors:
524
+ handle_x = door_center_x - hinge_sign * (door_width_half - 0.06)
525
+ else:
526
+ handle_x = door_center_x - hinge_sign * (door_width_half - 0.05)
527
+ handle_y = -door_half_y - handle_offset_y
528
+ door_bottom = -door_half_z
529
+ door_top = door_half_z
530
+ handle_z = (
531
+ door_bottom + handle_position * (door_top - door_bottom) + handle_offset_z
532
+ )
533
+
534
+ if hinge_side.lower() == "right":
535
+ jmin, jmax = 0.0, 1.57
536
+ else:
537
+ jmin, jmax = -1.57, 0.0
538
+
539
+ holder_xml, mesh_asset = build_holder(
540
+ hinge_side,
541
+ holder_type,
542
+ handle_x,
543
+ handle_y,
544
+ handle_z,
545
+ friction_geom,
546
+ door_mass,
547
+ handle_radius,
548
+ handle_length,
549
+ handle_name,
550
+ )
551
+
552
+ return (
553
+ f"""
554
+ <body name="hingedoor{name_suffix}" pos="{door_root_x} {-hy - door_gap} {door_root_z}">
555
+ <joint axis="0 0 1" name="{joint_name}" range="{jmin} {jmax}" damping="2" frictionloss="2" armature=".01"/>
556
+ {holder_xml}
557
+ <body name="door_body{name_suffix}">
558
+ <geom material="M_door_wood" pos="{door_center_x} {door_center_y} 0" size="{door_width_half} {door_half_z} {door_half_y}" euler="1.57 0 0" type="box"/>
559
+ </body>
560
+ <site type="sphere" name="boxdoor_site{name_suffix}" pos="{handle_x} {handle_y - 0.018} {handle_z}" size=".01" group="3" rgba="1 0 0 1"/>
561
+ </body>
562
+ """,
563
+ mesh_asset,
564
+ )
565
+
566
+ def make_single_cabinet(name_suffix, x_offset, hinge_side, joint_name, handle_name):
567
+ hinge_sign = +1.0 if hinge_side.lower() == "right" else -1.0
568
+ door_root_x = hinge_sign * side_x
569
+ door_root_z = 0.0
570
+ door_center_x = -hinge_sign * (door_half_x)
571
+ door_center_y = -(t / 2.0)
572
+
573
+ handle_x = door_center_x - hinge_sign * (door_half_x - 0.05)
574
+ handle_y = -door_half_y - handle_offset_y
575
+ door_bottom = -door_half_z
576
+ door_top = door_half_z
577
+ handle_z = (
578
+ door_bottom + handle_position * (door_top - door_bottom) + handle_offset_z
579
+ )
580
+
581
+ if hinge_side.lower() == "right":
582
+ jmin, jmax = 0.0, 1.57
583
+ else:
584
+ jmin, jmax = -1.57, 0.0
585
+
586
+ holder_xml, mesh_asset = build_holder(
587
+ hinge_side,
588
+ holder_type,
589
+ handle_x,
590
+ handle_y,
591
+ handle_z,
592
+ friction_geom,
593
+ door_mass,
594
+ handle_radius,
595
+ handle_length,
596
+ handle_name,
597
+ )
598
+
599
+ return (
600
+ f"""
601
+ <body name="cabinet_object{name_suffix}" childclass="boxcabinet" pos="{x_offset} 0 {hz}">
602
+ <body name="back_panel{name_suffix}">
603
+ <geom material="M_cabinet_paint" pos="-{side_x} {-t / 4.0} 0" size="{side_half_x} {side_half_y} {side_half_z}" type="box" conaffinity="0" contype="0"/>
604
+ <geom material="M_cabinet_paint" pos="{side_x} {-t / 4.0} 0" size="{side_half_x} {side_half_y} {side_half_z}" type="box" conaffinity="0" contype="0"/>
605
+ <geom material="M_cabinet_paint" pos="0 {-t / 4.0} {top_z}" size="{tb_half_x} {tb_half_y} {tb_half_z}" type="box" conaffinity="0" contype="0"/>
606
+ <geom material="M_cabinet_paint" pos="0 {-t / 4.0} {bottom_z}" size="{tb_half_x} {tb_half_y} {tb_half_z}" type="box" conaffinity="0" contype="0"/>
607
+ <geom material="M_cabinet_paint" pos="0 {back_y} 0" size="{back_half_x} {back_half_y} {back_half_z}" type="box" conaffinity="0" contype="0"/>
608
+ </body>
609
+
610
+ <body name="hingedoor{name_suffix}" pos="{door_root_x} {-hy - door_gap} {door_root_z}">
611
+ <joint axis="0 0 1" name="{joint_name}" range="{jmin} {jmax}" damping="2" frictionloss="2" armature=".01"/>
612
+ {holder_xml}
613
+ <body name="door_body{name_suffix}">
614
+ <geom material="M_door_wood" pos="{door_center_x} {door_center_y} 0" size="{door_half_x} {door_half_z} {door_half_y}" euler="1.57 0 0" type="box"/>
615
+ </body>
616
+ <site type="sphere" name="boxdoor_site{name_suffix}" pos="{handle_x} {handle_y - 0.018} {handle_z}" size=".01" group="3" rgba="1 0 0 1"/>
617
+ </body>
618
+ </body>
619
+ """,
620
+ mesh_asset,
621
+ )
622
+
623
+ cabinets_xml, extra_meshes = "", ""
624
+
625
+ if double_doors:
626
+ # Randomly pick which door gets 'task_joint' and 'handle_1'
627
+ if np_random.uniform() > 0.5:
628
+ left_joint = "task_joint"
629
+ right_joint = "task_joint_2"
630
+ else:
631
+ left_joint = "task_joint_2"
632
+ right_joint = "task_joint"
633
+
634
+ left_handle = "handle" if left_joint == "task_joint" else "handle_2"
635
+ right_handle = "handle" if right_joint == "task_joint" else "handle_2"
636
+
637
+ left_door_xml, mesh1 = make_single_door(
638
+ "_L", -door_half_x, "left", left_joint, left_handle, door_half_x
639
+ )
640
+ right_door_xml, mesh2 = make_single_door(
641
+ "_R", door_half_x, "right", right_joint, right_handle, door_half_x
642
+ )
643
+
644
+ cabinets_xml = f"""
645
+ <body name="cabinet_object" childclass="boxcabinet" pos="0 0 {hz}">
646
+ <body name="back_panel">
647
+ <geom material="M_cabinet_paint" pos="-{side_x} {-t / 4.0} 0" size="{side_half_x} {side_half_y} {side_half_z}" type="box" conaffinity="0" contype="0"/>
648
+ <geom material="M_cabinet_paint" pos="{side_x} {-t / 4.0} 0" size="{side_half_x} {side_half_y} {side_half_z}" type="box" conaffinity="0" contype="0"/>
649
+ <geom material="M_cabinet_paint" pos="0 {-t / 4.0} {top_z}" size="{tb_half_x} {tb_half_y} {tb_half_z}" type="box" conaffinity="0" contype="0"/>
650
+ <geom material="M_cabinet_paint" pos="0 {-t / 4.0} {bottom_z}" size="{tb_half_x} {tb_half_y} {tb_half_z}" type="box" conaffinity="0" contype="0"/>
651
+ <geom material="M_cabinet_paint" pos="0 {back_y} 0" size="{back_half_x} {back_half_y} {back_half_z}" type="box" conaffinity="0" contype="0"/>
652
+ </body>
653
+ {left_door_xml}
654
+ {right_door_xml}
655
+ </body>
656
+ """
657
+ extra_meshes = mesh1 + mesh2
658
+
659
+ exclude_doors_xml = """
660
+ <contact> <exclude body1="hingedoor_L" body2="hingedoor_R"/> <exclude body1="door_body_L" body2="door_body_R"/>
661
+
662
+ <exclude body1="left_collisions" body2="door_body_L"/>
663
+ <exclude body1="left_collisions" body2="door_body_R"/>
664
+ <exclude body1="right_collisions" body2="door_body_L"/>
665
+ <exclude body1="right_collisions" body2="door_body_R"/>
666
+
667
+ </contact>
668
+
669
+ """
670
+ else:
671
+ cabinets_xml, extra_meshes = make_single_cabinet(
672
+ "", 0, hinge_side, "task_joint", "handle"
673
+ )
674
+ exclude_doors_xml = """
675
+ <contact>
676
+ <exclude body1="left_collisions" body2="door_body"/>
677
+ <exclude body1="right_collisions" body2="door_body"/>
678
+
679
+
680
+
681
+ </contact>
682
+
683
+ """
684
+
685
+
686
+ xml = f"""
687
+ <mujoco model="hinge cabinet">
688
+ <compiler angle="radian"/>
689
+ <asset>
690
+ {extra_meshes}
691
+ <texture name="T_door_wood" type="2d" file="{door_texture_png}"/>
692
+ <texture name="T_cabinet_wood" type="2d" file="{cabinet_texture_png}"/>
693
+ <texture name="T_hinge_metal" type="2d" file="{handle_texture_png}"/>
694
+ <texture name="T_floor" type="2d" file="{floor_texture_png}"/>
695
+ <texture name="T_wall" type="2d" file="{wall_texture_png}"/>
696
+ <material name="M_door_wood" texture="T_door_wood" texrepeat="5 5" reflectance="0.0" shininess=".1" texuniform="true"/>
697
+ <material name="M_cabinet_wood" texture="T_cabinet_wood" texrepeat="{wood_texrepeat}" reflectance="0.0" shininess=".1" texuniform="true"/>
698
+ <material name="M_hinge_metal" texture="T_hinge_metal" texrepeat="4 4" reflectance="0.6" shininess=".6" texuniform="true"/>
699
+ <material name="M_floor" texture="T_floor" texrepeat="3 3" reflectance="0.0" shininess=".1" texuniform="true"/>
700
+ <material name="M_wall" texture="T_wall" texrepeat="4 4" reflectance="0.0" shininess=".1" texuniform="true"/>
701
+ <material name="M_cabinet_paint" texture="T_cabinet_wood" rgba="{paint_r} {paint_g} {paint_b} {paint_a}" reflectance="0.0" shininess=".01" texuniform="true"/>
702
+ <material name="hinge_collision_blue" rgba="{col_r} {col_g} {col_b} {col_a}" shininess="0" specular="0"/>
703
+ </asset>
704
+ <default>
705
+ <default class="boxcabinet">
706
+ <joint damping="2" frictionloss="2" limited="true"/>
707
+ <geom group="1" material="M_cabinet_wood" type="mesh"/>
708
+ <default class="box_collision">
709
+ <geom conaffinity="1" condim="6" contype="1" group="4" material="hinge_collision_blue"/>
710
+ </default>
711
+ </default>
712
+ </default>
713
+ <worldbody>
714
+ {wall_section}
715
+ {cabinets_xml}
716
+
717
+ <body name="floor">
718
+ <geom name="floor" pos="0 0 0" size="0 0 0.05" euler="0 0 0" type="plane" material="M_floor"/>
719
+ </body>
720
+ </worldbody>
721
+ {exclude_doors_xml}
722
+ </mujoco>
723
+ """
724
+ return xml
725
+
726
+ def make_drawer_xml(np_random=None):
727
+ if np_random is None:
728
+ np_random = np.random
729
+
730
+ handle_type = np_random.choice(["handle", "pullbar"])
731
+ height_per_drawer = np_random.uniform(0.05, 0.15)
732
+ handle_radius = np_random.uniform(0.005, 0.01)
733
+ width = np_random.uniform(0.2, 0.4)
734
+ depth = np_random.uniform(0.15, 0.3)
735
+ handle_length = np_random.uniform(0.07, max(0.05, width / 1.2))
736
+ num_drawers = np_random.integers(1, 5)
737
+
738
+ position = (0, 0, 0)
739
+ rotation = (0, 0, -1.57)
740
+ handle_protrusion = 0.05
741
+ thickness = 0.005
742
+ cover_color = (0.3, 0.2, 0.05, 1.0)
743
+ drawer_color = (0.4, 0.25, 0.1, 1.0)
744
+ handle_color = (0.2, 0.2, 0.2, 1.0)
745
+ texrepeat_xy = (3, 3)
746
+ add_wall = 1
747
+ joint_damping = 2
748
+
749
+ base_path = os.path.join(os.path.dirname(__file__), "assets/textures")
750
+ cover_texture_png = np_random.choice([f"{base_path}/door/{i}.png" for i in range(3)])
751
+ drawer_texture_png = np_random.choice([f"{base_path}/door/{i}.png" for i in range(3)])
752
+ door_texture_png = np_random.choice([f"{base_path}/door/{i}.png" for i in range(3)])
753
+ handle_texture_png = np_random.choice([f"{base_path}/handle/{i}.png" for i in range(4)])
754
+ floor_texture_png = np_random.choice([f"{base_path}/tiles/{i}.png" for i in range(3)])
755
+ wall_texture_png = np_random.choice([f"{base_path}/wall/{i}.png" for i in range(6)])
756
+
757
+ half_total_height = num_drawers * height_per_drawer + thickness * (num_drawers + 1)
758
+ total_height = half_total_height * 2
759
+
760
+ adjusted_position = (position[0], position[1], thickness)
761
+
762
+ repx, repy = texrepeat_xy
763
+ texrepeat_str = f"{repx} {repy}"
764
+
765
+ cover_r, cover_g, cover_b, cover_a = cover_color
766
+ drawer_r, drawer_g, drawer_b, drawer_a = drawer_color
767
+ handle_r, handle_g, handle_b, handle_a = handle_color
768
+
769
+ base_template = f"""<mujoco model="drawer">
770
+ <asset>
771
+ <texture name="T_cover" type="2d" height="1" width="1" file="{cover_texture_png}"/>
772
+ <texture name="T_drawer" type="2d" height="1" width="1" file="{drawer_texture_png}"/>
773
+ <texture name="T_door" type="2d" height="1" width="1" file="{door_texture_png}"/>
774
+ <texture name="T_handle" type="2d" height="1" width="1" file="{handle_texture_png}"/>
775
+ <texture name="T_floor" type="2d" height="2" width="2" file="{floor_texture_png}"/>
776
+ <texture name="T_wall" type="2d" height="2" width="1" file="{wall_texture_png}"/>
777
+
778
+ <material name="M_cover" texture="T_cover" texrepeat="{texrepeat_str}" reflectance="0.0" shininess=".1" texuniform="false"/>
779
+ <material name="M_drawer" texture="T_drawer" texrepeat="{texrepeat_str}" reflectance="0.0" shininess=".1" texuniform="false"/>
780
+ <material name="M_door" texture="T_door" texrepeat="{texrepeat_str}" reflectance="0.0" shininess=".1" texuniform="false"/>
781
+ <material name="M_handle" texture="T_handle" texrepeat="{texrepeat_str}" reflectance="0.0" shininess=".1" texuniform="false"/>
782
+ <material name="M_floor" texture="T_floor" texrepeat="5 5" reflectance="0.0" shininess=".1" texuniform="false"/>
783
+ <material name="M_wall" texture="T_wall" texrepeat="2 2" reflectance="0.0" shininess=".1" texuniform="false"/>
784
+ </asset>
785
+
786
+ <worldbody>
787
+
788
+ <body name="drawer_object" pos="{adjusted_position[0]} {adjusted_position[1]} {adjusted_position[2]}" euler="{rotation[0]} {rotation[1]} {rotation[2]}">
789
+ <body name="outer_frame">
790
+ <geom size="{depth + thickness * 3} {thickness} {half_total_height}" pos="0 -{width + thickness * 2} {half_total_height}" type="box" material="M_cover" />
791
+ <geom size="{depth + thickness * 3} {thickness} {half_total_height}" pos="0 {width + thickness * 2} {half_total_height}" type="box" material="M_cover" />
792
+ <geom size="{depth + thickness * 3} {width + thickness * 3} {thickness}" pos="0 0 {thickness}" type="box" material="M_cover" />
793
+ <geom size="{depth + thickness * 3} {width + thickness * 3} {thickness}" pos="0 0 {total_height}" type="box" material="M_cover" />
794
+ <geom size="{thickness} {width + thickness * 3} {half_total_height}" pos="-{depth + thickness * 2} 0 {half_total_height}" type="box" material="M_cover" />
795
+
796
+ </body>
797
+ """
798
+
799
+ drawer_bodies = ""
800
+
801
+ task_drawer_index = np_random.integers(0, num_drawers)
802
+
803
+ for i in range(num_drawers):
804
+ drawer_name = f"drawer_{i}"
805
+
806
+ if i == task_drawer_index:
807
+ joint_name = "task_joint"
808
+ handle_geom_name = "handle"
809
+ else:
810
+ joint_name = f"drawer_{i}_slide"
811
+ if i == 1 and task_drawer_index != 1:
812
+ handle_geom_name = "handle_1_normal"
813
+ else:
814
+ handle_geom_name = f"handle_{i}"
815
+
816
+ z_pos = height_per_drawer * (2 * i + 1) + thickness * (2 * i + 3)
817
+ collision_thickness = thickness * 0.8
818
+ collision_size_adjust = thickness * 0.8
819
+
820
+ drawer_body = f"""
821
+ <body name="{drawer_name}" pos="{thickness * 3} 0 {z_pos}">
822
+ <joint name="{joint_name}" type="slide" axis="1 0 0" limited="true" range="0 {
823
+ depth * 2
824
+ }" damping="10" frictionloss="10" armature=".01" />
825
+
826
+ <geom size="{depth} {thickness} {height_per_drawer}" type="box" pos="0 {
827
+ width - thickness
828
+ } 0" material="M_drawer" conaffinity="0" contype="0"/>
829
+ <geom size="{depth} {thickness} {height_per_drawer}" type="box" pos="0 -{
830
+ width - thickness
831
+ } 0" material="M_drawer" conaffinity="0" contype="0"/>
832
+ <geom size="{height_per_drawer} {width} {thickness}" type="box" pos="{
833
+ depth - thickness
834
+ } 0 0" material="M_drawer" conaffinity="0" contype="0" euler="1.57 1.57 1.57"/>
835
+ <geom size="{height_per_drawer} {width} {thickness}" type="box" pos="{
836
+ depth - thickness
837
+ } 0 0" material="M_drawer" conaffinity="0" contype="0" euler="1.57 1.57 1.57"/>
838
+ <geom size="{height_per_drawer} {width} {thickness}" type="box" pos="{
839
+ depth - thickness
840
+ } 0 0" material="M_drawer" euler="1.57 1.57 1.57"/>
841
+
842
+ <geom size="{depth * 0.9} {width * 0.9} {
843
+ collision_thickness
844
+ }" type="box" pos="0 0 -{height_per_drawer - thickness}" mass="0.0001"/>
845
+ <geom size="{depth * 0.9} {collision_size_adjust} {
846
+ height_per_drawer * 0.8
847
+ }" type="box" pos="0 {width - thickness} 0" mass="0.0001"/>
848
+ <geom size="{depth * 0.9} {collision_size_adjust} {
849
+ height_per_drawer * 0.8
850
+ }" type="box" pos="0 -{width - thickness} 0" mass="0.0001"/>
851
+ <geom size="{collision_size_adjust} {width * 0.9} {
852
+ height_per_drawer * 0.8
853
+ }" type="box" pos="{depth - thickness} 0 0" mass="0.0001"/>
854
+ <geom size="{collision_size_adjust} {width * 0.9} {
855
+ height_per_drawer * 0.8
856
+ }" type="box" pos="-{depth - thickness} 0 0" mass="0.0001"/>
857
+
858
+ {
859
+ make_handle_xml(
860
+ handle_type=handle_type,
861
+ handle_name=handle_geom_name,
862
+ position=(depth + handle_protrusion, 0, 0),
863
+ euler_orientation=(1.57, 0, 0),
864
+ handle_radius=handle_radius,
865
+ handle_length=handle_length,
866
+ handle_material="M_handle",
867
+ mass=0.1,
868
+ friction_geom="0 0 0",
869
+ )[0]
870
+ }
871
+ </body>"""
872
+
873
+ drawer_bodies += drawer_body
874
+
875
+ wall_y_pos = -(depth + 0.55)
876
+
877
+ floor_wall_section = """
878
+ <body name="floor">
879
+ <geom name="floor" pos="0 0 0" size="0 0 0.05" euler="0 0 0" type="plane" material="M_floor" conaffinity="0" contype="0"/>
880
+ </body>"""
881
+
882
+ if add_wall:
883
+ floor_wall_section += f"""
884
+ <!-- Large wall behind drawer -->
885
+ <body name="front_wall">
886
+ <geom name="wall" pos="0 {-wall_y_pos} 0" size="0 0 0.05" euler="1.57 0 0" type="plane" material="M_wall"/>
887
+ </body>
888
+ """
889
+
890
+ # Dynamically generate contact exclusions based on num_drawers
891
+ contact_excludes = ""
892
+ for i in range(num_drawers):
893
+ contact_excludes += (
894
+ f' <exclude body1="drawer_{i}" body2="outer_frame"/>\n'
895
+ )
896
+
897
+ closing_template = (
898
+ """
899
+ </body>"""
900
+ + floor_wall_section
901
+ + f"""
902
+ </worldbody>
903
+
904
+ <contact>
905
+ {contact_excludes} </contact>
906
+ </mujoco>"""
907
+ )
908
+
909
+ drawer_xml_string = base_template + drawer_bodies + closing_template
910
+ return drawer_xml_string
911
+
912
+ def resize_with_pad(images, height, width):
913
+ if images.shape[-3:-1] == (height, width):
914
+ return images
915
+
916
+ original_shape = images.shape
917
+ images = images.reshape(-1, *original_shape[-3:])
918
+ resized = np.stack(
919
+ [
920
+ _resize_with_pad_pil(Image.fromarray(im), height, width, method=Image.BILINEAR)
921
+ for im in images
922
+ ]
923
+ )
924
+ return resized.reshape(*original_shape[:-3], *resized.shape[-3:])
925
+
926
+
927
+ def _resize_with_pad_pil(image, height, width, method):
928
+ cur_width, cur_height = image.size
929
+ if cur_width == width and cur_height == height:
930
+ return image
931
+
932
+ ratio = max(cur_width / width, cur_height / height)
933
+ resized_height = int(cur_height / ratio)
934
+ resized_width = int(cur_width / ratio)
935
+ resized_image = image.resize((resized_width, resized_height), resample=method)
936
+
937
+ zero_image = Image.new(resized_image.mode, (width, height), 0)
938
+ pad_height = max(0, int((height - resized_height) / 2))
939
+ pad_width = max(0, int((width - resized_width) / 2))
940
+ zero_image.paste(resized_image, (pad_width, pad_height))
941
+ assert zero_image.size == (width, height)
942
+ return zero_image
943
+
944
+ def set_table_size(scene_xml_content, table_width, table_length):
945
+ return scene_xml_content.replace("{TABLE_WIDTH}", str(table_width)).replace(
946
+ "{TABLE_LENGTH}", str(table_length)
947
+ )
948
+
949
+ def pixel_to_world(u, v, depth, model, data, cam_name, img_width, img_height):
950
+ fovy = math.radians(model.camera(cam_name).fovy[0])
951
+ f = img_height / (2 * np.tan(fovy / 2))
952
+ cam_mat = np.array([[f, 0, img_width / 2], [0, f, img_height / 2], [0, 0, 1]])
953
+
954
+ cx = cam_mat[0, 2]
955
+ cy = cam_mat[1, 2]
956
+ fx = cam_mat[0, 0]
957
+ fy = cam_mat[1, 1]
958
+
959
+ z_cam = -depth
960
+ x_cam = -(u - cx) * z_cam / fx
961
+ y_cam = -(cy - v) * z_cam / fy
962
+
963
+ p_cam = np.array([x_cam, y_cam, z_cam])
964
+
965
+ xmat = data.camera(cam_name).xmat
966
+ R_wc = xmat.reshape(3, 3)
967
+ pos = data.camera(cam_name).xpos
968
+ p_world = R_wc @ p_cam + pos
969
+ return p_world