Spaces:
Running on Zero
Running on Zero
update
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- app.py +2 -2
- app_style.py +1 -1
- common.py +6 -13
- embodied_gen/__init__.py +8 -0
- embodied_gen/data/__init__.py +1 -0
- embodied_gen/data/asset_converter.py +242 -68
- embodied_gen/data/backproject_v3.py +1 -1
- embodied_gen/data/differentiable_render.py +35 -9
- embodied_gen/data/mesh_operator.py +5 -4
- embodied_gen/data/utils.py +73 -8
- embodied_gen/envs/__init__.py +1 -0
- embodied_gen/envs/pick_embodiedgen.py +1 -1
- embodied_gen/models/__init__.py +1 -0
- embodied_gen/models/gs_model.py +1 -1
- embodied_gen/models/hunyuan3d.py +957 -0
- embodied_gen/models/image_comm_model.py +6 -0
- embodied_gen/models/sam3d.py +1 -8
- embodied_gen/models/segment_model.py +3 -5
- embodied_gen/models/text_model.py +19 -7
- embodied_gen/models/texture_model.py +11 -5
- embodied_gen/scripts/__init__.py +1 -0
- embodied_gen/scripts/affordance_annot/affordance_annot.py +313 -0
- embodied_gen/scripts/affordance_annot/eval_grasps.py +1303 -0
- embodied_gen/scripts/affordance_annot/franka_panda/coll_mesh.obj +464 -0
- embodied_gen/scripts/affordance_annot/franka_panda/gripper.urdf +120 -0
- embodied_gen/scripts/affordance_annot/franka_panda/meshes/collision/hand.stl +0 -0
- embodied_gen/scripts/affordance_annot/franka_panda/meshes/visual/finger.dae +178 -0
- embodied_gen/scripts/affordance_annot/franka_panda/meshes/visual/hand.dae +0 -0
- embodied_gen/scripts/affordance_annot/gen_grasp.py +842 -0
- embodied_gen/scripts/affordance_annot/part_seg.py +697 -0
- embodied_gen/scripts/affordance_annot/partsemantics_annot.py +438 -0
- embodied_gen/scripts/compose_layout.py +0 -1
- embodied_gen/scripts/deformable_sim/genesis_cloth_converter.py +282 -0
- embodied_gen/scripts/deformable_sim/render_mesh_displacement_heatmap.py +551 -0
- embodied_gen/scripts/deformable_sim/simulate_genesis_cloth.py +608 -0
- embodied_gen/scripts/gen_texture.py +1 -1
- embodied_gen/scripts/imageto3d.py +102 -19
- embodied_gen/scripts/parallel_sim.py +1 -1
- embodied_gen/scripts/room_gen/custom_solve.gin +1 -0
- embodied_gen/scripts/room_gen/export_scene.py +13 -14
- embodied_gen/scripts/room_gen/gen_room.py +21 -0
- embodied_gen/scripts/room_gen/gen_trajectory.py +306 -0
- embodied_gen/scripts/room_gen/render_usd.py +1017 -22
- embodied_gen/scripts/room_gen/route_room.py +5 -0
- embodied_gen/scripts/textto3d.py +152 -31
- embodied_gen/skills/README.md +14 -14
- embodied_gen/skills/asset-process/SKILL.md +101 -0
- embodied_gen/skills/asset-process/__init__.py +36 -0
- embodied_gen/skills/asset-process/asset_process.py +443 -0
- embodied_gen/skills/asset-retrieval/scripts/retrieve_asset.py +16 -1
app.py
CHANGED
|
@@ -57,13 +57,13 @@ with gr.Blocks(delete_cache=(43200, 43200), theme=custom_theme) as demo:
|
|
| 57 |
<a href="https://horizonrobotics.github.io/EmbodiedGen">
|
| 58 |
<img alt="📖 Documentation" src="https://img.shields.io/badge/📖-Documentation-blue">
|
| 59 |
</a>
|
| 60 |
-
<a href="https://arxiv.org/abs/
|
| 61 |
<img alt="📄 arXiv" src="https://img.shields.io/badge/📄-arXiv-b31b1b">
|
| 62 |
</a>
|
| 63 |
<a href="https://github.com/HorizonRobotics/EmbodiedGen">
|
| 64 |
<img alt="💻 GitHub" src="https://img.shields.io/badge/GitHub-000000?logo=github">
|
| 65 |
</a>
|
| 66 |
-
<a href="https://
|
| 67 |
<img alt="🎥 Video" src="https://img.shields.io/badge/🎥-Video-red">
|
| 68 |
</a>
|
| 69 |
</p>
|
|
|
|
| 57 |
<a href="https://horizonrobotics.github.io/EmbodiedGen">
|
| 58 |
<img alt="📖 Documentation" src="https://img.shields.io/badge/📖-Documentation-blue">
|
| 59 |
</a>
|
| 60 |
+
<a href="https://arxiv.org/abs/2607.07459">
|
| 61 |
<img alt="📄 arXiv" src="https://img.shields.io/badge/📄-arXiv-b31b1b">
|
| 62 |
</a>
|
| 63 |
<a href="https://github.com/HorizonRobotics/EmbodiedGen">
|
| 64 |
<img alt="💻 GitHub" src="https://img.shields.io/badge/GitHub-000000?logo=github">
|
| 65 |
</a>
|
| 66 |
+
<a href="https://youtu.be/MIkJJSVM8L4">
|
| 67 |
<img alt="🎥 Video" src="https://img.shields.io/badge/🎥-Video-red">
|
| 68 |
</a>
|
| 69 |
</p>
|
app_style.py
CHANGED
|
@@ -15,7 +15,7 @@
|
|
| 15 |
# permissions and limitations under the License.
|
| 16 |
|
| 17 |
from gradio.themes import Soft
|
| 18 |
-
from gradio.themes.utils.colors import gray,
|
| 19 |
|
| 20 |
lighting_css = """
|
| 21 |
<style>
|
|
|
|
| 15 |
# permissions and limitations under the License.
|
| 16 |
|
| 17 |
from gradio.themes import Soft
|
| 18 |
+
from gradio.themes.utils.colors import gray, stone
|
| 19 |
|
| 20 |
lighting_css = """
|
| 21 |
<style>
|
common.py
CHANGED
|
@@ -16,15 +16,15 @@
|
|
| 16 |
|
| 17 |
import spaces # noqa: E402
|
| 18 |
from embodied_gen.utils.monkey_patch.gradio import (
|
| 19 |
-
_disable_xformers_flash3,
|
| 20 |
_neutralize_warp_in_parent,
|
| 21 |
_patch_open3d_cuda_device_count_bug,
|
| 22 |
)
|
| 23 |
from embodied_gen.utils.monkey_patch.trellis import monkey_path_trellis
|
|
|
|
| 24 |
|
| 25 |
_neutralize_warp_in_parent()
|
| 26 |
_patch_open3d_cuda_device_count_bug()
|
| 27 |
-
|
| 28 |
monkey_path_trellis()
|
| 29 |
|
| 30 |
|
|
@@ -93,12 +93,6 @@ os.environ.setdefault("OPENAI_API_KEY", "sk-placeholder")
|
|
| 93 |
MAX_SEED = 100000
|
| 94 |
|
| 95 |
|
| 96 |
-
def _move_pipeline(pipeline: object, device: str) -> None:
|
| 97 |
-
pipeline.to(device)
|
| 98 |
-
image_encoder = getattr(pipeline, "image_encoder", None)
|
| 99 |
-
if image_encoder is not None:
|
| 100 |
-
image_encoder.to(device)
|
| 101 |
-
|
| 102 |
# Global variables for lazy initialization
|
| 103 |
_RBG_REMOVER = None
|
| 104 |
_RBG14_REMOVER = None
|
|
@@ -136,6 +130,7 @@ elif os.getenv("GRADIO_APP").startswith("textto3d"):
|
|
| 136 |
text_model_dir,
|
| 137 |
device="cpu",
|
| 138 |
download_ip_adapter=True,
|
|
|
|
| 139 |
)
|
| 140 |
_PIPELINE_IMG_IP = None
|
| 141 |
SEG_CHECKER = ImageSegChecker(GPT_CLIENT)
|
|
@@ -624,12 +619,12 @@ def text2image_fn(
|
|
| 624 |
"weights/Kolors",
|
| 625 |
ref_scale=ip_adapt_scale,
|
| 626 |
device="cpu",
|
|
|
|
| 627 |
)
|
| 628 |
pipeline = _PIPELINE_IMG_IP
|
| 629 |
pipeline.set_ip_adapter_scale([ip_adapt_scale])
|
| 630 |
|
| 631 |
try:
|
| 632 |
-
_move_pipeline(pipeline, "cuda")
|
| 633 |
images = text2img_gen(
|
| 634 |
prompt=prompt,
|
| 635 |
n_sample=n_sample,
|
|
@@ -641,10 +636,8 @@ def text2image_fn(
|
|
| 641 |
seed=seed,
|
| 642 |
)
|
| 643 |
finally:
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
except Exception:
|
| 647 |
-
logger.exception("Failed to move text-to-image pipeline to CPU")
|
| 648 |
gc.collect()
|
| 649 |
if torch.cuda.is_available():
|
| 650 |
torch.cuda.empty_cache()
|
|
|
|
| 16 |
|
| 17 |
import spaces # noqa: E402
|
| 18 |
from embodied_gen.utils.monkey_patch.gradio import (
|
|
|
|
| 19 |
_neutralize_warp_in_parent,
|
| 20 |
_patch_open3d_cuda_device_count_bug,
|
| 21 |
)
|
| 22 |
from embodied_gen.utils.monkey_patch.trellis import monkey_path_trellis
|
| 23 |
+
from embodied_gen.utils.monkey_patch.xformers import disable_xformers_flash3
|
| 24 |
|
| 25 |
_neutralize_warp_in_parent()
|
| 26 |
_patch_open3d_cuda_device_count_bug()
|
| 27 |
+
disable_xformers_flash3()
|
| 28 |
monkey_path_trellis()
|
| 29 |
|
| 30 |
|
|
|
|
| 93 |
MAX_SEED = 100000
|
| 94 |
|
| 95 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
# Global variables for lazy initialization
|
| 97 |
_RBG_REMOVER = None
|
| 98 |
_RBG14_REMOVER = None
|
|
|
|
| 130 |
text_model_dir,
|
| 131 |
device="cpu",
|
| 132 |
download_ip_adapter=True,
|
| 133 |
+
enable_cpu_offload=True,
|
| 134 |
)
|
| 135 |
_PIPELINE_IMG_IP = None
|
| 136 |
SEG_CHECKER = ImageSegChecker(GPT_CLIENT)
|
|
|
|
| 619 |
"weights/Kolors",
|
| 620 |
ref_scale=ip_adapt_scale,
|
| 621 |
device="cpu",
|
| 622 |
+
enable_cpu_offload=True,
|
| 623 |
)
|
| 624 |
pipeline = _PIPELINE_IMG_IP
|
| 625 |
pipeline.set_ip_adapter_scale([ip_adapt_scale])
|
| 626 |
|
| 627 |
try:
|
|
|
|
| 628 |
images = text2img_gen(
|
| 629 |
prompt=prompt,
|
| 630 |
n_sample=n_sample,
|
|
|
|
| 636 |
seed=seed,
|
| 637 |
)
|
| 638 |
finally:
|
| 639 |
+
if hasattr(pipeline, "maybe_free_model_hooks"):
|
| 640 |
+
pipeline.maybe_free_model_hooks()
|
|
|
|
|
|
|
| 641 |
gc.collect()
|
| 642 |
if torch.cuda.is_available():
|
| 643 |
torch.cuda.empty_cache()
|
embodied_gen/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from importlib.metadata import PackageNotFoundError, version
|
| 2 |
+
|
| 3 |
+
try:
|
| 4 |
+
__version__ = version("embodied_gen")
|
| 5 |
+
except PackageNotFoundError:
|
| 6 |
+
__version__ = "0+unknown"
|
| 7 |
+
|
| 8 |
+
__all__ = ["__version__"]
|
embodied_gen/data/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Data processing utilities for EmbodiedGen."""
|
embodied_gen/data/asset_converter.py
CHANGED
|
@@ -24,6 +24,7 @@ from abc import ABC, abstractmethod
|
|
| 24 |
from glob import glob
|
| 25 |
from shutil import copy, copytree, rmtree
|
| 26 |
|
|
|
|
| 27 |
import trimesh
|
| 28 |
from scipy.spatial.transform import Rotation
|
| 29 |
from embodied_gen.utils.enum import AssetType
|
|
@@ -204,6 +205,123 @@ class MeshtoMJCFConverter(AssetConverterBase):
|
|
| 204 |
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
| 205 |
copy(src, dst)
|
| 206 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
def add_geometry(
|
| 208 |
self,
|
| 209 |
mujoco_element: ET.Element,
|
|
@@ -215,6 +333,7 @@ class MeshtoMJCFConverter(AssetConverterBase):
|
|
| 215 |
mesh_name: str,
|
| 216 |
material: ET.Element | None = None,
|
| 217 |
is_collision: bool = False,
|
|
|
|
| 218 |
) -> None:
|
| 219 |
"""Adds geometry to MJCF body from URDF link.
|
| 220 |
|
|
@@ -228,6 +347,8 @@ class MeshtoMJCFConverter(AssetConverterBase):
|
|
| 228 |
mesh_name (str): Mesh name.
|
| 229 |
material (ET.Element, optional): Material element.
|
| 230 |
is_collision (bool, optional): If True, treat as collision geometry.
|
|
|
|
|
|
|
| 231 |
"""
|
| 232 |
element = link.find(tag)
|
| 233 |
geometry = element.find("geometry")
|
|
@@ -238,9 +359,18 @@ class MeshtoMJCFConverter(AssetConverterBase):
|
|
| 238 |
output_mesh = f"{output_dir}/{filename}"
|
| 239 |
self._copy_asset_file(input_mesh, output_mesh)
|
| 240 |
|
| 241 |
-
mesh_origin = element.find("origin")
|
| 242 |
if mesh_origin is not None:
|
| 243 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 244 |
|
| 245 |
if is_collision:
|
| 246 |
mesh_parts = trimesh.load(
|
|
@@ -251,11 +381,13 @@ class MeshtoMJCFConverter(AssetConverterBase):
|
|
| 251 |
mesh_parts = [trimesh.load(output_mesh, force="mesh")]
|
| 252 |
for idx, mesh_part in enumerate(mesh_parts):
|
| 253 |
if is_collision:
|
|
|
|
| 254 |
idx_mesh_name = f"{mesh_name}_{idx}"
|
| 255 |
base, ext = os.path.splitext(filename)
|
| 256 |
idx_filename = f"{base}_{idx}{ext}"
|
| 257 |
-
|
| 258 |
-
|
|
|
|
| 259 |
geom_attrs = {
|
| 260 |
"contype": "1",
|
| 261 |
"conaffinity": "1",
|
|
@@ -263,7 +395,11 @@ class MeshtoMJCFConverter(AssetConverterBase):
|
|
| 263 |
}
|
| 264 |
else:
|
| 265 |
idx_mesh_name, idx_filename = mesh_name, filename
|
| 266 |
-
geom_attrs = {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 267 |
|
| 268 |
ET.SubElement(
|
| 269 |
mujoco_element,
|
|
@@ -271,6 +407,7 @@ class MeshtoMJCFConverter(AssetConverterBase):
|
|
| 271 |
name=idx_mesh_name,
|
| 272 |
file=idx_filename,
|
| 273 |
scale=scale,
|
|
|
|
| 274 |
)
|
| 275 |
geom = ET.SubElement(body, "geom", type="mesh", mesh=idx_mesh_name)
|
| 276 |
geom.attrib.update(geom_attrs)
|
|
@@ -286,7 +423,7 @@ class MeshtoMJCFConverter(AssetConverterBase):
|
|
| 286 |
output_dir: str,
|
| 287 |
name: str,
|
| 288 |
reflectance: float = 0.2,
|
| 289 |
-
) -> ET.Element:
|
| 290 |
"""Adds materials to MJCF asset from URDF link.
|
| 291 |
|
| 292 |
Args:
|
|
@@ -306,37 +443,51 @@ class MeshtoMJCFConverter(AssetConverterBase):
|
|
| 306 |
mesh = geometry.find("mesh")
|
| 307 |
filename = mesh.get("filename")
|
| 308 |
dirname = os.path.dirname(filename)
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
for
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
"
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 340 |
return material
|
| 341 |
|
| 342 |
def convert(self, urdf_path: str, mjcf_path: str):
|
|
@@ -369,7 +520,7 @@ class MeshtoMJCFConverter(AssetConverterBase):
|
|
| 369 |
output_dir,
|
| 370 |
name=str(idx),
|
| 371 |
)
|
| 372 |
-
|
| 373 |
self.add_geometry(
|
| 374 |
mujoco_asset,
|
| 375 |
link,
|
|
@@ -427,6 +578,37 @@ class URDFtoMJCFConverter(MeshtoMJCFConverter):
|
|
| 427 |
output_dir = os.path.dirname(mjcf_path)
|
| 428 |
os.makedirs(output_dir, exist_ok=True)
|
| 429 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 430 |
body_dict = {}
|
| 431 |
for idx, link in enumerate(root.findall("link")):
|
| 432 |
link_name = link.get("name", f"unnamed_link_{idx}")
|
|
@@ -450,6 +632,7 @@ class URDFtoMJCFConverter(MeshtoMJCFConverter):
|
|
| 450 |
output_dir,
|
| 451 |
f"visual_mesh_{idx}",
|
| 452 |
material,
|
|
|
|
| 453 |
)
|
| 454 |
if link.find("collision") is not None:
|
| 455 |
self.add_geometry(
|
|
@@ -461,18 +644,15 @@ class URDFtoMJCFConverter(MeshtoMJCFConverter):
|
|
| 461 |
output_dir,
|
| 462 |
f"collision_mesh_{idx}",
|
| 463 |
is_collision=True,
|
|
|
|
| 464 |
)
|
| 465 |
|
| 466 |
-
#
|
|
|
|
|
|
|
| 467 |
for joint in root.findall("joint"):
|
| 468 |
-
joint_type = joint.get("type")
|
| 469 |
-
if joint_type != "fixed":
|
| 470 |
-
logger.warning("Only support fixed joints in conversion now.")
|
| 471 |
-
continue
|
| 472 |
-
|
| 473 |
parent_link = joint.find("parent").get("link")
|
| 474 |
child_link = joint.find("child").get("link")
|
| 475 |
-
origin = joint.find("origin")
|
| 476 |
if parent_link not in body_dict or child_link not in body_dict:
|
| 477 |
logger.warning(
|
| 478 |
f"Parent or child link not found for joint: {joint.get('name')}"
|
|
@@ -483,11 +663,6 @@ class URDFtoMJCFConverter(MeshtoMJCFConverter):
|
|
| 483 |
mujoco_worldbody.remove(child_body)
|
| 484 |
parent_body = body_dict[parent_link]
|
| 485 |
parent_body.append(child_body)
|
| 486 |
-
if origin is not None:
|
| 487 |
-
xyz = origin.get("xyz", "0 0 0")
|
| 488 |
-
rpy = origin.get("rpy", "0 0 0")
|
| 489 |
-
child_body.set("pos", xyz)
|
| 490 |
-
child_body.set("euler", rpy)
|
| 491 |
|
| 492 |
tree = ET.ElementTree(mujoco_struct)
|
| 493 |
ET.indent(tree, space=" ", level=0)
|
|
@@ -916,13 +1091,6 @@ if __name__ == "__main__":
|
|
| 916 |
|
| 917 |
urdf_paths = [
|
| 918 |
'outputs/EmbodiedGenData/demo_assets/banana/result/banana.urdf',
|
| 919 |
-
'outputs/EmbodiedGenData/demo_assets/book/result/book.urdf',
|
| 920 |
-
'outputs/EmbodiedGenData/demo_assets/lamp/result/lamp.urdf',
|
| 921 |
-
'outputs/EmbodiedGenData/demo_assets/mug/result/mug.urdf',
|
| 922 |
-
'outputs/EmbodiedGenData/demo_assets/remote_control/result/remote_control.urdf',
|
| 923 |
-
"outputs/EmbodiedGenData/demo_assets/rubik's_cube/result/rubik's_cube.urdf",
|
| 924 |
-
'outputs/EmbodiedGenData/demo_assets/table/result/table.urdf',
|
| 925 |
-
'outputs/EmbodiedGenData/demo_assets/vase/result/vase.urdf',
|
| 926 |
]
|
| 927 |
|
| 928 |
if target_asset_type == AssetType.MJCF:
|
|
@@ -937,13 +1105,6 @@ if __name__ == "__main__":
|
|
| 937 |
elif target_asset_type == AssetType.USD:
|
| 938 |
output_files = [
|
| 939 |
'outputs/EmbodiedGenData/demo_assets/banana/usd/banana.usd',
|
| 940 |
-
'outputs/EmbodiedGenData/demo_assets/book/usd/book.usd',
|
| 941 |
-
'outputs/EmbodiedGenData/demo_assets/lamp/usd/lamp.usd',
|
| 942 |
-
'outputs/EmbodiedGenData/demo_assets/mug/usd/mug.usd',
|
| 943 |
-
'outputs/EmbodiedGenData/demo_assets/remote_control/usd/remote_control.usd',
|
| 944 |
-
"outputs/EmbodiedGenData/demo_assets/rubik's_cube/usd/rubik's_cube.usd",
|
| 945 |
-
'outputs/EmbodiedGenData/demo_assets/table/usd/table.usd',
|
| 946 |
-
'outputs/EmbodiedGenData/demo_assets/vase/usd/vase.usd',
|
| 947 |
]
|
| 948 |
asset_converter = AssetConverterFactory.create(
|
| 949 |
target_type=AssetType.USD,
|
|
@@ -979,8 +1140,21 @@ if __name__ == "__main__":
|
|
| 979 |
|
| 980 |
# # Convert infinigen usdc to physics usdc
|
| 981 |
# converter = PhysicsUSDAdder()
|
|
|
|
|
|
|
|
|
|
| 982 |
# with converter:
|
| 983 |
-
#
|
| 984 |
-
#
|
| 985 |
-
#
|
| 986 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
from glob import glob
|
| 25 |
from shutil import copy, copytree, rmtree
|
| 26 |
|
| 27 |
+
import numpy as np
|
| 28 |
import trimesh
|
| 29 |
from scipy.spatial.transform import Rotation
|
| 30 |
from embodied_gen.utils.enum import AssetType
|
|
|
|
| 205 |
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
| 206 |
copy(src, dst)
|
| 207 |
|
| 208 |
+
@staticmethod
|
| 209 |
+
def _ensure_collision_volume(
|
| 210 |
+
mesh: trimesh.Trimesh,
|
| 211 |
+
min_thickness: float = 1e-3,
|
| 212 |
+
) -> trimesh.Trimesh:
|
| 213 |
+
"""Replace degenerate collision surfaces with thin boxes."""
|
| 214 |
+
extents = mesh.extents
|
| 215 |
+
if min(extents) >= min_thickness:
|
| 216 |
+
return mesh
|
| 217 |
+
|
| 218 |
+
box_extents = [max(float(extent), min_thickness) for extent in extents]
|
| 219 |
+
transform = trimesh.transformations.translation_matrix(
|
| 220 |
+
mesh.bounds.mean(axis=0)
|
| 221 |
+
)
|
| 222 |
+
return trimesh.creation.box(extents=box_extents, transform=transform)
|
| 223 |
+
|
| 224 |
+
@staticmethod
|
| 225 |
+
def _origin_to_matrix(origin: ET.Element | None) -> np.ndarray:
|
| 226 |
+
"""Convert a URDF ``<origin>`` element to a 4x4 homogeneous matrix.
|
| 227 |
+
|
| 228 |
+
A missing element is treated as the identity transform. The rotation
|
| 229 |
+
follows the URDF convention (intrinsic XYZ Euler angles in radians),
|
| 230 |
+
matching ``transform_mesh`` which applies ``v' = R @ v + t``.
|
| 231 |
+
"""
|
| 232 |
+
matrix = np.eye(4)
|
| 233 |
+
if origin is None:
|
| 234 |
+
return matrix
|
| 235 |
+
xyz = list(map(float, origin.get("xyz", "0 0 0").split()))
|
| 236 |
+
rpy = list(map(float, origin.get("rpy", "0 0 0").split()))
|
| 237 |
+
matrix[:3, :3] = Rotation.from_euler(
|
| 238 |
+
"xyz", rpy, degrees=False
|
| 239 |
+
).as_matrix()
|
| 240 |
+
matrix[:3, 3] = xyz
|
| 241 |
+
return matrix
|
| 242 |
+
|
| 243 |
+
@staticmethod
|
| 244 |
+
def _bake_obj_vertices(
|
| 245 |
+
input_obj: str, output_obj: str, matrix: np.ndarray
|
| 246 |
+
) -> None:
|
| 247 |
+
"""Bake a rigid transform into an OBJ's vertex/normal lines only.
|
| 248 |
+
|
| 249 |
+
Unlike a full mesh re-export, this rewrites just the ``v`` (rotated and
|
| 250 |
+
translated) and ``vn`` (rotated) lines, leaving ``vt`` UVs, faces,
|
| 251 |
+
``usemtl``/``mtllib`` and the referenced texture atlas byte-identical.
|
| 252 |
+
That keeps MuJoCo/Isaac Gym texturing identical to the source asset
|
| 253 |
+
while the geometry is placed in world space.
|
| 254 |
+
|
| 255 |
+
Args:
|
| 256 |
+
input_obj (str): Source OBJ path.
|
| 257 |
+
output_obj (str): Destination OBJ path.
|
| 258 |
+
matrix (np.ndarray): 4x4 homogeneous transform (rotation only,
|
| 259 |
+
no scale).
|
| 260 |
+
"""
|
| 261 |
+
rotation = matrix[:3, :3]
|
| 262 |
+
translation = matrix[:3, 3]
|
| 263 |
+
|
| 264 |
+
lines: list[str] = []
|
| 265 |
+
with open(input_obj, "r", encoding="utf-8", errors="ignore") as handle:
|
| 266 |
+
for line in handle:
|
| 267 |
+
if line.startswith("v "):
|
| 268 |
+
parts = line.split()
|
| 269 |
+
point = (
|
| 270 |
+
rotation @ np.array(parts[1:4], dtype=float)
|
| 271 |
+
+ translation
|
| 272 |
+
)
|
| 273 |
+
extra = " " + " ".join(parts[4:]) if len(parts) > 4 else ""
|
| 274 |
+
lines.append(
|
| 275 |
+
f"v {point[0]:.6f} {point[1]:.6f} {point[2]:.6f}{extra}\n"
|
| 276 |
+
)
|
| 277 |
+
elif line.startswith("vn "):
|
| 278 |
+
parts = line.split()
|
| 279 |
+
normal = rotation @ np.array(parts[1:4], dtype=float)
|
| 280 |
+
lines.append(
|
| 281 |
+
f"vn {normal[0]:.6f} {normal[1]:.6f} {normal[2]:.6f}\n"
|
| 282 |
+
)
|
| 283 |
+
else:
|
| 284 |
+
lines.append(line)
|
| 285 |
+
|
| 286 |
+
os.makedirs(os.path.dirname(output_obj), exist_ok=True)
|
| 287 |
+
with open(output_obj, "w", encoding="utf-8") as handle:
|
| 288 |
+
handle.writelines(lines)
|
| 289 |
+
|
| 290 |
+
@classmethod
|
| 291 |
+
def _compose_origin(
|
| 292 |
+
cls,
|
| 293 |
+
outer: ET.Element | None,
|
| 294 |
+
inner: ET.Element | None,
|
| 295 |
+
) -> ET.Element | None:
|
| 296 |
+
"""Compose two URDF origins into a single ``<origin>`` element.
|
| 297 |
+
|
| 298 |
+
The returned transform applies ``inner`` first and then ``outer``
|
| 299 |
+
(``M = M_outer @ M_inner``), so baking a link's world frame onto its
|
| 300 |
+
local visual/collision origin yields world-space mesh vertices. The
|
| 301 |
+
result always carries both ``xyz`` and ``rpy`` so it can be consumed by
|
| 302 |
+
``transform_mesh``. Returns ``None`` when both inputs are absent.
|
| 303 |
+
|
| 304 |
+
Args:
|
| 305 |
+
outer (ET.Element | None): Outer (parent/world) origin.
|
| 306 |
+
inner (ET.Element | None): Inner (local) origin.
|
| 307 |
+
|
| 308 |
+
Returns:
|
| 309 |
+
ET.Element | None: Composed origin, or ``None`` if both are ``None``.
|
| 310 |
+
"""
|
| 311 |
+
if outer is None and inner is None:
|
| 312 |
+
return None
|
| 313 |
+
|
| 314 |
+
matrix = cls._origin_to_matrix(outer) @ cls._origin_to_matrix(inner)
|
| 315 |
+
xyz = matrix[:3, 3]
|
| 316 |
+
rpy = Rotation.from_matrix(matrix[:3, :3]).as_euler(
|
| 317 |
+
"xyz", degrees=False
|
| 318 |
+
)
|
| 319 |
+
|
| 320 |
+
origin = ET.Element("origin")
|
| 321 |
+
origin.set("xyz", " ".join(f"{value:.8g}" for value in xyz))
|
| 322 |
+
origin.set("rpy", " ".join(f"{value:.8g}" for value in rpy))
|
| 323 |
+
return origin
|
| 324 |
+
|
| 325 |
def add_geometry(
|
| 326 |
self,
|
| 327 |
mujoco_element: ET.Element,
|
|
|
|
| 333 |
mesh_name: str,
|
| 334 |
material: ET.Element | None = None,
|
| 335 |
is_collision: bool = False,
|
| 336 |
+
body_origin: ET.Element | None = None,
|
| 337 |
) -> None:
|
| 338 |
"""Adds geometry to MJCF body from URDF link.
|
| 339 |
|
|
|
|
| 347 |
mesh_name (str): Mesh name.
|
| 348 |
material (ET.Element, optional): Material element.
|
| 349 |
is_collision (bool, optional): If True, treat as collision geometry.
|
| 350 |
+
body_origin (ET.Element, optional): Link world origin to bake into
|
| 351 |
+
the mesh vertices; when None only the local origin is applied.
|
| 352 |
"""
|
| 353 |
element = link.find(tag)
|
| 354 |
geometry = element.find("geometry")
|
|
|
|
| 359 |
output_mesh = f"{output_dir}/{filename}"
|
| 360 |
self._copy_asset_file(input_mesh, output_mesh)
|
| 361 |
|
| 362 |
+
mesh_origin = self._compose_origin(body_origin, element.find("origin"))
|
| 363 |
if mesh_origin is not None:
|
| 364 |
+
if not is_collision and input_mesh.lower().endswith(".obj"):
|
| 365 |
+
# Preserve UVs/material atlas for textured visuals; a trimesh
|
| 366 |
+
# re-export would rewrite mtllib and drop the diffuse atlas.
|
| 367 |
+
self._bake_obj_vertices(
|
| 368 |
+
input_mesh,
|
| 369 |
+
output_mesh,
|
| 370 |
+
self._origin_to_matrix(mesh_origin),
|
| 371 |
+
)
|
| 372 |
+
else:
|
| 373 |
+
self.transform_mesh(input_mesh, output_mesh, mesh_origin)
|
| 374 |
|
| 375 |
if is_collision:
|
| 376 |
mesh_parts = trimesh.load(
|
|
|
|
| 381 |
mesh_parts = [trimesh.load(output_mesh, force="mesh")]
|
| 382 |
for idx, mesh_part in enumerate(mesh_parts):
|
| 383 |
if is_collision:
|
| 384 |
+
mesh_part = self._ensure_collision_volume(mesh_part)
|
| 385 |
idx_mesh_name = f"{mesh_name}_{idx}"
|
| 386 |
base, ext = os.path.splitext(filename)
|
| 387 |
idx_filename = f"{base}_{idx}{ext}"
|
| 388 |
+
idx_output_mesh = os.path.join(output_dir, idx_filename)
|
| 389 |
+
os.makedirs(os.path.dirname(idx_output_mesh), exist_ok=True)
|
| 390 |
+
mesh_part.export(idx_output_mesh)
|
| 391 |
geom_attrs = {
|
| 392 |
"contype": "1",
|
| 393 |
"conaffinity": "1",
|
|
|
|
| 395 |
}
|
| 396 |
else:
|
| 397 |
idx_mesh_name, idx_filename = mesh_name, filename
|
| 398 |
+
geom_attrs = {
|
| 399 |
+
"contype": "0",
|
| 400 |
+
"conaffinity": "0",
|
| 401 |
+
"density": "0",
|
| 402 |
+
}
|
| 403 |
|
| 404 |
ET.SubElement(
|
| 405 |
mujoco_element,
|
|
|
|
| 407 |
name=idx_mesh_name,
|
| 408 |
file=idx_filename,
|
| 409 |
scale=scale,
|
| 410 |
+
inertia="shell",
|
| 411 |
)
|
| 412 |
geom = ET.SubElement(body, "geom", type="mesh", mesh=idx_mesh_name)
|
| 413 |
geom.attrib.update(geom_attrs)
|
|
|
|
| 423 |
output_dir: str,
|
| 424 |
name: str,
|
| 425 |
reflectance: float = 0.2,
|
| 426 |
+
) -> ET.Element | None:
|
| 427 |
"""Adds materials to MJCF asset from URDF link.
|
| 428 |
|
| 429 |
Args:
|
|
|
|
| 443 |
mesh = geometry.find("mesh")
|
| 444 |
filename = mesh.get("filename")
|
| 445 |
dirname = os.path.dirname(filename)
|
| 446 |
+
texture_paths = sorted(glob(f"{input_dir}/{dirname}/*.png"))
|
| 447 |
+
keep_materials = self.kwargs.get("keep_materials")
|
| 448 |
+
if keep_materials:
|
| 449 |
+
texture_paths = [
|
| 450 |
+
path
|
| 451 |
+
for path in texture_paths
|
| 452 |
+
if any(
|
| 453 |
+
key.lower() in os.path.basename(path).lower()
|
| 454 |
+
for key in keep_materials
|
| 455 |
+
)
|
| 456 |
+
]
|
| 457 |
+
if not texture_paths:
|
| 458 |
+
return None
|
| 459 |
+
|
| 460 |
+
texture_path = next(
|
| 461 |
+
(
|
| 462 |
+
path
|
| 463 |
+
for path in texture_paths
|
| 464 |
+
if "diffuse" in os.path.basename(path).lower()
|
| 465 |
+
),
|
| 466 |
+
texture_paths[0],
|
| 467 |
+
)
|
| 468 |
+
file_name = os.path.basename(texture_path)
|
| 469 |
+
texture_file = (
|
| 470 |
+
os.path.join(dirname, file_name) if dirname else file_name
|
| 471 |
+
)
|
| 472 |
+
self._copy_asset_file(
|
| 473 |
+
texture_path,
|
| 474 |
+
os.path.join(output_dir, texture_file),
|
| 475 |
+
)
|
| 476 |
+
texture_name = f"texture_{name}_{os.path.splitext(file_name)[0]}"
|
| 477 |
+
material = ET.SubElement(
|
| 478 |
+
mujoco_element,
|
| 479 |
+
"material",
|
| 480 |
+
name=f"material_{name}",
|
| 481 |
+
texture=texture_name,
|
| 482 |
+
reflectance=str(reflectance),
|
| 483 |
+
)
|
| 484 |
+
ET.SubElement(
|
| 485 |
+
mujoco_element,
|
| 486 |
+
"texture",
|
| 487 |
+
name=texture_name,
|
| 488 |
+
type="2d",
|
| 489 |
+
file=texture_file,
|
| 490 |
+
)
|
| 491 |
return material
|
| 492 |
|
| 493 |
def convert(self, urdf_path: str, mjcf_path: str):
|
|
|
|
| 520 |
output_dir,
|
| 521 |
name=str(idx),
|
| 522 |
)
|
| 523 |
+
ET.SubElement(body, "joint", attrib={"type": "free"})
|
| 524 |
self.add_geometry(
|
| 525 |
mujoco_asset,
|
| 526 |
link,
|
|
|
|
| 578 |
output_dir = os.path.dirname(mjcf_path)
|
| 579 |
os.makedirs(output_dir, exist_ok=True)
|
| 580 |
|
| 581 |
+
parent_by_child = {}
|
| 582 |
+
origin_by_child = {}
|
| 583 |
+
for joint in root.findall("joint"):
|
| 584 |
+
joint_type = joint.get("type")
|
| 585 |
+
if joint_type != "fixed":
|
| 586 |
+
raise NotImplementedError(
|
| 587 |
+
f"URDFtoMJCFConverter only supports fixed joints, got "
|
| 588 |
+
f"{joint_type!r} for joint {joint.get('name')!r}."
|
| 589 |
+
)
|
| 590 |
+
parent_by_child[joint.find("child").get("link")] = joint.find(
|
| 591 |
+
"parent"
|
| 592 |
+
).get("link")
|
| 593 |
+
origin_by_child[joint.find("child").get("link")] = joint.find(
|
| 594 |
+
"origin"
|
| 595 |
+
)
|
| 596 |
+
|
| 597 |
+
link_world_origins = {}
|
| 598 |
+
|
| 599 |
+
def get_link_world_origin(link_name: str) -> ET.Element | None:
|
| 600 |
+
if link_name in link_world_origins:
|
| 601 |
+
return link_world_origins[link_name]
|
| 602 |
+
parent_link = parent_by_child.get(link_name)
|
| 603 |
+
if parent_link is None:
|
| 604 |
+
link_world_origins[link_name] = None
|
| 605 |
+
else:
|
| 606 |
+
link_world_origins[link_name] = self._compose_origin(
|
| 607 |
+
get_link_world_origin(parent_link),
|
| 608 |
+
origin_by_child.get(link_name),
|
| 609 |
+
)
|
| 610 |
+
return link_world_origins[link_name]
|
| 611 |
+
|
| 612 |
body_dict = {}
|
| 613 |
for idx, link in enumerate(root.findall("link")):
|
| 614 |
link_name = link.get("name", f"unnamed_link_{idx}")
|
|
|
|
| 632 |
output_dir,
|
| 633 |
f"visual_mesh_{idx}",
|
| 634 |
material,
|
| 635 |
+
body_origin=get_link_world_origin(link_name),
|
| 636 |
)
|
| 637 |
if link.find("collision") is not None:
|
| 638 |
self.add_geometry(
|
|
|
|
| 644 |
output_dir,
|
| 645 |
f"collision_mesh_{idx}",
|
| 646 |
is_collision=True,
|
| 647 |
+
body_origin=get_link_world_origin(link_name),
|
| 648 |
)
|
| 649 |
|
| 650 |
+
# Fixed-joint world transforms are baked into the mesh vertices above,
|
| 651 |
+
# so here we only rebuild the body hierarchy (all bodies stay at the
|
| 652 |
+
# identity frame), keeping the output identical across MuJoCo/Isaac Gym.
|
| 653 |
for joint in root.findall("joint"):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 654 |
parent_link = joint.find("parent").get("link")
|
| 655 |
child_link = joint.find("child").get("link")
|
|
|
|
| 656 |
if parent_link not in body_dict or child_link not in body_dict:
|
| 657 |
logger.warning(
|
| 658 |
f"Parent or child link not found for joint: {joint.get('name')}"
|
|
|
|
| 663 |
mujoco_worldbody.remove(child_body)
|
| 664 |
parent_body = body_dict[parent_link]
|
| 665 |
parent_body.append(child_body)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 666 |
|
| 667 |
tree = ET.ElementTree(mujoco_struct)
|
| 668 |
ET.indent(tree, space=" ", level=0)
|
|
|
|
| 1091 |
|
| 1092 |
urdf_paths = [
|
| 1093 |
'outputs/EmbodiedGenData/demo_assets/banana/result/banana.urdf',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1094 |
]
|
| 1095 |
|
| 1096 |
if target_asset_type == AssetType.MJCF:
|
|
|
|
| 1105 |
elif target_asset_type == AssetType.USD:
|
| 1106 |
output_files = [
|
| 1107 |
'outputs/EmbodiedGenData/demo_assets/banana/usd/banana.usd',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1108 |
]
|
| 1109 |
asset_converter = AssetConverterFactory.create(
|
| 1110 |
target_type=AssetType.USD,
|
|
|
|
| 1140 |
|
| 1141 |
# # Convert infinigen usdc to physics usdc
|
| 1142 |
# converter = PhysicsUSDAdder()
|
| 1143 |
+
# usd_paths = [
|
| 1144 |
+
# "outputs/gen_rooms_v2/simple/House_seed0/usd/export_scene/export_scene.usdc",
|
| 1145 |
+
# ]
|
| 1146 |
# with converter:
|
| 1147 |
+
# for usd_path in usd_paths:
|
| 1148 |
+
# output_path = usd_path.replace("/export_scene/", "/export_scene_physics/")
|
| 1149 |
+
# converter.convert(usd_path=usd_path, output_file=output_path)
|
| 1150 |
+
|
| 1151 |
+
# converter = URDFtoMJCFConverter()
|
| 1152 |
+
# urdf_paths = [
|
| 1153 |
+
# "outputs/gen_rooms_v2/simple/House_seed0/urdf/export_scene/scene.urdf",
|
| 1154 |
+
|
| 1155 |
+
# # "outputs/gen_rooms_v2/detail/House_seed0/urdf/export_scene/scene.urdf",
|
| 1156 |
+
# ]
|
| 1157 |
+
# for urdf_path in urdf_paths:
|
| 1158 |
+
# output_path = urdf_path.replace("/urdf/export_scene/", "/mjcf/export_scene/").replace(".urdf", ".xml")
|
| 1159 |
+
# os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
| 1160 |
+
# converter.convert(urdf_path=urdf_path, mjcf_path=output_path)
|
embodied_gen/data/backproject_v3.py
CHANGED
|
@@ -419,7 +419,7 @@ def parse_args():
|
|
| 419 |
parser.add_argument(
|
| 420 |
"--save_glb_path", type=str, default=None, help="Save glb path."
|
| 421 |
)
|
| 422 |
-
parser.add_argument("--n_max_faces", type=int, default=
|
| 423 |
args, unknown = parser.parse_known_args()
|
| 424 |
|
| 425 |
return args
|
|
|
|
| 419 |
parser.add_argument(
|
| 420 |
"--save_glb_path", type=str, default=None, help="Save glb path."
|
| 421 |
)
|
| 422 |
+
parser.add_argument("--n_max_faces", type=int, default=50000)
|
| 423 |
args, unknown = parser.parse_known_args()
|
| 424 |
|
| 425 |
return args
|
embodied_gen/data/differentiable_render.py
CHANGED
|
@@ -80,7 +80,11 @@ def create_mp4_from_images(
|
|
| 80 |
color = (255, 255, 255)
|
| 81 |
position = (20, 25)
|
| 82 |
|
| 83 |
-
with imageio.get_writer(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
for image in images:
|
| 85 |
image = image.clip(min=0, max=1)
|
| 86 |
image = (255.0 * image).astype(np.uint8)
|
|
@@ -147,6 +151,7 @@ class ImageRender(object):
|
|
| 147 |
gen_viewnormal_mp4 (bool, optional): Generate MP4 of view-space normals. Defaults to False.
|
| 148 |
gen_glonormal_mp4 (bool, optional): Generate MP4 of global-space normals. Defaults to False.
|
| 149 |
no_index_file (bool, optional): Skip saving index file. Defaults to False.
|
|
|
|
| 150 |
light_factor (float, optional): PBR light intensity multiplier. Defaults to 1.0.
|
| 151 |
|
| 152 |
Example:
|
|
@@ -185,7 +190,9 @@ class ImageRender(object):
|
|
| 185 |
gen_viewnormal_mp4: bool = False,
|
| 186 |
gen_glonormal_mp4: bool = False,
|
| 187 |
no_index_file: bool = False,
|
|
|
|
| 188 |
light_factor: float = 1.0,
|
|
|
|
| 189 |
) -> None:
|
| 190 |
camera = init_kal_camera(camera_params)
|
| 191 |
self.camera = camera
|
|
@@ -218,7 +225,9 @@ class ImageRender(object):
|
|
| 218 |
self.gen_color_mp4 = gen_color_mp4
|
| 219 |
self.gen_viewnormal_mp4 = gen_viewnormal_mp4
|
| 220 |
self.gen_glonormal_mp4 = gen_glonormal_mp4
|
|
|
|
| 221 |
self.light_factor = light_factor
|
|
|
|
| 222 |
self.no_index_file = no_index_file
|
| 223 |
|
| 224 |
def render_mesh(
|
|
@@ -313,7 +322,7 @@ class ImageRender(object):
|
|
| 313 |
create_mp4_from_images(
|
| 314 |
rendered_normals,
|
| 315 |
output_path=f"{output_dir}/normal.mp4",
|
| 316 |
-
fps=
|
| 317 |
prompt=prompt,
|
| 318 |
)
|
| 319 |
else:
|
|
@@ -325,9 +334,9 @@ class ImageRender(object):
|
|
| 325 |
data_dict[RenderItems.GLOBAL_NORMAL.value] = render_paths
|
| 326 |
|
| 327 |
if RenderItems.VIEW_NORMAL.value in self.render_items:
|
| 328 |
-
assert (
|
| 329 |
-
|
| 330 |
-
)
|
| 331 |
rendered_view_normals = self.renderer.transform_normal(
|
| 332 |
rendered_normals, self.mv, masks, to_view=True
|
| 333 |
)
|
|
@@ -336,7 +345,7 @@ class ImageRender(object):
|
|
| 336 |
create_mp4_from_images(
|
| 337 |
rendered_view_normals,
|
| 338 |
output_path=f"{output_dir}/view_normal.mp4",
|
| 339 |
-
fps=
|
| 340 |
prompt=prompt,
|
| 341 |
)
|
| 342 |
else:
|
|
@@ -388,7 +397,10 @@ class ImageRender(object):
|
|
| 388 |
try:
|
| 389 |
for idx, cam in enumerate(self.camera):
|
| 390 |
image, albedo, diffuse, _ = render_pbr(
|
| 391 |
-
mesh,
|
|
|
|
|
|
|
|
|
|
| 392 |
)
|
| 393 |
image = torch.cat([image[0], masks[idx]], axis=-1)
|
| 394 |
images.append(image.detach().cpu().numpy())
|
|
@@ -409,14 +421,14 @@ class ImageRender(object):
|
|
| 409 |
create_gif_from_images(
|
| 410 |
images,
|
| 411 |
output_path=f"{output_dir}/color.gif",
|
| 412 |
-
fps=
|
| 413 |
)
|
| 414 |
|
| 415 |
if self.gen_color_mp4:
|
| 416 |
create_mp4_from_images(
|
| 417 |
images,
|
| 418 |
output_path=f"{output_dir}/color.mp4",
|
| 419 |
-
fps=
|
| 420 |
prompt=prompt,
|
| 421 |
)
|
| 422 |
|
|
@@ -507,6 +519,12 @@ def parse_args():
|
|
| 507 |
default=1.0,
|
| 508 |
help="Light factor for mesh PBR rendering (default: 1.)",
|
| 509 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 510 |
parser.add_argument(
|
| 511 |
"--with_mtl",
|
| 512 |
action="store_true",
|
|
@@ -544,6 +562,12 @@ def parse_args():
|
|
| 544 |
default=None,
|
| 545 |
help="Text prompts for the rendering.",
|
| 546 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 547 |
|
| 548 |
args, unknown = parser.parse_known_args()
|
| 549 |
|
|
@@ -607,7 +631,9 @@ def entrypoint(**kwargs) -> None:
|
|
| 607 |
gen_color_mp4=args.gen_color_mp4,
|
| 608 |
gen_viewnormal_mp4=args.gen_viewnormal_mp4,
|
| 609 |
gen_glonormal_mp4=args.gen_glonormal_mp4,
|
|
|
|
| 610 |
light_factor=args.pbr_light_factor,
|
|
|
|
| 611 |
no_index_file=gen_video or args.no_index_file,
|
| 612 |
)
|
| 613 |
image_render.render_mesh(
|
|
|
|
| 80 |
color = (255, 255, 255)
|
| 81 |
position = (20, 25)
|
| 82 |
|
| 83 |
+
with imageio.get_writer(
|
| 84 |
+
output_path,
|
| 85 |
+
fps=fps,
|
| 86 |
+
macro_block_size=1,
|
| 87 |
+
) as writer:
|
| 88 |
for image in images:
|
| 89 |
image = image.clip(min=0, max=1)
|
| 90 |
image = (255.0 * image).astype(np.uint8)
|
|
|
|
| 151 |
gen_viewnormal_mp4 (bool, optional): Generate MP4 of view-space normals. Defaults to False.
|
| 152 |
gen_glonormal_mp4 (bool, optional): Generate MP4 of global-space normals. Defaults to False.
|
| 153 |
no_index_file (bool, optional): Skip saving index file. Defaults to False.
|
| 154 |
+
video_fps (int, optional): FPS for generated videos. Defaults to 15.
|
| 155 |
light_factor (float, optional): PBR light intensity multiplier. Defaults to 1.0.
|
| 156 |
|
| 157 |
Example:
|
|
|
|
| 190 |
gen_viewnormal_mp4: bool = False,
|
| 191 |
gen_glonormal_mp4: bool = False,
|
| 192 |
no_index_file: bool = False,
|
| 193 |
+
video_fps: int = 15,
|
| 194 |
light_factor: float = 1.0,
|
| 195 |
+
metallic: bool = False,
|
| 196 |
) -> None:
|
| 197 |
camera = init_kal_camera(camera_params)
|
| 198 |
self.camera = camera
|
|
|
|
| 225 |
self.gen_color_mp4 = gen_color_mp4
|
| 226 |
self.gen_viewnormal_mp4 = gen_viewnormal_mp4
|
| 227 |
self.gen_glonormal_mp4 = gen_glonormal_mp4
|
| 228 |
+
self.video_fps = video_fps
|
| 229 |
self.light_factor = light_factor
|
| 230 |
+
self.metallic = metallic
|
| 231 |
self.no_index_file = no_index_file
|
| 232 |
|
| 233 |
def render_mesh(
|
|
|
|
| 322 |
create_mp4_from_images(
|
| 323 |
rendered_normals,
|
| 324 |
output_path=f"{output_dir}/normal.mp4",
|
| 325 |
+
fps=self.video_fps,
|
| 326 |
prompt=prompt,
|
| 327 |
)
|
| 328 |
else:
|
|
|
|
| 334 |
data_dict[RenderItems.GLOBAL_NORMAL.value] = render_paths
|
| 335 |
|
| 336 |
if RenderItems.VIEW_NORMAL.value in self.render_items:
|
| 337 |
+
assert RenderItems.GLOBAL_NORMAL in self.render_items, (
|
| 338 |
+
f"Must render global normal firstly, got render_items: {self.render_items}."
|
| 339 |
+
) # noqa
|
| 340 |
rendered_view_normals = self.renderer.transform_normal(
|
| 341 |
rendered_normals, self.mv, masks, to_view=True
|
| 342 |
)
|
|
|
|
| 345 |
create_mp4_from_images(
|
| 346 |
rendered_view_normals,
|
| 347 |
output_path=f"{output_dir}/view_normal.mp4",
|
| 348 |
+
fps=self.video_fps,
|
| 349 |
prompt=prompt,
|
| 350 |
)
|
| 351 |
else:
|
|
|
|
| 397 |
try:
|
| 398 |
for idx, cam in enumerate(self.camera):
|
| 399 |
image, albedo, diffuse, _ = render_pbr(
|
| 400 |
+
mesh,
|
| 401 |
+
cam,
|
| 402 |
+
light_factor=self.light_factor,
|
| 403 |
+
metallic=self.metallic,
|
| 404 |
)
|
| 405 |
image = torch.cat([image[0], masks[idx]], axis=-1)
|
| 406 |
images.append(image.detach().cpu().numpy())
|
|
|
|
| 421 |
create_gif_from_images(
|
| 422 |
images,
|
| 423 |
output_path=f"{output_dir}/color.gif",
|
| 424 |
+
fps=self.video_fps,
|
| 425 |
)
|
| 426 |
|
| 427 |
if self.gen_color_mp4:
|
| 428 |
create_mp4_from_images(
|
| 429 |
images,
|
| 430 |
output_path=f"{output_dir}/color.mp4",
|
| 431 |
+
fps=self.video_fps,
|
| 432 |
prompt=prompt,
|
| 433 |
)
|
| 434 |
|
|
|
|
| 519 |
default=1.0,
|
| 520 |
help="Light factor for mesh PBR rendering (default: 1.)",
|
| 521 |
)
|
| 522 |
+
parser.add_argument(
|
| 523 |
+
"--pbr_metallic",
|
| 524 |
+
action=argparse.BooleanOptionalAction,
|
| 525 |
+
default=False,
|
| 526 |
+
help="Whether to keep metallic material properties for mesh PBR rendering (default: True).",
|
| 527 |
+
)
|
| 528 |
parser.add_argument(
|
| 529 |
"--with_mtl",
|
| 530 |
action="store_true",
|
|
|
|
| 562 |
default=None,
|
| 563 |
help="Text prompts for the rendering.",
|
| 564 |
)
|
| 565 |
+
parser.add_argument(
|
| 566 |
+
"--video_fps",
|
| 567 |
+
type=int,
|
| 568 |
+
default=15,
|
| 569 |
+
help="FPS for generated video outputs.",
|
| 570 |
+
)
|
| 571 |
|
| 572 |
args, unknown = parser.parse_known_args()
|
| 573 |
|
|
|
|
| 631 |
gen_color_mp4=args.gen_color_mp4,
|
| 632 |
gen_viewnormal_mp4=args.gen_viewnormal_mp4,
|
| 633 |
gen_glonormal_mp4=args.gen_glonormal_mp4,
|
| 634 |
+
video_fps=args.video_fps,
|
| 635 |
light_factor=args.pbr_light_factor,
|
| 636 |
+
metallic=args.pbr_metallic,
|
| 637 |
no_index_file=gen_video or args.no_index_file,
|
| 638 |
)
|
| 639 |
image_render.render_mesh(
|
embodied_gen/data/mesh_operator.py
CHANGED
|
@@ -159,9 +159,10 @@ class MeshFixer(object):
|
|
| 159 |
yaws.append(y)
|
| 160 |
pitchs.append(p)
|
| 161 |
|
| 162 |
-
yaws, pitchs =
|
| 163 |
-
|
| 164 |
-
|
|
|
|
| 165 |
radius, fov = 2.0, torch.deg2rad(torch.tensor(40)).to(vertices)
|
| 166 |
projection = utils3d.torch.perspective_from_fov_xy(fov, fov, 1, 3)
|
| 167 |
|
|
@@ -361,7 +362,7 @@ class MeshFixer(object):
|
|
| 361 |
|
| 362 |
tqdm.write(f"Removed {(~mask).sum()} faces by mincut")
|
| 363 |
else:
|
| 364 |
-
tqdm.write(
|
| 365 |
|
| 366 |
# Fill small boundaries (holes)
|
| 367 |
mesh = _meshfix.PyTMesh()
|
|
|
|
| 159 |
yaws.append(y)
|
| 160 |
pitchs.append(p)
|
| 161 |
|
| 162 |
+
yaws, pitchs = (
|
| 163 |
+
torch.tensor(yaws).to(vertices),
|
| 164 |
+
torch.tensor(pitchs).to(vertices),
|
| 165 |
+
)
|
| 166 |
radius, fov = 2.0, torch.deg2rad(torch.tensor(40)).to(vertices)
|
| 167 |
projection = utils3d.torch.perspective_from_fov_xy(fov, fov, 1, 3)
|
| 168 |
|
|
|
|
| 362 |
|
| 363 |
tqdm.write(f"Removed {(~mask).sum()} faces by mincut")
|
| 364 |
else:
|
| 365 |
+
tqdm.write("Removed 0 faces by mincut")
|
| 366 |
|
| 367 |
# Fill small boundaries (holes)
|
| 368 |
mesh = _meshfix.PyTMesh()
|
embodied_gen/data/utils.py
CHANGED
|
@@ -21,6 +21,7 @@ import os
|
|
| 21 |
import time
|
| 22 |
import zipfile
|
| 23 |
from contextlib import contextmanager
|
|
|
|
| 24 |
from dataclasses import dataclass, field
|
| 25 |
from shutil import rmtree
|
| 26 |
from typing import List, Tuple, Union
|
|
@@ -441,6 +442,37 @@ def save_images(
|
|
| 441 |
return save_paths
|
| 442 |
|
| 443 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 444 |
def _current_lighting(
|
| 445 |
azimuths: List[float],
|
| 446 |
elevations: List[float],
|
|
@@ -471,29 +503,59 @@ def _current_lighting(
|
|
| 471 |
return light_condition
|
| 472 |
|
| 473 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 474 |
def render_pbr(
|
| 475 |
mesh,
|
| 476 |
camera,
|
| 477 |
device="cuda",
|
| 478 |
cxt=None,
|
| 479 |
-
custom_materials=None,
|
| 480 |
light_factor=1.0,
|
|
|
|
| 481 |
):
|
| 482 |
if cxt is None:
|
| 483 |
cxt = dr.RasterizeCudaContext()
|
| 484 |
|
| 485 |
-
light_condition =
|
| 486 |
-
azimuths=[0, 90, 180, 270],
|
| 487 |
-
elevations=[90, 60, 30, 20],
|
| 488 |
light_factor=light_factor,
|
| 489 |
device=device,
|
| 490 |
)
|
|
|
|
| 491 |
render_res = kal.render.easy_render.render_mesh(
|
| 492 |
camera,
|
| 493 |
mesh,
|
| 494 |
lighting=light_condition,
|
| 495 |
nvdiffrast_context=cxt,
|
| 496 |
-
custom_materials=
|
| 497 |
)
|
| 498 |
|
| 499 |
image = render_res[kal.render.easy_render.RenderPass.render]
|
|
@@ -676,9 +738,9 @@ def import_kaolin_mesh(mesh_path: str, with_mtl: bool = False):
|
|
| 676 |
mesh = kal.io.obj.import_mesh(mesh_path, with_materials=with_material)
|
| 677 |
if with_mtl and mesh.materials and len(mesh.materials) > 0:
|
| 678 |
material = kal.render.materials.PBRMaterial()
|
| 679 |
-
assert (
|
| 680 |
-
"map_Kd
|
| 681 |
-
)
|
| 682 |
material.diffuse_texture = mesh.materials[0]["map_Kd"] / 255.0
|
| 683 |
mesh.materials = [material]
|
| 684 |
elif mesh_path.endswith(".ply"):
|
|
@@ -727,6 +789,7 @@ def save_mesh_with_mtl(
|
|
| 727 |
output_path: str,
|
| 728 |
material_base=(250, 250, 250, 255),
|
| 729 |
mesh_process: bool = True,
|
|
|
|
| 730 |
) -> trimesh.Trimesh:
|
| 731 |
if isinstance(texture, np.ndarray):
|
| 732 |
texture = Image.fromarray(texture)
|
|
@@ -742,6 +805,8 @@ def save_mesh_with_mtl(
|
|
| 742 |
diffuse=material_base,
|
| 743 |
ambient=material_base,
|
| 744 |
specular=material_base,
|
|
|
|
|
|
|
| 745 |
)
|
| 746 |
|
| 747 |
dir_name = os.path.dirname(output_path)
|
|
|
|
| 21 |
import time
|
| 22 |
import zipfile
|
| 23 |
from contextlib import contextmanager
|
| 24 |
+
from copy import deepcopy
|
| 25 |
from dataclasses import dataclass, field
|
| 26 |
from shutil import rmtree
|
| 27 |
from typing import List, Tuple, Union
|
|
|
|
| 442 |
return save_paths
|
| 443 |
|
| 444 |
|
| 445 |
+
def _disable_metallic_for_render(materials):
|
| 446 |
+
if materials is None:
|
| 447 |
+
return
|
| 448 |
+
|
| 449 |
+
for material in materials:
|
| 450 |
+
if hasattr(material, "metallic_texture"):
|
| 451 |
+
material.metallic_texture = None
|
| 452 |
+
if (
|
| 453 |
+
hasattr(material, "metallic_value")
|
| 454 |
+
and material.metallic_value is not None
|
| 455 |
+
):
|
| 456 |
+
if torch.is_tensor(material.metallic_value):
|
| 457 |
+
material.metallic_value = torch.zeros_like(
|
| 458 |
+
material.metallic_value
|
| 459 |
+
)
|
| 460 |
+
else:
|
| 461 |
+
material.metallic_value = 0.0
|
| 462 |
+
|
| 463 |
+
|
| 464 |
+
def _build_render_materials(mesh, metallic: bool = False):
|
| 465 |
+
if metallic:
|
| 466 |
+
return None
|
| 467 |
+
|
| 468 |
+
if mesh.materials is None:
|
| 469 |
+
return None
|
| 470 |
+
|
| 471 |
+
render_materials = deepcopy(mesh.materials)
|
| 472 |
+
_disable_metallic_for_render(render_materials)
|
| 473 |
+
return render_materials
|
| 474 |
+
|
| 475 |
+
|
| 476 |
def _current_lighting(
|
| 477 |
azimuths: List[float],
|
| 478 |
elevations: List[float],
|
|
|
|
| 503 |
return light_condition
|
| 504 |
|
| 505 |
|
| 506 |
+
def _uniform_lighting(
|
| 507 |
+
light_factor: float = 1.0,
|
| 508 |
+
device: str = "cuda",
|
| 509 |
+
sharpness: float = 0.5,
|
| 510 |
+
num_lights: int = 1024,
|
| 511 |
+
):
|
| 512 |
+
indices = torch.arange(num_lights, dtype=torch.float32, device=device)
|
| 513 |
+
golden_angle = math.pi * (3.0 - math.sqrt(5.0))
|
| 514 |
+
z = 1.0 - 2.0 * (indices + 0.5) / num_lights
|
| 515 |
+
radius = torch.sqrt(torch.clamp(1.0 - z * z, min=0.0))
|
| 516 |
+
theta = golden_angle * indices
|
| 517 |
+
directions = torch.stack(
|
| 518 |
+
[
|
| 519 |
+
radius * torch.cos(theta),
|
| 520 |
+
radius * torch.sin(theta),
|
| 521 |
+
z,
|
| 522 |
+
],
|
| 523 |
+
dim=1,
|
| 524 |
+
)
|
| 525 |
+
directions = F.normalize(directions, dim=1)
|
| 526 |
+
|
| 527 |
+
amplitude = torch.ones_like(directions) * (light_factor / len(directions))
|
| 528 |
+
light_condition = kal.render.lighting.SgLightingParameters(
|
| 529 |
+
amplitude=amplitude,
|
| 530 |
+
direction=directions,
|
| 531 |
+
sharpness=sharpness,
|
| 532 |
+
).to(device)
|
| 533 |
+
|
| 534 |
+
return light_condition
|
| 535 |
+
|
| 536 |
+
|
| 537 |
def render_pbr(
|
| 538 |
mesh,
|
| 539 |
camera,
|
| 540 |
device="cuda",
|
| 541 |
cxt=None,
|
|
|
|
| 542 |
light_factor=1.0,
|
| 543 |
+
metallic: bool = False,
|
| 544 |
):
|
| 545 |
if cxt is None:
|
| 546 |
cxt = dr.RasterizeCudaContext()
|
| 547 |
|
| 548 |
+
light_condition = _uniform_lighting(
|
|
|
|
|
|
|
| 549 |
light_factor=light_factor,
|
| 550 |
device=device,
|
| 551 |
)
|
| 552 |
+
render_materials = _build_render_materials(mesh, metallic)
|
| 553 |
render_res = kal.render.easy_render.render_mesh(
|
| 554 |
camera,
|
| 555 |
mesh,
|
| 556 |
lighting=light_condition,
|
| 557 |
nvdiffrast_context=cxt,
|
| 558 |
+
custom_materials=render_materials,
|
| 559 |
)
|
| 560 |
|
| 561 |
image = render_res[kal.render.easy_render.RenderPass.render]
|
|
|
|
| 738 |
mesh = kal.io.obj.import_mesh(mesh_path, with_materials=with_material)
|
| 739 |
if with_mtl and mesh.materials and len(mesh.materials) > 0:
|
| 740 |
material = kal.render.materials.PBRMaterial()
|
| 741 |
+
assert "map_Kd" in mesh.materials[0], (
|
| 742 |
+
"'map_Kd' not found in materials."
|
| 743 |
+
)
|
| 744 |
material.diffuse_texture = mesh.materials[0]["map_Kd"] / 255.0
|
| 745 |
mesh.materials = [material]
|
| 746 |
elif mesh_path.endswith(".ply"):
|
|
|
|
| 789 |
output_path: str,
|
| 790 |
material_base=(250, 250, 250, 255),
|
| 791 |
mesh_process: bool = True,
|
| 792 |
+
glossiness: float = 250.0,
|
| 793 |
) -> trimesh.Trimesh:
|
| 794 |
if isinstance(texture, np.ndarray):
|
| 795 |
texture = Image.fromarray(texture)
|
|
|
|
| 805 |
diffuse=material_base,
|
| 806 |
ambient=material_base,
|
| 807 |
specular=material_base,
|
| 808 |
+
# 250 gives a tight visible highlight similar to glossy plastic.
|
| 809 |
+
glossiness=glossiness,
|
| 810 |
)
|
| 811 |
|
| 812 |
dir_name = os.path.dirname(output_path)
|
embodied_gen/envs/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Simulation environments for EmbodiedGen."""
|
embodied_gen/envs/pick_embodiedgen.py
CHANGED
|
@@ -289,7 +289,7 @@ class PickEmbodiedGen(BaseEnv):
|
|
| 289 |
options: A dictionary of options for loading the scene.
|
| 290 |
"""
|
| 291 |
all_objects = []
|
| 292 |
-
logger.info(
|
| 293 |
for env_idx in range(self.num_envs):
|
| 294 |
env_actors = load_assets_from_layout_file(
|
| 295 |
self.scene,
|
|
|
|
| 289 |
options: A dictionary of options for loading the scene.
|
| 290 |
"""
|
| 291 |
all_objects = []
|
| 292 |
+
logger.info("Loading EmbodiedGen assets...")
|
| 293 |
for env_idx in range(self.num_envs):
|
| 294 |
env_actors = load_assets_from_layout_file(
|
| 295 |
self.scene,
|
embodied_gen/models/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Model implementations for EmbodiedGen."""
|
embodied_gen/models/gs_model.py
CHANGED
|
@@ -488,7 +488,7 @@ class GaussianOperator(GaussianBase):
|
|
| 488 |
renders = renders[0]
|
| 489 |
alphas = alphas[0].squeeze(-1)
|
| 490 |
|
| 491 |
-
assert renders.shape[-1] == 4,
|
| 492 |
rendered_rgb, rendered_depth = torch.split(renders, [3, 1], dim=-1)
|
| 493 |
|
| 494 |
return RenderResult(
|
|
|
|
| 488 |
renders = renders[0]
|
| 489 |
alphas = alphas[0].squeeze(-1)
|
| 490 |
|
| 491 |
+
assert renders.shape[-1] == 4, "Must render rgb, depth and alpha"
|
| 492 |
rendered_rgb, rendered_depth = torch.split(renders, [3, 1], dim=-1)
|
| 493 |
|
| 494 |
return RenderResult(
|
embodied_gen/models/hunyuan3d.py
ADDED
|
@@ -0,0 +1,957 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Project EmbodiedGen
|
| 2 |
+
#
|
| 3 |
+
# Copyright (c) 2025 Horizon Robotics. All Rights Reserved.
|
| 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
|
| 14 |
+
# implied. See the License for the specific language governing
|
| 15 |
+
# permissions and limitations under the License.
|
| 16 |
+
|
| 17 |
+
from __future__ import annotations
|
| 18 |
+
|
| 19 |
+
import base64
|
| 20 |
+
import hashlib
|
| 21 |
+
import hmac
|
| 22 |
+
import json
|
| 23 |
+
import os
|
| 24 |
+
import re
|
| 25 |
+
import socket
|
| 26 |
+
import time
|
| 27 |
+
import urllib.error
|
| 28 |
+
import urllib.request
|
| 29 |
+
from dataclasses import dataclass
|
| 30 |
+
from datetime import datetime, timezone
|
| 31 |
+
from glob import glob
|
| 32 |
+
from http.client import HTTPSConnection
|
| 33 |
+
from shutil import copy, copytree, rmtree
|
| 34 |
+
from typing import Optional, Tuple
|
| 35 |
+
|
| 36 |
+
import numpy as np
|
| 37 |
+
import trimesh
|
| 38 |
+
from PIL import Image
|
| 39 |
+
from embodied_gen.data.differentiable_render import (
|
| 40 |
+
entrypoint as render_pbr_video,
|
| 41 |
+
)
|
| 42 |
+
from embodied_gen.data.utils import delete_dir
|
| 43 |
+
from embodied_gen.utils.gpt_clients import GPT_CLIENT
|
| 44 |
+
from embodied_gen.utils.log import logger
|
| 45 |
+
from embodied_gen.utils.process_media import combine_images_to_grid
|
| 46 |
+
from embodied_gen.utils.tags import VERSION
|
| 47 |
+
from embodied_gen.validators.quality_checkers import (
|
| 48 |
+
BaseChecker,
|
| 49 |
+
ImageSegChecker,
|
| 50 |
+
)
|
| 51 |
+
from embodied_gen.validators.urdf_convertor import URDFGenerator
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
@dataclass(frozen=True)
|
| 55 |
+
class HunyuanConfig:
|
| 56 |
+
"""Tencent Hunyuan3D Pro endpoint + timing.
|
| 57 |
+
|
| 58 |
+
Defaults match the validated probe in ``outputs/hunyuan3d_api_expert/``.
|
| 59 |
+
Only the Pro action set is supported.
|
| 60 |
+
"""
|
| 61 |
+
|
| 62 |
+
host: str = "ai3d.tencentcloudapi.com"
|
| 63 |
+
service: str = "ai3d"
|
| 64 |
+
region: str = "ap-guangzhou"
|
| 65 |
+
version: str = "2025-05-13"
|
| 66 |
+
image_action: str = "SubmitHunyuanTo3DProJob"
|
| 67 |
+
query_action: str = "QueryHunyuanTo3DProJob"
|
| 68 |
+
result_format: str = "GLB"
|
| 69 |
+
texture_size: int = 2048
|
| 70 |
+
connect_timeout: float = 10.0
|
| 71 |
+
read_timeout: float = 60.0
|
| 72 |
+
poll_interval: float = 10.0
|
| 73 |
+
max_wait_seconds: float = 900.0
|
| 74 |
+
max_download_bytes: int = 512 * 1024 * 1024
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def load_credentials() -> Tuple[str, str]:
|
| 78 |
+
"""Read Tencent Cloud SecretId/SecretKey from environment.
|
| 79 |
+
|
| 80 |
+
Prefers ``TENCENT_SECRET_ID/KEY``; falls back to ``TENCENTCLOUD_*``.
|
| 81 |
+
Raises ``RuntimeError`` (credential-free message) when missing.
|
| 82 |
+
"""
|
| 83 |
+
sid = os.environ.get("TENCENT_SECRET_ID") or os.environ.get(
|
| 84 |
+
"TENCENTCLOUD_SECRET_ID"
|
| 85 |
+
)
|
| 86 |
+
skey = os.environ.get("TENCENT_SECRET_KEY") or os.environ.get(
|
| 87 |
+
"TENCENTCLOUD_SECRET_KEY"
|
| 88 |
+
)
|
| 89 |
+
if not sid or not skey:
|
| 90 |
+
raise RuntimeError(
|
| 91 |
+
"HUNYUAN3D backend requires Tencent Cloud credentials. Set "
|
| 92 |
+
"TENCENT_SECRET_ID and TENCENT_SECRET_KEY (or TENCENTCLOUD_*) "
|
| 93 |
+
"in the environment, e.g. `source .secrets/hunyuan3d.env`."
|
| 94 |
+
)
|
| 95 |
+
return sid, skey
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def _signed_headers(
|
| 99 |
+
payload: str,
|
| 100 |
+
action: str,
|
| 101 |
+
credentials: Tuple[str, str],
|
| 102 |
+
cfg: HunyuanConfig,
|
| 103 |
+
) -> dict:
|
| 104 |
+
"""Build fresh TC3-HMAC-SHA256 auth headers (re-built every request)."""
|
| 105 |
+
sid, skey = credentials
|
| 106 |
+
ts = int(time.time())
|
| 107 |
+
date = datetime.fromtimestamp(ts, tz=timezone.utc).strftime("%Y-%m-%d")
|
| 108 |
+
canon_headers = (
|
| 109 |
+
"content-type:application/json; charset=utf-8\n"
|
| 110 |
+
f"host:{cfg.host}\n"
|
| 111 |
+
f"x-tc-action:{action.lower()}\n"
|
| 112 |
+
)
|
| 113 |
+
signed = "content-type;host;x-tc-action"
|
| 114 |
+
canon_req = (
|
| 115 |
+
f"POST\n/\n\n{canon_headers}\n{signed}\n"
|
| 116 |
+
f"{hashlib.sha256(payload.encode()).hexdigest()}"
|
| 117 |
+
)
|
| 118 |
+
scope = f"{date}/{cfg.service}/tc3_request"
|
| 119 |
+
string_to_sign = (
|
| 120 |
+
f"TC3-HMAC-SHA256\n{ts}\n{scope}\n"
|
| 121 |
+
f"{hashlib.sha256(canon_req.encode()).hexdigest()}"
|
| 122 |
+
)
|
| 123 |
+
|
| 124 |
+
def _sign(key, msg):
|
| 125 |
+
return hmac.new(key, msg.encode(), hashlib.sha256).digest()
|
| 126 |
+
|
| 127 |
+
sd = _sign(("TC3" + skey).encode(), date)
|
| 128 |
+
ss = _sign(sd, cfg.service)
|
| 129 |
+
signing = _sign(ss, "tc3_request")
|
| 130 |
+
sig = hmac.new(
|
| 131 |
+
signing, string_to_sign.encode(), hashlib.sha256
|
| 132 |
+
).hexdigest()
|
| 133 |
+
return {
|
| 134 |
+
"Authorization": (
|
| 135 |
+
f"TC3-HMAC-SHA256 Credential={sid}/{scope}, "
|
| 136 |
+
f"SignedHeaders={signed}, Signature={sig}"
|
| 137 |
+
),
|
| 138 |
+
"Content-Type": "application/json; charset=utf-8",
|
| 139 |
+
"Host": cfg.host,
|
| 140 |
+
"X-TC-Action": action,
|
| 141 |
+
"X-TC-Timestamp": str(ts),
|
| 142 |
+
"X-TC-Version": cfg.version,
|
| 143 |
+
"X-TC-Region": cfg.region,
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def _post_signed(
|
| 148 |
+
payload_obj: dict,
|
| 149 |
+
action: str,
|
| 150 |
+
credentials: Tuple[str, str],
|
| 151 |
+
cfg: HunyuanConfig,
|
| 152 |
+
) -> dict:
|
| 153 |
+
"""POST a Tencent Cloud TC3-signed JSON request and return ``Response``.
|
| 154 |
+
|
| 155 |
+
Routes through ``HTTPS_PROXY`` via CONNECT (``HTTPSConnection`` does not
|
| 156 |
+
honor the env var on its own). Never logs credentials, signed headers,
|
| 157 |
+
or the request payload (which carries base64 image data).
|
| 158 |
+
"""
|
| 159 |
+
payload = json.dumps(
|
| 160 |
+
payload_obj, separators=(",", ":"), ensure_ascii=False
|
| 161 |
+
)
|
| 162 |
+
headers = _signed_headers(payload, action, credentials, cfg)
|
| 163 |
+
# ``http.client.HTTPSConnection`` does NOT auto-honor ``HTTPS_PROXY``
|
| 164 |
+
# (unlike ``urllib.request.urlopen``); read it explicitly and tunnel
|
| 165 |
+
# via CONNECT, otherwise direct connections to Tencent Cloud will be
|
| 166 |
+
# blocked by the corporate egress firewall.
|
| 167 |
+
proxy = os.environ.get("HTTPS_PROXY") or os.environ.get("HTTP_PROXY")
|
| 168 |
+
timeout = cfg.connect_timeout + cfg.read_timeout
|
| 169 |
+
if proxy:
|
| 170 |
+
from urllib.parse import urlparse
|
| 171 |
+
|
| 172 |
+
p = urlparse(proxy)
|
| 173 |
+
conn = HTTPSConnection(p.hostname, p.port or 80, timeout=timeout)
|
| 174 |
+
conn.set_tunnel(cfg.host, 443)
|
| 175 |
+
else:
|
| 176 |
+
conn = HTTPSConnection(cfg.host, timeout=timeout)
|
| 177 |
+
try:
|
| 178 |
+
conn.request("POST", "/", body=payload.encode(), headers=headers)
|
| 179 |
+
resp = conn.getresponse()
|
| 180 |
+
status, body = resp.status, resp.read().decode(errors="replace")
|
| 181 |
+
finally:
|
| 182 |
+
conn.close()
|
| 183 |
+
|
| 184 |
+
if not 200 <= status < 300:
|
| 185 |
+
raise RuntimeError(
|
| 186 |
+
f"Hunyuan3D {action} HTTP {status}; len={len(body)}."
|
| 187 |
+
)
|
| 188 |
+
try:
|
| 189 |
+
data = json.loads(body).get("Response", {})
|
| 190 |
+
except json.JSONDecodeError as exc:
|
| 191 |
+
raise RuntimeError(
|
| 192 |
+
f"Hunyuan3D {action} non-JSON (HTTP {status}): {exc}"
|
| 193 |
+
)
|
| 194 |
+
err = data.get("Error")
|
| 195 |
+
if err:
|
| 196 |
+
raise RuntimeError(
|
| 197 |
+
f"Hunyuan3D {action} Tencent error: "
|
| 198 |
+
f"Code={err.get('Code')} Message={err.get('Message')} "
|
| 199 |
+
f"RequestId={data.get('RequestId')}"
|
| 200 |
+
)
|
| 201 |
+
return data
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
def submit_pro_job(
|
| 205 |
+
image_path: Optional[str] = None,
|
| 206 |
+
credentials: Tuple[str, str] = None,
|
| 207 |
+
cfg: HunyuanConfig = None,
|
| 208 |
+
prompt: Optional[str] = None,
|
| 209 |
+
) -> str:
|
| 210 |
+
"""Submit a Hunyuan3D Pro job, return its ``JobId``.
|
| 211 |
+
|
| 212 |
+
Provide exactly one of ``image_path`` (image-to-3D, body field
|
| 213 |
+
``ImageBase64``) or ``prompt`` (text-to-3D, body field ``Prompt``).
|
| 214 |
+
Tencent's ``SubmitHunyuanTo3DProJob`` action is shared between both
|
| 215 |
+
modes; only the body discriminator differs.
|
| 216 |
+
"""
|
| 217 |
+
if (image_path is None) == (prompt is None):
|
| 218 |
+
raise ValueError(
|
| 219 |
+
"submit_pro_job requires exactly one of image_path or prompt."
|
| 220 |
+
)
|
| 221 |
+
if credentials is None or cfg is None:
|
| 222 |
+
raise ValueError("credentials and cfg are required.")
|
| 223 |
+
|
| 224 |
+
payload = {"ResultFormat": cfg.result_format, "EnablePBR": True}
|
| 225 |
+
if image_path is not None:
|
| 226 |
+
if not os.path.isfile(image_path):
|
| 227 |
+
raise FileNotFoundError(
|
| 228 |
+
f"Hunyuan3D input image missing: {image_path}"
|
| 229 |
+
)
|
| 230 |
+
with open(image_path, "rb") as fh:
|
| 231 |
+
payload["ImageBase64"] = base64.b64encode(fh.read()).decode()
|
| 232 |
+
mode = "image"
|
| 233 |
+
else:
|
| 234 |
+
payload["Prompt"] = prompt
|
| 235 |
+
mode = "text"
|
| 236 |
+
|
| 237 |
+
resp = _post_signed(payload, cfg.image_action, credentials, cfg)
|
| 238 |
+
job_id = resp.get("JobId")
|
| 239 |
+
if not job_id:
|
| 240 |
+
raise RuntimeError(
|
| 241 |
+
f"Hunyuan3D submit returned no JobId; "
|
| 242 |
+
f"RequestId={resp.get('RequestId')}."
|
| 243 |
+
)
|
| 244 |
+
logger.info(
|
| 245 |
+
"HUNYUAN3D submit OK (%s): JobId=%s RequestId=%s",
|
| 246 |
+
mode,
|
| 247 |
+
job_id,
|
| 248 |
+
resp.get("RequestId"),
|
| 249 |
+
)
|
| 250 |
+
return job_id
|
| 251 |
+
|
| 252 |
+
|
| 253 |
+
def wait_for_pro_job(
|
| 254 |
+
job_id: str,
|
| 255 |
+
credentials: Tuple[str, str],
|
| 256 |
+
cfg: HunyuanConfig,
|
| 257 |
+
) -> dict:
|
| 258 |
+
"""Poll the job until DONE; raise on FAIL/unknown/timeout."""
|
| 259 |
+
deadline = time.time() + cfg.max_wait_seconds
|
| 260 |
+
last_status = None
|
| 261 |
+
while True:
|
| 262 |
+
resp = _post_signed(
|
| 263 |
+
{"JobId": job_id}, cfg.query_action, credentials, cfg
|
| 264 |
+
)
|
| 265 |
+
status = resp.get("Status")
|
| 266 |
+
if status != last_status:
|
| 267 |
+
logger.info(
|
| 268 |
+
"HUNYUAN3D job %s status=%s RequestId=%s",
|
| 269 |
+
job_id,
|
| 270 |
+
status,
|
| 271 |
+
resp.get("RequestId"),
|
| 272 |
+
)
|
| 273 |
+
if last_status is None and status in ("WAIT", "RUN"):
|
| 274 |
+
logger.info(
|
| 275 |
+
"HUNYUAN3D Pro inference typically takes ~3 minutes; "
|
| 276 |
+
"polling every %ss.",
|
| 277 |
+
int(cfg.poll_interval),
|
| 278 |
+
)
|
| 279 |
+
last_status = status
|
| 280 |
+
if status == "DONE":
|
| 281 |
+
return resp
|
| 282 |
+
if status == "FAIL":
|
| 283 |
+
raise RuntimeError(
|
| 284 |
+
f"Hunyuan3D job {job_id} FAIL: "
|
| 285 |
+
f"code={resp.get('ErrorCode')} "
|
| 286 |
+
f"message={resp.get('ErrorMessage')} "
|
| 287 |
+
f"RequestId={resp.get('RequestId')}."
|
| 288 |
+
)
|
| 289 |
+
if status not in ("WAIT", "RUN"):
|
| 290 |
+
raise RuntimeError(
|
| 291 |
+
f"Hunyuan3D job {job_id} unknown status={status!r}; "
|
| 292 |
+
f"RequestId={resp.get('RequestId')}."
|
| 293 |
+
)
|
| 294 |
+
if time.time() >= deadline:
|
| 295 |
+
raise TimeoutError(
|
| 296 |
+
f"Hunyuan3D job {job_id} did not finish within "
|
| 297 |
+
f"{cfg.max_wait_seconds}s (last status={status})."
|
| 298 |
+
)
|
| 299 |
+
time.sleep(cfg.poll_interval)
|
| 300 |
+
|
| 301 |
+
|
| 302 |
+
def _download_url_to_path(url: str, dst: str, cfg: HunyuanConfig) -> int:
|
| 303 |
+
"""Stream ``url`` to ``dst`` with size/timeout caps. Returns bytes written.
|
| 304 |
+
|
| 305 |
+
Logs only the host (signed URL paths carry short-lived auth tokens).
|
| 306 |
+
"""
|
| 307 |
+
from urllib.parse import urlparse
|
| 308 |
+
|
| 309 |
+
os.makedirs(os.path.dirname(os.path.abspath(dst)), exist_ok=True)
|
| 310 |
+
logger.info(
|
| 311 |
+
"HUNYUAN3D downloading %s from host=%s",
|
| 312 |
+
os.path.basename(dst),
|
| 313 |
+
urlparse(url).hostname or "?",
|
| 314 |
+
)
|
| 315 |
+
total = 0
|
| 316 |
+
timeout = cfg.connect_timeout + cfg.read_timeout
|
| 317 |
+
try:
|
| 318 |
+
with urllib.request.urlopen(url, timeout=timeout) as resp:
|
| 319 |
+
if not 200 <= resp.status < 300:
|
| 320 |
+
raise RuntimeError(
|
| 321 |
+
f"Hunyuan3D download HTTP {resp.status} for "
|
| 322 |
+
f"{os.path.basename(dst)}."
|
| 323 |
+
)
|
| 324 |
+
with open(dst, "wb") as out:
|
| 325 |
+
while True:
|
| 326 |
+
chunk = resp.read(1024 * 1024)
|
| 327 |
+
if not chunk:
|
| 328 |
+
break
|
| 329 |
+
total += len(chunk)
|
| 330 |
+
if total > cfg.max_download_bytes:
|
| 331 |
+
raise RuntimeError(
|
| 332 |
+
f"Hunyuan3D download exceeded "
|
| 333 |
+
f"{cfg.max_download_bytes} bytes for "
|
| 334 |
+
f"{os.path.basename(dst)}."
|
| 335 |
+
)
|
| 336 |
+
out.write(chunk)
|
| 337 |
+
except (urllib.error.URLError, socket.timeout) as exc:
|
| 338 |
+
raise RuntimeError(
|
| 339 |
+
f"Hunyuan3D download failed for {os.path.basename(dst)}: {exc}"
|
| 340 |
+
)
|
| 341 |
+
return total
|
| 342 |
+
|
| 343 |
+
|
| 344 |
+
def acquire_pro_glb(
|
| 345 |
+
image_path: Optional[str] = None,
|
| 346 |
+
output_dir: str = None,
|
| 347 |
+
asset_name: str = None,
|
| 348 |
+
credentials: Tuple[str, str] = None,
|
| 349 |
+
cfg: HunyuanConfig = None,
|
| 350 |
+
prompt: Optional[str] = None,
|
| 351 |
+
) -> str:
|
| 352 |
+
"""End-to-end: submit + poll + download GLB into the output dir.
|
| 353 |
+
|
| 354 |
+
Provide exactly one of ``image_path`` or ``prompt`` (see
|
| 355 |
+
:func:`submit_pro_job` for the body-field difference).
|
| 356 |
+
"""
|
| 357 |
+
if (
|
| 358 |
+
output_dir is None
|
| 359 |
+
or asset_name is None
|
| 360 |
+
or credentials is None
|
| 361 |
+
or cfg is None
|
| 362 |
+
):
|
| 363 |
+
raise ValueError(
|
| 364 |
+
"output_dir, asset_name, credentials and cfg are required."
|
| 365 |
+
)
|
| 366 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 367 |
+
glb_path = os.path.join(output_dir, f"{asset_name}.glb")
|
| 368 |
+
|
| 369 |
+
job_id = submit_pro_job(
|
| 370 |
+
image_path=image_path,
|
| 371 |
+
prompt=prompt,
|
| 372 |
+
credentials=credentials,
|
| 373 |
+
cfg=cfg,
|
| 374 |
+
)
|
| 375 |
+
resp = wait_for_pro_job(job_id, credentials, cfg)
|
| 376 |
+
|
| 377 |
+
files = resp.get("ResultFile3Ds") or []
|
| 378 |
+
glb_url = next(
|
| 379 |
+
(
|
| 380 |
+
f.get("Url")
|
| 381 |
+
for f in files
|
| 382 |
+
if (f.get("Type") or "").upper() == "GLB" and f.get("Url")
|
| 383 |
+
),
|
| 384 |
+
None,
|
| 385 |
+
)
|
| 386 |
+
if not glb_url:
|
| 387 |
+
raise RuntimeError(
|
| 388 |
+
f"Hunyuan3D job {job_id} returned no GLB; "
|
| 389 |
+
f"RequestId={resp.get('RequestId')}."
|
| 390 |
+
)
|
| 391 |
+
_download_url_to_path(glb_url, glb_path, cfg)
|
| 392 |
+
return glb_path
|
| 393 |
+
|
| 394 |
+
|
| 395 |
+
def _texture_array(tex) -> Optional[np.ndarray]:
|
| 396 |
+
"""Return RGB ndarray for a glTF texture, or None if absent/invalid."""
|
| 397 |
+
if tex is None or not hasattr(tex, "convert"):
|
| 398 |
+
return None
|
| 399 |
+
return np.asarray(tex.convert("RGB"))
|
| 400 |
+
|
| 401 |
+
|
| 402 |
+
def _save_rgb(arr: Optional[np.ndarray], dst: str, max_edge: int) -> bool:
|
| 403 |
+
"""Save an RGB texture as PNG, capping the longest edge at ``max_edge``."""
|
| 404 |
+
if arr is None:
|
| 405 |
+
return False
|
| 406 |
+
img = Image.fromarray(arr)
|
| 407 |
+
longest = max(img.size)
|
| 408 |
+
if longest > max_edge:
|
| 409 |
+
scale = max_edge / float(longest)
|
| 410 |
+
img = img.resize(
|
| 411 |
+
(
|
| 412 |
+
max(1, int(img.size[0] * scale)),
|
| 413 |
+
max(1, int(img.size[1] * scale)),
|
| 414 |
+
),
|
| 415 |
+
Image.LANCZOS,
|
| 416 |
+
)
|
| 417 |
+
img.save(dst)
|
| 418 |
+
return True
|
| 419 |
+
|
| 420 |
+
|
| 421 |
+
def _bake_scene_transform(
|
| 422 |
+
scene: trimesh.Scene,
|
| 423 |
+
) -> Tuple[trimesh.Trimesh, np.ndarray, str]:
|
| 424 |
+
"""Apply scene-graph transforms to mesh vertices; return one Trimesh."""
|
| 425 |
+
if len(scene.graph.nodes_geometry) != 1:
|
| 426 |
+
parts = []
|
| 427 |
+
for n in scene.graph.nodes_geometry:
|
| 428 |
+
xform, gname = scene.graph[n]
|
| 429 |
+
m = scene.geometry[gname].copy()
|
| 430 |
+
m.apply_transform(xform)
|
| 431 |
+
parts.append(m)
|
| 432 |
+
return trimesh.util.concatenate(parts), np.eye(4), "concatenated"
|
| 433 |
+
n = next(iter(scene.graph.nodes_geometry))
|
| 434 |
+
xform, gname = scene.graph[n]
|
| 435 |
+
mesh = scene.geometry[gname].copy()
|
| 436 |
+
mesh.apply_transform(xform)
|
| 437 |
+
return mesh, xform, gname
|
| 438 |
+
|
| 439 |
+
|
| 440 |
+
def export_glb_to_obj(
|
| 441 |
+
glb_path: str,
|
| 442 |
+
output_dir: str,
|
| 443 |
+
asset_name: str,
|
| 444 |
+
texture_size: int = 2048,
|
| 445 |
+
pre_align_rotation: Optional[np.ndarray] = None,
|
| 446 |
+
) -> str:
|
| 447 |
+
"""Convert a Hunyuan3D Pro GLB into the full-PBR OBJ + MTL + PBR PNGs.
|
| 448 |
+
|
| 449 |
+
Bakes the GLB scene transform, optionally applies ``pre_align_rotation``
|
| 450 |
+
to the vertex array (used by the text-to-3D path, whose endpoint emits
|
| 451 |
+
a frame rotated 90° around the up axis relative to the image-to-3D
|
| 452 |
+
endpoint), recenters to the bbox origin (matching SAM3D's convention
|
| 453 |
+
of putting the model origin at the geometric center), and writes a
|
| 454 |
+
Blender-compatible OBJ/MTL referencing 4 PBR PNGs (baseColor /
|
| 455 |
+
metallic / roughness / normal) plus a ``_pbr_material.json`` metadata
|
| 456 |
+
sidecar. The source GLB at ``glb_path`` is overwritten with the
|
| 457 |
+
aligned mesh so downstream steps can reuse it. Returns the OBJ path.
|
| 458 |
+
"""
|
| 459 |
+
from trimesh.exchange.obj import export_obj
|
| 460 |
+
|
| 461 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 462 |
+
obj_path = os.path.join(output_dir, f"{asset_name}.obj")
|
| 463 |
+
mtl_path = os.path.join(output_dir, f"{asset_name}.mtl")
|
| 464 |
+
json_path = os.path.join(output_dir, f"{asset_name}_pbr_material.json")
|
| 465 |
+
|
| 466 |
+
scene = trimesh.load(glb_path, force="scene", process=False)
|
| 467 |
+
mesh, baked_xform, geom_name = _bake_scene_transform(scene)
|
| 468 |
+
material = getattr(getattr(mesh, "visual", None), "material", None)
|
| 469 |
+
|
| 470 |
+
# Align to SAM3D convention: optional pre-rotation (text-to-3D needs a
|
| 471 |
+
# -90° around the up axis to share the image-to-3D frame) + recenter to
|
| 472 |
+
# the bbox origin. Overwrite the source GLB so downstream steps can
|
| 473 |
+
# reuse the aligned full-PBR mesh without an extra load/export pass.
|
| 474 |
+
V = np.asarray(mesh.vertices, dtype=np.float32)
|
| 475 |
+
if pre_align_rotation is not None:
|
| 476 |
+
V = V @ np.asarray(pre_align_rotation, dtype=np.float32)
|
| 477 |
+
bbox_center = (V.min(axis=0) + V.max(axis=0)) * 0.5
|
| 478 |
+
mesh.vertices = V - bbox_center
|
| 479 |
+
mesh.export(glb_path)
|
| 480 |
+
|
| 481 |
+
raw_name = getattr(material, "name", None) or f"{asset_name}_material"
|
| 482 |
+
material_name = re.sub(r"[^A-Za-z0-9_.-]+", "_", raw_name).strip("._")
|
| 483 |
+
if not material_name:
|
| 484 |
+
material_name = f"{asset_name}_material"
|
| 485 |
+
|
| 486 |
+
# Write OBJ (rewrite usemtl so it points at our material, after mtllib).
|
| 487 |
+
obj_text = export_obj(
|
| 488 |
+
mesh,
|
| 489 |
+
include_normals=True,
|
| 490 |
+
include_color=True,
|
| 491 |
+
include_texture=True,
|
| 492 |
+
return_texture=False,
|
| 493 |
+
write_texture=False,
|
| 494 |
+
mtl_name=os.path.basename(mtl_path),
|
| 495 |
+
header=(
|
| 496 |
+
"Exported from Hunyuan3D Pro GLB; "
|
| 497 |
+
"scene transform baked, recentered to bbox origin"
|
| 498 |
+
),
|
| 499 |
+
)
|
| 500 |
+
obj_text = re.sub(
|
| 501 |
+
r"^usemtl\s+.+$",
|
| 502 |
+
f"usemtl {material_name}",
|
| 503 |
+
obj_text,
|
| 504 |
+
flags=re.MULTILINE,
|
| 505 |
+
)
|
| 506 |
+
mtllib_line = f"mtllib {os.path.basename(mtl_path)}\n"
|
| 507 |
+
if f"usemtl {material_name}" not in obj_text:
|
| 508 |
+
obj_text = obj_text.replace(
|
| 509 |
+
mtllib_line, f"{mtllib_line}usemtl {material_name}\n", 1
|
| 510 |
+
)
|
| 511 |
+
with open(obj_path, "w", encoding="utf-8") as fh:
|
| 512 |
+
fh.write(obj_text)
|
| 513 |
+
|
| 514 |
+
# PBR textures. metallicRoughnessTexture: G=roughness, B=metallic.
|
| 515 |
+
base_arr = _texture_array(getattr(material, "baseColorTexture", None))
|
| 516 |
+
mr_arr = _texture_array(
|
| 517 |
+
getattr(material, "metallicRoughnessTexture", None)
|
| 518 |
+
)
|
| 519 |
+
normal_arr = _texture_array(getattr(material, "normalTexture", None))
|
| 520 |
+
files = {
|
| 521 |
+
"baseColor": f"{asset_name}_baseColor.png",
|
| 522 |
+
"metallic": f"{asset_name}_metallic.png",
|
| 523 |
+
"roughness": f"{asset_name}_roughness.png",
|
| 524 |
+
"normal": f"{asset_name}_normal.png",
|
| 525 |
+
}
|
| 526 |
+
metallic_arr = (
|
| 527 |
+
np.stack([mr_arr[:, :, 2]] * 3, axis=-1)
|
| 528 |
+
if mr_arr is not None
|
| 529 |
+
else None
|
| 530 |
+
)
|
| 531 |
+
roughness_arr = (
|
| 532 |
+
np.stack([mr_arr[:, :, 1]] * 3, axis=-1)
|
| 533 |
+
if mr_arr is not None
|
| 534 |
+
else None
|
| 535 |
+
)
|
| 536 |
+
saved = {
|
| 537 |
+
"baseColor": _save_rgb(
|
| 538 |
+
base_arr,
|
| 539 |
+
os.path.join(output_dir, files["baseColor"]),
|
| 540 |
+
texture_size,
|
| 541 |
+
),
|
| 542 |
+
"metallic": _save_rgb(
|
| 543 |
+
metallic_arr,
|
| 544 |
+
os.path.join(output_dir, files["metallic"]),
|
| 545 |
+
texture_size,
|
| 546 |
+
),
|
| 547 |
+
"roughness": _save_rgb(
|
| 548 |
+
roughness_arr,
|
| 549 |
+
os.path.join(output_dir, files["roughness"]),
|
| 550 |
+
texture_size,
|
| 551 |
+
),
|
| 552 |
+
"normal": _save_rgb(
|
| 553 |
+
normal_arr,
|
| 554 |
+
os.path.join(output_dir, files["normal"]),
|
| 555 |
+
texture_size,
|
| 556 |
+
),
|
| 557 |
+
}
|
| 558 |
+
|
| 559 |
+
def _factor(attr: str, default: float = 1.0) -> float:
|
| 560 |
+
v = getattr(material, attr, default)
|
| 561 |
+
return default if v is None else float(v)
|
| 562 |
+
|
| 563 |
+
bc = getattr(material, "baseColorFactor", None)
|
| 564 |
+
if bc is None:
|
| 565 |
+
base_factor = [1.0, 1.0, 1.0, 1.0]
|
| 566 |
+
else:
|
| 567 |
+
arr = np.asarray(bc, dtype=float).reshape(-1)
|
| 568 |
+
if arr.max(initial=1.0) > 1.0:
|
| 569 |
+
arr = arr / 255.0
|
| 570 |
+
base_factor = [float(arr[0]), float(arr[1]), float(arr[2])]
|
| 571 |
+
base_factor.append(float(arr[3]) if len(arr) >= 4 else 1.0)
|
| 572 |
+
metallic_factor = _factor("metallicFactor", 1.0)
|
| 573 |
+
roughness_factor = _factor("roughnessFactor", 1.0)
|
| 574 |
+
|
| 575 |
+
ns = max(1.0, min(1000.0, (1.0 - roughness_factor) * 1000.0))
|
| 576 |
+
lines = [
|
| 577 |
+
"# Exported from Hunyuan3D Pro GLB",
|
| 578 |
+
"# PBR note: glTF metallicRoughnessTexture stores roughness in G "
|
| 579 |
+
"and metallic in B.",
|
| 580 |
+
f"newmtl {material_name}",
|
| 581 |
+
f"Ka {base_factor[0]:.8g} {base_factor[1]:.8g} {base_factor[2]:.8g}",
|
| 582 |
+
f"Kd {base_factor[0]:.8g} {base_factor[1]:.8g} {base_factor[2]:.8g}",
|
| 583 |
+
"Ks 0 0 0",
|
| 584 |
+
f"Ns {ns:.8g}",
|
| 585 |
+
f"d {base_factor[3]:.8g}",
|
| 586 |
+
"illum 2",
|
| 587 |
+
f"Pm {metallic_factor:.8g}",
|
| 588 |
+
f"Pr {roughness_factor:.8g}",
|
| 589 |
+
]
|
| 590 |
+
if saved["baseColor"]:
|
| 591 |
+
lines.append(f"map_Kd {files['baseColor']}")
|
| 592 |
+
if saved["normal"]:
|
| 593 |
+
lines.append(f"norm {files['normal']}")
|
| 594 |
+
lines.append(f"bump {files['normal']}")
|
| 595 |
+
if saved["metallic"]:
|
| 596 |
+
lines.append(f"map_Pm {files['metallic']}")
|
| 597 |
+
if saved["roughness"]:
|
| 598 |
+
lines.append(f"map_Pr {files['roughness']}")
|
| 599 |
+
with open(mtl_path, "w", encoding="utf-8") as fh:
|
| 600 |
+
fh.write("\n".join(lines) + "\n")
|
| 601 |
+
|
| 602 |
+
metadata = {
|
| 603 |
+
"source": glb_path,
|
| 604 |
+
"obj": os.path.basename(obj_path),
|
| 605 |
+
"mtl": os.path.basename(mtl_path),
|
| 606 |
+
"material": material_name,
|
| 607 |
+
"geometry": geom_name,
|
| 608 |
+
"alignment": "recenter_to_bbox_origin",
|
| 609 |
+
"bakedTransform": np.asarray(baked_xform).tolist(),
|
| 610 |
+
"sourceSceneBounds": np.asarray(scene.bounds).tolist(),
|
| 611 |
+
"exportedObjBounds": np.asarray(mesh.bounds).tolist(),
|
| 612 |
+
"baseColorFactor": base_factor,
|
| 613 |
+
"metallicFactor": metallic_factor,
|
| 614 |
+
"roughnessFactor": roughness_factor,
|
| 615 |
+
"textureMaxEdge": texture_size,
|
| 616 |
+
"textures": {k: (files[k] if saved[k] else None) for k in files},
|
| 617 |
+
}
|
| 618 |
+
with open(json_path, "w", encoding="utf-8") as fh:
|
| 619 |
+
fh.write(json.dumps(metadata, indent=2) + "\n")
|
| 620 |
+
|
| 621 |
+
return obj_path
|
| 622 |
+
|
| 623 |
+
|
| 624 |
+
def _ship_scaled_pbr_artefacts(
|
| 625 |
+
aligned_glb: str,
|
| 626 |
+
urdf_path: str,
|
| 627 |
+
output_root: str,
|
| 628 |
+
final_mesh_dir: str,
|
| 629 |
+
asset_name: str,
|
| 630 |
+
) -> None:
|
| 631 |
+
"""Write scaled OBJ companions + GLB with full PBR into ``final_mesh_dir``.
|
| 632 |
+
|
| 633 |
+
URDFGen's trimesh roundtrip drops Hunyuan's metallic/roughness/normal
|
| 634 |
+
maps; we restore PBR fidelity by:
|
| 635 |
+
|
| 636 |
+
1. Inferring the scale factor URDFGen applied by comparing the
|
| 637 |
+
scaled OBJ's extent with the aligned source GLB's extent (the
|
| 638 |
+
URDF ``<scale>`` element stores ``real_height`` instead, which is
|
| 639 |
+
a different quantity).
|
| 640 |
+
2. Loading the aligned full-PBR source GLB, scaling it, and writing
|
| 641 |
+
it next to URDFGen's OBJ so both share the same scale.
|
| 642 |
+
3. Copying the 4 PBR PNGs from ``output_root`` into the mesh dir.
|
| 643 |
+
4. Patching URDFGen's ``material.mtl`` so Phong rendering has a
|
| 644 |
+
visible specular highlight and PBR-aware OBJ importers pick up
|
| 645 |
+
``map_Pm`` / ``map_Pr`` / ``norm`` / ``bump`` references.
|
| 646 |
+
"""
|
| 647 |
+
# Derive the actual scale factor from URDFGen's scaled OBJ rather than
|
| 648 |
+
# the URDF ``<scale>`` element (which stores real_height — a midpoint
|
| 649 |
+
# value distinct from the geometric scaling factor URDFGen applied).
|
| 650 |
+
urdfgen_obj = trimesh.load(
|
| 651 |
+
os.path.join(final_mesh_dir, f"{asset_name}.obj"),
|
| 652 |
+
force="mesh",
|
| 653 |
+
process=False,
|
| 654 |
+
)
|
| 655 |
+
target_max = float(urdfgen_obj.extents.max())
|
| 656 |
+
|
| 657 |
+
scene = trimesh.load(aligned_glb, force="scene", process=False)
|
| 658 |
+
mesh, _, _ = _bake_scene_transform(scene)
|
| 659 |
+
V = np.asarray(mesh.vertices, dtype=np.float32)
|
| 660 |
+
src_max = float((V.max(axis=0) - V.min(axis=0)).max())
|
| 661 |
+
scale = target_max / src_max if src_max > 1e-9 else 1.0
|
| 662 |
+
mesh.vertices = V * scale
|
| 663 |
+
mesh.export(os.path.join(final_mesh_dir, f"{asset_name}.glb"))
|
| 664 |
+
|
| 665 |
+
pbr_pngs = {
|
| 666 |
+
"metallic": f"{asset_name}_metallic.png",
|
| 667 |
+
"roughness": f"{asset_name}_roughness.png",
|
| 668 |
+
"normal": f"{asset_name}_normal.png",
|
| 669 |
+
}
|
| 670 |
+
base_color_png = f"{asset_name}_baseColor.png"
|
| 671 |
+
pbr_json = f"{asset_name}_pbr_material.json"
|
| 672 |
+
for fname in (
|
| 673 |
+
base_color_png,
|
| 674 |
+
pbr_pngs["metallic"],
|
| 675 |
+
pbr_pngs["roughness"],
|
| 676 |
+
pbr_pngs["normal"],
|
| 677 |
+
pbr_json,
|
| 678 |
+
):
|
| 679 |
+
src = os.path.join(output_root, fname)
|
| 680 |
+
if os.path.exists(src):
|
| 681 |
+
copy(src, os.path.join(final_mesh_dir, fname))
|
| 682 |
+
|
| 683 |
+
mtl_path = os.path.join(final_mesh_dir, "material.mtl")
|
| 684 |
+
if os.path.exists(mtl_path):
|
| 685 |
+
with open(mtl_path) as fh:
|
| 686 |
+
mtl_text = fh.read()
|
| 687 |
+
# trimesh's OBJ exporter writes ``Ks 0 0 0`` + ``Ns 1`` which makes
|
| 688 |
+
# Blender's OBJ Phong path render the surface as flat matte. Bump
|
| 689 |
+
# specular and shininess so the OBJ has visible highlights matching
|
| 690 |
+
# the PBR GLB, then append the PBR texture map references that
|
| 691 |
+
# PBR-aware OBJ importers (Blender 3.6+, others) will pick up.
|
| 692 |
+
mtl_text = re.sub(
|
| 693 |
+
r"^Ks\s.+$", "Ks 0.5 0.5 0.5", mtl_text, flags=re.MULTILINE
|
| 694 |
+
)
|
| 695 |
+
mtl_text = re.sub(r"^Ns\s.+$", "Ns 250", mtl_text, flags=re.MULTILINE)
|
| 696 |
+
if not re.search(r"^illum\s", mtl_text, re.MULTILINE):
|
| 697 |
+
mtl_text = mtl_text.rstrip() + "\nillum 2\n"
|
| 698 |
+
extras = []
|
| 699 |
+
if os.path.exists(os.path.join(final_mesh_dir, pbr_pngs["metallic"])):
|
| 700 |
+
extras.append(f"map_Pm {pbr_pngs['metallic']}")
|
| 701 |
+
if os.path.exists(os.path.join(final_mesh_dir, pbr_pngs["roughness"])):
|
| 702 |
+
extras.append(f"map_Pr {pbr_pngs['roughness']}")
|
| 703 |
+
if os.path.exists(os.path.join(final_mesh_dir, pbr_pngs["normal"])):
|
| 704 |
+
extras.append(f"norm {pbr_pngs['normal']}")
|
| 705 |
+
extras.append(f"bump {pbr_pngs['normal']}")
|
| 706 |
+
if extras and not any(
|
| 707 |
+
line in mtl_text for line in ("map_Pm", "map_Pr", "norm ")
|
| 708 |
+
):
|
| 709 |
+
mtl_text = mtl_text.rstrip() + "\n" + "\n".join(extras) + "\n"
|
| 710 |
+
with open(mtl_path, "w") as fh:
|
| 711 |
+
fh.write(mtl_text)
|
| 712 |
+
|
| 713 |
+
|
| 714 |
+
def _build_asset_attrs(args, idx: int) -> dict:
|
| 715 |
+
"""Build the URDF asset_attrs dict from CLI args."""
|
| 716 |
+
attrs = {"version": args.version or VERSION}
|
| 717 |
+
if args.height_range:
|
| 718 |
+
lo, hi = map(float, args.height_range.split("-"))
|
| 719 |
+
attrs["min_height"], attrs["max_height"] = lo, hi
|
| 720 |
+
if args.mass_range:
|
| 721 |
+
lo, hi = map(float, args.mass_range.split("-"))
|
| 722 |
+
attrs["min_mass"], attrs["max_mass"] = lo, hi
|
| 723 |
+
if isinstance(args.asset_type, list) and args.asset_type[idx]:
|
| 724 |
+
attrs["category"] = args.asset_type[idx]
|
| 725 |
+
return attrs
|
| 726 |
+
|
| 727 |
+
|
| 728 |
+
def _render_color_video(
|
| 729 |
+
obj_path: str, work_dir: str, filename: str
|
| 730 |
+
) -> Optional[str]:
|
| 731 |
+
"""Render a turntable color mp4 via the shared kaolin renderer.
|
| 732 |
+
|
| 733 |
+
Returns the produced mp4 path, or ``None`` on failure (caller logs).
|
| 734 |
+
"""
|
| 735 |
+
try:
|
| 736 |
+
# differentiable_render hardcodes mp4 fps=15; 90 frames -> 6s,
|
| 737 |
+
# matching SAM3D/TRELLIS gs_mesh.mp4 duration.
|
| 738 |
+
render_pbr_video(
|
| 739 |
+
mesh_path=obj_path,
|
| 740 |
+
output_root=work_dir,
|
| 741 |
+
uuid=[filename],
|
| 742 |
+
num_images=90,
|
| 743 |
+
elevation=[20.0],
|
| 744 |
+
distance=5.0,
|
| 745 |
+
fov=30.0,
|
| 746 |
+
with_mtl=True,
|
| 747 |
+
gen_color_mp4=True,
|
| 748 |
+
no_index_file=True,
|
| 749 |
+
)
|
| 750 |
+
mp4 = os.path.join(work_dir, filename, "color.mp4")
|
| 751 |
+
return mp4 if os.path.exists(mp4) else None
|
| 752 |
+
except Exception as exc: # pragma: no cover - rendering is optional
|
| 753 |
+
logger.warning(f"HUNYUAN3D video render failed: {exc}")
|
| 754 |
+
return None
|
| 755 |
+
|
| 756 |
+
|
| 757 |
+
def _process_glb(
|
| 758 |
+
args,
|
| 759 |
+
idx: int,
|
| 760 |
+
output_root: str,
|
| 761 |
+
filename: str,
|
| 762 |
+
cfg: HunyuanConfig,
|
| 763 |
+
checkers: list,
|
| 764 |
+
log_label: str,
|
| 765 |
+
seg_input_pair: Optional[Tuple[str, str]] = None,
|
| 766 |
+
pre_align_rotation: Optional[np.ndarray] = None,
|
| 767 |
+
) -> str:
|
| 768 |
+
"""GLB-to-result post-processing shared by image and text paths.
|
| 769 |
+
|
| 770 |
+
Expects an aligned full-PBR GLB at ``{output_root}/{filename}.glb``.
|
| 771 |
+
Runs ``export_glb_to_obj`` → video render → URDFGen → PBR fidelity
|
| 772 |
+
fixup → single-arg quality checks (skipped when ``checkers`` is empty)
|
| 773 |
+
→ ``result/`` organization. ``seg_input_pair`` lets the image path
|
| 774 |
+
feed raw/cond images to ``ImageSegChecker``; text path passes ``None``.
|
| 775 |
+
``pre_align_rotation`` (3x3) is folded into the single mesh transform
|
| 776 |
+
inside ``export_glb_to_obj``, avoiding a separate load/export pass.
|
| 777 |
+
Returns the result dir path.
|
| 778 |
+
"""
|
| 779 |
+
export_glb_to_obj(
|
| 780 |
+
glb_path=os.path.join(output_root, f"{filename}.glb"),
|
| 781 |
+
output_dir=output_root,
|
| 782 |
+
asset_name=filename,
|
| 783 |
+
texture_size=cfg.texture_size,
|
| 784 |
+
pre_align_rotation=pre_align_rotation,
|
| 785 |
+
)
|
| 786 |
+
mesh_obj_path = os.path.join(output_root, f"{filename}.obj")
|
| 787 |
+
|
| 788 |
+
video_path = _render_color_video(
|
| 789 |
+
mesh_obj_path, os.path.join(output_root, "_video"), filename
|
| 790 |
+
)
|
| 791 |
+
|
| 792 |
+
urdf_convertor = URDFGenerator(
|
| 793 |
+
GPT_CLIENT,
|
| 794 |
+
render_view_num=4,
|
| 795 |
+
decompose_convex=not args.disable_decompose_convex,
|
| 796 |
+
)
|
| 797 |
+
urdf_root = f"{output_root}/URDF_{filename}"
|
| 798 |
+
urdf_path = urdf_convertor(
|
| 799 |
+
mesh_path=mesh_obj_path,
|
| 800 |
+
output_root=urdf_root,
|
| 801 |
+
**_build_asset_attrs(args, idx),
|
| 802 |
+
)
|
| 803 |
+
|
| 804 |
+
# Final mesh dir: keep URDFGen's scaled OBJ + collision, restore full
|
| 805 |
+
# PBR fidelity that URDFGen's simple trimesh roundtrip strips (rescaled
|
| 806 |
+
# source GLB + PBR map refs appended to material.mtl).
|
| 807 |
+
final_mesh_dir = f"{urdf_root}/{urdf_convertor.output_mesh_dir}"
|
| 808 |
+
_ship_scaled_pbr_artefacts(
|
| 809 |
+
aligned_glb=os.path.join(output_root, f"{filename}.glb"),
|
| 810 |
+
urdf_path=urdf_path,
|
| 811 |
+
output_root=output_root,
|
| 812 |
+
final_mesh_dir=final_mesh_dir,
|
| 813 |
+
asset_name=filename,
|
| 814 |
+
)
|
| 815 |
+
|
| 816 |
+
# Quality checks: only the single-arg (BaseChecker.validate) ones go
|
| 817 |
+
# here. Two-arg checkers like TextGenAlignChecker run in the caller.
|
| 818 |
+
if checkers:
|
| 819 |
+
render_image_paths = glob(
|
| 820 |
+
f"{urdf_root}/{urdf_convertor.output_render_dir}/image_color/*.png"
|
| 821 |
+
)
|
| 822 |
+
images_list = []
|
| 823 |
+
for ch in checkers:
|
| 824 |
+
if isinstance(ch, ImageSegChecker) and seg_input_pair is not None:
|
| 825 |
+
images_list.append(list(seg_input_pair))
|
| 826 |
+
else:
|
| 827 |
+
images_list.append(combine_images_to_grid(render_image_paths))
|
| 828 |
+
qa_results = BaseChecker.validate(checkers, images_list)
|
| 829 |
+
urdf_convertor.add_quality_tag(urdf_path, qa_results)
|
| 830 |
+
|
| 831 |
+
# Organize result/ (no gs.ply; video.mp4 included when render OK).
|
| 832 |
+
result_dir = f"{output_root}/result"
|
| 833 |
+
if os.path.exists(result_dir):
|
| 834 |
+
rmtree(result_dir, ignore_errors=True)
|
| 835 |
+
os.makedirs(result_dir, exist_ok=True)
|
| 836 |
+
copy(urdf_path, f"{result_dir}/{os.path.basename(urdf_path)}")
|
| 837 |
+
copytree(
|
| 838 |
+
f"{urdf_root}/{urdf_convertor.output_mesh_dir}",
|
| 839 |
+
f"{result_dir}/{urdf_convertor.output_mesh_dir}",
|
| 840 |
+
)
|
| 841 |
+
if video_path and os.path.exists(video_path):
|
| 842 |
+
copy(video_path, f"{result_dir}/video.mp4")
|
| 843 |
+
|
| 844 |
+
if not args.keep_intermediate:
|
| 845 |
+
delete_dir(output_root, keep_subs=["result"])
|
| 846 |
+
|
| 847 |
+
logger.info(f"Saved results for {log_label} in {result_dir}")
|
| 848 |
+
return result_dir
|
| 849 |
+
|
| 850 |
+
|
| 851 |
+
# Rotation that aligns a Hunyuan3D **text**-to-3D GLB with the **image**-to-3D
|
| 852 |
+
# frame. -90° around the file-coord up axis (Y), i.e. x' = z, z' = -x.
|
| 853 |
+
# Applied as a single multiplication inside ``export_glb_to_obj`` so the
|
| 854 |
+
# text path does not need a separate GLB load/save pass.
|
| 855 |
+
TEXT_TO_IMAGE_FRAME_ROTATION = np.array(
|
| 856 |
+
[[0.0, 0.0, -1.0], [0.0, 1.0, 0.0], [1.0, 0.0, 0.0]],
|
| 857 |
+
dtype=np.float32,
|
| 858 |
+
)
|
| 859 |
+
|
| 860 |
+
|
| 861 |
+
def _acquire_or_reuse_glb(
|
| 862 |
+
output_root: str,
|
| 863 |
+
filename: str,
|
| 864 |
+
cfg: HunyuanConfig,
|
| 865 |
+
hunyuan_credentials: Optional[Tuple[str, str]],
|
| 866 |
+
*,
|
| 867 |
+
image_path: Optional[str] = None,
|
| 868 |
+
prompt: Optional[str] = None,
|
| 869 |
+
) -> None:
|
| 870 |
+
"""Ensure ``{output_root}/{filename}.glb`` exists.
|
| 871 |
+
|
| 872 |
+
Reuses an existing GLB at that path (dev fixture short-circuit) or
|
| 873 |
+
calls :func:`acquire_pro_glb` with ``image_path`` or ``prompt``.
|
| 874 |
+
"""
|
| 875 |
+
glb_path = os.path.join(output_root, f"{filename}.glb")
|
| 876 |
+
if os.path.exists(glb_path):
|
| 877 |
+
logger.info(
|
| 878 |
+
"HUNYUAN3D reusing existing GLB at %s; skipping Tencent API call.",
|
| 879 |
+
glb_path,
|
| 880 |
+
)
|
| 881 |
+
return
|
| 882 |
+
creds = hunyuan_credentials or load_credentials()
|
| 883 |
+
acquire_pro_glb(
|
| 884 |
+
image_path=image_path,
|
| 885 |
+
prompt=prompt,
|
| 886 |
+
output_dir=output_root,
|
| 887 |
+
asset_name=filename,
|
| 888 |
+
credentials=creds,
|
| 889 |
+
cfg=cfg,
|
| 890 |
+
)
|
| 891 |
+
|
| 892 |
+
|
| 893 |
+
def process_image(
|
| 894 |
+
args,
|
| 895 |
+
idx: int,
|
| 896 |
+
image_path: str,
|
| 897 |
+
output_root: str,
|
| 898 |
+
filename: str,
|
| 899 |
+
hunyuan_config: Optional[HunyuanConfig],
|
| 900 |
+
hunyuan_credentials: Optional[Tuple[str, str]],
|
| 901 |
+
checkers: list,
|
| 902 |
+
) -> None:
|
| 903 |
+
"""HUNYUAN3D image-to-3D entry: image → GLB → export → URDF → result/."""
|
| 904 |
+
cfg = hunyuan_config or HunyuanConfig()
|
| 905 |
+
_acquire_or_reuse_glb(
|
| 906 |
+
output_root, filename, cfg, hunyuan_credentials, image_path=image_path
|
| 907 |
+
)
|
| 908 |
+
_process_glb(
|
| 909 |
+
args=args,
|
| 910 |
+
idx=idx,
|
| 911 |
+
output_root=output_root,
|
| 912 |
+
filename=filename,
|
| 913 |
+
cfg=cfg,
|
| 914 |
+
checkers=checkers,
|
| 915 |
+
log_label=image_path,
|
| 916 |
+
seg_input_pair=(
|
| 917 |
+
f"{output_root}/{filename}_raw.png",
|
| 918 |
+
f"{output_root}/{filename}_cond.png",
|
| 919 |
+
),
|
| 920 |
+
)
|
| 921 |
+
|
| 922 |
+
|
| 923 |
+
def process_prompt(
|
| 924 |
+
args,
|
| 925 |
+
idx: int,
|
| 926 |
+
prompt: str,
|
| 927 |
+
output_root: str,
|
| 928 |
+
filename: str,
|
| 929 |
+
hunyuan_config: Optional[HunyuanConfig],
|
| 930 |
+
hunyuan_credentials: Optional[Tuple[str, str]],
|
| 931 |
+
checkers: list,
|
| 932 |
+
) -> None:
|
| 933 |
+
"""HUNYUAN3D text-to-3D entry: prompt → GLB → export → URDF → result/.
|
| 934 |
+
|
| 935 |
+
Text path skips ``text-to-image`` entirely; ``checkers`` should only
|
| 936 |
+
contain single-arg (``BaseChecker.validate``-compatible) checkers.
|
| 937 |
+
Two-arg checkers like ``TextGenAlignChecker`` should be invoked by
|
| 938 |
+
the caller after this returns.
|
| 939 |
+
"""
|
| 940 |
+
cfg = hunyuan_config or HunyuanConfig()
|
| 941 |
+
_acquire_or_reuse_glb(
|
| 942 |
+
output_root, filename, cfg, hunyuan_credentials, prompt=prompt
|
| 943 |
+
)
|
| 944 |
+
# Text endpoint sits 90° offset around the up axis vs the image
|
| 945 |
+
# endpoint; fold the alignment rotation into export_glb_to_obj's
|
| 946 |
+
# single mesh-transform pass to avoid a separate GLB roundtrip.
|
| 947 |
+
_process_glb(
|
| 948 |
+
args=args,
|
| 949 |
+
idx=idx,
|
| 950 |
+
output_root=output_root,
|
| 951 |
+
filename=filename,
|
| 952 |
+
cfg=cfg,
|
| 953 |
+
checkers=checkers,
|
| 954 |
+
log_label=f"prompt={prompt!r}",
|
| 955 |
+
seg_input_pair=None,
|
| 956 |
+
pre_align_rotation=TEXT_TO_IMAGE_FRAME_ROTATION,
|
| 957 |
+
)
|
embodied_gen/models/image_comm_model.py
CHANGED
|
@@ -31,6 +31,9 @@ from diffusers.quantizers import PipelineQuantizationConfig
|
|
| 31 |
from huggingface_hub import snapshot_download
|
| 32 |
from PIL import Image
|
| 33 |
from transformers import AutoModelForCausalLM, SiglipProcessor
|
|
|
|
|
|
|
|
|
|
| 34 |
|
| 35 |
__all__ = [
|
| 36 |
"build_hf_image_pipeline",
|
|
@@ -99,6 +102,7 @@ class SD35Loader(BasePipelineLoader):
|
|
| 99 |
)
|
| 100 |
pipe = pipe.to(self.device)
|
| 101 |
pipe.enable_model_cpu_offload()
|
|
|
|
| 102 |
pipe.enable_xformers_memory_efficient_attention()
|
| 103 |
pipe.enable_attention_slicing()
|
| 104 |
return pipe
|
|
@@ -230,6 +234,7 @@ class KolorsLoader(BasePipelineLoader):
|
|
| 230 |
variant="fp16",
|
| 231 |
).to(self.device)
|
| 232 |
pipe.enable_model_cpu_offload()
|
|
|
|
| 233 |
pipe.enable_xformers_memory_efficient_attention()
|
| 234 |
pipe.scheduler = DPMSolverMultistepScheduler.from_config(
|
| 235 |
pipe.scheduler.config, use_karras_sigmas=True
|
|
@@ -268,6 +273,7 @@ class FluxLoader(BasePipelineLoader):
|
|
| 268 |
"black-forest-labs/FLUX.1-schnell", torch_dtype=torch.bfloat16
|
| 269 |
)
|
| 270 |
pipe.enable_model_cpu_offload()
|
|
|
|
| 271 |
pipe.enable_xformers_memory_efficient_attention()
|
| 272 |
pipe.enable_attention_slicing()
|
| 273 |
return pipe.to(self.device)
|
|
|
|
| 31 |
from huggingface_hub import snapshot_download
|
| 32 |
from PIL import Image
|
| 33 |
from transformers import AutoModelForCausalLM, SiglipProcessor
|
| 34 |
+
from embodied_gen.utils.monkey_patch.xformers import (
|
| 35 |
+
disable_xformers_flash3_on_blackwell,
|
| 36 |
+
)
|
| 37 |
|
| 38 |
__all__ = [
|
| 39 |
"build_hf_image_pipeline",
|
|
|
|
| 102 |
)
|
| 103 |
pipe = pipe.to(self.device)
|
| 104 |
pipe.enable_model_cpu_offload()
|
| 105 |
+
disable_xformers_flash3_on_blackwell()
|
| 106 |
pipe.enable_xformers_memory_efficient_attention()
|
| 107 |
pipe.enable_attention_slicing()
|
| 108 |
return pipe
|
|
|
|
| 234 |
variant="fp16",
|
| 235 |
).to(self.device)
|
| 236 |
pipe.enable_model_cpu_offload()
|
| 237 |
+
disable_xformers_flash3_on_blackwell()
|
| 238 |
pipe.enable_xformers_memory_efficient_attention()
|
| 239 |
pipe.scheduler = DPMSolverMultistepScheduler.from_config(
|
| 240 |
pipe.scheduler.config, use_karras_sigmas=True
|
|
|
|
| 273 |
"black-forest-labs/FLUX.1-schnell", torch_dtype=torch.bfloat16
|
| 274 |
)
|
| 275 |
pipe.enable_model_cpu_offload()
|
| 276 |
+
disable_xformers_flash3_on_blackwell()
|
| 277 |
pipe.enable_xformers_memory_efficient_attention()
|
| 278 |
pipe.enable_attention_slicing()
|
| 279 |
return pipe.to(self.device)
|
embodied_gen/models/sam3d.py
CHANGED
|
@@ -53,13 +53,6 @@ class Sam3dInference:
|
|
| 53 |
compile (bool): Whether to compile the model for faster inference.
|
| 54 |
device (str): Device to run the model on (e.g., "cuda" or "cpu").
|
| 55 |
|
| 56 |
-
Methods:
|
| 57 |
-
merge_mask_to_rgba(image, mask):
|
| 58 |
-
Merges a binary mask into the alpha channel of an RGB image.
|
| 59 |
-
|
| 60 |
-
run(image, mask=None, seed=None, pointmap=None, use_stage1_distillation=False,
|
| 61 |
-
use_stage2_distillation=False, stage1_inference_steps=25, stage2_inference_steps=25):
|
| 62 |
-
Runs the inference pipeline and returns the output dictionary.
|
| 63 |
"""
|
| 64 |
|
| 65 |
def __init__(
|
|
@@ -145,7 +138,7 @@ if __name__ == "__main__":
|
|
| 145 |
|
| 146 |
start = time()
|
| 147 |
output = pipeline.run(clean_image, seed=42)
|
| 148 |
-
print(f"Running cost: {round(time()-start, 1)}")
|
| 149 |
|
| 150 |
if torch.cuda.is_available():
|
| 151 |
max_memory = torch.cuda.max_memory_allocated() / (1024**3)
|
|
|
|
| 53 |
compile (bool): Whether to compile the model for faster inference.
|
| 54 |
device (str): Device to run the model on (e.g., "cuda" or "cpu").
|
| 55 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
"""
|
| 57 |
|
| 58 |
def __init__(
|
|
|
|
| 138 |
|
| 139 |
start = time()
|
| 140 |
output = pipeline.run(clean_image, seed=42)
|
| 141 |
+
print(f"Running cost: {round(time() - start, 1)}")
|
| 142 |
|
| 143 |
if torch.cuda.is_available():
|
| 144 |
max_memory = torch.cuda.max_memory_allocated() / (1024**3)
|
embodied_gen/models/segment_model.py
CHANGED
|
@@ -236,7 +236,7 @@ class SAMPredictor(object):
|
|
| 236 |
)
|
| 237 |
|
| 238 |
labels = (
|
| 239 |
-
torch.Tensor([int(
|
| 240 |
.to(self.predictor.device)
|
| 241 |
.unsqueeze(1)
|
| 242 |
)
|
|
@@ -469,13 +469,11 @@ def get_segmented_image_by_agent(
|
|
| 469 |
elif _is_valid_seg(image, seg_image_inv):
|
| 470 |
final_image = seg_image_inv
|
| 471 |
elif _is_valid_seg(image, seg_image_rbg):
|
| 472 |
-
logger.warning(
|
| 473 |
final_image = seg_image_rbg
|
| 474 |
else:
|
| 475 |
if mode == "strict":
|
| 476 |
-
raise RuntimeError(
|
| 477 |
-
f"Failed to segment by `SAM` or `rembg`, abort."
|
| 478 |
-
)
|
| 479 |
logger.warning("Failed to segment by SAM or rembg, use raw image.")
|
| 480 |
final_image = image.convert("RGBA")
|
| 481 |
|
|
|
|
| 236 |
)
|
| 237 |
|
| 238 |
labels = (
|
| 239 |
+
torch.Tensor([int(label) for _, label in selected_points])
|
| 240 |
.to(self.predictor.device)
|
| 241 |
.unsqueeze(1)
|
| 242 |
)
|
|
|
|
| 469 |
elif _is_valid_seg(image, seg_image_inv):
|
| 470 |
final_image = seg_image_inv
|
| 471 |
elif _is_valid_seg(image, seg_image_rbg):
|
| 472 |
+
logger.warning("Failed to segment by `SAM`, retry with `rembg`.")
|
| 473 |
final_image = seg_image_rbg
|
| 474 |
else:
|
| 475 |
if mode == "strict":
|
| 476 |
+
raise RuntimeError("Failed to segment by `SAM` or `rembg`, abort.")
|
|
|
|
|
|
|
| 477 |
logger.warning("Failed to segment by SAM or rembg, use raw image.")
|
| 478 |
final_image = image.convert("RGBA")
|
| 479 |
|
embodied_gen/models/text_model.py
CHANGED
|
@@ -40,6 +40,9 @@ from kolors.pipelines.pipeline_stable_diffusion_xl_chatglm_256_ipadapter import
|
|
| 40 |
)
|
| 41 |
from PIL import Image
|
| 42 |
from transformers import CLIPImageProcessor, CLIPVisionModelWithProjection
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
logging.basicConfig(level=logging.INFO)
|
| 45 |
logger = logging.getLogger(__name__)
|
|
@@ -110,6 +113,7 @@ def build_text2img_ip_pipeline(
|
|
| 110 |
ckpt_dir: str,
|
| 111 |
ref_scale: float,
|
| 112 |
device: str = "cuda",
|
|
|
|
| 113 |
) -> StableDiffusionXLPipelineIP:
|
| 114 |
"""Builds a Stable Diffusion XL pipeline with IP-Adapter for text-to-image generation.
|
| 115 |
|
|
@@ -117,6 +121,7 @@ def build_text2img_ip_pipeline(
|
|
| 117 |
ckpt_dir (str): Directory containing model checkpoints.
|
| 118 |
ref_scale (float): Reference scale for IP-Adapter.
|
| 119 |
device (str, optional): Device for inference.
|
|
|
|
| 120 |
|
| 121 |
Returns:
|
| 122 |
StableDiffusionXLPipelineIP: Configured pipeline.
|
|
@@ -173,11 +178,12 @@ def build_text2img_ip_pipeline(
|
|
| 173 |
)
|
| 174 |
pipe.set_ip_adapter_scale([ref_scale])
|
| 175 |
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
|
|
|
| 181 |
|
| 182 |
return pipe
|
| 183 |
|
|
@@ -186,6 +192,7 @@ def build_text2img_pipeline(
|
|
| 186 |
ckpt_dir: str,
|
| 187 |
device: str = "cuda",
|
| 188 |
download_ip_adapter: bool = False,
|
|
|
|
| 189 |
) -> StableDiffusionXLPipeline:
|
| 190 |
"""Builds a Stable Diffusion XL pipeline for text-to-image generation.
|
| 191 |
|
|
@@ -193,6 +200,7 @@ def build_text2img_pipeline(
|
|
| 193 |
ckpt_dir (str): Directory containing model checkpoints.
|
| 194 |
device (str, optional): Device for inference.
|
| 195 |
download_ip_adapter: Whether to predownload IP-Adapter weights.
|
|
|
|
| 196 |
|
| 197 |
Returns:
|
| 198 |
StableDiffusionXLPipeline: Configured pipeline.
|
|
@@ -233,9 +241,13 @@ def build_text2img_pipeline(
|
|
| 233 |
scheduler=scheduler,
|
| 234 |
force_zeros_for_empty_prompt=False,
|
| 235 |
)
|
| 236 |
-
|
| 237 |
-
|
|
|
|
|
|
|
|
|
|
| 238 |
# pipe.enable_xformers_memory_efficient_attention()
|
|
|
|
| 239 |
|
| 240 |
return pipe
|
| 241 |
|
|
|
|
| 40 |
)
|
| 41 |
from PIL import Image
|
| 42 |
from transformers import CLIPImageProcessor, CLIPVisionModelWithProjection
|
| 43 |
+
from embodied_gen.utils.monkey_patch.xformers import (
|
| 44 |
+
disable_xformers_flash3_on_blackwell,
|
| 45 |
+
)
|
| 46 |
|
| 47 |
logging.basicConfig(level=logging.INFO)
|
| 48 |
logger = logging.getLogger(__name__)
|
|
|
|
| 113 |
ckpt_dir: str,
|
| 114 |
ref_scale: float,
|
| 115 |
device: str = "cuda",
|
| 116 |
+
enable_cpu_offload: bool = True,
|
| 117 |
) -> StableDiffusionXLPipelineIP:
|
| 118 |
"""Builds a Stable Diffusion XL pipeline with IP-Adapter for text-to-image generation.
|
| 119 |
|
|
|
|
| 121 |
ckpt_dir (str): Directory containing model checkpoints.
|
| 122 |
ref_scale (float): Reference scale for IP-Adapter.
|
| 123 |
device (str, optional): Device for inference.
|
| 124 |
+
enable_cpu_offload: Whether to offload inactive modules to CPU.
|
| 125 |
|
| 126 |
Returns:
|
| 127 |
StableDiffusionXLPipelineIP: Configured pipeline.
|
|
|
|
| 178 |
)
|
| 179 |
pipe.set_ip_adapter_scale([ref_scale])
|
| 180 |
|
| 181 |
+
if enable_cpu_offload:
|
| 182 |
+
# pipe.enable_model_cpu_offload()
|
| 183 |
+
else:
|
| 184 |
+
pipe = pipe.to(device)
|
| 185 |
+
pipe.image_encoder = pipe.image_encoder.to(device)
|
| 186 |
+
pipe.enable_vae_slicing()
|
| 187 |
|
| 188 |
return pipe
|
| 189 |
|
|
|
|
| 192 |
ckpt_dir: str,
|
| 193 |
device: str = "cuda",
|
| 194 |
download_ip_adapter: bool = False,
|
| 195 |
+
enable_cpu_offload: bool = True,
|
| 196 |
) -> StableDiffusionXLPipeline:
|
| 197 |
"""Builds a Stable Diffusion XL pipeline for text-to-image generation.
|
| 198 |
|
|
|
|
| 200 |
ckpt_dir (str): Directory containing model checkpoints.
|
| 201 |
device (str, optional): Device for inference.
|
| 202 |
download_ip_adapter: Whether to predownload IP-Adapter weights.
|
| 203 |
+
enable_cpu_offload: Whether to offload inactive modules to CPU.
|
| 204 |
|
| 205 |
Returns:
|
| 206 |
StableDiffusionXLPipeline: Configured pipeline.
|
|
|
|
| 241 |
scheduler=scheduler,
|
| 242 |
force_zeros_for_empty_prompt=False,
|
| 243 |
)
|
| 244 |
+
if enable_cpu_offload:
|
| 245 |
+
# pipe.enable_model_cpu_offload()
|
| 246 |
+
else:
|
| 247 |
+
pipe = pipe.to(device)
|
| 248 |
+
disable_xformers_flash3_on_blackwell()
|
| 249 |
# pipe.enable_xformers_memory_efficient_attention()
|
| 250 |
+
pipe.enable_vae_slicing()
|
| 251 |
|
| 252 |
return pipe
|
| 253 |
|
embodied_gen/models/texture_model.py
CHANGED
|
@@ -93,7 +93,7 @@ def build_texture_gen_pipe(
|
|
| 93 |
"""
|
| 94 |
|
| 95 |
download_kolors_weights(f"{base_ckpt_dir}/Kolors")
|
| 96 |
-
logger.info(
|
| 97 |
tokenizer = ChatGLMTokenizer.from_pretrained(
|
| 98 |
f"{base_ckpt_dir}/Kolors/text_encoder"
|
| 99 |
)
|
|
@@ -101,11 +101,17 @@ def build_texture_gen_pipe(
|
|
| 101 |
f"{base_ckpt_dir}/Kolors/text_encoder", torch_dtype=torch.float16
|
| 102 |
).half()
|
| 103 |
vae = AutoencoderKL.from_pretrained(
|
| 104 |
-
f"{base_ckpt_dir}/Kolors/vae",
|
| 105 |
-
|
|
|
|
|
|
|
|
|
|
| 106 |
unet = UNet2DConditionModel.from_pretrained(
|
| 107 |
-
f"{base_ckpt_dir}/Kolors/unet",
|
| 108 |
-
|
|
|
|
|
|
|
|
|
|
| 109 |
scheduler = EulerDiscreteScheduler.from_pretrained(
|
| 110 |
f"{base_ckpt_dir}/Kolors/scheduler"
|
| 111 |
)
|
|
|
|
| 93 |
"""
|
| 94 |
|
| 95 |
download_kolors_weights(f"{base_ckpt_dir}/Kolors")
|
| 96 |
+
logger.info("Load Kolors weights...")
|
| 97 |
tokenizer = ChatGLMTokenizer.from_pretrained(
|
| 98 |
f"{base_ckpt_dir}/Kolors/text_encoder"
|
| 99 |
)
|
|
|
|
| 101 |
f"{base_ckpt_dir}/Kolors/text_encoder", torch_dtype=torch.float16
|
| 102 |
).half()
|
| 103 |
vae = AutoencoderKL.from_pretrained(
|
| 104 |
+
f"{base_ckpt_dir}/Kolors/vae",
|
| 105 |
+
torch_dtype=torch.float16,
|
| 106 |
+
variant="fp16",
|
| 107 |
+
use_safetensors=True,
|
| 108 |
+
)
|
| 109 |
unet = UNet2DConditionModel.from_pretrained(
|
| 110 |
+
f"{base_ckpt_dir}/Kolors/unet",
|
| 111 |
+
torch_dtype=torch.float16,
|
| 112 |
+
variant="fp16",
|
| 113 |
+
use_safetensors=True,
|
| 114 |
+
)
|
| 115 |
scheduler = EulerDiscreteScheduler.from_pretrained(
|
| 116 |
f"{base_ckpt_dir}/Kolors/scheduler"
|
| 117 |
)
|
embodied_gen/scripts/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Command-line workflows for EmbodiedGen."""
|
embodied_gen/scripts/affordance_annot/affordance_annot.py
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Project EmbodiedGen
|
| 2 |
+
#
|
| 3 |
+
# Copyright (c) 2025 Horizon Robotics. All Rights Reserved.
|
| 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
|
| 14 |
+
# implied. See the License for the specific language governing
|
| 15 |
+
# permissions and limitations under the License.
|
| 16 |
+
|
| 17 |
+
import dataclasses
|
| 18 |
+
import os
|
| 19 |
+
import sys
|
| 20 |
+
import time
|
| 21 |
+
from dataclasses import dataclass, field
|
| 22 |
+
|
| 23 |
+
import tyro
|
| 24 |
+
|
| 25 |
+
if __package__ is None or __package__ == "":
|
| 26 |
+
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../../..")))
|
| 27 |
+
|
| 28 |
+
from embodied_gen.utils.general import filter_warnings
|
| 29 |
+
|
| 30 |
+
filter_warnings()
|
| 31 |
+
|
| 32 |
+
from embodied_gen.scripts.affordance_annot.eval_grasps import (
|
| 33 |
+
EvalGraspsConfig,
|
| 34 |
+
run_eval,
|
| 35 |
+
)
|
| 36 |
+
from embodied_gen.scripts.affordance_annot.gen_grasp import (
|
| 37 |
+
GraspGenerator,
|
| 38 |
+
GraspPoseConfig,
|
| 39 |
+
)
|
| 40 |
+
from embodied_gen.scripts.affordance_annot.part_seg import (
|
| 41 |
+
PartSegConfig,
|
| 42 |
+
PartSegmenter,
|
| 43 |
+
)
|
| 44 |
+
from embodied_gen.scripts.affordance_annot.partsemantics_annot import (
|
| 45 |
+
PartSemanticsAnnotator,
|
| 46 |
+
PartSemanticsAnnotConfig,
|
| 47 |
+
)
|
| 48 |
+
from embodied_gen.utils.log import logger
|
| 49 |
+
|
| 50 |
+
__all__ = [
|
| 51 |
+
"AffordanceAnnotConfig",
|
| 52 |
+
"AffordanceAnnotator",
|
| 53 |
+
"run_affordance_annot",
|
| 54 |
+
"entrypoint",
|
| 55 |
+
]
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
@dataclass
|
| 59 |
+
class AffordanceAnnotConfig:
|
| 60 |
+
urdf_paths: list[str] = field(default_factory=list)
|
| 61 |
+
output_dirs: list[str] = field(default_factory=list)
|
| 62 |
+
run_part_seg: bool = True
|
| 63 |
+
run_partsemantics_annot: bool = True
|
| 64 |
+
run_grasp: bool = True
|
| 65 |
+
run_grasp_eval: bool = True
|
| 66 |
+
part_seg: PartSegConfig = field(default_factory=PartSegConfig)
|
| 67 |
+
partsemantics: PartSemanticsAnnotConfig = field(
|
| 68 |
+
default_factory=PartSemanticsAnnotConfig
|
| 69 |
+
)
|
| 70 |
+
grasp: GraspPoseConfig = field(default_factory=GraspPoseConfig)
|
| 71 |
+
grasp_eval: EvalGraspsConfig = field(default_factory=EvalGraspsConfig)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
class AffordanceAnnotator:
|
| 75 |
+
def __init__(self, cfg: AffordanceAnnotConfig):
|
| 76 |
+
self.validate_config(cfg)
|
| 77 |
+
self.cfg = cfg
|
| 78 |
+
self.part_segmenter: PartSegmenter | None = None
|
| 79 |
+
self.partsemantics_annotator: PartSemanticsAnnotator | None = None
|
| 80 |
+
self.grasp_generator: GraspGenerator | None = None
|
| 81 |
+
self.asset_timings: list[dict[str, float | str]] = []
|
| 82 |
+
|
| 83 |
+
def validate_config(self, cfg: AffordanceAnnotConfig) -> None:
|
| 84 |
+
if not cfg.urdf_paths:
|
| 85 |
+
raise ValueError("urdf_paths must be provided.")
|
| 86 |
+
|
| 87 |
+
if len(cfg.output_dirs) == 0:
|
| 88 |
+
cfg.output_dirs = [
|
| 89 |
+
os.path.join(os.path.dirname(path), "affordance")
|
| 90 |
+
for path in cfg.urdf_paths
|
| 91 |
+
]
|
| 92 |
+
|
| 93 |
+
if len(cfg.urdf_paths) != len(cfg.output_dirs):
|
| 94 |
+
raise ValueError(
|
| 95 |
+
"urdf_paths and output_dirs must have the same length, "
|
| 96 |
+
f"got {len(cfg.urdf_paths)} and {len(cfg.output_dirs)}."
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
def sync_batch_stage_config(self, cfg):
|
| 100 |
+
updates = {"urdf_paths": list(self.cfg.urdf_paths)}
|
| 101 |
+
if "output_dirs" in {field.name for field in dataclasses.fields(cfg)}:
|
| 102 |
+
updates["output_dirs"] = list(self.cfg.output_dirs)
|
| 103 |
+
return dataclasses.replace(cfg, **updates)
|
| 104 |
+
|
| 105 |
+
def sync_single_stage_config(self, cfg, urdf_path: str, output_dir: str):
|
| 106 |
+
updates = {"urdf_paths": [urdf_path]}
|
| 107 |
+
if "output_dirs" in {field.name for field in dataclasses.fields(cfg)}:
|
| 108 |
+
updates["output_dirs"] = [output_dir]
|
| 109 |
+
return dataclasses.replace(cfg, **updates)
|
| 110 |
+
|
| 111 |
+
def process_part_seg(self, urdf_path: str, output_dir: str) -> bool:
|
| 112 |
+
if not self.cfg.run_part_seg:
|
| 113 |
+
logger.info("Skipping part segmentation stage.")
|
| 114 |
+
return True
|
| 115 |
+
|
| 116 |
+
return bool(self.get_part_segmenter().process(urdf_path, output_dir))
|
| 117 |
+
|
| 118 |
+
def process_partsemantics_annot(
|
| 119 |
+
self, urdf_path: str, output_dir: str
|
| 120 |
+
) -> bool:
|
| 121 |
+
if not self.cfg.run_partsemantics_annot:
|
| 122 |
+
logger.info("Skipping part semantics annotation stage.")
|
| 123 |
+
return True
|
| 124 |
+
|
| 125 |
+
return bool(
|
| 126 |
+
self.get_partsemantics_annotator().process(urdf_path, output_dir)
|
| 127 |
+
)
|
| 128 |
+
|
| 129 |
+
def process_grasp(self, urdf_path: str, output_dir: str) -> bool:
|
| 130 |
+
if not self.cfg.run_grasp:
|
| 131 |
+
logger.info("Skipping grasp generation stage.")
|
| 132 |
+
return True
|
| 133 |
+
|
| 134 |
+
return bool(self.get_grasp_generator().process(urdf_path))
|
| 135 |
+
|
| 136 |
+
def process_grasp_eval(self, urdf_path: str, output_dir: str) -> bool:
|
| 137 |
+
if not self.cfg.run_grasp_eval:
|
| 138 |
+
logger.info("Skipping grasp evaluation stage.")
|
| 139 |
+
return True
|
| 140 |
+
|
| 141 |
+
cfg = self.sync_single_stage_config(
|
| 142 |
+
self.cfg.grasp_eval,
|
| 143 |
+
urdf_path,
|
| 144 |
+
output_dir,
|
| 145 |
+
)
|
| 146 |
+
run_eval(cfg)
|
| 147 |
+
return True
|
| 148 |
+
|
| 149 |
+
def get_part_segmenter(self) -> PartSegmenter:
|
| 150 |
+
if self.part_segmenter is None:
|
| 151 |
+
cfg = self.sync_batch_stage_config(self.cfg.part_seg)
|
| 152 |
+
self.part_segmenter = PartSegmenter(cfg)
|
| 153 |
+
return self.part_segmenter
|
| 154 |
+
|
| 155 |
+
def get_partsemantics_annotator(self) -> PartSemanticsAnnotator:
|
| 156 |
+
if self.partsemantics_annotator is None:
|
| 157 |
+
cfg = self.sync_batch_stage_config(self.cfg.partsemantics)
|
| 158 |
+
self.partsemantics_annotator = PartSemanticsAnnotator(cfg)
|
| 159 |
+
return self.partsemantics_annotator
|
| 160 |
+
|
| 161 |
+
def get_grasp_generator(self) -> GraspGenerator:
|
| 162 |
+
if self.grasp_generator is None:
|
| 163 |
+
cfg = self.sync_batch_stage_config(self.cfg.grasp)
|
| 164 |
+
self.grasp_generator = GraspGenerator(cfg)
|
| 165 |
+
return self.grasp_generator
|
| 166 |
+
|
| 167 |
+
def run_stage(
|
| 168 |
+
self,
|
| 169 |
+
stage_name: str,
|
| 170 |
+
stage_fn,
|
| 171 |
+
urdf_path: str,
|
| 172 |
+
) -> bool:
|
| 173 |
+
try:
|
| 174 |
+
success = bool(stage_fn())
|
| 175 |
+
except Exception as exc:
|
| 176 |
+
logger.warning(
|
| 177 |
+
"{} failed for URDF {}: {}".format(
|
| 178 |
+
stage_name,
|
| 179 |
+
urdf_path,
|
| 180 |
+
exc,
|
| 181 |
+
)
|
| 182 |
+
)
|
| 183 |
+
return False
|
| 184 |
+
|
| 185 |
+
if not success:
|
| 186 |
+
logger.warning(
|
| 187 |
+
"{} returned failure for URDF {}".format(
|
| 188 |
+
stage_name,
|
| 189 |
+
urdf_path,
|
| 190 |
+
)
|
| 191 |
+
)
|
| 192 |
+
return success
|
| 193 |
+
|
| 194 |
+
def process_one_urdf(
|
| 195 |
+
self,
|
| 196 |
+
urdf_path: str,
|
| 197 |
+
output_dir: str,
|
| 198 |
+
index: int,
|
| 199 |
+
) -> dict[str, bool]:
|
| 200 |
+
start_time = time.perf_counter()
|
| 201 |
+
logger.info(
|
| 202 |
+
"Starting affordance pipeline for URDF {}/{}: {}".format(
|
| 203 |
+
index,
|
| 204 |
+
len(self.cfg.urdf_paths),
|
| 205 |
+
urdf_path,
|
| 206 |
+
)
|
| 207 |
+
)
|
| 208 |
+
stages = [
|
| 209 |
+
(
|
| 210 |
+
"part segmentation",
|
| 211 |
+
lambda: self.process_part_seg(urdf_path, output_dir),
|
| 212 |
+
),
|
| 213 |
+
(
|
| 214 |
+
"part semantics annotation",
|
| 215 |
+
lambda: self.process_partsemantics_annot(urdf_path, output_dir),
|
| 216 |
+
),
|
| 217 |
+
(
|
| 218 |
+
"grasp generation",
|
| 219 |
+
lambda: self.process_grasp(urdf_path, output_dir),
|
| 220 |
+
),
|
| 221 |
+
(
|
| 222 |
+
"grasp evaluation",
|
| 223 |
+
lambda: self.process_grasp_eval(urdf_path, output_dir),
|
| 224 |
+
),
|
| 225 |
+
]
|
| 226 |
+
|
| 227 |
+
results = {}
|
| 228 |
+
for stage_name, stage_fn in stages:
|
| 229 |
+
logger.info(f"Starting affordance stage: {stage_name}")
|
| 230 |
+
results[stage_name] = self.run_stage(
|
| 231 |
+
stage_name,
|
| 232 |
+
stage_fn,
|
| 233 |
+
urdf_path,
|
| 234 |
+
)
|
| 235 |
+
logger.info(
|
| 236 |
+
"Finished affordance stage: {} ({})".format(
|
| 237 |
+
stage_name,
|
| 238 |
+
"success" if results[stage_name] else "failed",
|
| 239 |
+
)
|
| 240 |
+
)
|
| 241 |
+
|
| 242 |
+
failed_stages = [
|
| 243 |
+
stage_name for stage_name, success in results.items() if not success
|
| 244 |
+
]
|
| 245 |
+
if failed_stages:
|
| 246 |
+
logger.warning(
|
| 247 |
+
"Affordance pipeline finished with failed stages for {}: {}".format(
|
| 248 |
+
urdf_path,
|
| 249 |
+
", ".join(failed_stages),
|
| 250 |
+
)
|
| 251 |
+
)
|
| 252 |
+
else:
|
| 253 |
+
logger.info(f"Affordance pipeline finished successfully: {urdf_path}")
|
| 254 |
+
|
| 255 |
+
elapsed_seconds = time.perf_counter() - start_time
|
| 256 |
+
self.asset_timings.append(
|
| 257 |
+
{
|
| 258 |
+
"urdf_path": urdf_path,
|
| 259 |
+
"output_dir": output_dir,
|
| 260 |
+
"elapsed_seconds": elapsed_seconds,
|
| 261 |
+
}
|
| 262 |
+
)
|
| 263 |
+
logger.info(
|
| 264 |
+
"Affordance pipeline time for {}: {:.2f}s".format(
|
| 265 |
+
urdf_path,
|
| 266 |
+
elapsed_seconds,
|
| 267 |
+
)
|
| 268 |
+
)
|
| 269 |
+
return results
|
| 270 |
+
|
| 271 |
+
def log_timing_summary(self) -> None:
|
| 272 |
+
if not self.asset_timings:
|
| 273 |
+
return
|
| 274 |
+
|
| 275 |
+
total_seconds = sum(
|
| 276 |
+
float(item["elapsed_seconds"]) for item in self.asset_timings
|
| 277 |
+
)
|
| 278 |
+
asset_count = len(self.asset_timings)
|
| 279 |
+
logger.info(
|
| 280 |
+
"Affordance pipeline total time: {:.2f}s for {} asset(s).".format(
|
| 281 |
+
total_seconds,
|
| 282 |
+
asset_count,
|
| 283 |
+
)
|
| 284 |
+
)
|
| 285 |
+
logger.info(
|
| 286 |
+
"Affordance pipeline average time per asset: {:.2f}s".format(
|
| 287 |
+
total_seconds / asset_count,
|
| 288 |
+
)
|
| 289 |
+
)
|
| 290 |
+
|
| 291 |
+
def process(self) -> list[dict[str, bool]]:
|
| 292 |
+
results = []
|
| 293 |
+
for idx, (urdf_path, output_dir) in enumerate(
|
| 294 |
+
zip(self.cfg.urdf_paths, self.cfg.output_dirs),
|
| 295 |
+
start=1,
|
| 296 |
+
):
|
| 297 |
+
results.append(self.process_one_urdf(urdf_path, output_dir, idx))
|
| 298 |
+
self.log_timing_summary()
|
| 299 |
+
return results
|
| 300 |
+
|
| 301 |
+
|
| 302 |
+
def run_affordance_annot(cfg: AffordanceAnnotConfig) -> None:
|
| 303 |
+
annotator = AffordanceAnnotator(cfg)
|
| 304 |
+
annotator.process()
|
| 305 |
+
|
| 306 |
+
|
| 307 |
+
def entrypoint(*args, **kwargs) -> None:
|
| 308 |
+
cfg = tyro.cli(AffordanceAnnotConfig)
|
| 309 |
+
run_affordance_annot(cfg)
|
| 310 |
+
|
| 311 |
+
|
| 312 |
+
if __name__ == "__main__":
|
| 313 |
+
entrypoint()
|
embodied_gen/scripts/affordance_annot/eval_grasps.py
ADDED
|
@@ -0,0 +1,1303 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Project EmbodiedGen
|
| 2 |
+
#
|
| 3 |
+
# Copyright (c) 2025 Horizon Robotics. All Rights Reserved.
|
| 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
|
| 14 |
+
# implied. See the License for the specific language governing
|
| 15 |
+
# permissions and limitations under the License.
|
| 16 |
+
|
| 17 |
+
import concurrent.futures
|
| 18 |
+
import dataclasses
|
| 19 |
+
import os
|
| 20 |
+
import threading
|
| 21 |
+
import time
|
| 22 |
+
import warnings
|
| 23 |
+
from collections import defaultdict
|
| 24 |
+
from dataclasses import dataclass, field
|
| 25 |
+
|
| 26 |
+
warnings.filterwarnings("ignore", category=UserWarning)
|
| 27 |
+
|
| 28 |
+
from typing import Literal
|
| 29 |
+
|
| 30 |
+
import imageio
|
| 31 |
+
import numpy as np
|
| 32 |
+
import sapien.core as sapien
|
| 33 |
+
import sapien.physx as physx
|
| 34 |
+
import torch
|
| 35 |
+
import tyro
|
| 36 |
+
from PIL import Image, ImageColor
|
| 37 |
+
from scipy.spatial.transform import Rotation as R
|
| 38 |
+
from tqdm import tqdm
|
| 39 |
+
from embodied_gen.data.utils import DiffrastRender
|
| 40 |
+
from embodied_gen.utils.geometry import MeshInfo
|
| 41 |
+
from embodied_gen.utils.io_utils import (
|
| 42 |
+
URDFFile,
|
| 43 |
+
load_json,
|
| 44 |
+
load_mesh_info,
|
| 45 |
+
write_json,
|
| 46 |
+
)
|
| 47 |
+
from embodied_gen.utils.log import logger
|
| 48 |
+
|
| 49 |
+
COLORMAP = list(set(ImageColor.colormap.values()))
|
| 50 |
+
COLOR_PALETTE = np.array(
|
| 51 |
+
[ImageColor.getrgb(c) for c in COLORMAP], dtype=np.uint8
|
| 52 |
+
)
|
| 53 |
+
MAX_NUM_WORKERS = 10
|
| 54 |
+
MAX_GRASPS_PER_WORKER_PROCESS = 8
|
| 55 |
+
SAPIEN_DEFAULT_STATIC_FRICTION = 0.3
|
| 56 |
+
SAPIEN_DEFAULT_DYNAMIC_FRICTION = 0.3
|
| 57 |
+
SAPIEN_DEFAULT_RESTITUTION = 0.1
|
| 58 |
+
|
| 59 |
+
_EVALUATOR_KEEPALIVE: list["EvalGrasps"] = []
|
| 60 |
+
_EVALUATOR_KEEPALIVE_LOCK = threading.Lock()
|
| 61 |
+
|
| 62 |
+
__all__ = [
|
| 63 |
+
"EvalGraspsConfig",
|
| 64 |
+
"GripperControlInfo",
|
| 65 |
+
"SapienSceneManager",
|
| 66 |
+
"EvalGrasps",
|
| 67 |
+
"run_eval",
|
| 68 |
+
"entrypoint",
|
| 69 |
+
]
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
@dataclass
|
| 73 |
+
class EvalGraspsConfig:
|
| 74 |
+
urdf_paths: list[str] = field(default_factory=list)
|
| 75 |
+
gripper_name: str = "franka_panda"
|
| 76 |
+
gripper_urdf_path: str = (
|
| 77 |
+
f"embodied_gen/scripts/affordance_annot/{gripper_name}/gripper.urdf"
|
| 78 |
+
)
|
| 79 |
+
output_dirs: list[str] = field(default_factory=list)
|
| 80 |
+
sim_freq: int = 240
|
| 81 |
+
video_fps: int = 30
|
| 82 |
+
render_interval: int = 24
|
| 83 |
+
image_hw: tuple[int, int] = (512, 512)
|
| 84 |
+
num_cameras: int = 1
|
| 85 |
+
fovy_deg: float = 45.0
|
| 86 |
+
sim_backend: str = "cpu"
|
| 87 |
+
ray_tracing: bool = False
|
| 88 |
+
gripper_move_speed: float = 0.1
|
| 89 |
+
gripper_close_speed: float = 0.01
|
| 90 |
+
action_wait_seconds: float = 0.5
|
| 91 |
+
lift_height: float = 0.5
|
| 92 |
+
sweep_distance: float = 0.5
|
| 93 |
+
num_workers: int | None = None
|
| 94 |
+
max_grasps_per_worker_process: int = MAX_GRASPS_PER_WORKER_PROCESS
|
| 95 |
+
gripper_drive_stiffness: float = 2500.0
|
| 96 |
+
gripper_static_friction: float = SAPIEN_DEFAULT_STATIC_FRICTION
|
| 97 |
+
gripper_dynamic_friction: float = SAPIEN_DEFAULT_DYNAMIC_FRICTION
|
| 98 |
+
gripper_restitution: float = SAPIEN_DEFAULT_RESTITUTION
|
| 99 |
+
root_drive_stiffness: float = 1e6
|
| 100 |
+
root_drive_damping: float = 1e4
|
| 101 |
+
object_bottom_clearance: float = 0.1
|
| 102 |
+
hold_object_during_close: bool = True
|
| 103 |
+
slip_translation_threshold: float = 0.05
|
| 104 |
+
slip_rotation_threshold_deg: float = 30.0
|
| 105 |
+
check_close_slip: bool = True
|
| 106 |
+
debug_mode: bool = False
|
| 107 |
+
save_failed_grasps: bool = False
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
@dataclass
|
| 111 |
+
class GripperControlInfo:
|
| 112 |
+
open_qpos: float
|
| 113 |
+
close_qpos: float
|
| 114 |
+
drive_damping: float
|
| 115 |
+
force_limit: float
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
class GripperActionAgent:
|
| 119 |
+
def __init__(
|
| 120 |
+
self,
|
| 121 |
+
scene_manager: "SapienSceneManager",
|
| 122 |
+
gripper: physx.PhysxArticulation,
|
| 123 |
+
root_target: sapien.Entity,
|
| 124 |
+
root_drive: physx.PhysxDriveComponent,
|
| 125 |
+
) -> None:
|
| 126 |
+
self.scene_manager = scene_manager
|
| 127 |
+
self.gripper = gripper
|
| 128 |
+
self.root_target = root_target
|
| 129 |
+
self.root_drive = root_drive
|
| 130 |
+
|
| 131 |
+
def set_action(self, action: np.ndarray | torch.Tensor) -> None:
|
| 132 |
+
if isinstance(action, torch.Tensor):
|
| 133 |
+
values = action.detach().cpu().numpy().reshape(-1)
|
| 134 |
+
else:
|
| 135 |
+
values = np.asarray(action).reshape(-1)
|
| 136 |
+
if values.size != 8:
|
| 137 |
+
raise ValueError(
|
| 138 |
+
f"Gripper action must have 8 values, got {values.size}."
|
| 139 |
+
)
|
| 140 |
+
self.root_target.set_pose(
|
| 141 |
+
sapien.Pose(
|
| 142 |
+
p=values[:3].astype(np.float64),
|
| 143 |
+
q=values[3:7].astype(np.float64),
|
| 144 |
+
)
|
| 145 |
+
)
|
| 146 |
+
self.scene_manager.set_gripper_target(
|
| 147 |
+
self.gripper,
|
| 148 |
+
float(values[7]),
|
| 149 |
+
)
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
class SapienSceneManager:
|
| 153 |
+
def __init__(
|
| 154 |
+
self, sim_freq: int, ray_tracing: bool, device: str = "cpu"
|
| 155 |
+
) -> None:
|
| 156 |
+
self.sim_freq = sim_freq
|
| 157 |
+
self.ray_tracing = ray_tracing
|
| 158 |
+
self.device = device
|
| 159 |
+
self.renderer = sapien.SapienRenderer()
|
| 160 |
+
self.scene = self.setup_scene()
|
| 161 |
+
self.cameras: list[sapien.render.RenderCameraComponent] = []
|
| 162 |
+
self.actors: dict[str, sapien.pysapien.Entity] = {}
|
| 163 |
+
|
| 164 |
+
def setup_scene(self) -> sapien.Scene:
|
| 165 |
+
# Ray tracing settings
|
| 166 |
+
if self.ray_tracing:
|
| 167 |
+
sapien.render.set_camera_shader_dir("rt")
|
| 168 |
+
sapien.render.set_ray_tracing_samples_per_pixel(64)
|
| 169 |
+
sapien.render.set_ray_tracing_path_depth(10)
|
| 170 |
+
sapien.render.set_ray_tracing_denoiser("oidn")
|
| 171 |
+
|
| 172 |
+
scene = sapien.Scene()
|
| 173 |
+
scene.set_timestep(1 / self.sim_freq)
|
| 174 |
+
|
| 175 |
+
# Add lighting
|
| 176 |
+
scene.set_ambient_light([0.2, 0.2, 0.2])
|
| 177 |
+
scene.add_directional_light(
|
| 178 |
+
direction=[0, 1, -1],
|
| 179 |
+
color=[1.5, 1.45, 1.4],
|
| 180 |
+
shadow=True,
|
| 181 |
+
shadow_map_size=2048,
|
| 182 |
+
)
|
| 183 |
+
scene.add_directional_light(
|
| 184 |
+
direction=[0, -0.5, 1], color=[0.8, 0.8, 0.85], shadow=False
|
| 185 |
+
)
|
| 186 |
+
scene.add_directional_light(
|
| 187 |
+
direction=[0, -1, 1], color=[1.0, 1.0, 1.0], shadow=False
|
| 188 |
+
)
|
| 189 |
+
|
| 190 |
+
ground_material = self.renderer.create_material()
|
| 191 |
+
ground_material.base_color = [0.5, 0.5, 0.5, 1] # rgba, gray
|
| 192 |
+
ground_material.roughness = 0.7
|
| 193 |
+
ground_material.metallic = 0.0
|
| 194 |
+
scene.add_ground(0, render_material=ground_material)
|
| 195 |
+
return scene
|
| 196 |
+
|
| 197 |
+
def set_ground_color(self, rgba: list[float]) -> None:
|
| 198 |
+
"""Update the default ground plane material color for this scene."""
|
| 199 |
+
for actor in self.scene.get_all_actors():
|
| 200 |
+
if actor.name != "ground":
|
| 201 |
+
continue
|
| 202 |
+
for component in actor.components:
|
| 203 |
+
render_shapes = getattr(component, "render_shapes", None)
|
| 204 |
+
if render_shapes is None:
|
| 205 |
+
continue
|
| 206 |
+
for render_shape in render_shapes:
|
| 207 |
+
render_shape.material.set_base_color(rgba)
|
| 208 |
+
return
|
| 209 |
+
raise ValueError("Ground actor not found in the scene.")
|
| 210 |
+
|
| 211 |
+
def create_camera(
|
| 212 |
+
self,
|
| 213 |
+
cam_name: str,
|
| 214 |
+
pose: sapien.Pose,
|
| 215 |
+
image_hw: tuple[int, int],
|
| 216 |
+
fovy_deg: float,
|
| 217 |
+
) -> sapien.render.RenderCameraComponent:
|
| 218 |
+
cam_actor = self.scene.create_actor_builder().build_kinematic()
|
| 219 |
+
cam_actor.set_pose(pose)
|
| 220 |
+
camera = self.scene.add_mounted_camera(
|
| 221 |
+
name=cam_name,
|
| 222 |
+
mount=cam_actor,
|
| 223 |
+
pose=sapien.Pose(p=[0, 0, 0], q=[1, 0, 0, 0]),
|
| 224 |
+
width=image_hw[1],
|
| 225 |
+
height=image_hw[0],
|
| 226 |
+
fovy=np.deg2rad(fovy_deg),
|
| 227 |
+
near=0.01,
|
| 228 |
+
far=100,
|
| 229 |
+
)
|
| 230 |
+
self.cameras.append(camera)
|
| 231 |
+
return camera
|
| 232 |
+
|
| 233 |
+
def add_ring_cameras(
|
| 234 |
+
self,
|
| 235 |
+
num_cameras: int,
|
| 236 |
+
radius: float,
|
| 237 |
+
height: float,
|
| 238 |
+
target_pt: list[float],
|
| 239 |
+
image_hw: tuple[int, int],
|
| 240 |
+
fovy_deg: float,
|
| 241 |
+
) -> list[sapien.render.RenderCameraComponent]:
|
| 242 |
+
angle_step = 2 * np.pi / num_cameras
|
| 243 |
+
world_up_vec = np.array([0.0, 0.0, 1.0])
|
| 244 |
+
target_pt = np.array(target_pt)
|
| 245 |
+
|
| 246 |
+
for i in range(num_cameras):
|
| 247 |
+
angle = i * angle_step
|
| 248 |
+
cam_x = target_pt[0] + radius * np.cos(angle)
|
| 249 |
+
cam_y = target_pt[1] + radius * np.sin(angle)
|
| 250 |
+
cam_z = height
|
| 251 |
+
eye_pos = [cam_x, cam_y, cam_z]
|
| 252 |
+
|
| 253 |
+
forward_vec = target_pt - eye_pos
|
| 254 |
+
forward_vec = forward_vec / np.linalg.norm(forward_vec)
|
| 255 |
+
temp_right_vec = np.cross(forward_vec, world_up_vec)
|
| 256 |
+
|
| 257 |
+
if np.linalg.norm(temp_right_vec) < 1e-6:
|
| 258 |
+
temp_right_vec = np.array([1.0, 0.0, 0.0])
|
| 259 |
+
if np.abs(np.dot(temp_right_vec, forward_vec)) > 0.99:
|
| 260 |
+
temp_right_vec = np.array([0.0, 1.0, 0.0])
|
| 261 |
+
|
| 262 |
+
right_vec = temp_right_vec / np.linalg.norm(temp_right_vec)
|
| 263 |
+
up_vec = np.cross(right_vec, forward_vec)
|
| 264 |
+
rotation_matrix = np.array([forward_vec, -right_vec, up_vec]).T
|
| 265 |
+
|
| 266 |
+
rot = R.from_matrix(rotation_matrix)
|
| 267 |
+
scipy_quat = rot.as_quat() # (x, y, z, w)
|
| 268 |
+
quat = [
|
| 269 |
+
scipy_quat[3],
|
| 270 |
+
scipy_quat[0],
|
| 271 |
+
scipy_quat[1],
|
| 272 |
+
scipy_quat[2],
|
| 273 |
+
] # (w, x, y, z)
|
| 274 |
+
|
| 275 |
+
self.create_camera(
|
| 276 |
+
f"camera_{i}",
|
| 277 |
+
sapien.Pose(p=eye_pos, q=quat),
|
| 278 |
+
image_hw,
|
| 279 |
+
fovy_deg,
|
| 280 |
+
)
|
| 281 |
+
|
| 282 |
+
return self.cameras
|
| 283 |
+
|
| 284 |
+
def add_actor(
|
| 285 |
+
self,
|
| 286 |
+
mesh_info: MeshInfo,
|
| 287 |
+
position: list[float],
|
| 288 |
+
) -> sapien.Entity:
|
| 289 |
+
builder = self.scene.create_actor_builder()
|
| 290 |
+
material = physx.PhysxMaterial(
|
| 291 |
+
static_friction=float(
|
| 292 |
+
np.clip(mesh_info.static_friction, 0.1, 0.7)
|
| 293 |
+
),
|
| 294 |
+
dynamic_friction=float(
|
| 295 |
+
np.clip(mesh_info.dynamic_friction, 0.1, 0.6)
|
| 296 |
+
),
|
| 297 |
+
restitution=0.05,
|
| 298 |
+
)
|
| 299 |
+
builder.set_physx_body_type("dynamic")
|
| 300 |
+
builder.add_multiple_convex_collisions_from_file(
|
| 301 |
+
mesh_info.collision_mesh_path,
|
| 302 |
+
scale=mesh_info.collision_mesh_scale,
|
| 303 |
+
pose=mesh_info.collision_local_pose,
|
| 304 |
+
material=material,
|
| 305 |
+
)
|
| 306 |
+
builder.add_visual_from_file(
|
| 307 |
+
mesh_info.visual_mesh_path,
|
| 308 |
+
scale=mesh_info.visual_mesh_scale,
|
| 309 |
+
pose=mesh_info.visual_local_pose,
|
| 310 |
+
)
|
| 311 |
+
|
| 312 |
+
builder.set_initial_pose(sapien.Pose(p=position))
|
| 313 |
+
actor = builder.build(name=mesh_info.actor_name)
|
| 314 |
+
if mesh_info.mass is not None and hasattr(
|
| 315 |
+
actor.components[1], "set_mass"
|
| 316 |
+
):
|
| 317 |
+
actor.components[1].set_mass(mesh_info.mass)
|
| 318 |
+
return actor
|
| 319 |
+
|
| 320 |
+
def add_gripper(
|
| 321 |
+
self,
|
| 322 |
+
gripper_urdf_path: str,
|
| 323 |
+
root_pose: sapien.Pose,
|
| 324 |
+
open_qpos: float,
|
| 325 |
+
drive_stiffness: float,
|
| 326 |
+
drive_damping: float,
|
| 327 |
+
force_limit: float,
|
| 328 |
+
static_friction: float,
|
| 329 |
+
dynamic_friction: float,
|
| 330 |
+
restitution: float,
|
| 331 |
+
root_drive_stiffness: float,
|
| 332 |
+
root_drive_damping: float,
|
| 333 |
+
) -> tuple[
|
| 334 |
+
physx.PhysxArticulation,
|
| 335 |
+
sapien.Entity,
|
| 336 |
+
physx.PhysxDriveComponent,
|
| 337 |
+
]:
|
| 338 |
+
loader = self.scene.create_urdf_loader()
|
| 339 |
+
loader.fix_root_link = False
|
| 340 |
+
gripper = loader.load(gripper_urdf_path)
|
| 341 |
+
gripper.set_root_pose(root_pose)
|
| 342 |
+
self.set_articulation_contact_material(
|
| 343 |
+
gripper,
|
| 344 |
+
physx.PhysxMaterial(
|
| 345 |
+
static_friction,
|
| 346 |
+
dynamic_friction,
|
| 347 |
+
restitution,
|
| 348 |
+
),
|
| 349 |
+
)
|
| 350 |
+
for link in gripper.get_links():
|
| 351 |
+
link.set_disable_gravity(True)
|
| 352 |
+
|
| 353 |
+
root_target = self.scene.create_actor_builder().build_kinematic(
|
| 354 |
+
name="gripper_root_target"
|
| 355 |
+
)
|
| 356 |
+
root_target.set_pose(root_pose)
|
| 357 |
+
root_drive = self.scene.create_drive(
|
| 358 |
+
root_target,
|
| 359 |
+
sapien.Pose(),
|
| 360 |
+
gripper.get_root(),
|
| 361 |
+
sapien.Pose(),
|
| 362 |
+
)
|
| 363 |
+
root_drive.set_limit_x(
|
| 364 |
+
0.0, 0.0, root_drive_stiffness, root_drive_damping
|
| 365 |
+
)
|
| 366 |
+
root_drive.set_limit_y(
|
| 367 |
+
0.0, 0.0, root_drive_stiffness, root_drive_damping
|
| 368 |
+
)
|
| 369 |
+
root_drive.set_limit_z(
|
| 370 |
+
0.0, 0.0, root_drive_stiffness, root_drive_damping
|
| 371 |
+
)
|
| 372 |
+
root_drive.set_limit_twist(
|
| 373 |
+
0.0, 0.0, root_drive_stiffness, root_drive_damping
|
| 374 |
+
)
|
| 375 |
+
root_drive.set_limit_cone(
|
| 376 |
+
0.0, 0.0, root_drive_stiffness, root_drive_damping
|
| 377 |
+
)
|
| 378 |
+
root_drive.set_drive_property_x(
|
| 379 |
+
root_drive_stiffness, root_drive_damping
|
| 380 |
+
)
|
| 381 |
+
root_drive.set_drive_property_y(
|
| 382 |
+
root_drive_stiffness, root_drive_damping
|
| 383 |
+
)
|
| 384 |
+
root_drive.set_drive_property_z(
|
| 385 |
+
root_drive_stiffness, root_drive_damping
|
| 386 |
+
)
|
| 387 |
+
root_drive.set_drive_property_slerp(
|
| 388 |
+
root_drive_stiffness,
|
| 389 |
+
root_drive_damping,
|
| 390 |
+
)
|
| 391 |
+
root_drive.set_inv_mass_scales(0.0, 1.0)
|
| 392 |
+
|
| 393 |
+
qpos = []
|
| 394 |
+
for joint in gripper.get_active_joints():
|
| 395 |
+
joint.set_drive_properties(
|
| 396 |
+
drive_stiffness,
|
| 397 |
+
drive_damping,
|
| 398 |
+
force_limit,
|
| 399 |
+
)
|
| 400 |
+
joint.set_friction(0.1)
|
| 401 |
+
limit = np.asarray(joint.get_limits()).reshape(-1, 2)[0]
|
| 402 |
+
target = float(np.clip(open_qpos, limit[0], limit[1]))
|
| 403 |
+
joint.set_drive_target(target)
|
| 404 |
+
qpos.append(target)
|
| 405 |
+
if qpos:
|
| 406 |
+
gripper.set_qpos(qpos)
|
| 407 |
+
gripper.set_qvel(np.zeros(len(qpos), dtype=np.float32))
|
| 408 |
+
return gripper, root_target, root_drive
|
| 409 |
+
|
| 410 |
+
@staticmethod
|
| 411 |
+
def set_articulation_contact_material(
|
| 412 |
+
articulation: physx.PhysxArticulation,
|
| 413 |
+
material: physx.PhysxMaterial,
|
| 414 |
+
) -> None:
|
| 415 |
+
for link in articulation.get_links():
|
| 416 |
+
for shape in link.get_collision_shapes():
|
| 417 |
+
shape.set_physical_material(material)
|
| 418 |
+
|
| 419 |
+
def set_gripper_target(
|
| 420 |
+
self,
|
| 421 |
+
gripper: physx.PhysxArticulation,
|
| 422 |
+
finger_qpos: float,
|
| 423 |
+
) -> None:
|
| 424 |
+
for joint in gripper.get_active_joints():
|
| 425 |
+
limit = np.asarray(joint.get_limits()).reshape(-1, 2)[0]
|
| 426 |
+
target = float(np.clip(finger_qpos, limit[0], limit[1]))
|
| 427 |
+
joint.set_drive_target(target)
|
| 428 |
+
|
| 429 |
+
def render_images(
|
| 430 |
+
self,
|
| 431 |
+
camera,
|
| 432 |
+
render_keys: list[
|
| 433 |
+
Literal[
|
| 434 |
+
"Color",
|
| 435 |
+
"Segmentation",
|
| 436 |
+
"Normal",
|
| 437 |
+
"Mask",
|
| 438 |
+
"Depth",
|
| 439 |
+
"Foreground",
|
| 440 |
+
]
|
| 441 |
+
] = None,
|
| 442 |
+
) -> dict[str, Image.Image]:
|
| 443 |
+
if render_keys is None:
|
| 444 |
+
render_keys = [
|
| 445 |
+
"Color",
|
| 446 |
+
"Segmentation",
|
| 447 |
+
"Normal",
|
| 448 |
+
"Mask",
|
| 449 |
+
"Depth",
|
| 450 |
+
"Foreground",
|
| 451 |
+
]
|
| 452 |
+
|
| 453 |
+
results: dict[str, Image.Image] = {}
|
| 454 |
+
if "Color" in render_keys:
|
| 455 |
+
color = camera.get_picture("Color")
|
| 456 |
+
color_rgb = (np.clip(color[..., :3], 0, 1) * 255).astype(np.uint8)
|
| 457 |
+
results["Color"] = Image.fromarray(color_rgb)
|
| 458 |
+
|
| 459 |
+
if "Mask" in render_keys:
|
| 460 |
+
alpha = (np.clip(color[..., 3], 0, 1) * 255).astype(np.uint8)
|
| 461 |
+
results["Mask"] = Image.fromarray(alpha)
|
| 462 |
+
|
| 463 |
+
if "Segmentation" in render_keys:
|
| 464 |
+
seg_labels = camera.get_picture("Segmentation")
|
| 465 |
+
label0 = seg_labels[..., 0].astype(np.uint8)
|
| 466 |
+
seg_color = COLOR_PALETTE[label0]
|
| 467 |
+
results["Segmentation"] = Image.fromarray(seg_color)
|
| 468 |
+
|
| 469 |
+
if "Foreground" in render_keys:
|
| 470 |
+
seg_labels = camera.get_picture("Segmentation")
|
| 471 |
+
label0 = seg_labels[..., 0]
|
| 472 |
+
mask = np.where((label0 > 1), 255, 0).astype(np.uint8)
|
| 473 |
+
color = camera.get_picture("Color")
|
| 474 |
+
color_rgb = (np.clip(color[..., :3], 0, 1) * 255).astype(np.uint8)
|
| 475 |
+
foreground = np.concatenate([color_rgb, mask[..., None]], axis=-1)
|
| 476 |
+
results["Foreground"] = Image.fromarray(foreground)
|
| 477 |
+
|
| 478 |
+
if "Normal" in render_keys:
|
| 479 |
+
normal = camera.get_picture("Normal")[..., :3]
|
| 480 |
+
normal_img = (((normal + 1) / 2) * 255).astype(np.uint8)
|
| 481 |
+
results["Normal"] = Image.fromarray(normal_img)
|
| 482 |
+
|
| 483 |
+
if "Depth" in render_keys:
|
| 484 |
+
position_map = camera.get_picture("Position")
|
| 485 |
+
depth = -position_map[..., 2]
|
| 486 |
+
alpha = torch.tensor(color[..., 3], dtype=torch.float32)
|
| 487 |
+
norm_depth = DiffrastRender.normalize_map_by_mask(
|
| 488 |
+
torch.tensor(depth), alpha
|
| 489 |
+
)
|
| 490 |
+
depth_img = (norm_depth * 255).to(torch.uint8).numpy()
|
| 491 |
+
results["Depth"] = Image.fromarray(depth_img)
|
| 492 |
+
|
| 493 |
+
return results
|
| 494 |
+
|
| 495 |
+
def step_action(
|
| 496 |
+
self,
|
| 497 |
+
agent: GripperActionAgent | None,
|
| 498 |
+
action: np.ndarray | torch.Tensor | None,
|
| 499 |
+
render_keys: list[str],
|
| 500 |
+
sim_steps_per_control: int = 1,
|
| 501 |
+
render: bool = True,
|
| 502 |
+
) -> dict:
|
| 503 |
+
if action is not None:
|
| 504 |
+
agent.set_action(action)
|
| 505 |
+
frames = defaultdict(defaultdict)
|
| 506 |
+
for _ in range(sim_steps_per_control):
|
| 507 |
+
self.scene.step()
|
| 508 |
+
if not render:
|
| 509 |
+
return frames
|
| 510 |
+
self.scene.update_render()
|
| 511 |
+
for camera in self.cameras:
|
| 512 |
+
camera.take_picture()
|
| 513 |
+
images = self.render_images(camera, render_keys=render_keys)
|
| 514 |
+
frames[camera.name].update(images)
|
| 515 |
+
return frames
|
| 516 |
+
|
| 517 |
+
|
| 518 |
+
class EvalGrasps:
|
| 519 |
+
def __init__(
|
| 520 |
+
self,
|
| 521 |
+
cfg: EvalGraspsConfig,
|
| 522 |
+
mesh_info: MeshInfo,
|
| 523 |
+
gripper_control: GripperControlInfo,
|
| 524 |
+
grasp: dict,
|
| 525 |
+
) -> None:
|
| 526 |
+
self.cfg = cfg
|
| 527 |
+
self.mesh_info = mesh_info
|
| 528 |
+
self.gripper_control = gripper_control
|
| 529 |
+
self.grasp = grasp
|
| 530 |
+
self.init_scene()
|
| 531 |
+
if self.cfg.debug_mode:
|
| 532 |
+
self.setup_cameras(mesh_info)
|
| 533 |
+
self.object_actor = self.add_object(mesh_info)
|
| 534 |
+
|
| 535 |
+
self.frames = {}
|
| 536 |
+
self.step_index = 0
|
| 537 |
+
self.world_x_offset = np.array(
|
| 538 |
+
[self.cfg.sweep_distance, 0.0, 0.0],
|
| 539 |
+
dtype=np.float64,
|
| 540 |
+
)
|
| 541 |
+
self.world_y_offset = np.array(
|
| 542 |
+
[0.0, self.cfg.sweep_distance, 0.0],
|
| 543 |
+
dtype=np.float64,
|
| 544 |
+
)
|
| 545 |
+
|
| 546 |
+
def should_render(self) -> bool:
|
| 547 |
+
return (
|
| 548 |
+
self.cfg.debug_mode
|
| 549 |
+
and self.step_index % max(1, self.cfg.render_interval) == 0
|
| 550 |
+
)
|
| 551 |
+
|
| 552 |
+
def init_scene(self) -> None:
|
| 553 |
+
self.scene_manager = SapienSceneManager(
|
| 554 |
+
sim_freq=self.cfg.sim_freq,
|
| 555 |
+
ray_tracing=self.cfg.ray_tracing,
|
| 556 |
+
device=self.cfg.sim_backend,
|
| 557 |
+
)
|
| 558 |
+
if self.cfg.debug_mode:
|
| 559 |
+
self.scene_manager.set_ground_color([0.78, 0.90, 0.72, 1.0])
|
| 560 |
+
|
| 561 |
+
def setup_cameras(self, mesh_info: MeshInfo) -> None:
|
| 562 |
+
bounds = mesh_info.transformed_mesh.bounds.copy()
|
| 563 |
+
|
| 564 |
+
mesh_size = bounds[1] - bounds[0]
|
| 565 |
+
mesh_height = max(float(mesh_size[2]), 0.5)
|
| 566 |
+
target_pt = [
|
| 567 |
+
float((bounds[0, 0] + bounds[1, 0]) * 0.5),
|
| 568 |
+
float((bounds[0, 1] + bounds[1, 1]) * 0.5),
|
| 569 |
+
float(bounds[1, 2]),
|
| 570 |
+
]
|
| 571 |
+
|
| 572 |
+
aspect = self.cfg.image_hw[1] / self.cfg.image_hw[0]
|
| 573 |
+
half_vfov = np.deg2rad(self.cfg.fovy_deg) * 0.5
|
| 574 |
+
half_hfov = np.arctan(np.tan(half_vfov) * aspect)
|
| 575 |
+
half_min_fov = min(half_vfov, half_hfov)
|
| 576 |
+
framing_half_extents = np.array(
|
| 577 |
+
[
|
| 578 |
+
max(float(mesh_size[0]) * 0.5, 1e-6),
|
| 579 |
+
max(float(mesh_size[1]) * 0.5, 1e-6),
|
| 580 |
+
mesh_height,
|
| 581 |
+
],
|
| 582 |
+
dtype=np.float64,
|
| 583 |
+
)
|
| 584 |
+
camera_radius = max(
|
| 585 |
+
float(
|
| 586 |
+
np.linalg.norm(framing_half_extents)
|
| 587 |
+
/ np.sin(half_min_fov)
|
| 588 |
+
* 1.5
|
| 589 |
+
),
|
| 590 |
+
2.5,
|
| 591 |
+
)
|
| 592 |
+
camera_height = target_pt[2]
|
| 593 |
+
|
| 594 |
+
self.scene_manager.add_ring_cameras(
|
| 595 |
+
num_cameras=self.cfg.num_cameras,
|
| 596 |
+
radius=camera_radius,
|
| 597 |
+
height=camera_height,
|
| 598 |
+
target_pt=target_pt,
|
| 599 |
+
image_hw=self.cfg.image_hw,
|
| 600 |
+
fovy_deg=self.cfg.fovy_deg,
|
| 601 |
+
)
|
| 602 |
+
|
| 603 |
+
def add_object(
|
| 604 |
+
self,
|
| 605 |
+
mesh_info: MeshInfo,
|
| 606 |
+
) -> sapien.Entity:
|
| 607 |
+
spawn_z = self.cfg.object_bottom_clearance - float(
|
| 608 |
+
mesh_info.transformed_mesh.bounds[0, 2]
|
| 609 |
+
)
|
| 610 |
+
actor = self.scene_manager.add_actor(mesh_info, [0.0, 0.0, spawn_z])
|
| 611 |
+
self.set_object_gravity(not self.cfg.hold_object_during_close, actor)
|
| 612 |
+
return actor
|
| 613 |
+
|
| 614 |
+
def set_object_gravity(
|
| 615 |
+
self,
|
| 616 |
+
enabled: bool,
|
| 617 |
+
actor: sapien.Entity | None = None,
|
| 618 |
+
) -> None:
|
| 619 |
+
actor = self.object_actor if actor is None else actor
|
| 620 |
+
for component in actor.components:
|
| 621 |
+
set_disable_gravity = getattr(
|
| 622 |
+
component,
|
| 623 |
+
"set_disable_gravity",
|
| 624 |
+
None,
|
| 625 |
+
)
|
| 626 |
+
if set_disable_gravity is not None:
|
| 627 |
+
set_disable_gravity(not enabled)
|
| 628 |
+
if enabled:
|
| 629 |
+
wake_up = getattr(component, "wake_up", None)
|
| 630 |
+
if wake_up is not None:
|
| 631 |
+
wake_up()
|
| 632 |
+
|
| 633 |
+
def run_actions(
|
| 634 |
+
self,
|
| 635 |
+
agent: GripperActionAgent,
|
| 636 |
+
actions: np.ndarray,
|
| 637 |
+
) -> None:
|
| 638 |
+
for action in actions:
|
| 639 |
+
this_frames = self.scene_manager.step_action(
|
| 640 |
+
agent=agent,
|
| 641 |
+
action=action,
|
| 642 |
+
render_keys=["Color"],
|
| 643 |
+
sim_steps_per_control=1,
|
| 644 |
+
render=self.should_render(),
|
| 645 |
+
)
|
| 646 |
+
self.update_frames(this_frames)
|
| 647 |
+
self.step_index += 1
|
| 648 |
+
|
| 649 |
+
@staticmethod
|
| 650 |
+
def make_pose(
|
| 651 |
+
position: np.ndarray,
|
| 652 |
+
quat_wxyz: np.ndarray,
|
| 653 |
+
) -> sapien.Pose:
|
| 654 |
+
return sapien.Pose(
|
| 655 |
+
p=[float(v) for v in position],
|
| 656 |
+
q=[float(v) for v in quat_wxyz],
|
| 657 |
+
)
|
| 658 |
+
|
| 659 |
+
@staticmethod
|
| 660 |
+
def pose_matrix(pose: sapien.Pose) -> np.ndarray:
|
| 661 |
+
quat_wxyz = np.asarray(pose.q, dtype=np.float64)
|
| 662 |
+
quat_norm = np.linalg.norm(quat_wxyz)
|
| 663 |
+
if quat_norm <= 0.0:
|
| 664 |
+
raise ValueError("Pose quaternion has zero norm.")
|
| 665 |
+
quat_wxyz = quat_wxyz / quat_norm
|
| 666 |
+
|
| 667 |
+
matrix = np.eye(4, dtype=np.float64)
|
| 668 |
+
matrix[:3, :3] = R.from_quat(
|
| 669 |
+
[
|
| 670 |
+
quat_wxyz[1],
|
| 671 |
+
quat_wxyz[2],
|
| 672 |
+
quat_wxyz[3],
|
| 673 |
+
quat_wxyz[0],
|
| 674 |
+
]
|
| 675 |
+
).as_matrix()
|
| 676 |
+
matrix[:3, 3] = np.asarray(pose.p, dtype=np.float64)
|
| 677 |
+
return matrix
|
| 678 |
+
|
| 679 |
+
@staticmethod
|
| 680 |
+
def pose_delta(
|
| 681 |
+
reference_relative_pose: np.ndarray,
|
| 682 |
+
current_relative_pose: np.ndarray,
|
| 683 |
+
) -> dict[str, float]:
|
| 684 |
+
delta_pose = (
|
| 685 |
+
np.linalg.inv(reference_relative_pose) @ current_relative_pose
|
| 686 |
+
)
|
| 687 |
+
translation_m = float(np.linalg.norm(delta_pose[:3, 3]))
|
| 688 |
+
rotation_rad = float(R.from_matrix(delta_pose[:3, :3]).magnitude())
|
| 689 |
+
return {
|
| 690 |
+
"translation_m": translation_m,
|
| 691 |
+
"rotation_rad": rotation_rad,
|
| 692 |
+
"rotation_deg": float(np.rad2deg(rotation_rad)),
|
| 693 |
+
}
|
| 694 |
+
|
| 695 |
+
def object_rel_pose(
|
| 696 |
+
self,
|
| 697 |
+
agent: GripperActionAgent,
|
| 698 |
+
) -> np.ndarray:
|
| 699 |
+
gripper_pose = self.pose_matrix(agent.gripper.get_root_pose())
|
| 700 |
+
object_pose = self.pose_matrix(self.object_actor.get_pose())
|
| 701 |
+
return np.linalg.inv(gripper_pose) @ object_pose
|
| 702 |
+
|
| 703 |
+
def object_bottom_z(self) -> float:
|
| 704 |
+
object_pose = self.pose_matrix(self.object_actor.get_pose())
|
| 705 |
+
vertices = np.asarray(self.mesh_info.transformed_mesh.vertices)
|
| 706 |
+
world_vertices = (object_pose[:3, :3] @ vertices.T).T + object_pose[
|
| 707 |
+
:3, 3
|
| 708 |
+
]
|
| 709 |
+
return float(np.min(world_vertices[:, 2]))
|
| 710 |
+
|
| 711 |
+
def check_slip(
|
| 712 |
+
self,
|
| 713 |
+
agent: GripperActionAgent,
|
| 714 |
+
reference_relative_pose: np.ndarray,
|
| 715 |
+
) -> dict[str, float | bool]:
|
| 716 |
+
delta = self.pose_delta(
|
| 717 |
+
reference_relative_pose,
|
| 718 |
+
self.object_rel_pose(agent),
|
| 719 |
+
)
|
| 720 |
+
slipped = (
|
| 721 |
+
delta["translation_m"] > self.cfg.slip_translation_threshold
|
| 722 |
+
or delta["rotation_deg"] > self.cfg.slip_rotation_threshold_deg
|
| 723 |
+
)
|
| 724 |
+
return {
|
| 725 |
+
**delta,
|
| 726 |
+
"slipped": slipped,
|
| 727 |
+
}
|
| 728 |
+
|
| 729 |
+
def checked_action(
|
| 730 |
+
self,
|
| 731 |
+
step_name: str,
|
| 732 |
+
agent: GripperActionAgent,
|
| 733 |
+
action_fn,
|
| 734 |
+
*,
|
| 735 |
+
check_slip: bool = True,
|
| 736 |
+
) -> dict:
|
| 737 |
+
reference_relative_pose = self.object_rel_pose(agent)
|
| 738 |
+
action_fn()
|
| 739 |
+
self.wait_scene(self.cfg.action_wait_seconds)
|
| 740 |
+
delta = self.check_slip(
|
| 741 |
+
agent,
|
| 742 |
+
reference_relative_pose,
|
| 743 |
+
)
|
| 744 |
+
slipped = bool(delta["slipped"])
|
| 745 |
+
success = not slipped if check_slip else True
|
| 746 |
+
return {
|
| 747 |
+
"step": step_name,
|
| 748 |
+
"success": success,
|
| 749 |
+
"checked": check_slip,
|
| 750 |
+
"slipped": slipped,
|
| 751 |
+
"reason": None if success else "relative_pose_slip",
|
| 752 |
+
"relative_pose_delta": {
|
| 753 |
+
"translation_m": delta["translation_m"],
|
| 754 |
+
"rotation_rad": delta["rotation_rad"],
|
| 755 |
+
"rotation_deg": delta["rotation_deg"],
|
| 756 |
+
},
|
| 757 |
+
}
|
| 758 |
+
|
| 759 |
+
def get_steps(
|
| 760 |
+
self,
|
| 761 |
+
distance: float,
|
| 762 |
+
speed: float,
|
| 763 |
+
) -> int:
|
| 764 |
+
if distance <= 0.0:
|
| 765 |
+
return 2
|
| 766 |
+
duration = distance / speed
|
| 767 |
+
return max(2, int(np.ceil(duration * self.cfg.sim_freq)))
|
| 768 |
+
|
| 769 |
+
def move_to_pose(
|
| 770 |
+
self,
|
| 771 |
+
agent: GripperActionAgent,
|
| 772 |
+
start_pose: sapien.Pose,
|
| 773 |
+
end_pose: sapien.Pose,
|
| 774 |
+
finger_qpos: float | None = None,
|
| 775 |
+
) -> None:
|
| 776 |
+
if finger_qpos is None:
|
| 777 |
+
finger_qpos = self.gripper_control.close_qpos
|
| 778 |
+
|
| 779 |
+
start_pos = np.asarray(start_pose.p, dtype=np.float64)
|
| 780 |
+
end_pos = np.asarray(end_pose.p, dtype=np.float64)
|
| 781 |
+
travel_distance = float(np.linalg.norm(end_pos - start_pos))
|
| 782 |
+
num_steps = self.get_steps(
|
| 783 |
+
travel_distance,
|
| 784 |
+
self.cfg.gripper_move_speed,
|
| 785 |
+
)
|
| 786 |
+
alphas = np.linspace(0.0, 1.0, num_steps)
|
| 787 |
+
quat = np.asarray(start_pose.q, dtype=np.float64)
|
| 788 |
+
|
| 789 |
+
actions = []
|
| 790 |
+
for alpha in alphas:
|
| 791 |
+
pose = self.make_pose(
|
| 792 |
+
(1.0 - alpha) * start_pos + alpha * end_pos,
|
| 793 |
+
quat,
|
| 794 |
+
)
|
| 795 |
+
actions.append(self.make_action(pose, finger_qpos))
|
| 796 |
+
self.run_actions(agent, np.stack(actions, axis=0))
|
| 797 |
+
|
| 798 |
+
def move_gripper(
|
| 799 |
+
self,
|
| 800 |
+
agent: GripperActionAgent,
|
| 801 |
+
offset: np.ndarray,
|
| 802 |
+
) -> None:
|
| 803 |
+
start_pose = agent.gripper.get_root_pose()
|
| 804 |
+
end_pose = self.make_pose(
|
| 805 |
+
np.asarray(start_pose.p, dtype=np.float64)
|
| 806 |
+
+ np.asarray(offset, dtype=np.float64),
|
| 807 |
+
np.asarray(start_pose.q, dtype=np.float64),
|
| 808 |
+
)
|
| 809 |
+
self.move_to_pose(agent, start_pose, end_pose)
|
| 810 |
+
|
| 811 |
+
def init_grasp_pose(self) -> sapien.Pose:
|
| 812 |
+
orientation = self.grasp["orientation"]
|
| 813 |
+
self.grasp = sapien.Pose(
|
| 814 |
+
p=[float(v) for v in self.grasp["position"]],
|
| 815 |
+
q=[
|
| 816 |
+
float(orientation["w"]),
|
| 817 |
+
float(orientation["xyz"][0]),
|
| 818 |
+
float(orientation["xyz"][1]),
|
| 819 |
+
float(orientation["xyz"][2]),
|
| 820 |
+
],
|
| 821 |
+
)
|
| 822 |
+
gripper_pose = self.object_actor.get_pose() * self.grasp
|
| 823 |
+
return gripper_pose
|
| 824 |
+
|
| 825 |
+
def make_action(
|
| 826 |
+
self,
|
| 827 |
+
pose: sapien.Pose,
|
| 828 |
+
finger_qpos: float,
|
| 829 |
+
) -> np.ndarray:
|
| 830 |
+
return np.array(
|
| 831 |
+
[
|
| 832 |
+
float(pose.p[0]),
|
| 833 |
+
float(pose.p[1]),
|
| 834 |
+
float(pose.p[2]),
|
| 835 |
+
float(pose.q[0]),
|
| 836 |
+
float(pose.q[1]),
|
| 837 |
+
float(pose.q[2]),
|
| 838 |
+
float(pose.q[3]),
|
| 839 |
+
float(finger_qpos),
|
| 840 |
+
],
|
| 841 |
+
dtype=np.float32,
|
| 842 |
+
)
|
| 843 |
+
|
| 844 |
+
def add_gripper_agent(self) -> GripperActionAgent:
|
| 845 |
+
gripper_pose = self.init_grasp_pose()
|
| 846 |
+
gripper, root_target, root_drive = self.scene_manager.add_gripper(
|
| 847 |
+
self.cfg.gripper_urdf_path,
|
| 848 |
+
gripper_pose,
|
| 849 |
+
self.gripper_control.open_qpos,
|
| 850 |
+
self.cfg.gripper_drive_stiffness,
|
| 851 |
+
self.gripper_control.drive_damping,
|
| 852 |
+
self.gripper_control.force_limit,
|
| 853 |
+
self.cfg.gripper_static_friction,
|
| 854 |
+
self.cfg.gripper_dynamic_friction,
|
| 855 |
+
self.cfg.gripper_restitution,
|
| 856 |
+
self.cfg.root_drive_stiffness,
|
| 857 |
+
self.cfg.root_drive_damping,
|
| 858 |
+
)
|
| 859 |
+
return GripperActionAgent(
|
| 860 |
+
self.scene_manager,
|
| 861 |
+
gripper,
|
| 862 |
+
root_target,
|
| 863 |
+
root_drive,
|
| 864 |
+
)
|
| 865 |
+
|
| 866 |
+
def update_frames(self, this_frames: dict) -> None:
|
| 867 |
+
for cam_name, images_list in this_frames.items():
|
| 868 |
+
if cam_name not in self.frames:
|
| 869 |
+
self.frames[cam_name] = []
|
| 870 |
+
self.frames[cam_name].append(np.array(images_list["Color"]))
|
| 871 |
+
|
| 872 |
+
def wait_scene(self, seconds: float) -> None:
|
| 873 |
+
num_steps = int(self.cfg.sim_freq * seconds)
|
| 874 |
+
for _ in range(num_steps):
|
| 875 |
+
this_frames = self.scene_manager.step_action(
|
| 876 |
+
agent=None,
|
| 877 |
+
action=None,
|
| 878 |
+
render_keys=["Color"],
|
| 879 |
+
sim_steps_per_control=1,
|
| 880 |
+
render=self.should_render(),
|
| 881 |
+
)
|
| 882 |
+
self.update_frames(this_frames)
|
| 883 |
+
self.step_index += 1
|
| 884 |
+
|
| 885 |
+
def close_gripper(self, agent: GripperActionAgent) -> None:
|
| 886 |
+
num_close_steps = self.get_steps(
|
| 887 |
+
abs(
|
| 888 |
+
self.gripper_control.open_qpos
|
| 889 |
+
- self.gripper_control.close_qpos
|
| 890 |
+
),
|
| 891 |
+
self.cfg.gripper_close_speed,
|
| 892 |
+
)
|
| 893 |
+
root_pose = agent.gripper.get_root_pose()
|
| 894 |
+
actions = [
|
| 895 |
+
self.make_action(
|
| 896 |
+
root_pose,
|
| 897 |
+
(1.0 - alpha) * self.gripper_control.open_qpos
|
| 898 |
+
+ alpha * self.gripper_control.close_qpos,
|
| 899 |
+
)
|
| 900 |
+
for alpha in np.linspace(0.0, 1.0, max(1, num_close_steps))
|
| 901 |
+
]
|
| 902 |
+
self.run_actions(agent, np.stack(actions, axis=0))
|
| 903 |
+
|
| 904 |
+
@staticmethod
|
| 905 |
+
def summarize_steps(step_results: list[dict]) -> dict:
|
| 906 |
+
failure = next(
|
| 907 |
+
(
|
| 908 |
+
step_result
|
| 909 |
+
for step_result in step_results
|
| 910 |
+
if not step_result["success"]
|
| 911 |
+
),
|
| 912 |
+
None,
|
| 913 |
+
)
|
| 914 |
+
result = {
|
| 915 |
+
"success": failure is None,
|
| 916 |
+
"relative_pose_deltas": {
|
| 917 |
+
step_result["step"]: step_result["relative_pose_delta"]
|
| 918 |
+
for step_result in step_results
|
| 919 |
+
},
|
| 920 |
+
}
|
| 921 |
+
if failure is not None:
|
| 922 |
+
result.update(
|
| 923 |
+
{
|
| 924 |
+
"failure_step": failure["step"],
|
| 925 |
+
"reason": failure["reason"],
|
| 926 |
+
}
|
| 927 |
+
)
|
| 928 |
+
return result
|
| 929 |
+
|
| 930 |
+
def run(self) -> dict:
|
| 931 |
+
step_results = []
|
| 932 |
+
self.wait_scene(2.0)
|
| 933 |
+
|
| 934 |
+
this_agent = self.add_gripper_agent()
|
| 935 |
+
self.wait_scene(self.cfg.action_wait_seconds)
|
| 936 |
+
|
| 937 |
+
# Close the gripper around the object.
|
| 938 |
+
step_result = self.checked_action(
|
| 939 |
+
"close_gripper",
|
| 940 |
+
this_agent,
|
| 941 |
+
lambda: self.close_gripper(this_agent),
|
| 942 |
+
check_slip=self.cfg.check_close_slip,
|
| 943 |
+
)
|
| 944 |
+
step_results.append(step_result)
|
| 945 |
+
if not step_result["success"]:
|
| 946 |
+
return self.summarize_steps(step_results)
|
| 947 |
+
|
| 948 |
+
if self.cfg.hold_object_during_close:
|
| 949 |
+
self.set_object_gravity(True)
|
| 950 |
+
|
| 951 |
+
# Lift the grasped object upward.
|
| 952 |
+
step_result = self.checked_action(
|
| 953 |
+
"lift_gripper",
|
| 954 |
+
this_agent,
|
| 955 |
+
lambda: self.move_gripper(
|
| 956 |
+
this_agent,
|
| 957 |
+
np.array(
|
| 958 |
+
[0.0, 0.0, self.cfg.lift_height],
|
| 959 |
+
dtype=np.float64,
|
| 960 |
+
),
|
| 961 |
+
),
|
| 962 |
+
)
|
| 963 |
+
step_results.append(step_result)
|
| 964 |
+
if not step_result["success"]:
|
| 965 |
+
return self.summarize_steps(step_results)
|
| 966 |
+
|
| 967 |
+
shake_start_pose = this_agent.gripper.get_root_pose()
|
| 968 |
+
shake_end_pose = self.make_pose(
|
| 969 |
+
np.asarray(shake_start_pose.p, dtype=np.float64)
|
| 970 |
+
+ self.world_x_offset,
|
| 971 |
+
np.asarray(shake_start_pose.q, dtype=np.float64),
|
| 972 |
+
)
|
| 973 |
+
|
| 974 |
+
# Move the gripper outward along the X axis.
|
| 975 |
+
step_result = self.checked_action(
|
| 976 |
+
"move_gripper_out_x",
|
| 977 |
+
this_agent,
|
| 978 |
+
lambda: self.move_to_pose(
|
| 979 |
+
this_agent,
|
| 980 |
+
shake_start_pose,
|
| 981 |
+
shake_end_pose,
|
| 982 |
+
),
|
| 983 |
+
)
|
| 984 |
+
step_results.append(step_result)
|
| 985 |
+
if not step_result["success"]:
|
| 986 |
+
return self.summarize_steps(step_results)
|
| 987 |
+
|
| 988 |
+
# Move the gripper back from the X-axis offset.
|
| 989 |
+
step_result = self.checked_action(
|
| 990 |
+
"move_gripper_back_x",
|
| 991 |
+
this_agent,
|
| 992 |
+
lambda: self.move_to_pose(
|
| 993 |
+
this_agent,
|
| 994 |
+
this_agent.gripper.get_root_pose(),
|
| 995 |
+
shake_start_pose,
|
| 996 |
+
),
|
| 997 |
+
)
|
| 998 |
+
step_results.append(step_result)
|
| 999 |
+
if not step_result["success"]:
|
| 1000 |
+
return self.summarize_steps(step_results)
|
| 1001 |
+
|
| 1002 |
+
shake_start_pose = this_agent.gripper.get_root_pose()
|
| 1003 |
+
shake_end_pose = self.make_pose(
|
| 1004 |
+
np.asarray(shake_start_pose.p, dtype=np.float64)
|
| 1005 |
+
+ self.world_y_offset,
|
| 1006 |
+
np.asarray(shake_start_pose.q, dtype=np.float64),
|
| 1007 |
+
)
|
| 1008 |
+
|
| 1009 |
+
# Move the gripper outward along the Y axis.
|
| 1010 |
+
step_result = self.checked_action(
|
| 1011 |
+
"move_gripper_out_y",
|
| 1012 |
+
this_agent,
|
| 1013 |
+
lambda: self.move_to_pose(
|
| 1014 |
+
this_agent,
|
| 1015 |
+
shake_start_pose,
|
| 1016 |
+
shake_end_pose,
|
| 1017 |
+
),
|
| 1018 |
+
)
|
| 1019 |
+
step_results.append(step_result)
|
| 1020 |
+
if not step_result["success"]:
|
| 1021 |
+
return self.summarize_steps(step_results)
|
| 1022 |
+
|
| 1023 |
+
# Move the gripper back from the Y-axis offset.
|
| 1024 |
+
step_result = self.checked_action(
|
| 1025 |
+
"move_gripper_back_y",
|
| 1026 |
+
this_agent,
|
| 1027 |
+
lambda: self.move_to_pose(
|
| 1028 |
+
this_agent,
|
| 1029 |
+
this_agent.gripper.get_root_pose(),
|
| 1030 |
+
shake_start_pose,
|
| 1031 |
+
),
|
| 1032 |
+
)
|
| 1033 |
+
step_results.append(step_result)
|
| 1034 |
+
if not step_result["success"]:
|
| 1035 |
+
return self.summarize_steps(step_results)
|
| 1036 |
+
|
| 1037 |
+
# Lower the grasped object back down.
|
| 1038 |
+
descend_offset = np.array(
|
| 1039 |
+
[0.0, 0.0, 0.01 - self.object_bottom_z()],
|
| 1040 |
+
dtype=np.float64,
|
| 1041 |
+
)
|
| 1042 |
+
step_result = self.checked_action(
|
| 1043 |
+
"descend_gripper",
|
| 1044 |
+
this_agent,
|
| 1045 |
+
lambda: self.move_gripper(this_agent, descend_offset),
|
| 1046 |
+
)
|
| 1047 |
+
step_results.append(step_result)
|
| 1048 |
+
if not step_result["success"]:
|
| 1049 |
+
return self.summarize_steps(step_results)
|
| 1050 |
+
|
| 1051 |
+
return self.summarize_steps(step_results)
|
| 1052 |
+
|
| 1053 |
+
def export_video(self, output_dir: str) -> None:
|
| 1054 |
+
if not self.cfg.debug_mode:
|
| 1055 |
+
return
|
| 1056 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 1057 |
+
for cam_name, this_frames in self.frames.items():
|
| 1058 |
+
this_output_path = os.path.join(output_dir, f"{cam_name}.mp4")
|
| 1059 |
+
imageio.mimsave(
|
| 1060 |
+
this_output_path, this_frames, fps=self.cfg.video_fps
|
| 1061 |
+
)
|
| 1062 |
+
|
| 1063 |
+
|
| 1064 |
+
def _validate_config(cfg: EvalGraspsConfig) -> None:
|
| 1065 |
+
if not cfg.urdf_paths:
|
| 1066 |
+
raise ValueError("urdf_paths must be provided.")
|
| 1067 |
+
|
| 1068 |
+
if len(cfg.output_dirs) == 0:
|
| 1069 |
+
cfg.output_dirs = [
|
| 1070 |
+
os.path.join(os.path.dirname(path), "affordance")
|
| 1071 |
+
for path in cfg.urdf_paths
|
| 1072 |
+
]
|
| 1073 |
+
|
| 1074 |
+
if len(cfg.urdf_paths) != len(cfg.output_dirs):
|
| 1075 |
+
raise ValueError(
|
| 1076 |
+
"urdf_paths and output_dirs must have the same length, "
|
| 1077 |
+
f"got {len(cfg.urdf_paths)} and {len(cfg.output_dirs)}."
|
| 1078 |
+
)
|
| 1079 |
+
|
| 1080 |
+
|
| 1081 |
+
def _set_single_config(
|
| 1082 |
+
cfg: EvalGraspsConfig,
|
| 1083 |
+
urdf_path: str,
|
| 1084 |
+
output_dir: str,
|
| 1085 |
+
) -> EvalGraspsConfig:
|
| 1086 |
+
return dataclasses.replace(
|
| 1087 |
+
cfg,
|
| 1088 |
+
urdf_paths=[urdf_path],
|
| 1089 |
+
output_dirs=[output_dir],
|
| 1090 |
+
)
|
| 1091 |
+
|
| 1092 |
+
|
| 1093 |
+
def _eval_grasp(
|
| 1094 |
+
cfg: EvalGraspsConfig,
|
| 1095 |
+
grasp_id: str,
|
| 1096 |
+
grasp: dict,
|
| 1097 |
+
) -> dict:
|
| 1098 |
+
output_dir = cfg.output_dirs[0]
|
| 1099 |
+
|
| 1100 |
+
mesh_info = load_mesh_info(cfg.urdf_paths[0])
|
| 1101 |
+
gripper_control = GripperControlInfo(
|
| 1102 |
+
**URDFFile(cfg.gripper_urdf_path).get_prismatic_joint_control_info()
|
| 1103 |
+
)
|
| 1104 |
+
evaluator = EvalGrasps(cfg, mesh_info, gripper_control, grasp)
|
| 1105 |
+
result = evaluator.run()
|
| 1106 |
+
evaluator.export_video(
|
| 1107 |
+
os.path.join(output_dir, "eval_grasp_renders", grasp_id)
|
| 1108 |
+
)
|
| 1109 |
+
if threading.current_thread() is not threading.main_thread():
|
| 1110 |
+
with _EVALUATOR_KEEPALIVE_LOCK:
|
| 1111 |
+
_EVALUATOR_KEEPALIVE.append(evaluator)
|
| 1112 |
+
return {
|
| 1113 |
+
"grasp_id": grasp_id,
|
| 1114 |
+
"output_dir": output_dir,
|
| 1115 |
+
"simulation_result": result,
|
| 1116 |
+
}
|
| 1117 |
+
|
| 1118 |
+
|
| 1119 |
+
def _release_thread_evaluators() -> None:
|
| 1120 |
+
with _EVALUATOR_KEEPALIVE_LOCK:
|
| 1121 |
+
evaluators = list(_EVALUATOR_KEEPALIVE)
|
| 1122 |
+
_EVALUATOR_KEEPALIVE.clear()
|
| 1123 |
+
for evaluator in evaluators:
|
| 1124 |
+
del evaluator
|
| 1125 |
+
|
| 1126 |
+
|
| 1127 |
+
def _eval_grasp_task(task: tuple[EvalGraspsConfig, str, dict]) -> dict:
|
| 1128 |
+
cfg, grasp_id, grasp = task
|
| 1129 |
+
return _eval_grasp(cfg, grasp_id, grasp)
|
| 1130 |
+
|
| 1131 |
+
|
| 1132 |
+
def _slip_thresholds(cfg: EvalGraspsConfig) -> dict[str, float]:
|
| 1133 |
+
return {
|
| 1134 |
+
"translation_m": cfg.slip_translation_threshold,
|
| 1135 |
+
"rotation_deg": cfg.slip_rotation_threshold_deg,
|
| 1136 |
+
}
|
| 1137 |
+
|
| 1138 |
+
|
| 1139 |
+
def _is_successful_grasp_result(result: dict) -> bool:
|
| 1140 |
+
simulation_result = result.get("simulation_result", {})
|
| 1141 |
+
if not isinstance(simulation_result, dict):
|
| 1142 |
+
return False
|
| 1143 |
+
return bool(simulation_result.get("success", False))
|
| 1144 |
+
|
| 1145 |
+
|
| 1146 |
+
def _grasp_success_by_id(results: list[dict]) -> dict[str, bool]:
|
| 1147 |
+
return {
|
| 1148 |
+
result["grasp_id"]: _is_successful_grasp_result(result)
|
| 1149 |
+
for result in results
|
| 1150 |
+
}
|
| 1151 |
+
|
| 1152 |
+
|
| 1153 |
+
def _build_eval_summary(
|
| 1154 |
+
cfg: EvalGraspsConfig,
|
| 1155 |
+
results: list[dict],
|
| 1156 |
+
elapsed_seconds: float,
|
| 1157 |
+
) -> dict:
|
| 1158 |
+
successful_grasp_ids = [
|
| 1159 |
+
result["grasp_id"]
|
| 1160 |
+
for result in results
|
| 1161 |
+
if _is_successful_grasp_result(result)
|
| 1162 |
+
]
|
| 1163 |
+
failed_grasp_ids = [
|
| 1164 |
+
result["grasp_id"]
|
| 1165 |
+
for result in results
|
| 1166 |
+
if not _is_successful_grasp_result(result)
|
| 1167 |
+
]
|
| 1168 |
+
num_grasps = len(results)
|
| 1169 |
+
num_success = len(successful_grasp_ids)
|
| 1170 |
+
return {
|
| 1171 |
+
"num_grasps": num_grasps,
|
| 1172 |
+
"num_success": num_success,
|
| 1173 |
+
"num_failure": len(failed_grasp_ids),
|
| 1174 |
+
"success_rate": num_success / max(1, num_grasps),
|
| 1175 |
+
"successful_grasp_ids": successful_grasp_ids,
|
| 1176 |
+
"failed_grasp_ids": failed_grasp_ids,
|
| 1177 |
+
"slip_thresholds": _slip_thresholds(cfg),
|
| 1178 |
+
"elapsed_seconds": elapsed_seconds,
|
| 1179 |
+
}
|
| 1180 |
+
|
| 1181 |
+
|
| 1182 |
+
def _run_single_eval(cfg: EvalGraspsConfig) -> None:
|
| 1183 |
+
start_time = time.perf_counter()
|
| 1184 |
+
output_dir = cfg.output_dirs[0]
|
| 1185 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 1186 |
+
urdf_path = cfg.urdf_paths[0]
|
| 1187 |
+
urdf = URDFFile(urdf_path)
|
| 1188 |
+
|
| 1189 |
+
affordance_path = urdf.get_affordance_annot_path()
|
| 1190 |
+
affordance_payload = load_json(affordance_path)
|
| 1191 |
+
affordances = affordance_payload.get("affordances", [])
|
| 1192 |
+
grasps = {}
|
| 1193 |
+
for affordance in affordances:
|
| 1194 |
+
grasps.update(affordance.get("grasp_group", {}))
|
| 1195 |
+
|
| 1196 |
+
if not grasps:
|
| 1197 |
+
return
|
| 1198 |
+
|
| 1199 |
+
num_workers = cfg.num_workers
|
| 1200 |
+
if num_workers is None:
|
| 1201 |
+
num_workers = min(len(grasps), os.cpu_count() or 1, MAX_NUM_WORKERS)
|
| 1202 |
+
else:
|
| 1203 |
+
num_workers = min(num_workers, len(grasps), MAX_NUM_WORKERS)
|
| 1204 |
+
|
| 1205 |
+
logger.info(
|
| 1206 |
+
f"Running {len(grasps)} grasp poses with {num_workers} worker(s)"
|
| 1207 |
+
)
|
| 1208 |
+
results = []
|
| 1209 |
+
if num_workers == 1:
|
| 1210 |
+
for grasp_id, grasp in tqdm(grasps.items(), desc="Evaluating grasps"):
|
| 1211 |
+
results.append(_eval_grasp(cfg, grasp_id, grasp))
|
| 1212 |
+
else:
|
| 1213 |
+
worker_cfg = dataclasses.replace(cfg, num_workers=1)
|
| 1214 |
+
tasks = [
|
| 1215 |
+
(worker_cfg, grasp_id, grasp) for grasp_id, grasp in grasps.items()
|
| 1216 |
+
]
|
| 1217 |
+
try:
|
| 1218 |
+
with concurrent.futures.ThreadPoolExecutor(
|
| 1219 |
+
max_workers=num_workers
|
| 1220 |
+
) as executor:
|
| 1221 |
+
futures = [
|
| 1222 |
+
executor.submit(_eval_grasp_task, task) for task in tasks
|
| 1223 |
+
]
|
| 1224 |
+
for future in tqdm(
|
| 1225 |
+
concurrent.futures.as_completed(futures),
|
| 1226 |
+
total=len(futures),
|
| 1227 |
+
desc="Evaluating grasps",
|
| 1228 |
+
):
|
| 1229 |
+
results.append(future.result())
|
| 1230 |
+
results.sort(
|
| 1231 |
+
key=lambda result: list(grasps.keys()).index(
|
| 1232 |
+
result["grasp_id"]
|
| 1233 |
+
)
|
| 1234 |
+
)
|
| 1235 |
+
finally:
|
| 1236 |
+
_release_thread_evaluators()
|
| 1237 |
+
|
| 1238 |
+
elapsed_seconds = time.perf_counter() - start_time
|
| 1239 |
+
summary = _build_eval_summary(cfg, results, elapsed_seconds)
|
| 1240 |
+
failed_grasp_ids = set(summary["failed_grasp_ids"])
|
| 1241 |
+
grasp_success_by_id = _grasp_success_by_id(results)
|
| 1242 |
+
for affordance in affordance_payload.get("affordances", []):
|
| 1243 |
+
grasp_group = affordance.get("grasp_group")
|
| 1244 |
+
if isinstance(grasp_group, dict):
|
| 1245 |
+
if cfg.debug_mode or cfg.save_failed_grasps:
|
| 1246 |
+
for grasp_id, grasp in grasp_group.items():
|
| 1247 |
+
if (
|
| 1248 |
+
isinstance(grasp, dict)
|
| 1249 |
+
and grasp_id in grasp_success_by_id
|
| 1250 |
+
):
|
| 1251 |
+
grasp["simulation_success"] = grasp_success_by_id[
|
| 1252 |
+
grasp_id
|
| 1253 |
+
]
|
| 1254 |
+
continue
|
| 1255 |
+
for failed_grasp_id in failed_grasp_ids:
|
| 1256 |
+
grasp_group.pop(failed_grasp_id, None)
|
| 1257 |
+
|
| 1258 |
+
write_json(affordance_payload, affordance_path)
|
| 1259 |
+
|
| 1260 |
+
logger.info(f"Grasp evaluation simulation took {elapsed_seconds:.2f}s")
|
| 1261 |
+
logger.info(f"Updated grasp simulation results in {affordance_path}")
|
| 1262 |
+
logger.info(
|
| 1263 |
+
f"Grasp success rate: {summary['num_success']}/"
|
| 1264 |
+
f"{summary['num_grasps']} ({summary['success_rate']:.2%})"
|
| 1265 |
+
)
|
| 1266 |
+
|
| 1267 |
+
|
| 1268 |
+
def _run_single_eval_safe(cfg: EvalGraspsConfig) -> bool:
|
| 1269 |
+
urdf_path = cfg.urdf_paths[0]
|
| 1270 |
+
try:
|
| 1271 |
+
_run_single_eval(cfg)
|
| 1272 |
+
except Exception as exc:
|
| 1273 |
+
logger.error(
|
| 1274 |
+
"Grasp evaluation failed for URDF {}: {}".format(
|
| 1275 |
+
urdf_path,
|
| 1276 |
+
exc,
|
| 1277 |
+
)
|
| 1278 |
+
)
|
| 1279 |
+
return False
|
| 1280 |
+
return True
|
| 1281 |
+
|
| 1282 |
+
|
| 1283 |
+
def run_eval(cfg: EvalGraspsConfig) -> None:
|
| 1284 |
+
_validate_config(cfg)
|
| 1285 |
+
for idx, (urdf_path, output_dir) in enumerate(
|
| 1286 |
+
zip(cfg.urdf_paths, cfg.output_dirs),
|
| 1287 |
+
start=1,
|
| 1288 |
+
):
|
| 1289 |
+
single_cfg = _set_single_config(cfg, urdf_path, output_dir)
|
| 1290 |
+
logger.info(
|
| 1291 |
+
f"Running grasp evaluation for URDF {idx}/{len(cfg.urdf_paths)}: "
|
| 1292 |
+
f"{urdf_path}"
|
| 1293 |
+
)
|
| 1294 |
+
_run_single_eval_safe(single_cfg)
|
| 1295 |
+
|
| 1296 |
+
|
| 1297 |
+
def entrypoint(*args, **kwargs) -> None:
|
| 1298 |
+
cfg = tyro.cli(EvalGraspsConfig)
|
| 1299 |
+
run_eval(cfg)
|
| 1300 |
+
|
| 1301 |
+
|
| 1302 |
+
if __name__ == "__main__":
|
| 1303 |
+
entrypoint()
|
embodied_gen/scripts/affordance_annot/franka_panda/coll_mesh.obj
ADDED
|
@@ -0,0 +1,464 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# https://github.com/mikedh/trimesh
|
| 2 |
+
v -0.09012898 0.01673458 0.06443186 0.00000000 1.00000000 0.00000000
|
| 3 |
+
v 0.08927116 0.01713458 0.06361439 0.00000000 1.00000000 0.00000000
|
| 4 |
+
v 0.00055840 0.03158979 0.00621423 0.00000000 1.00000000 0.00000000
|
| 5 |
+
v 0.09552484 -0.01705332 0.02212120 0.00000000 1.00000000 0.00000000
|
| 6 |
+
v 0.09790768 -0.01751002 0.05166616 0.00000000 1.00000000 0.00000000
|
| 7 |
+
v 0.00198662 -0.03159750 0.00115293 0.00000000 1.00000000 0.00000000
|
| 8 |
+
v -0.07576962 -0.02598126 -0.00508223 0.00000000 1.00000000 0.00000000
|
| 9 |
+
v 0.00057885 -0.03163592 0.00597307 0.00000000 1.00000000 0.00000000
|
| 10 |
+
v 0.09680219 0.01703352 0.03032204 0.00000000 1.00000000 0.00000000
|
| 11 |
+
v 0.00186074 0.03161579 0.00117971 0.00000000 1.00000000 0.00000000
|
| 12 |
+
v 0.09740699 0.01758546 0.05198465 0.00000000 1.00000000 0.00000000
|
| 13 |
+
v -0.09460201 0.01855551 0.05661729 0.00000000 1.00000000 0.00000000
|
| 14 |
+
v 0.09190618 -0.01680383 0.00629548 0.00000000 1.00000000 0.00000000
|
| 15 |
+
v 0.08753296 -0.01681192 -0.00120645 0.00000000 1.00000000 0.00000000
|
| 16 |
+
v 0.09953746 0.01623047 0.05158409 0.00000000 1.00000000 0.00000000
|
| 17 |
+
v 0.10398972 -0.00004070 0.00569088 0.00000000 1.00000000 0.00000000
|
| 18 |
+
v 0.10374270 -0.00001689 -0.00919930 0.00000000 1.00000000 0.00000000
|
| 19 |
+
v 0.10115783 -0.00361640 -0.01261032 0.00000000 1.00000000 0.00000000
|
| 20 |
+
v 0.10259786 -0.00342918 -0.00896430 0.00000000 1.00000000 0.00000000
|
| 21 |
+
v -0.08206502 -0.02527066 0.00519704 0.00000000 1.00000000 0.00000000
|
| 22 |
+
v -0.04178420 -0.02801102 -0.00639640 0.00000000 1.00000000 0.00000000
|
| 23 |
+
v -0.09280849 -0.00011291 0.06596215 0.00000000 1.00000000 0.00000000
|
| 24 |
+
v -0.09371245 -0.01256706 0.06516847 0.00000000 1.00000000 0.00000000
|
| 25 |
+
v -0.09127641 -0.01442229 0.06568974 0.00000000 1.00000000 0.00000000
|
| 26 |
+
v 0.09070038 -0.01725433 0.06308201 0.00000000 1.00000000 0.00000000
|
| 27 |
+
v 0.09070638 -0.01547137 0.06519010 0.00000000 1.00000000 0.00000000
|
| 28 |
+
v -0.09025214 -0.01676757 0.06433220 0.00000000 1.00000000 0.00000000
|
| 29 |
+
v 0.09160285 0.01695586 0.00556283 0.00000000 1.00000000 0.00000000
|
| 30 |
+
v 0.09384751 0.01760751 0.05844849 0.00000000 1.00000000 0.00000000
|
| 31 |
+
v 0.10018747 0.01250605 0.05689415 0.00000000 1.00000000 0.00000000
|
| 32 |
+
v -0.10019985 0.01555056 0.05467260 0.00000000 1.00000000 0.00000000
|
| 33 |
+
v -0.09849364 0.01926846 0.04494174 0.00000000 1.00000000 0.00000000
|
| 34 |
+
v -0.10028528 0.01621802 0.04586977 0.00000000 1.00000000 0.00000000
|
| 35 |
+
v 0.04551993 0.00191760 -0.02565943 0.00000000 1.00000000 0.00000000
|
| 36 |
+
v -0.04204245 0.02801493 -0.00634960 0.00000000 1.00000000 0.00000000
|
| 37 |
+
v 0.08296617 0.00381064 -0.02444934 0.00000000 1.00000000 0.00000000
|
| 38 |
+
v -0.09369192 0.02067053 0.04330829 0.00000000 1.00000000 0.00000000
|
| 39 |
+
v -0.08118060 0.02544764 0.00350060 0.00000000 1.00000000 0.00000000
|
| 40 |
+
v 0.08358592 -0.00363651 -0.02460626 0.00000000 1.00000000 0.00000000
|
| 41 |
+
v 0.08223634 -0.01653239 -0.00511811 0.00000000 1.00000000 0.00000000
|
| 42 |
+
v 0.10009117 -0.01401675 0.05602695 0.00000000 1.00000000 0.00000000
|
| 43 |
+
v 0.09962869 -0.01601276 0.04930143 0.00000000 1.00000000 0.00000000
|
| 44 |
+
v 0.08776600 -0.00321477 -0.02388439 0.00000000 1.00000000 0.00000000
|
| 45 |
+
v 0.09249580 -0.01298759 0.06571195 0.00000000 1.00000000 0.00000000
|
| 46 |
+
v 0.09294532 -0.00050530 0.06595311 0.00000000 1.00000000 0.00000000
|
| 47 |
+
v -0.09267219 0.01353924 0.06568788 0.00000000 1.00000000 0.00000000
|
| 48 |
+
v 0.09192696 0.01342515 0.06573131 0.00000000 1.00000000 0.00000000
|
| 49 |
+
v -0.09726050 -0.01922958 0.05058843 0.00000000 1.00000000 0.00000000
|
| 50 |
+
v 0.08513211 0.01655853 -0.00387274 0.00000000 1.00000000 0.00000000
|
| 51 |
+
v 0.10271563 0.00349515 -0.00802934 0.00000000 1.00000000 0.00000000
|
| 52 |
+
v -0.09040149 0.02386882 0.00436824 0.00000000 1.00000000 0.00000000
|
| 53 |
+
v -0.09373564 0.02270746 0.01434002 0.00000000 1.00000000 0.00000000
|
| 54 |
+
v -0.07529767 0.02603403 -0.00503085 0.00000000 1.00000000 0.00000000
|
| 55 |
+
v -0.07437764 0.02481648 -0.00715570 0.00000000 1.00000000 0.00000000
|
| 56 |
+
v 0.04586986 -0.00149998 -0.02592480 0.00000000 1.00000000 0.00000000
|
| 57 |
+
v -0.08025997 -0.00083407 -0.00732172 0.00000000 1.00000000 0.00000000
|
| 58 |
+
v -0.07790146 0.02295881 -0.00739904 0.00000000 1.00000000 0.00000000
|
| 59 |
+
v -0.08017428 -0.02276142 -0.00690954 0.00000000 1.00000000 0.00000000
|
| 60 |
+
v 0.09945143 0.00003218 -0.01716121 0.00000000 1.00000000 0.00000000
|
| 61 |
+
v 0.09855405 -0.01663178 0.05473394 0.00000000 1.00000000 0.00000000
|
| 62 |
+
v 0.10119467 0.00360250 -0.01262764 0.00000000 1.00000000 0.00000000
|
| 63 |
+
v -0.09747105 -0.00161709 0.06230734 0.00000000 1.00000000 0.00000000
|
| 64 |
+
v 0.09177952 0.01571872 0.06491254 0.00000000 1.00000000 0.00000000
|
| 65 |
+
v -0.09201546 -0.01844902 0.05839317 0.00000000 1.00000000 0.00000000
|
| 66 |
+
v 0.08797474 -0.00062667 -0.02496160 0.00000000 1.00000000 0.00000000
|
| 67 |
+
v 0.08797288 0.00307632 -0.02397873 0.00000000 1.00000000 0.00000000
|
| 68 |
+
v -0.09701113 0.01980169 0.02091398 0.00000000 1.00000000 0.00000000
|
| 69 |
+
v -0.10003477 0.00994118 0.05834080 0.00000000 1.00000000 0.00000000
|
| 70 |
+
v -0.09906728 0.01712909 0.05430516 0.00000000 1.00000000 0.00000000
|
| 71 |
+
v -0.09461535 -0.00031448 0.00757466 0.00000000 1.00000000 0.00000000
|
| 72 |
+
v -0.09849606 -0.00001631 0.02708179 0.00000000 1.00000000 0.00000000
|
| 73 |
+
v -0.08656162 -0.00033764 -0.00416328 0.00000000 1.00000000 0.00000000
|
| 74 |
+
v -0.08825006 0.02231396 -0.00193320 0.00000000 1.00000000 0.00000000
|
| 75 |
+
v -0.08335528 0.02237667 -0.00552611 0.00000000 1.00000000 0.00000000
|
| 76 |
+
v -0.09092682 -0.02132529 0.00097980 0.00000000 1.00000000 0.00000000
|
| 77 |
+
v -0.08543439 -0.02273309 -0.00412033 0.00000000 1.00000000 0.00000000
|
| 78 |
+
v -0.08589226 -0.02449557 -0.00183273 0.00000000 1.00000000 0.00000000
|
| 79 |
+
v 0.08400230 0.00184310 -0.02552490 0.00000000 1.00000000 0.00000000
|
| 80 |
+
v -0.07666791 -0.02465053 -0.00693213 0.00000000 1.00000000 0.00000000
|
| 81 |
+
v 0.09406284 0.01313908 0.06468318 0.00000000 1.00000000 0.00000000
|
| 82 |
+
v 0.09998268 -0.00932350 0.05848920 0.00000000 1.00000000 0.00000000
|
| 83 |
+
v -0.09769707 -0.01906715 0.02462748 0.00000000 1.00000000 0.00000000
|
| 84 |
+
v -0.10042586 -0.00449476 0.05811909 0.00000000 1.00000000 0.00000000
|
| 85 |
+
v -0.10016816 -0.01462347 0.05573492 0.00000000 1.00000000 0.00000000
|
| 86 |
+
v 0.09941621 0.01580527 0.05497027 0.00000000 1.00000000 0.00000000
|
| 87 |
+
v -0.09433321 0.02134224 0.00894201 0.00000000 1.00000000 0.00000000
|
| 88 |
+
v -0.09099504 -0.00017374 0.00057678 0.00000000 1.00000000 0.00000000
|
| 89 |
+
v -0.09179476 0.02148257 0.00268413 0.00000000 1.00000000 0.00000000
|
| 90 |
+
v -0.08170268 0.02451487 -0.00505142 0.00000000 1.00000000 0.00000000
|
| 91 |
+
v -0.08574020 0.02472907 -0.00123169 0.00000000 1.00000000 0.00000000
|
| 92 |
+
v -0.09466784 -0.02065799 0.00953798 0.00000000 1.00000000 0.00000000
|
| 93 |
+
v 0.10030148 0.00119096 0.05862313 0.00000000 1.00000000 0.00000000
|
| 94 |
+
v 0.09665588 0.00046996 0.06307836 0.00000000 1.00000000 0.00000000
|
| 95 |
+
v -0.09290565 0.01539656 0.06455627 0.00000000 1.00000000 0.00000000
|
| 96 |
+
v 0.08254847 -0.00086753 -0.02587020 0.00000000 1.00000000 0.00000000
|
| 97 |
+
v -0.09116623 -0.02355612 0.00460792 0.00000000 1.00000000 0.00000000
|
| 98 |
+
v 0.09344175 -0.01461668 0.06450005 0.00000000 1.00000000 0.00000000
|
| 99 |
+
v -0.09289678 -0.02302643 0.00987686 0.00000000 1.00000000 0.00000000
|
| 100 |
+
v -0.09544163 -0.02182249 0.02175636 0.00000000 1.00000000 0.00000000
|
| 101 |
+
v -0.09951787 -0.01819300 0.04381417 0.00000000 1.00000000 0.00000000
|
| 102 |
+
v -0.09320920 -0.01604587 0.06361687 0.00000000 1.00000000 0.00000000
|
| 103 |
+
v -0.09936280 -0.01699521 0.05411479 0.00000000 1.00000000 0.00000000
|
| 104 |
+
v 0.06600004 -0.01099976 0.05940000 0.00000000 1.00000000 0.00000000
|
| 105 |
+
v 0.06600004 -0.01099976 0.07940000 0.00000000 1.00000000 0.00000000
|
| 106 |
+
v 0.05100004 -0.01099981 0.05940000 0.00000000 1.00000000 0.00000000
|
| 107 |
+
v 0.05100004 -0.01099981 0.07940000 0.00000000 1.00000000 0.00000000
|
| 108 |
+
v 0.06599996 0.01100024 0.05940000 0.00000000 1.00000000 0.00000000
|
| 109 |
+
v 0.06599996 0.01100024 0.07940000 0.00000000 1.00000000 0.00000000
|
| 110 |
+
v 0.05099996 0.01100019 0.05940000 0.00000000 1.00000000 0.00000000
|
| 111 |
+
v 0.05099996 0.01100019 0.07940000 0.00000000 1.00000000 0.00000000
|
| 112 |
+
v 0.05120004 -0.01099981 0.05870000 0.00000000 1.00000000 0.00000000
|
| 113 |
+
v 0.05120004 -0.01099981 0.06250000 0.00000000 1.00000000 0.00000000
|
| 114 |
+
v 0.04240004 -0.01099984 0.05870000 0.00000000 1.00000000 0.00000000
|
| 115 |
+
v 0.04240004 -0.01099984 0.06250000 0.00000000 1.00000000 0.00000000
|
| 116 |
+
v 0.05119996 0.01100019 0.05870000 0.00000000 1.00000000 0.00000000
|
| 117 |
+
v 0.05119996 0.01100019 0.06250000 0.00000000 1.00000000 0.00000000
|
| 118 |
+
v 0.04239996 0.01100016 0.05870000 0.00000000 1.00000000 0.00000000
|
| 119 |
+
v 0.04239996 0.01100016 0.06250000 0.00000000 1.00000000 0.00000000
|
| 120 |
+
v 0.06480612 -0.00874976 0.07832420 0.00000000 1.00000000 0.00000000
|
| 121 |
+
v 0.05305612 -0.00874981 0.09867580 0.00000000 1.00000000 0.00000000
|
| 122 |
+
v 0.05874394 -0.00874978 0.07482420 0.00000000 1.00000000 0.00000000
|
| 123 |
+
v 0.04699394 -0.00874983 0.09517580 0.00000000 1.00000000 0.00000000
|
| 124 |
+
v 0.06480606 0.00875024 0.07832420 0.00000000 1.00000000 0.00000000
|
| 125 |
+
v 0.05305606 0.00875019 0.09867580 0.00000000 1.00000000 0.00000000
|
| 126 |
+
v 0.05874388 0.00875022 0.07482420 0.00000000 1.00000000 0.00000000
|
| 127 |
+
v 0.04699388 0.00875017 0.09517580 0.00000000 1.00000000 0.00000000
|
| 128 |
+
v 0.05518003 -0.00874980 0.09440000 0.00000000 1.00000000 0.00000000
|
| 129 |
+
v 0.05518003 -0.00874980 0.11290000 0.00000000 1.00000000 0.00000000
|
| 130 |
+
v 0.03998003 -0.00874985 0.09440000 0.00000000 1.00000000 0.00000000
|
| 131 |
+
v 0.03998003 -0.00874985 0.11290000 0.00000000 1.00000000 0.00000000
|
| 132 |
+
v 0.05517997 0.00875020 0.09440000 0.00000000 1.00000000 0.00000000
|
| 133 |
+
v 0.05517997 0.00875020 0.11290000 0.00000000 1.00000000 0.00000000
|
| 134 |
+
v 0.03997997 0.00875015 0.09440000 0.00000000 1.00000000 0.00000000
|
| 135 |
+
v 0.03997997 0.00875015 0.11290000 0.00000000 1.00000000 0.00000000
|
| 136 |
+
v -0.05099996 -0.01100019 0.05940000 0.00000000 1.00000000 0.00000000
|
| 137 |
+
v -0.05099996 -0.01100019 0.07940000 0.00000000 1.00000000 0.00000000
|
| 138 |
+
v -0.06599996 -0.01100024 0.05940000 0.00000000 1.00000000 0.00000000
|
| 139 |
+
v -0.06599996 -0.01100024 0.07940000 0.00000000 1.00000000 0.00000000
|
| 140 |
+
v -0.05100004 0.01099981 0.05940000 0.00000000 1.00000000 0.00000000
|
| 141 |
+
v -0.05100004 0.01099981 0.07940000 0.00000000 1.00000000 0.00000000
|
| 142 |
+
v -0.06600004 0.01099976 0.05940000 0.00000000 1.00000000 0.00000000
|
| 143 |
+
v -0.06600004 0.01099976 0.07940000 0.00000000 1.00000000 0.00000000
|
| 144 |
+
v -0.04239996 -0.01100016 0.05870000 0.00000000 1.00000000 0.00000000
|
| 145 |
+
v -0.04239996 -0.01100016 0.06250000 0.00000000 1.00000000 0.00000000
|
| 146 |
+
v -0.05119996 -0.01100019 0.05870000 0.00000000 1.00000000 0.00000000
|
| 147 |
+
v -0.05119996 -0.01100019 0.06250000 0.00000000 1.00000000 0.00000000
|
| 148 |
+
v -0.04240004 0.01099984 0.05870000 0.00000000 1.00000000 0.00000000
|
| 149 |
+
v -0.04240004 0.01099984 0.06250000 0.00000000 1.00000000 0.00000000
|
| 150 |
+
v -0.05120004 0.01099981 0.05870000 0.00000000 1.00000000 0.00000000
|
| 151 |
+
v -0.05120004 0.01099981 0.06250000 0.00000000 1.00000000 0.00000000
|
| 152 |
+
v -0.05874388 -0.00875022 0.07482420 0.00000000 1.00000000 0.00000000
|
| 153 |
+
v -0.04699388 -0.00875017 0.09517580 0.00000000 1.00000000 0.00000000
|
| 154 |
+
v -0.06480606 -0.00875024 0.07832420 0.00000000 1.00000000 0.00000000
|
| 155 |
+
v -0.05305606 -0.00875019 0.09867580 0.00000000 1.00000000 0.00000000
|
| 156 |
+
v -0.05874394 0.00874978 0.07482420 0.00000000 1.00000000 0.00000000
|
| 157 |
+
v -0.04699394 0.00874983 0.09517580 0.00000000 1.00000000 0.00000000
|
| 158 |
+
v -0.06480612 0.00874976 0.07832420 0.00000000 1.00000000 0.00000000
|
| 159 |
+
v -0.05305612 0.00874981 0.09867580 0.00000000 1.00000000 0.00000000
|
| 160 |
+
v -0.03997997 -0.00875015 0.09440000 0.00000000 1.00000000 0.00000000
|
| 161 |
+
v -0.03997997 -0.00875015 0.11290000 0.00000000 1.00000000 0.00000000
|
| 162 |
+
v -0.05517997 -0.00875020 0.09440000 0.00000000 1.00000000 0.00000000
|
| 163 |
+
v -0.05517997 -0.00875020 0.11290000 0.00000000 1.00000000 0.00000000
|
| 164 |
+
v -0.03998003 0.00874985 0.09440000 0.00000000 1.00000000 0.00000000
|
| 165 |
+
v -0.03998003 0.00874985 0.11290000 0.00000000 1.00000000 0.00000000
|
| 166 |
+
v -0.05518003 0.00874980 0.09440000 0.00000000 1.00000000 0.00000000
|
| 167 |
+
v -0.05518003 0.00874980 0.11290000 0.00000000 1.00000000 0.00000000
|
| 168 |
+
f 1 2 3
|
| 169 |
+
f 4 5 6
|
| 170 |
+
f 7 6 8
|
| 171 |
+
f 9 10 11
|
| 172 |
+
f 10 3 11
|
| 173 |
+
f 12 1 3
|
| 174 |
+
f 13 6 14
|
| 175 |
+
f 8 6 5
|
| 176 |
+
f 4 6 13
|
| 177 |
+
f 15 16 17
|
| 178 |
+
f 18 19 13
|
| 179 |
+
f 20 7 8
|
| 180 |
+
f 21 6 7
|
| 181 |
+
f 22 23 24
|
| 182 |
+
f 25 26 27
|
| 183 |
+
f 28 10 9
|
| 184 |
+
f 2 29 3
|
| 185 |
+
f 29 11 3
|
| 186 |
+
f 30 16 15
|
| 187 |
+
f 31 32 33
|
| 188 |
+
f 34 35 36
|
| 189 |
+
f 37 3 38
|
| 190 |
+
f 6 39 40
|
| 191 |
+
f 25 8 5
|
| 192 |
+
f 41 42 16
|
| 193 |
+
f 18 14 43
|
| 194 |
+
f 22 24 44
|
| 195 |
+
f 44 45 22
|
| 196 |
+
f 46 22 45
|
| 197 |
+
f 45 47 46
|
| 198 |
+
f 20 8 48
|
| 199 |
+
f 9 11 15
|
| 200 |
+
f 36 10 49
|
| 201 |
+
f 50 28 9
|
| 202 |
+
f 10 36 35
|
| 203 |
+
f 12 37 32
|
| 204 |
+
f 37 12 3
|
| 205 |
+
f 37 38 51
|
| 206 |
+
f 37 52 32
|
| 207 |
+
f 52 37 51
|
| 208 |
+
f 10 35 53
|
| 209 |
+
f 10 53 38
|
| 210 |
+
f 10 38 3
|
| 211 |
+
f 54 35 34
|
| 212 |
+
f 55 56 57
|
| 213 |
+
f 56 55 58
|
| 214 |
+
f 57 54 34
|
| 215 |
+
f 39 6 21
|
| 216 |
+
f 59 18 43
|
| 217 |
+
f 40 39 43
|
| 218 |
+
f 14 40 43
|
| 219 |
+
f 14 6 40
|
| 220 |
+
f 60 25 5
|
| 221 |
+
f 59 61 17
|
| 222 |
+
f 42 60 5
|
| 223 |
+
f 16 42 17
|
| 224 |
+
f 62 22 46
|
| 225 |
+
f 22 62 23
|
| 226 |
+
f 1 46 63
|
| 227 |
+
f 47 63 46
|
| 228 |
+
f 25 64 8
|
| 229 |
+
f 64 25 27
|
| 230 |
+
f 64 48 8
|
| 231 |
+
f 65 59 43
|
| 232 |
+
f 66 36 49
|
| 233 |
+
f 63 2 1
|
| 234 |
+
f 33 32 67
|
| 235 |
+
f 68 62 46
|
| 236 |
+
f 32 69 12
|
| 237 |
+
f 70 71 67
|
| 238 |
+
f 53 35 54
|
| 239 |
+
f 72 73 74
|
| 240 |
+
f 75 76 77
|
| 241 |
+
f 78 55 34
|
| 242 |
+
f 55 79 58
|
| 243 |
+
f 4 42 5
|
| 244 |
+
f 50 15 17
|
| 245 |
+
f 59 17 18
|
| 246 |
+
f 7 20 77
|
| 247 |
+
f 24 26 44
|
| 248 |
+
f 27 26 24
|
| 249 |
+
f 45 80 47
|
| 250 |
+
f 41 16 81
|
| 251 |
+
f 82 71 70
|
| 252 |
+
f 62 83 23
|
| 253 |
+
f 33 84 83
|
| 254 |
+
f 49 28 61
|
| 255 |
+
f 61 28 50
|
| 256 |
+
f 61 50 17
|
| 257 |
+
f 63 85 29
|
| 258 |
+
f 85 30 15
|
| 259 |
+
f 80 85 63
|
| 260 |
+
f 85 80 30
|
| 261 |
+
f 9 15 50
|
| 262 |
+
f 85 15 11
|
| 263 |
+
f 11 29 85
|
| 264 |
+
f 83 62 68
|
| 265 |
+
f 34 36 78
|
| 266 |
+
f 70 67 86
|
| 267 |
+
f 71 33 67
|
| 268 |
+
f 58 76 72
|
| 269 |
+
f 73 87 88
|
| 270 |
+
f 72 74 56
|
| 271 |
+
f 56 74 57
|
| 272 |
+
f 34 55 57
|
| 273 |
+
f 21 79 55
|
| 274 |
+
f 21 7 79
|
| 275 |
+
f 89 73 90
|
| 276 |
+
f 39 21 55
|
| 277 |
+
f 91 82 70
|
| 278 |
+
f 84 23 83
|
| 279 |
+
f 28 49 10
|
| 280 |
+
f 92 80 93
|
| 281 |
+
f 31 33 68
|
| 282 |
+
f 68 33 83
|
| 283 |
+
f 51 86 52
|
| 284 |
+
f 69 32 31
|
| 285 |
+
f 46 94 68
|
| 286 |
+
f 94 31 68
|
| 287 |
+
f 94 12 69
|
| 288 |
+
f 76 75 72
|
| 289 |
+
f 90 53 89
|
| 290 |
+
f 86 51 88
|
| 291 |
+
f 75 70 87
|
| 292 |
+
f 87 70 88
|
| 293 |
+
f 95 55 78
|
| 294 |
+
f 39 55 95
|
| 295 |
+
f 14 18 13
|
| 296 |
+
f 42 41 60
|
| 297 |
+
f 19 17 42
|
| 298 |
+
f 19 4 13
|
| 299 |
+
f 45 93 80
|
| 300 |
+
f 92 93 81
|
| 301 |
+
f 16 30 92
|
| 302 |
+
f 92 81 16
|
| 303 |
+
f 96 77 20
|
| 304 |
+
f 66 59 65
|
| 305 |
+
f 65 78 66
|
| 306 |
+
f 59 66 61
|
| 307 |
+
f 49 61 66
|
| 308 |
+
f 2 63 29
|
| 309 |
+
f 80 63 47
|
| 310 |
+
f 80 92 30
|
| 311 |
+
f 78 36 66
|
| 312 |
+
f 94 69 31
|
| 313 |
+
f 94 46 1
|
| 314 |
+
f 12 94 1
|
| 315 |
+
f 70 86 88
|
| 316 |
+
f 67 52 86
|
| 317 |
+
f 52 67 32
|
| 318 |
+
f 57 89 54
|
| 319 |
+
f 53 90 38
|
| 320 |
+
f 90 51 38
|
| 321 |
+
f 75 87 72
|
| 322 |
+
f 72 87 73
|
| 323 |
+
f 56 58 72
|
| 324 |
+
f 42 4 19
|
| 325 |
+
f 19 18 17
|
| 326 |
+
f 97 81 44
|
| 327 |
+
f 81 93 44
|
| 328 |
+
f 41 97 60
|
| 329 |
+
f 26 97 44
|
| 330 |
+
f 98 99 91
|
| 331 |
+
f 100 33 71
|
| 332 |
+
f 84 33 100
|
| 333 |
+
f 96 75 77
|
| 334 |
+
f 95 78 65
|
| 335 |
+
f 73 89 74
|
| 336 |
+
f 53 54 89
|
| 337 |
+
f 73 88 51
|
| 338 |
+
f 90 73 51
|
| 339 |
+
f 41 81 97
|
| 340 |
+
f 44 93 45
|
| 341 |
+
f 25 97 26
|
| 342 |
+
f 101 48 64
|
| 343 |
+
f 64 27 101
|
| 344 |
+
f 102 48 101
|
| 345 |
+
f 96 98 91
|
| 346 |
+
f 48 99 20
|
| 347 |
+
f 99 98 20
|
| 348 |
+
f 98 96 20
|
| 349 |
+
f 96 91 75
|
| 350 |
+
f 84 101 23
|
| 351 |
+
f 27 24 101
|
| 352 |
+
f 89 57 74
|
| 353 |
+
f 77 76 79
|
| 354 |
+
f 79 76 58
|
| 355 |
+
f 79 7 77
|
| 356 |
+
f 102 84 100
|
| 357 |
+
f 91 99 82
|
| 358 |
+
f 48 100 99
|
| 359 |
+
f 100 48 102
|
| 360 |
+
f 75 91 70
|
| 361 |
+
f 84 102 101
|
| 362 |
+
f 24 23 101
|
| 363 |
+
f 43 95 65
|
| 364 |
+
f 25 60 97
|
| 365 |
+
f 82 100 71
|
| 366 |
+
f 43 39 95
|
| 367 |
+
f 100 82 99
|
| 368 |
+
f 104 106 103
|
| 369 |
+
f 107 104 103
|
| 370 |
+
f 103 106 105
|
| 371 |
+
f 105 107 103
|
| 372 |
+
f 104 110 106
|
| 373 |
+
f 108 104 107
|
| 374 |
+
f 108 110 104
|
| 375 |
+
f 106 110 105
|
| 376 |
+
f 109 107 105
|
| 377 |
+
f 105 110 109
|
| 378 |
+
f 109 108 107
|
| 379 |
+
f 110 108 109
|
| 380 |
+
f 112 114 111
|
| 381 |
+
f 115 112 111
|
| 382 |
+
f 111 114 113
|
| 383 |
+
f 113 115 111
|
| 384 |
+
f 112 118 114
|
| 385 |
+
f 116 112 115
|
| 386 |
+
f 116 118 112
|
| 387 |
+
f 114 118 113
|
| 388 |
+
f 117 115 113
|
| 389 |
+
f 113 118 117
|
| 390 |
+
f 117 116 115
|
| 391 |
+
f 118 116 117
|
| 392 |
+
f 120 122 119
|
| 393 |
+
f 123 120 119
|
| 394 |
+
f 119 122 121
|
| 395 |
+
f 121 123 119
|
| 396 |
+
f 120 126 122
|
| 397 |
+
f 124 120 123
|
| 398 |
+
f 124 126 120
|
| 399 |
+
f 122 126 121
|
| 400 |
+
f 125 123 121
|
| 401 |
+
f 121 126 125
|
| 402 |
+
f 125 124 123
|
| 403 |
+
f 126 124 125
|
| 404 |
+
f 128 130 127
|
| 405 |
+
f 131 128 127
|
| 406 |
+
f 127 130 129
|
| 407 |
+
f 129 131 127
|
| 408 |
+
f 128 134 130
|
| 409 |
+
f 132 128 131
|
| 410 |
+
f 132 134 128
|
| 411 |
+
f 130 134 129
|
| 412 |
+
f 133 131 129
|
| 413 |
+
f 129 134 133
|
| 414 |
+
f 133 132 131
|
| 415 |
+
f 134 132 133
|
| 416 |
+
f 136 138 135
|
| 417 |
+
f 139 136 135
|
| 418 |
+
f 135 138 137
|
| 419 |
+
f 137 139 135
|
| 420 |
+
f 136 142 138
|
| 421 |
+
f 140 136 139
|
| 422 |
+
f 140 142 136
|
| 423 |
+
f 138 142 137
|
| 424 |
+
f 141 139 137
|
| 425 |
+
f 137 142 141
|
| 426 |
+
f 141 140 139
|
| 427 |
+
f 142 140 141
|
| 428 |
+
f 144 146 143
|
| 429 |
+
f 147 144 143
|
| 430 |
+
f 143 146 145
|
| 431 |
+
f 145 147 143
|
| 432 |
+
f 144 150 146
|
| 433 |
+
f 148 144 147
|
| 434 |
+
f 148 150 144
|
| 435 |
+
f 146 150 145
|
| 436 |
+
f 149 147 145
|
| 437 |
+
f 145 150 149
|
| 438 |
+
f 149 148 147
|
| 439 |
+
f 150 148 149
|
| 440 |
+
f 152 154 151
|
| 441 |
+
f 155 152 151
|
| 442 |
+
f 151 154 153
|
| 443 |
+
f 153 155 151
|
| 444 |
+
f 152 158 154
|
| 445 |
+
f 156 152 155
|
| 446 |
+
f 156 158 152
|
| 447 |
+
f 154 158 153
|
| 448 |
+
f 157 155 153
|
| 449 |
+
f 153 158 157
|
| 450 |
+
f 157 156 155
|
| 451 |
+
f 158 156 157
|
| 452 |
+
f 160 162 159
|
| 453 |
+
f 163 160 159
|
| 454 |
+
f 159 162 161
|
| 455 |
+
f 161 163 159
|
| 456 |
+
f 160 166 162
|
| 457 |
+
f 164 160 163
|
| 458 |
+
f 164 166 160
|
| 459 |
+
f 162 166 161
|
| 460 |
+
f 165 163 161
|
| 461 |
+
f 161 166 165
|
| 462 |
+
f 165 164 163
|
| 463 |
+
f 166 164 165
|
| 464 |
+
|
embodied_gen/scripts/affordance_annot/franka_panda/gripper.urdf
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?xml version="1.0" encoding="utf-8"?>
|
| 2 |
+
<!-- =================================================================================== -->
|
| 3 |
+
<!-- | This document was autogenerated by xacro from hand.urdf.xacro | -->
|
| 4 |
+
<!-- | EDITING THIS FILE BY HAND IS NOT RECOMMENDED | -->
|
| 5 |
+
<!-- =================================================================================== -->
|
| 6 |
+
<robot name="hand">
|
| 7 |
+
<link name="world"/>
|
| 8 |
+
<joint name="world_joint" type="fixed">
|
| 9 |
+
<parent link="world"/>
|
| 10 |
+
<child link="panda_hand"/>
|
| 11 |
+
<origin rpy="0 0 1.5708" xyz="0 0 0.0"/>
|
| 12 |
+
</joint>
|
| 13 |
+
<link name="panda_hand">
|
| 14 |
+
<visual>
|
| 15 |
+
<geometry>
|
| 16 |
+
<mesh filename="./meshes/visual/hand.dae"/>
|
| 17 |
+
</geometry>
|
| 18 |
+
</visual>
|
| 19 |
+
<collision>
|
| 20 |
+
<geometry>
|
| 21 |
+
<mesh filename="./meshes/collision/hand.stl"/>
|
| 22 |
+
</geometry>
|
| 23 |
+
</collision>
|
| 24 |
+
</link>
|
| 25 |
+
<!-- Define the hand_tcp frame -->
|
| 26 |
+
<link name="panda_hand_tcp"/>
|
| 27 |
+
<joint name="panda_hand_tcp_joint" type="fixed">
|
| 28 |
+
<origin rpy="0 0 0" xyz="0 0 0.1034"/>
|
| 29 |
+
<parent link="panda_hand"/>
|
| 30 |
+
<child link="panda_hand_tcp"/>
|
| 31 |
+
</joint>
|
| 32 |
+
<link name="panda_leftfinger">
|
| 33 |
+
<visual>
|
| 34 |
+
<geometry>
|
| 35 |
+
<mesh filename="./meshes/visual/finger.dae"/>
|
| 36 |
+
</geometry>
|
| 37 |
+
</visual>
|
| 38 |
+
<!-- screw mount -->
|
| 39 |
+
<collision>
|
| 40 |
+
<origin rpy="0 0 0" xyz="0 18.5e-3 11e-3"/>
|
| 41 |
+
<geometry>
|
| 42 |
+
<box size="22e-3 15e-3 20e-3"/>
|
| 43 |
+
</geometry>
|
| 44 |
+
</collision>
|
| 45 |
+
<!-- cartriage sledge -->
|
| 46 |
+
<collision>
|
| 47 |
+
<origin rpy="0 0 0" xyz="0 6.8e-3 2.2e-3"/>
|
| 48 |
+
<geometry>
|
| 49 |
+
<box size="22e-3 8.8e-3 3.8e-3"/>
|
| 50 |
+
</geometry>
|
| 51 |
+
</collision>
|
| 52 |
+
<!-- diagonal finger -->
|
| 53 |
+
<collision>
|
| 54 |
+
<origin rpy="0.523598775598 0 0" xyz="0 15.9e-3 28.35e-3"/>
|
| 55 |
+
<geometry>
|
| 56 |
+
<box size="17.5e-3 7e-3 23.5e-3"/>
|
| 57 |
+
</geometry>
|
| 58 |
+
</collision>
|
| 59 |
+
<!-- rubber tip with which to grasp -->
|
| 60 |
+
<collision>
|
| 61 |
+
<origin rpy="0 0 0" xyz="0 7.58e-3 45.25e-3"/>
|
| 62 |
+
<geometry>
|
| 63 |
+
<box size="17.5e-3 15.2e-3 18.5e-3"/>
|
| 64 |
+
</geometry>
|
| 65 |
+
</collision>
|
| 66 |
+
</link>
|
| 67 |
+
<link name="panda_rightfinger">
|
| 68 |
+
<visual>
|
| 69 |
+
<origin rpy="0 0 3.14159265359" xyz="0 0 0"/>
|
| 70 |
+
<geometry>
|
| 71 |
+
<mesh filename="./meshes/visual/finger.dae"/>
|
| 72 |
+
</geometry>
|
| 73 |
+
</visual>
|
| 74 |
+
<!-- screw mount -->
|
| 75 |
+
<collision>
|
| 76 |
+
<origin rpy="0 0 0" xyz="0 -18.5e-3 11e-3"/>
|
| 77 |
+
<geometry>
|
| 78 |
+
<box size="22e-3 15e-3 20e-3"/>
|
| 79 |
+
</geometry>
|
| 80 |
+
</collision>
|
| 81 |
+
<!-- cartriage sledge -->
|
| 82 |
+
<collision>
|
| 83 |
+
<origin rpy="0 0 0" xyz="0 -6.8e-3 2.2e-3"/>
|
| 84 |
+
<geometry>
|
| 85 |
+
<box size="22e-3 8.8e-3 3.8e-3"/>
|
| 86 |
+
</geometry>
|
| 87 |
+
</collision>
|
| 88 |
+
<!-- diagonal finger -->
|
| 89 |
+
<collision>
|
| 90 |
+
<origin rpy="-0.523598775598 0 0" xyz="0 -15.9e-3 28.35e-3"/>
|
| 91 |
+
<geometry>
|
| 92 |
+
<box size="17.5e-3 7e-3 23.5e-3"/>
|
| 93 |
+
</geometry>
|
| 94 |
+
</collision>
|
| 95 |
+
<!-- rubber tip with which to grasp -->
|
| 96 |
+
<collision>
|
| 97 |
+
<origin rpy="0 0 0" xyz="0 -7.58e-3 45.25e-3"/>
|
| 98 |
+
<geometry>
|
| 99 |
+
<box size="17.5e-3 15.2e-3 18.5e-3"/>
|
| 100 |
+
</geometry>
|
| 101 |
+
</collision>
|
| 102 |
+
</link>
|
| 103 |
+
<joint name="panda_finger_joint1" type="prismatic">
|
| 104 |
+
<parent link="panda_hand"/>
|
| 105 |
+
<child link="panda_leftfinger"/>
|
| 106 |
+
<origin rpy="0 0 0" xyz="0 0 0.0584"/>
|
| 107 |
+
<axis xyz="0 1 0"/>
|
| 108 |
+
<limit effort="1400" lower="0.0" upper="0.05268743" velocity="0.05"/>
|
| 109 |
+
<dynamics damping="0.3"/>
|
| 110 |
+
</joint>
|
| 111 |
+
<joint name="panda_finger_joint2" type="prismatic">
|
| 112 |
+
<parent link="panda_hand"/>
|
| 113 |
+
<child link="panda_rightfinger"/>
|
| 114 |
+
<origin rpy="0 0 0" xyz="0 0 0.0584"/>
|
| 115 |
+
<axis xyz="0 -1 0"/>
|
| 116 |
+
<limit effort="1400" lower="0.0" upper="0.05268743" velocity="0.05"/>
|
| 117 |
+
<mimic joint="panda_finger_joint1"/>
|
| 118 |
+
<dynamics damping="0.3"/>
|
| 119 |
+
</joint>
|
| 120 |
+
</robot>
|
embodied_gen/scripts/affordance_annot/franka_panda/meshes/collision/hand.stl
ADDED
|
Binary file (10.1 kB). View file
|
|
|
embodied_gen/scripts/affordance_annot/franka_panda/meshes/visual/finger.dae
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?xml version="1.0" encoding="utf-8"?>
|
| 2 |
+
<COLLADA xmlns="http://www.collada.org/2005/11/COLLADASchema" version="1.4.1">
|
| 3 |
+
<asset>
|
| 4 |
+
<contributor>
|
| 5 |
+
<author>Blender User</author>
|
| 6 |
+
<authoring_tool>Blender 2.79.0</authoring_tool>
|
| 7 |
+
</contributor>
|
| 8 |
+
<created>2018-02-20T15:00:35</created>
|
| 9 |
+
<modified>2018-02-20T15:00:35</modified>
|
| 10 |
+
<unit name="meter" meter="1"/>
|
| 11 |
+
<up_axis>Z_UP</up_axis>
|
| 12 |
+
</asset>
|
| 13 |
+
<library_images/>
|
| 14 |
+
<library_effects>
|
| 15 |
+
<effect id="Part__Feature_007-effect">
|
| 16 |
+
<profile_COMMON>
|
| 17 |
+
<technique sid="common">
|
| 18 |
+
<phong>
|
| 19 |
+
<emission>
|
| 20 |
+
<color sid="emission">0 0 0 1</color>
|
| 21 |
+
</emission>
|
| 22 |
+
<ambient>
|
| 23 |
+
<color sid="ambient">0 0 0 1</color>
|
| 24 |
+
</ambient>
|
| 25 |
+
<diffuse>
|
| 26 |
+
<color sid="diffuse">0.2509804 0.2509804 0.2509804 1</color>
|
| 27 |
+
</diffuse>
|
| 28 |
+
<specular>
|
| 29 |
+
<color sid="specular">0.5 0.5 0.5 1</color>
|
| 30 |
+
</specular>
|
| 31 |
+
<shininess>
|
| 32 |
+
<float sid="shininess">0</float>
|
| 33 |
+
</shininess>
|
| 34 |
+
<transparent opaque="A_ONE">
|
| 35 |
+
<color>1 1 1 1</color>
|
| 36 |
+
</transparent>
|
| 37 |
+
<transparency><float>1</float></transparency>
|
| 38 |
+
<index_of_refraction>
|
| 39 |
+
<float sid="index_of_refraction">1</float>
|
| 40 |
+
</index_of_refraction>
|
| 41 |
+
</phong>
|
| 42 |
+
</technique>
|
| 43 |
+
</profile_COMMON>
|
| 44 |
+
</effect>
|
| 45 |
+
<effect id="Part__Feature001_006-effect">
|
| 46 |
+
<profile_COMMON>
|
| 47 |
+
<technique sid="common">
|
| 48 |
+
<phong>
|
| 49 |
+
<emission>
|
| 50 |
+
<color sid="emission">0 0 0 1</color>
|
| 51 |
+
</emission>
|
| 52 |
+
<ambient>
|
| 53 |
+
<color sid="ambient">0 0 0 1</color>
|
| 54 |
+
</ambient>
|
| 55 |
+
<diffuse>
|
| 56 |
+
<color sid="diffuse">0.9019608 0.9215686 0.9294118 1</color>
|
| 57 |
+
</diffuse>
|
| 58 |
+
<specular>
|
| 59 |
+
<color sid="specular">0.5 0.5 0.5 1</color>
|
| 60 |
+
</specular>
|
| 61 |
+
<shininess>
|
| 62 |
+
<float sid="shininess">0</float>
|
| 63 |
+
</shininess>
|
| 64 |
+
<transparent opaque="A_ONE">
|
| 65 |
+
<color>1 1 1 1</color>
|
| 66 |
+
</transparent>
|
| 67 |
+
<transparency><float>1</float></transparency>
|
| 68 |
+
<index_of_refraction>
|
| 69 |
+
<float sid="index_of_refraction">1</float>
|
| 70 |
+
</index_of_refraction>
|
| 71 |
+
</phong>
|
| 72 |
+
</technique>
|
| 73 |
+
</profile_COMMON>
|
| 74 |
+
</effect>
|
| 75 |
+
</library_effects>
|
| 76 |
+
<library_materials>
|
| 77 |
+
<material id="Part__Feature_007-material" name="Part__Feature_007">
|
| 78 |
+
<instance_effect url="#Part__Feature_007-effect"/>
|
| 79 |
+
</material>
|
| 80 |
+
<material id="Part__Feature001_006-material" name="Part__Feature001_006">
|
| 81 |
+
<instance_effect url="#Part__Feature001_006-effect"/>
|
| 82 |
+
</material>
|
| 83 |
+
</library_materials>
|
| 84 |
+
<library_geometries>
|
| 85 |
+
<geometry id="Part__Feature_007-mesh" name="Part__Feature.007">
|
| 86 |
+
<mesh>
|
| 87 |
+
<source id="Part__Feature_007-mesh-positions">
|
| 88 |
+
<float_array id="Part__Feature_007-mesh-positions-array" count="399">0.002999246 0.01089721 0.04464477 3.59715e-4 0.01089966 0.04796832 0.002338767 0.010885 0.04695528 0.00234574 0.01088672 0.04317915 0.001025557 0.01089477 0.04214996 0.001999616 0.01089692 0.04408597 8.26939e-4 0.01089149 0.04701918 0.002051293 0.01086962 0.0458244 9.84775e-4 0.01088815 0.04306632 -0.001819312 0.009437561 0.04387903 -1.12074e-5 0.00943017 0.04711711 -6.97705e-4 0.0094316 0.04301643 -0.001464784 0.009429216 0.0465517 -0.00208646 0.009431064 0.04526871 -1.55725e-4 0.009388387 0.04323405 0.001297712 0.009390771 0.04378831 1.56458e-4 0.009388387 0.04676586 0.001741111 0.009388566 0.04558634 -0.002886593 0.009435176 0.04399472 -0.001784682 0.009434103 0.0424965 5.2098e-4 0.009434282 0.04803037 -0.001791715 0.009433507 0.04755115 1.48399e-5 0.009437739 0.04194247 -0.002975225 0.009436786 0.04570305 -0.008930683 0.009314417 0.03609037 0.008931338 0.009313642 0.03609037 -8.60488e-4 0.001913011 0.04356497 -0.001740396 0.009388566 0.04441362 -0.00152558 0.001912534 0.04591751 -0.001296997 0.009390771 0.0462116 -9.70155e-4 0.010885 0.04216766 0.004843115 -1.70612e-5 0.05371719 0.008756995 -2.97773e-5 0.04727178 0.008758902 -2.87572e-5 0.04272794 -0.002268254 -3.64761e-5 0.05374616 -0.008709371 -1.03085e-5 0.04993945 -0.00875771 -2.8976e-5 0.0472719 -0.002273738 -3.68374e-5 0.03624957 -0.002243995 -7.46945e-5 0.0427556 -0.008758783 -3.04484e-5 0.04272389 0.002263069 -8.37643e-5 0.04726266 0.00226897 -3.64764e-5 0.05374616 3.65234e-7 0.001558899 0.05380403 3.6521e-7 0.001488626 0.04499995 3.65614e-7 0.001561164 0.03619641 0.002270221 -3.43831e-5 0.0362488 0.002248227 -7.64219e-5 0.04275208 -0.002262294 -8.37643e-5 0.04726266 -0.005691945 1.10341e-5 0.05360394 0.00686866 0.009352147 0.05310285 -0.007860779 0.009355306 0.05219143 0.003445267 0.009386718 0.04705679 0.008785963 0.009348154 0.0505712 0.003923535 0.009389936 0.04573357 -7.30475e-4 0.009391903 0.04108011 0.003038406 0.009433627 0.04465281 7.34002e-4 0.009389936 0.04107677 0.002057075 0.009386718 0.04155498 0.004730045 0.009351372 0.05389988 -7.33049e-4 0.009390354 0.04892301 -0.00205636 0.009386718 0.04844492 0.007075071 -1.88821e-5 0.05276924 0.008674919 -6.99964e-6 0.05031853 0.007492721 0.009395003 0.0442506 0.007331073 0.001441657 0.04408031 0.004115462 0.00143361 0.04404103 9.13617e-4 0.001440465 0.0409246 0.002675354 0.001427888 0.04171341 -9.16098e-4 0.001442193 0.03767275 -0.004073202 0.001442134 0.04408985 -0.003944575 0.009400427 0.04425376 -0.002707123 0.009383678 0.04193902 -0.003045201 0.001419723 0.04195654 -0.007492482 0.00939387 0.03750175 -9.09494e-4 0.001442074 0.04092669 -0.004972159 0.009392738 0.0524379 -0.007140517 0.001431167 0.05044227 -0.007258474 0.009387791 0.05068916 -0.007491827 0.009394824 0.04574936 -0.004114806 0.001433432 0.04595905 -9.13103e-4 0.001440107 0.04907548 -0.002675294 0.001426517 0.04828691 -9.16344e-4 0.001443028 0.05232822 -0.005090415 0.001432299 0.05222845 0.007365286 0.009393692 0.05032676 0.004924833 0.001437306 0.05226349 0.00534445 0.009385824 0.0523557 9.15562e-4 0.001443564 0.05232697 7.49809e-4 0.009395062 0.05249238 9.59386e-4 0.001433491 0.04911512 0.004075884 0.001440107 0.04591345 0.003287315 0.001426517 0.04767566 0.007309973 0.001435518 0.0495814 0.006608247 0.00141263 0.05116909 -0.001967191 0.0108897 0.04407691 -0.002455234 0.0108962 0.04326856 -7.23478e-4 0.01089477 0.04287755 0.002154886 0.009418427 0.04456418 7.21602e-4 0.009433567 0.04298967 0.001448452 0.009429216 0.04656761 8.61216e-4 0.00191313 0.04643499 0.001526296 0.001912474 0.0440824 -0.007818162 -3.97311e-5 0.05191779 -0.008771955 2.75426e-5 0.03626441 -0.008893072 0.009342074 0.04989963 0.008762598 1.08415e-5 0.03626585 -0.005339801 0.009355187 0.05381947 -0.007331967 0.00144273 0.04407978 -0.007496535 0.009394824 0.04424685 -0.007332861 0.001443088 0.03767186 -7.55759e-4 0.009394407 0.03750163 0.00254625 0.009436607 0.04679846 0.002077639 0.009434342 0.04267549 0.007333219 0.001442193 0.03767275 0.007498383 0.00939387 0.03750687 9.16392e-4 0.001442492 0.03767257 0.003923356 0.009390354 0.04426658 7.54132e-4 0.009395062 0.03750258 0.003375649 0.00939083 0.04281467 -0.007332205 0.001443505 0.04591983 -0.003922641 0.009390354 0.04573339 -7.53554e-4 0.009394705 0.05249726 -0.003374874 0.00939083 0.0471853 0.007332324 0.001442849 0.04592007 7.34002e-4 0.009389936 0.04892319 0.007497489 0.009394824 0.04575371 0.002185642 0.00939083 0.0483753 -0.002153158 0.01088654 0.0454204 -0.001070141 0.01088356 0.04698097 -0.003009378 0.01089417 0.04527872 -0.00187093 0.01090669 0.04741126 -0.008804023 0.001558899 0.04499995 0.008809328 0.001564621 0.04499995</float_array>
|
| 89 |
+
<technique_common>
|
| 90 |
+
<accessor source="#Part__Feature_007-mesh-positions-array" count="133" stride="3">
|
| 91 |
+
<param name="X" type="float"/>
|
| 92 |
+
<param name="Y" type="float"/>
|
| 93 |
+
<param name="Z" type="float"/>
|
| 94 |
+
</accessor>
|
| 95 |
+
</technique_common>
|
| 96 |
+
</source>
|
| 97 |
+
<source id="Part__Feature_007-mesh-normals">
|
| 98 |
+
<float_array id="Part__Feature_007-mesh-normals-array" count="786">0.9590276 0.08210206 0.2711559 0.9707201 -0.02025878 -0.2393575 0.356231 0.02584302 -0.9340405 0.2114398 0.06310987 -0.9753515 0.5969651 -0.02077841 0.8019981 0.8966066 0.08637297 -0.4343227 0.8202709 -0.05451494 -0.5693714 0.01862794 0.09478819 0.9953232 0.3569161 0.1740584 -0.917777 0.6076866 0.08563488 0.7895466 0.6939369 -0.0273537 0.7195162 0.9792594 0.07404083 0.1885975 0.9900662 0.03109622 0.1371198 -0.9979203 -0.0384593 -0.05173081 -0.9530383 0.1508886 -0.2625846 -0.2413043 0.07884323 0.9672415 -0.2028678 0.01909875 0.9790199 -0.8043072 -0.05650782 -0.5915207 -0.5880531 0.1500057 -0.7947906 -0.2937553 -0.05141419 -0.954497 -0.009515166 0.1474139 -0.9890291 -0.8421047 0.005896389 0.539282 -0.8765521 0.1154791 0.4672484 0.007623791 -0.9999444 0.00728625 0.003054559 -0.9999539 0.009112834 0.01089298 -0.9999382 0.002262771 0.008302092 -0.9999359 0.007699072 0.007297933 -0.9999547 -0.006123363 0.006982326 -0.9999549 -0.006440281 -0.0141775 -0.9998731 0.007279753 0.008410274 -0.9997746 0.01949483 -0.01479846 -0.999857 -0.008204162 -0.008424222 -0.999939 0.007150232 -0.006763219 -0.9999605 -0.005787611 -0.009926855 -0.9999107 -0.008956611 0.999624 0.005454719 0.02687352 0.9996895 -0.0124461 0.0215857 0.9999408 -0.004187285 0.01004832 0.9997202 -0.02283918 -0.006177306 0.9998357 -0.01812744 4.58244e-4 -0.9998302 -0.003178 0.01815414 -0.9997246 -0.01958203 0.01294118 -0.9997529 -0.02172666 -0.004701018 -0.9998528 -0.01705491 0.001882493 -0.9999096 -0.01316875 0.002750456 -0.5751125 -0.8180485 0.006493091 -0.5750488 -0.8180931 0.006525993 -0.5749478 -0.8181582 -0.007234573 -0.5759127 -0.8174836 -0.006735205 0.575049 -0.8180931 0.006525993 0.5751125 -0.8180485 0.006493151 0.5760833 -0.8173633 -0.006734192 0.5748195 -0.818247 -0.007387042 -0.002469599 -0.01878017 -0.9998206 -0.007602214 -0.02243256 -0.9997196 0.007440984 -0.02224701 -0.9997249 0.002750813 -0.01890742 -0.9998174 -8.40573e-7 -0.01367795 -0.9999065 -0.00798428 -0.007449865 0.9999404 -0.02916109 -0.0219537 0.9993337 -0.04118311 0.02231454 0.9989025 0.01138383 -0.02008819 0.9997335 0.0116266 -0.01935064 0.9997453 -0.03979802 0.9988015 -0.02849191 -0.03750216 0.998896 -0.02829426 0.03617066 0.9993457 1.13911e-6 0.01915013 0.9992586 0.03339964 -0.0110442 0.9997916 0.01717156 2.13537e-4 0.998414 -0.05629932 -0.01905494 0.9995746 0.02207905 0.05574911 0.9984449 -1.19817e-4 0.04555356 0.9989601 -0.00192207 4.14317e-5 0.9983577 -0.05728983 -3.92216e-4 0.9985226 -0.05433702 -0.05497872 0.9984876 -1.74394e-4 -0.04532593 0.9989706 -0.001876235 -0.03215342 0.9994451 0.008700311 0.04420334 0.9982572 0.03909951 0.01356661 0.9997569 0.01738047 -0.02931368 0.9995663 0.002813339 -8.40756e-4 0.9996371 0.02692854 0.02857166 0.9993905 0.02005881 -0.0197553 0.9981452 -0.05758422 0.04960417 0.9985613 0.02036315 -0.01980298 0.9991547 -0.03602528 0.03987985 0.9991744 0.007757186 -1.57562e-4 0.9995709 0.02929502 -0.03734642 0.9993024 9.92346e-5 0.04899358 0.9987992 2.78484e-4 0.001217305 0.9985316 -0.05415779 0.01685023 0.9989423 -0.04278373 0.01720976 0.9990151 -0.04089969 0.0420978 0.9989927 -0.01554173 1.35017e-4 0.9995306 0.0306375 -2.56052e-4 0.9999992 -0.001215219 0.001257061 0.9999992 -2.30649e-5 0.03529703 0.9992358 -0.01679521 -0.03647798 0.9984742 0.04145729 0.03809303 0.9983424 -0.04314595 -8.39925e-5 0.9984248 -0.0561068 -0.001366019 0.9999991 2.84488e-4 -0.03207468 0.9992364 0.02231508 0.03182566 0.9994928 0.00116384 -0.01953393 0.998728 0.04648566 -0.0483641 0.9986693 0.01790457 0.002701222 0.999723 0.02338445 -0.008154749 0.9992551 0.03772181 0.001346409 0.9999982 0.001443564 2.85219e-4 0.999999 -0.001440584 -4.26651e-4 0.9999998 7.07991e-4 -0.001589 0.9999756 0.006819844 -0.04870289 0.9987851 0.007510781 2.78449e-4 0.9987719 0.04954522 -0.001296997 0.9999992 1.20695e-4 -0.03349715 0.9994353 -0.002676308 0.009328544 0.999618 0.02601873 0.01659917 0.9988194 0.04565507 0.7971736 -0.003928363 0.6037375 0.8371688 -0.02469092 0.5463871 0.3907819 -0.02414345 0.9201667 0.349193 -0.01405209 0.9369456 -0.6207736 -0.02563965 0.7835705 -0.542501 0.001066267 0.8400545 -0.9117302 -0.01616036 0.4104716 -0.9116256 -0.01609712 0.4107064 -0.9997932 0.02033382 -3.35866e-4 -0.9997844 0.02074885 -8.37824e-4 0.01214045 0.02862757 -0.9995165 -0.004507839 0.02149677 -0.9997588 0.3398677 0.02567046 -0.940123 0.4086727 0.01358455 -0.91258 0.6901827 0.03923451 -0.7225708 0.8504014 -0.002022802 -0.5261305 0.935144 0.03257489 -0.3527673 0.9997449 0.02257227 8.67363e-4 0.9997735 0.02051991 0.005660176 -2.96537e-5 0.02086174 0.9997825 -6.34519e-4 0.02136081 0.9997717 0.006855368 0.9999764 6.32379e-4 4.84183e-5 0.9999936 0.00359565 -0.002482593 0.9999966 -9.32055e-4 -0.002998709 0.9999955 8.29423e-5 1.54639e-5 0.02138823 0.9997712 -1.39133e-4 0.02151739 0.9997684 -0.9997454 0.02247363 0.002029716 -0.9997687 0.02031034 0.00707817 -0.8811984 0.03637057 -0.4713457 -0.9005144 0.02349865 -0.4341907 -0.3984712 0.02666592 -0.9167931 -0.4344211 0.01645815 -0.9005595 0.001909852 0.02104699 -0.9997767 0.003094434 0.02054262 -0.9997842 0.9997859 0.02069485 -1.41352e-4 0.9997981 0.02008581 5.95311e-4 1.38179e-4 0.9999859 0.005308985 0.005393385 0.9999855 5.00562e-5 -0.01043295 0.9999456 5.47132e-5 2.11252e-4 0.9999458 -0.01041203 0.6565264 0.03311085 -0.7535761 0.6074793 0.01187402 -0.7942469 0.9989751 0.01611906 -0.04229778 0.998706 0.01903963 -0.04715883 0.004506647 0.02152347 0.9997582 -0.01208919 0.02863895 0.9995168 -0.3397133 0.02572649 0.9401772 -0.4084981 0.01364552 0.9126572 -0.6901832 0.03932148 0.7225656 -0.8505302 -0.001964092 0.5259227 -0.9351432 0.03258693 0.3527681 -0.9997715 0.02059018 -0.005760371 -0.9997436 0.0226224 -0.001016139 0.02383154 0.02594166 -0.9993794 0.01405942 0.02096039 -0.9996815 0.003094553 0.9999918 0.002593219 0.001917719 0.9999966 0.001798987 4.78276e-4 0.9999992 -0.001183092 -0.005550026 0.9999728 -0.004876852 -0.002536773 0.9999964 -8.84468e-4 -0.9139914 0.04412901 -0.4033268 -0.5442567 0.0384522 -0.8380371 -0.7084817 -0.007274329 -0.7056916 -0.0296927 0.02016824 -0.9993556 -0.01581388 0.01243138 -0.9997978 0.9997709 0.02093356 -0.00445789 0.999498 0.02863979 0.01354718 0.9401232 0.0257008 0.3398647 0.9126565 0.01364713 0.4084995 0.7225604 0.03932052 0.6901888 0.5259218 -0.001965582 0.8505307 0.3529133 0.03255659 0.9350895 -0.002043247 0.02258056 0.9997431 -0.005658864 0.02102994 0.9997629 -0.9999532 0.00752151 -0.006095588 -0.9993792 0.02015662 -0.02889633 0.001256287 0.9999954 -0.002805233 0.001515209 0.9999939 -0.00313121 -0.004870176 0.9999728 0.005549013 -8.41709e-4 0.9999977 0.001997113 -0.007501363 0.9999103 0.01110303 0.01065987 0.9999243 -0.006161391 0.002291321 0.9999756 0.006608426 0.00793755 0.9999635 0.003200709 -0.002152085 0.9999123 -0.01307111 0.008297443 0.9999594 0.003510475 -0.005011558 0.9999145 -0.01207667 0.003060996 0.9999685 0.007328271 0.01080811 0.9998736 -0.01166355 -0.01513278 0.9998643 0.0065158 0.0114302 0.9999113 -0.006848156 0.02179026 0.9996752 -0.01321882 0.004884779 0.9999445 -0.009344935 -0.003964126 0.9999364 -0.010562 0.01132041 0.9994015 -0.03268706 -0.00926429 0.9998297 0.01596432 -0.0349425 0.9993784 -0.004681348 0.01821058 0.9998341 3.55366e-4 4.26147e-4 0.9999064 -0.01367837 0.974332 0.0273106 0.2234531 0.9557826 0.1073004 0.2737999 0.3330882 -0.09754341 -0.9378367 0.5195872 -0.01804101 0.8542271 0.6077356 0.157705 -0.7783231 0.454423 0.08751672 0.8864765 0.9130505 -0.02745264 -0.4069218 0.8992295 0.02166306 -0.4369408 -0.7316485 0.1390356 0.6673527 0.02041035 -0.07851171 -0.9967043 -0.7062311 0.09077769 0.7021376 -0.3488763 0.1379652 -0.9269579 -0.1099019 -0.03246331 0.9934122 -0.6970786 -0.07612544 -0.7129421 -0.9202278 0.2171145 -0.3256413 -0.9950422 -0.0954045 0.02808898 -0.9590265 0.08210206 -0.2711592 -0.9707195 -0.0202586 0.2393601 -0.3562378 0.02584266 0.9340379 -0.2114403 0.06310951 0.9753514 -0.5969607 -0.02077889 -0.8020014 0.2474833 0.9674915 0.05208033 -0.2217323 0.9520062 0.2109952 0.002188146 0.9818791 0.1894964 0.03618705 0.9948542 -0.094635 -0.03970462 0.9977954 -0.0531792 0.1493319 0.9880552 -0.03803724 0.05737549 0.9980043 -0.02637308 0.03952348 0.9942676 -0.09934759 0.06763625 0.9944091 0.08109295 0.05163925 0.9962646 0.06921261 -0.5420612 0.8172764 -0.1955223 -0.05316966 0.9883101 -0.1428857 -0.04296433 0.9990067 0.01181876 -0.03037488 0.9966946 0.07534843 2.75087e-4 1 -2.00694e-4 -2.71444e-4 1 1.27208e-4 -0.006520807 -0.8174653 0.5759411 -0.008352398 -0.819939 0.5723901 -0.007724702 -0.8196955 -0.5727477 -0.006526708 -0.8180925 -0.5750496 0.007609963 -0.8185901 -0.5743277 0.007052063 -0.8178415 -0.5754005 0.007047355 -0.8173345 0.5761204 0.008122503 -0.8187863 0.574041</float_array>
|
| 99 |
+
<technique_common>
|
| 100 |
+
<accessor source="#Part__Feature_007-mesh-normals-array" count="262" stride="3">
|
| 101 |
+
<param name="X" type="float"/>
|
| 102 |
+
<param name="Y" type="float"/>
|
| 103 |
+
<param name="Z" type="float"/>
|
| 104 |
+
</accessor>
|
| 105 |
+
</technique_common>
|
| 106 |
+
</source>
|
| 107 |
+
<vertices id="Part__Feature_007-mesh-vertices">
|
| 108 |
+
<input semantic="POSITION" source="#Part__Feature_007-mesh-positions"/>
|
| 109 |
+
</vertices>
|
| 110 |
+
<triangles material="Part__Feature_007-material" count="262">
|
| 111 |
+
<input semantic="VERTEX" source="#Part__Feature_007-mesh-vertices" offset="0"/>
|
| 112 |
+
<input semantic="NORMAL" source="#Part__Feature_007-mesh-normals" offset="1"/>
|
| 113 |
+
<p>26 0 27 0 28 0 27 1 29 1 28 1 29 2 16 2 28 2 28 3 16 3 100 3 14 4 27 4 26 4 13 5 127 5 12 5 127 6 128 6 12 6 98 7 96 7 11 7 12 8 128 8 10 8 11 9 96 9 9 9 96 10 94 10 9 10 9 11 94 11 13 11 94 12 127 12 13 12 18 13 23 13 129 13 18 14 129 14 95 14 21 15 1 15 130 15 21 16 20 16 1 16 19 17 18 17 95 17 19 18 95 18 30 18 22 19 19 19 30 19 22 20 30 20 4 20 23 21 21 21 130 21 23 22 130 22 129 22 40 23 31 23 41 23 40 24 61 24 31 24 62 25 61 25 40 25 32 26 62 26 40 26 105 27 33 27 46 27 105 28 46 28 45 28 47 29 34 29 48 29 47 30 48 30 102 30 47 31 102 31 35 31 47 32 35 32 36 32 37 33 38 33 39 33 37 34 39 34 103 34 132 35 62 35 32 35 132 36 52 36 62 36 25 37 52 37 132 37 33 38 25 38 132 38 105 39 25 39 33 39 131 40 36 40 35 40 104 41 131 41 35 41 24 42 39 42 131 42 24 43 103 43 39 43 24 44 131 44 104 44 40 45 41 45 42 45 43 46 40 46 42 46 44 47 45 47 46 47 44 48 46 48 43 48 47 49 43 49 42 49 47 50 42 50 34 50 44 51 43 51 38 51 37 52 44 52 38 52 24 53 37 53 103 53 44 54 37 54 24 54 25 55 45 55 44 55 25 56 105 56 45 56 25 57 44 57 24 57 42 58 58 58 106 58 42 59 106 59 48 59 42 60 48 60 34 60 31 61 42 61 41 61 31 62 58 62 42 62 18 63 71 63 70 63 18 64 19 64 71 64 52 65 63 65 125 65 126 66 111 66 20 66 106 67 77 67 50 67 114 68 25 68 117 68 106 69 75 69 77 69 63 70 25 70 114 70 52 71 25 71 63 71 25 72 24 72 117 72 117 73 24 73 110 73 73 74 24 74 108 74 108 75 24 75 104 75 50 76 77 76 104 76 126 77 51 77 111 77 49 78 84 78 86 78 77 79 78 79 104 79 106 80 121 80 75 80 49 81 52 81 84 81 22 82 71 82 19 82 51 83 53 83 111 83 22 84 54 84 71 84 111 85 53 85 55 85 106 86 88 86 121 86 78 87 108 87 104 87 53 88 116 88 55 88 22 89 56 89 54 89 112 90 57 90 22 90 22 91 57 91 56 91 116 92 118 92 55 92 106 93 58 93 88 93 88 94 59 94 121 94 78 95 120 95 108 95 55 96 118 96 112 96 60 97 21 97 122 97 112 98 118 98 57 98 73 99 110 99 24 99 53 100 125 100 116 100 21 101 23 101 122 101 52 102 125 102 84 102 59 103 21 103 60 103 122 104 23 104 120 104 58 105 86 105 88 105 59 106 20 106 21 106 56 107 117 107 54 107 88 108 124 108 59 108 54 109 117 109 110 109 120 110 70 110 108 110 120 111 23 111 70 111 124 112 20 112 59 112 125 113 63 113 116 113 23 114 18 114 70 114 49 115 86 115 58 115 126 116 20 116 124 116 52 117 49 117 61 117 52 118 61 118 62 118 49 119 31 119 61 119 49 120 58 120 31 120 50 121 102 121 48 121 106 122 50 122 48 122 104 123 35 123 102 123 50 124 104 124 102 124 63 125 113 125 64 125 114 126 113 126 63 126 116 127 64 127 65 127 63 128 64 128 116 128 66 129 56 129 57 129 67 130 66 130 57 130 67 131 57 131 118 131 65 132 67 132 118 132 65 133 118 133 116 133 115 134 56 134 66 134 115 135 117 135 56 135 113 136 114 136 115 136 115 137 114 137 117 137 67 138 115 138 66 138 113 139 115 139 67 139 64 140 67 140 65 140 64 141 113 141 67 141 109 142 110 142 73 142 68 143 110 143 109 143 54 144 68 144 74 144 110 145 68 145 54 145 72 146 70 146 71 146 72 147 69 147 70 147 74 148 71 148 54 148 74 149 72 149 71 149 70 150 107 150 108 150 70 151 69 151 107 151 109 152 108 152 107 152 109 153 73 153 108 153 72 154 68 154 109 154 72 155 109 155 107 155 74 156 68 156 72 156 69 157 72 157 107 157 76 158 77 158 83 158 77 159 75 159 83 159 119 160 77 160 76 160 119 161 78 161 77 161 119 162 120 162 78 162 79 163 120 163 119 163 80 164 59 164 60 164 81 165 80 165 60 165 81 166 60 166 122 166 79 167 81 167 122 167 79 168 122 168 120 168 59 169 82 169 121 169 59 170 80 170 82 170 75 171 82 171 83 171 121 172 82 172 75 172 79 173 119 173 76 173 81 174 79 174 76 174 83 175 81 175 76 175 83 176 80 176 81 176 82 177 80 177 83 177 93 178 84 178 92 178 85 179 86 179 93 179 86 180 84 180 93 180 86 181 87 181 88 181 86 182 85 182 87 182 87 183 124 183 88 183 89 184 124 184 87 184 90 185 53 185 51 185 91 186 90 186 51 186 91 187 51 187 126 187 89 188 91 188 126 188 89 189 126 189 124 189 123 190 53 190 90 190 123 191 125 191 53 191 84 192 123 192 92 192 125 193 123 193 84 193 85 194 91 194 89 194 85 195 89 195 87 195 92 196 90 196 91 196 92 197 123 197 90 197 93 198 92 198 91 198 93 199 91 199 85 199 96 200 95 200 94 200 94 201 95 201 129 201 96 202 30 202 95 202 127 203 94 203 129 203 96 204 4 204 30 204 8 205 4 205 96 205 127 206 129 206 130 206 5 207 4 207 8 207 3 208 4 208 5 208 128 209 127 209 130 209 0 210 3 210 5 210 1 211 6 211 128 211 1 212 128 212 130 212 0 213 5 213 7 213 2 214 0 214 7 214 1 215 7 215 6 215 2 216 7 216 1 216 111 217 55 217 0 217 111 218 0 218 2 218 112 219 22 219 4 219 20 220 111 220 2 220 112 221 4 221 3 221 20 222 2 222 1 222 112 223 3 223 0 223 55 224 112 224 0 224 97 225 5 225 98 225 128 226 6 226 10 226 5 227 8 227 98 227 10 228 6 228 99 228 8 229 96 229 98 229 6 230 7 230 99 230 99 231 7 231 97 231 7 232 5 232 97 232 100 233 17 233 101 233 17 234 15 234 101 234 15 235 14 235 101 235 101 236 14 236 26 236 16 237 17 237 100 237 27 238 9 238 13 238 15 239 97 239 98 239 14 240 98 240 11 240 10 241 29 241 12 241 99 242 17 242 16 242 29 243 27 243 12 243 12 244 27 244 13 244 10 245 16 245 29 245 27 246 11 246 9 246 27 247 14 247 11 247 99 248 97 248 17 248 10 249 99 249 16 249 17 250 97 250 15 250 15 251 98 251 14 251 100 252 101 252 26 252 100 253 26 253 28 253 38 254 43 254 131 254 38 255 131 255 39 255 47 256 36 256 131 256 43 257 47 257 131 257 132 258 32 258 40 258 132 259 40 259 43 259 132 260 43 260 46 260 33 261 132 261 46 261</p>
|
| 114 |
+
</triangles>
|
| 115 |
+
</mesh>
|
| 116 |
+
</geometry>
|
| 117 |
+
<geometry id="Part__Feature001_006-mesh" name="Part__Feature001.006">
|
| 118 |
+
<mesh>
|
| 119 |
+
<source id="Part__Feature001_006-mesh-positions">
|
| 120 |
+
<float_array id="Part__Feature001_006-mesh-positions-array" count="555">-9.00074e-4 0.009423434 0.04836726 -0.003461062 0.009425997 0.04545509 0.003367424 0.009420156 0.04579621 0.002354204 0.009420096 0.04753577 0.001209616 0.009421825 0.04820466 -0.002568781 0.009414196 0.04729557 -0.01040536 0.01838725 0.003964126 0.01030278 0.01967525 0.003999948 -0.01030677 0.0197041 0.003999948 -0.01032835 0.01741087 0.006311535 0.01033771 0.01741051 0.006348371 -0.01034963 0.002563178 0.001799941 -0.01046431 0.008072018 0.00174117 -0.01036626 0.002524316 1.33332e-4 -0.01033806 0.008028566 0.003838419 -0.01049768 0.01066809 0.003808438 -0.01037496 0.01157689 0.01777273 -0.01037907 0.01279222 0.01885205 -0.01040232 0.01741403 0.004769504 -0.01033318 0.01621478 0.01882749 -0.0105009 0.01703566 0.01771438 -0.01046431 0.02075433 0.00174117 -0.01025623 0.02079778 0.003920257 -0.01039052 0.02630412 1.63633e-4 -0.01034963 0.02626317 0.001799941 -0.005033195 0.005965054 0.003874957 0.006783962 0.007870256 0.003922104 0.01033878 0.008028566 0.003838419 -0.006688237 0.007825791 0.003912925 -0.004874587 0.002588212 0.003874957 0.004875361 0.002538204 0.003824949 0.004882395 0.006314396 0.00399816 0.01035034 0.002563178 0.001799941 0.004861891 0.002482414 0.001761496 0.0103783 0.002535223 1.52542e-4 -0.004861116 0.002482414 0.001761496 0.0103892 0.02627986 1.33333e-4 0.004900753 0.02634549 0.001760601 0.01035034 0.02626317 0.0018 -0.004838049 0.0262978 0.003838419 0.004838824 0.02625167 0.003884613 -0.004913091 0.02631509 0.001778841 -0.004740357 0.02308672 0.003991544 0.004736721 0.02311271 0.003989934 0.01039779 0.0114066 0.004751801 -0.01040315 0.01144498 0.004798948 0.01038271 0.01261192 0.0187999 0.0102685 0.02076315 0.00393176 0.01045852 0.02080255 0.001745045 0.01046502 0.008072018 0.00174117 0.0104981 0.01065367 0.00379908 0.01040905 0.01161694 0.01771855 0.01049864 0.01723229 0.004730284 0.01032787 0.01605618 0.01887345 0.0104016 0.01821219 0.003997564 0.01050156 0.01703572 0.01771444 0.007072865 0.02100592 0.001899957 0.005206525 0.0225414 0.001899957 0.005030691 0.02334862 0.003700613 0.005556464 0.02213406 0.003700196 0.00697416 0.02102053 0.003698945 0.00515145 0.006138801 0.001899957 0.006992816 0.007812917 0.001899957 0.00512129 0.005962073 0.003693938 -0.00520581 0.0225414 0.001899957 -0.007104516 0.02099138 0.001899957 -0.006963312 0.02102977 0.003688812 -0.005555689 0.02213406 0.003700196 -0.005031466 0.02334022 0.003702998 -0.005051195 0.005644857 0.001899957 -0.005777955 0.006963193 0.001899957 -0.007155239 0.007843613 0.001899957 0.0102126 0.01028144 0.004017531 -0.01020997 0.01025789 0.004015088 0.01020628 0.01744186 0.005109727 0.01019817 0.01733398 0.0177704 -0.01019358 0.01735031 0.01768088 -0.006566226 0.02081871 0.003997445 0.006602585 0.02079534 0.003997147 -0.010293 0.02316558 0.004009008 -0.01038885 0.02522027 0.01918762 0.01040047 0.02525341 0.01903659 -0.008802294 0.01565176 0.0359255 0.008803725 0.01566779 0.03590935 -0.00874871 0.01441204 0.0360046 0.008746623 0.01441162 0.0360043 -0.005330324 0.01430171 0.05381 -0.007801711 0.01423847 0.05229121 0.006546616 0.01429051 0.05324614 0.004800438 0.01433104 0.05383157 0.008537709 0.01441401 0.05053359 -0.008656799 0.01441329 0.04983228 -0.007084131 0.009504258 0.05282139 -0.008779346 0.009461343 0.05011796 -0.005467712 0.009490966 0.05374723 -0.008745193 0.009413957 0.03600448 -0.003062069 0.009426355 0.04349088 0.005491077 0.009412288 0.05355769 0.007737815 0.009533464 0.05224508 0.008802413 0.00950849 0.05020254 0.008748948 0.009413778 0.03600472 -0.009015202 0.008043587 0.03574204 0.009009778 0.008124589 0.03584498 -0.01019251 0.01739948 0.01925081 0.008962452 0.008020699 0.03557926 0.01018828 0.01739251 0.01925343 -0.01036179 0.02530467 0.006144404 0.01038175 0.02526181 0.006118535 0.01029372 0.02316558 0.004009008 -0.01047909 0.01767802 0.01932358 -0.009055435 0.015468 0.03570801 -0.009049296 0.009711623 0.03573459 -0.009055495 0.01410454 0.03572821 0.008973121 0.0141133 0.04968112 0.009049952 0.009715855 0.03573578 0.00905615 0.015468 0.03570801 0.00905627 0.01410877 0.03572702 0.01047974 0.01767796 0.01932418 -0.008394658 0.009712696 0.0514611 -0.008945405 0.01410996 0.04996031 0.008197128 0.01411491 0.05185294 0.005532205 0.009717047 0.05385756 -0.00195378 0.009417772 0.04215568 0.003167867 0.009425044 0.04377222 -2.00269e-6 0.01280522 0.04499375 1.44479e-4 0.00943315 0.04151052 0.001943349 0.009422361 0.04219353 -0.009823203 0.01750212 0.01110798 -0.008970022 0.01753544 0.00949794 -0.008382201 0.02041316 0.008978068 -0.006987035 0.02041316 0.01564162 -0.007580995 0.01771301 0.01545852 -0.007902503 0.01771277 0.008742332 -0.008936882 0.02041321 0.01448625 -0.006165742 0.01771926 0.008206784 -0.005999624 0.02041316 0.008207798 -0.009010076 0.01771265 0.01427406 -0.009712457 0.01771759 0.01274335 -0.009828627 0.02041321 0.01161444 -0.008163809 0.01741361 0.01548886 -0.002160131 0.01771426 0.01179015 -0.001917064 0.0174154 0.01287972 -0.009805202 0.01741385 0.01357632 -0.002761185 0.01771289 0.01393586 -0.003453075 0.01741337 0.01519817 -0.003756344 0.01771312 0.01503348 -0.005418896 0.01771914 0.01575213 -0.005659103 0.01741391 0.01611226 -0.002533555 0.01741403 0.009761214 -0.007441639 0.01741355 0.008155763 -0.004799664 0.01741427 0.008044362 -0.00424081 0.01770812 0.008646845 -0.002989172 0.01771265 0.009725868 -0.003062367 0.02041316 0.01448625 -0.003617048 0.02041316 0.008978068 -0.00524491 0.02041316 0.01571643 -0.002170562 0.02041321 0.01161444 0.002471983 0.01753765 0.01027405 0.003617763 0.02041316 0.008978068 0.005012929 0.02041316 0.01564162 0.004418909 0.01771301 0.01545852 0.004097461 0.01771277 0.008742332 0.003063082 0.02041316 0.01448625 0.005834221 0.01771926 0.008206784 0.006000339 0.02041316 0.008207798 0.002989888 0.01771265 0.01427406 0.002158105 0.01760274 0.01248186 0.002171337 0.02041321 0.01161444 0.002778768 0.01741474 0.01464384 0.009839832 0.01771426 0.01179015 0.009967148 0.01741391 0.01086372 0.009844422 0.01741361 0.01344197 0.009238719 0.01771289 0.01393586 0.008363068 0.01759368 0.01505017 0.006581008 0.01771914 0.01575213 0.00593394 0.01741504 0.01617604 0.006598711 0.01741325 0.00796616 0.008496105 0.01741367 0.008740007 0.004167318 0.01741486 0.008277237 0.007759153 0.01770812 0.008646845 0.009010791 0.01771265 0.009725868 0.008937597 0.02041316 0.01448625 0.008382916 0.02041316 0.008978068 0.006755053 0.02041316 0.01571643 0.009829342 0.02041321 0.01161444</float_array>
|
| 121 |
+
<technique_common>
|
| 122 |
+
<accessor source="#Part__Feature001_006-mesh-positions-array" count="185" stride="3">
|
| 123 |
+
<param name="X" type="float"/>
|
| 124 |
+
<param name="Y" type="float"/>
|
| 125 |
+
<param name="Z" type="float"/>
|
| 126 |
+
</accessor>
|
| 127 |
+
</technique_common>
|
| 128 |
+
</source>
|
| 129 |
+
<source id="Part__Feature001_006-mesh-normals">
|
| 130 |
+
<float_array id="Part__Feature001_006-mesh-normals-array" count="1080">-0.6173208 -0.6997168 -0.3595991 -0.3576143 -0.707046 -0.6100803 -0.05557465 -0.6977123 -0.7142192 0.3884154 -0.7004421 -0.5987607 0.6412798 -0.6994922 -0.3153902 -3.80453e-5 -0.02722054 0.9996295 -0.001623749 -0.00173664 0.9999972 -0.001385629 0.9999968 0.002131819 -2.94082e-5 0.9996801 0.02529293 -0.9997308 -0.02070355 0.01047992 -0.996492 -0.05960285 0.05874812 -0.9995005 -0.0228253 0.0218603 -0.9997318 -0.02303814 0.002409517 -0.9999711 1.03593e-4 -0.007609546 -0.9871179 0.01438945 0.1593462 -0.9997745 0.01160097 0.01778519 -0.9955217 6.06525e-4 0.09453237 -0.9998695 0 -0.01615536 -0.9992002 0.01117813 0.03839474 -0.9936029 0.06317126 0.09361022 -0.9989078 0 -0.04672628 -0.9983385 -9.44458e-4 -0.05761367 -0.9994639 0.02053815 0.02549898 0.02691906 -0.07772469 0.9966114 -7.57925e-5 -0.03944176 0.9992219 -5.39687e-4 -0.04273873 0.9990862 -0.02465045 -0.07755041 0.9966837 -0.01240146 -5.80474e-4 0.999923 0.004887759 -0.04581922 0.9989379 -8.45258e-4 0.04984575 0.9987566 -0.0113424 -0.03047823 0.9994711 0.01459175 -0.9997453 0.017219 0 -0.999635 0.02701693 -0.004865527 -0.9987398 0.04995161 -0.01454573 -0.9996188 0.02346885 5.54909e-4 -0.9995224 -0.03089886 0 -0.9996694 -0.02571326 0.01502466 0.9998336 0.01034975 0.004553616 0.9990093 0.04427027 -0.003079414 0.9999591 0.008511543 -0.009448766 0.9996371 0.02522802 0.00111252 0.999331 -0.03655552 -0.003088533 0.9999886 0.003658115 -0.004537403 0.04749792 0.998861 8.04919e-5 0.0335403 0.9994374 9.2647e-4 -8.09439e-4 -0.9999993 -0.001456677 0.001272737 -0.9999982 0.001968502 -0.9998666 0.01621508 -0.001822233 -0.9999467 0.01016861 -0.00216782 0.007191956 0.9999718 0.002324879 -0.02130818 0.9997704 0.9947002 0.05414348 0.08740782 0.9991514 5.23699e-4 -0.04118567 0.9986065 -5.02086e-4 -0.05277365 0.9996373 -0.02062958 0.01731967 0.9995319 0.0195676 0.02351826 0.9964926 -0.05959349 0.05874836 0.9998502 -0.01730716 -4.88135e-4 0.9998542 -0.0170713 -5.9048e-4 0.9990535 -0.01702678 0.04002922 0.9873026 0.01232677 0.1583726 0.9928981 -0.01674908 0.1177833 0.9937262 0.01559793 -0.1107475 0.9998602 5.16274e-4 -0.01671421 0.9991797 0.01174086 0.03876018 0.0451585 -0.009151577 0.998938 0.01034229 0.01257103 0.9998675 -0.006676495 0.03607821 0.9993267 0.9141238 0.39574 -0.08813399 0.6350729 0.7719225 0.02860087 0.616694 0.7850795 0.05778169 0.9974451 0.06390476 0.03193026 0.9947919 0.08216506 0.06031489 0.05975353 0.9982017 -0.004800856 0.07612389 0.9967962 0.02454829 -0.006453335 -0.03733032 0.9992822 0.04496628 0.009730398 0.9989412 0.01094931 -0.01204329 0.9998676 0.6699684 -0.7369019 0.09009993 0.7559792 -0.652562 -0.05155879 0.07367104 -0.9971508 -0.0162183 0.04523348 -0.998431 0.03301161 0.9974095 -0.07126808 0.009756922 0.9968822 -0.07878315 -0.004374206 -0.04653424 -0.00969702 0.9988697 0.00358504 0.03181004 0.9994875 -0.01030498 0.01262295 0.9998673 -0.6320471 0.7742158 0.03326278 -0.6164938 0.7852315 0.05785328 -0.9135053 0.397269 -0.08766609 -0.07136505 0.9973254 -0.01578521 -0.06973493 0.9974779 -0.01322406 -0.9973001 0.06351983 0.03685021 -0.9949859 0.07914489 0.06114923 0.006362855 -0.04335838 0.9990394 0 0 1 -0.01212346 -0.0116012 0.9998593 -0.04727256 0.009681224 0.9988352 -0.8725091 -0.4809787 0.08595079 -0.7466658 -0.6630428 -0.05352085 -0.5349152 -0.8368087 0.1166919 -0.9987596 -0.04689872 0.01672834 -0.9982018 -0.0598486 -0.003362953 -0.06805241 -0.9975442 -0.01656448 -0.05555498 -0.9984475 0.004042565 8.8907e-4 -0.5465997 0.8373936 5.33059e-4 -0.5509776 0.83452 0.004123806 0.6375477 0.7703999 -0.01049071 0.8228656 0.5681393 -0.002518236 0.7106881 0.7035028 0.003334283 0.6536505 0.7567892 -0.003888607 -0.6640311 0.7476949 0.003183066 -0.7358403 0.6771478 -0.01673263 0.8151549 0.5790015 -0.373636 0.4680295 0.8008399 -0.7724587 0.6221879 0.1272386 -0.5668749 0.2445532 0.7866681 -0.6982629 0.01288533 0.7157256 -0.9592375 0.06434804 0.2751776 -0.7147101 0.6993146 0.0121932 -0.4388056 0.8982897 0.02291625 -0.5722144 0.6164847 0.5408489 0.7010007 0.001137077 0.7131597 0.5716685 0.2472016 0.7823597 0.7760108 0.6250861 0.08411097 0.3656786 0.464663 0.8064537 0.01790195 0.7870998 0.6165658 -0.8352044 -0.3911832 0.3865352 0.8707776 0.05801951 0.4882419 -0.6089326 -0.02781635 0.7927342 0.8206587 0.3920653 0.4156971 0.5498748 0.8344168 -0.03723388 0.7022116 0.7118432 0.01335102 0.7825589 0.6225079 0.009247243 0.6339415 0.5412512 0.5524178 0.825253 -0.3975813 0.4011068 0.6290233 -0.02652961 0.7769336 0.7685624 -0.03074413 0.6390359 0.3894513 -0.4363483 0.8111275 -0.00314182 -6.49825e-5 -0.9999951 0.02068835 0.03777194 -0.9990723 -3.36067e-4 0.8681395 0.4963201 0.002206206 0.8693922 0.4941177 8.46412e-4 0.07534265 0.9971574 1.67392e-5 0.0636698 0.9979711 -0.08732438 0.9910644 0.1008257 -0.002960801 0.9995301 0.03051143 -0.001022875 0.9997116 0.02399551 2.38456e-5 1 -1.64642e-4 -4.06208e-5 1 -8.76669e-5 0.05079007 0.9952879 0.08259797 -0.002142012 0.005291461 0.9999838 -0.009764373 -0.01276755 0.9998709 -2.90966e-4 -0.9998711 0.01605635 -0.01195007 -0.9999074 0.006530165 -0.001630723 -0.9999364 0.01116681 -0.008608043 -0.9999605 -0.00224334 -0.007068097 -0.9999536 0.006551086 -0.003706216 -0.9999876 0.003347218 0.001899778 -0.9999982 2.12748e-4 -8.44581e-4 -0.9999991 -0.001112341 -0.0017578 -0.9999984 -3.81976e-4 -0.003001391 -0.9999879 0.003933608 0.04073494 -0.9989163 -0.02251619 0.007122337 -0.9998478 0.01593202 0.005625069 -0.9999686 -0.005591452 0.0110715 -0.9999184 0.006386518 -0.00471729 -0.9999871 -0.001937091 1.70855e-4 -0.9999988 0.001574873 0.01087236 -0.9999189 0.006631433 -0.004614412 -0.9999826 -0.003687381 -1.05398e-5 -0.9999939 0.003507912 -1.60083e-5 -0.1229799 0.9924092 -0.004767358 -0.1871572 0.9823184 -0.005576372 -0.8699347 -0.4931353 -2.32577e-4 -0.8672547 -0.4978647 -9.39586e-4 -0.9995101 0.03128528 -3.46689e-4 -0.999222 0.03943848 0.9927443 -0.1197105 0.01132869 -0.9563636 -0.2920154 0.00978285 -0.001544773 0.9999779 0.006467223 0.002068638 0.9999976 6.48183e-4 -0.01753318 0.03230512 -0.9993243 0.003435969 -3.46711e-4 -0.999994 0.9999831 -0.005649566 -0.001454889 0.9998875 0.01003545 -0.01114994 0.9990121 0.002701461 -0.04435819 0.9997629 -0.006217002 -0.02087032 -0.9999886 -0.00428152 -0.00210607 -0.9998611 0.01174783 -0.01182723 -0.9993346 0.004062354 -0.03624898 -0.9998942 -0.004529297 -0.01382708 -0.9959966 0.01350748 0.08836561 -0.9962916 -0.005001425 0.08589476 0.1560158 -0.001218974 -0.9877539 -0.8825191 -0.006937742 -0.4702252 -0.9992199 -0.03507834 0.01814252 -0.9999691 -0.001396 0.007733643 0.9999812 -0.001430571 0.005960941 0.999244 -0.03516072 0.01659327 0.9861967 -0.03598099 -0.1616215 0.9268067 -0.005166351 -0.3755034 0.8828601 -0.002207458 -0.469631 0.99599 0.01377451 0.08839821 0.9963099 -0.01399958 0.08467978 -9.92955e-4 -0.7099062 -0.7042956 0.001225948 -0.7197995 -0.694181 -0.004333555 0.7094319 -0.7047607 -0.002025246 0.6938492 -0.7201175 5.66681e-4 0.7029646 -0.7112247 0.007812917 0.7066024 -0.7075677 -0.5236616 0.003840386 0.8519178 -0.4967814 0.02186524 0.8676003 -0.722224 -0.03209161 0.6909145 -0.897934 0.03721666 0.4385541 -0.9598154 -0.02480667 0.2795337 0.9416905 0.003191292 0.3364652 0.8854467 -0.04922211 0.4621273 0.6450628 7.23494e-4 0.7641294 0.5888193 -0.02258223 0.8079492 0.3185542 0.05585145 0.9462579 0.6454741 0.03606939 0.7629301 0.7021592 -0.638997 -0.3140944 0.7092592 -0.704945 0.00203222 0.40139 -0.669321 0.6252164 0.007280707 -0.701826 0.7123112 -0.4307134 -0.8567411 0.2836915 0.6988441 -0.7152389 0.007094681 -0.6998528 -0.7142868 7.04012e-4 0.9143519 0.003415346 -0.4049062 -0.6693444 -0.01035511 0.7428802 -0.7015902 -0.6401234 -0.3130708 0.7415134 0.5508129 0.3830968 -0.6960386 -0.7179734 0.006675958 0.6727006 0.01042085 0.7398415 0.6373302 0.02924919 0.7700356 -0.8728793 0.004739582 -0.4879134 0.5790942 0.8152195 0.008191704 0.7009998 0.7131506 0.003946483 0.8712383 0.3797509 0.3110197 0.3404745 0.8942072 0.2906386 -0.6368297 0.7211554 0.2727323 -0.7100141 0.7041722 0.00465697 -0.7232721 0.6905426 0.005329906 -0.7358887 0.5554174 0.3872717 -0.6753374 0.01095151 0.7374276 -0.6596332 0.01942574 0.7513366 -0.7151693 -0.6955538 0.06883221 -0.5688336 -0.6933217 0.4424177 0.7013344 -0.6984758 0.1423438 0.5473927 -0.6998478 0.4588838 0.215398 -0.6970678 0.6838861 -0.2572834 -0.6996731 0.6665304 0.002106964 -0.9314457 0.3638744 0.881494 -0.07933795 0.4654824 0.8730121 -0.09179598 0.4789817 0.04290962 -0.01601725 -0.9989507 0.1343826 0.03758537 -0.9902165 0.5740847 0.03054869 0.8182259 0.5090357 -0.05372262 -0.8590674 0.3075678 -0.01931774 0.9513301 0.2946367 -0.03103911 0.9551053 0.6375395 0.04316061 -0.7692078 0.908856 0.008138895 -0.4170307 0.9517405 -0.08275151 -0.2955374 0.9974929 0.013933 -0.06938272 0.3853201 -0.7972646 -0.464648 0.6791377 -0.4461136 -0.5828849 -0.4734979 -0.8706647 -0.1332018 0.5195622 -0.8197226 -0.2410601 -0.4938721 -0.7454801 -0.4476045 -0.7729855 -0.3740035 -0.5124595 0.9405061 -0.3392458 -0.01898998 -0.2958472 -0.6721335 -0.678757 -0.3007575 -0.6191048 -0.7254338 0.08517628 -0.7873173 -0.6106361 0.2099796 -0.49436 -0.8435145 -2.64102e-4 1 1.74548e-4 -1.1962e-5 1 1.26157e-4 2.06489e-4 1 -9.33148e-5 -0.001511156 0.9999989 -1.27516e-4 -0.04181706 0.9984763 0.03600627 -0.005665898 0.9997345 0.02233892 -0.002253532 0.9998073 0.01950085 0.06646537 0.9976769 -0.01493656 -0.4830842 -0.6704126 0.5631843 -0.5432407 -0.436838 0.7169812 -0.5465892 -0.8079434 0.2201545 -0.9770032 -0.09024524 0.193185 0.560274 -0.4717366 0.6808507 0.1733226 -0.8155052 0.5521872 0.03994673 -0.3244244 0.9450678 -0.1251857 -0.8410195 0.526322 -0.9616039 -0.05233085 -0.2694056 -0.7397416 0.05420136 -0.6707046 -0.3073062 -0.04552447 0.9505212 -0.2227703 0.0133779 0.9747792 -0.4903982 -0.05028861 -0.8700463 -0.3967096 0.01345318 -0.9178457 -0.6519777 0.05776602 0.7560345 -0.8745346 -0.07046216 0.479817 -0.9277643 0.02065652 0.3725948 -0.9547636 -0.02300214 -0.2964749 -8.72595e-7 -1 -9.46364e-6 -3.17071e-7 -1 0 -3.17071e-7 -1 8.62569e-7 -4.37507e-7 -1 0 0 -1 0 3.97679e-6 -1 0 0 -1 1.98224e-6 0.8691138 -0.1313883 0.476842 0.04290962 -0.01601725 -0.9989507 0.1343826 0.03758537 -0.9902165 0.6814061 0.0573436 0.7296558 0.5090351 -0.0537225 -0.8590676 0.3075684 -0.01931768 0.9513298 0.2946355 -0.03103935 0.9551055 0.6375406 0.04316031 -0.7692069 0.9068413 0.008536219 -0.4213859 0.9506452 -0.09556525 -0.295197 0.9894707 0.03842526 0.1395403 0.3696854 -0.8152374 -0.445781 -0.9627035 -0.2666468 -0.04584342 -0.5335696 -0.8323478 -0.1500031 0.6228252 -0.7435162 -0.2434592 -0.6888185 -0.4255956 -0.5868538 -0.3595518 -0.4060154 -0.8401631 0.07133811 -0.8588427 -0.5072478 0.4361646 0.05485165 -0.8981936 -3.43774e-4 1 1.10847e-4 3.18553e-4 1 -7.99342e-5 -0.2044163 0.9340437 -0.2928762 0.09703636 0.9950554 0.02118116 6.65985e-4 0.9999999 1.63433e-4 -1.12083e-4 1 -2.28791e-4 -3.81542e-6 1 -2.18706e-4 0.2618119 0.9650791 0.008770644 -0.3162638 -0.5461402 0.7756986 -0.3802565 -0.8110634 0.4445014 -0.7460662 -0.4198443 0.5168328 -0.5037703 -0.839652 0.2029784 0.5497425 -0.6634614 0.5075454 0.1725108 -0.8174732 0.5495251 0.1148756 -0.4207645 0.8998672 -0.1376492 -0.8015738 0.5818353 -0.9616038 -0.05233103 -0.2694066 -0.7876538 0.03749954 -0.6149759 -0.3073074 -0.04552465 0.9505208 -0.4896735 -0.07398062 -0.8687616 -0.3657975 0.01128739 -0.9306262 -0.6519766 0.05776649 0.7560356 -0.8745356 -0.07046252 0.4798154 -0.9277645 0.02065676 0.372594 -0.9547633 -0.02300065 -0.2964761 -3.97679e-6 -1 1.11854e-6 -1.04644e-6 -1 -6.13438e-7 1.79553e-6 -1 5.72717e-7 3.97679e-6 -1 5.59268e-7 -8.85583e-7 -1 0 0 -1 4.29807e-7</float_array>
|
| 131 |
+
<technique_common>
|
| 132 |
+
<accessor source="#Part__Feature001_006-mesh-normals-array" count="360" stride="3">
|
| 133 |
+
<param name="X" type="float"/>
|
| 134 |
+
<param name="Y" type="float"/>
|
| 135 |
+
<param name="Z" type="float"/>
|
| 136 |
+
</accessor>
|
| 137 |
+
</technique_common>
|
| 138 |
+
</source>
|
| 139 |
+
<vertices id="Part__Feature001_006-mesh-vertices">
|
| 140 |
+
<input semantic="POSITION" source="#Part__Feature001_006-mesh-positions"/>
|
| 141 |
+
</vertices>
|
| 142 |
+
<triangles material="Part__Feature001_006-material" count="362">
|
| 143 |
+
<input semantic="VERTEX" source="#Part__Feature001_006-mesh-vertices" offset="0"/>
|
| 144 |
+
<input semantic="NORMAL" source="#Part__Feature001_006-mesh-normals" offset="1"/>
|
| 145 |
+
<p>3 0 124 0 2 0 4 1 124 1 3 1 4 2 0 2 124 2 0 3 5 3 124 3 1 4 124 4 5 4 6 5 7 5 8 5 6 6 54 6 7 6 9 7 74 7 18 7 9 8 10 8 74 8 11 9 12 9 13 9 14 10 15 10 12 10 17 11 20 11 16 11 16 12 20 12 45 12 45 13 20 13 18 13 17 14 19 14 20 14 18 15 6 15 15 15 45 16 18 16 15 16 15 17 21 17 12 17 6 18 21 18 15 18 6 19 22 19 21 19 21 20 23 20 12 20 12 21 23 21 13 21 21 22 24 22 23 22 72 23 26 23 27 23 72 24 73 24 26 24 26 25 73 25 28 25 73 26 14 26 28 26 25 27 29 27 31 27 31 28 29 28 30 28 26 29 28 29 31 29 28 30 25 30 31 30 32 31 33 31 34 31 30 32 35 32 33 32 29 33 35 33 30 33 11 34 13 34 35 34 13 35 34 35 33 35 13 36 33 36 35 36 36 37 37 37 38 37 37 38 39 38 40 38 41 39 39 39 37 39 23 40 24 40 41 40 36 41 23 41 37 41 37 42 23 42 41 42 42 43 40 43 39 43 42 44 43 44 40 44 13 45 36 45 34 45 13 46 23 46 36 46 51 47 16 47 44 47 16 48 45 48 44 48 19 49 17 49 53 49 53 50 17 50 46 50 47 51 54 51 48 51 36 52 48 52 49 52 36 53 49 53 34 53 49 54 32 54 34 54 38 55 48 55 36 55 50 56 27 56 49 56 55 57 44 57 52 57 55 58 51 58 44 58 55 59 46 59 51 59 53 60 46 60 55 60 52 61 44 61 50 61 54 62 52 62 50 62 48 63 50 63 49 63 48 64 54 64 50 64 38 65 56 65 48 65 57 66 56 66 38 66 37 67 57 67 38 67 58 68 59 68 57 68 57 69 60 69 56 69 59 70 60 70 57 70 40 71 58 71 37 71 37 72 58 72 57 72 60 73 48 73 56 73 60 74 47 74 48 74 61 75 33 75 32 75 62 76 32 76 49 76 62 77 61 77 32 77 62 78 26 78 61 78 26 79 63 79 61 79 27 80 62 80 49 80 27 81 26 81 62 81 63 82 30 82 61 82 61 83 30 83 33 83 24 84 21 84 65 84 24 85 64 85 41 85 24 86 65 86 64 86 65 87 66 87 64 87 66 88 67 88 64 88 67 89 68 89 64 89 22 90 66 90 65 90 22 91 65 91 21 91 68 92 39 92 41 92 68 93 41 93 64 93 11 94 35 94 69 94 71 95 69 95 70 95 71 96 11 96 69 96 12 97 11 97 71 97 69 98 25 98 70 98 25 99 28 99 70 99 70 100 28 100 71 100 29 101 25 101 69 101 29 102 69 102 35 102 71 103 14 103 12 103 28 104 14 104 71 104 44 105 45 105 72 105 45 106 73 106 72 106 6 107 18 107 54 107 54 108 18 108 74 108 75 109 76 109 19 109 53 110 75 110 19 110 46 111 17 111 16 111 51 112 46 112 16 112 66 113 22 113 77 113 66 114 77 114 67 114 77 115 42 115 67 115 67 116 42 116 68 116 39 117 68 117 42 117 8 118 22 118 6 118 76 119 9 119 20 119 9 120 18 120 20 120 19 121 76 121 20 121 40 122 43 122 58 122 58 123 43 123 59 123 43 124 78 124 59 124 59 125 78 125 60 125 78 126 47 126 60 126 45 127 15 127 73 127 54 128 47 128 7 128 14 129 73 129 15 129 74 130 52 130 54 130 10 131 52 131 74 131 55 132 10 132 75 132 55 133 52 133 10 133 75 134 53 134 55 134 72 135 50 135 44 135 27 136 50 136 72 136 31 137 30 137 63 137 26 138 31 138 63 138 77 139 79 139 42 139 77 140 22 140 79 140 82 141 83 141 80 141 83 142 81 142 80 142 82 143 85 143 83 143 84 144 85 144 82 144 86 145 91 145 87 145 89 146 91 146 86 146 90 147 91 147 89 147 85 148 84 148 90 148 90 149 84 149 91 149 90 150 89 150 88 150 86 151 121 151 89 151 94 152 121 152 86 152 93 153 5 153 92 153 92 154 5 154 94 154 5 155 0 155 94 155 93 156 1 156 5 156 94 157 0 157 97 157 93 158 95 158 1 158 95 159 96 159 1 159 0 160 4 160 97 160 4 161 3 161 97 161 95 162 122 162 96 162 97 163 3 163 98 163 3 164 99 164 98 164 95 165 125 165 122 165 2 166 99 166 3 166 123 167 100 167 2 167 126 168 100 168 123 168 2 169 100 169 99 169 125 170 100 170 126 170 95 171 100 171 125 171 95 172 102 172 100 172 101 173 102 173 95 173 103 174 104 174 101 174 105 175 104 175 103 175 76 176 75 176 103 176 75 177 105 177 103 177 10 178 105 178 75 178 103 179 9 179 76 179 80 180 81 180 106 180 81 181 107 181 106 181 47 182 78 182 108 182 108 183 78 183 43 183 10 184 107 184 81 184 10 185 81 185 117 185 7 186 108 186 107 186 7 187 107 187 10 187 106 188 9 188 80 188 80 189 9 189 109 189 79 190 8 190 106 190 106 191 8 191 9 191 109 192 110 192 80 192 109 193 101 193 110 193 112 194 101 194 111 194 112 195 110 195 101 195 111 196 93 196 119 196 111 197 119 197 112 197 113 198 114 198 116 198 99 199 114 199 113 199 104 200 114 200 102 200 115 201 116 201 104 201 104 202 116 202 114 202 115 203 117 203 81 203 104 204 117 204 115 204 7 205 9 205 8 205 10 206 9 206 7 206 108 207 43 207 107 207 42 208 106 208 43 208 43 209 106 209 107 209 42 210 79 210 106 210 94 211 86 211 87 211 92 212 94 212 87 212 118 213 92 213 87 213 118 214 87 214 119 214 93 215 118 215 119 215 99 216 113 216 120 216 98 217 99 217 120 217 98 218 120 218 88 218 121 219 98 219 88 219 121 220 88 220 89 220 100 221 102 221 114 221 104 222 105 222 117 222 99 223 100 223 114 223 98 224 121 224 97 224 121 225 94 225 97 225 118 226 93 226 92 226 105 227 10 227 117 227 95 228 93 228 111 228 7 229 47 229 108 229 101 230 95 230 111 230 103 231 101 231 109 231 81 232 83 232 115 232 109 233 9 233 103 233 115 234 85 234 116 234 83 235 85 235 115 235 8 236 79 236 22 236 85 237 90 237 113 237 85 238 113 238 116 238 120 239 113 239 90 239 120 240 90 240 88 240 119 241 87 241 91 241 91 242 84 242 112 242 91 243 112 243 119 243 82 244 80 244 110 244 84 245 110 245 112 245 84 246 82 246 110 246 2 247 124 247 123 247 126 248 123 248 124 248 96 249 124 249 1 249 96 250 122 250 124 250 122 251 125 251 124 251 125 252 126 252 124 252 102 253 101 253 104 253 127 254 128 254 138 254 138 255 128 255 129 255 155 256 146 256 130 256 146 257 131 257 130 257 128 258 132 258 129 258 130 259 131 259 133 259 129 260 134 260 135 260 132 261 134 261 129 261 131 262 136 262 133 262 136 263 137 263 133 263 133 264 137 264 138 264 137 265 127 265 138 265 131 266 139 266 136 266 139 267 142 267 136 267 140 268 141 268 143 268 136 269 142 269 137 269 143 270 144 270 145 270 141 271 144 271 143 271 142 272 127 272 137 272 145 273 144 273 146 273 144 274 147 274 146 274 146 275 147 275 131 275 147 276 139 276 131 276 149 277 144 277 150 277 150 278 144 278 148 278 147 279 144 279 149 279 148 280 144 280 141 280 142 281 139 281 127 281 139 282 147 282 127 282 127 283 147 283 128 283 128 284 147 284 149 284 151 285 148 285 152 285 150 286 148 286 151 286 152 287 148 287 140 287 148 288 141 288 140 288 128 289 149 289 132 289 132 290 149 290 134 290 149 291 150 291 134 291 134 292 150 292 151 292 140 293 143 293 153 293 143 294 145 294 153 294 135 295 151 295 154 295 134 296 151 296 135 296 153 297 145 297 155 297 145 298 146 298 155 298 151 299 152 299 154 299 154 300 152 300 156 300 152 301 140 301 156 301 156 302 140 302 153 302 153 303 155 303 130 303 153 304 130 304 133 304 153 305 133 305 138 305 154 306 153 306 138 306 154 307 138 307 129 307 154 308 156 308 153 308 154 309 129 309 135 309 167 310 157 310 158 310 183 311 174 311 159 311 174 312 160 312 159 312 157 313 161 313 158 313 159 314 160 314 162 314 158 315 163 315 164 315 161 316 163 316 158 316 160 317 165 317 162 317 165 318 166 318 162 318 162 319 166 319 167 319 166 320 157 320 167 320 160 321 168 321 165 321 170 322 171 322 169 322 169 323 171 323 172 323 165 324 168 324 166 324 171 325 173 325 172 325 173 326 175 326 174 326 174 327 175 327 160 327 175 328 168 328 160 328 177 329 171 329 170 329 175 330 171 330 177 330 175 331 173 331 171 331 157 332 168 332 178 332 178 333 168 333 176 333 176 334 175 334 177 334 168 335 175 335 176 335 166 336 168 336 157 336 176 337 177 337 179 337 179 338 177 338 180 338 177 339 170 339 180 339 180 340 170 340 169 340 157 341 178 341 161 341 161 342 178 342 163 342 178 343 176 343 163 343 163 344 176 344 179 344 169 345 172 345 181 345 172 346 173 346 181 346 164 347 179 347 182 347 163 296 179 296 164 296 181 348 173 348 183 348 173 349 174 349 183 349 179 350 180 350 182 350 182 351 180 351 184 351 180 352 169 352 184 352 184 353 169 353 181 353 158 354 162 354 167 354 182 355 183 355 159 355 182 356 181 356 183 356 182 357 184 357 181 357 182 307 159 307 164 307 164 358 159 358 162 358 164 359 162 359 158 359</p>
|
| 146 |
+
</triangles>
|
| 147 |
+
</mesh>
|
| 148 |
+
</geometry>
|
| 149 |
+
</library_geometries>
|
| 150 |
+
<library_controllers/>
|
| 151 |
+
<library_visual_scenes>
|
| 152 |
+
<visual_scene id="Scene" name="Scene">
|
| 153 |
+
<node id="node0" name="node0" type="NODE">
|
| 154 |
+
<matrix sid="transform">1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1</matrix>
|
| 155 |
+
<instance_geometry url="#Part__Feature_007-mesh" name="node0">
|
| 156 |
+
<bind_material>
|
| 157 |
+
<technique_common>
|
| 158 |
+
<instance_material symbol="Part__Feature_007-material" target="#Part__Feature_007-material"/>
|
| 159 |
+
</technique_common>
|
| 160 |
+
</bind_material>
|
| 161 |
+
</instance_geometry>
|
| 162 |
+
</node>
|
| 163 |
+
<node id="node1" name="node1" type="NODE">
|
| 164 |
+
<matrix sid="transform">1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1</matrix>
|
| 165 |
+
<instance_geometry url="#Part__Feature001_006-mesh" name="node1">
|
| 166 |
+
<bind_material>
|
| 167 |
+
<technique_common>
|
| 168 |
+
<instance_material symbol="Part__Feature001_006-material" target="#Part__Feature001_006-material"/>
|
| 169 |
+
</technique_common>
|
| 170 |
+
</bind_material>
|
| 171 |
+
</instance_geometry>
|
| 172 |
+
</node>
|
| 173 |
+
</visual_scene>
|
| 174 |
+
</library_visual_scenes>
|
| 175 |
+
<scene>
|
| 176 |
+
<instance_visual_scene url="#Scene"/>
|
| 177 |
+
</scene>
|
| 178 |
+
</COLLADA>
|
embodied_gen/scripts/affordance_annot/franka_panda/meshes/visual/hand.dae
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
embodied_gen/scripts/affordance_annot/gen_grasp.py
ADDED
|
@@ -0,0 +1,842 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Project EmbodiedGen
|
| 2 |
+
#
|
| 3 |
+
# Copyright (c) 2025 Horizon Robotics. All Rights Reserved.
|
| 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
|
| 14 |
+
# implied. See the License for the specific language governing
|
| 15 |
+
# permissions and limitations under the License.
|
| 16 |
+
|
| 17 |
+
import os
|
| 18 |
+
import sys
|
| 19 |
+
import time
|
| 20 |
+
from dataclasses import dataclass, field
|
| 21 |
+
from typing import Literal
|
| 22 |
+
|
| 23 |
+
from embodied_gen.utils.general import filter_warnings
|
| 24 |
+
|
| 25 |
+
filter_warnings()
|
| 26 |
+
|
| 27 |
+
import numpy as np
|
| 28 |
+
import trimesh
|
| 29 |
+
import trimesh.transformations as tra
|
| 30 |
+
import tyro
|
| 31 |
+
import yaml
|
| 32 |
+
|
| 33 |
+
sys.path.append(".")
|
| 34 |
+
from embodied_gen.utils.geometry import (
|
| 35 |
+
sample_surface_points,
|
| 36 |
+
)
|
| 37 |
+
from embodied_gen.utils.io_utils import (
|
| 38 |
+
URDFFile,
|
| 39 |
+
load_json,
|
| 40 |
+
load_mesh,
|
| 41 |
+
write_json,
|
| 42 |
+
)
|
| 43 |
+
from embodied_gen.utils.log import logger
|
| 44 |
+
from embodied_gen.utils.vis_utils import show_grasps
|
| 45 |
+
|
| 46 |
+
__all__ = [
|
| 47 |
+
"GraspGenHyperConfig",
|
| 48 |
+
"GraspGenXHyperConfig",
|
| 49 |
+
"GraspPoseConfig",
|
| 50 |
+
"GraspGenerator",
|
| 51 |
+
"run_grasp",
|
| 52 |
+
"entrypoint",
|
| 53 |
+
]
|
| 54 |
+
|
| 55 |
+
FINGER_CENTER_CACHE: dict[str, np.ndarray] = {
|
| 56 |
+
"franka_panda": np.array([0.0, 0.0, 0.1034], dtype=np.float64),
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
GRIPPER_GEOMETRY_CACHE: dict[str, dict[str, float]] = {
|
| 60 |
+
"franka_panda": {
|
| 61 |
+
"width": 0.10537486,
|
| 62 |
+
"depth": 0.10527314,
|
| 63 |
+
},
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
@dataclass
|
| 68 |
+
class GraspGenHyperConfig:
|
| 69 |
+
gripper_config: str = (
|
| 70 |
+
"~/.cache/huggingface/hub/GraspGenModels/checkpoints/graspgen_{}.yml"
|
| 71 |
+
)
|
| 72 |
+
grasp_threshold: float = -1.0
|
| 73 |
+
num_grasps: int = 4000
|
| 74 |
+
return_topk: bool = True
|
| 75 |
+
topk_num_grasps: int = 4000
|
| 76 |
+
mesh_scale: tuple[float, float, float] = (1.0, 1.0, 1.0)
|
| 77 |
+
num_sample_points: int = 2000
|
| 78 |
+
min_grasps: int = 40
|
| 79 |
+
max_tries: int = 6
|
| 80 |
+
remove_outliers: bool = False
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
@dataclass
|
| 84 |
+
class GraspGenXHyperConfig:
|
| 85 |
+
checkpoint_root: str = "~/.cache/huggingface/hub/GraspGenXModels/release"
|
| 86 |
+
gen_pth: str | None = None
|
| 87 |
+
dis_pth: str | None = None
|
| 88 |
+
assets_dir: str = (
|
| 89 |
+
"thirdparty/gripper_descriptions/gripper_descriptions/assets"
|
| 90 |
+
)
|
| 91 |
+
grasp_threshold: float = -1.0
|
| 92 |
+
num_grasps: int = 4000
|
| 93 |
+
return_topk: bool = True
|
| 94 |
+
topk_num_grasps: int = 4000
|
| 95 |
+
mesh_scale: tuple[float, float, float] = (1.0, 1.0, 1.0)
|
| 96 |
+
num_sample_points: int = 2000
|
| 97 |
+
min_grasps: int = 40
|
| 98 |
+
max_tries: int = 6
|
| 99 |
+
remove_outliers: bool = False
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
@dataclass
|
| 103 |
+
class GraspPoseConfig:
|
| 104 |
+
urdf_paths: list[str] = field(default_factory=list)
|
| 105 |
+
model_name: str = "GraspGen"
|
| 106 |
+
gripper_name: str = "franka_panda"
|
| 107 |
+
mesh_type: Literal["collision", "visual"] = "collision"
|
| 108 |
+
update_urdf: bool = True
|
| 109 |
+
overwrite: bool = True
|
| 110 |
+
gripper_mesh_path: str = (
|
| 111 |
+
f"embodied_gen/scripts/affordance_annot/{gripper_name}/coll_mesh.obj"
|
| 112 |
+
)
|
| 113 |
+
grasps_per_part: int = 20
|
| 114 |
+
visualize_grasps: bool = False
|
| 115 |
+
visualizer_port: int = 8080
|
| 116 |
+
graspgen: GraspGenHyperConfig | None = None
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
class GraspGenerator:
|
| 120 |
+
def __init__(self, cfg: GraspPoseConfig):
|
| 121 |
+
self.cfg = cfg
|
| 122 |
+
self.grasp_visualizer = None
|
| 123 |
+
self.validate_config(cfg)
|
| 124 |
+
self.modules = self.load_model()
|
| 125 |
+
self.pipeline = self.build_sampler()
|
| 126 |
+
self.finger_center = FINGER_CENTER_CACHE.get(
|
| 127 |
+
self.cfg.gripper_name,
|
| 128 |
+
np.array([0.0, 0.0, 0.0], dtype=np.float64),
|
| 129 |
+
)
|
| 130 |
+
(
|
| 131 |
+
self.local_contact_points,
|
| 132 |
+
self.local_closing_directions,
|
| 133 |
+
) = self.build_local_contact_samples()
|
| 134 |
+
|
| 135 |
+
def validate_config(self, cfg: GraspPoseConfig) -> None:
|
| 136 |
+
cfg.model_name = cfg.model_name.lower()
|
| 137 |
+
if cfg.model_name not in {"graspgen", "graspgenx"}:
|
| 138 |
+
raise ValueError(
|
| 139 |
+
"unsupported model '{}', available models: GraspGen, GraspGenX".format(
|
| 140 |
+
cfg.model_name
|
| 141 |
+
)
|
| 142 |
+
)
|
| 143 |
+
|
| 144 |
+
cfg.graspgen = (
|
| 145 |
+
GraspGenHyperConfig()
|
| 146 |
+
if cfg.model_name == "graspgen"
|
| 147 |
+
else GraspGenXHyperConfig()
|
| 148 |
+
)
|
| 149 |
+
if cfg.model_name == "graspgen":
|
| 150 |
+
cfg.graspgen.gripper_config = cfg.graspgen.gripper_config.format(
|
| 151 |
+
cfg.gripper_name
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
+
def load_model(self):
|
| 155 |
+
if self.cfg.model_name == "graspgen":
|
| 156 |
+
sys.path.append("thirdparty/GraspGen")
|
| 157 |
+
from grasp_gen.grasp_server import GraspGenSampler, load_grasp_cfg
|
| 158 |
+
|
| 159 |
+
return {
|
| 160 |
+
"GraspGenSampler": GraspGenSampler,
|
| 161 |
+
"model_cfg": load_grasp_cfg,
|
| 162 |
+
}
|
| 163 |
+
if self.cfg.model_name == "graspgenx":
|
| 164 |
+
sys.path.append("thirdparty/GraspGenX")
|
| 165 |
+
from graspgenx.grasp_server import GraspGenXSampler
|
| 166 |
+
from graspgenx.utils.checkpoint_io import load_model_cfg
|
| 167 |
+
|
| 168 |
+
return {
|
| 169 |
+
"GraspGenSampler": GraspGenXSampler,
|
| 170 |
+
"model_cfg": load_model_cfg,
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
@staticmethod
|
| 174 |
+
def _extract_checkpoint_filenames(yml_path: str) -> list[str]:
|
| 175 |
+
"""Recursively collect all `checkpoint: <name>.pth` entries.
|
| 176 |
+
|
| 177 |
+
Walks a GraspGen config yaml so we know every weight file it
|
| 178 |
+
requires.
|
| 179 |
+
"""
|
| 180 |
+
with open(yml_path) as f:
|
| 181 |
+
cfg = yaml.safe_load(f)
|
| 182 |
+
|
| 183 |
+
names: set[str] = set()
|
| 184 |
+
|
| 185 |
+
def _walk(node) -> None:
|
| 186 |
+
if isinstance(node, dict):
|
| 187 |
+
for key, value in node.items():
|
| 188 |
+
if (
|
| 189 |
+
key == "checkpoint"
|
| 190 |
+
and isinstance(value, str)
|
| 191 |
+
and value.strip()
|
| 192 |
+
):
|
| 193 |
+
names.add(value.strip())
|
| 194 |
+
else:
|
| 195 |
+
_walk(value)
|
| 196 |
+
elif isinstance(node, list):
|
| 197 |
+
for item in node:
|
| 198 |
+
_walk(item)
|
| 199 |
+
|
| 200 |
+
_walk(cfg)
|
| 201 |
+
return sorted(names)
|
| 202 |
+
|
| 203 |
+
@staticmethod
|
| 204 |
+
def _dir_has_pth(directory: str, filename: str | None) -> bool:
|
| 205 |
+
if filename:
|
| 206 |
+
return os.path.exists(os.path.join(directory, filename))
|
| 207 |
+
return os.path.isdir(directory) and any(
|
| 208 |
+
f.endswith(".pth") for f in os.listdir(directory)
|
| 209 |
+
)
|
| 210 |
+
|
| 211 |
+
def _ensure_checkpoints(
|
| 212 |
+
self,
|
| 213 |
+
is_ready: callable,
|
| 214 |
+
repo_id: str,
|
| 215 |
+
allow_patterns: str,
|
| 216 |
+
download_dir: str,
|
| 217 |
+
desc: str,
|
| 218 |
+
) -> None:
|
| 219 |
+
"""Download `desc` weights from `repo_id` unless already ready.
|
| 220 |
+
|
| 221 |
+
Re-checks afterwards so a stale/partial local cache (e.g. config
|
| 222 |
+
present but a referenced .pth missing) is retried instead of
|
| 223 |
+
silently failing deep inside third-party code.
|
| 224 |
+
"""
|
| 225 |
+
if is_ready():
|
| 226 |
+
return
|
| 227 |
+
|
| 228 |
+
logger.info(
|
| 229 |
+
f"Downloading {desc} weights from Hugging Face. "
|
| 230 |
+
"First-time use may take several minutes; please wait patiently."
|
| 231 |
+
)
|
| 232 |
+
from huggingface_hub import snapshot_download
|
| 233 |
+
|
| 234 |
+
snapshot_download(
|
| 235 |
+
repo_id=repo_id,
|
| 236 |
+
allow_patterns=allow_patterns,
|
| 237 |
+
local_dir=os.path.expanduser(download_dir),
|
| 238 |
+
)
|
| 239 |
+
|
| 240 |
+
if not is_ready():
|
| 241 |
+
raise FileNotFoundError(
|
| 242 |
+
f"{desc} checkpoint(s) still missing after downloading from "
|
| 243 |
+
f"Hugging Face repo '{repo_id}'. The local cache may be from "
|
| 244 |
+
"a previous partial download; please remove it and retry, "
|
| 245 |
+
"or download manually."
|
| 246 |
+
)
|
| 247 |
+
|
| 248 |
+
def build_sampler(self):
|
| 249 |
+
if self.cfg.model_name == "graspgen":
|
| 250 |
+
gripper_config = os.path.expanduser(
|
| 251 |
+
self.cfg.graspgen.gripper_config
|
| 252 |
+
)
|
| 253 |
+
checkpoints_dir = os.path.dirname(gripper_config)
|
| 254 |
+
|
| 255 |
+
def _graspgen_ready() -> bool:
|
| 256 |
+
if not os.path.exists(gripper_config):
|
| 257 |
+
return False
|
| 258 |
+
required = self._extract_checkpoint_filenames(gripper_config)
|
| 259 |
+
return all(
|
| 260 |
+
os.path.exists(os.path.join(checkpoints_dir, name))
|
| 261 |
+
for name in required
|
| 262 |
+
)
|
| 263 |
+
|
| 264 |
+
self._ensure_checkpoints(
|
| 265 |
+
_graspgen_ready,
|
| 266 |
+
"adithyamurali/GraspGenModels",
|
| 267 |
+
"checkpoints/*",
|
| 268 |
+
"~/.cache/huggingface/hub/GraspGenModels",
|
| 269 |
+
"GraspGen",
|
| 270 |
+
)
|
| 271 |
+
grasp_cfg = self.modules["model_cfg"](gripper_config)
|
| 272 |
+
return self.modules["GraspGenSampler"](grasp_cfg)
|
| 273 |
+
if self.cfg.model_name == "graspgenx":
|
| 274 |
+
checkpoint_root = os.path.expanduser(
|
| 275 |
+
self.cfg.graspgen.checkpoint_root
|
| 276 |
+
)
|
| 277 |
+
gen_dir = os.path.join(checkpoint_root, "gen")
|
| 278 |
+
dis_dir = os.path.join(checkpoint_root, "dis")
|
| 279 |
+
|
| 280 |
+
def _graspgenx_ready() -> bool:
|
| 281 |
+
return (
|
| 282 |
+
os.path.exists(os.path.join(gen_dir, "config.yaml"))
|
| 283 |
+
and self._dir_has_pth(gen_dir, self.cfg.graspgen.gen_pth)
|
| 284 |
+
and self._dir_has_pth(dis_dir, self.cfg.graspgen.dis_pth)
|
| 285 |
+
)
|
| 286 |
+
|
| 287 |
+
self._ensure_checkpoints(
|
| 288 |
+
_graspgenx_ready,
|
| 289 |
+
"adithyamurali/GraspGenXModel",
|
| 290 |
+
"release/*",
|
| 291 |
+
"~/.cache/huggingface/hub/GraspGenXModels",
|
| 292 |
+
"GraspGenX",
|
| 293 |
+
)
|
| 294 |
+
model_cfg = self.modules["model_cfg"](
|
| 295 |
+
os.path.join(checkpoint_root, "gen"),
|
| 296 |
+
os.path.join(checkpoint_root, "dis"),
|
| 297 |
+
self.cfg.graspgen.gen_pth,
|
| 298 |
+
self.cfg.graspgen.dis_pth,
|
| 299 |
+
)
|
| 300 |
+
return self.modules["GraspGenSampler"](
|
| 301 |
+
model_cfg,
|
| 302 |
+
self.cfg.gripper_name,
|
| 303 |
+
assets_dir=self.cfg.graspgen.assets_dir,
|
| 304 |
+
)
|
| 305 |
+
|
| 306 |
+
def update_urdf(self, urdf_path: str | None, grasp_path: str) -> None:
|
| 307 |
+
if urdf_path is None or not self.cfg.update_urdf:
|
| 308 |
+
return
|
| 309 |
+
|
| 310 |
+
URDFFile(urdf_path).write(
|
| 311 |
+
{
|
| 312 |
+
"custom_data/affordance/affordance_annot": os.path.relpath(
|
| 313 |
+
grasp_path,
|
| 314 |
+
os.path.dirname(urdf_path),
|
| 315 |
+
),
|
| 316 |
+
}
|
| 317 |
+
)
|
| 318 |
+
|
| 319 |
+
def get_part_seg_mesh_transform(self, urdf: URDFFile) -> dict:
|
| 320 |
+
return {
|
| 321 |
+
"origin_xyz": urdf.read(
|
| 322 |
+
".//custom_data/affordance/visual_seg/origin",
|
| 323 |
+
attr="xyz",
|
| 324 |
+
default="0 0 0",
|
| 325 |
+
),
|
| 326 |
+
"origin_rpy": urdf.read(
|
| 327 |
+
".//custom_data/affordance/visual_seg/origin",
|
| 328 |
+
attr="rpy",
|
| 329 |
+
default="0 0 0",
|
| 330 |
+
),
|
| 331 |
+
"scale": urdf.read(
|
| 332 |
+
".//custom_data/affordance/visual_seg/geometry/mesh",
|
| 333 |
+
attr="scale",
|
| 334 |
+
default="1.0 1.0 1.0",
|
| 335 |
+
),
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
def add_grasps_to_affordance(
|
| 339 |
+
self,
|
| 340 |
+
affordance_payload: dict,
|
| 341 |
+
grasps: np.ndarray,
|
| 342 |
+
confidences: np.ndarray,
|
| 343 |
+
part_grasp_ids: dict[int, list[int]] | None,
|
| 344 |
+
) -> None:
|
| 345 |
+
for affordance in affordance_payload.get("affordances", []):
|
| 346 |
+
if (
|
| 347 |
+
not isinstance(affordance, dict)
|
| 348 |
+
or affordance.get("id") is None
|
| 349 |
+
):
|
| 350 |
+
continue
|
| 351 |
+
affordance.pop("grasps", None)
|
| 352 |
+
affordance.pop("confidences", None)
|
| 353 |
+
grasp_ids = part_grasp_ids.get(int(affordance["id"]), [])
|
| 354 |
+
grasp_group = {}
|
| 355 |
+
for grasp_idx in grasp_ids:
|
| 356 |
+
grasp = grasps[grasp_idx]
|
| 357 |
+
quaternion = tra.quaternion_from_matrix(grasp[:3, :3])
|
| 358 |
+
grasp_group[f"grasp_{grasp_idx}"] = {
|
| 359 |
+
"confidence": float(confidences[grasp_idx]),
|
| 360 |
+
"position": grasp[:3, 3].tolist(),
|
| 361 |
+
"orientation": {
|
| 362 |
+
"w": float(quaternion[0]),
|
| 363 |
+
"xyz": quaternion[1:].tolist(),
|
| 364 |
+
},
|
| 365 |
+
}
|
| 366 |
+
affordance["grasp_group"] = grasp_group
|
| 367 |
+
|
| 368 |
+
def infer_grasps(
|
| 369 |
+
self,
|
| 370 |
+
mesh: trimesh.Trimesh,
|
| 371 |
+
mesh_path: str,
|
| 372 |
+
) -> tuple[np.ndarray, np.ndarray, np.ndarray] | None:
|
| 373 |
+
points = sample_surface_points(
|
| 374 |
+
mesh, self.cfg.graspgen.num_sample_points
|
| 375 |
+
)
|
| 376 |
+
center_transform = tra.translation_matrix(-points.mean(axis=0))
|
| 377 |
+
centered_points = tra.transform_points(
|
| 378 |
+
points, center_transform
|
| 379 |
+
).astype(np.float32)
|
| 380 |
+
topk_limit = (
|
| 381 |
+
self.cfg.graspgen.topk_num_grasps
|
| 382 |
+
if self.cfg.graspgen.return_topk
|
| 383 |
+
else -1
|
| 384 |
+
)
|
| 385 |
+
grasps, confidences = self.modules["GraspGenSampler"].run_inference(
|
| 386 |
+
centered_points,
|
| 387 |
+
self.pipeline,
|
| 388 |
+
grasp_threshold=self.cfg.graspgen.grasp_threshold,
|
| 389 |
+
num_grasps=self.cfg.graspgen.num_grasps,
|
| 390 |
+
topk_num_grasps=topk_limit,
|
| 391 |
+
min_grasps=self.cfg.graspgen.min_grasps,
|
| 392 |
+
max_tries=self.cfg.graspgen.max_tries,
|
| 393 |
+
remove_outliers=self.cfg.graspgen.remove_outliers,
|
| 394 |
+
)
|
| 395 |
+
|
| 396 |
+
confidences = confidences.detach().cpu().numpy()
|
| 397 |
+
grasps = grasps.detach().cpu().numpy()
|
| 398 |
+
if len(grasps) == 0:
|
| 399 |
+
logger.error(f"No grasp poses generated for mesh: {mesh_path}")
|
| 400 |
+
return None
|
| 401 |
+
|
| 402 |
+
grasps = np.asarray(
|
| 403 |
+
[tra.inverse_matrix(center_transform) @ grasp for grasp in grasps]
|
| 404 |
+
)
|
| 405 |
+
return grasps, confidences
|
| 406 |
+
|
| 407 |
+
def get_grasp_centers(
|
| 408 |
+
self,
|
| 409 |
+
grasps: np.ndarray,
|
| 410 |
+
) -> np.ndarray:
|
| 411 |
+
local_centers = np.repeat(
|
| 412 |
+
np.asarray(self.finger_center, dtype=np.float64)[None, :],
|
| 413 |
+
len(grasps),
|
| 414 |
+
axis=0,
|
| 415 |
+
)
|
| 416 |
+
grasp_center_points = (
|
| 417 |
+
np.einsum(
|
| 418 |
+
"nij,nj->ni",
|
| 419 |
+
grasps[:, :3, :3],
|
| 420 |
+
local_centers,
|
| 421 |
+
)
|
| 422 |
+
+ grasps[:, :3, 3]
|
| 423 |
+
)
|
| 424 |
+
return grasp_center_points
|
| 425 |
+
|
| 426 |
+
def build_local_contact_samples(self) -> tuple[np.ndarray, np.ndarray]:
|
| 427 |
+
geometry = GRIPPER_GEOMETRY_CACHE.get(self.cfg.gripper_name)
|
| 428 |
+
if geometry is None:
|
| 429 |
+
return (
|
| 430 |
+
np.asarray(self.finger_center, dtype=np.float64)[None, :],
|
| 431 |
+
np.array([[1.0, 0.0, 0.0]], dtype=np.float64),
|
| 432 |
+
)
|
| 433 |
+
|
| 434 |
+
half_width = float(geometry["width"]) / 2.0
|
| 435 |
+
depth = float(geometry["depth"])
|
| 436 |
+
z_values = np.linspace(depth - 0.015, depth, 6, dtype=np.float64)
|
| 437 |
+
points = []
|
| 438 |
+
directions = []
|
| 439 |
+
for z_value in z_values:
|
| 440 |
+
points.extend(
|
| 441 |
+
[
|
| 442 |
+
[-half_width, 0.0, z_value],
|
| 443 |
+
[half_width, 0.0, z_value],
|
| 444 |
+
]
|
| 445 |
+
)
|
| 446 |
+
directions.extend(
|
| 447 |
+
[
|
| 448 |
+
[1.0, 0.0, 0.0],
|
| 449 |
+
[-1.0, 0.0, 0.0],
|
| 450 |
+
]
|
| 451 |
+
)
|
| 452 |
+
return (
|
| 453 |
+
np.asarray(points, dtype=np.float64),
|
| 454 |
+
np.asarray(directions, dtype=np.float64),
|
| 455 |
+
)
|
| 456 |
+
|
| 457 |
+
def get_grasp_contact_rays(
|
| 458 |
+
self,
|
| 459 |
+
grasps: np.ndarray,
|
| 460 |
+
) -> tuple[np.ndarray, np.ndarray]:
|
| 461 |
+
local_contact_points = np.repeat(
|
| 462 |
+
self.local_contact_points[None, :, :],
|
| 463 |
+
len(grasps),
|
| 464 |
+
axis=0,
|
| 465 |
+
)
|
| 466 |
+
local_closing_directions = np.repeat(
|
| 467 |
+
self.local_closing_directions[None, :, :],
|
| 468 |
+
len(grasps),
|
| 469 |
+
axis=0,
|
| 470 |
+
)
|
| 471 |
+
ray_origins = (
|
| 472 |
+
np.einsum(
|
| 473 |
+
"nij,nkj->nki",
|
| 474 |
+
grasps[:, :3, :3],
|
| 475 |
+
local_contact_points,
|
| 476 |
+
)
|
| 477 |
+
+ grasps[:, None, :3, 3]
|
| 478 |
+
)
|
| 479 |
+
ray_directions = np.einsum(
|
| 480 |
+
"nij,nkj->nki",
|
| 481 |
+
grasps[:, :3, :3],
|
| 482 |
+
local_closing_directions,
|
| 483 |
+
)
|
| 484 |
+
ray_norms = np.linalg.norm(ray_directions, axis=-1, keepdims=True)
|
| 485 |
+
ray_directions = np.divide(
|
| 486 |
+
ray_directions,
|
| 487 |
+
ray_norms,
|
| 488 |
+
out=np.zeros_like(ray_directions),
|
| 489 |
+
where=ray_norms > 0.0,
|
| 490 |
+
)
|
| 491 |
+
return ray_origins, ray_directions
|
| 492 |
+
|
| 493 |
+
def bind_contact_rays_to_faces(
|
| 494 |
+
self,
|
| 495 |
+
part_mesh: trimesh.Trimesh,
|
| 496 |
+
ray_origins: np.ndarray,
|
| 497 |
+
ray_directions: np.ndarray,
|
| 498 |
+
) -> np.ndarray:
|
| 499 |
+
ray_origins = np.asarray(ray_origins, dtype=np.float64)
|
| 500 |
+
ray_directions = np.asarray(ray_directions, dtype=np.float64)
|
| 501 |
+
|
| 502 |
+
if (
|
| 503 |
+
not isinstance(part_mesh, trimesh.Trimesh)
|
| 504 |
+
or len(part_mesh.faces) == 0
|
| 505 |
+
):
|
| 506 |
+
return np.full(len(ray_origins), -1, dtype=np.int64)
|
| 507 |
+
|
| 508 |
+
try:
|
| 509 |
+
face_ids = part_mesh.ray.intersects_first(
|
| 510 |
+
ray_origins,
|
| 511 |
+
ray_directions,
|
| 512 |
+
)
|
| 513 |
+
return np.asarray(face_ids, dtype=np.int64)
|
| 514 |
+
except Exception as exc:
|
| 515 |
+
logger.warning(
|
| 516 |
+
"Failed to raycast grasp closing directions to part mesh: "
|
| 517 |
+
f"{exc}"
|
| 518 |
+
)
|
| 519 |
+
return np.full(len(ray_origins), -1, dtype=np.int64)
|
| 520 |
+
|
| 521 |
+
def classify_grasps_by_contact(
|
| 522 |
+
self,
|
| 523 |
+
grasps: np.ndarray,
|
| 524 |
+
part_mesh: trimesh.Trimesh,
|
| 525 |
+
face_ids: np.ndarray | None,
|
| 526 |
+
) -> tuple[np.ndarray | None, np.ndarray | None]:
|
| 527 |
+
if face_ids is None:
|
| 528 |
+
return None, None
|
| 529 |
+
|
| 530 |
+
face_ids = np.asarray(face_ids, dtype=np.int64)
|
| 531 |
+
if len(face_ids) != len(part_mesh.faces):
|
| 532 |
+
raise ValueError(
|
| 533 |
+
"face_ids length must match segmentation mesh faces, got "
|
| 534 |
+
f"{len(face_ids)} and {len(part_mesh.faces)}"
|
| 535 |
+
)
|
| 536 |
+
|
| 537 |
+
ray_origins, ray_directions = self.get_grasp_contact_rays(grasps)
|
| 538 |
+
num_grasps, num_contact_points, _ = ray_origins.shape
|
| 539 |
+
logger.info(
|
| 540 |
+
"Binding {} grasp closing rays to {} part faces.".format(
|
| 541 |
+
num_grasps * num_contact_points,
|
| 542 |
+
len(part_mesh.faces),
|
| 543 |
+
)
|
| 544 |
+
)
|
| 545 |
+
nearest_face_ids = self.bind_contact_rays_to_faces(
|
| 546 |
+
part_mesh,
|
| 547 |
+
ray_origins.reshape(-1, 3),
|
| 548 |
+
ray_directions.reshape(-1, 3),
|
| 549 |
+
).reshape(num_grasps, num_contact_points)
|
| 550 |
+
contact_part_ids = self.parts_for_faces(
|
| 551 |
+
nearest_face_ids.reshape(-1),
|
| 552 |
+
face_ids,
|
| 553 |
+
).reshape(num_grasps, num_contact_points)
|
| 554 |
+
|
| 555 |
+
grasp_part_ids = np.full(num_grasps, -1, dtype=np.int64)
|
| 556 |
+
contact_scores = np.zeros(num_grasps, dtype=np.float64)
|
| 557 |
+
for grasp_idx, part_ids in enumerate(contact_part_ids):
|
| 558 |
+
valid_part_ids = part_ids[part_ids >= 0]
|
| 559 |
+
if len(valid_part_ids) == 0:
|
| 560 |
+
continue
|
| 561 |
+
unique_ids, counts = np.unique(
|
| 562 |
+
valid_part_ids,
|
| 563 |
+
return_counts=True,
|
| 564 |
+
)
|
| 565 |
+
best_idx = int(np.argmax(counts))
|
| 566 |
+
grasp_part_ids[grasp_idx] = int(unique_ids[best_idx])
|
| 567 |
+
contact_scores[grasp_idx] = float(counts[best_idx]) / float(
|
| 568 |
+
num_contact_points
|
| 569 |
+
)
|
| 570 |
+
|
| 571 |
+
return grasp_part_ids, contact_scores
|
| 572 |
+
|
| 573 |
+
def parts_for_faces(
|
| 574 |
+
self,
|
| 575 |
+
nearest_face_ids: np.ndarray,
|
| 576 |
+
face_ids: np.ndarray | None,
|
| 577 |
+
) -> np.ndarray | None:
|
| 578 |
+
if face_ids is None:
|
| 579 |
+
return None
|
| 580 |
+
|
| 581 |
+
nearest_face_ids = np.asarray(nearest_face_ids, dtype=np.int64)
|
| 582 |
+
grasp_part_ids = np.full(len(nearest_face_ids), -1, dtype=np.int64)
|
| 583 |
+
valid_mask = (nearest_face_ids >= 0) & (
|
| 584 |
+
nearest_face_ids < len(face_ids)
|
| 585 |
+
)
|
| 586 |
+
grasp_part_ids[valid_mask] = face_ids[nearest_face_ids[valid_mask]]
|
| 587 |
+
return grasp_part_ids
|
| 588 |
+
|
| 589 |
+
def pick_top_grasps(
|
| 590 |
+
self,
|
| 591 |
+
grasps: np.ndarray,
|
| 592 |
+
confidences: np.ndarray,
|
| 593 |
+
part_ids: np.ndarray | None,
|
| 594 |
+
graspable_part_ids: set[int] | None = None,
|
| 595 |
+
part_scores: np.ndarray | None = None,
|
| 596 |
+
) -> tuple[
|
| 597 |
+
np.ndarray, np.ndarray, np.ndarray, dict[int, list[int]] | None
|
| 598 |
+
]:
|
| 599 |
+
if part_ids is None:
|
| 600 |
+
return (
|
| 601 |
+
grasps,
|
| 602 |
+
confidences,
|
| 603 |
+
np.arange(len(grasps), dtype=np.int64),
|
| 604 |
+
None,
|
| 605 |
+
)
|
| 606 |
+
|
| 607 |
+
part_ids = np.asarray(part_ids, dtype=np.int64)
|
| 608 |
+
selected_indices = []
|
| 609 |
+
selected_part_ids = []
|
| 610 |
+
for part_id in sorted(
|
| 611 |
+
int(pid) for pid in np.unique(part_ids[part_ids >= 0])
|
| 612 |
+
):
|
| 613 |
+
if (
|
| 614 |
+
graspable_part_ids is not None
|
| 615 |
+
and part_id not in graspable_part_ids
|
| 616 |
+
):
|
| 617 |
+
continue
|
| 618 |
+
part_indices = np.flatnonzero(part_ids == part_id)
|
| 619 |
+
if part_scores is None:
|
| 620 |
+
rank = np.argsort(
|
| 621 |
+
-confidences[part_indices],
|
| 622 |
+
kind="mergesort",
|
| 623 |
+
)
|
| 624 |
+
else:
|
| 625 |
+
rank = np.lexsort(
|
| 626 |
+
(
|
| 627 |
+
-confidences[part_indices],
|
| 628 |
+
-part_scores[part_indices],
|
| 629 |
+
)
|
| 630 |
+
)
|
| 631 |
+
ranked_indices = part_indices[rank]
|
| 632 |
+
if len(ranked_indices) > self.cfg.grasps_per_part:
|
| 633 |
+
selected = ranked_indices[: self.cfg.grasps_per_part]
|
| 634 |
+
else:
|
| 635 |
+
selected = ranked_indices
|
| 636 |
+
selected_indices.extend(int(idx) for idx in selected)
|
| 637 |
+
selected_part_ids.extend([part_id] * len(selected))
|
| 638 |
+
|
| 639 |
+
selected_indices = np.asarray(selected_indices, dtype=np.int64)
|
| 640 |
+
if len(selected_indices) == 0:
|
| 641 |
+
return (
|
| 642 |
+
grasps[:0],
|
| 643 |
+
confidences[:0],
|
| 644 |
+
selected_indices,
|
| 645 |
+
{},
|
| 646 |
+
)
|
| 647 |
+
|
| 648 |
+
part_grasp_ids: dict[int, list[int]] = {}
|
| 649 |
+
for new_idx, part_id in enumerate(selected_part_ids):
|
| 650 |
+
part_grasp_ids.setdefault(part_id, []).append(new_idx)
|
| 651 |
+
|
| 652 |
+
return (
|
| 653 |
+
grasps[selected_indices],
|
| 654 |
+
confidences[selected_indices],
|
| 655 |
+
selected_indices,
|
| 656 |
+
part_grasp_ids,
|
| 657 |
+
)
|
| 658 |
+
|
| 659 |
+
def select_grasps(
|
| 660 |
+
self,
|
| 661 |
+
grasps: np.ndarray,
|
| 662 |
+
confidences: np.ndarray,
|
| 663 |
+
part_mesh: trimesh.Trimesh,
|
| 664 |
+
face_ids: np.ndarray,
|
| 665 |
+
affordance_payload: dict,
|
| 666 |
+
) -> tuple[np.ndarray, np.ndarray, np.ndarray, dict[int, list[int]]]:
|
| 667 |
+
original_count = len(grasps)
|
| 668 |
+
|
| 669 |
+
# Raycast from fingertip samples along the gripper closing direction.
|
| 670 |
+
grasp_center_points = self.get_grasp_centers(grasps)
|
| 671 |
+
grasp_part_ids, contact_scores = self.classify_grasps_by_contact(
|
| 672 |
+
grasps,
|
| 673 |
+
part_mesh,
|
| 674 |
+
face_ids,
|
| 675 |
+
)
|
| 676 |
+
|
| 677 |
+
affordances = affordance_payload.get("affordances", [])
|
| 678 |
+
graspable_part_ids = {
|
| 679 |
+
int(affordance["id"])
|
| 680 |
+
for affordance in affordances
|
| 681 |
+
if isinstance(affordance, dict)
|
| 682 |
+
and affordance.get("id") is not None
|
| 683 |
+
and affordance.get("graspable") is True
|
| 684 |
+
}
|
| 685 |
+
|
| 686 |
+
# Select top grasps per part
|
| 687 |
+
grasps, confidences, selected_indices, part_grasp_ids = (
|
| 688 |
+
self.pick_top_grasps(
|
| 689 |
+
grasps,
|
| 690 |
+
confidences,
|
| 691 |
+
grasp_part_ids,
|
| 692 |
+
graspable_part_ids,
|
| 693 |
+
contact_scores,
|
| 694 |
+
)
|
| 695 |
+
)
|
| 696 |
+
grasp_center_points = grasp_center_points[selected_indices]
|
| 697 |
+
if len(grasps) == 0:
|
| 698 |
+
logger.info(
|
| 699 |
+
"Selected per-part grasps: {} -> 0 grasps across 0 parts".format(
|
| 700 |
+
original_count,
|
| 701 |
+
)
|
| 702 |
+
)
|
| 703 |
+
return grasps, confidences, grasp_center_points, part_grasp_ids
|
| 704 |
+
|
| 705 |
+
logger.info(
|
| 706 |
+
"Selected per-part grasps: {} -> {} grasps across {} parts, confidences range from {:.4f} to {:.4f}".format(
|
| 707 |
+
original_count,
|
| 708 |
+
len(grasps),
|
| 709 |
+
len(part_grasp_ids),
|
| 710 |
+
np.min(confidences),
|
| 711 |
+
np.max(confidences),
|
| 712 |
+
)
|
| 713 |
+
)
|
| 714 |
+
return grasps, confidences, grasp_center_points, part_grasp_ids
|
| 715 |
+
|
| 716 |
+
def process(
|
| 717 |
+
self,
|
| 718 |
+
urdf_path: str,
|
| 719 |
+
) -> bool:
|
| 720 |
+
try:
|
| 721 |
+
return self._process_impl(urdf_path)
|
| 722 |
+
except Exception as exc:
|
| 723 |
+
logger.error(
|
| 724 |
+
"Grasp generation failed for URDF {}: {}".format(
|
| 725 |
+
urdf_path,
|
| 726 |
+
exc,
|
| 727 |
+
)
|
| 728 |
+
)
|
| 729 |
+
return False
|
| 730 |
+
|
| 731 |
+
def _process_impl(
|
| 732 |
+
self,
|
| 733 |
+
urdf_path: str,
|
| 734 |
+
) -> bool:
|
| 735 |
+
urdf = URDFFile(urdf_path)
|
| 736 |
+
mesh_path = urdf.get_mesh_path(self.cfg.mesh_type)
|
| 737 |
+
mesh_transform = urdf.get_mesh_transform(self.cfg.mesh_type)
|
| 738 |
+
seg_mesh_path = urdf.get_mesh_part_seg_path()
|
| 739 |
+
seg_mesh_transform = self.get_part_seg_mesh_transform(urdf)
|
| 740 |
+
affordance_path = urdf.get_affordance_annot_path()
|
| 741 |
+
affordance_payload = load_json(affordance_path)
|
| 742 |
+
|
| 743 |
+
if not self.cfg.overwrite:
|
| 744 |
+
has_existing_grasp_group = any(
|
| 745 |
+
isinstance(affordance, dict) and "grasp_group" in affordance
|
| 746 |
+
for affordance in affordance_payload.get("affordances", [])
|
| 747 |
+
)
|
| 748 |
+
if has_existing_grasp_group:
|
| 749 |
+
logger.info(
|
| 750 |
+
f"Skip existing grasp poses in affordance JSON: {affordance_path}"
|
| 751 |
+
)
|
| 752 |
+
return True
|
| 753 |
+
|
| 754 |
+
logger.info(f"Generating grasps for mesh: {mesh_path}")
|
| 755 |
+
mesh = load_mesh(
|
| 756 |
+
mesh_path,
|
| 757 |
+
apply_origin=True,
|
| 758 |
+
apply_scale=True,
|
| 759 |
+
**mesh_transform,
|
| 760 |
+
)
|
| 761 |
+
|
| 762 |
+
seg_mesh, face_ids = load_mesh(
|
| 763 |
+
seg_mesh_path,
|
| 764 |
+
apply_origin=True,
|
| 765 |
+
apply_scale=True,
|
| 766 |
+
**seg_mesh_transform,
|
| 767 |
+
)
|
| 768 |
+
|
| 769 |
+
grasp_result = self.infer_grasps(mesh, mesh_path)
|
| 770 |
+
if grasp_result is None:
|
| 771 |
+
return False
|
| 772 |
+
grasps, confidences = grasp_result
|
| 773 |
+
grasps, confidences, grasp_center_points, part_grasp_ids = (
|
| 774 |
+
self.select_grasps(
|
| 775 |
+
grasps,
|
| 776 |
+
confidences,
|
| 777 |
+
seg_mesh,
|
| 778 |
+
face_ids,
|
| 779 |
+
affordance_payload,
|
| 780 |
+
)
|
| 781 |
+
)
|
| 782 |
+
|
| 783 |
+
self.add_grasps_to_affordance(
|
| 784 |
+
affordance_payload,
|
| 785 |
+
grasps,
|
| 786 |
+
confidences,
|
| 787 |
+
part_grasp_ids,
|
| 788 |
+
)
|
| 789 |
+
write_json(affordance_payload, affordance_path)
|
| 790 |
+
logger.info(f"Saved predicted grasp poses to {affordance_path}")
|
| 791 |
+
|
| 792 |
+
self.update_urdf(urdf_path, affordance_path)
|
| 793 |
+
logger.info(
|
| 794 |
+
"Grasp generation succeeded: {} -> {} ({} grasps)".format(
|
| 795 |
+
mesh_path,
|
| 796 |
+
affordance_path,
|
| 797 |
+
len(grasps),
|
| 798 |
+
)
|
| 799 |
+
)
|
| 800 |
+
if self.cfg.visualize_grasps:
|
| 801 |
+
gripper_mesh = load_mesh(self.cfg.gripper_mesh_path)
|
| 802 |
+
self.grasp_visualizer = show_grasps(
|
| 803 |
+
mesh,
|
| 804 |
+
grasps,
|
| 805 |
+
confidences,
|
| 806 |
+
self.cfg.gripper_name,
|
| 807 |
+
self.cfg.model_name,
|
| 808 |
+
grasp_center_points=grasp_center_points,
|
| 809 |
+
visualizer=self.grasp_visualizer,
|
| 810 |
+
visualizer_port=self.cfg.visualizer_port,
|
| 811 |
+
gripper_info=self.pipeline.get_gripper_info()
|
| 812 |
+
if self.cfg.model_name == "graspgenx"
|
| 813 |
+
else None,
|
| 814 |
+
gripper_mesh=gripper_mesh,
|
| 815 |
+
)
|
| 816 |
+
return True
|
| 817 |
+
|
| 818 |
+
|
| 819 |
+
def run_grasp(cfg: GraspPoseConfig) -> None:
|
| 820 |
+
generator = GraspGenerator(cfg)
|
| 821 |
+
for urdf_path in cfg.urdf_paths:
|
| 822 |
+
generator.process(urdf_path)
|
| 823 |
+
|
| 824 |
+
if cfg.visualize_grasps and generator.grasp_visualizer is not None:
|
| 825 |
+
logger.info(
|
| 826 |
+
"Keeping grasp visualization alive at http://localhost:{} "
|
| 827 |
+
"(press Ctrl+C to exit).".format(cfg.visualizer_port)
|
| 828 |
+
)
|
| 829 |
+
try:
|
| 830 |
+
while True:
|
| 831 |
+
time.sleep(1)
|
| 832 |
+
except KeyboardInterrupt:
|
| 833 |
+
logger.info("Stopping grasp visualization.")
|
| 834 |
+
|
| 835 |
+
|
| 836 |
+
def entrypoint(*args, **kwargs) -> None:
|
| 837 |
+
cfg = tyro.cli(GraspPoseConfig)
|
| 838 |
+
run_grasp(cfg)
|
| 839 |
+
|
| 840 |
+
|
| 841 |
+
if __name__ == "__main__":
|
| 842 |
+
entrypoint()
|
embodied_gen/scripts/affordance_annot/part_seg.py
ADDED
|
@@ -0,0 +1,697 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Project EmbodiedGen
|
| 2 |
+
#
|
| 3 |
+
# Copyright (c) 2025 Horizon Robotics. All Rights Reserved.
|
| 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
|
| 14 |
+
# implied. See the License for the specific language governing
|
| 15 |
+
# permissions and limitations under the License.
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
import os
|
| 19 |
+
import shutil
|
| 20 |
+
import sys
|
| 21 |
+
from dataclasses import asdict, dataclass, field
|
| 22 |
+
from typing import Literal
|
| 23 |
+
|
| 24 |
+
import json_repair
|
| 25 |
+
from embodied_gen.utils.general import filter_warnings
|
| 26 |
+
from embodied_gen.utils.gpt_clients import GPT_CLIENT
|
| 27 |
+
|
| 28 |
+
filter_warnings()
|
| 29 |
+
|
| 30 |
+
import numpy as np
|
| 31 |
+
import trimesh
|
| 32 |
+
import tyro
|
| 33 |
+
from huggingface_hub import snapshot_download
|
| 34 |
+
from embodied_gen.utils.general import set_seed
|
| 35 |
+
from embodied_gen.utils.geometry import (
|
| 36 |
+
EnclosedFaceLabelFillConfig,
|
| 37 |
+
SmoothFaceLabelMergeConfig,
|
| 38 |
+
fill_small_enclosed_face_labels,
|
| 39 |
+
merge_smooth_face_labels,
|
| 40 |
+
normalize_mesh,
|
| 41 |
+
)
|
| 42 |
+
from embodied_gen.utils.io_utils import URDFFile, load_mesh, save_mesh
|
| 43 |
+
from embodied_gen.utils.log import logger
|
| 44 |
+
from embodied_gen.utils.process_media import render_asset3d
|
| 45 |
+
from embodied_gen.utils.vis_utils import PALETTE, collect_colors, render_grid
|
| 46 |
+
from embodied_gen.validators.quality_checkers import PartSegChecker
|
| 47 |
+
|
| 48 |
+
__all__ = [
|
| 49 |
+
"FaceIdPostProcessConfig",
|
| 50 |
+
"PartSegConfig",
|
| 51 |
+
"PartSegmenter",
|
| 52 |
+
"entrypoint",
|
| 53 |
+
]
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
@dataclass
|
| 57 |
+
class P3SAMConfig:
|
| 58 |
+
threshold: float = 0.95
|
| 59 |
+
post_process: bool = True
|
| 60 |
+
save_mid_res: bool = False
|
| 61 |
+
show_info: bool = True
|
| 62 |
+
is_parallel: bool = False
|
| 63 |
+
seed: int = 42
|
| 64 |
+
clean_mesh_flag: bool = False
|
| 65 |
+
prompt_bs: int = 8 # Lower this for reduced GPU memory usage.
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
@dataclass
|
| 69 |
+
class FaceIdPostProcessConfig:
|
| 70 |
+
enabled: bool = True
|
| 71 |
+
max_smooth_angle_deg: float = 5.0
|
| 72 |
+
vertex_merge_tolerance: float = 0.0
|
| 73 |
+
min_component_area_ratio: float = 0.0
|
| 74 |
+
min_component_faces: int = 1
|
| 75 |
+
preserve_negative: bool = False
|
| 76 |
+
fill_enclosed_components: bool = True
|
| 77 |
+
max_enclosed_component_area_ratio: float = 0.005
|
| 78 |
+
max_enclosed_component_faces: int = 100
|
| 79 |
+
min_enclosure_neighbor_ratio: float = 0.8
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
@dataclass
|
| 83 |
+
class PartSegConfig:
|
| 84 |
+
urdf_paths: list[str] = field(default_factory=list)
|
| 85 |
+
output_dirs: list[str] = field(default_factory=list)
|
| 86 |
+
model_name: str = "p3sam"
|
| 87 |
+
thirdparty_root: str = ""
|
| 88 |
+
mesh_type: Literal["visual", "collision"] = "visual"
|
| 89 |
+
merge_retries: int = 3
|
| 90 |
+
p3sam: P3SAMConfig = field(default_factory=P3SAMConfig)
|
| 91 |
+
face_id_postprocess: FaceIdPostProcessConfig = field(
|
| 92 |
+
default_factory=FaceIdPostProcessConfig
|
| 93 |
+
)
|
| 94 |
+
grid_num_images: int = 6
|
| 95 |
+
grid_rows: int = 2
|
| 96 |
+
grid_cols: int = 3
|
| 97 |
+
grid_image_size: int = 512
|
| 98 |
+
debug_mode: bool = False
|
| 99 |
+
overwrite: bool = True
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
class PartSegmenter:
|
| 103 |
+
def __init__(self, cfg: PartSegConfig):
|
| 104 |
+
self.validate_config(cfg)
|
| 105 |
+
self.cfg = cfg
|
| 106 |
+
self.seg_checker = PartSegChecker(GPT_CLIENT)
|
| 107 |
+
self.model = self.import_model()
|
| 108 |
+
self.pipeline = self.build_pipeline(self.model)
|
| 109 |
+
|
| 110 |
+
def validate_config(self, cfg: PartSegConfig) -> None:
|
| 111 |
+
if cfg.model_name != "p3sam":
|
| 112 |
+
raise ValueError(
|
| 113 |
+
"unsupported model '{}', available models: p3sam".format(
|
| 114 |
+
cfg.model_name
|
| 115 |
+
)
|
| 116 |
+
)
|
| 117 |
+
|
| 118 |
+
if cfg.model_name == "p3sam":
|
| 119 |
+
cfg.thirdparty_root = "thirdparty/Hunyuan3D-Part"
|
| 120 |
+
|
| 121 |
+
if not cfg.urdf_paths:
|
| 122 |
+
raise ValueError("urdf_paths must be provided.")
|
| 123 |
+
|
| 124 |
+
if len(cfg.output_dirs) == 0:
|
| 125 |
+
cfg.output_dirs = [
|
| 126 |
+
os.path.join(os.path.dirname(path), "affordance")
|
| 127 |
+
for path in cfg.urdf_paths
|
| 128 |
+
]
|
| 129 |
+
if len(cfg.urdf_paths) != len(cfg.output_dirs):
|
| 130 |
+
raise ValueError(
|
| 131 |
+
"urdf-derived mesh paths and output_dirs must have the same length, "
|
| 132 |
+
f"got {len(cfg.urdf_paths)} and {len(cfg.output_dirs)}."
|
| 133 |
+
)
|
| 134 |
+
|
| 135 |
+
def import_model(self):
|
| 136 |
+
if self.cfg.model_name == "p3sam":
|
| 137 |
+
p3sam_root = os.path.join(self.cfg.thirdparty_root, "P3-SAM")
|
| 138 |
+
p3sam_demo = os.path.join(p3sam_root, "demo")
|
| 139 |
+
for path in [p3sam_demo, p3sam_root]:
|
| 140 |
+
if path not in sys.path:
|
| 141 |
+
sys.path.append(path)
|
| 142 |
+
from embodied_gen.utils.monkey_patch.p3sam import (
|
| 143 |
+
monkey_patch_p3sam,
|
| 144 |
+
)
|
| 145 |
+
|
| 146 |
+
monkey_patch_p3sam()
|
| 147 |
+
from auto_mask import AutoMask
|
| 148 |
+
|
| 149 |
+
return AutoMask
|
| 150 |
+
|
| 151 |
+
raise ValueError("unsupported model '{}'".format(self.cfg.model_name))
|
| 152 |
+
|
| 153 |
+
def build_pipeline(self, model):
|
| 154 |
+
if self.cfg.model_name == "p3sam":
|
| 155 |
+
ckpt_path = os.path.expanduser(
|
| 156 |
+
"~/.cache/huggingface/hub/P3-SAM/p3sam/p3sam.safetensors"
|
| 157 |
+
)
|
| 158 |
+
if not os.path.exists(ckpt_path):
|
| 159 |
+
download_dir = os.path.expanduser(
|
| 160 |
+
"~/.cache/huggingface/hub/P3-SAM"
|
| 161 |
+
)
|
| 162 |
+
logger.info(
|
| 163 |
+
"Downloading P3-SAM weights from Hugging Face to {}. "
|
| 164 |
+
"First-time use may take several minutes; please wait patiently.".format(
|
| 165 |
+
ckpt_path
|
| 166 |
+
)
|
| 167 |
+
)
|
| 168 |
+
snapshot_download(
|
| 169 |
+
repo_id="tencent/Hunyuan3D-Part",
|
| 170 |
+
allow_patterns="p3sam/*",
|
| 171 |
+
local_dir=download_dir,
|
| 172 |
+
)
|
| 173 |
+
sonata_path = os.path.expanduser(
|
| 174 |
+
"~/.cache/huggingface/hub/sonata/sonata.pth"
|
| 175 |
+
)
|
| 176 |
+
if not os.path.exists(sonata_path):
|
| 177 |
+
logger.info(
|
| 178 |
+
"Downloading Sonata weights from Hugging Face to {}. "
|
| 179 |
+
"First-time use may take several minutes; please wait patiently.".format(
|
| 180 |
+
sonata_path
|
| 181 |
+
)
|
| 182 |
+
)
|
| 183 |
+
return model(
|
| 184 |
+
ckpt_path=ckpt_path,
|
| 185 |
+
threshold=self.cfg.p3sam.threshold,
|
| 186 |
+
post_process=self.cfg.p3sam.post_process,
|
| 187 |
+
)
|
| 188 |
+
|
| 189 |
+
raise ValueError("unsupported model '{}'".format(self.cfg.model_name))
|
| 190 |
+
|
| 191 |
+
def build_texture(
|
| 192 |
+
self,
|
| 193 |
+
mesh: trimesh.Trimesh,
|
| 194 |
+
face_colors: np.ndarray,
|
| 195 |
+
min_cell: int = 16,
|
| 196 |
+
max_tex: int = 8192,
|
| 197 |
+
) -> trimesh.Trimesh:
|
| 198 |
+
if len(face_colors) != len(mesh.faces):
|
| 199 |
+
raise ValueError(
|
| 200 |
+
f"face_colors length must match mesh faces, got {len(face_colors)} and {len(mesh.faces)}"
|
| 201 |
+
)
|
| 202 |
+
|
| 203 |
+
from PIL import Image, ImageDraw
|
| 204 |
+
|
| 205 |
+
faces = np.asarray(mesh.faces, dtype=np.int64)
|
| 206 |
+
vertices = np.asarray(mesh.vertices, dtype=np.float64)
|
| 207 |
+
n_faces = len(faces)
|
| 208 |
+
grid = int(np.ceil(np.sqrt(n_faces)))
|
| 209 |
+
textured_size = 1 << int(np.ceil(np.log2(max(1, grid * min_cell))))
|
| 210 |
+
textured_size = min(max_tex, max(min_cell, textured_size))
|
| 211 |
+
cell = textured_size / grid
|
| 212 |
+
|
| 213 |
+
image = Image.new(
|
| 214 |
+
"RGBA", (textured_size, textured_size), (255, 255, 255, 255)
|
| 215 |
+
)
|
| 216 |
+
draw = ImageDraw.Draw(image)
|
| 217 |
+
uv = np.zeros((n_faces * 3, 2), dtype=np.float64)
|
| 218 |
+
local_uv = np.asarray(
|
| 219 |
+
[
|
| 220 |
+
[0.18, 0.18],
|
| 221 |
+
[0.82, 0.18],
|
| 222 |
+
[0.18, 0.82],
|
| 223 |
+
],
|
| 224 |
+
dtype=np.float64,
|
| 225 |
+
)
|
| 226 |
+
|
| 227 |
+
for face_idx, color in enumerate(face_colors):
|
| 228 |
+
row, col = divmod(face_idx, grid)
|
| 229 |
+
rgba = tuple(int(channel) for channel in color)
|
| 230 |
+
x0 = int(np.floor(col * cell))
|
| 231 |
+
y0 = int(np.floor(row * cell))
|
| 232 |
+
x1 = int(np.ceil((col + 1) * cell))
|
| 233 |
+
y1 = int(np.ceil((row + 1) * cell))
|
| 234 |
+
draw.rectangle((x0, y0, x1, y1), fill=rgba)
|
| 235 |
+
uv[face_idx * 3 : face_idx * 3 + 3, 0] = (
|
| 236 |
+
col + local_uv[:, 0]
|
| 237 |
+
) / grid
|
| 238 |
+
uv[face_idx * 3 : face_idx * 3 + 3, 1] = (
|
| 239 |
+
1.0 - (row + local_uv[:, 1]) / grid
|
| 240 |
+
)
|
| 241 |
+
|
| 242 |
+
textured_mesh = trimesh.Trimesh(
|
| 243 |
+
vertices=vertices[faces.reshape(-1)],
|
| 244 |
+
faces=np.arange(n_faces * 3, dtype=np.int64).reshape(n_faces, 3),
|
| 245 |
+
process=False,
|
| 246 |
+
)
|
| 247 |
+
material = trimesh.visual.texture.SimpleMaterial(
|
| 248 |
+
image=image,
|
| 249 |
+
diffuse=[255, 255, 255, 255],
|
| 250 |
+
name="part_segmentation",
|
| 251 |
+
)
|
| 252 |
+
textured_mesh.visual = trimesh.visual.texture.TextureVisuals(
|
| 253 |
+
uv=uv, material=material
|
| 254 |
+
)
|
| 255 |
+
return textured_mesh
|
| 256 |
+
|
| 257 |
+
def remap_to_palette(self, face_ids: np.ndarray) -> np.ndarray:
|
| 258 |
+
face_ids = np.asarray(face_ids)
|
| 259 |
+
mapped_ids = np.full(face_ids.shape, -1, dtype=np.int64)
|
| 260 |
+
unique_ids = np.unique(face_ids[face_ids >= 0])
|
| 261 |
+
if len(unique_ids) > len(PALETTE):
|
| 262 |
+
return None
|
| 263 |
+
|
| 264 |
+
for palette_idx, part_id in enumerate(unique_ids):
|
| 265 |
+
palette_id = PALETTE[palette_idx][0]
|
| 266 |
+
mapped_ids[face_ids == part_id] = palette_id
|
| 267 |
+
return mapped_ids
|
| 268 |
+
|
| 269 |
+
def export_seg_mesh(
|
| 270 |
+
self,
|
| 271 |
+
mesh,
|
| 272 |
+
face_ids: np.ndarray,
|
| 273 |
+
output_dir: str,
|
| 274 |
+
mesh_transform: dict | None = None,
|
| 275 |
+
) -> None:
|
| 276 |
+
output_path = os.path.join(output_dir, "mesh_part_seg.glb")
|
| 277 |
+
palette_colors = {
|
| 278 |
+
palette_id: color for palette_id, color, _ in PALETTE
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
face_colors = np.zeros((len(face_ids), 4), dtype=np.uint8)
|
| 282 |
+
face_colors[:, 3] = 255
|
| 283 |
+
for idx, part_id in enumerate(face_ids):
|
| 284 |
+
if part_id < 0:
|
| 285 |
+
face_colors[idx, :3] = np.array([0, 0, 0], dtype=np.uint8)
|
| 286 |
+
else:
|
| 287 |
+
face_colors[idx, :3] = palette_colors[int(part_id)]
|
| 288 |
+
|
| 289 |
+
color_mesh = self.build_texture(mesh.copy(), face_colors)
|
| 290 |
+
color_mesh.metadata["face_ids"] = face_ids.tolist()
|
| 291 |
+
if mesh_transform is None:
|
| 292 |
+
save_mesh(
|
| 293 |
+
color_mesh, output_path, apply_origin=False, apply_scale=False
|
| 294 |
+
)
|
| 295 |
+
else:
|
| 296 |
+
save_mesh(
|
| 297 |
+
color_mesh,
|
| 298 |
+
output_path,
|
| 299 |
+
apply_origin=True,
|
| 300 |
+
apply_scale=True,
|
| 301 |
+
**mesh_transform,
|
| 302 |
+
)
|
| 303 |
+
return output_path
|
| 304 |
+
|
| 305 |
+
def count_parts(self, face_ids: np.ndarray) -> int:
|
| 306 |
+
valid_ids = np.asarray(face_ids)[np.asarray(face_ids) >= 0]
|
| 307 |
+
return int(len(np.unique(valid_ids)))
|
| 308 |
+
|
| 309 |
+
def postprocess_face_ids(
|
| 310 |
+
self,
|
| 311 |
+
mesh: trimesh.Trimesh,
|
| 312 |
+
face_ids: np.ndarray | None,
|
| 313 |
+
) -> np.ndarray | None:
|
| 314 |
+
if face_ids is None or not self.cfg.face_id_postprocess.enabled:
|
| 315 |
+
return face_ids
|
| 316 |
+
|
| 317 |
+
merge_cfg = SmoothFaceLabelMergeConfig(
|
| 318 |
+
max_smooth_angle_deg=self.cfg.face_id_postprocess.max_smooth_angle_deg,
|
| 319 |
+
vertex_merge_tolerance=self.cfg.face_id_postprocess.vertex_merge_tolerance,
|
| 320 |
+
min_component_area_ratio=self.cfg.face_id_postprocess.min_component_area_ratio,
|
| 321 |
+
min_component_faces=self.cfg.face_id_postprocess.min_component_faces,
|
| 322 |
+
preserve_negative=self.cfg.face_id_postprocess.preserve_negative,
|
| 323 |
+
)
|
| 324 |
+
face_ids, stats = merge_smooth_face_labels(mesh, face_ids, merge_cfg)
|
| 325 |
+
if stats["faces_changed"] > 0:
|
| 326 |
+
logger.info(
|
| 327 |
+
"Part seg face_id postprocess changed "
|
| 328 |
+
f"{stats['faces_changed']} faces across "
|
| 329 |
+
f"{stats['components_changed']} smooth components."
|
| 330 |
+
)
|
| 331 |
+
if self.cfg.face_id_postprocess.fill_enclosed_components:
|
| 332 |
+
fill_cfg = EnclosedFaceLabelFillConfig(
|
| 333 |
+
max_component_area_ratio=(
|
| 334 |
+
self.cfg.face_id_postprocess.max_enclosed_component_area_ratio
|
| 335 |
+
),
|
| 336 |
+
max_component_faces=(
|
| 337 |
+
self.cfg.face_id_postprocess.max_enclosed_component_faces
|
| 338 |
+
),
|
| 339 |
+
min_enclosure_neighbor_ratio=(
|
| 340 |
+
self.cfg.face_id_postprocess.min_enclosure_neighbor_ratio
|
| 341 |
+
),
|
| 342 |
+
vertex_merge_tolerance=(
|
| 343 |
+
self.cfg.face_id_postprocess.vertex_merge_tolerance
|
| 344 |
+
),
|
| 345 |
+
)
|
| 346 |
+
face_ids, stats = fill_small_enclosed_face_labels(
|
| 347 |
+
mesh,
|
| 348 |
+
face_ids,
|
| 349 |
+
fill_cfg,
|
| 350 |
+
)
|
| 351 |
+
if stats["faces_changed"] > 0:
|
| 352 |
+
logger.info(
|
| 353 |
+
"Part seg face_id postprocess filled "
|
| 354 |
+
f"{stats['faces_changed']} enclosed faces across "
|
| 355 |
+
f"{stats['components_filled']} components."
|
| 356 |
+
)
|
| 357 |
+
return face_ids
|
| 358 |
+
|
| 359 |
+
def update_urdf(
|
| 360 |
+
self,
|
| 361 |
+
urdf_path: str,
|
| 362 |
+
seg_mesh_path: str,
|
| 363 |
+
n_parts: int,
|
| 364 |
+
check_info: str,
|
| 365 |
+
mesh_transform: dict | None = None,
|
| 366 |
+
) -> None:
|
| 367 |
+
urdf_dir = os.path.dirname(urdf_path)
|
| 368 |
+
seg_mesh_file = (
|
| 369 |
+
os.path.relpath(
|
| 370 |
+
seg_mesh_path,
|
| 371 |
+
urdf_dir,
|
| 372 |
+
)
|
| 373 |
+
if seg_mesh_path
|
| 374 |
+
else None
|
| 375 |
+
)
|
| 376 |
+
origin_attrs = {"xyz": "0 0 0", "rpy": "0 0 0"}
|
| 377 |
+
mesh_attrs = {"filename": seg_mesh_file, "scale": "1 1 1"}
|
| 378 |
+
if mesh_transform:
|
| 379 |
+
origin_attrs = {
|
| 380 |
+
"xyz": str(mesh_transform["origin_xyz"]),
|
| 381 |
+
"rpy": str(mesh_transform["origin_rpy"]),
|
| 382 |
+
}
|
| 383 |
+
mesh_attrs["scale"] = str(mesh_transform["scale"])
|
| 384 |
+
|
| 385 |
+
updates = {
|
| 386 |
+
"custom_data/affordance/part_count": n_parts,
|
| 387 |
+
"custom_data/affordance/visual_seg": {
|
| 388 |
+
"text": None,
|
| 389 |
+
"clear_attrs": True,
|
| 390 |
+
"clear_children": True,
|
| 391 |
+
},
|
| 392 |
+
"custom_data/affordance/visual_seg/origin": {
|
| 393 |
+
"attrs": origin_attrs,
|
| 394 |
+
},
|
| 395 |
+
"custom_data/affordance/visual_seg/geometry/mesh": {
|
| 396 |
+
"attrs": mesh_attrs,
|
| 397 |
+
},
|
| 398 |
+
"custom_data/quality/PartSegChecker": check_info,
|
| 399 |
+
}
|
| 400 |
+
|
| 401 |
+
URDFFile(urdf_path).write(updates)
|
| 402 |
+
|
| 403 |
+
def render_video(
|
| 404 |
+
self, mesh_path: str, output_dir: str, output_subdir: str = "renders"
|
| 405 |
+
) -> list[str]:
|
| 406 |
+
return render_asset3d(
|
| 407 |
+
mesh_path,
|
| 408 |
+
output_root=output_dir,
|
| 409 |
+
distance=5.0,
|
| 410 |
+
num_images=128,
|
| 411 |
+
elevation=(45.0,),
|
| 412 |
+
gen_color_mp4=True,
|
| 413 |
+
output_subdir=output_subdir,
|
| 414 |
+
pbr_light_factor=1.5,
|
| 415 |
+
pbr_metallic=False,
|
| 416 |
+
)
|
| 417 |
+
|
| 418 |
+
def render_grid(
|
| 419 |
+
self,
|
| 420 |
+
mesh_path: str,
|
| 421 |
+
output_dir: str,
|
| 422 |
+
output_subdir: str = "renders",
|
| 423 |
+
) -> tuple[str, list[str]]:
|
| 424 |
+
grid_path, view_paths = render_grid(
|
| 425 |
+
mesh_path,
|
| 426 |
+
output_dir,
|
| 427 |
+
output_subdir=output_subdir,
|
| 428 |
+
num_images=self.cfg.grid_num_images,
|
| 429 |
+
grid_rows=self.cfg.grid_rows,
|
| 430 |
+
grid_cols=self.cfg.grid_cols,
|
| 431 |
+
view_size=self.cfg.grid_image_size,
|
| 432 |
+
)
|
| 433 |
+
return grid_path, view_paths
|
| 434 |
+
|
| 435 |
+
def render_seg_result(
|
| 436 |
+
self,
|
| 437 |
+
mesh: trimesh.Trimesh,
|
| 438 |
+
face_ids: np.ndarray,
|
| 439 |
+
output_dir: str,
|
| 440 |
+
mesh_transform: dict | None = None,
|
| 441 |
+
) -> tuple[str, str, str]:
|
| 442 |
+
seg_mesh_path = self.export_seg_mesh(
|
| 443 |
+
mesh,
|
| 444 |
+
face_ids,
|
| 445 |
+
output_dir,
|
| 446 |
+
mesh_transform,
|
| 447 |
+
)
|
| 448 |
+
mask_grid_path, _ = self.render_grid(
|
| 449 |
+
seg_mesh_path,
|
| 450 |
+
output_dir,
|
| 451 |
+
output_subdir="part_seg_renders",
|
| 452 |
+
)
|
| 453 |
+
color_names = collect_colors(face_ids)
|
| 454 |
+
logger.info(f"Part seg face colors: {color_names}")
|
| 455 |
+
return seg_mesh_path, mask_grid_path, color_names
|
| 456 |
+
|
| 457 |
+
def apply_merge_hints(
|
| 458 |
+
self,
|
| 459 |
+
face_ids: np.ndarray,
|
| 460 |
+
check_info: str,
|
| 461 |
+
) -> tuple[np.ndarray, bool]:
|
| 462 |
+
merge_groups = self.parse_merge_groups(check_info)
|
| 463 |
+
if not merge_groups:
|
| 464 |
+
return face_ids, False
|
| 465 |
+
|
| 466 |
+
merged_face_ids = np.asarray(face_ids).copy()
|
| 467 |
+
color_to_id = {
|
| 468 |
+
color_name.lower(): palette_id
|
| 469 |
+
for palette_id, _, color_name in PALETTE
|
| 470 |
+
}
|
| 471 |
+
applied_groups = 0
|
| 472 |
+
|
| 473 |
+
for group in merge_groups:
|
| 474 |
+
color_names = group.get("colors", [])
|
| 475 |
+
palette_ids = []
|
| 476 |
+
for color_name in color_names:
|
| 477 |
+
palette_id = color_to_id.get(str(color_name).strip().lower())
|
| 478 |
+
if palette_id is not None and palette_id not in palette_ids:
|
| 479 |
+
palette_ids.append(palette_id)
|
| 480 |
+
|
| 481 |
+
active_ids = [
|
| 482 |
+
palette_id
|
| 483 |
+
for palette_id in palette_ids
|
| 484 |
+
if np.any(merged_face_ids == palette_id)
|
| 485 |
+
]
|
| 486 |
+
if len(active_ids) < 2:
|
| 487 |
+
continue
|
| 488 |
+
|
| 489 |
+
target_id = max(
|
| 490 |
+
active_ids,
|
| 491 |
+
key=lambda palette_id: int(
|
| 492 |
+
np.count_nonzero(merged_face_ids == palette_id)
|
| 493 |
+
),
|
| 494 |
+
)
|
| 495 |
+
changed_faces = 0
|
| 496 |
+
for palette_id in active_ids:
|
| 497 |
+
if palette_id == target_id:
|
| 498 |
+
continue
|
| 499 |
+
source_mask = merged_face_ids == palette_id
|
| 500 |
+
changed_faces += int(np.count_nonzero(source_mask))
|
| 501 |
+
merged_face_ids[source_mask] = target_id
|
| 502 |
+
|
| 503 |
+
applied_groups += 1
|
| 504 |
+
logger.info(
|
| 505 |
+
"Applied PartSegChecker merge group for %s: colors=%s, "
|
| 506 |
+
"target_palette_id=%s, faces_changed=%s",
|
| 507 |
+
group.get("target_part", "unknown part"),
|
| 508 |
+
color_names,
|
| 509 |
+
target_id,
|
| 510 |
+
changed_faces,
|
| 511 |
+
)
|
| 512 |
+
|
| 513 |
+
return merged_face_ids, applied_groups > 0
|
| 514 |
+
|
| 515 |
+
def parse_merge_groups(self, check_info: str) -> list[dict]:
|
| 516 |
+
if not isinstance(check_info, str):
|
| 517 |
+
return []
|
| 518 |
+
|
| 519 |
+
json_start = check_info.find("{")
|
| 520 |
+
json_end = check_info.rfind("}")
|
| 521 |
+
if json_start < 0 or json_end <= json_start:
|
| 522 |
+
return []
|
| 523 |
+
|
| 524 |
+
try:
|
| 525 |
+
parsed = json_repair.loads(check_info[json_start : json_end + 1])
|
| 526 |
+
except Exception as exc:
|
| 527 |
+
logger.warning(
|
| 528 |
+
f"Failed to parse PartSegChecker merge hints: {exc}"
|
| 529 |
+
)
|
| 530 |
+
return []
|
| 531 |
+
|
| 532 |
+
merge_groups = parsed.get("merge_groups", [])
|
| 533 |
+
if not isinstance(merge_groups, list):
|
| 534 |
+
return []
|
| 535 |
+
|
| 536 |
+
valid_groups = []
|
| 537 |
+
for group in merge_groups:
|
| 538 |
+
if not isinstance(group, dict):
|
| 539 |
+
continue
|
| 540 |
+
colors = group.get("colors", [])
|
| 541 |
+
if not isinstance(colors, list) or len(colors) < 2:
|
| 542 |
+
continue
|
| 543 |
+
valid_groups.append(group)
|
| 544 |
+
return valid_groups
|
| 545 |
+
|
| 546 |
+
def infer_seg(self, mesh: trimesh.Trimesh, output_dir: str) -> bool:
|
| 547 |
+
unit_mesh = normalize_mesh(mesh)
|
| 548 |
+
if self.cfg.model_name == "p3sam":
|
| 549 |
+
set_seed(self.cfg.p3sam.seed)
|
| 550 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 551 |
+
_, face_ids, _ = self.pipeline.predict_aabb(
|
| 552 |
+
unit_mesh,
|
| 553 |
+
save_path=output_dir,
|
| 554 |
+
**asdict(self.cfg.p3sam),
|
| 555 |
+
)
|
| 556 |
+
else:
|
| 557 |
+
raise ValueError(
|
| 558 |
+
"unsupported model '{}'".format(self.cfg.model_name)
|
| 559 |
+
)
|
| 560 |
+
face_ids = self.remap_to_palette(face_ids)
|
| 561 |
+
return face_ids
|
| 562 |
+
|
| 563 |
+
def process(self, urdf_path: str, output_dir: str) -> bool:
|
| 564 |
+
try:
|
| 565 |
+
return self._process_impl(urdf_path, output_dir)
|
| 566 |
+
except Exception as exc:
|
| 567 |
+
logger.error(
|
| 568 |
+
"Part segmentation failed for URDF {}: {}".format(
|
| 569 |
+
urdf_path,
|
| 570 |
+
exc,
|
| 571 |
+
)
|
| 572 |
+
)
|
| 573 |
+
return False
|
| 574 |
+
|
| 575 |
+
def _process_impl(self, urdf_path: str, output_dir: str) -> bool:
|
| 576 |
+
seg_mesh_path = os.path.join(output_dir, "mesh_part_seg.glb")
|
| 577 |
+
mask_grid_path = None
|
| 578 |
+
if not self.cfg.overwrite and os.path.exists(seg_mesh_path):
|
| 579 |
+
logger.info(
|
| 580 |
+
f"Skip existing part segmentation result: {seg_mesh_path}"
|
| 581 |
+
)
|
| 582 |
+
return True
|
| 583 |
+
|
| 584 |
+
urdf = URDFFile(urdf_path)
|
| 585 |
+
category = urdf.get_category()
|
| 586 |
+
mesh_path = urdf.get_mesh_path(self.cfg.mesh_type)
|
| 587 |
+
mesh_transform = {}
|
| 588 |
+
if urdf_path is not None:
|
| 589 |
+
mesh_transform = urdf.get_mesh_transform(self.cfg.mesh_type)
|
| 590 |
+
mesh = load_mesh(
|
| 591 |
+
mesh_path,
|
| 592 |
+
apply_origin=True,
|
| 593 |
+
apply_scale=True,
|
| 594 |
+
**mesh_transform,
|
| 595 |
+
)
|
| 596 |
+
rgb_grid_path, _ = self.render_grid(mesh_path, output_dir)
|
| 597 |
+
|
| 598 |
+
face_ids = self.infer_seg(mesh, output_dir)
|
| 599 |
+
face_ids = self.postprocess_face_ids(mesh, face_ids)
|
| 600 |
+
|
| 601 |
+
if face_ids is None:
|
| 602 |
+
logger.error("Part seg failed due to exceeding palette size. ")
|
| 603 |
+
success, check_info = (
|
| 604 |
+
False,
|
| 605 |
+
f"NO: part number exceeds palette size {len(PALETTE)}",
|
| 606 |
+
)
|
| 607 |
+
seg_mesh_path = None
|
| 608 |
+
else:
|
| 609 |
+
seg_mesh_path, mask_grid_path, color_names = (
|
| 610 |
+
self.render_seg_result(
|
| 611 |
+
mesh,
|
| 612 |
+
face_ids,
|
| 613 |
+
output_dir,
|
| 614 |
+
mesh_transform if urdf_path is not None else None,
|
| 615 |
+
)
|
| 616 |
+
)
|
| 617 |
+
success, check_info = self.seg_checker(
|
| 618 |
+
category,
|
| 619 |
+
color_names,
|
| 620 |
+
rgb_grid_path,
|
| 621 |
+
mask_grid_path,
|
| 622 |
+
)
|
| 623 |
+
self.retry_count = 0
|
| 624 |
+
while not success and self.retry_count < self.cfg.merge_retries:
|
| 625 |
+
merged_face_ids, merge_applied = self.apply_merge_hints(
|
| 626 |
+
face_ids,
|
| 627 |
+
check_info,
|
| 628 |
+
)
|
| 629 |
+
if merge_applied:
|
| 630 |
+
face_ids = merged_face_ids
|
| 631 |
+
# 备份mask_grid_path
|
| 632 |
+
mask_grid_path_bak = mask_grid_path.replace(
|
| 633 |
+
"affordance_grid",
|
| 634 |
+
f"affordance_grid_{self.retry_count}",
|
| 635 |
+
)
|
| 636 |
+
if os.path.exists(mask_grid_path):
|
| 637 |
+
shutil.copy2(mask_grid_path, mask_grid_path_bak)
|
| 638 |
+
seg_mesh_path, mask_grid_path, color_names = (
|
| 639 |
+
self.render_seg_result(
|
| 640 |
+
mesh,
|
| 641 |
+
face_ids,
|
| 642 |
+
output_dir,
|
| 643 |
+
mesh_transform if urdf_path is not None else None,
|
| 644 |
+
)
|
| 645 |
+
)
|
| 646 |
+
success, check_info = self.seg_checker(
|
| 647 |
+
category,
|
| 648 |
+
color_names,
|
| 649 |
+
rgb_grid_path,
|
| 650 |
+
mask_grid_path,
|
| 651 |
+
)
|
| 652 |
+
self.retry_count += 1
|
| 653 |
+
|
| 654 |
+
if success:
|
| 655 |
+
logger.info(
|
| 656 |
+
f"Part seg successful for {mesh_path} with {self.count_parts(face_ids)} parts. "
|
| 657 |
+
)
|
| 658 |
+
else:
|
| 659 |
+
logger.error(
|
| 660 |
+
f"Part seg failed for {mesh_path}. Checker info: {check_info}"
|
| 661 |
+
)
|
| 662 |
+
self.update_urdf(
|
| 663 |
+
urdf_path,
|
| 664 |
+
seg_mesh_path,
|
| 665 |
+
self.count_parts(face_ids) if face_ids is not None else 0,
|
| 666 |
+
check_info,
|
| 667 |
+
mesh_transform if urdf_path is not None else None,
|
| 668 |
+
)
|
| 669 |
+
if self.cfg.debug_mode:
|
| 670 |
+
self.render_video(mesh_path, output_dir)
|
| 671 |
+
if seg_mesh_path:
|
| 672 |
+
self.render_video(
|
| 673 |
+
seg_mesh_path, output_dir, output_subdir="part_seg_renders"
|
| 674 |
+
)
|
| 675 |
+
else:
|
| 676 |
+
grid_dirs = {os.path.dirname(rgb_grid_path)}
|
| 677 |
+
if mask_grid_path:
|
| 678 |
+
grid_dirs.add(os.path.dirname(mask_grid_path))
|
| 679 |
+
for grid_dir in grid_dirs:
|
| 680 |
+
if grid_dir:
|
| 681 |
+
shutil.rmtree(grid_dir, ignore_errors=True)
|
| 682 |
+
return success
|
| 683 |
+
|
| 684 |
+
|
| 685 |
+
def run_part_seg(cfg: PartSegConfig) -> None:
|
| 686 |
+
segmenter = PartSegmenter(cfg)
|
| 687 |
+
for urdf_path, output_dir in zip(cfg.urdf_paths, cfg.output_dirs):
|
| 688 |
+
segmenter.process(urdf_path, output_dir)
|
| 689 |
+
|
| 690 |
+
|
| 691 |
+
def entrypoint(*args, **kwargs) -> None:
|
| 692 |
+
cfg = tyro.cli(PartSegConfig)
|
| 693 |
+
run_part_seg(cfg)
|
| 694 |
+
|
| 695 |
+
|
| 696 |
+
if __name__ == "__main__":
|
| 697 |
+
entrypoint()
|
embodied_gen/scripts/affordance_annot/partsemantics_annot.py
ADDED
|
@@ -0,0 +1,438 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Project EmbodiedGen
|
| 2 |
+
#
|
| 3 |
+
# Copyright (c) 2025 Horizon Robotics. All Rights Reserved.
|
| 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
|
| 14 |
+
# implied. See the License for the specific language governing
|
| 15 |
+
# permissions and limitations under the License.
|
| 16 |
+
|
| 17 |
+
import json
|
| 18 |
+
import os
|
| 19 |
+
import shutil
|
| 20 |
+
from dataclasses import dataclass, field
|
| 21 |
+
from typing import Literal
|
| 22 |
+
|
| 23 |
+
import json_repair
|
| 24 |
+
import tyro
|
| 25 |
+
from embodied_gen.utils.general import filter_warnings
|
| 26 |
+
|
| 27 |
+
filter_warnings()
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
from embodied_gen.utils.gpt_clients import GPT_CLIENT
|
| 31 |
+
from embodied_gen.utils.io_utils import URDFFile, load_mesh
|
| 32 |
+
from embodied_gen.utils.log import logger
|
| 33 |
+
from embodied_gen.utils.vis_utils import (
|
| 34 |
+
PALETTE,
|
| 35 |
+
collect_colors,
|
| 36 |
+
render_grid,
|
| 37 |
+
visualize_partsemantics,
|
| 38 |
+
)
|
| 39 |
+
from embodied_gen.validators.quality_checkers import PartSemanticsChecker
|
| 40 |
+
|
| 41 |
+
__all__ = [
|
| 42 |
+
"PartSemanticsAnnotConfig",
|
| 43 |
+
"PartSemanticsAnnotator",
|
| 44 |
+
"run_partsemantics_annot",
|
| 45 |
+
"entrypoint",
|
| 46 |
+
]
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
@dataclass
|
| 50 |
+
class PartSemanticsAnnotConfig:
|
| 51 |
+
urdf_paths: list[str] = field(default_factory=list)
|
| 52 |
+
output_dirs: list[str] = field(default_factory=list)
|
| 53 |
+
mesh_type: Literal["visual", "collision"] = "visual"
|
| 54 |
+
max_tokens: int = 10240
|
| 55 |
+
max_repairs: int = 2
|
| 56 |
+
visualize: bool = False
|
| 57 |
+
vis_fps: int = 12
|
| 58 |
+
vis_frames_per_part: int = 36
|
| 59 |
+
vis_view_size: int = 1080
|
| 60 |
+
grid_num_images: int = 6
|
| 61 |
+
grid_rows: int = 2
|
| 62 |
+
grid_cols: int = 3
|
| 63 |
+
grid_image_size: int = 512
|
| 64 |
+
debug_mode: bool = False
|
| 65 |
+
overwrite: bool = True
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
class PartSemanticsAnnotator:
|
| 69 |
+
def __init__(self, cfg: PartSemanticsAnnotConfig):
|
| 70 |
+
self.validate_config(cfg)
|
| 71 |
+
self.cfg = cfg
|
| 72 |
+
self.gpt_client = GPT_CLIENT
|
| 73 |
+
self.checker = PartSemanticsChecker(GPT_CLIENT)
|
| 74 |
+
self.system_prompt = (
|
| 75 |
+
"You are an expert annotator for 3D object affordances. You will receive two aligned multi-view grid images of the same object: "
|
| 76 |
+
"the first grid is the RGB render, and the second grid is a colored part mask where each color denotes one segmented part. "
|
| 77 |
+
"You will also receive the object category and the list of colors that appear in the mask.\n"
|
| 78 |
+
"Your task is to infer affordance information for each meaningful colored part.\n"
|
| 79 |
+
"For every annotated part, provide:\n"
|
| 80 |
+
"1. A concise self-contained part name. Prefer the natural standalone part name when it is clear, e.g. \"lampshade\" or \"light bulb\"; add the object category only when the name would otherwise be ambiguous, e.g. \"stem of apple\" rather than \"stem\".\n"
|
| 81 |
+
"2. Whether the part is realistically useful for robotic grasping with a gripper.\n"
|
| 82 |
+
"3. If it is graspable, concrete robotic grasp scenarios with confidence scores.\n"
|
| 83 |
+
"4. Functional labels: concrete short phrases describing what the part can be used for and what functional role it provides.\n"
|
| 84 |
+
"5. Semantic description: a concise natural-language description of the part, including its role, RGB appearance color, material type, surface finish, tactile texture, shape, relative size/position, and any visible markings when inferable.\n"
|
| 85 |
+
"Use the RGB grid to understand physical appearance and the mask grid to map parts to colors. "
|
| 86 |
+
"The mask color is only an ID for the segmented part; do not describe mask colors as physical object colors. "
|
| 87 |
+
"Only use mask colors from the provided color list. Do not invent mask colors. "
|
| 88 |
+
"If a colored region has no clear affordance, still include it when it is a distinct physical part, with conservative functional labels. "
|
| 89 |
+
"Return only valid JSON. Do not wrap the JSON in Markdown or code fences."
|
| 90 |
+
)
|
| 91 |
+
self.user_prompt = (
|
| 92 |
+
"Object category: {category}\n"
|
| 93 |
+
"Mask color list: {color_names}\n"
|
| 94 |
+
"Image inputs:\n"
|
| 95 |
+
"- Image 1: RGB multi-view grid of the object.\n"
|
| 96 |
+
"- Image 2: part mask multi-view grid aligned with Image 1. Different colors indicate different segmented parts.\n\n"
|
| 97 |
+
"Annotate the affordance of the visible colored parts. Match each part to exactly one color from the color list.\n"
|
| 98 |
+
"For each part, include these fields:\n"
|
| 99 |
+
"- \"part_name\": concise semantic name of the part. Use natural standalone names when they are already clear, such as \"lampshade\", \"light bulb\", \"keyboard\", \"wheel\", or \"mug handle\". Use \"part of object\" only when the standalone part name is ambiguous or too generic, such as \"stem of apple\", \"blade of knife\", or \"base of lamp\".\n"
|
| 100 |
+
"- \"mask_color\": one mask color from the provided color list. This is the segmentation color, not the physical RGB color.\n"
|
| 101 |
+
"- \"graspable\": true or false.\n"
|
| 102 |
+
"- \"grasp_scenarios\": list of dicts, each with \"scenario\" and \"confidence\"; each \"confidence\" belongs to that same scenario and is a float from 0.0 to 1.0 estimating how likely a robot gripper would choose this part for that scenario. Use accurate, distinguishable confidences when multiple parts share the same scenario. Do not give high confidence to theoretically possible but uncommon grasps. Use an empty list if graspable is false.\n"
|
| 103 |
+
"- \"functional_labels\": list of 2 to 6 concrete short phrases describing what this part can be used to do and what functional role it provides.\n"
|
| 104 |
+
"- \"semantic_description\": one concise but complete sentence describing the part's RGB appearance color, material type, surface finish, tactile texture, shape, relative size, location, and any visible markings when inferable. Do not mention the mask color unless it is also the real RGB appearance.\n\n"
|
| 105 |
+
"Guidelines:\n"
|
| 106 |
+
"- Use the RGB grid for appearance and the mask grid for color-to-part mapping.\n"
|
| 107 |
+
"- Treat grasping as robotic gripper grasping, not human hand grasping.\n"
|
| 108 |
+
"- A part is graspable only if it is a plausible and useful place for a robot gripper to hold, pick up, stabilize, pull, or manipulate the object.\n"
|
| 109 |
+
"- If a part is only theoretically graspable but normally poor or uncommon for grasping, keep \"graspable\" True only when there is a realistic scenario, and assign low confidence. For example, an apple stem may be graspable in a delicate stem-picking scenario but is low confidence for lifting the apple.\n"
|
| 110 |
+
"- If a part is functional but not graspable, set \"graspable\" to False and still describe its function.\n"
|
| 111 |
+
"- Functional labels should describe what the part can be used for or what function it enables, such as \"support\", \"contain\", \"cut\", \"press\", \"cover\", \"connect\", \"stabilize\", or \"indicate orientation\".\n"
|
| 112 |
+
"- Semantic descriptions should cover visible material and texture cues when inferable: material type such as metal, plastic, ceramic, rubber, fabric, wood, glass, or food skin; surface finish such as glossy, matte, translucent, reflective, or dull; tactile texture such as soft, hard, smooth, rough, fine-grained, pebbled, ribbed, woven, or granular; inherent patterns or special markings such as wood grain, fabric weave, seams, engraved text, logos, printed icons, labels, or scratches.\n"
|
| 113 |
+
"- Keep semantic descriptions concise, usually one sentence under 35 words, and do not invent material or markings that are not visually supported.\n"
|
| 114 |
+
"- Do not assign affordances to background regions.\n"
|
| 115 |
+
"- Do not include colors that are not in the color list.\n\n"
|
| 116 |
+
"Output format: Return exactly one JSON object with this schema:\n"
|
| 117 |
+
"{{\"affordances\": [{{\"part_name\": \"mug handle\", \"mask_color\": \"Red\", \"graspable\": true, \"grasp_scenarios\": [{{\"scenario\": \"grasp the handle to lift the mug\", \"confidence\": 0.92}}, {{\"scenario\": \"hold the handle to stabilize the mug while pouring\", \"confidence\": 0.84}}], \"functional_labels\": [\"provide a side grip\", \"lift the mug without touching the body\", \"stabilize the mug while pouring\"], \"semantic_description\": \"Curved glossy ceramic handle with a hard smooth surface, matching the mug body and sized for side gripping.\"}}, "
|
| 118 |
+
"{{\"part_name\": \"stem of apple\", \"mask_color\": \"Green\", \"graspable\": true, \"grasp_scenarios\": [{{\"scenario\": \"delicately grasp the stem to orient the apple\", \"confidence\": 0.28}}, {{\"scenario\": \"lift the apple by the stem\", \"confidence\": 0.12}}], \"functional_labels\": [\"connect to branch\", \"indicate top orientation\"], \"semantic_description\": \"Small thin brown woody stem with a hard rough fibrous texture protruding from the apple top.\"}}, "
|
| 119 |
+
"{{\"part_name\": \"body of apple\", \"mask_color\": \"Blue\", \"graspable\": true, \"grasp_scenarios\": [{{\"scenario\": \"grasp the apple body to pick it up\", \"confidence\": 0.88}}], \"functional_labels\": [\"contain edible flesh\", \"support biting\", \"main grasp surface\"], \"semantic_description\": \"Large rounded red apple body with glossy smooth food skin, firm texture, and subtle natural color speckles.\"}}]}}\n"
|
| 120 |
+
"If the target object cannot be identified, output exactly: {{\"affordances\": []}}"
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
def validate_config(self, cfg: PartSemanticsAnnotConfig) -> None:
|
| 124 |
+
if not cfg.urdf_paths:
|
| 125 |
+
raise ValueError("urdf_paths must be provided.")
|
| 126 |
+
|
| 127 |
+
if len(cfg.output_dirs) == 0:
|
| 128 |
+
cfg.output_dirs = [
|
| 129 |
+
os.path.join(os.path.dirname(path), "affordance")
|
| 130 |
+
for path in cfg.urdf_paths
|
| 131 |
+
]
|
| 132 |
+
|
| 133 |
+
if len(cfg.urdf_paths) != len(cfg.output_dirs):
|
| 134 |
+
raise ValueError(
|
| 135 |
+
"urdf_paths and output_dirs must have the same length, "
|
| 136 |
+
f"got {len(cfg.urdf_paths)} and {len(cfg.output_dirs)}."
|
| 137 |
+
)
|
| 138 |
+
|
| 139 |
+
if cfg.grid_num_images != cfg.grid_rows * cfg.grid_cols:
|
| 140 |
+
raise ValueError(
|
| 141 |
+
"grid_num_images must equal grid_rows * grid_cols, "
|
| 142 |
+
f"got {cfg.grid_num_images} and "
|
| 143 |
+
f"{cfg.grid_rows} * {cfg.grid_cols}."
|
| 144 |
+
)
|
| 145 |
+
|
| 146 |
+
def check_requires(self, urdf_path: str) -> tuple[str, str]:
|
| 147 |
+
urdf = URDFFile(urdf_path)
|
| 148 |
+
seg_mesh_path = urdf.get_mesh_part_seg_path()
|
| 149 |
+
if not os.path.exists(seg_mesh_path):
|
| 150 |
+
raise FileNotFoundError(
|
| 151 |
+
f"Part segmentation result not found. Run part segmentation before "
|
| 152 |
+
f"partsemantics annotation. Missing: {seg_mesh_path}"
|
| 153 |
+
)
|
| 154 |
+
return seg_mesh_path
|
| 155 |
+
|
| 156 |
+
def parse_response(self, response: str) -> dict:
|
| 157 |
+
answer = str(response).strip()
|
| 158 |
+
parsed = json.loads(answer)
|
| 159 |
+
return parsed
|
| 160 |
+
|
| 161 |
+
def add_palette_ids(self, payload: dict) -> dict:
|
| 162 |
+
color_to_id = {
|
| 163 |
+
color_name: palette_id for palette_id, _, color_name in PALETTE
|
| 164 |
+
}
|
| 165 |
+
for partsemantics in payload.get("affordances", []):
|
| 166 |
+
if isinstance(partsemantics, dict):
|
| 167 |
+
partsemantics["id"] = color_to_id.get(
|
| 168 |
+
partsemantics.get("mask_color")
|
| 169 |
+
)
|
| 170 |
+
return payload
|
| 171 |
+
|
| 172 |
+
def check_response(
|
| 173 |
+
self, response: str, rgb_grid_path: str, mask_grid_path: str
|
| 174 |
+
) -> dict:
|
| 175 |
+
passed, message = self.checker(
|
| 176 |
+
response,
|
| 177 |
+
rgb_grid_path,
|
| 178 |
+
mask_grid_path,
|
| 179 |
+
)
|
| 180 |
+
check_info = {
|
| 181 |
+
"success": passed,
|
| 182 |
+
"message": message,
|
| 183 |
+
}
|
| 184 |
+
if not passed:
|
| 185 |
+
feedback = self._parse_checker_message(message)
|
| 186 |
+
modified_response = (
|
| 187 |
+
feedback.get("modified_response")
|
| 188 |
+
if isinstance(feedback, dict)
|
| 189 |
+
else None
|
| 190 |
+
)
|
| 191 |
+
if isinstance(modified_response, str):
|
| 192 |
+
check_info["modified_response"] = modified_response.strip()
|
| 193 |
+
elif modified_response is not None:
|
| 194 |
+
check_info["modified_response"] = json.dumps(
|
| 195 |
+
modified_response,
|
| 196 |
+
ensure_ascii=False,
|
| 197 |
+
)
|
| 198 |
+
return check_info
|
| 199 |
+
|
| 200 |
+
def _parse_checker_message(self, checker_message: str) -> dict | None:
|
| 201 |
+
if not isinstance(checker_message, str):
|
| 202 |
+
return None
|
| 203 |
+
|
| 204 |
+
answer = checker_message.strip()
|
| 205 |
+
try:
|
| 206 |
+
feedback = json_repair.loads(answer)
|
| 207 |
+
except (json.JSONDecodeError, ValueError, TypeError):
|
| 208 |
+
json_start = answer.find("{")
|
| 209 |
+
json_end = answer.rfind("}")
|
| 210 |
+
if json_start < 0 or json_end <= json_start:
|
| 211 |
+
return None
|
| 212 |
+
try:
|
| 213 |
+
feedback = json_repair.loads(answer[json_start : json_end + 1])
|
| 214 |
+
except (json.JSONDecodeError, ValueError, TypeError):
|
| 215 |
+
return None
|
| 216 |
+
|
| 217 |
+
return feedback if isinstance(feedback, dict) else None
|
| 218 |
+
|
| 219 |
+
def _quality_check_for_save(
|
| 220 |
+
self, quality_check: dict | None
|
| 221 |
+
) -> dict | None:
|
| 222 |
+
if quality_check is None:
|
| 223 |
+
return None
|
| 224 |
+
if not isinstance(quality_check, dict):
|
| 225 |
+
return {"success": None, "message": str(quality_check)}
|
| 226 |
+
|
| 227 |
+
message = quality_check.get("message", "")
|
| 228 |
+
feedback = self._parse_checker_message(message)
|
| 229 |
+
if isinstance(feedback, dict) and feedback.get("reason") is not None:
|
| 230 |
+
message = str(feedback["reason"]).strip()
|
| 231 |
+
elif isinstance(message, str) and message.strip().startswith("NO:"):
|
| 232 |
+
message = message.strip()[3:].strip()
|
| 233 |
+
|
| 234 |
+
return {
|
| 235 |
+
"success": quality_check.get("success"),
|
| 236 |
+
"message": message,
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
def save_annotation(
|
| 240 |
+
self, response: str, output_dir: str, quality_check: dict | None = None
|
| 241 |
+
) -> str:
|
| 242 |
+
annotation_path = os.path.join(output_dir, "affordance_annot.json")
|
| 243 |
+
if self.cfg.debug_mode:
|
| 244 |
+
raw_output_path = os.path.join(
|
| 245 |
+
output_dir, "affordance_annot_raw.txt"
|
| 246 |
+
)
|
| 247 |
+
with open(raw_output_path, "w", encoding="utf-8") as f:
|
| 248 |
+
f.write(str(response))
|
| 249 |
+
|
| 250 |
+
try:
|
| 251 |
+
payload = self.parse_response(response)
|
| 252 |
+
payload = self.add_palette_ids(payload)
|
| 253 |
+
except (json.JSONDecodeError, ValueError, TypeError) as exc:
|
| 254 |
+
payload = {
|
| 255 |
+
"status": "parse failed",
|
| 256 |
+
"affordances": [],
|
| 257 |
+
"parse_error": str(exc),
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
saved_quality_check = self._quality_check_for_save(quality_check)
|
| 261 |
+
if saved_quality_check is not None:
|
| 262 |
+
payload["quality_check"] = saved_quality_check
|
| 263 |
+
|
| 264 |
+
with open(annotation_path, "w", encoding="utf-8") as f:
|
| 265 |
+
json.dump(payload, f, ensure_ascii=False, indent=2)
|
| 266 |
+
return annotation_path
|
| 267 |
+
|
| 268 |
+
def update_urdf(self, urdf_path: str, annotation_path: str) -> None:
|
| 269 |
+
if urdf_path is None:
|
| 270 |
+
return
|
| 271 |
+
|
| 272 |
+
URDFFile(urdf_path).write(
|
| 273 |
+
{
|
| 274 |
+
"custom_data/affordance/affordance_annot": os.path.relpath(
|
| 275 |
+
annotation_path,
|
| 276 |
+
os.path.dirname(urdf_path),
|
| 277 |
+
),
|
| 278 |
+
}
|
| 279 |
+
)
|
| 280 |
+
|
| 281 |
+
def render_grid(
|
| 282 |
+
self,
|
| 283 |
+
mesh_path: str,
|
| 284 |
+
output_dir: str,
|
| 285 |
+
output_subdir: str = "renders",
|
| 286 |
+
) -> tuple[str, list[str]]:
|
| 287 |
+
grid_path, view_paths = render_grid(
|
| 288 |
+
mesh_path,
|
| 289 |
+
output_dir,
|
| 290 |
+
output_subdir=output_subdir,
|
| 291 |
+
num_images=self.cfg.grid_num_images,
|
| 292 |
+
grid_rows=self.cfg.grid_rows,
|
| 293 |
+
grid_cols=self.cfg.grid_cols,
|
| 294 |
+
view_size=self.cfg.grid_image_size,
|
| 295 |
+
)
|
| 296 |
+
return grid_path, view_paths
|
| 297 |
+
|
| 298 |
+
def _process_once(
|
| 299 |
+
self,
|
| 300 |
+
rgb_grid_path: str,
|
| 301 |
+
mask_grid_path: str,
|
| 302 |
+
color_names: str,
|
| 303 |
+
category: str,
|
| 304 |
+
) -> tuple[bool, dict, str]:
|
| 305 |
+
response = self.gpt_client.query(
|
| 306 |
+
text_prompt=self.user_prompt.format(
|
| 307 |
+
category=category,
|
| 308 |
+
color_names=color_names,
|
| 309 |
+
),
|
| 310 |
+
image_base64=[rgb_grid_path, mask_grid_path],
|
| 311 |
+
system_role=self.system_prompt,
|
| 312 |
+
params={"max_tokens": self.cfg.max_tokens},
|
| 313 |
+
)
|
| 314 |
+
check_info = {"success": False, "message": "NO: checker was not run"}
|
| 315 |
+
success = False
|
| 316 |
+
max_checks = self.cfg.max_repairs + 1
|
| 317 |
+
for check_idx in range(max_checks):
|
| 318 |
+
logger.info(
|
| 319 |
+
"PartSemantics quality check: attempt "
|
| 320 |
+
f"{check_idx + 1}/{max_checks}"
|
| 321 |
+
)
|
| 322 |
+
check_info = self.check_response(
|
| 323 |
+
response,
|
| 324 |
+
rgb_grid_path,
|
| 325 |
+
mask_grid_path,
|
| 326 |
+
)
|
| 327 |
+
success = check_info.get("success")
|
| 328 |
+
if success:
|
| 329 |
+
return success, check_info, response
|
| 330 |
+
|
| 331 |
+
if check_idx == max_checks - 1:
|
| 332 |
+
break
|
| 333 |
+
|
| 334 |
+
modified_response = check_info.get("modified_response")
|
| 335 |
+
if modified_response is None:
|
| 336 |
+
break
|
| 337 |
+
|
| 338 |
+
response = modified_response
|
| 339 |
+
logger.warning(
|
| 340 |
+
"PartSemantics checker failed; retrying with modified_response. "
|
| 341 |
+
)
|
| 342 |
+
|
| 343 |
+
return success, check_info, response
|
| 344 |
+
|
| 345 |
+
def process(self, urdf_path: str, output_dir: str) -> bool:
|
| 346 |
+
try:
|
| 347 |
+
return self._process_impl(urdf_path, output_dir)
|
| 348 |
+
except Exception as exc:
|
| 349 |
+
logger.error(
|
| 350 |
+
"PartSemantics annotation failed for URDF {}: {}".format(
|
| 351 |
+
urdf_path,
|
| 352 |
+
exc,
|
| 353 |
+
)
|
| 354 |
+
)
|
| 355 |
+
return False
|
| 356 |
+
|
| 357 |
+
def _process_impl(self, urdf_path: str, output_dir: str) -> bool:
|
| 358 |
+
annotation_path = os.path.join(output_dir, "affordance_annot.json")
|
| 359 |
+
if not self.cfg.overwrite and os.path.exists(annotation_path):
|
| 360 |
+
logger.info(
|
| 361 |
+
f"Skip existing PartSemantics annotation: {annotation_path}"
|
| 362 |
+
)
|
| 363 |
+
return True
|
| 364 |
+
|
| 365 |
+
urdf = URDFFile(urdf_path)
|
| 366 |
+
mesh_path = urdf.get_mesh_path(self.cfg.mesh_type)
|
| 367 |
+
seg_mesh_path = self.check_requires(urdf_path)
|
| 368 |
+
|
| 369 |
+
logger.info("Processing PartSemantics annotation...")
|
| 370 |
+
rgb_grid_path, _ = self.render_grid(mesh_path, output_dir)
|
| 371 |
+
mask_grid_path, _ = self.render_grid(
|
| 372 |
+
seg_mesh_path, output_dir, output_subdir="part_seg_renders"
|
| 373 |
+
)
|
| 374 |
+
|
| 375 |
+
_, face_ids = load_mesh(
|
| 376 |
+
seg_mesh_path, apply_origin=False, apply_scale=False
|
| 377 |
+
)
|
| 378 |
+
color_names = collect_colors(face_ids)
|
| 379 |
+
logger.info(f"Mask colors: {color_names}")
|
| 380 |
+
|
| 381 |
+
category = urdf.get_category()
|
| 382 |
+
success, check_info, response = self._process_once(
|
| 383 |
+
rgb_grid_path,
|
| 384 |
+
mask_grid_path,
|
| 385 |
+
color_names,
|
| 386 |
+
category,
|
| 387 |
+
)
|
| 388 |
+
if success:
|
| 389 |
+
logger.info(
|
| 390 |
+
"PartSemantics annotation passed quality check."
|
| 391 |
+
f"{check_info.get('message')}"
|
| 392 |
+
)
|
| 393 |
+
else:
|
| 394 |
+
logger.error(
|
| 395 |
+
"PartSemantics annotation failed quality check after "
|
| 396 |
+
f"{self.cfg.max_repairs} repair attempts;\nsaving last result. "
|
| 397 |
+
)
|
| 398 |
+
|
| 399 |
+
annotation_path = self.save_annotation(
|
| 400 |
+
response, output_dir, check_info
|
| 401 |
+
)
|
| 402 |
+
self.update_urdf(urdf_path, annotation_path)
|
| 403 |
+
if self.cfg.visualize:
|
| 404 |
+
visualize_partsemantics(
|
| 405 |
+
mesh_path,
|
| 406 |
+
seg_mesh_path,
|
| 407 |
+
annotation_path,
|
| 408 |
+
fps=self.cfg.vis_fps,
|
| 409 |
+
frames_per_part=self.cfg.vis_frames_per_part,
|
| 410 |
+
view_size=self.cfg.vis_view_size,
|
| 411 |
+
)
|
| 412 |
+
|
| 413 |
+
if not self.cfg.debug_mode:
|
| 414 |
+
for grid_dir in {
|
| 415 |
+
os.path.dirname(rgb_grid_path),
|
| 416 |
+
os.path.dirname(mask_grid_path),
|
| 417 |
+
}:
|
| 418 |
+
if grid_dir:
|
| 419 |
+
shutil.rmtree(grid_dir, ignore_errors=True)
|
| 420 |
+
|
| 421 |
+
return success
|
| 422 |
+
|
| 423 |
+
|
| 424 |
+
def run_partsemantics_annot(cfg: PartSemanticsAnnotConfig) -> None:
|
| 425 |
+
annotator = PartSemanticsAnnotator(cfg)
|
| 426 |
+
for urdf_path, output_dir in zip(
|
| 427 |
+
annotator.cfg.urdf_paths, annotator.cfg.output_dirs
|
| 428 |
+
):
|
| 429 |
+
annotator.process(urdf_path, output_dir)
|
| 430 |
+
|
| 431 |
+
|
| 432 |
+
def entrypoint() -> None:
|
| 433 |
+
cfg = tyro.cli(PartSemanticsAnnotConfig)
|
| 434 |
+
run_partsemantics_annot(cfg)
|
| 435 |
+
|
| 436 |
+
|
| 437 |
+
if __name__ == "__main__":
|
| 438 |
+
entrypoint()
|
embodied_gen/scripts/compose_layout.py
CHANGED
|
@@ -21,7 +21,6 @@ from dataclasses import dataclass
|
|
| 21 |
|
| 22 |
import tyro
|
| 23 |
from embodied_gen.scripts.simulate_sapien import entrypoint as sim_cli
|
| 24 |
-
from embodied_gen.utils.enum import LayoutInfo
|
| 25 |
from embodied_gen.utils.geometry import bfs_placement, compose_mesh_scene
|
| 26 |
from embodied_gen.utils.log import logger
|
| 27 |
|
|
|
|
| 21 |
|
| 22 |
import tyro
|
| 23 |
from embodied_gen.scripts.simulate_sapien import entrypoint as sim_cli
|
|
|
|
| 24 |
from embodied_gen.utils.geometry import bfs_placement, compose_mesh_scene
|
| 25 |
from embodied_gen.utils.log import logger
|
| 26 |
|
embodied_gen/scripts/deformable_sim/genesis_cloth_converter.py
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Project EmbodiedGen
|
| 2 |
+
#
|
| 3 |
+
# Copyright (c) 2025 Horizon Robotics. All Rights Reserved.
|
| 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
|
| 14 |
+
# implied. See the License for the specific language governing
|
| 15 |
+
# permissions and limitations under the License.
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
import json
|
| 19 |
+
import xml.etree.ElementTree as ET
|
| 20 |
+
from dataclasses import dataclass
|
| 21 |
+
from pathlib import Path
|
| 22 |
+
from typing import Any
|
| 23 |
+
|
| 24 |
+
import numpy as np
|
| 25 |
+
import trimesh
|
| 26 |
+
import tyro
|
| 27 |
+
from scipy.spatial.transform import Rotation
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
@dataclass
|
| 31 |
+
class GenesisClothConvertConfig:
|
| 32 |
+
input_path: str
|
| 33 |
+
output_dir: str
|
| 34 |
+
scale: float | None = None
|
| 35 |
+
overwrite: bool = False
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
DEFAULT_CLOTH_MATERIAL: dict[str, float | bool] = {
|
| 39 |
+
"rho": 0.2,
|
| 40 |
+
"static_friction": 0.5,
|
| 41 |
+
"kinetic_friction": 0.4,
|
| 42 |
+
"stretch_compliance": 1e-9,
|
| 43 |
+
"bending_compliance": 1e-6,
|
| 44 |
+
"stretch_relaxation": 0.5,
|
| 45 |
+
"bending_relaxation": 0.2,
|
| 46 |
+
"air_resistance": 1e-3,
|
| 47 |
+
"particle_size": 0.02,
|
| 48 |
+
"self_collision": True,
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def convert_cloth_to_genesis(
|
| 53 |
+
input_path: str,
|
| 54 |
+
output_dir: str,
|
| 55 |
+
scale: float | None = None,
|
| 56 |
+
overwrite: bool = False,
|
| 57 |
+
) -> str:
|
| 58 |
+
"""Convert one URDF visual mesh to a Genesis cloth asset package.
|
| 59 |
+
|
| 60 |
+
Args:
|
| 61 |
+
input_path: URDF path containing a visual mesh reference.
|
| 62 |
+
output_dir: Directory where the ``genesis`` package is written.
|
| 63 |
+
scale: Optional uniform scale recorded in the manifest and used by the
|
| 64 |
+
simulator. The exported mesh itself is not rescaled.
|
| 65 |
+
overwrite: Whether to replace an existing Genesis package.
|
| 66 |
+
|
| 67 |
+
Returns:
|
| 68 |
+
Path to the generated manifest JSON file.
|
| 69 |
+
"""
|
| 70 |
+
|
| 71 |
+
urdf_path = Path(input_path)
|
| 72 |
+
if not urdf_path.exists():
|
| 73 |
+
raise FileNotFoundError(f"Input URDF does not exist: {urdf_path}")
|
| 74 |
+
if urdf_path.suffix.lower() != ".urdf":
|
| 75 |
+
raise ValueError(f"input_path must point to a URDF file: {urdf_path}")
|
| 76 |
+
|
| 77 |
+
source_mesh, urdf_transform = _resolve_urdf_visual_mesh(urdf_path)
|
| 78 |
+
package_root = Path(output_dir)
|
| 79 |
+
genesis_dir = package_root / "genesis"
|
| 80 |
+
if genesis_dir.exists() and not overwrite:
|
| 81 |
+
raise FileExistsError(
|
| 82 |
+
f"Genesis cloth package already exists: {genesis_dir}. "
|
| 83 |
+
"Pass overwrite=True to replace it."
|
| 84 |
+
)
|
| 85 |
+
genesis_dir.mkdir(parents=True, exist_ok=True)
|
| 86 |
+
|
| 87 |
+
mesh = _load_mesh(source_mesh)
|
| 88 |
+
mesh, warnings = _clean_mesh(mesh)
|
| 89 |
+
mesh.apply_transform(urdf_transform["matrix"])
|
| 90 |
+
warnings.append("applied URDF visual origin transform to cloth mesh")
|
| 91 |
+
|
| 92 |
+
cloth_mesh_path = genesis_dir / "cloth_mesh.obj"
|
| 93 |
+
material_path = genesis_dir / "cloth_material.json"
|
| 94 |
+
manifest_path = genesis_dir / "manifest.json"
|
| 95 |
+
|
| 96 |
+
mesh.export(cloth_mesh_path)
|
| 97 |
+
_write_json(material_path, DEFAULT_CLOTH_MATERIAL)
|
| 98 |
+
|
| 99 |
+
mesh_stats = _mesh_stats(mesh)
|
| 100 |
+
warnings.extend(_mesh_warnings(mesh_stats))
|
| 101 |
+
manifest = {
|
| 102 |
+
"asset_name": source_mesh.stem,
|
| 103 |
+
"source_urdf": _relative_path(urdf_path, package_root),
|
| 104 |
+
"source_mesh": _relative_path(source_mesh, package_root),
|
| 105 |
+
"cloth_mesh": _relative_path(cloth_mesh_path, package_root),
|
| 106 |
+
"material": _relative_path(material_path, package_root),
|
| 107 |
+
"scale": 1.0 if scale is None else float(scale),
|
| 108 |
+
"up_axis": "z",
|
| 109 |
+
"urdf_visual_origin": urdf_transform["origin"],
|
| 110 |
+
"mesh_stats": mesh_stats,
|
| 111 |
+
"warnings": warnings,
|
| 112 |
+
}
|
| 113 |
+
_write_json(manifest_path, manifest)
|
| 114 |
+
return str(manifest_path)
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def entrypoint(**kwargs) -> str:
|
| 118 |
+
if kwargs:
|
| 119 |
+
cfg = GenesisClothConvertConfig(**kwargs)
|
| 120 |
+
else:
|
| 121 |
+
cfg = tyro.cli(GenesisClothConvertConfig)
|
| 122 |
+
|
| 123 |
+
manifest_path = convert_cloth_to_genesis(
|
| 124 |
+
input_path=cfg.input_path,
|
| 125 |
+
output_dir=cfg.output_dir,
|
| 126 |
+
scale=cfg.scale,
|
| 127 |
+
overwrite=cfg.overwrite,
|
| 128 |
+
)
|
| 129 |
+
print(manifest_path)
|
| 130 |
+
return manifest_path
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def _resolve_urdf_visual_mesh(urdf_path: Path) -> tuple[Path, dict[str, Any]]:
|
| 134 |
+
root = ET.parse(urdf_path).getroot()
|
| 135 |
+
for visual_tag in root.findall(".//visual"):
|
| 136 |
+
mesh_tag = visual_tag.find("geometry/mesh")
|
| 137 |
+
if mesh_tag is None:
|
| 138 |
+
continue
|
| 139 |
+
filename = mesh_tag.get("filename")
|
| 140 |
+
if not filename:
|
| 141 |
+
raise ValueError(
|
| 142 |
+
f"Visual mesh in URDF has no filename: {urdf_path}"
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
source_mesh = Path(filename)
|
| 146 |
+
if not source_mesh.is_absolute():
|
| 147 |
+
source_mesh = urdf_path.parent / source_mesh
|
| 148 |
+
if not source_mesh.exists():
|
| 149 |
+
raise FileNotFoundError(
|
| 150 |
+
f"Visual mesh does not exist: {source_mesh}"
|
| 151 |
+
)
|
| 152 |
+
|
| 153 |
+
origin_tag = visual_tag.find("origin")
|
| 154 |
+
xyz, rpy = _parse_origin(origin_tag)
|
| 155 |
+
matrix = np.eye(4, dtype=float)
|
| 156 |
+
matrix[:3, :3] = Rotation.from_euler("xyz", rpy).as_matrix()
|
| 157 |
+
matrix[:3, 3] = xyz
|
| 158 |
+
return source_mesh, {
|
| 159 |
+
"matrix": matrix,
|
| 160 |
+
"origin": {
|
| 161 |
+
"urdf": urdf_path.name,
|
| 162 |
+
"xyz": [round(float(value), 6) for value in xyz],
|
| 163 |
+
"rpy": [round(float(value), 6) for value in rpy],
|
| 164 |
+
},
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
raise ValueError(f"No visual mesh found in URDF: {urdf_path}")
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
def _parse_origin(
|
| 171 |
+
origin_tag: ET.Element | None,
|
| 172 |
+
) -> tuple[np.ndarray, np.ndarray]:
|
| 173 |
+
if origin_tag is None:
|
| 174 |
+
return np.zeros(3, dtype=float), np.zeros(3, dtype=float)
|
| 175 |
+
xyz = np.fromiter(
|
| 176 |
+
(float(value) for value in origin_tag.get("xyz", "0 0 0").split()),
|
| 177 |
+
dtype=float,
|
| 178 |
+
count=3,
|
| 179 |
+
)
|
| 180 |
+
rpy = np.fromiter(
|
| 181 |
+
(float(value) for value in origin_tag.get("rpy", "0 0 0").split()),
|
| 182 |
+
dtype=float,
|
| 183 |
+
count=3,
|
| 184 |
+
)
|
| 185 |
+
return xyz, rpy
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
def _load_mesh(source_mesh: Path) -> trimesh.Trimesh:
|
| 189 |
+
loaded = trimesh.load(source_mesh, force="mesh")
|
| 190 |
+
if isinstance(loaded, trimesh.Scene):
|
| 191 |
+
meshes = [
|
| 192 |
+
geometry
|
| 193 |
+
for geometry in loaded.geometry.values()
|
| 194 |
+
if isinstance(geometry, trimesh.Trimesh)
|
| 195 |
+
]
|
| 196 |
+
if not meshes:
|
| 197 |
+
raise ValueError(f"No mesh geometry found in scene: {source_mesh}")
|
| 198 |
+
loaded = trimesh.util.concatenate(meshes)
|
| 199 |
+
if not isinstance(loaded, trimesh.Trimesh):
|
| 200 |
+
raise TypeError(
|
| 201 |
+
f"Expected a Trimesh from {source_mesh}, got {type(loaded)}"
|
| 202 |
+
)
|
| 203 |
+
if loaded.vertices.size == 0 or loaded.faces.size == 0:
|
| 204 |
+
raise ValueError(f"Source mesh is empty: {source_mesh}")
|
| 205 |
+
return loaded
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
def _clean_mesh(mesh: trimesh.Trimesh) -> tuple[trimesh.Trimesh, list[str]]:
|
| 209 |
+
mesh = mesh.copy()
|
| 210 |
+
warnings: list[str] = []
|
| 211 |
+
original_vertices = len(mesh.vertices)
|
| 212 |
+
original_faces = len(mesh.faces)
|
| 213 |
+
|
| 214 |
+
if not mesh.is_watertight:
|
| 215 |
+
warnings.append(
|
| 216 |
+
"mesh is not watertight; this is usually acceptable for cloth"
|
| 217 |
+
)
|
| 218 |
+
|
| 219 |
+
mesh.remove_unreferenced_vertices()
|
| 220 |
+
if hasattr(mesh, "nondegenerate_faces"):
|
| 221 |
+
mesh.update_faces(mesh.nondegenerate_faces())
|
| 222 |
+
else:
|
| 223 |
+
mesh.remove_degenerate_faces()
|
| 224 |
+
mesh.merge_vertices()
|
| 225 |
+
mesh.remove_unreferenced_vertices()
|
| 226 |
+
|
| 227 |
+
if (
|
| 228 |
+
len(mesh.vertices) != original_vertices
|
| 229 |
+
or len(mesh.faces) != original_faces
|
| 230 |
+
):
|
| 231 |
+
warnings.append(
|
| 232 |
+
"mesh cleanup changed topology from "
|
| 233 |
+
f"{original_vertices} vertices/{original_faces} faces to "
|
| 234 |
+
f"{len(mesh.vertices)} vertices/{len(mesh.faces)} faces"
|
| 235 |
+
)
|
| 236 |
+
if len(mesh.vertices) == 0 or len(mesh.faces) == 0:
|
| 237 |
+
raise ValueError("Mesh cleanup produced an empty cloth mesh")
|
| 238 |
+
return mesh, warnings
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
def _mesh_stats(mesh: trimesh.Trimesh) -> dict[str, Any]:
|
| 242 |
+
bbox_min, bbox_max = mesh.bounds
|
| 243 |
+
extents = np.asarray(mesh.extents, dtype=float)
|
| 244 |
+
return {
|
| 245 |
+
"vertices": int(len(mesh.vertices)),
|
| 246 |
+
"faces": int(len(mesh.faces)),
|
| 247 |
+
"bbox_min": bbox_min.astype(float).round(6).tolist(),
|
| 248 |
+
"bbox_max": bbox_max.astype(float).round(6).tolist(),
|
| 249 |
+
"extents": extents.round(6).tolist(),
|
| 250 |
+
"watertight": bool(mesh.is_watertight),
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
def _mesh_warnings(mesh_stats: dict[str, Any]) -> list[str]:
|
| 255 |
+
warnings: list[str] = []
|
| 256 |
+
if mesh_stats["faces"] > 20000:
|
| 257 |
+
warnings.append(
|
| 258 |
+
"mesh has more than 20000 faces; Genesis cloth simulation may be slow"
|
| 259 |
+
)
|
| 260 |
+
extents = np.asarray(mesh_stats["extents"], dtype=float)
|
| 261 |
+
if np.any(extents <= 1e-6):
|
| 262 |
+
warnings.append("mesh has a near-zero bounding-box extent")
|
| 263 |
+
if float(extents.max()) > 10.0:
|
| 264 |
+
warnings.append(
|
| 265 |
+
"mesh bounding box is larger than 10 meters; check scale"
|
| 266 |
+
)
|
| 267 |
+
return warnings
|
| 268 |
+
|
| 269 |
+
|
| 270 |
+
def _relative_path(path: Path, root: Path) -> str:
|
| 271 |
+
try:
|
| 272 |
+
return path.relative_to(root).as_posix()
|
| 273 |
+
except ValueError:
|
| 274 |
+
return path.as_posix()
|
| 275 |
+
|
| 276 |
+
|
| 277 |
+
def _write_json(path: Path, data: dict[str, Any]) -> None:
|
| 278 |
+
path.write_text(json.dumps(data, indent=2) + "\n")
|
| 279 |
+
|
| 280 |
+
|
| 281 |
+
if __name__ == "__main__":
|
| 282 |
+
entrypoint()
|
embodied_gen/scripts/deformable_sim/render_mesh_displacement_heatmap.py
ADDED
|
@@ -0,0 +1,551 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
import json
|
| 3 |
+
import math
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
import cv2
|
| 7 |
+
import numpy as np
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def main() -> None:
|
| 11 |
+
parser = argparse.ArgumentParser()
|
| 12 |
+
parser.add_argument("--video-path", required=True)
|
| 13 |
+
parser.add_argument("--mesh-path", required=True)
|
| 14 |
+
parser.add_argument("--particle-positions", required=True)
|
| 15 |
+
parser.add_argument("--sim-config", required=True)
|
| 16 |
+
parser.add_argument("--output-image", required=True)
|
| 17 |
+
parser.add_argument("--output-video", required=True)
|
| 18 |
+
parser.add_argument("--tmp-video", required=True)
|
| 19 |
+
args = parser.parse_args()
|
| 20 |
+
|
| 21 |
+
render_mesh_displacement_heatmap(
|
| 22 |
+
video_path=Path(args.video_path),
|
| 23 |
+
mesh_path=Path(args.mesh_path),
|
| 24 |
+
particle_positions_path=Path(args.particle_positions),
|
| 25 |
+
sim_config_path=Path(args.sim_config),
|
| 26 |
+
output_image_path=Path(args.output_image),
|
| 27 |
+
output_video_path=Path(args.output_video),
|
| 28 |
+
tmp_video_path=Path(args.tmp_video),
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def render_mesh_displacement_heatmap(
|
| 33 |
+
video_path: Path,
|
| 34 |
+
mesh_path: Path,
|
| 35 |
+
particle_positions_path: Path,
|
| 36 |
+
sim_config_path: Path,
|
| 37 |
+
output_image_path: Path,
|
| 38 |
+
output_video_path: Path,
|
| 39 |
+
tmp_video_path: Path,
|
| 40 |
+
) -> None:
|
| 41 |
+
data = np.load(particle_positions_path)
|
| 42 |
+
positions = data["positions"].astype(np.float32)
|
| 43 |
+
rest_positions = data["rest_positions"].astype(np.float32)
|
| 44 |
+
config = json.loads(sim_config_path.read_text())
|
| 45 |
+
|
| 46 |
+
if positions.ndim != 3 or positions.shape[2] != 3:
|
| 47 |
+
raise ValueError(
|
| 48 |
+
f"Invalid particle positions shape: {positions.shape}"
|
| 49 |
+
)
|
| 50 |
+
if rest_positions.shape != positions.shape[1:]:
|
| 51 |
+
raise ValueError(
|
| 52 |
+
"rest_positions must have shape "
|
| 53 |
+
f"{positions.shape[1:]}, got {rest_positions.shape}"
|
| 54 |
+
)
|
| 55 |
+
capture = cv2.VideoCapture(str(video_path))
|
| 56 |
+
if not capture.isOpened():
|
| 57 |
+
raise RuntimeError(f"Could not open video: {video_path}")
|
| 58 |
+
|
| 59 |
+
fps = capture.get(cv2.CAP_PROP_FPS) or float(config.get("fps", 30))
|
| 60 |
+
width = int(capture.get(cv2.CAP_PROP_FRAME_WIDTH))
|
| 61 |
+
height = int(capture.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
| 62 |
+
writer = cv2.VideoWriter(
|
| 63 |
+
str(tmp_video_path),
|
| 64 |
+
cv2.VideoWriter_fourcc(*"mp4v"),
|
| 65 |
+
fps,
|
| 66 |
+
(width, height),
|
| 67 |
+
)
|
| 68 |
+
if not writer.isOpened():
|
| 69 |
+
raise RuntimeError(f"Could not write video: {tmp_video_path}")
|
| 70 |
+
|
| 71 |
+
displacement = np.linalg.norm(
|
| 72 |
+
positions - rest_positions[None, :, :], axis=2
|
| 73 |
+
)
|
| 74 |
+
scale = (
|
| 75 |
+
float(np.percentile(displacement, 95)) if displacement.size else 1.0
|
| 76 |
+
)
|
| 77 |
+
scale = max(scale, 1e-6)
|
| 78 |
+
|
| 79 |
+
camera_pos = np.asarray(config["camera_pos"], dtype=np.float32)
|
| 80 |
+
camera_lookat = np.asarray(config["camera_lookat"], dtype=np.float32)
|
| 81 |
+
camera_fov = float(config["camera_fov"])
|
| 82 |
+
inset_bbox = _compute_global_inset_bbox(
|
| 83 |
+
positions=positions,
|
| 84 |
+
camera_pos=camera_pos,
|
| 85 |
+
camera_lookat=camera_lookat,
|
| 86 |
+
camera_fov=camera_fov,
|
| 87 |
+
image_size=(width, height),
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
snapshot_index = min(
|
| 91 |
+
positions.shape[0] - 1,
|
| 92 |
+
max(0, int(round(fps)) - 1),
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
frame_index = 0
|
| 96 |
+
snapshot_frame = None
|
| 97 |
+
try:
|
| 98 |
+
while frame_index < positions.shape[0]:
|
| 99 |
+
ok, frame_bgr = capture.read()
|
| 100 |
+
if not ok:
|
| 101 |
+
break
|
| 102 |
+
|
| 103 |
+
frame_rgb = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB)
|
| 104 |
+
heatmap = _compose_displacement_frame(
|
| 105 |
+
frame_rgb=frame_rgb,
|
| 106 |
+
positions=positions[frame_index],
|
| 107 |
+
displacement=displacement[frame_index],
|
| 108 |
+
scale=scale,
|
| 109 |
+
camera_pos=camera_pos,
|
| 110 |
+
camera_lookat=camera_lookat,
|
| 111 |
+
camera_fov=camera_fov,
|
| 112 |
+
inset_bbox=inset_bbox,
|
| 113 |
+
)
|
| 114 |
+
if frame_index == snapshot_index:
|
| 115 |
+
snapshot_frame = heatmap
|
| 116 |
+
writer.write(cv2.cvtColor(heatmap, cv2.COLOR_RGB2BGR))
|
| 117 |
+
frame_index += 1
|
| 118 |
+
finally:
|
| 119 |
+
capture.release()
|
| 120 |
+
writer.release()
|
| 121 |
+
|
| 122 |
+
if frame_index == 0 or snapshot_frame is None:
|
| 123 |
+
raise RuntimeError("No frames were written to mesh displacement video")
|
| 124 |
+
|
| 125 |
+
output_image_path.parent.mkdir(parents=True, exist_ok=True)
|
| 126 |
+
output_video_path.parent.mkdir(parents=True, exist_ok=True)
|
| 127 |
+
cv2.imwrite(
|
| 128 |
+
str(output_image_path), cv2.cvtColor(snapshot_frame, cv2.COLOR_RGB2BGR)
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def _compose_displacement_frame(
|
| 133 |
+
frame_rgb: np.ndarray,
|
| 134 |
+
positions: np.ndarray,
|
| 135 |
+
displacement: np.ndarray,
|
| 136 |
+
scale: float,
|
| 137 |
+
camera_pos: np.ndarray,
|
| 138 |
+
camera_lookat: np.ndarray,
|
| 139 |
+
camera_fov: float,
|
| 140 |
+
inset_bbox: tuple[int, int, int, int],
|
| 141 |
+
) -> np.ndarray:
|
| 142 |
+
height, width = frame_rgb.shape[:2]
|
| 143 |
+
pixels, depths = _project_points(
|
| 144 |
+
points=positions,
|
| 145 |
+
camera_pos=camera_pos,
|
| 146 |
+
camera_lookat=camera_lookat,
|
| 147 |
+
camera_fov=camera_fov,
|
| 148 |
+
image_size=(width, height),
|
| 149 |
+
)
|
| 150 |
+
normalized = np.clip(displacement / scale, 0.0, 1.0)
|
| 151 |
+
scalar_layer, weight_layer = _rasterize_particle_scalar_field(
|
| 152 |
+
pixels=pixels,
|
| 153 |
+
depths=depths,
|
| 154 |
+
normalized=normalized,
|
| 155 |
+
image_size=(width, height),
|
| 156 |
+
)
|
| 157 |
+
scalar_blur, weight_blur = _smooth_scalar_field(scalar_layer, weight_layer)
|
| 158 |
+
return _compose_rgb_with_inset(
|
| 159 |
+
frame_rgb=frame_rgb,
|
| 160 |
+
scalar_field=scalar_blur,
|
| 161 |
+
weight_field=weight_blur,
|
| 162 |
+
scale=scale,
|
| 163 |
+
inset_bbox=inset_bbox,
|
| 164 |
+
)
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
def _compute_global_inset_bbox(
|
| 168 |
+
positions: np.ndarray,
|
| 169 |
+
camera_pos: np.ndarray,
|
| 170 |
+
camera_lookat: np.ndarray,
|
| 171 |
+
camera_fov: float,
|
| 172 |
+
image_size: tuple[int, int],
|
| 173 |
+
) -> tuple[int, int, int, int]:
|
| 174 |
+
width, height = image_size
|
| 175 |
+
pad = 28
|
| 176 |
+
flat = positions.reshape(-1, 3)
|
| 177 |
+
pixels, depths = _project_points(
|
| 178 |
+
points=flat,
|
| 179 |
+
camera_pos=camera_pos,
|
| 180 |
+
camera_lookat=camera_lookat,
|
| 181 |
+
camera_fov=camera_fov,
|
| 182 |
+
image_size=image_size,
|
| 183 |
+
)
|
| 184 |
+
valid = depths > 1e-5
|
| 185 |
+
if not np.any(valid):
|
| 186 |
+
return (0, 0, width, height)
|
| 187 |
+
px = pixels[valid, 0]
|
| 188 |
+
py = pixels[valid, 1]
|
| 189 |
+
x0 = max(0, int(np.floor(px.min())) - pad)
|
| 190 |
+
x1 = min(width, int(np.ceil(px.max())) + pad)
|
| 191 |
+
y0 = max(0, int(np.floor(py.min())) - pad)
|
| 192 |
+
y1 = min(height, int(np.ceil(py.max())) + pad)
|
| 193 |
+
if x1 <= x0:
|
| 194 |
+
x1 = min(width, x0 + 1)
|
| 195 |
+
if y1 <= y0:
|
| 196 |
+
y1 = min(height, y0 + 1)
|
| 197 |
+
return (x0, y0, x1, y1)
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
def _rasterize_particle_scalar_field(
|
| 201 |
+
pixels: np.ndarray,
|
| 202 |
+
depths: np.ndarray,
|
| 203 |
+
normalized: np.ndarray,
|
| 204 |
+
image_size: tuple[int, int],
|
| 205 |
+
) -> tuple[np.ndarray, np.ndarray]:
|
| 206 |
+
width, height = image_size
|
| 207 |
+
scalar_layer = np.zeros((height, width), dtype=np.float32)
|
| 208 |
+
weight_layer = np.zeros((height, width), dtype=np.float32)
|
| 209 |
+
radius = max(5, int(round(min(width, height) / 140.0)))
|
| 210 |
+
for particle_index in np.argsort(depths)[::-1]:
|
| 211 |
+
if depths[particle_index] <= 1e-5:
|
| 212 |
+
continue
|
| 213 |
+
|
| 214 |
+
x_value, y_value = np.rint(pixels[particle_index]).astype(np.int32)
|
| 215 |
+
if (
|
| 216 |
+
x_value < -radius
|
| 217 |
+
or x_value >= width + radius
|
| 218 |
+
or y_value < -radius
|
| 219 |
+
or y_value >= height + radius
|
| 220 |
+
):
|
| 221 |
+
continue
|
| 222 |
+
|
| 223 |
+
center = (int(x_value), int(y_value))
|
| 224 |
+
cv2.circle(
|
| 225 |
+
scalar_layer, center, radius, float(normalized[particle_index]), -1
|
| 226 |
+
)
|
| 227 |
+
cv2.circle(weight_layer, center, radius, 1.0, -1)
|
| 228 |
+
|
| 229 |
+
return scalar_layer, weight_layer
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
def _smooth_scalar_field(
|
| 233 |
+
scalar_layer: np.ndarray,
|
| 234 |
+
weight_layer: np.ndarray,
|
| 235 |
+
) -> tuple[np.ndarray, np.ndarray]:
|
| 236 |
+
height, width = scalar_layer.shape
|
| 237 |
+
radius = max(5, int(round(min(width, height) / 140.0)))
|
| 238 |
+
sigma = max(1.2, float(radius) * 0.55)
|
| 239 |
+
scalar_blur = cv2.GaussianBlur(scalar_layer * weight_layer, (0, 0), sigma)
|
| 240 |
+
weight_blur = cv2.GaussianBlur(weight_layer, (0, 0), sigma)
|
| 241 |
+
scalar_blur = scalar_blur / np.maximum(weight_blur, 1e-6)
|
| 242 |
+
hard_mask = _close_mask(weight_layer > 0.0, radius=max(4, radius * 2))
|
| 243 |
+
scalar_blur[~hard_mask] = 0.0
|
| 244 |
+
weight_blur[~hard_mask] = 0.0
|
| 245 |
+
return scalar_blur, weight_blur
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
def _close_mask(mask: np.ndarray, radius: int) -> np.ndarray:
|
| 249 |
+
kernel_size = max(3, radius * 2 + 1)
|
| 250 |
+
kernel = cv2.getStructuringElement(
|
| 251 |
+
cv2.MORPH_ELLIPSE,
|
| 252 |
+
(kernel_size, kernel_size),
|
| 253 |
+
)
|
| 254 |
+
closed = cv2.morphologyEx(mask.astype(np.uint8), cv2.MORPH_CLOSE, kernel)
|
| 255 |
+
return closed.astype(bool)
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
def _compose_rgb_with_inset(
|
| 259 |
+
frame_rgb: np.ndarray,
|
| 260 |
+
scalar_field: np.ndarray,
|
| 261 |
+
weight_field: np.ndarray,
|
| 262 |
+
scale: float,
|
| 263 |
+
inset_bbox: tuple[int, int, int, int],
|
| 264 |
+
) -> np.ndarray:
|
| 265 |
+
height, width = frame_rgb.shape[:2]
|
| 266 |
+
x0, y0, x1, y1 = inset_bbox
|
| 267 |
+
|
| 268 |
+
heat_bgr = cv2.applyColorMap(
|
| 269 |
+
(np.clip(scalar_field, 0.0, 1.0) * 255).astype(np.uint8),
|
| 270 |
+
cv2.COLORMAP_TURBO,
|
| 271 |
+
)
|
| 272 |
+
heat_layer = cv2.cvtColor(heat_bgr, cv2.COLOR_BGR2RGB)
|
| 273 |
+
|
| 274 |
+
gray = cv2.cvtColor(frame_rgb, cv2.COLOR_RGB2GRAY)
|
| 275 |
+
gray_rgb = cv2.cvtColor(gray, cv2.COLOR_GRAY2RGB)
|
| 276 |
+
hard_mask = weight_field > 0.0
|
| 277 |
+
alpha = np.where(hard_mask, 0.82, 0.0).astype(np.float32)
|
| 278 |
+
inset = (
|
| 279 |
+
gray_rgb.astype(np.float32) * 0.68
|
| 280 |
+
+ frame_rgb.astype(np.float32) * 0.12
|
| 281 |
+
)
|
| 282 |
+
inset = (
|
| 283 |
+
inset * (1.0 - alpha[:, :, None])
|
| 284 |
+
+ heat_layer.astype(np.float32) * alpha[:, :, None]
|
| 285 |
+
)
|
| 286 |
+
inset = np.clip(inset[y0:y1, x0:x1], 0, 255).astype(np.uint8)
|
| 287 |
+
|
| 288 |
+
inset_width = 250
|
| 289 |
+
crop_height, crop_width = inset.shape[:2]
|
| 290 |
+
inset_height = max(1, int(crop_height * inset_width / max(crop_width, 1)))
|
| 291 |
+
inset = cv2.resize(
|
| 292 |
+
inset, (inset_width, inset_height), interpolation=cv2.INTER_AREA
|
| 293 |
+
)
|
| 294 |
+
|
| 295 |
+
legend_width = 30
|
| 296 |
+
legend_gap = 10
|
| 297 |
+
label_width = 54
|
| 298 |
+
margin = 28
|
| 299 |
+
panel_pad = 12
|
| 300 |
+
panel_width = (
|
| 301 |
+
inset_width + legend_gap + legend_width + label_width + panel_pad * 2
|
| 302 |
+
)
|
| 303 |
+
panel_height = inset_height + panel_pad * 2
|
| 304 |
+
panel_x = width - panel_width - margin
|
| 305 |
+
panel_y = margin
|
| 306 |
+
|
| 307 |
+
canvas = frame_rgb.copy()
|
| 308 |
+
panel = np.full((panel_height, panel_width, 3), 247, dtype=np.uint8)
|
| 309 |
+
panel = (panel.astype(np.float32) * 0.92).astype(np.uint8)
|
| 310 |
+
cv2.rectangle(
|
| 311 |
+
panel,
|
| 312 |
+
(0, 0),
|
| 313 |
+
(panel_width - 1, panel_height - 1),
|
| 314 |
+
(160, 168, 178),
|
| 315 |
+
1,
|
| 316 |
+
)
|
| 317 |
+
|
| 318 |
+
ix = panel_pad
|
| 319 |
+
iy = panel_pad
|
| 320 |
+
panel[iy : iy + inset_height, ix : ix + inset_width] = inset
|
| 321 |
+
|
| 322 |
+
bar_x = ix + inset_width + legend_gap
|
| 323 |
+
bar = np.linspace(1.0, 0.0, inset_height, dtype=np.float32)[:, None]
|
| 324 |
+
bar_bgr = cv2.applyColorMap(
|
| 325 |
+
(bar * 255).astype(np.uint8), cv2.COLORMAP_TURBO
|
| 326 |
+
)
|
| 327 |
+
bar_rgb = cv2.cvtColor(bar_bgr, cv2.COLOR_BGR2RGB)
|
| 328 |
+
panel[iy : iy + inset_height, bar_x : bar_x + legend_width] = np.repeat(
|
| 329 |
+
bar_rgb,
|
| 330 |
+
legend_width,
|
| 331 |
+
axis=1,
|
| 332 |
+
)
|
| 333 |
+
text_x = bar_x + legend_width + 4
|
| 334 |
+
cv2.putText(
|
| 335 |
+
panel,
|
| 336 |
+
f"{scale:.3f}m",
|
| 337 |
+
(text_x, iy + 12),
|
| 338 |
+
cv2.FONT_HERSHEY_SIMPLEX,
|
| 339 |
+
0.36,
|
| 340 |
+
(35, 40, 48),
|
| 341 |
+
1,
|
| 342 |
+
cv2.LINE_AA,
|
| 343 |
+
)
|
| 344 |
+
cv2.putText(
|
| 345 |
+
panel,
|
| 346 |
+
"0",
|
| 347 |
+
(text_x, iy + inset_height),
|
| 348 |
+
cv2.FONT_HERSHEY_SIMPLEX,
|
| 349 |
+
0.36,
|
| 350 |
+
(35, 40, 48),
|
| 351 |
+
1,
|
| 352 |
+
cv2.LINE_AA,
|
| 353 |
+
)
|
| 354 |
+
canvas[
|
| 355 |
+
panel_y : panel_y + panel_height, panel_x : panel_x + panel_width
|
| 356 |
+
] = panel
|
| 357 |
+
return canvas
|
| 358 |
+
|
| 359 |
+
|
| 360 |
+
def _project_points(
|
| 361 |
+
points: np.ndarray,
|
| 362 |
+
camera_pos: np.ndarray,
|
| 363 |
+
camera_lookat: np.ndarray,
|
| 364 |
+
camera_fov: float,
|
| 365 |
+
image_size: tuple[int, int],
|
| 366 |
+
) -> tuple[np.ndarray, np.ndarray]:
|
| 367 |
+
width, height = image_size
|
| 368 |
+
forward = _normalize(camera_lookat - camera_pos)
|
| 369 |
+
world_up = np.array([0.0, 0.0, 1.0], dtype=np.float32)
|
| 370 |
+
right = np.cross(forward, world_up)
|
| 371 |
+
if np.linalg.norm(right) < 1e-6:
|
| 372 |
+
world_up = np.array([0.0, 1.0, 0.0], dtype=np.float32)
|
| 373 |
+
right = np.cross(forward, world_up)
|
| 374 |
+
right = _normalize(right)
|
| 375 |
+
up = _normalize(np.cross(right, forward))
|
| 376 |
+
|
| 377 |
+
relative = points - camera_pos[None, :]
|
| 378 |
+
cam_x = relative @ right
|
| 379 |
+
cam_y = relative @ up
|
| 380 |
+
cam_z = relative @ forward
|
| 381 |
+
focal = 0.5 * float(height) / math.tan(math.radians(camera_fov) * 0.5)
|
| 382 |
+
pixels = np.stack(
|
| 383 |
+
[
|
| 384 |
+
width * 0.5 + focal * cam_x / np.maximum(cam_z, 1e-6),
|
| 385 |
+
height * 0.5 - focal * cam_y / np.maximum(cam_z, 1e-6),
|
| 386 |
+
],
|
| 387 |
+
axis=1,
|
| 388 |
+
)
|
| 389 |
+
return pixels, cam_z
|
| 390 |
+
|
| 391 |
+
|
| 392 |
+
def _draw_legend(image: np.ndarray, scale: float) -> None:
|
| 393 |
+
height, width = image.shape[:2]
|
| 394 |
+
bar_height = min(240, max(120, height // 4))
|
| 395 |
+
bar_width = 26
|
| 396 |
+
margin = 28
|
| 397 |
+
x0 = width - margin - bar_width - 52
|
| 398 |
+
y0 = margin
|
| 399 |
+
|
| 400 |
+
panel = image[
|
| 401 |
+
y0 - 10 : y0 + bar_height + 18, x0 - 10 : x0 + bar_width + 52
|
| 402 |
+
]
|
| 403 |
+
panel[:] = (panel.astype(np.float32) * 0.35 + 245.0 * 0.65).astype(
|
| 404 |
+
np.uint8
|
| 405 |
+
)
|
| 406 |
+
cv2.rectangle(
|
| 407 |
+
image,
|
| 408 |
+
(x0 - 10, y0 - 10),
|
| 409 |
+
(x0 + bar_width + 52, y0 + bar_height + 18),
|
| 410 |
+
(155, 164, 176),
|
| 411 |
+
1,
|
| 412 |
+
)
|
| 413 |
+
|
| 414 |
+
values = np.linspace(1.0, 0.0, bar_height, dtype=np.float32)[:, None]
|
| 415 |
+
colors_bgr = cv2.applyColorMap(
|
| 416 |
+
(values * 255).astype(np.uint8),
|
| 417 |
+
cv2.COLORMAP_TURBO,
|
| 418 |
+
)
|
| 419 |
+
colors_rgb = cv2.cvtColor(colors_bgr, cv2.COLOR_BGR2RGB)
|
| 420 |
+
image[y0 : y0 + bar_height, x0 : x0 + bar_width] = np.repeat(
|
| 421 |
+
colors_rgb,
|
| 422 |
+
bar_width,
|
| 423 |
+
axis=1,
|
| 424 |
+
)
|
| 425 |
+
cv2.putText(
|
| 426 |
+
image,
|
| 427 |
+
f"{scale:.3f}m",
|
| 428 |
+
(x0 + bar_width + 6, y0 + 12),
|
| 429 |
+
cv2.FONT_HERSHEY_SIMPLEX,
|
| 430 |
+
0.38,
|
| 431 |
+
(35, 40, 48),
|
| 432 |
+
1,
|
| 433 |
+
cv2.LINE_AA,
|
| 434 |
+
)
|
| 435 |
+
cv2.putText(
|
| 436 |
+
image,
|
| 437 |
+
"0",
|
| 438 |
+
(x0 + bar_width + 6, y0 + bar_height),
|
| 439 |
+
cv2.FONT_HERSHEY_SIMPLEX,
|
| 440 |
+
0.38,
|
| 441 |
+
(35, 40, 48),
|
| 442 |
+
1,
|
| 443 |
+
cv2.LINE_AA,
|
| 444 |
+
)
|
| 445 |
+
|
| 446 |
+
|
| 447 |
+
def _load_obj_mesh(mesh_path: Path) -> tuple[np.ndarray, np.ndarray]:
|
| 448 |
+
vertices: list[list[float]] = []
|
| 449 |
+
triangles: list[list[int]] = []
|
| 450 |
+
for line in mesh_path.read_text(errors="ignore").splitlines():
|
| 451 |
+
if line.startswith("v "):
|
| 452 |
+
_, x_value, y_value, z_value, *_ = line.split()
|
| 453 |
+
vertices.append([float(x_value), float(y_value), float(z_value)])
|
| 454 |
+
continue
|
| 455 |
+
if not line.startswith("f "):
|
| 456 |
+
continue
|
| 457 |
+
|
| 458 |
+
indices = [
|
| 459 |
+
_parse_obj_index(token, len(vertices))
|
| 460 |
+
for token in line.split()[1:]
|
| 461 |
+
]
|
| 462 |
+
if len(indices) < 3:
|
| 463 |
+
continue
|
| 464 |
+
for offset in range(1, len(indices) - 1):
|
| 465 |
+
triangles.append(
|
| 466 |
+
[indices[0], indices[offset], indices[offset + 1]]
|
| 467 |
+
)
|
| 468 |
+
|
| 469 |
+
if not vertices:
|
| 470 |
+
raise ValueError(f"No vertices found in OBJ mesh: {mesh_path}")
|
| 471 |
+
if not triangles:
|
| 472 |
+
raise ValueError(f"No faces found in OBJ mesh: {mesh_path}")
|
| 473 |
+
return (
|
| 474 |
+
np.asarray(vertices, dtype=np.float32),
|
| 475 |
+
np.asarray(triangles, dtype=np.int32),
|
| 476 |
+
)
|
| 477 |
+
|
| 478 |
+
|
| 479 |
+
def _map_mesh_faces_to_particles(
|
| 480 |
+
mesh_vertices: np.ndarray,
|
| 481 |
+
mesh_faces: np.ndarray,
|
| 482 |
+
rest_positions: np.ndarray,
|
| 483 |
+
init_height: float,
|
| 484 |
+
mesh_scale: float,
|
| 485 |
+
) -> np.ndarray:
|
| 486 |
+
world_vertices = mesh_vertices * float(mesh_scale)
|
| 487 |
+
world_vertices[:, 2] += float(init_height)
|
| 488 |
+
vertex_to_particle = _nearest_indices(
|
| 489 |
+
queries=world_vertices,
|
| 490 |
+
points=rest_positions,
|
| 491 |
+
)
|
| 492 |
+
particle_faces = vertex_to_particle[mesh_faces]
|
| 493 |
+
unique_counts = np.asarray(
|
| 494 |
+
[len(set(face.tolist())) for face in particle_faces],
|
| 495 |
+
dtype=np.int32,
|
| 496 |
+
)
|
| 497 |
+
particle_faces = particle_faces[unique_counts == 3]
|
| 498 |
+
if particle_faces.size == 0:
|
| 499 |
+
raise ValueError(
|
| 500 |
+
"No non-degenerate particle faces remained after mapping"
|
| 501 |
+
)
|
| 502 |
+
return np.unique(particle_faces, axis=0).astype(np.int32)
|
| 503 |
+
|
| 504 |
+
|
| 505 |
+
def _nearest_indices(
|
| 506 |
+
queries: np.ndarray,
|
| 507 |
+
points: np.ndarray,
|
| 508 |
+
chunk_size: int = 1024,
|
| 509 |
+
) -> np.ndarray:
|
| 510 |
+
nearest: list[np.ndarray] = []
|
| 511 |
+
points_sq = np.sum(points * points, axis=1)[None, :]
|
| 512 |
+
for start in range(0, queries.shape[0], chunk_size):
|
| 513 |
+
chunk = queries[start : start + chunk_size]
|
| 514 |
+
distances = (
|
| 515 |
+
np.sum(chunk * chunk, axis=1)[:, None]
|
| 516 |
+
+ points_sq
|
| 517 |
+
- 2.0 * chunk @ points.T
|
| 518 |
+
)
|
| 519 |
+
nearest.append(np.argmin(distances, axis=1).astype(np.int32))
|
| 520 |
+
return np.concatenate(nearest, axis=0)
|
| 521 |
+
|
| 522 |
+
|
| 523 |
+
def _parse_obj_index(token: str, vertex_count: int) -> int:
|
| 524 |
+
raw = int(token.split("/")[0])
|
| 525 |
+
if raw < 0:
|
| 526 |
+
return vertex_count + raw
|
| 527 |
+
return raw - 1
|
| 528 |
+
|
| 529 |
+
|
| 530 |
+
def _is_triangle_outside_image(
|
| 531 |
+
triangle: np.ndarray,
|
| 532 |
+
width: int,
|
| 533 |
+
height: int,
|
| 534 |
+
) -> bool:
|
| 535 |
+
return (
|
| 536 |
+
triangle[:, 0].max() < 0
|
| 537 |
+
or triangle[:, 0].min() >= width
|
| 538 |
+
or triangle[:, 1].max() < 0
|
| 539 |
+
or triangle[:, 1].min() >= height
|
| 540 |
+
)
|
| 541 |
+
|
| 542 |
+
|
| 543 |
+
def _normalize(vector: np.ndarray) -> np.ndarray:
|
| 544 |
+
norm = float(np.linalg.norm(vector))
|
| 545 |
+
if norm <= 1e-8:
|
| 546 |
+
raise ValueError("Cannot normalize near-zero vector")
|
| 547 |
+
return vector / norm
|
| 548 |
+
|
| 549 |
+
|
| 550 |
+
if __name__ == "__main__":
|
| 551 |
+
main()
|
embodied_gen/scripts/deformable_sim/simulate_genesis_cloth.py
ADDED
|
@@ -0,0 +1,608 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Project EmbodiedGen
|
| 2 |
+
#
|
| 3 |
+
# Copyright (c) 2025 Horizon Robotics. All Rights Reserved.
|
| 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
|
| 14 |
+
# implied. See the License for the specific language governing
|
| 15 |
+
# permissions and limitations under the License.
|
| 16 |
+
|
| 17 |
+
import json
|
| 18 |
+
import math
|
| 19 |
+
from dataclasses import dataclass
|
| 20 |
+
from pathlib import Path
|
| 21 |
+
from typing import Any
|
| 22 |
+
|
| 23 |
+
import tyro
|
| 24 |
+
from tqdm import tqdm
|
| 25 |
+
from embodied_gen.utils.log import logger
|
| 26 |
+
|
| 27 |
+
DEFAULT_CAMERA_FOV = 45
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
@dataclass
|
| 31 |
+
class GenesisClothSimConfig:
|
| 32 |
+
asset_path: str = "outputs/cloth"
|
| 33 |
+
output_dir: str | None = None
|
| 34 |
+
init_height: float = 1.0
|
| 35 |
+
duration_seconds: float = 6.0
|
| 36 |
+
sim_steps: int | None = None
|
| 37 |
+
shake: bool = True
|
| 38 |
+
shake_steps: int | None = None
|
| 39 |
+
shake_amplitude: float = 0.08
|
| 40 |
+
device: str = "cpu"
|
| 41 |
+
headless: bool = True
|
| 42 |
+
fps: int = 30
|
| 43 |
+
render_interval: int = 1
|
| 44 |
+
camera_res: tuple[int, int] = (512, 512)
|
| 45 |
+
gravity: tuple[float, float, float] = (0.0, 0.0, 0.0)
|
| 46 |
+
background_color: tuple[float, float, float] = (0.78, 0.82, 0.88)
|
| 47 |
+
ambient_light: tuple[float, float, float] = (0.55, 0.55, 0.55)
|
| 48 |
+
floor_size: tuple[float, float] = (6.0, 6.0)
|
| 49 |
+
floor_tile_size: tuple[float, float] = (0.5, 0.5)
|
| 50 |
+
floor_color: tuple[float, float, float, float] = (0.72, 0.72, 0.68, 1.0)
|
| 51 |
+
backdrop: bool = False
|
| 52 |
+
backdrop_color: tuple[float, float, float, float] = (0.80, 0.84, 0.90, 1.0)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def simulate_genesis_cloth(
|
| 56 |
+
asset_path: str = "outputs/cloth",
|
| 57 |
+
output_dir: str | None = None,
|
| 58 |
+
init_height: float = 1.0,
|
| 59 |
+
duration_seconds: float = 6.0,
|
| 60 |
+
sim_steps: int | None = None,
|
| 61 |
+
shake: bool = True,
|
| 62 |
+
shake_steps: int | None = None,
|
| 63 |
+
shake_amplitude: float = 0.08,
|
| 64 |
+
device: str = "cpu",
|
| 65 |
+
headless: bool = True,
|
| 66 |
+
fps: int = 30,
|
| 67 |
+
render_interval: int = 1,
|
| 68 |
+
camera_res: tuple[int, int] = (512, 512),
|
| 69 |
+
gravity: tuple[float, float, float] = (0.0, 0.0, 0.0),
|
| 70 |
+
background_color: tuple[float, float, float] = (0.78, 0.82, 0.88),
|
| 71 |
+
ambient_light: tuple[float, float, float] = (0.55, 0.55, 0.55),
|
| 72 |
+
floor_size: tuple[float, float] = (6.0, 6.0),
|
| 73 |
+
floor_tile_size: tuple[float, float] = (0.5, 0.5),
|
| 74 |
+
floor_color: tuple[float, float, float, float] = (0.72, 0.72, 0.68, 1.0),
|
| 75 |
+
backdrop: bool = False,
|
| 76 |
+
backdrop_color: tuple[float, float, float, float] = (
|
| 77 |
+
0.80,
|
| 78 |
+
0.84,
|
| 79 |
+
0.90,
|
| 80 |
+
1.0,
|
| 81 |
+
),
|
| 82 |
+
) -> str:
|
| 83 |
+
"""Run a Genesis cloth drop-and-shake demo.
|
| 84 |
+
|
| 85 |
+
Args:
|
| 86 |
+
asset_path: Directory containing ``genesis/manifest.json``.
|
| 87 |
+
output_dir: Directory for simulation outputs. If unset, writes to
|
| 88 |
+
``asset_path/genesis_sim``.
|
| 89 |
+
init_height: Height in meters for the cloth initial position.
|
| 90 |
+
duration_seconds: Default output video duration when ``sim_steps`` is not
|
| 91 |
+
set. Defaults to 6 seconds.
|
| 92 |
+
sim_steps: Optional number of Genesis simulation steps to run. If unset,
|
| 93 |
+
it is derived from ``duration_seconds * fps * render_interval``.
|
| 94 |
+
shake: Whether to apply a visible nonuniform particle displacement.
|
| 95 |
+
shake_steps: Number of steps that receive the shake motion. If unset,
|
| 96 |
+
all simulation steps are shaken.
|
| 97 |
+
shake_amplitude: Maximum displacement magnitude in meters.
|
| 98 |
+
device: Genesis backend name: ``"cpu"``, ``"cuda"``, or ``"auto"``.
|
| 99 |
+
headless: Whether to run Genesis without opening the viewer.
|
| 100 |
+
Genesis 0.4.7 uses Scene(show_viewer=False) for headless mode.
|
| 101 |
+
fps: Output video frames per second.
|
| 102 |
+
render_interval: Render every N simulation steps.
|
| 103 |
+
camera_res: Output camera resolution as ``(width, height)``. Defaults
|
| 104 |
+
to 512x512.
|
| 105 |
+
gravity: Gravity vector. Defaults to zero for the wind-sway demo so
|
| 106 |
+
the shirt does not visibly sag before the lower edge starts moving.
|
| 107 |
+
background_color: RGB color for the renderer background.
|
| 108 |
+
ambient_light: RGB ambient light color.
|
| 109 |
+
floor_size: Visible floor plane size in meters.
|
| 110 |
+
floor_tile_size: Visual tile size for the floor plane.
|
| 111 |
+
floor_color: RGBA floor material color.
|
| 112 |
+
backdrop: Whether to add a vertical background plane behind the cloth.
|
| 113 |
+
backdrop_color: RGBA backdrop material color.
|
| 114 |
+
|
| 115 |
+
Returns:
|
| 116 |
+
Path to the generated video file.
|
| 117 |
+
"""
|
| 118 |
+
|
| 119 |
+
if duration_seconds <= 0:
|
| 120 |
+
raise ValueError("duration_seconds must be positive")
|
| 121 |
+
if fps <= 0:
|
| 122 |
+
raise ValueError("fps must be positive")
|
| 123 |
+
if render_interval <= 0:
|
| 124 |
+
raise ValueError("render_interval must be positive")
|
| 125 |
+
if sim_steps is None:
|
| 126 |
+
sim_steps = max(
|
| 127 |
+
1, int(round(duration_seconds * fps * render_interval))
|
| 128 |
+
)
|
| 129 |
+
if sim_steps <= 0:
|
| 130 |
+
raise ValueError("sim_steps must be positive")
|
| 131 |
+
if shake_steps is None:
|
| 132 |
+
shake_steps = sim_steps
|
| 133 |
+
if shake_steps < 0:
|
| 134 |
+
raise ValueError("shake_steps must be non-negative")
|
| 135 |
+
|
| 136 |
+
asset_dir = Path(asset_path)
|
| 137 |
+
manifest_path = asset_dir / "genesis" / "manifest.json"
|
| 138 |
+
manifest = _read_json(manifest_path)
|
| 139 |
+
material_config = _read_json(asset_dir / manifest["material"])
|
| 140 |
+
cloth_mesh_path = asset_dir / manifest["cloth_mesh"]
|
| 141 |
+
if not cloth_mesh_path.exists():
|
| 142 |
+
raise FileNotFoundError(
|
| 143 |
+
f"Converted cloth mesh not found: {cloth_mesh_path}"
|
| 144 |
+
)
|
| 145 |
+
|
| 146 |
+
sim_output_dir = (
|
| 147 |
+
Path(output_dir) if output_dir else asset_dir / "genesis_sim"
|
| 148 |
+
)
|
| 149 |
+
sim_output_dir.mkdir(parents=True, exist_ok=True)
|
| 150 |
+
logger.info(f"Genesis cloth outputs will be saved to {sim_output_dir}")
|
| 151 |
+
video_path = sim_output_dir / "video.mp4"
|
| 152 |
+
summary_path = sim_output_dir / "run_summary.json"
|
| 153 |
+
config_path = sim_output_dir / "sim_config.json"
|
| 154 |
+
particle_positions_path = sim_output_dir / "particle_positions.npz"
|
| 155 |
+
|
| 156 |
+
import genesis as gs
|
| 157 |
+
|
| 158 |
+
backend_name, backend_warning = _init_genesis_backend(gs, device)
|
| 159 |
+
|
| 160 |
+
pbd_options = gs.options.PBDOptions(
|
| 161 |
+
particle_size=float(material_config.get("particle_size", 0.02)),
|
| 162 |
+
gravity=tuple(float(value) for value in gravity),
|
| 163 |
+
)
|
| 164 |
+
camera_pos, camera_lookat = _front_camera_pose(manifest, init_height)
|
| 165 |
+
scene = gs.Scene(
|
| 166 |
+
show_viewer=not headless,
|
| 167 |
+
sim_options=gs.options.SimOptions(
|
| 168 |
+
dt=1.0 / float(fps * render_interval),
|
| 169 |
+
gravity=tuple(float(value) for value in gravity),
|
| 170 |
+
),
|
| 171 |
+
pbd_options=pbd_options,
|
| 172 |
+
vis_options=gs.options.VisOptions(
|
| 173 |
+
background_color=tuple(float(value) for value in background_color),
|
| 174 |
+
ambient_light=tuple(float(value) for value in ambient_light),
|
| 175 |
+
shadow=True,
|
| 176 |
+
plane_reflection=False,
|
| 177 |
+
),
|
| 178 |
+
)
|
| 179 |
+
_add_scene_floor_and_backdrop(
|
| 180 |
+
gs=gs,
|
| 181 |
+
scene=scene,
|
| 182 |
+
camera_pos=camera_pos,
|
| 183 |
+
camera_lookat=camera_lookat,
|
| 184 |
+
floor_size=floor_size,
|
| 185 |
+
floor_tile_size=floor_tile_size,
|
| 186 |
+
floor_color=floor_color,
|
| 187 |
+
backdrop=backdrop,
|
| 188 |
+
backdrop_color=backdrop_color,
|
| 189 |
+
)
|
| 190 |
+
cloth = scene.add_entity(
|
| 191 |
+
morph=gs.morphs.Mesh(
|
| 192 |
+
file=str(cloth_mesh_path),
|
| 193 |
+
pos=(0.0, 0.0, float(init_height)),
|
| 194 |
+
scale=float(manifest.get("scale", 1.0)),
|
| 195 |
+
convexify=False,
|
| 196 |
+
),
|
| 197 |
+
material=gs.materials.PBD.Cloth(
|
| 198 |
+
rho=float(material_config.get("rho", 0.2)),
|
| 199 |
+
static_friction=float(material_config.get("static_friction", 0.5)),
|
| 200 |
+
kinetic_friction=float(
|
| 201 |
+
material_config.get("kinetic_friction", 0.4)
|
| 202 |
+
),
|
| 203 |
+
stretch_compliance=float(
|
| 204 |
+
material_config.get("stretch_compliance", 1e-6)
|
| 205 |
+
),
|
| 206 |
+
bending_compliance=float(
|
| 207 |
+
material_config.get("bending_compliance", 1e-4)
|
| 208 |
+
),
|
| 209 |
+
stretch_relaxation=float(
|
| 210 |
+
material_config.get("stretch_relaxation", 0.3)
|
| 211 |
+
),
|
| 212 |
+
bending_relaxation=float(
|
| 213 |
+
material_config.get("bending_relaxation", 0.1)
|
| 214 |
+
),
|
| 215 |
+
air_resistance=float(material_config.get("air_resistance", 1e-3)),
|
| 216 |
+
),
|
| 217 |
+
)
|
| 218 |
+
camera = scene.add_camera(
|
| 219 |
+
res=camera_res,
|
| 220 |
+
pos=camera_pos,
|
| 221 |
+
lookat=camera_lookat,
|
| 222 |
+
fov=DEFAULT_CAMERA_FOV,
|
| 223 |
+
)
|
| 224 |
+
scene.build()
|
| 225 |
+
|
| 226 |
+
config = {
|
| 227 |
+
"asset_path": str(asset_dir),
|
| 228 |
+
"manifest": str(manifest_path),
|
| 229 |
+
"manifest_scale": float(manifest.get("scale", 1.0)),
|
| 230 |
+
"init_height": init_height,
|
| 231 |
+
"duration_seconds": duration_seconds,
|
| 232 |
+
"sim_steps": sim_steps,
|
| 233 |
+
"shake": shake,
|
| 234 |
+
"shake_steps": shake_steps,
|
| 235 |
+
"shake_amplitude": shake_amplitude,
|
| 236 |
+
"device": device,
|
| 237 |
+
"resolved_backend": backend_name,
|
| 238 |
+
"backend_warning": backend_warning,
|
| 239 |
+
"headless": headless,
|
| 240 |
+
"fps": fps,
|
| 241 |
+
"render_interval": render_interval,
|
| 242 |
+
"camera_res": list(camera_res),
|
| 243 |
+
"camera_pos": list(camera_pos),
|
| 244 |
+
"camera_lookat": list(camera_lookat),
|
| 245 |
+
"camera_fov": DEFAULT_CAMERA_FOV,
|
| 246 |
+
"gravity": list(gravity),
|
| 247 |
+
"background_color": list(background_color),
|
| 248 |
+
"ambient_light": list(ambient_light),
|
| 249 |
+
"floor_size": list(floor_size),
|
| 250 |
+
"floor_tile_size": list(floor_tile_size),
|
| 251 |
+
"floor_color": list(floor_color),
|
| 252 |
+
"backdrop": backdrop,
|
| 253 |
+
"backdrop_color": list(backdrop_color),
|
| 254 |
+
}
|
| 255 |
+
_write_json(config_path, config)
|
| 256 |
+
|
| 257 |
+
rest_positions = cloth.get_particles_pos().clone()
|
| 258 |
+
particle_frames = []
|
| 259 |
+
rendered_frames = 0
|
| 260 |
+
camera.start_recording()
|
| 261 |
+
for step in tqdm(range(sim_steps), desc="Genesis Cloth Simulation"):
|
| 262 |
+
if shake and step < shake_steps:
|
| 263 |
+
_shake_cloth_particles(
|
| 264 |
+
gs,
|
| 265 |
+
cloth,
|
| 266 |
+
rest_positions,
|
| 267 |
+
step,
|
| 268 |
+
shake_steps,
|
| 269 |
+
shake_amplitude,
|
| 270 |
+
)
|
| 271 |
+
scene.step()
|
| 272 |
+
if step % render_interval == 0:
|
| 273 |
+
particle_frames.append(_tensor_to_numpy(cloth.get_particles_pos()))
|
| 274 |
+
camera.render()
|
| 275 |
+
rendered_frames += 1
|
| 276 |
+
_save_camera_recording(camera, video_path, fps)
|
| 277 |
+
_save_particle_positions(
|
| 278 |
+
particle_positions_path=particle_positions_path,
|
| 279 |
+
particle_frames=particle_frames,
|
| 280 |
+
rest_positions=rest_positions,
|
| 281 |
+
fps=fps,
|
| 282 |
+
render_interval=render_interval,
|
| 283 |
+
)
|
| 284 |
+
|
| 285 |
+
summary = {
|
| 286 |
+
"video": str(video_path),
|
| 287 |
+
"particle_positions": str(particle_positions_path),
|
| 288 |
+
"rendered_frames": rendered_frames,
|
| 289 |
+
"particles": int(cloth.n_particles),
|
| 290 |
+
"sim_steps": sim_steps,
|
| 291 |
+
"duration_seconds": duration_seconds,
|
| 292 |
+
"requested_device": device,
|
| 293 |
+
"resolved_backend": backend_name,
|
| 294 |
+
"backend_warning": backend_warning,
|
| 295 |
+
"status": "completed",
|
| 296 |
+
}
|
| 297 |
+
_write_json(summary_path, summary)
|
| 298 |
+
logger.info(
|
| 299 |
+
"Genesis cloth simulation outputs: "
|
| 300 |
+
f"video {video_path}, particles {particle_positions_path}, "
|
| 301 |
+
f"config {config_path}, summary {summary_path}"
|
| 302 |
+
)
|
| 303 |
+
return str(video_path)
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
def entrypoint(**kwargs) -> str:
|
| 307 |
+
if kwargs:
|
| 308 |
+
cfg = GenesisClothSimConfig(**kwargs)
|
| 309 |
+
else:
|
| 310 |
+
cfg = tyro.cli(GenesisClothSimConfig)
|
| 311 |
+
|
| 312 |
+
return simulate_genesis_cloth(
|
| 313 |
+
asset_path=cfg.asset_path,
|
| 314 |
+
output_dir=cfg.output_dir,
|
| 315 |
+
init_height=cfg.init_height,
|
| 316 |
+
duration_seconds=cfg.duration_seconds,
|
| 317 |
+
sim_steps=cfg.sim_steps,
|
| 318 |
+
shake=cfg.shake,
|
| 319 |
+
shake_steps=cfg.shake_steps,
|
| 320 |
+
shake_amplitude=cfg.shake_amplitude,
|
| 321 |
+
device=cfg.device,
|
| 322 |
+
headless=cfg.headless,
|
| 323 |
+
fps=cfg.fps,
|
| 324 |
+
render_interval=cfg.render_interval,
|
| 325 |
+
camera_res=cfg.camera_res,
|
| 326 |
+
gravity=cfg.gravity,
|
| 327 |
+
background_color=cfg.background_color,
|
| 328 |
+
ambient_light=cfg.ambient_light,
|
| 329 |
+
floor_size=cfg.floor_size,
|
| 330 |
+
floor_tile_size=cfg.floor_tile_size,
|
| 331 |
+
floor_color=cfg.floor_color,
|
| 332 |
+
backdrop=cfg.backdrop,
|
| 333 |
+
backdrop_color=cfg.backdrop_color,
|
| 334 |
+
)
|
| 335 |
+
|
| 336 |
+
|
| 337 |
+
def _shake_cloth_particles(
|
| 338 |
+
gs: Any,
|
| 339 |
+
cloth: Any,
|
| 340 |
+
rest_positions: Any,
|
| 341 |
+
step: int,
|
| 342 |
+
shake_steps: int,
|
| 343 |
+
amplitude: float,
|
| 344 |
+
) -> None:
|
| 345 |
+
if amplitude <= 0.0 or shake_steps <= 0:
|
| 346 |
+
return
|
| 347 |
+
|
| 348 |
+
positions = cloth.get_particles_pos().clone()
|
| 349 |
+
min_values = rest_positions.min(dim=0).values
|
| 350 |
+
max_values = rest_positions.max(dim=0).values
|
| 351 |
+
height = gs.torch.clamp(max_values[2] - min_values[2], min=1e-6)
|
| 352 |
+
y_values = rest_positions[:, 1]
|
| 353 |
+
z_values = rest_positions[:, 2]
|
| 354 |
+
|
| 355 |
+
top_ratio = gs.torch.clamp((max_values[2] - z_values) / height, 0.0, 1.0)
|
| 356 |
+
anchor_mask = top_ratio < 0.01
|
| 357 |
+
wind_strength = (
|
| 358 |
+
gs.torch.clamp((top_ratio - 0.005) / 0.995, 0.0, 1.0) ** 1.1
|
| 359 |
+
)
|
| 360 |
+
|
| 361 |
+
progress = float(step) / max(float(shake_steps), 1.0)
|
| 362 |
+
phase = 2.0 * math.pi * 2.2 * progress
|
| 363 |
+
traveling_phase = phase - 0.85 * top_ratio
|
| 364 |
+
center_y = rest_positions[:, 1].mean()
|
| 365 |
+
width = gs.torch.clamp(max_values[1] - min_values[1], min=1e-6)
|
| 366 |
+
across = (y_values - center_y) / width
|
| 367 |
+
|
| 368 |
+
offsets = gs.torch.zeros_like(rest_positions)
|
| 369 |
+
offsets[:, 0] = (
|
| 370 |
+
float(amplitude) * 2.25 * gs.torch.sin(traveling_phase) * wind_strength
|
| 371 |
+
)
|
| 372 |
+
offsets[:, 1] = (
|
| 373 |
+
float(amplitude)
|
| 374 |
+
* 0.04
|
| 375 |
+
* gs.torch.sin(traveling_phase * 1.10 + across)
|
| 376 |
+
* wind_strength
|
| 377 |
+
)
|
| 378 |
+
offsets[:, 2] = (
|
| 379 |
+
float(amplitude)
|
| 380 |
+
* 0.12
|
| 381 |
+
* gs.torch.sin(traveling_phase * 1.15 + across)
|
| 382 |
+
* wind_strength
|
| 383 |
+
)
|
| 384 |
+
|
| 385 |
+
target_positions = rest_positions + offsets
|
| 386 |
+
blend = (0.58 * wind_strength).unsqueeze(1)
|
| 387 |
+
positions = positions + (target_positions - positions) * blend
|
| 388 |
+
positions[anchor_mask] = rest_positions[anchor_mask]
|
| 389 |
+
cloth.set_particles_pos(positions)
|
| 390 |
+
|
| 391 |
+
|
| 392 |
+
def _add_scene_floor_and_backdrop(
|
| 393 |
+
gs: Any,
|
| 394 |
+
scene: Any,
|
| 395 |
+
camera_pos: tuple[float, float, float],
|
| 396 |
+
camera_lookat: tuple[float, float, float],
|
| 397 |
+
floor_size: tuple[float, float],
|
| 398 |
+
floor_tile_size: tuple[float, float],
|
| 399 |
+
floor_color: tuple[float, float, float, float],
|
| 400 |
+
backdrop: bool,
|
| 401 |
+
backdrop_color: tuple[float, float, float, float],
|
| 402 |
+
) -> None:
|
| 403 |
+
scene.add_entity(
|
| 404 |
+
morph=gs.morphs.Plane(
|
| 405 |
+
pos=(0.0, 0.0, 0.0),
|
| 406 |
+
plane_size=tuple(float(value) for value in floor_size),
|
| 407 |
+
tile_size=tuple(float(value) for value in floor_tile_size),
|
| 408 |
+
),
|
| 409 |
+
material=gs.materials.Rigid(),
|
| 410 |
+
surface=gs.surfaces.Rough(
|
| 411 |
+
color=tuple(float(value) for value in floor_color)
|
| 412 |
+
),
|
| 413 |
+
name="floor",
|
| 414 |
+
)
|
| 415 |
+
|
| 416 |
+
if not backdrop:
|
| 417 |
+
return
|
| 418 |
+
|
| 419 |
+
normal = _horizontal_unit_vector(
|
| 420 |
+
(
|
| 421 |
+
camera_pos[0] - camera_lookat[0],
|
| 422 |
+
camera_pos[1] - camera_lookat[1],
|
| 423 |
+
0.0,
|
| 424 |
+
)
|
| 425 |
+
)
|
| 426 |
+
camera_distance = math.sqrt(
|
| 427 |
+
(camera_pos[0] - camera_lookat[0]) ** 2
|
| 428 |
+
+ (camera_pos[1] - camera_lookat[1]) ** 2
|
| 429 |
+
+ (camera_pos[2] - camera_lookat[2]) ** 2
|
| 430 |
+
)
|
| 431 |
+
backdrop_distance = max(1.5, camera_distance * 0.72)
|
| 432 |
+
backdrop_pos = (
|
| 433 |
+
camera_lookat[0] - normal[0] * backdrop_distance,
|
| 434 |
+
camera_lookat[1] - normal[1] * backdrop_distance,
|
| 435 |
+
camera_lookat[2],
|
| 436 |
+
)
|
| 437 |
+
backdrop_size = (
|
| 438 |
+
max(float(floor_size[0]), float(floor_size[1])),
|
| 439 |
+
max(float(floor_size[0]), float(floor_size[1])) * 0.7,
|
| 440 |
+
)
|
| 441 |
+
scene.add_entity(
|
| 442 |
+
morph=gs.morphs.Plane(
|
| 443 |
+
pos=backdrop_pos,
|
| 444 |
+
normal=normal,
|
| 445 |
+
plane_size=backdrop_size,
|
| 446 |
+
collision=False,
|
| 447 |
+
),
|
| 448 |
+
material=gs.materials.Rigid(),
|
| 449 |
+
surface=gs.surfaces.Rough(
|
| 450 |
+
color=tuple(float(value) for value in backdrop_color)
|
| 451 |
+
),
|
| 452 |
+
name="backdrop",
|
| 453 |
+
)
|
| 454 |
+
|
| 455 |
+
|
| 456 |
+
def _horizontal_unit_vector(
|
| 457 |
+
vector: tuple[float, float, float],
|
| 458 |
+
) -> tuple[float, float, float]:
|
| 459 |
+
length = math.sqrt(vector[0] ** 2 + vector[1] ** 2)
|
| 460 |
+
if length <= 1e-6:
|
| 461 |
+
return (1.0, 0.0, 0.0)
|
| 462 |
+
return (vector[0] / length, vector[1] / length, 0.0)
|
| 463 |
+
|
| 464 |
+
|
| 465 |
+
def _save_camera_recording(camera: Any, video_path: Path, fps: int) -> None:
|
| 466 |
+
from moviepy.video.io.ImageSequenceClip import ImageSequenceClip
|
| 467 |
+
|
| 468 |
+
if not camera._in_recording:
|
| 469 |
+
raise RuntimeError("Camera recording was not started.")
|
| 470 |
+
if not camera._recorded_imgs:
|
| 471 |
+
raise RuntimeError("No frames were recorded.")
|
| 472 |
+
|
| 473 |
+
video_path.parent.mkdir(parents=True, exist_ok=True)
|
| 474 |
+
clip = ImageSequenceClip(list(camera._recorded_imgs), fps=fps)
|
| 475 |
+
try:
|
| 476 |
+
clip.write_videofile(
|
| 477 |
+
str(video_path),
|
| 478 |
+
fps=fps,
|
| 479 |
+
logger=None,
|
| 480 |
+
codec="libx264",
|
| 481 |
+
preset="ultrafast",
|
| 482 |
+
)
|
| 483 |
+
finally:
|
| 484 |
+
close = getattr(clip, "close", None)
|
| 485 |
+
if close is not None:
|
| 486 |
+
close()
|
| 487 |
+
camera._recorded_t_prev = -1
|
| 488 |
+
camera._recorded_imgs.clear()
|
| 489 |
+
camera._in_recording = False
|
| 490 |
+
|
| 491 |
+
|
| 492 |
+
def _save_particle_positions(
|
| 493 |
+
particle_positions_path: Path,
|
| 494 |
+
particle_frames: list[Any],
|
| 495 |
+
rest_positions: Any,
|
| 496 |
+
fps: int,
|
| 497 |
+
render_interval: int,
|
| 498 |
+
) -> None:
|
| 499 |
+
import numpy as np
|
| 500 |
+
|
| 501 |
+
if not particle_frames:
|
| 502 |
+
raise RuntimeError("No particle frames were recorded.")
|
| 503 |
+
|
| 504 |
+
particle_positions_path.parent.mkdir(parents=True, exist_ok=True)
|
| 505 |
+
np.savez_compressed(
|
| 506 |
+
particle_positions_path,
|
| 507 |
+
positions=np.stack(particle_frames, axis=0).astype(np.float32),
|
| 508 |
+
rest_positions=_tensor_to_numpy(rest_positions).astype(np.float32),
|
| 509 |
+
fps=np.array(fps, dtype=np.int32),
|
| 510 |
+
render_interval=np.array(render_interval, dtype=np.int32),
|
| 511 |
+
)
|
| 512 |
+
|
| 513 |
+
|
| 514 |
+
def _tensor_to_numpy(tensor: Any) -> Any:
|
| 515 |
+
array = tensor.detach() if hasattr(tensor, "detach") else tensor
|
| 516 |
+
array = array.cpu() if hasattr(array, "cpu") else array
|
| 517 |
+
array = array.numpy() if hasattr(array, "numpy") else array
|
| 518 |
+
return array
|
| 519 |
+
|
| 520 |
+
|
| 521 |
+
def _collar_handle_weights(gs: Any, rest_positions: Any) -> Any:
|
| 522 |
+
min_values = rest_positions.min(dim=0).values
|
| 523 |
+
max_values = rest_positions.max(dim=0).values
|
| 524 |
+
center = rest_positions.mean(dim=0)
|
| 525 |
+
width = gs.torch.clamp(max_values[1] - min_values[1], min=1e-6)
|
| 526 |
+
height = gs.torch.clamp(max_values[2] - min_values[2], min=1e-6)
|
| 527 |
+
|
| 528 |
+
y_values = rest_positions[:, 1]
|
| 529 |
+
z_values = rest_positions[:, 2]
|
| 530 |
+
top_start = max_values[2] - 0.32 * height
|
| 531 |
+
top_weights = gs.torch.clamp(
|
| 532 |
+
(z_values - top_start) / (0.32 * height), 0.0, 1.0
|
| 533 |
+
)
|
| 534 |
+
|
| 535 |
+
left_center = center[1] - 0.22 * width
|
| 536 |
+
right_center = center[1] + 0.22 * width
|
| 537 |
+
sigma = 0.12 * width
|
| 538 |
+
left_weights = gs.torch.exp(-0.5 * ((y_values - left_center) / sigma) ** 2)
|
| 539 |
+
right_weights = gs.torch.exp(
|
| 540 |
+
-0.5 * ((y_values - right_center) / sigma) ** 2
|
| 541 |
+
)
|
| 542 |
+
side_weights = gs.torch.clamp(left_weights + right_weights, 0.0, 1.0)
|
| 543 |
+
return top_weights * side_weights
|
| 544 |
+
|
| 545 |
+
|
| 546 |
+
def _front_camera_pose(
|
| 547 |
+
manifest: dict[str, Any], init_height: float
|
| 548 |
+
) -> tuple[tuple[float, float, float], tuple[float, float, float]]:
|
| 549 |
+
stats = manifest.get("mesh_stats", {})
|
| 550 |
+
bbox_min = stats.get("bbox_min", [-0.5, -0.5, -0.5])
|
| 551 |
+
bbox_max = stats.get("bbox_max", [0.5, 0.5, 0.5])
|
| 552 |
+
extents = stats.get("extents", [1.0, 1.0, 1.0])
|
| 553 |
+
|
| 554 |
+
center = [
|
| 555 |
+
0.5 * (float(bbox_min[index]) + float(bbox_max[index]))
|
| 556 |
+
for index in range(3)
|
| 557 |
+
]
|
| 558 |
+
width = float(extents[1])
|
| 559 |
+
height = float(extents[2])
|
| 560 |
+
depth = max(float(extents[0]), 0.05)
|
| 561 |
+
distance = max(width, height) * 2.45 + depth
|
| 562 |
+
|
| 563 |
+
lookat = (center[0], center[1], center[2] + float(init_height))
|
| 564 |
+
pos = (
|
| 565 |
+
center[0] + distance,
|
| 566 |
+
center[1] - 0.22 * distance,
|
| 567 |
+
lookat[2] + 0.04 * height,
|
| 568 |
+
)
|
| 569 |
+
return pos, lookat
|
| 570 |
+
|
| 571 |
+
|
| 572 |
+
def _init_genesis_backend(gs: Any, device: str) -> tuple[str, str | None]:
|
| 573 |
+
normalized = device.lower()
|
| 574 |
+
if normalized == "cpu":
|
| 575 |
+
gs.init(backend=gs.cpu, logging_level="warning")
|
| 576 |
+
return "cpu", None
|
| 577 |
+
if normalized == "cuda":
|
| 578 |
+
gs.init(backend=gs.cuda, logging_level="warning")
|
| 579 |
+
return "cuda", None
|
| 580 |
+
if normalized != "auto":
|
| 581 |
+
raise ValueError(
|
| 582 |
+
f"Unsupported Genesis backend: {device}. Use 'cpu', 'cuda', or 'auto'."
|
| 583 |
+
)
|
| 584 |
+
|
| 585 |
+
try:
|
| 586 |
+
gs.init(backend=gs.cuda, logging_level="warning")
|
| 587 |
+
return "cuda", None
|
| 588 |
+
except Exception as exc:
|
| 589 |
+
warning = (
|
| 590 |
+
"Genesis CUDA backend is unavailable; falling back to CPU. "
|
| 591 |
+
f"Original error: {type(exc).__name__}: {exc}"
|
| 592 |
+
)
|
| 593 |
+
gs.init(backend=gs.cpu, logging_level="warning")
|
| 594 |
+
return "cpu", warning
|
| 595 |
+
|
| 596 |
+
|
| 597 |
+
def _read_json(path: Path) -> dict[str, Any]:
|
| 598 |
+
if not path.exists():
|
| 599 |
+
raise FileNotFoundError(f"Required JSON file does not exist: {path}")
|
| 600 |
+
return json.loads(path.read_text())
|
| 601 |
+
|
| 602 |
+
|
| 603 |
+
def _write_json(path: Path, data: dict[str, Any]) -> None:
|
| 604 |
+
path.write_text(json.dumps(data, indent=2) + "\n")
|
| 605 |
+
|
| 606 |
+
|
| 607 |
+
if __name__ == "__main__":
|
| 608 |
+
entrypoint()
|
embodied_gen/scripts/gen_texture.py
CHANGED
|
@@ -100,7 +100,7 @@ def entrypoint() -> None:
|
|
| 100 |
pipeline=PIPELINE,
|
| 101 |
seed=cfg.seed,
|
| 102 |
)
|
| 103 |
-
|
| 104 |
delight_model=DELIGHT,
|
| 105 |
imagesr_model=IMAGESR_MODEL,
|
| 106 |
mesh_path=mesh_path,
|
|
|
|
| 100 |
pipeline=PIPELINE,
|
| 101 |
seed=cfg.seed,
|
| 102 |
)
|
| 103 |
+
backproject_api(
|
| 104 |
delight_model=DELIGHT,
|
| 105 |
imagesr_model=IMAGESR_MODEL,
|
| 106 |
mesh_path=mesh_path,
|
embodied_gen/scripts/imageto3d.py
CHANGED
|
@@ -29,6 +29,11 @@ from embodied_gen.data.utils import delete_dir
|
|
| 29 |
# from embodied_gen.models.sr_model import ImageRealESRGAN
|
| 30 |
# from embodied_gen.models.delight_model import DelightingModel
|
| 31 |
from embodied_gen.models.gs_model import GaussianOperator
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
from embodied_gen.models.segment_model import RembgRemover
|
| 33 |
from embodied_gen.scripts.render_gs import entrypoint as render_gs_api
|
| 34 |
from embodied_gen.utils.gpt_clients import GPT_CLIENT
|
|
@@ -49,19 +54,47 @@ from embodied_gen.validators.quality_checkers import (
|
|
| 49 |
from embodied_gen.validators.urdf_convertor import URDFGenerator
|
| 50 |
|
| 51 |
# random.seed(0)
|
| 52 |
-
IMAGE3D_MODEL = "SAM3D" # TRELLIS or
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
|
| 64 |
-
PIPELINE = Sam3dInference()
|
| 65 |
|
| 66 |
# DELIGHT = DelightingModel()
|
| 67 |
# IMAGESR_MODEL = ImageRealESRGAN(outscale=4)
|
|
@@ -109,6 +142,17 @@ def parse_args():
|
|
| 109 |
)
|
| 110 |
parser.add_argument("--disable_decompose_convex", action="store_true")
|
| 111 |
parser.add_argument("--texture_size", type=int, default=2048)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
args, unknown = parser.parse_known_args()
|
| 113 |
|
| 114 |
return args
|
|
@@ -120,9 +164,31 @@ def entrypoint(**kwargs):
|
|
| 120 |
if hasattr(args, k) and v is not None:
|
| 121 |
setattr(args, k, v)
|
| 122 |
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
if not args.image_path:
|
| 127 |
args.image_path = glob(os.path.join(args.image_root, "*.png"))
|
| 128 |
args.image_path += glob(os.path.join(args.image_root, "*.jpg"))
|
|
@@ -151,6 +217,19 @@ def entrypoint(**kwargs):
|
|
| 151 |
seg_image = RBG_REMOVER(image) if image.mode != "RGBA" else image
|
| 152 |
seg_image.save(seg_path)
|
| 153 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
seed = args.seed
|
| 155 |
asset_node = "unknown"
|
| 156 |
gs_model = None
|
|
@@ -161,10 +240,10 @@ def entrypoint(**kwargs):
|
|
| 161 |
f"Try: {try_idx + 1}/{args.n_retry}, Seed: {seed}, Prompt: {seg_path}"
|
| 162 |
)
|
| 163 |
try:
|
| 164 |
-
outputs = image3d_model_infer(
|
| 165 |
except Exception as e:
|
| 166 |
logger.error(
|
| 167 |
-
f"[Image3D Failed] process {image_path}: {e}, retry: {try_idx+1}/{args.n_retry}"
|
| 168 |
)
|
| 169 |
seed = (
|
| 170 |
random.randint(0, 100000) if seed is not None else None
|
|
@@ -290,10 +369,14 @@ def entrypoint(**kwargs):
|
|
| 290 |
)
|
| 291 |
|
| 292 |
# Quality check and update .urdf file.
|
| 293 |
-
mesh_out =
|
|
|
|
|
|
|
| 294 |
trimesh.load(mesh_out).export(mesh_out.replace(".obj", ".glb"))
|
| 295 |
|
| 296 |
-
image_dir =
|
|
|
|
|
|
|
| 297 |
image_paths = glob(f"{image_dir}/*.png")
|
| 298 |
images_list = []
|
| 299 |
for checker in CHECKERS:
|
|
|
|
| 29 |
# from embodied_gen.models.sr_model import ImageRealESRGAN
|
| 30 |
# from embodied_gen.models.delight_model import DelightingModel
|
| 31 |
from embodied_gen.models.gs_model import GaussianOperator
|
| 32 |
+
from embodied_gen.models.hunyuan3d import (
|
| 33 |
+
HunyuanConfig,
|
| 34 |
+
load_credentials,
|
| 35 |
+
process_image,
|
| 36 |
+
)
|
| 37 |
from embodied_gen.models.segment_model import RembgRemover
|
| 38 |
from embodied_gen.scripts.render_gs import entrypoint as render_gs_api
|
| 39 |
from embodied_gen.utils.gpt_clients import GPT_CLIENT
|
|
|
|
| 54 |
from embodied_gen.validators.urdf_convertor import URDFGenerator
|
| 55 |
|
| 56 |
# random.seed(0)
|
| 57 |
+
IMAGE3D_MODEL = "SAM3D" # default backend; SAM3D, TRELLIS, or HUNYUAN3D
|
| 58 |
+
SUPPORTED_IMAGE3D_MODELS = ("SAM3D", "TRELLIS", "HUNYUAN3D")
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
_PIPELINE_CACHE: dict = {}
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def _build_image3d_pipeline(name: str):
|
| 65 |
+
"""Lazily instantiate (and cache) the local image-to-3D pipeline.
|
| 66 |
+
|
| 67 |
+
The cache preserves the pre-refactor invariant that the local backend
|
| 68 |
+
is loaded once per process: ``textto3d.py`` calls ``entrypoint`` in a
|
| 69 |
+
per-node loop, and re-loading weights each call would regress runtime.
|
| 70 |
+
Returns ``None`` for backends that have no local model (HUNYUAN3D).
|
| 71 |
+
"""
|
| 72 |
+
if name == "HUNYUAN3D":
|
| 73 |
+
return None
|
| 74 |
+
if name in _PIPELINE_CACHE:
|
| 75 |
+
return _PIPELINE_CACHE[name]
|
| 76 |
+
if name == "TRELLIS":
|
| 77 |
+
logger.info("Loading TRELLIS as Image3D Models...")
|
| 78 |
+
from thirdparty.TRELLIS.trellis.pipelines import (
|
| 79 |
+
TrellisImageTo3DPipeline,
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
pipeline = TrellisImageTo3DPipeline.from_pretrained(
|
| 83 |
+
"microsoft/TRELLIS-image-large"
|
| 84 |
+
)
|
| 85 |
+
elif name == "SAM3D":
|
| 86 |
+
logger.info("Loading SAM3D as Image3D Models...")
|
| 87 |
+
from embodied_gen.models.sam3d import Sam3dInference
|
| 88 |
+
|
| 89 |
+
pipeline = Sam3dInference()
|
| 90 |
+
else:
|
| 91 |
+
raise ValueError(
|
| 92 |
+
f"Unsupported image3d backend {name!r}; "
|
| 93 |
+
f"expected one of {SUPPORTED_IMAGE3D_MODELS}."
|
| 94 |
+
)
|
| 95 |
+
_PIPELINE_CACHE[name] = pipeline
|
| 96 |
+
return pipeline
|
| 97 |
|
|
|
|
| 98 |
|
| 99 |
# DELIGHT = DelightingModel()
|
| 100 |
# IMAGESR_MODEL = ImageRealESRGAN(outscale=4)
|
|
|
|
| 142 |
)
|
| 143 |
parser.add_argument("--disable_decompose_convex", action="store_true")
|
| 144 |
parser.add_argument("--texture_size", type=int, default=2048)
|
| 145 |
+
parser.add_argument(
|
| 146 |
+
"--image3d_model",
|
| 147 |
+
type=str,
|
| 148 |
+
default=IMAGE3D_MODEL,
|
| 149 |
+
help=(
|
| 150 |
+
"Image-to-3D backend. One of "
|
| 151 |
+
f"{', '.join(SUPPORTED_IMAGE3D_MODELS)} (case-insensitive). "
|
| 152 |
+
"HUNYUAN3D calls Tencent Hunyuan3D Pro API and requires "
|
| 153 |
+
"TENCENT_SECRET_ID/TENCENT_SECRET_KEY in the environment."
|
| 154 |
+
),
|
| 155 |
+
)
|
| 156 |
args, unknown = parser.parse_known_args()
|
| 157 |
|
| 158 |
return args
|
|
|
|
| 164 |
if hasattr(args, k) and v is not None:
|
| 165 |
setattr(args, k, v)
|
| 166 |
|
| 167 |
+
args.image3d_model = str(args.image3d_model).strip().upper()
|
| 168 |
+
if args.image3d_model not in SUPPORTED_IMAGE3D_MODELS:
|
| 169 |
+
raise ValueError(
|
| 170 |
+
f"Unsupported --image3d_model {args.image3d_model!r}; "
|
| 171 |
+
f"expected one of {SUPPORTED_IMAGE3D_MODELS}."
|
| 172 |
+
)
|
| 173 |
+
|
| 174 |
+
hunyuan_config = None
|
| 175 |
+
hunyuan_credentials = None
|
| 176 |
+
if args.image3d_model == "HUNYUAN3D":
|
| 177 |
+
# Fail fast on missing creds before any local model load or network I/O.
|
| 178 |
+
hunyuan_credentials = load_credentials()
|
| 179 |
+
hunyuan_config = HunyuanConfig()
|
| 180 |
+
logger.info(
|
| 181 |
+
"HUNYUAN3D backend: action=%s host=%s result_format=%s",
|
| 182 |
+
hunyuan_config.image_action,
|
| 183 |
+
hunyuan_config.host,
|
| 184 |
+
hunyuan_config.result_format,
|
| 185 |
+
)
|
| 186 |
+
|
| 187 |
+
pipeline = _build_image3d_pipeline(args.image3d_model)
|
| 188 |
+
|
| 189 |
+
assert args.image_path or args.image_root, (
|
| 190 |
+
"Please provide either --image_path or --image_root."
|
| 191 |
+
)
|
| 192 |
if not args.image_path:
|
| 193 |
args.image_path = glob(os.path.join(args.image_root, "*.png"))
|
| 194 |
args.image_path += glob(os.path.join(args.image_root, "*.jpg"))
|
|
|
|
| 217 |
seg_image = RBG_REMOVER(image) if image.mode != "RGBA" else image
|
| 218 |
seg_image.save(seg_path)
|
| 219 |
|
| 220 |
+
if args.image3d_model == "HUNYUAN3D":
|
| 221 |
+
process_image(
|
| 222 |
+
args=args,
|
| 223 |
+
idx=idx,
|
| 224 |
+
image_path=image_path,
|
| 225 |
+
output_root=output_root,
|
| 226 |
+
filename=filename,
|
| 227 |
+
hunyuan_config=hunyuan_config,
|
| 228 |
+
hunyuan_credentials=hunyuan_credentials,
|
| 229 |
+
checkers=CHECKERS,
|
| 230 |
+
)
|
| 231 |
+
continue
|
| 232 |
+
|
| 233 |
seed = args.seed
|
| 234 |
asset_node = "unknown"
|
| 235 |
gs_model = None
|
|
|
|
| 240 |
f"Try: {try_idx + 1}/{args.n_retry}, Seed: {seed}, Prompt: {seg_path}"
|
| 241 |
)
|
| 242 |
try:
|
| 243 |
+
outputs = image3d_model_infer(pipeline, seg_image, seed)
|
| 244 |
except Exception as e:
|
| 245 |
logger.error(
|
| 246 |
+
f"[Image3D Failed] process {image_path}: {e}, retry: {try_idx + 1}/{args.n_retry}"
|
| 247 |
)
|
| 248 |
seed = (
|
| 249 |
random.randint(0, 100000) if seed is not None else None
|
|
|
|
| 369 |
)
|
| 370 |
|
| 371 |
# Quality check and update .urdf file.
|
| 372 |
+
mesh_out = (
|
| 373 |
+
f"{urdf_root}/{urdf_convertor.output_mesh_dir}/{filename}.obj" # noqa
|
| 374 |
+
)
|
| 375 |
trimesh.load(mesh_out).export(mesh_out.replace(".obj", ".glb"))
|
| 376 |
|
| 377 |
+
image_dir = (
|
| 378 |
+
f"{urdf_root}/{urdf_convertor.output_render_dir}/image_color" # noqa
|
| 379 |
+
)
|
| 380 |
image_paths = glob(f"{image_dir}/*.png")
|
| 381 |
images_list = []
|
| 382 |
for checker in CHECKERS:
|
embodied_gen/scripts/parallel_sim.py
CHANGED
|
@@ -29,7 +29,7 @@ import torch
|
|
| 29 |
import tyro
|
| 30 |
from mani_skill.utils.wrappers import RecordEpisode
|
| 31 |
from tqdm import tqdm
|
| 32 |
-
import embodied_gen.envs.pick_embodiedgen
|
| 33 |
from embodied_gen.utils.enum import LayoutInfo, Scene3DItemEnum
|
| 34 |
from embodied_gen.utils.log import logger
|
| 35 |
from embodied_gen.utils.simulation import FrankaPandaGrasper
|
|
|
|
| 29 |
import tyro
|
| 30 |
from mani_skill.utils.wrappers import RecordEpisode
|
| 31 |
from tqdm import tqdm
|
| 32 |
+
import embodied_gen.envs.pick_embodiedgen # noqa: F401
|
| 33 |
from embodied_gen.utils.enum import LayoutInfo, Scene3DItemEnum
|
| 34 |
from embodied_gen.utils.log import logger
|
| 35 |
from embodied_gen.utils.simulation import FrankaPandaGrasper
|
embodied_gen/scripts/room_gen/custom_solve.gin
CHANGED
|
@@ -27,3 +27,4 @@ compose_indoors.skirting_floor_enabled = False
|
|
| 27 |
compose_indoors.skirting_ceiling_enabled = False
|
| 28 |
BlueprintSolidifier.enable_open = False
|
| 29 |
|
|
|
|
|
|
| 27 |
compose_indoors.skirting_ceiling_enabled = False
|
| 28 |
BlueprintSolidifier.enable_open = False
|
| 29 |
|
| 30 |
+
compose_indoors.room_doors_enabled = False
|
embodied_gen/scripts/room_gen/export_scene.py
CHANGED
|
@@ -20,7 +20,6 @@ import logging
|
|
| 20 |
import math
|
| 21 |
import os
|
| 22 |
import shutil
|
| 23 |
-
import subprocess
|
| 24 |
from collections import defaultdict
|
| 25 |
from pathlib import Path
|
| 26 |
from typing import Dict, List, Optional, Tuple
|
|
@@ -547,12 +546,12 @@ def apply_baked_tex(obj, paramDict={}):
|
|
| 547 |
principled_bsdf_node.inputs["Metallic"].default_value = paramDict[
|
| 548 |
mat.name
|
| 549 |
]["Metallic"]
|
| 550 |
-
principled_bsdf_node.inputs[
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
principled_bsdf_node.inputs[
|
| 554 |
-
|
| 555 |
-
|
| 556 |
|
| 557 |
|
| 558 |
def create_glass_shader(node_tree, export_usd):
|
|
@@ -783,9 +782,9 @@ def bake_special_emit(
|
|
| 783 |
logger.debug(
|
| 784 |
f"Removing added link:\t{n.name}: {from_soc.name} => {to_soc.name}"
|
| 785 |
)
|
| 786 |
-
for
|
| 787 |
-
if
|
| 788 |
-
n.node_tree.links.remove(
|
| 789 |
logger.debug(
|
| 790 |
f"Removed link:\t{n.name}: {from_soc.name} => {to_soc.name}"
|
| 791 |
)
|
|
@@ -1182,9 +1181,9 @@ def export_sim_ready(
|
|
| 1182 |
) -> Dict[str, List[Path]]:
|
| 1183 |
"""Exports both the visual and collision assets for a geometry."""
|
| 1184 |
if not visual_only:
|
| 1185 |
-
assert (
|
| 1186 |
-
coacd is
|
| 1187 |
-
)
|
| 1188 |
|
| 1189 |
asset_exports = defaultdict(list)
|
| 1190 |
export_name = name if name is not None else obj.name
|
|
@@ -1626,7 +1625,7 @@ def main(args):
|
|
| 1626 |
|
| 1627 |
bpy.ops.wm.open_mainfile(filepath=str(blendfile))
|
| 1628 |
|
| 1629 |
-
|
| 1630 |
blendfile,
|
| 1631 |
args.output_folder,
|
| 1632 |
format=args.format,
|
|
|
|
| 20 |
import math
|
| 21 |
import os
|
| 22 |
import shutil
|
|
|
|
| 23 |
from collections import defaultdict
|
| 24 |
from pathlib import Path
|
| 25 |
from typing import Dict, List, Optional, Tuple
|
|
|
|
| 546 |
principled_bsdf_node.inputs["Metallic"].default_value = paramDict[
|
| 547 |
mat.name
|
| 548 |
]["Metallic"]
|
| 549 |
+
principled_bsdf_node.inputs[
|
| 550 |
+
"Sheen Weight"
|
| 551 |
+
].default_value = paramDict[mat.name]["Sheen Weight"]
|
| 552 |
+
principled_bsdf_node.inputs[
|
| 553 |
+
"Coat Weight"
|
| 554 |
+
].default_value = paramDict[mat.name]["Coat Weight"]
|
| 555 |
|
| 556 |
|
| 557 |
def create_glass_shader(node_tree, export_usd):
|
|
|
|
| 782 |
logger.debug(
|
| 783 |
f"Removing added link:\t{n.name}: {from_soc.name} => {to_soc.name}"
|
| 784 |
)
|
| 785 |
+
for link in n.node_tree.links:
|
| 786 |
+
if link.from_socket == from_soc and link.to_socket == to_soc:
|
| 787 |
+
n.node_tree.links.remove(link)
|
| 788 |
logger.debug(
|
| 789 |
f"Removed link:\t{n.name}: {from_soc.name} => {to_soc.name}"
|
| 790 |
)
|
|
|
|
| 1181 |
) -> Dict[str, List[Path]]:
|
| 1182 |
"""Exports both the visual and collision assets for a geometry."""
|
| 1183 |
if not visual_only:
|
| 1184 |
+
assert coacd is not None, (
|
| 1185 |
+
"coacd is required to export simulation assets."
|
| 1186 |
+
)
|
| 1187 |
|
| 1188 |
asset_exports = defaultdict(list)
|
| 1189 |
export_name = name if name is not None else obj.name
|
|
|
|
| 1625 |
|
| 1626 |
bpy.ops.wm.open_mainfile(filepath=str(blendfile))
|
| 1627 |
|
| 1628 |
+
export_scene(
|
| 1629 |
blendfile,
|
| 1630 |
args.output_folder,
|
| 1631 |
format=args.format,
|
embodied_gen/scripts/room_gen/gen_room.py
CHANGED
|
@@ -81,6 +81,11 @@ class GenRoomArgs:
|
|
| 81 |
complexity: Complexity = Complexity.simple
|
| 82 |
"""Complexity level: minimalist, simple, medium, or detail."""
|
| 83 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
|
| 85 |
def run_command(cmd: list[str], task_name: str):
|
| 86 |
"""Helper: Use Popen to allow killing the child process on Ctrl+C.
|
|
@@ -114,6 +119,22 @@ def run_command(cmd: list[str], task_name: str):
|
|
| 114 |
|
| 115 |
|
| 116 |
def generate_room(cfg: GenRoomArgs):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
room_type = cfg.room_type.value
|
| 118 |
seed = cfg.seed
|
| 119 |
if seed is None:
|
|
|
|
| 81 |
complexity: Complexity = Complexity.simple
|
| 82 |
"""Complexity level: minimalist, simple, medium, or detail."""
|
| 83 |
|
| 84 |
+
prompt: str | None = None
|
| 85 |
+
"""Natural-language task/scene description. When set, room_type and
|
| 86 |
+
complexity are inferred from it via a GPT router, overriding the
|
| 87 |
+
explicit values."""
|
| 88 |
+
|
| 89 |
|
| 90 |
def run_command(cmd: list[str], task_name: str):
|
| 91 |
"""Helper: Use Popen to allow killing the child process on Ctrl+C.
|
|
|
|
| 119 |
|
| 120 |
|
| 121 |
def generate_room(cfg: GenRoomArgs):
|
| 122 |
+
if cfg.prompt:
|
| 123 |
+
# Deferred import: GPT routing deps are only needed in prompt mode.
|
| 124 |
+
from embodied_gen.scripts.room_gen.route_room import (
|
| 125 |
+
InfinigenGenRouter,
|
| 126 |
+
)
|
| 127 |
+
from embodied_gen.utils.gpt_clients import GPT_CLIENT
|
| 128 |
+
|
| 129 |
+
router = InfinigenGenRouter(gpt_client=GPT_CLIENT)
|
| 130 |
+
room_name, complexity = router.query(cfg.prompt)
|
| 131 |
+
cfg.room_type = RoomType(room_name)
|
| 132 |
+
cfg.complexity = Complexity(complexity)
|
| 133 |
+
logger.info(
|
| 134 |
+
f"Router mapped prompt {cfg.prompt!r} to room={room_name}, "
|
| 135 |
+
f"complexity={complexity}."
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
room_type = cfg.room_type.value
|
| 139 |
seed = cfg.seed
|
| 140 |
if seed is None:
|
embodied_gen/scripts/room_gen/gen_trajectory.py
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Project EmbodiedGen
|
| 2 |
+
#
|
| 3 |
+
# Copyright (c) 2025 Horizon Robotics. All Rights Reserved.
|
| 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
|
| 14 |
+
# implied. See the License for the specific language governing
|
| 15 |
+
# permissions and limitations under the License.
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
from __future__ import annotations
|
| 19 |
+
|
| 20 |
+
import json
|
| 21 |
+
import logging
|
| 22 |
+
import subprocess
|
| 23 |
+
from dataclasses import dataclass
|
| 24 |
+
from importlib import import_module
|
| 25 |
+
from pathlib import Path
|
| 26 |
+
|
| 27 |
+
import numpy as np
|
| 28 |
+
import tyro
|
| 29 |
+
from shapely.geometry import Polygon
|
| 30 |
+
from shapely.ops import unary_union
|
| 31 |
+
|
| 32 |
+
# The spatial-computing package name contains a hyphen, so it cannot be
|
| 33 |
+
# imported with a normal ``import`` statement; use importlib instead.
|
| 34 |
+
_SC = "embodied_gen.skills.spatial-computing.core"
|
| 35 |
+
UrdfSemanticInfoCollector = import_module(
|
| 36 |
+
f"{_SC}.collector"
|
| 37 |
+
).UrdfSemanticInfoCollector
|
| 38 |
+
RoamTrajectoryGenerator = import_module(
|
| 39 |
+
f"{_SC}.trajectory"
|
| 40 |
+
).RoamTrajectoryGenerator
|
| 41 |
+
FloorplanVisualizer = import_module(f"{_SC}.visualizer").FloorplanVisualizer
|
| 42 |
+
|
| 43 |
+
logger = logging.getLogger(__name__)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
@dataclass
|
| 47 |
+
class GenTrajectoryArgs:
|
| 48 |
+
"""Configuration for roaming-trajectory generation."""
|
| 49 |
+
|
| 50 |
+
urdf_path: str
|
| 51 |
+
"""Path to the input URDF scene file."""
|
| 52 |
+
|
| 53 |
+
output_dir: str = "outputs/trajectory"
|
| 54 |
+
"""Directory for the trajectory JSON and floorplan PNG."""
|
| 55 |
+
|
| 56 |
+
clearance: float = 0.4
|
| 57 |
+
"""Minimum clearance (m) from walls; keep small so doorways stay passable."""
|
| 58 |
+
|
| 59 |
+
obstacle_clearance: float = 0.5
|
| 60 |
+
"""Minimum clearance (m) from furniture/objects (doors/walls excluded)."""
|
| 61 |
+
|
| 62 |
+
resolution: float = 0.05
|
| 63 |
+
"""Occupancy-grid cell size in meters (smaller = finer, slower)."""
|
| 64 |
+
|
| 65 |
+
num_waypoints: int = 8
|
| 66 |
+
"""Number of roam waypoints to visit across the navigable space."""
|
| 67 |
+
|
| 68 |
+
speed: float = 0.5
|
| 69 |
+
"""Constant roaming speed (m/s). Sets the spacing with --time_step."""
|
| 70 |
+
|
| 71 |
+
time_step: float = 0.1
|
| 72 |
+
"""Time between consecutive waypoints (s). Spacing = speed * time_step."""
|
| 73 |
+
|
| 74 |
+
turn_radius: float = 0.8
|
| 75 |
+
"""Target turning-arc radius (m); larger gives wider, rounder turns."""
|
| 76 |
+
|
| 77 |
+
endpoint_clearance: float = 1.5
|
| 78 |
+
"""Minimum distance (m) the start and end keep from walls/obstacles."""
|
| 79 |
+
|
| 80 |
+
arrow_stride: int = 10
|
| 81 |
+
"""Draw a heading arrow every N points in the PNG (0 disables)."""
|
| 82 |
+
|
| 83 |
+
remove_doors: bool = False
|
| 84 |
+
"""Hide door footprints in the floorplan image and animation."""
|
| 85 |
+
|
| 86 |
+
animate: bool = False
|
| 87 |
+
"""Also render a per-frame floorplan animation (frames/ + trajectory.mp4)."""
|
| 88 |
+
|
| 89 |
+
anim_frame_stride: int = 1
|
| 90 |
+
"""Animate every Nth waypoint. Match the render --frame_stride to sync."""
|
| 91 |
+
|
| 92 |
+
anim_fps: float = 0.0
|
| 93 |
+
"""Animation frame rate. 0 = auto (real-time from speed / stride)."""
|
| 94 |
+
|
| 95 |
+
anim_dpi: int = 100
|
| 96 |
+
"""Resolution (dpi) of the animation frames."""
|
| 97 |
+
|
| 98 |
+
start_xy: tuple[float, float] | None = None
|
| 99 |
+
"""Optional world (x, y) start; defaults to the largest free region."""
|
| 100 |
+
|
| 101 |
+
mesh_sample_num: int = 5000
|
| 102 |
+
"""Number of points sampled per mesh when parsing the URDF."""
|
| 103 |
+
|
| 104 |
+
seed: int | None = None
|
| 105 |
+
"""Optional RNG seed for reproducible roaming."""
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def generate_trajectory(cfg: GenTrajectoryArgs) -> None:
|
| 109 |
+
"""Run the roaming-trajectory pipeline and write outputs.
|
| 110 |
+
|
| 111 |
+
Args:
|
| 112 |
+
cfg: Parsed CLI configuration.
|
| 113 |
+
"""
|
| 114 |
+
output_dir = Path(cfg.output_dir)
|
| 115 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 116 |
+
|
| 117 |
+
collector = UrdfSemanticInfoCollector(mesh_sample_num=cfg.mesh_sample_num)
|
| 118 |
+
collector.collect(cfg.urdf_path)
|
| 119 |
+
|
| 120 |
+
generator = RoamTrajectoryGenerator.from_collector(
|
| 121 |
+
collector,
|
| 122 |
+
clearance=cfg.clearance,
|
| 123 |
+
resolution=cfg.resolution,
|
| 124 |
+
obstacle_clearance=cfg.obstacle_clearance,
|
| 125 |
+
)
|
| 126 |
+
result = generator.generate(
|
| 127 |
+
start_xy=cfg.start_xy,
|
| 128 |
+
num_waypoints=cfg.num_waypoints,
|
| 129 |
+
point_spacing=cfg.speed * cfg.time_step,
|
| 130 |
+
turn_radius=cfg.turn_radius,
|
| 131 |
+
endpoint_clearance=cfg.endpoint_clearance,
|
| 132 |
+
seed=cfg.seed,
|
| 133 |
+
)
|
| 134 |
+
|
| 135 |
+
stem = Path(cfg.urdf_path).stem
|
| 136 |
+
json_path = output_dir / f"{stem}_trajectory.json"
|
| 137 |
+
png_path = output_dir / f"{stem}_trajectory.png"
|
| 138 |
+
|
| 139 |
+
# Points are equidistant, so timestamps follow constant-speed motion:
|
| 140 |
+
# t = cumulative arc length / speed.
|
| 141 |
+
xy = result.points[:, :2]
|
| 142 |
+
seg = np.linalg.norm(np.diff(xy, axis=0), axis=1)
|
| 143 |
+
times = np.concatenate([[0.0], np.cumsum(seg)]) / cfg.speed
|
| 144 |
+
waypoints = [
|
| 145 |
+
{
|
| 146 |
+
"x": round(float(x), 4),
|
| 147 |
+
"y": round(float(y), 4),
|
| 148 |
+
"rot": round(float(r), 2),
|
| 149 |
+
"t": round(float(t), 3),
|
| 150 |
+
}
|
| 151 |
+
for (x, y, r), t in zip(result.points, times)
|
| 152 |
+
]
|
| 153 |
+
|
| 154 |
+
payload = {
|
| 155 |
+
"num_waypoints": len(waypoints),
|
| 156 |
+
"clearance": result.clearance,
|
| 157 |
+
"min_clearance": round(result.min_clearance, 4),
|
| 158 |
+
"length": round(result.length, 4),
|
| 159 |
+
"speed": cfg.speed,
|
| 160 |
+
"time_step": cfg.time_step,
|
| 161 |
+
"reachable_rooms": result.reachable_rooms,
|
| 162 |
+
"rot_convention": (
|
| 163 |
+
"0 deg points +Y (12 o'clock); counter-clockwise positive; "
|
| 164 |
+
"tangent to the roaming curve (forward heading); range [0, 360)."
|
| 165 |
+
),
|
| 166 |
+
"waypoints": waypoints,
|
| 167 |
+
}
|
| 168 |
+
json_path.write_text(json.dumps(payload, indent=2, ensure_ascii=False))
|
| 169 |
+
|
| 170 |
+
footprints, occ_area = visible_footprints(collector, cfg.remove_doors)
|
| 171 |
+
|
| 172 |
+
FloorplanVisualizer.plot(
|
| 173 |
+
collector.rooms,
|
| 174 |
+
footprints,
|
| 175 |
+
occ_area,
|
| 176 |
+
str(png_path),
|
| 177 |
+
trajectory=result.points,
|
| 178 |
+
arrow_stride=cfg.arrow_stride,
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
logger.info("Trajectory waypoints : %s", json_path)
|
| 182 |
+
logger.info("Floorplan overlay : %s", png_path)
|
| 183 |
+
logger.info(
|
| 184 |
+
"Length %.2fm, min clearance %.2fm, reaches rooms: %s",
|
| 185 |
+
result.length,
|
| 186 |
+
result.min_clearance,
|
| 187 |
+
", ".join(result.reachable_rooms) or "(none)",
|
| 188 |
+
)
|
| 189 |
+
|
| 190 |
+
if cfg.animate:
|
| 191 |
+
fps = cfg.anim_fps
|
| 192 |
+
if fps <= 0.0:
|
| 193 |
+
fps = (1.0 / cfg.time_step) / max(1, cfg.anim_frame_stride)
|
| 194 |
+
animate_floorplan(
|
| 195 |
+
rooms=collector.rooms,
|
| 196 |
+
footprints=footprints,
|
| 197 |
+
occ_area=occ_area,
|
| 198 |
+
points=result.points,
|
| 199 |
+
output_dir=output_dir,
|
| 200 |
+
frame_stride=cfg.anim_frame_stride,
|
| 201 |
+
fps=fps,
|
| 202 |
+
dpi=cfg.anim_dpi,
|
| 203 |
+
)
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
def visible_footprints(collector, remove_doors: bool):
|
| 207 |
+
"""Return footprints and occupied area for floorplan drawing.
|
| 208 |
+
|
| 209 |
+
When ``remove_doors`` is set, door footprints are excluded and the
|
| 210 |
+
occupied area is recomputed without them so doors do not appear.
|
| 211 |
+
|
| 212 |
+
Args:
|
| 213 |
+
collector: Parsed scene collector.
|
| 214 |
+
remove_doors: Whether to hide door footprints.
|
| 215 |
+
|
| 216 |
+
Returns:
|
| 217 |
+
Tuple of (footprints dict, occupied-area geometry).
|
| 218 |
+
"""
|
| 219 |
+
if not remove_doors:
|
| 220 |
+
return collector.footprints, collector.occ_area
|
| 221 |
+
|
| 222 |
+
footprints = {
|
| 223 |
+
key: poly
|
| 224 |
+
for key, poly in collector.footprints.items()
|
| 225 |
+
if "door" not in key.lower()
|
| 226 |
+
}
|
| 227 |
+
occ_area = (
|
| 228 |
+
unary_union(list(footprints.values())) if footprints else Polygon()
|
| 229 |
+
)
|
| 230 |
+
return footprints, occ_area
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
def animate_floorplan(
|
| 234 |
+
rooms: dict,
|
| 235 |
+
footprints: dict,
|
| 236 |
+
occ_area,
|
| 237 |
+
points: np.ndarray,
|
| 238 |
+
output_dir: Path,
|
| 239 |
+
frame_stride: int,
|
| 240 |
+
fps: float,
|
| 241 |
+
dpi: int,
|
| 242 |
+
) -> None:
|
| 243 |
+
"""Render a floorplan animation that follows the trajectory progress.
|
| 244 |
+
|
| 245 |
+
Each frame shows the traveled path in red, a green dot at the current
|
| 246 |
+
position, and a red heading arrow; the future path is hidden. Frame
|
| 247 |
+
indices match the render frames (same stride) for easy pairing.
|
| 248 |
+
|
| 249 |
+
Args:
|
| 250 |
+
rooms: Room-name to floor-polygon map.
|
| 251 |
+
footprints: Object-name to footprint-polygon map (to draw).
|
| 252 |
+
occ_area: Occupied-area geometry overlay.
|
| 253 |
+
points: (N, 3) array of (x, y, rot_deg) waypoints.
|
| 254 |
+
output_dir: Directory to write ``frames/`` and ``trajectory.mp4``.
|
| 255 |
+
frame_stride: Render every Nth waypoint.
|
| 256 |
+
fps: Output video frame rate.
|
| 257 |
+
dpi: Resolution of each frame.
|
| 258 |
+
"""
|
| 259 |
+
frames_dir = output_dir / "frames"
|
| 260 |
+
frames_dir.mkdir(parents=True, exist_ok=True)
|
| 261 |
+
|
| 262 |
+
for i in range(0, len(points), max(1, frame_stride)):
|
| 263 |
+
FloorplanVisualizer.plot(
|
| 264 |
+
rooms,
|
| 265 |
+
footprints,
|
| 266 |
+
occ_area,
|
| 267 |
+
str(frames_dir / f"frame_{i:04d}.png"),
|
| 268 |
+
trajectory=points,
|
| 269 |
+
current_index=i,
|
| 270 |
+
dpi=dpi,
|
| 271 |
+
)
|
| 272 |
+
|
| 273 |
+
video_path = output_dir / "trajectory.mp4"
|
| 274 |
+
cmd = [
|
| 275 |
+
"ffmpeg",
|
| 276 |
+
"-y",
|
| 277 |
+
"-framerate",
|
| 278 |
+
f"{max(fps, 1.0):g}",
|
| 279 |
+
"-pattern_type",
|
| 280 |
+
"glob",
|
| 281 |
+
"-i",
|
| 282 |
+
str(frames_dir / "frame_*.png"),
|
| 283 |
+
"-vf",
|
| 284 |
+
"pad=ceil(iw/2)*2:ceil(ih/2)*2",
|
| 285 |
+
"-c:v",
|
| 286 |
+
"libx264",
|
| 287 |
+
"-pix_fmt",
|
| 288 |
+
"yuv420p",
|
| 289 |
+
str(video_path),
|
| 290 |
+
]
|
| 291 |
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
| 292 |
+
if result.returncode != 0:
|
| 293 |
+
raise RuntimeError(f"ffmpeg failed for {video_path}:\n{result.stderr}")
|
| 294 |
+
|
| 295 |
+
logger.info("Trajectory animation : %s", video_path)
|
| 296 |
+
|
| 297 |
+
|
| 298 |
+
def main() -> None:
|
| 299 |
+
"""CLI entry point."""
|
| 300 |
+
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
| 301 |
+
cfg = tyro.cli(GenTrajectoryArgs)
|
| 302 |
+
generate_trajectory(cfg)
|
| 303 |
+
|
| 304 |
+
|
| 305 |
+
if __name__ == "__main__":
|
| 306 |
+
main()
|
embodied_gen/scripts/room_gen/render_usd.py
CHANGED
|
@@ -18,11 +18,15 @@
|
|
| 18 |
from __future__ import annotations
|
| 19 |
|
| 20 |
import argparse
|
|
|
|
|
|
|
| 21 |
import logging
|
| 22 |
import math
|
| 23 |
import shutil
|
|
|
|
| 24 |
import tempfile
|
| 25 |
from collections.abc import Callable
|
|
|
|
| 26 |
from pathlib import Path
|
| 27 |
|
| 28 |
import bpy
|
|
@@ -32,6 +36,31 @@ from mathutils import Euler, Matrix, Vector
|
|
| 32 |
|
| 33 |
logger = logging.getLogger(__name__)
|
| 34 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
def build_arg_parser() -> argparse.ArgumentParser:
|
| 37 |
"""Build the CLI parser for USD rendering."""
|
|
@@ -61,6 +90,16 @@ def build_arg_parser() -> argparse.ArgumentParser:
|
|
| 61 |
"--depth_mode",
|
| 62 |
choices=("normalized", "metric"),
|
| 63 |
default="normalized",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
)
|
| 65 |
parser.add_argument(
|
| 66 |
"--resolution",
|
|
@@ -75,14 +114,20 @@ def build_arg_parser() -> argparse.ArgumentParser:
|
|
| 75 |
type=float,
|
| 76 |
nargs=3,
|
| 77 |
metavar=("X", "Y", "Z"),
|
| 78 |
-
|
|
|
|
|
|
|
|
|
|
| 79 |
)
|
| 80 |
parser.add_argument(
|
| 81 |
"--camera_rotation_deg",
|
| 82 |
type=float,
|
| 83 |
nargs=3,
|
| 84 |
metavar=("RX", "RY", "RZ"),
|
| 85 |
-
|
|
|
|
|
|
|
|
|
|
| 86 |
)
|
| 87 |
parser.add_argument(
|
| 88 |
"--flow_camera_xyz",
|
|
@@ -100,6 +145,113 @@ def build_arg_parser() -> argparse.ArgumentParser:
|
|
| 100 |
parser.add_argument("--exposure", type=float, default=2.2)
|
| 101 |
parser.add_argument("--world_strength", type=float, default=8.0)
|
| 102 |
parser.add_argument("--fill_light_energy", type=float, default=14000.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
return parser
|
| 104 |
|
| 105 |
|
|
@@ -120,6 +272,7 @@ class RenderUsd:
|
|
| 120 |
output_dir: Path,
|
| 121 |
render_passes: tuple[str, ...] | list[str],
|
| 122 |
depth_mode: str,
|
|
|
|
| 123 |
resolution: tuple[int, int] | list[int],
|
| 124 |
samples: int,
|
| 125 |
camera_xyz: tuple[float, float, float] | list[float],
|
|
@@ -132,17 +285,21 @@ class RenderUsd:
|
|
| 132 |
exposure: float,
|
| 133 |
world_strength: float,
|
| 134 |
fill_light_energy: float,
|
|
|
|
| 135 |
) -> None:
|
| 136 |
"""Initialize renderer configuration independent of CLI parsing."""
|
| 137 |
self.usd_path = usd_path
|
| 138 |
self.glb_path = self.normalize_optional_path(glb_path)
|
| 139 |
self.glb_xyz = tuple(glb_xyz) if glb_xyz is not None else None
|
| 140 |
self.glb_rotation_deg = (
|
| 141 |
-
|
| 142 |
)
|
|
|
|
|
|
|
| 143 |
self.output_dir = output_dir
|
| 144 |
self.render_passes = tuple(render_passes)
|
| 145 |
self.depth_mode = depth_mode
|
|
|
|
| 146 |
self.resolution = tuple(resolution)
|
| 147 |
self.samples = samples
|
| 148 |
self.camera_xyz = tuple(camera_xyz)
|
|
@@ -159,6 +316,12 @@ class RenderUsd:
|
|
| 159 |
self.exposure = exposure
|
| 160 |
self.world_strength = world_strength
|
| 161 |
self.fill_light_energy = fill_light_energy
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
self.temp_dir: Path | None = None
|
| 163 |
|
| 164 |
@classmethod
|
|
@@ -172,6 +335,7 @@ class RenderUsd:
|
|
| 172 |
output_dir=args.output_dir,
|
| 173 |
render_passes=args.render_passes,
|
| 174 |
depth_mode=args.depth_mode,
|
|
|
|
| 175 |
resolution=args.resolution,
|
| 176 |
samples=args.samples,
|
| 177 |
camera_xyz=args.camera_xyz,
|
|
@@ -182,6 +346,7 @@ class RenderUsd:
|
|
| 182 |
exposure=args.exposure,
|
| 183 |
world_strength=args.world_strength,
|
| 184 |
fill_light_energy=args.fill_light_energy,
|
|
|
|
| 185 |
)
|
| 186 |
|
| 187 |
@property
|
|
@@ -203,7 +368,17 @@ class RenderUsd:
|
|
| 203 |
return Path(normalized)
|
| 204 |
|
| 205 |
def build_output_path(self, filename: str) -> Path:
|
| 206 |
-
"""Build a normalized output path under the render directory.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
return self.output_dir / filename
|
| 208 |
|
| 209 |
def build_temp_path(self, filename: str) -> Path:
|
|
@@ -308,19 +483,43 @@ class RenderUsd:
|
|
| 308 |
raise FileNotFoundError(f"USD file not found: {self.usd_path}")
|
| 309 |
bpy.ops.wm.usd_import(filepath=str(self.usd_path))
|
| 310 |
|
| 311 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 312 |
"""Normalize optional GLB arguments and ensure all-or-none usage."""
|
| 313 |
has_glb_path = self.glb_path is not None
|
| 314 |
has_glb_xyz = self.glb_xyz is not None
|
| 315 |
has_glb_rotation = self.glb_rotation_deg is not None
|
| 316 |
-
if
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 317 |
raise ValueError(
|
| 318 |
"--glb_path, --glb_xyz, and --glb_rotation_deg must be "
|
| 319 |
-
"provided together
|
|
|
|
| 320 |
)
|
| 321 |
-
if not has_glb_path:
|
| 322 |
-
return
|
| 323 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 324 |
if not self.glb_path.exists():
|
| 325 |
raise FileNotFoundError(f"GLB file not found: {self.glb_path}")
|
| 326 |
if self.glb_path.suffix.lower() != ".glb":
|
|
@@ -566,17 +765,43 @@ class RenderUsd:
|
|
| 566 |
self.scene.render.resolution_y = self.resolution[1]
|
| 567 |
self.scene.render.image_settings.file_format = "PNG"
|
| 568 |
self.scene.render.film_transparent = False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 569 |
|
|
|
|
|
|
|
| 570 |
prefs = bpy.context.preferences.addons["cycles"].preferences
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 580 |
|
| 581 |
def configure_color_management(self) -> None:
|
| 582 |
self.scene.view_settings.exposure = self.exposure
|
|
@@ -602,6 +827,7 @@ class RenderUsd:
|
|
| 602 |
"color_depth": self.scene.render.image_settings.color_depth,
|
| 603 |
"use_nodes": self.scene.use_nodes,
|
| 604 |
"samples": self.scene.cycles.samples,
|
|
|
|
| 605 |
}
|
| 606 |
if include_filepath:
|
| 607 |
state["filepath"] = self.scene.render.filepath
|
|
@@ -627,6 +853,7 @@ class RenderUsd:
|
|
| 627 |
self.scene.render.image_settings.color_depth = state["color_depth"]
|
| 628 |
self.scene.use_nodes = state["use_nodes"]
|
| 629 |
self.scene.cycles.samples = state["samples"]
|
|
|
|
| 630 |
if "filepath" in state:
|
| 631 |
self.scene.render.filepath = state["filepath"]
|
| 632 |
if "material_override" in state:
|
|
@@ -652,6 +879,9 @@ class RenderUsd:
|
|
| 652 |
self.scene.view_settings.gamma = 1.0
|
| 653 |
self.scene.use_nodes = use_nodes
|
| 654 |
self.scene.cycles.samples = samples
|
|
|
|
|
|
|
|
|
|
| 655 |
self.scene.render.image_settings.file_format = "PNG"
|
| 656 |
self.scene.render.image_settings.color_mode = color_mode
|
| 657 |
self.scene.render.image_settings.color_depth = color_depth
|
|
@@ -785,6 +1015,7 @@ class RenderUsd:
|
|
| 785 |
depth_mode: str,
|
| 786 |
) -> bpy.types.Node:
|
| 787 |
"""Build the compositor node that converts raw depth to a previewable map."""
|
|
|
|
| 788 |
if depth_mode == "normalized":
|
| 789 |
normalize = tree.nodes.new(type="CompositorNodeNormalize")
|
| 790 |
tree.links.new(render_layers.outputs["Depth"], normalize.inputs[0])
|
|
@@ -794,8 +1025,9 @@ class RenderUsd:
|
|
| 794 |
raise ValueError(f"Unsupported depth mode: {depth_mode}")
|
| 795 |
|
| 796 |
depth_map = tree.nodes.new(type="CompositorNodeMapRange")
|
| 797 |
-
|
| 798 |
-
depth_map.inputs["From
|
|
|
|
| 799 |
depth_map.inputs["To Min"].default_value = 0.0
|
| 800 |
depth_map.inputs["To Max"].default_value = 1.0
|
| 801 |
depth_map.use_clamp = True
|
|
@@ -861,6 +1093,7 @@ class RenderUsd:
|
|
| 861 |
self, temp_path: Path, output_path: Path
|
| 862 |
) -> None:
|
| 863 |
"""Convert the grayscale depth temp image into the final colored preview."""
|
|
|
|
| 864 |
if output_path.exists():
|
| 865 |
output_path.unlink()
|
| 866 |
if not temp_path.exists():
|
|
@@ -1097,8 +1330,8 @@ class RenderUsd:
|
|
| 1097 |
output_path: Path,
|
| 1098 |
instance_seg: np.ndarray,
|
| 1099 |
) -> None:
|
| 1100 |
-
output_path.parent.mkdir(parents=True, exist_ok=True)
|
| 1101 |
vis_output_path = self.get_instance_seg_vis_output_path(output_path)
|
|
|
|
| 1102 |
|
| 1103 |
visualization = self.build_instance_seg_visualization(
|
| 1104 |
instance_seg=instance_seg,
|
|
@@ -1255,10 +1488,15 @@ class RenderUsd:
|
|
| 1255 |
flow: np.ndarray,
|
| 1256 |
valid_mask: np.ndarray,
|
| 1257 |
) -> None:
|
| 1258 |
-
output_path.parent.mkdir(parents=True, exist_ok=True)
|
| 1259 |
flow_output_path = self.get_flow_output_path(output_path)
|
| 1260 |
flow_valid_output_path = self.get_flow_valid_output_path(output_path)
|
| 1261 |
flow_vis_output_path = self.get_flow_vis_output_path(output_path)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1262 |
|
| 1263 |
self.save_numpy_array(flow_output_path, flow)
|
| 1264 |
self.save_numpy_array(flow_valid_output_path, valid_mask)
|
|
@@ -1437,6 +1675,7 @@ class RenderUsd:
|
|
| 1437 |
composite_output_path = self.get_composite_output_path(
|
| 1438 |
tuple(render_pass_name for render_pass_name, _ in composite_images)
|
| 1439 |
)
|
|
|
|
| 1440 |
composite_image = self.build_composite_image(
|
| 1441 |
[image for _, image in composite_images],
|
| 1442 |
[render_pass_name for render_pass_name, _ in composite_images],
|
|
@@ -1560,6 +1799,8 @@ class RenderUsd:
|
|
| 1560 |
self.output_dir.mkdir(parents=True, exist_ok=True)
|
| 1561 |
self.clear_scene()
|
| 1562 |
self.import_usd()
|
|
|
|
|
|
|
| 1563 |
self.validate_glb_args()
|
| 1564 |
imported_glb_objects = self.import_glb_asset()
|
| 1565 |
self.place_glb_asset(imported_glb_objects)
|
|
@@ -1595,10 +1836,764 @@ class RenderUsd:
|
|
| 1595 |
|
| 1596 |
logger.info("Rendered outputs to %s", self.output_dir)
|
| 1597 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1598 |
|
| 1599 |
def main() -> None:
|
| 1600 |
logging.basicConfig(level=logging.INFO)
|
| 1601 |
args = _parse_args()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1602 |
RenderUsd.from_args(args).run()
|
| 1603 |
|
| 1604 |
|
|
|
|
| 18 |
from __future__ import annotations
|
| 19 |
|
| 20 |
import argparse
|
| 21 |
+
import csv
|
| 22 |
+
import json
|
| 23 |
import logging
|
| 24 |
import math
|
| 25 |
import shutil
|
| 26 |
+
import subprocess
|
| 27 |
import tempfile
|
| 28 |
from collections.abc import Callable
|
| 29 |
+
from dataclasses import dataclass
|
| 30 |
from pathlib import Path
|
| 31 |
|
| 32 |
import bpy
|
|
|
|
| 36 |
|
| 37 |
logger = logging.getLogger(__name__)
|
| 38 |
|
| 39 |
+
DOOR_KEYWORDS = ("door",)
|
| 40 |
+
"""Object-name keywords used to identify door meshes for removal."""
|
| 41 |
+
|
| 42 |
+
CAMERA_TRAJECTORY_CSV_FIELDS = (
|
| 43 |
+
"frame",
|
| 44 |
+
"time_sec",
|
| 45 |
+
"x",
|
| 46 |
+
"y",
|
| 47 |
+
"z",
|
| 48 |
+
"roll_x_deg",
|
| 49 |
+
"pitch_y_deg",
|
| 50 |
+
"yaw_z_deg",
|
| 51 |
+
)
|
| 52 |
+
"""Required fields for a camera trajectory CSV."""
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
@dataclass(frozen=True)
|
| 56 |
+
class CameraTrajectoryFrame:
|
| 57 |
+
"""One fully specified camera pose from a trajectory CSV."""
|
| 58 |
+
|
| 59 |
+
frame_index: int
|
| 60 |
+
time_sec: float
|
| 61 |
+
xyz: tuple[float, float, float]
|
| 62 |
+
rotation_deg: tuple[float, float, float]
|
| 63 |
+
|
| 64 |
|
| 65 |
def build_arg_parser() -> argparse.ArgumentParser:
|
| 66 |
"""Build the CLI parser for USD rendering."""
|
|
|
|
| 90 |
"--depth_mode",
|
| 91 |
choices=("normalized", "metric"),
|
| 92 |
default="normalized",
|
| 93 |
+
help=(
|
| 94 |
+
"normalized: per-frame min-max (default); metric: fixed "
|
| 95 |
+
"0..--depth_max mapping, consistent across frames."
|
| 96 |
+
),
|
| 97 |
+
)
|
| 98 |
+
parser.add_argument(
|
| 99 |
+
"--depth_max",
|
| 100 |
+
type=float,
|
| 101 |
+
default=10.0,
|
| 102 |
+
help="Max depth (m) mapped to white in metric depth mode.",
|
| 103 |
)
|
| 104 |
parser.add_argument(
|
| 105 |
"--resolution",
|
|
|
|
| 114 |
type=float,
|
| 115 |
nargs=3,
|
| 116 |
metavar=("X", "Y", "Z"),
|
| 117 |
+
help=(
|
| 118 |
+
"Required for single-frame mode; ignored with --trajectory_json "
|
| 119 |
+
"or --trajectory_csv."
|
| 120 |
+
),
|
| 121 |
)
|
| 122 |
parser.add_argument(
|
| 123 |
"--camera_rotation_deg",
|
| 124 |
type=float,
|
| 125 |
nargs=3,
|
| 126 |
metavar=("RX", "RY", "RZ"),
|
| 127 |
+
help=(
|
| 128 |
+
"Required for single-frame mode; ignored with --trajectory_json "
|
| 129 |
+
"or --trajectory_csv."
|
| 130 |
+
),
|
| 131 |
)
|
| 132 |
parser.add_argument(
|
| 133 |
"--flow_camera_xyz",
|
|
|
|
| 145 |
parser.add_argument("--exposure", type=float, default=2.2)
|
| 146 |
parser.add_argument("--world_strength", type=float, default=8.0)
|
| 147 |
parser.add_argument("--fill_light_energy", type=float, default=14000.0)
|
| 148 |
+
parser.add_argument(
|
| 149 |
+
"--remove_doors",
|
| 150 |
+
action="store_true",
|
| 151 |
+
help="Remove all door meshes from the USD before rendering.",
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
+
# Trajectory modes: roaming JSON follows (x, y, rot) waypoints with a GLB
|
| 155 |
+
# offset, while camera CSV uses a complete 6DoF camera pose per row.
|
| 156 |
+
trajectory_group = parser.add_mutually_exclusive_group()
|
| 157 |
+
trajectory_group.add_argument(
|
| 158 |
+
"--trajectory_json",
|
| 159 |
+
type=Path,
|
| 160 |
+
default=None,
|
| 161 |
+
help=(
|
| 162 |
+
"Trajectory JSON ({waypoints:[{x,y,rot,...}]}). When set, renders "
|
| 163 |
+
"one frame per waypoint instead of a single frame."
|
| 164 |
+
),
|
| 165 |
+
)
|
| 166 |
+
trajectory_group.add_argument(
|
| 167 |
+
"--trajectory_csv",
|
| 168 |
+
type=Path,
|
| 169 |
+
default=None,
|
| 170 |
+
help=(
|
| 171 |
+
"Camera trajectory CSV with fields frame,time_sec,x,y,z,"
|
| 172 |
+
"roll_x_deg,pitch_y_deg,yaw_z_deg. Each row is rendered as one "
|
| 173 |
+
"frame using its full camera pose."
|
| 174 |
+
),
|
| 175 |
+
)
|
| 176 |
+
parser.add_argument(
|
| 177 |
+
"--camera_z",
|
| 178 |
+
type=float,
|
| 179 |
+
default=1.36,
|
| 180 |
+
help="Camera height (m) for trajectory mode.",
|
| 181 |
+
)
|
| 182 |
+
parser.add_argument(
|
| 183 |
+
"--camera_pitch",
|
| 184 |
+
type=float,
|
| 185 |
+
default=80.0,
|
| 186 |
+
help="Camera pitch deg (rotation_deg[0]) for trajectory mode.",
|
| 187 |
+
)
|
| 188 |
+
parser.add_argument(
|
| 189 |
+
"--camera_roll",
|
| 190 |
+
type=float,
|
| 191 |
+
default=0.0,
|
| 192 |
+
help="Camera roll deg (rotation_deg[1]) for trajectory mode.",
|
| 193 |
+
)
|
| 194 |
+
parser.add_argument(
|
| 195 |
+
"--glb_offset_xyz",
|
| 196 |
+
type=float,
|
| 197 |
+
nargs=3,
|
| 198 |
+
default=(0.0012, 0.345, -1.36),
|
| 199 |
+
metavar=("DX", "DY", "DZ"),
|
| 200 |
+
help=(
|
| 201 |
+
"GLB position minus camera position at yaw=0 (world frame). The "
|
| 202 |
+
"horizontal part rotates with the camera yaw (rot); DZ is fixed."
|
| 203 |
+
),
|
| 204 |
+
)
|
| 205 |
+
parser.add_argument(
|
| 206 |
+
"--glb_base_rotation_deg",
|
| 207 |
+
type=float,
|
| 208 |
+
nargs=3,
|
| 209 |
+
default=(-90.0, 0.0, 90.0),
|
| 210 |
+
metavar=("RX", "RY", "RZ"),
|
| 211 |
+
help=(
|
| 212 |
+
"GLB rotation deg at yaw=0; the Z component gets + rot per frame."
|
| 213 |
+
),
|
| 214 |
+
)
|
| 215 |
+
parser.add_argument(
|
| 216 |
+
"--frame_stride",
|
| 217 |
+
type=int,
|
| 218 |
+
default=1,
|
| 219 |
+
help="Render every Nth waypoint/CSV row in trajectory mode.",
|
| 220 |
+
)
|
| 221 |
+
parser.add_argument(
|
| 222 |
+
"--frame_indices",
|
| 223 |
+
type=int,
|
| 224 |
+
nargs="+",
|
| 225 |
+
default=None,
|
| 226 |
+
metavar="INDEX",
|
| 227 |
+
help=(
|
| 228 |
+
"Render only these trajectory frame/waypoint indices. "
|
| 229 |
+
"When set, --frame_stride is ignored."
|
| 230 |
+
),
|
| 231 |
+
)
|
| 232 |
+
parser.add_argument(
|
| 233 |
+
"--overwrite",
|
| 234 |
+
action="store_true",
|
| 235 |
+
help=(
|
| 236 |
+
"Re-render trajectory frames even if they already exist "
|
| 237 |
+
"(default: skip existing frames for per-frame resume)."
|
| 238 |
+
),
|
| 239 |
+
)
|
| 240 |
+
parser.add_argument(
|
| 241 |
+
"--make_video",
|
| 242 |
+
action=argparse.BooleanOptionalAction,
|
| 243 |
+
default=True,
|
| 244 |
+
help="Stitch trajectory RGB frames into an mp4 (default: on).",
|
| 245 |
+
)
|
| 246 |
+
parser.add_argument(
|
| 247 |
+
"--video_fps",
|
| 248 |
+
type=float,
|
| 249 |
+
default=0.0,
|
| 250 |
+
help=(
|
| 251 |
+
"Video frame rate. 0 = auto from JSON time_step or CSV time_sec "
|
| 252 |
+
"and --frame_stride for real-time playback."
|
| 253 |
+
),
|
| 254 |
+
)
|
| 255 |
return parser
|
| 256 |
|
| 257 |
|
|
|
|
| 272 |
output_dir: Path,
|
| 273 |
render_passes: tuple[str, ...] | list[str],
|
| 274 |
depth_mode: str,
|
| 275 |
+
depth_max: float,
|
| 276 |
resolution: tuple[int, int] | list[int],
|
| 277 |
samples: int,
|
| 278 |
camera_xyz: tuple[float, float, float] | list[float],
|
|
|
|
| 285 |
exposure: float,
|
| 286 |
world_strength: float,
|
| 287 |
fill_light_energy: float,
|
| 288 |
+
remove_doors: bool = False,
|
| 289 |
) -> None:
|
| 290 |
"""Initialize renderer configuration independent of CLI parsing."""
|
| 291 |
self.usd_path = usd_path
|
| 292 |
self.glb_path = self.normalize_optional_path(glb_path)
|
| 293 |
self.glb_xyz = tuple(glb_xyz) if glb_xyz is not None else None
|
| 294 |
self.glb_rotation_deg = (
|
| 295 |
+
list(glb_rotation_deg) if glb_rotation_deg is not None else None
|
| 296 |
)
|
| 297 |
+
if self.glb_rotation_deg is not None:
|
| 298 |
+
self.glb_rotation_deg[0] += 90
|
| 299 |
self.output_dir = output_dir
|
| 300 |
self.render_passes = tuple(render_passes)
|
| 301 |
self.depth_mode = depth_mode
|
| 302 |
+
self.depth_max = depth_max
|
| 303 |
self.resolution = tuple(resolution)
|
| 304 |
self.samples = samples
|
| 305 |
self.camera_xyz = tuple(camera_xyz)
|
|
|
|
| 316 |
self.exposure = exposure
|
| 317 |
self.world_strength = world_strength
|
| 318 |
self.fill_light_energy = fill_light_energy
|
| 319 |
+
self.remove_doors = remove_doors
|
| 320 |
+
# Trajectory frame layout: route each pass to frames_<pass>/
|
| 321 |
+
# frame_<idx>.<ext> under frame_base_dir instead of output_dir.
|
| 322 |
+
self.frame_layout = False
|
| 323 |
+
self.frame_base_dir: Path | None = None
|
| 324 |
+
self.frame_index = 0
|
| 325 |
self.temp_dir: Path | None = None
|
| 326 |
|
| 327 |
@classmethod
|
|
|
|
| 335 |
output_dir=args.output_dir,
|
| 336 |
render_passes=args.render_passes,
|
| 337 |
depth_mode=args.depth_mode,
|
| 338 |
+
depth_max=args.depth_max,
|
| 339 |
resolution=args.resolution,
|
| 340 |
samples=args.samples,
|
| 341 |
camera_xyz=args.camera_xyz,
|
|
|
|
| 346 |
exposure=args.exposure,
|
| 347 |
world_strength=args.world_strength,
|
| 348 |
fill_light_energy=args.fill_light_energy,
|
| 349 |
+
remove_doors=args.remove_doors,
|
| 350 |
)
|
| 351 |
|
| 352 |
@property
|
|
|
|
| 368 |
return Path(normalized)
|
| 369 |
|
| 370 |
def build_output_path(self, filename: str) -> Path:
|
| 371 |
+
"""Build a normalized output path under the render directory.
|
| 372 |
+
|
| 373 |
+
In trajectory frame layout, each pass is routed to its own
|
| 374 |
+
``frames_<pass>/frame_<idx>.<ext>`` directory under
|
| 375 |
+
``frame_base_dir``; otherwise the file lives directly in
|
| 376 |
+
``output_dir``.
|
| 377 |
+
"""
|
| 378 |
+
if self.frame_layout and self.frame_base_dir is not None:
|
| 379 |
+
name = Path(filename)
|
| 380 |
+
subdir = self.frame_base_dir / f"frames_{name.stem}"
|
| 381 |
+
return subdir / f"frame_{self.frame_index:04d}{name.suffix}"
|
| 382 |
return self.output_dir / filename
|
| 383 |
|
| 384 |
def build_temp_path(self, filename: str) -> Path:
|
|
|
|
| 483 |
raise FileNotFoundError(f"USD file not found: {self.usd_path}")
|
| 484 |
bpy.ops.wm.usd_import(filepath=str(self.usd_path))
|
| 485 |
|
| 486 |
+
def remove_door_objects(self) -> int:
|
| 487 |
+
"""Delete any object whose name contains a door keyword."""
|
| 488 |
+
removed = 0
|
| 489 |
+
for obj in list(self.scene.objects):
|
| 490 |
+
if any(kw in obj.name.lower() for kw in DOOR_KEYWORDS):
|
| 491 |
+
bpy.data.objects.remove(obj, do_unlink=True)
|
| 492 |
+
removed += 1
|
| 493 |
+
logger.info("Removed %d door objects.", removed)
|
| 494 |
+
return removed
|
| 495 |
+
|
| 496 |
+
def validate_glb_args(self, *, allow_path_only: bool = False) -> None:
|
| 497 |
"""Normalize optional GLB arguments and ensure all-or-none usage."""
|
| 498 |
has_glb_path = self.glb_path is not None
|
| 499 |
has_glb_xyz = self.glb_xyz is not None
|
| 500 |
has_glb_rotation = self.glb_rotation_deg is not None
|
| 501 |
+
if not has_glb_path:
|
| 502 |
+
if has_glb_xyz or has_glb_rotation:
|
| 503 |
+
raise ValueError(
|
| 504 |
+
"--glb_xyz and --glb_rotation_deg require --glb_path."
|
| 505 |
+
)
|
| 506 |
+
return
|
| 507 |
+
if allow_path_only and not has_glb_xyz and not has_glb_rotation:
|
| 508 |
+
self.validate_glb_path()
|
| 509 |
+
return
|
| 510 |
+
if not has_glb_xyz or not has_glb_rotation:
|
| 511 |
raise ValueError(
|
| 512 |
"--glb_path, --glb_xyz, and --glb_rotation_deg must be "
|
| 513 |
+
"provided together, unless trajectory rendering follows the "
|
| 514 |
+
"camera with --glb_offset_xyz/--glb_base_rotation_deg."
|
| 515 |
)
|
|
|
|
|
|
|
| 516 |
|
| 517 |
+
self.validate_glb_path()
|
| 518 |
+
|
| 519 |
+
def validate_glb_path(self) -> None:
|
| 520 |
+
"""Ensure the optional GLB path points to an existing .glb file."""
|
| 521 |
+
if self.glb_path is None:
|
| 522 |
+
return
|
| 523 |
if not self.glb_path.exists():
|
| 524 |
raise FileNotFoundError(f"GLB file not found: {self.glb_path}")
|
| 525 |
if self.glb_path.suffix.lower() != ".glb":
|
|
|
|
| 765 |
self.scene.render.resolution_y = self.resolution[1]
|
| 766 |
self.scene.render.image_settings.file_format = "PNG"
|
| 767 |
self.scene.render.film_transparent = False
|
| 768 |
+
self.scene.render.use_persistent_data = False
|
| 769 |
+
|
| 770 |
+
# Adaptive sampling + denoising keep quality high at low sample counts.
|
| 771 |
+
self.scene.cycles.use_adaptive_sampling = True
|
| 772 |
+
self.scene.cycles.use_denoising = True
|
| 773 |
+
|
| 774 |
+
self.enable_gpu_devices()
|
| 775 |
|
| 776 |
+
def enable_gpu_devices(self) -> None:
|
| 777 |
+
"""Enable Cycles GPU devices, preferring OptiX then CUDA."""
|
| 778 |
prefs = bpy.context.preferences.addons["cycles"].preferences
|
| 779 |
+
for backend in ("OPTIX", "CUDA"):
|
| 780 |
+
try:
|
| 781 |
+
prefs.compute_device_type = backend
|
| 782 |
+
except TypeError:
|
| 783 |
+
continue
|
| 784 |
+
prefs.get_devices()
|
| 785 |
+
gpu_devices = [d for d in prefs.devices if d.type == backend]
|
| 786 |
+
if not gpu_devices:
|
| 787 |
+
continue
|
| 788 |
+
|
| 789 |
+
for device in prefs.devices:
|
| 790 |
+
device.use = device.type == backend
|
| 791 |
+
try:
|
| 792 |
+
self.scene.cycles.denoiser = (
|
| 793 |
+
"OPTIX" if backend == "OPTIX" else "OPENIMAGEDENOISE"
|
| 794 |
+
)
|
| 795 |
+
except (TypeError, AttributeError):
|
| 796 |
+
pass
|
| 797 |
+
logger.info(
|
| 798 |
+
"Cycles GPU backend: %s (%d device(s)).",
|
| 799 |
+
backend,
|
| 800 |
+
len(gpu_devices),
|
| 801 |
+
)
|
| 802 |
+
return
|
| 803 |
+
|
| 804 |
+
raise RuntimeError("No OptiX/CUDA GPU device found in Blender Cycles.")
|
| 805 |
|
| 806 |
def configure_color_management(self) -> None:
|
| 807 |
self.scene.view_settings.exposure = self.exposure
|
|
|
|
| 827 |
"color_depth": self.scene.render.image_settings.color_depth,
|
| 828 |
"use_nodes": self.scene.use_nodes,
|
| 829 |
"samples": self.scene.cycles.samples,
|
| 830 |
+
"use_denoising": self.scene.cycles.use_denoising,
|
| 831 |
}
|
| 832 |
if include_filepath:
|
| 833 |
state["filepath"] = self.scene.render.filepath
|
|
|
|
| 853 |
self.scene.render.image_settings.color_depth = state["color_depth"]
|
| 854 |
self.scene.use_nodes = state["use_nodes"]
|
| 855 |
self.scene.cycles.samples = state["samples"]
|
| 856 |
+
self.scene.cycles.use_denoising = state["use_denoising"]
|
| 857 |
if "filepath" in state:
|
| 858 |
self.scene.render.filepath = state["filepath"]
|
| 859 |
if "material_override" in state:
|
|
|
|
| 879 |
self.scene.view_settings.gamma = 1.0
|
| 880 |
self.scene.use_nodes = use_nodes
|
| 881 |
self.scene.cycles.samples = samples
|
| 882 |
+
# Auxiliary passes (normal/mesh/seg/flow depth) must not be denoised:
|
| 883 |
+
# denoising would blur normals and corrupt integer index passes.
|
| 884 |
+
self.scene.cycles.use_denoising = False
|
| 885 |
self.scene.render.image_settings.file_format = "PNG"
|
| 886 |
self.scene.render.image_settings.color_mode = color_mode
|
| 887 |
self.scene.render.image_settings.color_depth = color_depth
|
|
|
|
| 1015 |
depth_mode: str,
|
| 1016 |
) -> bpy.types.Node:
|
| 1017 |
"""Build the compositor node that converts raw depth to a previewable map."""
|
| 1018 |
+
del camera # Mapping uses self.depth_max, not the camera clip range.
|
| 1019 |
if depth_mode == "normalized":
|
| 1020 |
normalize = tree.nodes.new(type="CompositorNodeNormalize")
|
| 1021 |
tree.links.new(render_layers.outputs["Depth"], normalize.inputs[0])
|
|
|
|
| 1025 |
raise ValueError(f"Unsupported depth mode: {depth_mode}")
|
| 1026 |
|
| 1027 |
depth_map = tree.nodes.new(type="CompositorNodeMapRange")
|
| 1028 |
+
# Fixed 0..depth_max mapping keeps depth consistent across frames.
|
| 1029 |
+
depth_map.inputs["From Min"].default_value = 0.0
|
| 1030 |
+
depth_map.inputs["From Max"].default_value = self.depth_max
|
| 1031 |
depth_map.inputs["To Min"].default_value = 0.0
|
| 1032 |
depth_map.inputs["To Max"].default_value = 1.0
|
| 1033 |
depth_map.use_clamp = True
|
|
|
|
| 1093 |
self, temp_path: Path, output_path: Path
|
| 1094 |
) -> None:
|
| 1095 |
"""Convert the grayscale depth temp image into the final colored preview."""
|
| 1096 |
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
| 1097 |
if output_path.exists():
|
| 1098 |
output_path.unlink()
|
| 1099 |
if not temp_path.exists():
|
|
|
|
| 1330 |
output_path: Path,
|
| 1331 |
instance_seg: np.ndarray,
|
| 1332 |
) -> None:
|
|
|
|
| 1333 |
vis_output_path = self.get_instance_seg_vis_output_path(output_path)
|
| 1334 |
+
vis_output_path.parent.mkdir(parents=True, exist_ok=True)
|
| 1335 |
|
| 1336 |
visualization = self.build_instance_seg_visualization(
|
| 1337 |
instance_seg=instance_seg,
|
|
|
|
| 1488 |
flow: np.ndarray,
|
| 1489 |
valid_mask: np.ndarray,
|
| 1490 |
) -> None:
|
|
|
|
| 1491 |
flow_output_path = self.get_flow_output_path(output_path)
|
| 1492 |
flow_valid_output_path = self.get_flow_valid_output_path(output_path)
|
| 1493 |
flow_vis_output_path = self.get_flow_vis_output_path(output_path)
|
| 1494 |
+
for path in (
|
| 1495 |
+
flow_output_path,
|
| 1496 |
+
flow_valid_output_path,
|
| 1497 |
+
flow_vis_output_path,
|
| 1498 |
+
):
|
| 1499 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 1500 |
|
| 1501 |
self.save_numpy_array(flow_output_path, flow)
|
| 1502 |
self.save_numpy_array(flow_valid_output_path, valid_mask)
|
|
|
|
| 1675 |
composite_output_path = self.get_composite_output_path(
|
| 1676 |
tuple(render_pass_name for render_pass_name, _ in composite_images)
|
| 1677 |
)
|
| 1678 |
+
composite_output_path.parent.mkdir(parents=True, exist_ok=True)
|
| 1679 |
composite_image = self.build_composite_image(
|
| 1680 |
[image for _, image in composite_images],
|
| 1681 |
[render_pass_name for render_pass_name, _ in composite_images],
|
|
|
|
| 1799 |
self.output_dir.mkdir(parents=True, exist_ok=True)
|
| 1800 |
self.clear_scene()
|
| 1801 |
self.import_usd()
|
| 1802 |
+
if self.remove_doors:
|
| 1803 |
+
self.remove_door_objects()
|
| 1804 |
self.validate_glb_args()
|
| 1805 |
imported_glb_objects = self.import_glb_asset()
|
| 1806 |
self.place_glb_asset(imported_glb_objects)
|
|
|
|
| 1836 |
|
| 1837 |
logger.info("Rendered outputs to %s", self.output_dir)
|
| 1838 |
|
| 1839 |
+
def compute_trajectory_frame_transforms(
|
| 1840 |
+
self,
|
| 1841 |
+
x: float,
|
| 1842 |
+
y: float,
|
| 1843 |
+
rot_deg: float,
|
| 1844 |
+
*,
|
| 1845 |
+
camera_z: float,
|
| 1846 |
+
camera_pitch: float,
|
| 1847 |
+
camera_roll: float,
|
| 1848 |
+
glb_offset_xyz: tuple[float, float, float],
|
| 1849 |
+
glb_base_rotation_deg: tuple[float, float, float],
|
| 1850 |
+
) -> tuple[Vector, Euler, Matrix]:
|
| 1851 |
+
"""Compute per-frame camera and GLB transforms for one waypoint.
|
| 1852 |
+
|
| 1853 |
+
The camera sits at (x, y, camera_z) and yaws by ``rot_deg`` (its
|
| 1854 |
+
heading). The GLB keeps a fixed offset relative to the camera: the
|
| 1855 |
+
horizontal offset rotates with the yaw and its yaw adds ``rot_deg``,
|
| 1856 |
+
while its height stays on the floor.
|
| 1857 |
+
|
| 1858 |
+
Args:
|
| 1859 |
+
x: Waypoint X (m).
|
| 1860 |
+
y: Waypoint Y (m).
|
| 1861 |
+
rot_deg: Waypoint heading in degrees (camera/GLB yaw).
|
| 1862 |
+
camera_z: Camera height (m).
|
| 1863 |
+
camera_pitch: Camera pitch deg (rotation_deg[0]).
|
| 1864 |
+
camera_roll: Camera roll deg (rotation_deg[1]).
|
| 1865 |
+
glb_offset_xyz: GLB-minus-camera offset at yaw=0 (world frame).
|
| 1866 |
+
glb_base_rotation_deg: GLB rotation deg at yaw=0.
|
| 1867 |
+
|
| 1868 |
+
Returns:
|
| 1869 |
+
Tuple of (camera_location, camera_euler, glb_world_transform).
|
| 1870 |
+
"""
|
| 1871 |
+
camera_location = Vector((x, y, camera_z))
|
| 1872 |
+
camera_euler = Euler(
|
| 1873 |
+
self.get_rotation_radians((camera_pitch, camera_roll, rot_deg)),
|
| 1874 |
+
"XYZ",
|
| 1875 |
+
)
|
| 1876 |
+
|
| 1877 |
+
yaw = math.radians(rot_deg)
|
| 1878 |
+
cos_yaw, sin_yaw = math.cos(yaw), math.sin(yaw)
|
| 1879 |
+
off_x, off_y, off_z = glb_offset_xyz
|
| 1880 |
+
glb_xyz = (
|
| 1881 |
+
x + off_x * cos_yaw - off_y * sin_yaw,
|
| 1882 |
+
y + off_x * sin_yaw + off_y * cos_yaw,
|
| 1883 |
+
camera_z + off_z,
|
| 1884 |
+
)
|
| 1885 |
+
base_rx, base_ry, base_rz = glb_base_rotation_deg
|
| 1886 |
+
glb_rotation_deg = (base_rx + 90.0, base_ry, base_rz + rot_deg)
|
| 1887 |
+
glb_transform = self.build_camera_matrix_world(
|
| 1888 |
+
glb_xyz, glb_rotation_deg
|
| 1889 |
+
)
|
| 1890 |
+
return camera_location, camera_euler, glb_transform
|
| 1891 |
+
|
| 1892 |
+
def compute_camera_follow_glb_transform(
|
| 1893 |
+
self,
|
| 1894 |
+
camera_xyz: tuple[float, float, float],
|
| 1895 |
+
camera_rotation_deg: tuple[float, float, float],
|
| 1896 |
+
*,
|
| 1897 |
+
glb_offset_xyz: tuple[float, float, float],
|
| 1898 |
+
glb_base_rotation_deg: tuple[float, float, float],
|
| 1899 |
+
) -> Matrix:
|
| 1900 |
+
"""Compute a GLB transform from the default camera-follow offset."""
|
| 1901 |
+
_, _, glb_transform = self.compute_trajectory_frame_transforms(
|
| 1902 |
+
camera_xyz[0],
|
| 1903 |
+
camera_xyz[1],
|
| 1904 |
+
camera_rotation_deg[2],
|
| 1905 |
+
camera_z=camera_xyz[2],
|
| 1906 |
+
camera_pitch=camera_rotation_deg[0],
|
| 1907 |
+
camera_roll=camera_rotation_deg[1],
|
| 1908 |
+
glb_offset_xyz=glb_offset_xyz,
|
| 1909 |
+
glb_base_rotation_deg=glb_base_rotation_deg,
|
| 1910 |
+
)
|
| 1911 |
+
return glb_transform
|
| 1912 |
+
|
| 1913 |
+
def frame_outputs_exist(self) -> bool:
|
| 1914 |
+
"""Whether every requested pass output for the current frame exists.
|
| 1915 |
+
|
| 1916 |
+
Used for per-frame resume: a frame is skipped only when all its
|
| 1917 |
+
requested pass preview outputs are already on disk.
|
| 1918 |
+
"""
|
| 1919 |
+
rgb_path = self.get_rgb_output_path()
|
| 1920 |
+
for pass_name, occurrence in self.iter_render_pass_occurrences():
|
| 1921 |
+
preview = self.get_preview_output_path(
|
| 1922 |
+
rgb_path, pass_name, occurrence
|
| 1923 |
+
)
|
| 1924 |
+
if preview is None or not preview.exists():
|
| 1925 |
+
return False
|
| 1926 |
+
return True
|
| 1927 |
+
|
| 1928 |
+
def run_trajectory(
|
| 1929 |
+
self,
|
| 1930 |
+
*,
|
| 1931 |
+
waypoints: list[tuple[float, float, float]],
|
| 1932 |
+
camera_z: float,
|
| 1933 |
+
camera_pitch: float,
|
| 1934 |
+
camera_roll: float,
|
| 1935 |
+
glb_offset_xyz: tuple[float, float, float],
|
| 1936 |
+
glb_base_rotation_deg: tuple[float, float, float],
|
| 1937 |
+
frame_stride: int,
|
| 1938 |
+
frame_indices: list[int] | None = None,
|
| 1939 |
+
make_video: bool = False,
|
| 1940 |
+
video_fps: float = 10.0,
|
| 1941 |
+
overwrite: bool = False,
|
| 1942 |
+
) -> None:
|
| 1943 |
+
"""Render one frame per trajectory waypoint following the camera path.
|
| 1944 |
+
|
| 1945 |
+
The scene (USD, GLB, lighting, world) is set up once; each frame only
|
| 1946 |
+
repositions the camera and GLB, then renders into a shared ``frames/``
|
| 1947 |
+
directory with a ``frame_<idx>_`` filename prefix. When ``make_video``
|
| 1948 |
+
is set, the RGB frames are stitched into an mp4.
|
| 1949 |
+
|
| 1950 |
+
Args:
|
| 1951 |
+
waypoints: List of (x, y, rot_deg) waypoints.
|
| 1952 |
+
camera_z: Camera height (m).
|
| 1953 |
+
camera_pitch: Camera pitch deg (rotation_deg[0]).
|
| 1954 |
+
camera_roll: Camera roll deg (rotation_deg[1]).
|
| 1955 |
+
glb_offset_xyz: GLB-minus-camera offset at yaw=0 (world frame).
|
| 1956 |
+
glb_base_rotation_deg: GLB rotation deg at yaw=0.
|
| 1957 |
+
frame_stride: Render every Nth waypoint.
|
| 1958 |
+
frame_indices: Optional zero-based waypoint indices to render.
|
| 1959 |
+
make_video: Stitch the RGB frames into an mp4 after rendering.
|
| 1960 |
+
video_fps: Frame rate of the stitched video.
|
| 1961 |
+
overwrite: Re-render every frame; if False, frames whose pass
|
| 1962 |
+
outputs already exist are skipped (per-frame resume).
|
| 1963 |
+
"""
|
| 1964 |
+
if not waypoints:
|
| 1965 |
+
raise ValueError("Trajectory contains no waypoints.")
|
| 1966 |
+
if self.glb_path is None:
|
| 1967 |
+
raise ValueError(
|
| 1968 |
+
"--glb_path is required for trajectory rendering."
|
| 1969 |
+
)
|
| 1970 |
+
if not self.glb_path.exists():
|
| 1971 |
+
raise FileNotFoundError(f"GLB file not found: {self.glb_path}")
|
| 1972 |
+
if self.glb_path.suffix.lower() != ".glb":
|
| 1973 |
+
raise ValueError(
|
| 1974 |
+
f"Expected a .glb asset, but got: {self.glb_path}"
|
| 1975 |
+
)
|
| 1976 |
+
|
| 1977 |
+
base_output_dir = self.output_dir
|
| 1978 |
+
base_output_dir.mkdir(parents=True, exist_ok=True)
|
| 1979 |
+
|
| 1980 |
+
self.clear_scene()
|
| 1981 |
+
self.import_usd()
|
| 1982 |
+
if self.remove_doors:
|
| 1983 |
+
self.remove_door_objects()
|
| 1984 |
+
imported_glb_objects = self.import_glb_asset()
|
| 1985 |
+
root_objects = self.get_imported_root_objects(imported_glb_objects)
|
| 1986 |
+
original_matrices = {
|
| 1987 |
+
obj.as_pointer(): obj.matrix_world.copy() for obj in root_objects
|
| 1988 |
+
}
|
| 1989 |
+
|
| 1990 |
+
min_corner, max_corner = self.get_scene_bbox()
|
| 1991 |
+
center = (min_corner + max_corner) * 0.5
|
| 1992 |
+
diagonal = (max_corner - min_corner).length
|
| 1993 |
+
camera = self.create_camera()
|
| 1994 |
+
self.ensure_lighting(diagonal, center, max_corner.z)
|
| 1995 |
+
world_created = self.ensure_world()
|
| 1996 |
+
self.add_fill_light(
|
| 1997 |
+
diagonal, center, max_corner.z, energy=self.fill_light_energy
|
| 1998 |
+
)
|
| 1999 |
+
if world_created:
|
| 2000 |
+
self.add_light_rig(
|
| 2001 |
+
diagonal,
|
| 2002 |
+
center,
|
| 2003 |
+
max_corner.z,
|
| 2004 |
+
area_energy=1500.0,
|
| 2005 |
+
sun_energy=0.35,
|
| 2006 |
+
prefix="Fill",
|
| 2007 |
+
)
|
| 2008 |
+
self.configure_color_management()
|
| 2009 |
+
self.configure_cycles()
|
| 2010 |
+
|
| 2011 |
+
if frame_indices is None:
|
| 2012 |
+
frames = list(enumerate(waypoints))[:: max(1, frame_stride)]
|
| 2013 |
+
else:
|
| 2014 |
+
invalid_indices = sorted(
|
| 2015 |
+
{
|
| 2016 |
+
index
|
| 2017 |
+
for index in frame_indices
|
| 2018 |
+
if index < 0 or index >= len(waypoints)
|
| 2019 |
+
}
|
| 2020 |
+
)
|
| 2021 |
+
if invalid_indices:
|
| 2022 |
+
raise ValueError(
|
| 2023 |
+
"Trajectory frame indices out of range "
|
| 2024 |
+
f"[0, {len(waypoints) - 1}]: {invalid_indices}"
|
| 2025 |
+
)
|
| 2026 |
+
frames = [
|
| 2027 |
+
(index, waypoints[index])
|
| 2028 |
+
for index in dict.fromkeys(frame_indices)
|
| 2029 |
+
]
|
| 2030 |
+
self.frame_layout = True
|
| 2031 |
+
self.frame_base_dir = base_output_dir
|
| 2032 |
+
|
| 2033 |
+
# The highest already-rendered frame is the one most likely left
|
| 2034 |
+
# half-written by an interrupted run, so re-render it even on resume.
|
| 2035 |
+
last_existing = -1
|
| 2036 |
+
if not overwrite:
|
| 2037 |
+
for frame_idx, _ in frames:
|
| 2038 |
+
self.frame_index = frame_idx
|
| 2039 |
+
if self.frame_outputs_exist():
|
| 2040 |
+
last_existing = frame_idx
|
| 2041 |
+
|
| 2042 |
+
with tempfile.TemporaryDirectory(
|
| 2043 |
+
prefix="render_usd_traj_", dir=None
|
| 2044 |
+
) as temp_dir:
|
| 2045 |
+
self.temp_dir = Path(temp_dir)
|
| 2046 |
+
for frame_idx, (x, y, rot_deg) in frames:
|
| 2047 |
+
self.frame_index = frame_idx
|
| 2048 |
+
# Per-frame resume: skip complete frames, but always redo the
|
| 2049 |
+
# last existing one (possibly a truncated/corrupt write).
|
| 2050 |
+
if (
|
| 2051 |
+
not overwrite
|
| 2052 |
+
and frame_idx != last_existing
|
| 2053 |
+
and self.frame_outputs_exist()
|
| 2054 |
+
):
|
| 2055 |
+
logger.info("Skip existing frame %04d", frame_idx)
|
| 2056 |
+
continue
|
| 2057 |
+
|
| 2058 |
+
cam_loc, cam_euler, glb_transform = (
|
| 2059 |
+
self.compute_trajectory_frame_transforms(
|
| 2060 |
+
x,
|
| 2061 |
+
y,
|
| 2062 |
+
rot_deg,
|
| 2063 |
+
camera_z=camera_z,
|
| 2064 |
+
camera_pitch=camera_pitch,
|
| 2065 |
+
camera_roll=camera_roll,
|
| 2066 |
+
glb_offset_xyz=glb_offset_xyz,
|
| 2067 |
+
glb_base_rotation_deg=glb_base_rotation_deg,
|
| 2068 |
+
)
|
| 2069 |
+
)
|
| 2070 |
+
camera.location = cam_loc
|
| 2071 |
+
camera.rotation_euler = cam_euler
|
| 2072 |
+
for obj in root_objects:
|
| 2073 |
+
obj.matrix_world = (
|
| 2074 |
+
glb_transform @ original_matrices[obj.as_pointer()]
|
| 2075 |
+
)
|
| 2076 |
+
bpy.context.view_layer.update()
|
| 2077 |
+
|
| 2078 |
+
self.render(self.get_rgb_output_path())
|
| 2079 |
+
logger.info(
|
| 2080 |
+
"Rendered frame %04d at (%.3f, %.3f, rot=%.1f)",
|
| 2081 |
+
frame_idx,
|
| 2082 |
+
x,
|
| 2083 |
+
y,
|
| 2084 |
+
rot_deg,
|
| 2085 |
+
)
|
| 2086 |
+
self.temp_dir = None
|
| 2087 |
+
|
| 2088 |
+
self.frame_layout = False
|
| 2089 |
+
self.frame_base_dir = None
|
| 2090 |
+
self.output_dir = base_output_dir
|
| 2091 |
+
logger.info(
|
| 2092 |
+
"Rendered %d trajectory frames to %s",
|
| 2093 |
+
len(frames),
|
| 2094 |
+
base_output_dir,
|
| 2095 |
+
)
|
| 2096 |
+
|
| 2097 |
+
if make_video:
|
| 2098 |
+
self.stitch_pass_videos(base_output_dir, fps=video_fps)
|
| 2099 |
+
|
| 2100 |
+
def run_camera_trajectory(
|
| 2101 |
+
self,
|
| 2102 |
+
*,
|
| 2103 |
+
frames: list[CameraTrajectoryFrame],
|
| 2104 |
+
glb_offset_xyz: tuple[float, float, float],
|
| 2105 |
+
glb_base_rotation_deg: tuple[float, float, float],
|
| 2106 |
+
frame_stride: int,
|
| 2107 |
+
frame_indices: list[int] | None = None,
|
| 2108 |
+
make_video: bool = False,
|
| 2109 |
+
video_fps: float = 10.0,
|
| 2110 |
+
overwrite: bool = False,
|
| 2111 |
+
) -> None:
|
| 2112 |
+
"""Render fully specified camera poses loaded from a trajectory CSV.
|
| 2113 |
+
|
| 2114 |
+
Args:
|
| 2115 |
+
frames: Camera frames with output index, timestamp, and 6DoF pose.
|
| 2116 |
+
glb_offset_xyz: Local XYZ offset applied to GLB assets.
|
| 2117 |
+
glb_base_rotation_deg: Base XYZ Euler rotation applied to GLB assets.
|
| 2118 |
+
frame_stride: Render every Nth CSV row.
|
| 2119 |
+
frame_indices: Optional CSV ``frame`` values to render.
|
| 2120 |
+
make_video: Stitch rendered image passes into mp4 files.
|
| 2121 |
+
video_fps: Frame rate of the stitched videos.
|
| 2122 |
+
overwrite: Re-render complete frames instead of resuming.
|
| 2123 |
+
"""
|
| 2124 |
+
if not frames:
|
| 2125 |
+
raise ValueError("Camera trajectory contains no frames.")
|
| 2126 |
+
|
| 2127 |
+
base_output_dir = self.output_dir
|
| 2128 |
+
base_output_dir.mkdir(parents=True, exist_ok=True)
|
| 2129 |
+
|
| 2130 |
+
self.clear_scene()
|
| 2131 |
+
self.import_usd()
|
| 2132 |
+
if self.remove_doors:
|
| 2133 |
+
self.remove_door_objects()
|
| 2134 |
+
self.validate_glb_args(allow_path_only=True)
|
| 2135 |
+
imported_glb_objects = self.import_glb_asset()
|
| 2136 |
+
glb_follows_camera = (
|
| 2137 |
+
self.glb_path is not None
|
| 2138 |
+
and self.glb_xyz is None
|
| 2139 |
+
and self.glb_rotation_deg is None
|
| 2140 |
+
)
|
| 2141 |
+
root_objects: list[bpy.types.Object] = []
|
| 2142 |
+
original_matrices: dict[int, Matrix] = {}
|
| 2143 |
+
if glb_follows_camera:
|
| 2144 |
+
root_objects = self.get_imported_root_objects(imported_glb_objects)
|
| 2145 |
+
original_matrices = {
|
| 2146 |
+
obj.as_pointer(): obj.matrix_world.copy()
|
| 2147 |
+
for obj in root_objects
|
| 2148 |
+
}
|
| 2149 |
+
else:
|
| 2150 |
+
self.place_glb_asset(imported_glb_objects)
|
| 2151 |
+
|
| 2152 |
+
min_corner, max_corner = self.get_scene_bbox()
|
| 2153 |
+
center = (min_corner + max_corner) * 0.5
|
| 2154 |
+
diagonal = (max_corner - min_corner).length
|
| 2155 |
+
camera = self.create_camera()
|
| 2156 |
+
self.ensure_lighting(diagonal, center, max_corner.z)
|
| 2157 |
+
world_created = self.ensure_world()
|
| 2158 |
+
self.add_fill_light(
|
| 2159 |
+
diagonal, center, max_corner.z, energy=self.fill_light_energy
|
| 2160 |
+
)
|
| 2161 |
+
if world_created:
|
| 2162 |
+
self.add_light_rig(
|
| 2163 |
+
diagonal,
|
| 2164 |
+
center,
|
| 2165 |
+
max_corner.z,
|
| 2166 |
+
area_energy=1500.0,
|
| 2167 |
+
sun_energy=0.35,
|
| 2168 |
+
prefix="Fill",
|
| 2169 |
+
)
|
| 2170 |
+
self.configure_color_management()
|
| 2171 |
+
self.configure_cycles()
|
| 2172 |
+
|
| 2173 |
+
if frame_indices is None:
|
| 2174 |
+
selected_frames = frames[:: max(1, frame_stride)]
|
| 2175 |
+
else:
|
| 2176 |
+
frames_by_index = {frame.frame_index: frame for frame in frames}
|
| 2177 |
+
invalid_indices = sorted(
|
| 2178 |
+
{
|
| 2179 |
+
index
|
| 2180 |
+
for index in frame_indices
|
| 2181 |
+
if index not in frames_by_index
|
| 2182 |
+
}
|
| 2183 |
+
)
|
| 2184 |
+
if invalid_indices:
|
| 2185 |
+
raise ValueError(
|
| 2186 |
+
"Camera trajectory frame indices not found: "
|
| 2187 |
+
f"{invalid_indices}"
|
| 2188 |
+
)
|
| 2189 |
+
selected_frames = [
|
| 2190 |
+
frames_by_index[index]
|
| 2191 |
+
for index in dict.fromkeys(frame_indices)
|
| 2192 |
+
]
|
| 2193 |
+
|
| 2194 |
+
self.frame_layout = True
|
| 2195 |
+
self.frame_base_dir = base_output_dir
|
| 2196 |
+
|
| 2197 |
+
last_existing = -1
|
| 2198 |
+
if not overwrite:
|
| 2199 |
+
for frame in selected_frames:
|
| 2200 |
+
self.frame_index = frame.frame_index
|
| 2201 |
+
if self.frame_outputs_exist():
|
| 2202 |
+
last_existing = frame.frame_index
|
| 2203 |
+
|
| 2204 |
+
with tempfile.TemporaryDirectory(
|
| 2205 |
+
prefix="render_usd_camera_traj_", dir=None
|
| 2206 |
+
) as temp_dir:
|
| 2207 |
+
self.temp_dir = Path(temp_dir)
|
| 2208 |
+
for frame in selected_frames:
|
| 2209 |
+
self.frame_index = frame.frame_index
|
| 2210 |
+
if (
|
| 2211 |
+
not overwrite
|
| 2212 |
+
and frame.frame_index != last_existing
|
| 2213 |
+
and self.frame_outputs_exist()
|
| 2214 |
+
):
|
| 2215 |
+
logger.info("Skip existing frame %04d", frame.frame_index)
|
| 2216 |
+
continue
|
| 2217 |
+
|
| 2218 |
+
camera.location = Vector(frame.xyz)
|
| 2219 |
+
camera.rotation_euler = Euler(
|
| 2220 |
+
self.get_rotation_radians(frame.rotation_deg),
|
| 2221 |
+
"XYZ",
|
| 2222 |
+
)
|
| 2223 |
+
if glb_follows_camera:
|
| 2224 |
+
glb_transform = self.compute_camera_follow_glb_transform(
|
| 2225 |
+
frame.xyz,
|
| 2226 |
+
frame.rotation_deg,
|
| 2227 |
+
glb_offset_xyz=glb_offset_xyz,
|
| 2228 |
+
glb_base_rotation_deg=glb_base_rotation_deg,
|
| 2229 |
+
)
|
| 2230 |
+
for obj in root_objects:
|
| 2231 |
+
obj.matrix_world = (
|
| 2232 |
+
glb_transform @ original_matrices[obj.as_pointer()]
|
| 2233 |
+
)
|
| 2234 |
+
bpy.context.view_layer.update()
|
| 2235 |
+
|
| 2236 |
+
self.render(self.get_rgb_output_path())
|
| 2237 |
+
logger.info(
|
| 2238 |
+
"Rendered frame %04d at "
|
| 2239 |
+
"(%.3f, %.3f, %.3f), rotation=(%.2f, %.2f, %.2f)",
|
| 2240 |
+
frame.frame_index,
|
| 2241 |
+
*frame.xyz,
|
| 2242 |
+
*frame.rotation_deg,
|
| 2243 |
+
)
|
| 2244 |
+
self.temp_dir = None
|
| 2245 |
+
|
| 2246 |
+
self.frame_layout = False
|
| 2247 |
+
self.frame_base_dir = None
|
| 2248 |
+
self.output_dir = base_output_dir
|
| 2249 |
+
logger.info(
|
| 2250 |
+
"Rendered %d camera trajectory frames to %s",
|
| 2251 |
+
len(selected_frames),
|
| 2252 |
+
base_output_dir,
|
| 2253 |
+
)
|
| 2254 |
+
|
| 2255 |
+
if make_video:
|
| 2256 |
+
self.stitch_pass_videos(base_output_dir, fps=video_fps)
|
| 2257 |
+
|
| 2258 |
+
def stitch_pass_videos(self, base_dir: Path, *, fps: float) -> None:
|
| 2259 |
+
"""Stitch each ``frames_<pass>/`` image sequence into ``<pass>.mp4``.
|
| 2260 |
+
|
| 2261 |
+
Scans ``base_dir`` for ``frames_<pass>`` directories, orders the
|
| 2262 |
+
``frame_*.png`` images, and writes one video per pass next to them
|
| 2263 |
+
(e.g. ``frames_render_rgb/`` -> ``render_rgb.mp4``) using ffmpeg.
|
| 2264 |
+
|
| 2265 |
+
Args:
|
| 2266 |
+
base_dir: Trajectory output directory containing the frame dirs.
|
| 2267 |
+
fps: Output frame rate (clamped to a small positive minimum).
|
| 2268 |
+
"""
|
| 2269 |
+
fps = max(fps, 1.0)
|
| 2270 |
+
for frames_subdir in sorted(base_dir.glob("frames_*")):
|
| 2271 |
+
if not frames_subdir.is_dir():
|
| 2272 |
+
continue
|
| 2273 |
+
frame_paths = sorted(frames_subdir.glob("frame_*.png"))
|
| 2274 |
+
if not frame_paths:
|
| 2275 |
+
continue
|
| 2276 |
+
|
| 2277 |
+
pass_name = frames_subdir.name[len("frames_") :]
|
| 2278 |
+
output_path = base_dir / f"{pass_name}.mp4"
|
| 2279 |
+
self.encode_frames_to_video(frames_subdir, output_path, fps)
|
| 2280 |
+
logger.info(
|
| 2281 |
+
"Stitched %d frames into %s (%.2f fps).",
|
| 2282 |
+
len(frame_paths),
|
| 2283 |
+
output_path,
|
| 2284 |
+
fps,
|
| 2285 |
+
)
|
| 2286 |
+
|
| 2287 |
+
@staticmethod
|
| 2288 |
+
def encode_frames_to_video(
|
| 2289 |
+
frames_dir: Path, output_path: Path, fps: float
|
| 2290 |
+
) -> None:
|
| 2291 |
+
"""Encode a ``frame_*.png`` sequence into an mp4 via ffmpeg.
|
| 2292 |
+
|
| 2293 |
+
Uses the system ffmpeg (h264/yuv420p) and a glob input pattern so
|
| 2294 |
+
non-contiguous frame indices (e.g. with --frame_stride) work. Even
|
| 2295 |
+
dimensions are enforced for yuv420p compatibility.
|
| 2296 |
+
|
| 2297 |
+
Args:
|
| 2298 |
+
frames_dir: Directory holding the ``frame_*.png`` images.
|
| 2299 |
+
output_path: Destination mp4 path.
|
| 2300 |
+
fps: Output frame rate.
|
| 2301 |
+
"""
|
| 2302 |
+
cmd = [
|
| 2303 |
+
"ffmpeg",
|
| 2304 |
+
"-y",
|
| 2305 |
+
"-framerate",
|
| 2306 |
+
f"{fps:g}",
|
| 2307 |
+
"-pattern_type",
|
| 2308 |
+
"glob",
|
| 2309 |
+
"-i",
|
| 2310 |
+
str(frames_dir / "frame_*.png"),
|
| 2311 |
+
"-vf",
|
| 2312 |
+
"pad=ceil(iw/2)*2:ceil(ih/2)*2",
|
| 2313 |
+
"-c:v",
|
| 2314 |
+
"libx264",
|
| 2315 |
+
"-pix_fmt",
|
| 2316 |
+
"yuv420p",
|
| 2317 |
+
str(output_path),
|
| 2318 |
+
]
|
| 2319 |
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
| 2320 |
+
if result.returncode != 0:
|
| 2321 |
+
raise RuntimeError(
|
| 2322 |
+
f"ffmpeg failed for {output_path}:\n{result.stderr}"
|
| 2323 |
+
)
|
| 2324 |
+
|
| 2325 |
+
|
| 2326 |
+
def load_trajectory_waypoints(
|
| 2327 |
+
trajectory_json: Path,
|
| 2328 |
+
) -> list[tuple[float, float, float]]:
|
| 2329 |
+
"""Load (x, y, rot) waypoints from a trajectory JSON file.
|
| 2330 |
+
|
| 2331 |
+
Accepts either a dict with a ``waypoints`` key or a bare list of records.
|
| 2332 |
+
|
| 2333 |
+
Args:
|
| 2334 |
+
trajectory_json: Path to the trajectory JSON.
|
| 2335 |
+
|
| 2336 |
+
Returns:
|
| 2337 |
+
List of (x, y, rot_deg) tuples.
|
| 2338 |
+
"""
|
| 2339 |
+
data = json.loads(Path(trajectory_json).read_text())
|
| 2340 |
+
records = data["waypoints"] if isinstance(data, dict) else data
|
| 2341 |
+
return [
|
| 2342 |
+
(float(rec["x"]), float(rec["y"]), float(rec["rot"]))
|
| 2343 |
+
for rec in records
|
| 2344 |
+
]
|
| 2345 |
+
|
| 2346 |
+
|
| 2347 |
+
def load_camera_trajectory_csv(
|
| 2348 |
+
trajectory_csv: Path,
|
| 2349 |
+
) -> list[CameraTrajectoryFrame]:
|
| 2350 |
+
"""Load full camera poses from a trajectory CSV file.
|
| 2351 |
+
|
| 2352 |
+
Args:
|
| 2353 |
+
trajectory_csv: CSV containing the required camera trajectory fields.
|
| 2354 |
+
|
| 2355 |
+
Returns:
|
| 2356 |
+
Camera frames in CSV row order.
|
| 2357 |
+
"""
|
| 2358 |
+
trajectory_csv = Path(trajectory_csv)
|
| 2359 |
+
if not trajectory_csv.exists():
|
| 2360 |
+
raise FileNotFoundError(
|
| 2361 |
+
f"Camera trajectory CSV not found: {trajectory_csv}"
|
| 2362 |
+
)
|
| 2363 |
+
|
| 2364 |
+
frames: list[CameraTrajectoryFrame] = []
|
| 2365 |
+
seen_indices: set[int] = set()
|
| 2366 |
+
previous_frame_index: int | None = None
|
| 2367 |
+
previous_time_sec: float | None = None
|
| 2368 |
+
with trajectory_csv.open(newline="", encoding="utf-8-sig") as csv_file:
|
| 2369 |
+
reader = csv.DictReader(csv_file)
|
| 2370 |
+
fieldnames = set(reader.fieldnames or ())
|
| 2371 |
+
missing_fields = [
|
| 2372 |
+
field
|
| 2373 |
+
for field in CAMERA_TRAJECTORY_CSV_FIELDS
|
| 2374 |
+
if field not in fieldnames
|
| 2375 |
+
]
|
| 2376 |
+
if missing_fields:
|
| 2377 |
+
raise ValueError(
|
| 2378 |
+
f"Camera trajectory CSV is missing fields: {missing_fields}"
|
| 2379 |
+
)
|
| 2380 |
+
|
| 2381 |
+
for row_number, row in enumerate(reader, start=2):
|
| 2382 |
+
try:
|
| 2383 |
+
frame_index = int(row["frame"])
|
| 2384 |
+
values = {
|
| 2385 |
+
field: float(row[field])
|
| 2386 |
+
for field in CAMERA_TRAJECTORY_CSV_FIELDS[1:]
|
| 2387 |
+
}
|
| 2388 |
+
except (TypeError, ValueError) as exc:
|
| 2389 |
+
raise ValueError(
|
| 2390 |
+
"Invalid camera trajectory value at CSV row "
|
| 2391 |
+
f"{row_number}: {exc}"
|
| 2392 |
+
) from exc
|
| 2393 |
+
|
| 2394 |
+
if frame_index < 0:
|
| 2395 |
+
raise ValueError(
|
| 2396 |
+
f"Camera frame must be non-negative at CSV row "
|
| 2397 |
+
f"{row_number}: {frame_index}"
|
| 2398 |
+
)
|
| 2399 |
+
if frame_index in seen_indices:
|
| 2400 |
+
raise ValueError(
|
| 2401 |
+
f"Duplicate camera frame at CSV row {row_number}: "
|
| 2402 |
+
f"{frame_index}"
|
| 2403 |
+
)
|
| 2404 |
+
non_finite_fields = [
|
| 2405 |
+
field
|
| 2406 |
+
for field, value in values.items()
|
| 2407 |
+
if not math.isfinite(value)
|
| 2408 |
+
]
|
| 2409 |
+
if non_finite_fields:
|
| 2410 |
+
raise ValueError(
|
| 2411 |
+
f"Non-finite values at CSV row {row_number}: "
|
| 2412 |
+
f"{non_finite_fields}"
|
| 2413 |
+
)
|
| 2414 |
+
if (
|
| 2415 |
+
previous_frame_index is not None
|
| 2416 |
+
and frame_index <= previous_frame_index
|
| 2417 |
+
):
|
| 2418 |
+
raise ValueError(
|
| 2419 |
+
"Camera frames must be strictly increasing at CSV row "
|
| 2420 |
+
f"{row_number}: {frame_index} <= {previous_frame_index}"
|
| 2421 |
+
)
|
| 2422 |
+
if (
|
| 2423 |
+
previous_time_sec is not None
|
| 2424 |
+
and values["time_sec"] <= previous_time_sec
|
| 2425 |
+
):
|
| 2426 |
+
raise ValueError(
|
| 2427 |
+
"Camera timestamps must be strictly increasing at CSV "
|
| 2428 |
+
f"row {row_number}: {values['time_sec']} <= "
|
| 2429 |
+
f"{previous_time_sec}"
|
| 2430 |
+
)
|
| 2431 |
+
|
| 2432 |
+
seen_indices.add(frame_index)
|
| 2433 |
+
previous_frame_index = frame_index
|
| 2434 |
+
previous_time_sec = values["time_sec"]
|
| 2435 |
+
frames.append(
|
| 2436 |
+
CameraTrajectoryFrame(
|
| 2437 |
+
frame_index=frame_index,
|
| 2438 |
+
time_sec=values["time_sec"],
|
| 2439 |
+
xyz=(values["x"], values["y"], values["z"]),
|
| 2440 |
+
rotation_deg=(
|
| 2441 |
+
values["roll_x_deg"],
|
| 2442 |
+
values["pitch_y_deg"],
|
| 2443 |
+
values["yaw_z_deg"],
|
| 2444 |
+
),
|
| 2445 |
+
)
|
| 2446 |
+
)
|
| 2447 |
+
|
| 2448 |
+
return frames
|
| 2449 |
+
|
| 2450 |
+
|
| 2451 |
+
def resolve_video_fps(
|
| 2452 |
+
video_fps: float, trajectory_json: Path, frame_stride: int
|
| 2453 |
+
) -> float:
|
| 2454 |
+
"""Resolve the video frame rate, auto-deriving real-time fps if 0.
|
| 2455 |
+
|
| 2456 |
+
Args:
|
| 2457 |
+
video_fps: Requested fps; 0 means auto.
|
| 2458 |
+
trajectory_json: Trajectory JSON (read for ``time_step``).
|
| 2459 |
+
frame_stride: Waypoint stride used during rendering.
|
| 2460 |
+
|
| 2461 |
+
Returns:
|
| 2462 |
+
A positive frame rate.
|
| 2463 |
+
"""
|
| 2464 |
+
if video_fps > 0.0:
|
| 2465 |
+
return video_fps
|
| 2466 |
+
|
| 2467 |
+
data = json.loads(Path(trajectory_json).read_text())
|
| 2468 |
+
time_step = data.get("time_step") if isinstance(data, dict) else None
|
| 2469 |
+
if not time_step or time_step <= 0.0:
|
| 2470 |
+
return 10.0
|
| 2471 |
+
return (1.0 / time_step) / max(1, frame_stride)
|
| 2472 |
+
|
| 2473 |
+
|
| 2474 |
+
def resolve_csv_video_fps(
|
| 2475 |
+
video_fps: float,
|
| 2476 |
+
frames: list[CameraTrajectoryFrame],
|
| 2477 |
+
frame_stride: int,
|
| 2478 |
+
) -> float:
|
| 2479 |
+
"""Resolve video fps from CSV timestamps when no fps is specified."""
|
| 2480 |
+
if video_fps > 0.0:
|
| 2481 |
+
return video_fps
|
| 2482 |
+
|
| 2483 |
+
time_deltas = [
|
| 2484 |
+
current.time_sec - previous.time_sec
|
| 2485 |
+
for previous, current in zip(frames, frames[1:])
|
| 2486 |
+
if current.time_sec > previous.time_sec
|
| 2487 |
+
]
|
| 2488 |
+
if not time_deltas:
|
| 2489 |
+
return 10.0
|
| 2490 |
+
|
| 2491 |
+
time_step = float(np.median(time_deltas))
|
| 2492 |
+
return (1.0 / time_step) / max(1, frame_stride)
|
| 2493 |
+
|
| 2494 |
+
|
| 2495 |
+
def run_trajectory_render(args: argparse.Namespace) -> None:
|
| 2496 |
+
"""Render a trajectory from parsed CLI arguments."""
|
| 2497 |
+
waypoints = load_trajectory_waypoints(args.trajectory_json)
|
| 2498 |
+
if not waypoints:
|
| 2499 |
+
raise ValueError(f"No waypoints found in {args.trajectory_json}.")
|
| 2500 |
+
|
| 2501 |
+
first_x, first_y, first_rot = waypoints[0]
|
| 2502 |
+
renderer = RenderUsd(
|
| 2503 |
+
usd_path=args.usd_path,
|
| 2504 |
+
glb_path=args.glb_path,
|
| 2505 |
+
glb_xyz=None,
|
| 2506 |
+
glb_rotation_deg=None,
|
| 2507 |
+
output_dir=args.output_dir,
|
| 2508 |
+
render_passes=args.render_passes,
|
| 2509 |
+
depth_mode=args.depth_mode,
|
| 2510 |
+
depth_max=args.depth_max,
|
| 2511 |
+
resolution=args.resolution,
|
| 2512 |
+
samples=args.samples,
|
| 2513 |
+
camera_xyz=(first_x, first_y, args.camera_z),
|
| 2514 |
+
camera_rotation_deg=(args.camera_pitch, args.camera_roll, first_rot),
|
| 2515 |
+
flow_camera_xyz=args.flow_camera_xyz,
|
| 2516 |
+
flow_camera_rotation_deg=args.flow_camera_rotation_deg,
|
| 2517 |
+
focal_length_mm=args.focal_length_mm,
|
| 2518 |
+
exposure=args.exposure,
|
| 2519 |
+
world_strength=args.world_strength,
|
| 2520 |
+
fill_light_energy=args.fill_light_energy,
|
| 2521 |
+
remove_doors=args.remove_doors,
|
| 2522 |
+
)
|
| 2523 |
+
renderer.run_trajectory(
|
| 2524 |
+
waypoints=waypoints,
|
| 2525 |
+
camera_z=args.camera_z,
|
| 2526 |
+
camera_pitch=args.camera_pitch,
|
| 2527 |
+
camera_roll=args.camera_roll,
|
| 2528 |
+
glb_offset_xyz=tuple(args.glb_offset_xyz),
|
| 2529 |
+
glb_base_rotation_deg=tuple(args.glb_base_rotation_deg),
|
| 2530 |
+
frame_stride=args.frame_stride,
|
| 2531 |
+
frame_indices=args.frame_indices,
|
| 2532 |
+
make_video=args.make_video,
|
| 2533 |
+
video_fps=resolve_video_fps(
|
| 2534 |
+
args.video_fps, args.trajectory_json, args.frame_stride
|
| 2535 |
+
),
|
| 2536 |
+
overwrite=args.overwrite,
|
| 2537 |
+
)
|
| 2538 |
+
|
| 2539 |
+
|
| 2540 |
+
def run_camera_trajectory_render(args: argparse.Namespace) -> None:
|
| 2541 |
+
"""Render a full-pose camera trajectory from parsed CLI arguments."""
|
| 2542 |
+
frames = load_camera_trajectory_csv(args.trajectory_csv)
|
| 2543 |
+
if not frames:
|
| 2544 |
+
raise ValueError(f"No frames found in {args.trajectory_csv}.")
|
| 2545 |
+
|
| 2546 |
+
first_frame = frames[0]
|
| 2547 |
+
renderer = RenderUsd(
|
| 2548 |
+
usd_path=args.usd_path,
|
| 2549 |
+
glb_path=args.glb_path,
|
| 2550 |
+
glb_xyz=args.glb_xyz,
|
| 2551 |
+
glb_rotation_deg=args.glb_rotation_deg,
|
| 2552 |
+
output_dir=args.output_dir,
|
| 2553 |
+
render_passes=args.render_passes,
|
| 2554 |
+
depth_mode=args.depth_mode,
|
| 2555 |
+
depth_max=args.depth_max,
|
| 2556 |
+
resolution=args.resolution,
|
| 2557 |
+
samples=args.samples,
|
| 2558 |
+
camera_xyz=first_frame.xyz,
|
| 2559 |
+
camera_rotation_deg=first_frame.rotation_deg,
|
| 2560 |
+
flow_camera_xyz=args.flow_camera_xyz,
|
| 2561 |
+
flow_camera_rotation_deg=args.flow_camera_rotation_deg,
|
| 2562 |
+
focal_length_mm=args.focal_length_mm,
|
| 2563 |
+
exposure=args.exposure,
|
| 2564 |
+
world_strength=args.world_strength,
|
| 2565 |
+
fill_light_energy=args.fill_light_energy,
|
| 2566 |
+
remove_doors=args.remove_doors,
|
| 2567 |
+
)
|
| 2568 |
+
renderer.run_camera_trajectory(
|
| 2569 |
+
frames=frames,
|
| 2570 |
+
glb_offset_xyz=tuple(args.glb_offset_xyz),
|
| 2571 |
+
glb_base_rotation_deg=tuple(args.glb_base_rotation_deg),
|
| 2572 |
+
frame_stride=args.frame_stride,
|
| 2573 |
+
frame_indices=args.frame_indices,
|
| 2574 |
+
make_video=args.make_video,
|
| 2575 |
+
video_fps=resolve_csv_video_fps(
|
| 2576 |
+
args.video_fps, frames, args.frame_stride
|
| 2577 |
+
),
|
| 2578 |
+
overwrite=args.overwrite,
|
| 2579 |
+
)
|
| 2580 |
+
|
| 2581 |
|
| 2582 |
def main() -> None:
|
| 2583 |
logging.basicConfig(level=logging.INFO)
|
| 2584 |
args = _parse_args()
|
| 2585 |
+
if args.trajectory_json is not None:
|
| 2586 |
+
run_trajectory_render(args)
|
| 2587 |
+
return
|
| 2588 |
+
if args.trajectory_csv is not None:
|
| 2589 |
+
run_camera_trajectory_render(args)
|
| 2590 |
+
return
|
| 2591 |
+
|
| 2592 |
+
if args.camera_xyz is None or args.camera_rotation_deg is None:
|
| 2593 |
+
build_arg_parser().error(
|
| 2594 |
+
"--camera_xyz and --camera_rotation_deg are required unless "
|
| 2595 |
+
"--trajectory_json or --trajectory_csv is given."
|
| 2596 |
+
)
|
| 2597 |
RenderUsd.from_args(args).run()
|
| 2598 |
|
| 2599 |
|
embodied_gen/scripts/room_gen/route_room.py
CHANGED
|
@@ -113,6 +113,11 @@ class InfinigenGenRouter:
|
|
| 113 |
"""
|
| 114 |
filled_prompt = self.prompt.format(prompt=task_description)
|
| 115 |
response_text = self.gpt_client.query(text_prompt=filled_prompt)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
parsed = json_repair.loads(response_text)
|
| 117 |
|
| 118 |
room_list = parsed[0]
|
|
|
|
| 113 |
"""
|
| 114 |
filled_prompt = self.prompt.format(prompt=task_description)
|
| 115 |
response_text = self.gpt_client.query(text_prompt=filled_prompt)
|
| 116 |
+
if response_text is None:
|
| 117 |
+
raise RuntimeError(
|
| 118 |
+
"GPT router got no response. Check the GPT agent setup in "
|
| 119 |
+
"embodied_gen/utils/gpt_config.yaml and network access."
|
| 120 |
+
)
|
| 121 |
parsed = json_repair.loads(response_text)
|
| 122 |
|
| 123 |
room_list = parsed[0]
|
embodied_gen/scripts/textto3d.py
CHANGED
|
@@ -17,6 +17,7 @@
|
|
| 17 |
import argparse
|
| 18 |
import os
|
| 19 |
import random
|
|
|
|
| 20 |
from collections import defaultdict
|
| 21 |
|
| 22 |
import numpy as np
|
|
@@ -25,12 +26,17 @@ from PIL import Image
|
|
| 25 |
from embodied_gen.models.image_comm_model import build_hf_image_pipeline
|
| 26 |
from embodied_gen.models.segment_model import RembgRemover
|
| 27 |
from embodied_gen.models.text_model import PROMPT_APPEND
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
from embodied_gen.scripts.imageto3d import entrypoint as imageto3d_api
|
| 29 |
from embodied_gen.utils.gpt_clients import GPT_CLIENT
|
| 30 |
from embodied_gen.utils.log import logger
|
| 31 |
from embodied_gen.utils.process_media import (
|
| 32 |
check_object_edge_truncated,
|
| 33 |
combine_images_to_grid,
|
|
|
|
| 34 |
render_asset3d,
|
| 35 |
)
|
| 36 |
from embodied_gen.validators.quality_checkers import (
|
|
@@ -43,12 +49,32 @@ from embodied_gen.validators.quality_checkers import (
|
|
| 43 |
os.environ["TOKENIZERS_PARALLELISM"] = "false"
|
| 44 |
random.seed(0)
|
| 45 |
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
SEG_CHECKER = ImageSegChecker(GPT_CLIENT)
|
| 49 |
TXTGEN_CHECKER = TextGenAlignChecker(GPT_CLIENT)
|
| 50 |
-
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
|
| 53 |
|
| 54 |
__all__ = [
|
|
@@ -66,6 +92,7 @@ def text_to_image(
|
|
| 66 |
image_hw: tuple[int, int] = (1024, 1024),
|
| 67 |
seed: int = None,
|
| 68 |
) -> bool:
|
|
|
|
| 69 |
select_image = None
|
| 70 |
success_flag = False
|
| 71 |
assert save_path.endswith(".png"), "Image save path must end with `.png`."
|
|
@@ -130,48 +157,115 @@ def text_to_3d(**kwargs) -> dict:
|
|
| 130 |
if hasattr(args, k) and v is not None:
|
| 131 |
setattr(args, k, v)
|
| 132 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
if args.asset_names is None or len(args.asset_names) == 0:
|
| 134 |
args.asset_names = [f"sample3d_{i}" for i in range(len(args.prompts))]
|
| 135 |
-
img_save_dir = os.path.join(args.output_root, "images")
|
| 136 |
asset_save_dir = os.path.join(args.output_root, "asset3d")
|
| 137 |
-
os.makedirs(img_save_dir, exist_ok=True)
|
| 138 |
os.makedirs(asset_save_dir, exist_ok=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
results = defaultdict(dict)
|
| 140 |
-
for prompt, node in zip(args.prompts, args.asset_names):
|
| 141 |
success_flag = False
|
| 142 |
n_pipe_retry = args.n_pipe_retry
|
| 143 |
seed_img = args.seed_img
|
| 144 |
seed_3d = args.seed_3d
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
while success_flag is False and n_pipe_retry > 0:
|
| 146 |
logger.info(
|
| 147 |
f"GEN pipeline for node {node}\n"
|
| 148 |
-
f"Try round: {args.n_pipe_retry-n_pipe_retry+1}/{args.n_pipe_retry}, Prompt: {prompt}"
|
| 149 |
)
|
| 150 |
-
# Text-to-image GEN
|
| 151 |
save_node = node.replace(" ", "_")
|
| 152 |
-
gen_image_path = f"{img_save_dir}/{save_node}.png"
|
| 153 |
-
textgen_flag = text_to_image(
|
| 154 |
-
prompt,
|
| 155 |
-
gen_image_path,
|
| 156 |
-
args.n_image_retry,
|
| 157 |
-
args.img_denoise_step,
|
| 158 |
-
args.text_guidance_scale,
|
| 159 |
-
args.n_img_sample,
|
| 160 |
-
seed=seed_img,
|
| 161 |
-
)
|
| 162 |
-
|
| 163 |
-
# Asset 3D GEN
|
| 164 |
node_save_dir = f"{asset_save_dir}/{save_node}"
|
| 165 |
asset_type = node if "sample3d_" not in node else None
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
mesh_path = f"{node_save_dir}/result/mesh/{save_node}.obj"
|
| 176 |
image_path = render_asset3d(
|
| 177 |
mesh_path,
|
|
@@ -182,6 +276,21 @@ def text_to_3d(**kwargs) -> dict:
|
|
| 182 |
no_index_file=True,
|
| 183 |
)
|
| 184 |
image_path = combine_images_to_grid(image_path)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
check_text = asset_type if asset_type is not None else prompt
|
| 186 |
qa_flag, qa_result = TXTGEN_CHECKER(check_text, image_path)
|
| 187 |
logger.warning(
|
|
@@ -272,6 +381,18 @@ def parse_args():
|
|
| 272 |
)
|
| 273 |
parser.add_argument("--keep_intermediate", action="store_true")
|
| 274 |
parser.add_argument("--disable_decompose_convex", action="store_true")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 275 |
|
| 276 |
args, unknown = parser.parse_known_args()
|
| 277 |
|
|
|
|
| 17 |
import argparse
|
| 18 |
import os
|
| 19 |
import random
|
| 20 |
+
import types
|
| 21 |
from collections import defaultdict
|
| 22 |
|
| 23 |
import numpy as np
|
|
|
|
| 26 |
from embodied_gen.models.image_comm_model import build_hf_image_pipeline
|
| 27 |
from embodied_gen.models.segment_model import RembgRemover
|
| 28 |
from embodied_gen.models.text_model import PROMPT_APPEND
|
| 29 |
+
from embodied_gen.scripts.imageto3d import (
|
| 30 |
+
IMAGE3D_MODEL,
|
| 31 |
+
SUPPORTED_IMAGE3D_MODELS,
|
| 32 |
+
)
|
| 33 |
from embodied_gen.scripts.imageto3d import entrypoint as imageto3d_api
|
| 34 |
from embodied_gen.utils.gpt_clients import GPT_CLIENT
|
| 35 |
from embodied_gen.utils.log import logger
|
| 36 |
from embodied_gen.utils.process_media import (
|
| 37 |
check_object_edge_truncated,
|
| 38 |
combine_images_to_grid,
|
| 39 |
+
parse_text_prompts,
|
| 40 |
render_asset3d,
|
| 41 |
)
|
| 42 |
from embodied_gen.validators.quality_checkers import (
|
|
|
|
| 49 |
os.environ["TOKENIZERS_PARALLELISM"] = "false"
|
| 50 |
random.seed(0)
|
| 51 |
|
| 52 |
+
# TXTGEN_CHECKER drives the final text↔3D quality gate for every backend
|
| 53 |
+
# (SAM3D / TRELLIS / HUNYUAN3D), so it stays eager.
|
|
|
|
| 54 |
TXTGEN_CHECKER = TextGenAlignChecker(GPT_CLIENT)
|
| 55 |
+
|
| 56 |
+
# The text-to-image stack (PIPE_IMG, BG_REMOVER, SEMANTIC_CHECKER, SEG_CHECKER)
|
| 57 |
+
# is only used on the SAM3D / TRELLIS path. HUNYUAN3D goes directly from prompt to 3D
|
| 58 |
+
SEMANTIC_CHECKER = None
|
| 59 |
+
SEG_CHECKER = None
|
| 60 |
+
PIPE_IMG = None
|
| 61 |
+
BG_REMOVER = None
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def _ensure_text2img_stack() -> None:
|
| 65 |
+
"""Construct the text-to-image pipeline + image-stage checkers once.
|
| 66 |
+
|
| 67 |
+
Called from the SAM3D / TRELLIS path before any ``text_to_image`` run.
|
| 68 |
+
Idempotent: subsequent calls return immediately.
|
| 69 |
+
"""
|
| 70 |
+
global SEMANTIC_CHECKER, SEG_CHECKER, PIPE_IMG, BG_REMOVER
|
| 71 |
+
if PIPE_IMG is not None:
|
| 72 |
+
return
|
| 73 |
+
logger.info("Loading TEXT2IMG_MODEL...")
|
| 74 |
+
SEMANTIC_CHECKER = SemanticConsistChecker(GPT_CLIENT)
|
| 75 |
+
SEG_CHECKER = ImageSegChecker(GPT_CLIENT)
|
| 76 |
+
PIPE_IMG = build_hf_image_pipeline(os.environ.get("TEXT_MODEL", "sd35"))
|
| 77 |
+
BG_REMOVER = RembgRemover()
|
| 78 |
|
| 79 |
|
| 80 |
__all__ = [
|
|
|
|
| 92 |
image_hw: tuple[int, int] = (1024, 1024),
|
| 93 |
seed: int = None,
|
| 94 |
) -> bool:
|
| 95 |
+
_ensure_text2img_stack()
|
| 96 |
select_image = None
|
| 97 |
success_flag = False
|
| 98 |
assert save_path.endswith(".png"), "Image save path must end with `.png`."
|
|
|
|
| 157 |
if hasattr(args, k) and v is not None:
|
| 158 |
setattr(args, k, v)
|
| 159 |
|
| 160 |
+
args.image3d_model = str(args.image3d_model).strip().upper()
|
| 161 |
+
if args.image3d_model not in SUPPORTED_IMAGE3D_MODELS:
|
| 162 |
+
raise ValueError(
|
| 163 |
+
f"Unsupported --image3d_model {args.image3d_model!r}; "
|
| 164 |
+
f"expected one of {SUPPORTED_IMAGE3D_MODELS}."
|
| 165 |
+
)
|
| 166 |
+
|
| 167 |
+
hunyuan_cfg = None
|
| 168 |
+
hunyuan_creds = None
|
| 169 |
+
process_prompt = None
|
| 170 |
+
if args.image3d_model == "HUNYUAN3D":
|
| 171 |
+
from embodied_gen.models.hunyuan3d import (
|
| 172 |
+
HunyuanConfig,
|
| 173 |
+
load_credentials,
|
| 174 |
+
process_prompt,
|
| 175 |
+
)
|
| 176 |
+
|
| 177 |
+
# Fail fast on missing creds before any network I/O.
|
| 178 |
+
hunyuan_creds = load_credentials()
|
| 179 |
+
hunyuan_cfg = HunyuanConfig()
|
| 180 |
+
logger.info(
|
| 181 |
+
"HUNYUAN3D text-to-3D backend: action=%s host=%s result_format=%s",
|
| 182 |
+
hunyuan_cfg.image_action,
|
| 183 |
+
hunyuan_cfg.host,
|
| 184 |
+
hunyuan_cfg.result_format,
|
| 185 |
+
)
|
| 186 |
+
|
| 187 |
+
args.prompts = parse_text_prompts(args.prompts)
|
| 188 |
if args.asset_names is None or len(args.asset_names) == 0:
|
| 189 |
args.asset_names = [f"sample3d_{i}" for i in range(len(args.prompts))]
|
|
|
|
| 190 |
asset_save_dir = os.path.join(args.output_root, "asset3d")
|
|
|
|
| 191 |
os.makedirs(asset_save_dir, exist_ok=True)
|
| 192 |
+
# HUNYUAN3D path skips text-to-image entirely; the images/ dir only
|
| 193 |
+
# exists when the local SAM3D / TRELLIS pipeline produces conditioning
|
| 194 |
+
# images.
|
| 195 |
+
img_save_dir = os.path.join(args.output_root, "images")
|
| 196 |
+
if args.image3d_model != "HUNYUAN3D":
|
| 197 |
+
os.makedirs(img_save_dir, exist_ok=True)
|
| 198 |
results = defaultdict(dict)
|
| 199 |
+
for idx, (prompt, node) in enumerate(zip(args.prompts, args.asset_names)):
|
| 200 |
success_flag = False
|
| 201 |
n_pipe_retry = args.n_pipe_retry
|
| 202 |
seed_img = args.seed_img
|
| 203 |
seed_3d = args.seed_3d
|
| 204 |
+
# Tencent Pro API is charged per submit; force a single attempt to
|
| 205 |
+
# avoid silently multiplying cost when --n_pipe_retry > 1.
|
| 206 |
+
if args.image3d_model == "HUNYUAN3D" and n_pipe_retry > 1:
|
| 207 |
+
logger.warning(
|
| 208 |
+
"HUNYUAN3D mode: --n_pipe_retry forced to 1 (Tencent API "
|
| 209 |
+
"is charged per submit); user passed %d.",
|
| 210 |
+
n_pipe_retry,
|
| 211 |
+
)
|
| 212 |
+
n_pipe_retry = 1
|
| 213 |
while success_flag is False and n_pipe_retry > 0:
|
| 214 |
logger.info(
|
| 215 |
f"GEN pipeline for node {node}\n"
|
| 216 |
+
f"Try round: {args.n_pipe_retry - n_pipe_retry + 1}/{args.n_pipe_retry}, Prompt: {prompt}"
|
| 217 |
)
|
|
|
|
| 218 |
save_node = node.replace(" ", "_")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
node_save_dir = f"{asset_save_dir}/{save_node}"
|
| 220 |
asset_type = node if "sample3d_" not in node else None
|
| 221 |
+
|
| 222 |
+
if args.image3d_model == "HUNYUAN3D":
|
| 223 |
+
hunyuan_args = types.SimpleNamespace(
|
| 224 |
+
asset_type=[asset_type],
|
| 225 |
+
version=None,
|
| 226 |
+
height_range=None,
|
| 227 |
+
mass_range=None,
|
| 228 |
+
disable_decompose_convex=args.disable_decompose_convex,
|
| 229 |
+
keep_intermediate=args.keep_intermediate,
|
| 230 |
+
)
|
| 231 |
+
process_prompt(
|
| 232 |
+
args=hunyuan_args,
|
| 233 |
+
idx=0,
|
| 234 |
+
prompt=prompt,
|
| 235 |
+
output_root=node_save_dir,
|
| 236 |
+
filename=save_node,
|
| 237 |
+
hunyuan_config=hunyuan_cfg,
|
| 238 |
+
hunyuan_credentials=hunyuan_creds,
|
| 239 |
+
checkers=[],
|
| 240 |
+
)
|
| 241 |
+
else:
|
| 242 |
+
# Text-to-image GEN (SAM3D / TRELLIS path).
|
| 243 |
+
gen_image_path = f"{img_save_dir}/{save_node}.png"
|
| 244 |
+
text_to_image(
|
| 245 |
+
prompt,
|
| 246 |
+
gen_image_path,
|
| 247 |
+
args.n_image_retry,
|
| 248 |
+
args.img_denoise_step,
|
| 249 |
+
args.text_guidance_scale,
|
| 250 |
+
args.n_img_sample,
|
| 251 |
+
seed=seed_img,
|
| 252 |
+
)
|
| 253 |
+
|
| 254 |
+
# Asset 3D GEN
|
| 255 |
+
imageto3d_api(
|
| 256 |
+
image_path=[gen_image_path],
|
| 257 |
+
output_root=node_save_dir,
|
| 258 |
+
asset_type=[asset_type],
|
| 259 |
+
seed=(
|
| 260 |
+
random.randint(0, 100000)
|
| 261 |
+
if seed_3d is None
|
| 262 |
+
else seed_3d
|
| 263 |
+
),
|
| 264 |
+
n_retry=args.n_asset_retry,
|
| 265 |
+
keep_intermediate=args.keep_intermediate,
|
| 266 |
+
disable_decompose_convex=args.disable_decompose_convex,
|
| 267 |
+
image3d_model=args.image3d_model,
|
| 268 |
+
)
|
| 269 |
mesh_path = f"{node_save_dir}/result/mesh/{save_node}.obj"
|
| 270 |
image_path = render_asset3d(
|
| 271 |
mesh_path,
|
|
|
|
| 276 |
no_index_file=True,
|
| 277 |
)
|
| 278 |
image_path = combine_images_to_grid(image_path)
|
| 279 |
+
if not image_path:
|
| 280 |
+
logger.error(
|
| 281 |
+
f"Node {node}: 3D asset generation produced no mesh, "
|
| 282 |
+
f"skip QA and retry round."
|
| 283 |
+
)
|
| 284 |
+
results["assets"][node] = f"asset3d/{save_node}/result"
|
| 285 |
+
results["quality"][node] = "NO: mesh generation failed."
|
| 286 |
+
n_pipe_retry -= 1
|
| 287 |
+
seed_img = (
|
| 288 |
+
random.randint(0, 100000) if seed_img is not None else None
|
| 289 |
+
)
|
| 290 |
+
seed_3d = (
|
| 291 |
+
random.randint(0, 100000) if seed_3d is not None else None
|
| 292 |
+
)
|
| 293 |
+
continue
|
| 294 |
check_text = asset_type if asset_type is not None else prompt
|
| 295 |
qa_flag, qa_result = TXTGEN_CHECKER(check_text, image_path)
|
| 296 |
logger.warning(
|
|
|
|
| 381 |
)
|
| 382 |
parser.add_argument("--keep_intermediate", action="store_true")
|
| 383 |
parser.add_argument("--disable_decompose_convex", action="store_true")
|
| 384 |
+
parser.add_argument(
|
| 385 |
+
"--image3d_model",
|
| 386 |
+
type=str,
|
| 387 |
+
default=IMAGE3D_MODEL,
|
| 388 |
+
help=(
|
| 389 |
+
"Image-to-3D backend selector forwarded to imageto3d. One of "
|
| 390 |
+
f"{', '.join(SUPPORTED_IMAGE3D_MODELS)} (case-insensitive). "
|
| 391 |
+
"HUNYUAN3D skips the text-to-image stage entirely and calls "
|
| 392 |
+
"Tencent Hunyuan3D Pro text-to-3D directly; it requires "
|
| 393 |
+
"TENCENT_SECRET_ID/TENCENT_SECRET_KEY in the environment."
|
| 394 |
+
),
|
| 395 |
+
)
|
| 396 |
|
| 397 |
args, unknown = parser.parse_known_args()
|
| 398 |
|
embodied_gen/skills/README.md
CHANGED
|
@@ -4,7 +4,7 @@ This directory is the canonical home for EmbodiedGen reusable skills.
|
|
| 4 |
|
| 5 |
The root of `embodied_gen/skills` only contains generic skill source.
|
| 6 |
Runtime-specific packaging should live in adapter subdirectories such as
|
| 7 |
-
`embodied_gen/skills/
|
| 8 |
|
| 9 |
## Included generic skills
|
| 10 |
|
|
@@ -14,7 +14,7 @@ Runtime-specific packaging should live in adapter subdirectories such as
|
|
| 14 |
- `layout-creator`
|
| 15 |
- `sim-runner`
|
| 16 |
- `asset-converter`
|
| 17 |
-
- `asset-
|
| 18 |
- `room-creator`
|
| 19 |
- `spatial-computing`
|
| 20 |
|
|
@@ -23,31 +23,31 @@ Runtime-specific packaging should live in adapter subdirectories such as
|
|
| 23 |
Claude-compatible slash commands and plugin manifest are under:
|
| 24 |
|
| 25 |
```text
|
| 26 |
-
embodied_gen/skills/
|
| 27 |
```
|
| 28 |
|
| 29 |
The local marketplace manifest is:
|
| 30 |
|
| 31 |
```text
|
| 32 |
-
embodied_gen/skills/
|
| 33 |
```
|
| 34 |
|
| 35 |
The actual Claude plugin package is:
|
| 36 |
|
| 37 |
```text
|
| 38 |
-
embodied_gen/skills/
|
| 39 |
```
|
| 40 |
|
| 41 |
Current commands include:
|
| 42 |
|
| 43 |
-
- `/embodiedgen
|
| 44 |
-
- `/embodiedgen
|
| 45 |
-
- `/embodiedgen
|
| 46 |
-
- `/embodiedgen
|
| 47 |
-
- `/embodiedgen
|
| 48 |
-
- `/embodiedgen
|
| 49 |
-
- `/embodiedgen
|
| 50 |
-
- `/embodiedgen
|
| 51 |
|
| 52 |
## Local install for Claude
|
| 53 |
|
|
@@ -58,5 +58,5 @@ bash install/install_agent_plugin.sh
|
|
| 58 |
## Notes
|
| 59 |
|
| 60 |
- Generic skills stay in their original directories under `embodied_gen/skills/`.
|
| 61 |
-
- Claude-specific files live only under `embodied_gen/skills/
|
| 62 |
- This keeps the skill source portable for Codex, Copilot, and other runtimes.
|
|
|
|
| 4 |
|
| 5 |
The root of `embodied_gen/skills` only contains generic skill source.
|
| 6 |
Runtime-specific packaging should live in adapter subdirectories such as
|
| 7 |
+
`embodied_gen/skills/claude_plugin/`.
|
| 8 |
|
| 9 |
## Included generic skills
|
| 10 |
|
|
|
|
| 14 |
- `layout-creator`
|
| 15 |
- `sim-runner`
|
| 16 |
- `asset-converter`
|
| 17 |
+
- `asset-process`
|
| 18 |
- `room-creator`
|
| 19 |
- `spatial-computing`
|
| 20 |
|
|
|
|
| 23 |
Claude-compatible slash commands and plugin manifest are under:
|
| 24 |
|
| 25 |
```text
|
| 26 |
+
embodied_gen/skills/claude_plugin/
|
| 27 |
```
|
| 28 |
|
| 29 |
The local marketplace manifest is:
|
| 30 |
|
| 31 |
```text
|
| 32 |
+
embodied_gen/skills/claude_plugin/.claude-plugin/marketplace.json
|
| 33 |
```
|
| 34 |
|
| 35 |
The actual Claude plugin package is:
|
| 36 |
|
| 37 |
```text
|
| 38 |
+
embodied_gen/skills/claude_plugin/plugins/embodiedgen/
|
| 39 |
```
|
| 40 |
|
| 41 |
Current commands include:
|
| 42 |
|
| 43 |
+
- `/embodiedgen:gen_assets`
|
| 44 |
+
- `/embodiedgen:gen_bg`
|
| 45 |
+
- `/embodiedgen:gen_layout`
|
| 46 |
+
- `/embodiedgen:sim`
|
| 47 |
+
- `/embodiedgen:convert`
|
| 48 |
+
- `/embodiedgen:process`
|
| 49 |
+
- `/embodiedgen:gen_indoor`
|
| 50 |
+
- `/embodiedgen:vibe3d`
|
| 51 |
|
| 52 |
## Local install for Claude
|
| 53 |
|
|
|
|
| 58 |
## Notes
|
| 59 |
|
| 60 |
- Generic skills stay in their original directories under `embodied_gen/skills/`.
|
| 61 |
+
- Claude-specific files live only under `embodied_gen/skills/claude_plugin/`.
|
| 62 |
- This keeps the skill source portable for Codex, Copilot, and other runtimes.
|
embodied_gen/skills/asset-process/SKILL.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: asset-process
|
| 3 |
+
description: Scale and rotate complete URDF-based 3D assets, including OBJ, GLB, collision meshes, Gaussian splats, and height metadata. Use this skill whenever a user asks to resize, rotate, reorient, or otherwise transform an EmbodiedGen asset while keeping its related files synchronized.
|
| 4 |
+
---
|
| 5 |
+
|
| 6 |
+
# Asset Processing
|
| 7 |
+
|
| 8 |
+
## Overview
|
| 9 |
+
|
| 10 |
+
Use this skill to scale or rotate an asset and all of its supported geometry
|
| 11 |
+
files consistently. Rotation is provided as XYZ Euler angles in degrees and is
|
| 12 |
+
applied around the asset's local origin.
|
| 13 |
+
|
| 14 |
+
The input URDF may use either supported asset layout:
|
| 15 |
+
|
| 16 |
+
```text
|
| 17 |
+
<asset_dir>/result/<asset_name>.urdf
|
| 18 |
+
<asset_dir>/<asset_name>.urdf
|
| 19 |
+
```
|
| 20 |
+
|
| 21 |
+
The mesh directory must be beside the URDF as `mesh/`.
|
| 22 |
+
|
| 23 |
+
Normal mode copies the complete asset directly to `<output_dir>`. Pass the
|
| 24 |
+
full target asset directory, including the desired asset directory name.
|
| 25 |
+
Inplace mode modifies the source asset.
|
| 26 |
+
|
| 27 |
+
After processing, the tool calculates the transformed mesh's Y-axis extent and
|
| 28 |
+
writes that value to `min_height`, `max_height`, and `real_height` in the URDF.
|
| 29 |
+
It uses the main OBJ first, then the GLB, then the collision OBJ.
|
| 30 |
+
|
| 31 |
+
## CLI Examples
|
| 32 |
+
|
| 33 |
+
### Scale Only
|
| 34 |
+
|
| 35 |
+
```bash
|
| 36 |
+
python -m embodied_gen.skills.asset-process.asset_process \
|
| 37 |
+
--urdf-path outputs/assets/red_box/result/red_box.urdf \
|
| 38 |
+
--scale-factor 0.8 \
|
| 39 |
+
--output-dir outputs/processed
|
| 40 |
+
```
|
| 41 |
+
|
| 42 |
+
### Rotate Only
|
| 43 |
+
|
| 44 |
+
```bash
|
| 45 |
+
python -m embodied_gen.skills.asset-process.asset_process \
|
| 46 |
+
--urdf-path outputs/assets/red_box/result/red_box.urdf \
|
| 47 |
+
--rot-xyz 90 0 45 \
|
| 48 |
+
--output-dir outputs/processed
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
### Scale and Rotate
|
| 52 |
+
|
| 53 |
+
```bash
|
| 54 |
+
python -m embodied_gen.skills.asset-process.asset_process \
|
| 55 |
+
--urdf-path outputs/test_wuwen/facial_cleanser_001/facial_cleanser_001.urdf \
|
| 56 |
+
--scale-factor 1.0 \
|
| 57 |
+
--rot-xyz 0 90 0 \
|
| 58 |
+
--keep-urdf-raw-rot \
|
| 59 |
+
--output-dir outputs/test_wuwen/facial_cleanser_001_processed
|
| 60 |
+
```
|
| 61 |
+
|
| 62 |
+
`--keep-urdf-raw-rot` is optional. When enabled, the inverse baked rotation is
|
| 63 |
+
composed into visual and collision `origin rpy` values so their effective
|
| 64 |
+
rotation remains unchanged. It is disabled by default.
|
| 65 |
+
|
| 66 |
+
### Process Inplace
|
| 67 |
+
|
| 68 |
+
```bash
|
| 69 |
+
python -m embodied_gen.skills.asset-process.asset_process \
|
| 70 |
+
--urdf-path outputs/assets/red_box/result/red_box.urdf \
|
| 71 |
+
--scale-factor 0.8 \
|
| 72 |
+
--rot-xyz 0 0 90 \
|
| 73 |
+
--inplace
|
| 74 |
+
```
|
| 75 |
+
|
| 76 |
+
Inplace mode modifies the original asset files.
|
| 77 |
+
|
| 78 |
+
## Parameters
|
| 79 |
+
|
| 80 |
+
| Parameter | Default | Meaning |
|
| 81 |
+
|-----------|---------|---------|
|
| 82 |
+
| `urdf_path` | Required | Input URDF path |
|
| 83 |
+
| `scale_factor` | `1.0` | Positive uniform scaling factor |
|
| 84 |
+
| `rot_xyz` | `0 0 0` | XYZ roll, pitch, yaw in degrees |
|
| 85 |
+
| `keep_urdf_raw_rot` | `False` | Preserve the original effective URDF rotation |
|
| 86 |
+
| `inplace` | `False` | Modify source files directly |
|
| 87 |
+
| `output_dir` | `None` | Complete target asset directory; required outside inplace mode |
|
| 88 |
+
|
| 89 |
+
## Processed Files
|
| 90 |
+
|
| 91 |
+
- `mesh/<name>.obj`
|
| 92 |
+
- `mesh/<name>.glb`
|
| 93 |
+
- `mesh/<name>_collision.obj`
|
| 94 |
+
- `mesh/<name>_gs.ply` (optional; skipped when absent)
|
| 95 |
+
- `<name>.urdf`
|
| 96 |
+
|
| 97 |
+
Rotation is always baked into the asset files. URDF visual and collision
|
| 98 |
+
`origin rpy` values are updated only when `keep_urdf_raw_rot` is enabled.
|
| 99 |
+
|
| 100 |
+
For Python API details, read
|
| 101 |
+
`embodied_gen/skills/asset-process/asset_process.py`.
|
embodied_gen/skills/asset-process/__init__.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Project EmbodiedGen
|
| 2 |
+
#
|
| 3 |
+
# Copyright (c) 2025 Horizon Robotics. All Rights Reserved.
|
| 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
|
| 14 |
+
# implied. See the License for the specific language governing
|
| 15 |
+
# permissions and limitations under the License.
|
| 16 |
+
|
| 17 |
+
"""Asset processing skills for EmbodiedGen."""
|
| 18 |
+
|
| 19 |
+
from importlib import import_module
|
| 20 |
+
|
| 21 |
+
__all__ = [
|
| 22 |
+
"AssetProcessConfig",
|
| 23 |
+
"AssetProcessor",
|
| 24 |
+
"entrypoint",
|
| 25 |
+
"process_asset",
|
| 26 |
+
]
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def __getattr__(name: str) -> object:
|
| 30 |
+
"""Lazily expose asset processing APIs without import side effects."""
|
| 31 |
+
if name not in __all__:
|
| 32 |
+
msg = f"module {__name__!r} has no attribute {name!r}"
|
| 33 |
+
raise AttributeError(msg)
|
| 34 |
+
|
| 35 |
+
module = import_module(f"{__name__}.asset_process")
|
| 36 |
+
return getattr(module, name)
|
embodied_gen/skills/asset-process/asset_process.py
ADDED
|
@@ -0,0 +1,443 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Project EmbodiedGen
|
| 2 |
+
#
|
| 3 |
+
# Copyright (c) 2025 Horizon Robotics. All Rights Reserved.
|
| 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
|
| 14 |
+
# implied. See the License for the specific language governing
|
| 15 |
+
# permissions and limitations under the License.
|
| 16 |
+
|
| 17 |
+
"""Asset processing utilities for scaling and rotating 3D assets."""
|
| 18 |
+
|
| 19 |
+
import math
|
| 20 |
+
import shutil
|
| 21 |
+
import warnings
|
| 22 |
+
import xml.etree.ElementTree as ET
|
| 23 |
+
from concurrent.futures import ThreadPoolExecutor
|
| 24 |
+
from dataclasses import dataclass
|
| 25 |
+
from importlib import import_module
|
| 26 |
+
from pathlib import Path
|
| 27 |
+
from typing import Optional
|
| 28 |
+
|
| 29 |
+
import numpy as np
|
| 30 |
+
import trimesh
|
| 31 |
+
import tyro
|
| 32 |
+
from scipy.spatial.transform import Rotation
|
| 33 |
+
from embodied_gen.utils.log import logger
|
| 34 |
+
|
| 35 |
+
__all__ = [
|
| 36 |
+
"AssetProcessConfig",
|
| 37 |
+
"AssetProcessor",
|
| 38 |
+
"entrypoint",
|
| 39 |
+
"process_asset",
|
| 40 |
+
]
|
| 41 |
+
|
| 42 |
+
URDF_HEIGHT_FIELDS = ("min_height", "max_height", "real_height")
|
| 43 |
+
URDF_RESULT_DIR = "result"
|
| 44 |
+
MESH_DIR = "mesh"
|
| 45 |
+
HEIGHT_AXIS = 1
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
@dataclass
|
| 49 |
+
class AssetProcessConfig:
|
| 50 |
+
"""Configuration for asset scaling and rotation.
|
| 51 |
+
|
| 52 |
+
Args:
|
| 53 |
+
urdf_path: Path to the URDF file to process.
|
| 54 |
+
scale_factor: Positive uniform scaling factor.
|
| 55 |
+
rot_xyz: XYZ Euler rotation in degrees.
|
| 56 |
+
keep_urdf_raw_rot: Whether to preserve the original URDF rotation.
|
| 57 |
+
inplace: Whether to modify the source asset directly.
|
| 58 |
+
output_dir: Target asset directory used when ``inplace`` is false.
|
| 59 |
+
"""
|
| 60 |
+
|
| 61 |
+
urdf_path: str
|
| 62 |
+
scale_factor: float = 1.0
|
| 63 |
+
rot_xyz: tuple[float, float, float] = (0.0, 0.0, 0.0)
|
| 64 |
+
keep_urdf_raw_rot: bool = False
|
| 65 |
+
inplace: bool = False
|
| 66 |
+
output_dir: Optional[str] = None
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
class AssetProcessor:
|
| 70 |
+
"""Scale and rotate mesh, collision, and Gaussian-splat asset files."""
|
| 71 |
+
|
| 72 |
+
def __init__(
|
| 73 |
+
self,
|
| 74 |
+
urdf_path: str | Path,
|
| 75 |
+
scale_factor: float = 1.0,
|
| 76 |
+
rot_xyz: tuple[float, float, float] = (0.0, 0.0, 0.0),
|
| 77 |
+
keep_urdf_raw_rot: bool = False,
|
| 78 |
+
output_dir: Optional[str | Path] = None,
|
| 79 |
+
inplace: bool = False,
|
| 80 |
+
) -> None:
|
| 81 |
+
"""Initialize an asset processor.
|
| 82 |
+
|
| 83 |
+
Args:
|
| 84 |
+
urdf_path: Path to the URDF file to process.
|
| 85 |
+
scale_factor: Positive uniform scaling factor.
|
| 86 |
+
rot_xyz: XYZ Euler rotation in degrees.
|
| 87 |
+
keep_urdf_raw_rot: Whether to preserve the original visual and
|
| 88 |
+
collision rotation after baking the asset rotation.
|
| 89 |
+
output_dir: Target asset directory used when ``inplace`` is false.
|
| 90 |
+
inplace: Whether to modify the source asset directly.
|
| 91 |
+
|
| 92 |
+
Raises:
|
| 93 |
+
FileNotFoundError: If the URDF file does not exist.
|
| 94 |
+
ValueError: If the transform or output configuration is invalid.
|
| 95 |
+
"""
|
| 96 |
+
self.urdf_path = Path(urdf_path)
|
| 97 |
+
self.scale_factor = float(scale_factor)
|
| 98 |
+
self.rot_xyz = tuple(float(value) for value in rot_xyz)
|
| 99 |
+
self.keep_urdf_raw_rot = keep_urdf_raw_rot
|
| 100 |
+
self.inplace = inplace
|
| 101 |
+
|
| 102 |
+
if not self.urdf_path.is_file():
|
| 103 |
+
raise FileNotFoundError(f"URDF file not found: {self.urdf_path}")
|
| 104 |
+
if not math.isfinite(self.scale_factor) or self.scale_factor <= 0:
|
| 105 |
+
raise ValueError(
|
| 106 |
+
f"Scale factor must be positive and finite, got: "
|
| 107 |
+
f"{self.scale_factor}"
|
| 108 |
+
)
|
| 109 |
+
if len(self.rot_xyz) != 3 or not all(
|
| 110 |
+
math.isfinite(value) for value in self.rot_xyz
|
| 111 |
+
):
|
| 112 |
+
raise ValueError(
|
| 113 |
+
"rot_xyz must contain three finite degree values, got: "
|
| 114 |
+
f"{self.rot_xyz}"
|
| 115 |
+
)
|
| 116 |
+
|
| 117 |
+
if self.urdf_path.parent.name == URDF_RESULT_DIR:
|
| 118 |
+
self.asset_dir = self.urdf_path.parent.parent
|
| 119 |
+
self.result_dir = Path(URDF_RESULT_DIR)
|
| 120 |
+
else:
|
| 121 |
+
self.asset_dir = self.urdf_path.parent
|
| 122 |
+
self.result_dir = Path()
|
| 123 |
+
self.node_name = self.urdf_path.stem
|
| 124 |
+
self.rotation = Rotation.from_euler("xyz", self.rot_xyz, degrees=True)
|
| 125 |
+
self.rotation_transform = np.eye(4)
|
| 126 |
+
self.rotation_transform[:3, :3] = self.rotation.as_matrix()
|
| 127 |
+
|
| 128 |
+
if self.inplace:
|
| 129 |
+
self.output_dir = self.asset_dir.parent
|
| 130 |
+
else:
|
| 131 |
+
if output_dir is None:
|
| 132 |
+
raise ValueError("output_dir is required when inplace=False")
|
| 133 |
+
self.output_dir = Path(output_dir)
|
| 134 |
+
if self.output_dir.resolve() == self.asset_dir.resolve():
|
| 135 |
+
raise ValueError(
|
| 136 |
+
"output_dir must differ from the source asset directory; "
|
| 137 |
+
"use inplace=True to modify the source asset"
|
| 138 |
+
)
|
| 139 |
+
|
| 140 |
+
def process(self) -> Path:
|
| 141 |
+
"""Run the complete asset processing workflow.
|
| 142 |
+
|
| 143 |
+
Returns:
|
| 144 |
+
Path to the processed URDF file.
|
| 145 |
+
"""
|
| 146 |
+
if self.inplace:
|
| 147 |
+
output_asset_dir = self.asset_dir
|
| 148 |
+
output_urdf_path = self.urdf_path
|
| 149 |
+
else:
|
| 150 |
+
output_asset_dir = self.output_dir
|
| 151 |
+
output_urdf_path = self._copy_asset_structure(output_asset_dir)
|
| 152 |
+
|
| 153 |
+
self._process_asset_files_parallel(output_asset_dir)
|
| 154 |
+
actual_height = self._calculate_actual_height(output_asset_dir)
|
| 155 |
+
self._write_urdf_height(output_urdf_path, actual_height)
|
| 156 |
+
if self.keep_urdf_raw_rot:
|
| 157 |
+
self._update_urdf_origins(output_urdf_path)
|
| 158 |
+
|
| 159 |
+
logger.info(
|
| 160 |
+
"Processed %s with scale=%s, rot_xyz=%s -> %s",
|
| 161 |
+
self.asset_dir,
|
| 162 |
+
self.scale_factor,
|
| 163 |
+
self.rot_xyz,
|
| 164 |
+
output_asset_dir,
|
| 165 |
+
)
|
| 166 |
+
return output_urdf_path
|
| 167 |
+
|
| 168 |
+
def _copy_asset_structure(self, output_asset_dir: Path) -> Path:
|
| 169 |
+
"""Copy the complete source asset to the output directory."""
|
| 170 |
+
shutil.rmtree(output_asset_dir, ignore_errors=True)
|
| 171 |
+
shutil.copytree(self.asset_dir, output_asset_dir)
|
| 172 |
+
return output_asset_dir / self.result_dir / f"{self.node_name}.urdf"
|
| 173 |
+
|
| 174 |
+
def _process_asset_files_parallel(self, output_asset_dir: Path) -> None:
|
| 175 |
+
"""Apply the configured transform to every supported asset format."""
|
| 176 |
+
mesh_dir = output_asset_dir / self.result_dir / MESH_DIR
|
| 177 |
+
|
| 178 |
+
# Keep the legacy asset-scale processing paths separate because each
|
| 179 |
+
# format has different loading and export behavior.
|
| 180 |
+
tasks = [
|
| 181 |
+
(mesh_dir / f"{self.node_name}.obj", self._scale_obj_mesh),
|
| 182 |
+
(mesh_dir / f"{self.node_name}.glb", self._scale_glb_mesh),
|
| 183 |
+
(
|
| 184 |
+
mesh_dir / f"{self.node_name}_collision.obj",
|
| 185 |
+
self._scale_collision_mesh,
|
| 186 |
+
),
|
| 187 |
+
(
|
| 188 |
+
mesh_dir / f"{self.node_name}_gs.ply",
|
| 189 |
+
self._scale_gaussian_splat,
|
| 190 |
+
),
|
| 191 |
+
]
|
| 192 |
+
|
| 193 |
+
with ThreadPoolExecutor(max_workers=4) as executor:
|
| 194 |
+
futures = [executor.submit(task, path) for path, task in tasks]
|
| 195 |
+
for future in futures:
|
| 196 |
+
future.result() # Propagate any exceptions
|
| 197 |
+
|
| 198 |
+
def _scale_obj_mesh(self, mesh_path: Path) -> None:
|
| 199 |
+
"""Scale and rotate an OBJ mesh."""
|
| 200 |
+
if not mesh_path.exists():
|
| 201 |
+
return
|
| 202 |
+
|
| 203 |
+
mesh = trimesh.load(str(mesh_path))
|
| 204 |
+
mesh.apply_scale(self.scale_factor)
|
| 205 |
+
self._apply_mesh_rotation(mesh)
|
| 206 |
+
mesh.export(str(mesh_path))
|
| 207 |
+
|
| 208 |
+
def _scale_glb_mesh(self, mesh_path: Path) -> None:
|
| 209 |
+
"""Scale and rotate every geometry in a GLB scene."""
|
| 210 |
+
if not mesh_path.exists():
|
| 211 |
+
return
|
| 212 |
+
|
| 213 |
+
mesh = trimesh.load(str(mesh_path))
|
| 214 |
+
for mesh_part in mesh.geometry.values():
|
| 215 |
+
mesh_part.apply_scale(self.scale_factor)
|
| 216 |
+
self._apply_mesh_rotation(mesh_part)
|
| 217 |
+
mesh.export(str(mesh_path))
|
| 218 |
+
|
| 219 |
+
def _scale_collision_mesh(self, mesh_path: Path) -> None:
|
| 220 |
+
"""Scale and rotate a potentially multi-object collision OBJ."""
|
| 221 |
+
if not mesh_path.exists():
|
| 222 |
+
return
|
| 223 |
+
|
| 224 |
+
meshes = self._load_collision_obj(str(mesh_path))
|
| 225 |
+
scene = trimesh.Scene()
|
| 226 |
+
for mesh_part in meshes:
|
| 227 |
+
mesh_part.apply_scale(self.scale_factor)
|
| 228 |
+
self._apply_mesh_rotation(mesh_part)
|
| 229 |
+
scene.add_geometry(mesh_part)
|
| 230 |
+
scene.export(str(mesh_path))
|
| 231 |
+
|
| 232 |
+
def _scale_gaussian_splat(self, mesh_path: Path) -> None:
|
| 233 |
+
"""Scale and rotate a Gaussian splatting model."""
|
| 234 |
+
if not mesh_path.exists():
|
| 235 |
+
return
|
| 236 |
+
|
| 237 |
+
gaussian_operator = import_module(
|
| 238 |
+
"embodied_gen.models.gs_model"
|
| 239 |
+
).GaussianOperator
|
| 240 |
+
gs_model = gaussian_operator.load_from_ply(str(mesh_path))
|
| 241 |
+
gs_model.rescale(self.scale_factor)
|
| 242 |
+
if not self._is_identity_rotation:
|
| 243 |
+
instance_pose = gaussian_operator.trans_to_quatpose(
|
| 244 |
+
self.rotation.as_matrix()
|
| 245 |
+
)
|
| 246 |
+
gs_model = gs_model.get_gaussians(instance_pose=instance_pose)
|
| 247 |
+
gs_model.save_to_ply(str(mesh_path))
|
| 248 |
+
|
| 249 |
+
@property
|
| 250 |
+
def _is_identity_rotation(self) -> bool:
|
| 251 |
+
"""Return whether the configured rotation is an identity operation."""
|
| 252 |
+
return np.allclose(self.rotation.as_matrix(), np.eye(3))
|
| 253 |
+
|
| 254 |
+
def _apply_mesh_rotation(self, mesh: trimesh.Trimesh) -> None:
|
| 255 |
+
"""Apply the configured rotation after the legacy scale operation."""
|
| 256 |
+
if self._is_identity_rotation:
|
| 257 |
+
return
|
| 258 |
+
mesh.apply_transform(self.rotation_transform)
|
| 259 |
+
|
| 260 |
+
def _calculate_actual_height(self, output_asset_dir: Path) -> float:
|
| 261 |
+
"""Calculate Y-axis height from the best available transformed mesh."""
|
| 262 |
+
mesh_dir = output_asset_dir / self.result_dir / MESH_DIR
|
| 263 |
+
candidates = (
|
| 264 |
+
mesh_dir / f"{self.node_name}.obj",
|
| 265 |
+
mesh_dir / f"{self.node_name}.glb",
|
| 266 |
+
mesh_dir / f"{self.node_name}_collision.obj",
|
| 267 |
+
)
|
| 268 |
+
for mesh_path in candidates:
|
| 269 |
+
if not mesh_path.exists():
|
| 270 |
+
continue
|
| 271 |
+
scene = trimesh.load(str(mesh_path), force="scene")
|
| 272 |
+
geometry = scene.to_geometry()
|
| 273 |
+
if len(geometry.vertices) == 0:
|
| 274 |
+
continue
|
| 275 |
+
height = float(np.ptp(geometry.vertices[:, HEIGHT_AXIS]))
|
| 276 |
+
if not math.isfinite(height):
|
| 277 |
+
raise ValueError(
|
| 278 |
+
f"Calculated non-finite height from mesh: {mesh_path}"
|
| 279 |
+
)
|
| 280 |
+
return height
|
| 281 |
+
|
| 282 |
+
candidate_text = ", ".join(str(path) for path in candidates)
|
| 283 |
+
raise FileNotFoundError(
|
| 284 |
+
"No mesh is available for height calculation. Expected one of: "
|
| 285 |
+
f"{candidate_text}"
|
| 286 |
+
)
|
| 287 |
+
|
| 288 |
+
@staticmethod
|
| 289 |
+
def _write_urdf_height(urdf_path: Path, height: float) -> None:
|
| 290 |
+
"""Write actual mesh height to all URDF height metadata fields."""
|
| 291 |
+
tree = ET.parse(str(urdf_path))
|
| 292 |
+
root = tree.getroot()
|
| 293 |
+
link = root.find("link")
|
| 294 |
+
if link is None:
|
| 295 |
+
raise ValueError(f"No link element found in URDF: {urdf_path}")
|
| 296 |
+
|
| 297 |
+
extra_info = link.find("extra_info")
|
| 298 |
+
if extra_info is None:
|
| 299 |
+
extra_info = ET.SubElement(link, "extra_info")
|
| 300 |
+
|
| 301 |
+
value = f"{height:.4f}"
|
| 302 |
+
for field in URDF_HEIGHT_FIELDS:
|
| 303 |
+
element = extra_info.find(field)
|
| 304 |
+
if element is None:
|
| 305 |
+
element = ET.SubElement(extra_info, field)
|
| 306 |
+
element.text = value
|
| 307 |
+
|
| 308 |
+
tree.write(str(urdf_path), encoding="utf-8", xml_declaration=True)
|
| 309 |
+
|
| 310 |
+
def _update_urdf_origins(self, urdf_path: Path) -> None:
|
| 311 |
+
"""Append the inverse baked rotation to visual/collision origins."""
|
| 312 |
+
if self._is_identity_rotation:
|
| 313 |
+
return
|
| 314 |
+
|
| 315 |
+
tree = ET.parse(str(urdf_path))
|
| 316 |
+
root = tree.getroot()
|
| 317 |
+
inverse_rotation = self.rotation.inv().as_matrix()
|
| 318 |
+
|
| 319 |
+
for link in root.findall(".//link"):
|
| 320 |
+
for geometry_tag in ("visual", "collision"):
|
| 321 |
+
for geometry in link.findall(geometry_tag):
|
| 322 |
+
origin = geometry.find("origin")
|
| 323 |
+
if origin is None:
|
| 324 |
+
origin = ET.SubElement(
|
| 325 |
+
geometry,
|
| 326 |
+
"origin",
|
| 327 |
+
attrib={"xyz": "0 0 0"},
|
| 328 |
+
)
|
| 329 |
+
|
| 330 |
+
current_rpy = np.fromstring(
|
| 331 |
+
origin.get("rpy", "0 0 0"),
|
| 332 |
+
sep=" ",
|
| 333 |
+
)
|
| 334 |
+
if current_rpy.size != 3:
|
| 335 |
+
raise ValueError(
|
| 336 |
+
f"Invalid rpy in URDF origin: {urdf_path}"
|
| 337 |
+
)
|
| 338 |
+
|
| 339 |
+
current_rotation = Rotation.from_euler(
|
| 340 |
+
"xyz",
|
| 341 |
+
current_rpy,
|
| 342 |
+
degrees=False,
|
| 343 |
+
).as_matrix()
|
| 344 |
+
updated_rotation = current_rotation @ inverse_rotation
|
| 345 |
+
with warnings.catch_warnings():
|
| 346 |
+
warnings.simplefilter("ignore", UserWarning)
|
| 347 |
+
updated_rpy = Rotation.from_matrix(
|
| 348 |
+
updated_rotation
|
| 349 |
+
).as_euler("xyz", degrees=False)
|
| 350 |
+
origin.set(
|
| 351 |
+
"rpy",
|
| 352 |
+
" ".join(f"{value:.4f}" for value in updated_rpy),
|
| 353 |
+
)
|
| 354 |
+
|
| 355 |
+
tree.write(str(urdf_path), encoding="utf-8", xml_declaration=True)
|
| 356 |
+
|
| 357 |
+
@staticmethod
|
| 358 |
+
def _load_collision_obj(filepath: str) -> list[trimesh.Trimesh]:
|
| 359 |
+
"""Robustly load collision OBJ with multiple objects.
|
| 360 |
+
|
| 361 |
+
Handles OBJ files with multiple objects/groups by parsing manually
|
| 362 |
+
to avoid issues with trimesh's default loader.
|
| 363 |
+
|
| 364 |
+
Args:
|
| 365 |
+
filepath: Path to collision OBJ file.
|
| 366 |
+
|
| 367 |
+
Returns:
|
| 368 |
+
List of trimesh objects, one per object group in the file.
|
| 369 |
+
"""
|
| 370 |
+
vertices = []
|
| 371 |
+
meshes = []
|
| 372 |
+
current_faces = []
|
| 373 |
+
|
| 374 |
+
# Use lazy iteration instead of readlines() for memory efficiency
|
| 375 |
+
with open(filepath, "r") as f:
|
| 376 |
+
for line in f:
|
| 377 |
+
if line.startswith("v "):
|
| 378 |
+
parts = line.split()
|
| 379 |
+
vertices.append(
|
| 380 |
+
[float(parts[1]), float(parts[2]), float(parts[3])]
|
| 381 |
+
)
|
| 382 |
+
elif line.startswith("f "):
|
| 383 |
+
parts = line.split()
|
| 384 |
+
face = [int(p.split("/")[0]) - 1 for p in parts[1:]]
|
| 385 |
+
current_faces.append(face)
|
| 386 |
+
elif line.startswith("o ") or line.startswith("g "):
|
| 387 |
+
if current_faces and vertices:
|
| 388 |
+
m = trimesh.Trimesh(
|
| 389 |
+
vertices=vertices,
|
| 390 |
+
faces=current_faces,
|
| 391 |
+
process=False,
|
| 392 |
+
)
|
| 393 |
+
m.remove_unreferenced_vertices()
|
| 394 |
+
meshes.append(m)
|
| 395 |
+
current_faces = []
|
| 396 |
+
|
| 397 |
+
# Flush final mesh
|
| 398 |
+
if current_faces and vertices:
|
| 399 |
+
m = trimesh.Trimesh(
|
| 400 |
+
vertices=vertices, faces=current_faces, process=False
|
| 401 |
+
)
|
| 402 |
+
m.remove_unreferenced_vertices()
|
| 403 |
+
meshes.append(m)
|
| 404 |
+
|
| 405 |
+
return meshes
|
| 406 |
+
|
| 407 |
+
|
| 408 |
+
def process_asset(
|
| 409 |
+
urdf_path: str | Path,
|
| 410 |
+
scale_factor: float = 1.0,
|
| 411 |
+
rot_xyz: tuple[float, float, float] = (0.0, 0.0, 0.0),
|
| 412 |
+
keep_urdf_raw_rot: bool = False,
|
| 413 |
+
output_dir: Optional[str | Path] = None,
|
| 414 |
+
inplace: bool = False,
|
| 415 |
+
) -> Path:
|
| 416 |
+
"""Scale and rotate a complete URDF-based asset."""
|
| 417 |
+
processor = AssetProcessor(
|
| 418 |
+
urdf_path=urdf_path,
|
| 419 |
+
scale_factor=scale_factor,
|
| 420 |
+
rot_xyz=rot_xyz,
|
| 421 |
+
keep_urdf_raw_rot=keep_urdf_raw_rot,
|
| 422 |
+
output_dir=output_dir,
|
| 423 |
+
inplace=inplace,
|
| 424 |
+
)
|
| 425 |
+
return processor.process()
|
| 426 |
+
|
| 427 |
+
|
| 428 |
+
def entrypoint() -> None:
|
| 429 |
+
"""Run the asset processing CLI."""
|
| 430 |
+
config = tyro.cli(AssetProcessConfig)
|
| 431 |
+
output_urdf = process_asset(
|
| 432 |
+
urdf_path=config.urdf_path,
|
| 433 |
+
scale_factor=config.scale_factor,
|
| 434 |
+
rot_xyz=config.rot_xyz,
|
| 435 |
+
keep_urdf_raw_rot=config.keep_urdf_raw_rot,
|
| 436 |
+
output_dir=config.output_dir,
|
| 437 |
+
inplace=config.inplace,
|
| 438 |
+
)
|
| 439 |
+
logger.info(f"Processed asset successfully: {output_urdf}")
|
| 440 |
+
|
| 441 |
+
|
| 442 |
+
if __name__ == "__main__":
|
| 443 |
+
entrypoint()
|
embodied_gen/skills/asset-retrieval/scripts/retrieve_asset.py
CHANGED
|
@@ -1,4 +1,19 @@
|
|
| 1 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
"""Retrieve EmbodiedGen asset URDF paths from a CSV index."""
|
| 3 |
|
| 4 |
from __future__ import annotations
|
|
|
|
| 1 |
+
# Project EmbodiedGen
|
| 2 |
+
#
|
| 3 |
+
# Copyright (c) 2025 Horizon Robotics. All Rights Reserved.
|
| 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
|
| 14 |
+
# implied. See the License for the specific language governing
|
| 15 |
+
# permissions and limitations under the License.
|
| 16 |
+
|
| 17 |
"""Retrieve EmbodiedGen asset URDF paths from a CSV index."""
|
| 18 |
|
| 19 |
from __future__ import annotations
|