Spaces:
Running on A100
Running on A100
Commit ·
42f0ff9
0
Parent(s):
Vendored VoMP with local debug edits; single commit (history rewritten to drop binary blobs for HF Hub).
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +37 -0
- .gitignore +2 -0
- README.md +15 -0
- app.py +561 -0
- deps/vomp/.gitignore +216 -0
- deps/vomp/ATTRIBUTIONS.md +0 -0
- deps/vomp/CONTRIBUTING.md +51 -0
- deps/vomp/LICENSE +201 -0
- deps/vomp/README.md +665 -0
- deps/vomp/README_train.md +285 -0
- deps/vomp/configs/materials/geometry_encoder/train.json +83 -0
- deps/vomp/configs/materials/geometry_encoder/train_encoder_decoder_direct.json +99 -0
- deps/vomp/configs/materials/geometry_encoder/train_encoder_decoder_matvae.json +99 -0
- deps/vomp/configs/materials/geometry_encoder/train_standard.json +83 -0
- deps/vomp/configs/materials/inference.json +86 -0
- deps/vomp/configs/materials/material_vae/beta_tc_final.json +68 -0
- deps/vomp/configs/materials/material_vae/matvae.json +76 -0
- deps/vomp/configs/materials/material_vae/matvae_log_minmax_no_density.json +76 -0
- deps/vomp/configs/materials/material_vae/matvae_no_beta_tc.json +77 -0
- deps/vomp/configs/materials/material_vae/matvae_no_flow.json +77 -0
- deps/vomp/configs/materials/material_vae/matvae_no_free_nats.json +76 -0
- deps/vomp/configs/materials/material_vae/matvae_standard.json +76 -0
- deps/vomp/configs/materials/material_vae/matvae_standard_norm.json +76 -0
- deps/vomp/configs/materials/material_vae/standard_vae_final.json +67 -0
- deps/vomp/configs/sim/armchair_and_orange.json +59 -0
- deps/vomp/configs/sim/falling_armchair.json +48 -0
- deps/vomp/configs/sim/falling_bar_stool.json +50 -0
- deps/vomp/configs/sim/falling_birch.json +50 -0
- deps/vomp/configs/sim/falling_oranges.json +80 -0
- deps/vomp/configs/sim/falling_sphere_soft.json +51 -0
- deps/vomp/configs/sim/zag_and_falling_orange.json +59 -0
- deps/vomp/configs/sim/zag_and_falling_oranges.json +98 -0
- deps/vomp/dataset_toolkits/abo/ABO500.py +204 -0
- deps/vomp/dataset_toolkits/abo/build_metadata.py +108 -0
- deps/vomp/dataset_toolkits/abo/extract_feature.py +381 -0
- deps/vomp/dataset_toolkits/abo/render.py +241 -0
- deps/vomp/dataset_toolkits/abo/voxelize.py +306 -0
- deps/vomp/dataset_toolkits/blender_script/render.py +695 -0
- deps/vomp/dataset_toolkits/build_metadata.py +551 -0
- deps/vomp/dataset_toolkits/datasets/ABO.py +132 -0
- deps/vomp/dataset_toolkits/datasets/__init__.py +16 -0
- deps/vomp/dataset_toolkits/datasets/allmats.py +510 -0
- deps/vomp/dataset_toolkits/datasets/simready.py +297 -0
- deps/vomp/dataset_toolkits/extract_feature.py +273 -0
- deps/vomp/dataset_toolkits/latent_space/analyze_data_distribution.py +111 -0
- deps/vomp/dataset_toolkits/latent_space/make_csv.py +411 -0
- deps/vomp/dataset_toolkits/material_objects/render_usd.py +1176 -0
- deps/vomp/dataset_toolkits/material_objects/vlm_annotations/data_subsets/commercial.py +427 -0
- deps/vomp/dataset_toolkits/material_objects/vlm_annotations/data_subsets/common.py +1457 -0
- deps/vomp/dataset_toolkits/material_objects/vlm_annotations/data_subsets/residential.py +582 -0
.gitattributes
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
*.ply filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
*.whl filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
venv/
|
| 2 |
+
weights/
|
README.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: VoMP
|
| 3 |
+
emoji: 🚀
|
| 4 |
+
colorFrom: green
|
| 5 |
+
colorTo: green
|
| 6 |
+
sdk: gradio
|
| 7 |
+
python_version: 3.12
|
| 8 |
+
sdk_version: 6.2.0
|
| 9 |
+
app_file: app.py
|
| 10 |
+
pinned: true
|
| 11 |
+
license: apache-2.0
|
| 12 |
+
short_description: Volumetric physics materials for interactive worlds
|
| 13 |
+
suggested_hardware: a100-large
|
| 14 |
+
suggested_storage: medium
|
| 15 |
+
---
|
app.py
ADDED
|
@@ -0,0 +1,561 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import glob
|
| 2 |
+
import os
|
| 3 |
+
import shutil
|
| 4 |
+
import tempfile
|
| 5 |
+
from typing import Dict, List, Optional, Tuple
|
| 6 |
+
|
| 7 |
+
import gradio as gr
|
| 8 |
+
import matplotlib
|
| 9 |
+
|
| 10 |
+
matplotlib.use("Agg")
|
| 11 |
+
import matplotlib.pyplot as plt
|
| 12 |
+
import matplotlib.colors as mcolors
|
| 13 |
+
from matplotlib.colorbar import ColorbarBase
|
| 14 |
+
import numpy as np
|
| 15 |
+
import spaces
|
| 16 |
+
import torch
|
| 17 |
+
from huggingface_hub import snapshot_download
|
| 18 |
+
|
| 19 |
+
from vomp.inference import Vomp
|
| 20 |
+
from vomp.inference.utils import save_materials
|
| 21 |
+
|
| 22 |
+
NUM_VIEWS = 150
|
| 23 |
+
PROPERTY_NAMES = ["youngs_modulus", "poissons_ratio", "density"]
|
| 24 |
+
PROPERTY_DISPLAY_NAMES = {
|
| 25 |
+
"youngs_modulus": "Young's Modulus",
|
| 26 |
+
"poissons_ratio": "Poisson's Ratio",
|
| 27 |
+
"density": "Density",
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
BLENDER_LINK = (
|
| 31 |
+
"https://download.blender.org/release/Blender3.0/blender-3.0.1-linux-x64.tar.xz"
|
| 32 |
+
)
|
| 33 |
+
BLENDER_INSTALLATION_PATH = "/tmp"
|
| 34 |
+
BLENDER_PATH = f"{BLENDER_INSTALLATION_PATH}/blender-3.0.1-linux-x64/blender"
|
| 35 |
+
|
| 36 |
+
EXAMPLES_DIR = "examples"
|
| 37 |
+
|
| 38 |
+
model_id = "nvidia/PhysicalAI-Simulation-VoMP-Model"
|
| 39 |
+
base_path = snapshot_download(repo_id=model_id, local_dir="weights")
|
| 40 |
+
print(os.listdir(base_path))
|
| 41 |
+
|
| 42 |
+
def _install_blender():
|
| 43 |
+
if not os.path.exists(BLENDER_PATH):
|
| 44 |
+
print("Installing Blender...")
|
| 45 |
+
os.system("sudo apt-get update")
|
| 46 |
+
os.system(
|
| 47 |
+
"sudo apt-get install -y libxrender1 libxi6 libxkbcommon-x11-0 libsm6"
|
| 48 |
+
)
|
| 49 |
+
os.system(f"wget {BLENDER_LINK} -P {BLENDER_INSTALLATION_PATH}")
|
| 50 |
+
os.system(
|
| 51 |
+
f"tar -xvf {BLENDER_INSTALLATION_PATH}/blender-3.0.1-linux-x64.tar.xz -C {BLENDER_INSTALLATION_PATH}"
|
| 52 |
+
)
|
| 53 |
+
print("Blender installed successfully!")
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def _is_gaussian_splat(file_path: str) -> bool:
|
| 57 |
+
if not file_path.lower().endswith(".ply"):
|
| 58 |
+
return False
|
| 59 |
+
|
| 60 |
+
try:
|
| 61 |
+
with open(file_path, "rb") as f:
|
| 62 |
+
header = b""
|
| 63 |
+
while True:
|
| 64 |
+
line = f.readline()
|
| 65 |
+
header += line
|
| 66 |
+
if b"end_header" in line:
|
| 67 |
+
break
|
| 68 |
+
if len(header) > 10000:
|
| 69 |
+
break
|
| 70 |
+
|
| 71 |
+
header_str = header.decode("utf-8", errors="ignore").lower()
|
| 72 |
+
gaussian_indicators = ["f_dc", "opacity", "scale_0", "rot_0"]
|
| 73 |
+
return any(indicator in header_str for indicator in gaussian_indicators)
|
| 74 |
+
except Exception:
|
| 75 |
+
return False
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def _setup_examples():
|
| 79 |
+
"""Ensure examples directory exists."""
|
| 80 |
+
os.makedirs(EXAMPLES_DIR, exist_ok=True)
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
_setup_examples()
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
print("Loading VoMP model...")
|
| 87 |
+
model = Vomp.from_checkpoint(
|
| 88 |
+
config_path="weights/inference.json",
|
| 89 |
+
geometry_checkpoint_dir="weights/geometry_transformer.pt",
|
| 90 |
+
matvae_checkpoint_dir="weights/matvae.safetensors",
|
| 91 |
+
normalization_params_path="weights/normalization_params.json",
|
| 92 |
+
)
|
| 93 |
+
print("VoMP model loaded successfully!")
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def _get_render_images(output_dir: str) -> List[str]:
|
| 97 |
+
renders_dir = os.path.join(output_dir, "renders")
|
| 98 |
+
if not os.path.exists(renders_dir):
|
| 99 |
+
return []
|
| 100 |
+
image_paths = sorted(glob.glob(os.path.join(renders_dir, "*.png")))
|
| 101 |
+
return image_paths
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def _create_colorbar(
|
| 105 |
+
data: np.ndarray, property_name: str, output_path: str, colormap: str = "viridis"
|
| 106 |
+
) -> str:
|
| 107 |
+
fig, ax = plt.subplots(figsize=(6, 0.8))
|
| 108 |
+
fig.subplots_adjust(bottom=0.5)
|
| 109 |
+
ax.remove()
|
| 110 |
+
|
| 111 |
+
cmap = plt.cm.get_cmap(colormap)
|
| 112 |
+
norm = mcolors.Normalize(vmin=np.min(data), vmax=np.max(data))
|
| 113 |
+
|
| 114 |
+
cbar_ax = fig.add_axes([0.1, 0.4, 0.8, 0.35])
|
| 115 |
+
cb = ColorbarBase(cbar_ax, cmap=cmap, norm=norm, orientation="horizontal")
|
| 116 |
+
cb.ax.set_xlabel(
|
| 117 |
+
f"{PROPERTY_DISPLAY_NAMES.get(property_name, property_name)}", fontsize=10
|
| 118 |
+
)
|
| 119 |
+
|
| 120 |
+
plt.savefig(
|
| 121 |
+
output_path, dpi=150, bbox_inches="tight", facecolor="white", transparent=False
|
| 122 |
+
)
|
| 123 |
+
plt.close()
|
| 124 |
+
return output_path
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def _render_point_cloud_views(
|
| 128 |
+
coords: np.ndarray,
|
| 129 |
+
values: np.ndarray,
|
| 130 |
+
output_dir: str,
|
| 131 |
+
property_name: str,
|
| 132 |
+
colormap: str = "viridis",
|
| 133 |
+
) -> List[str]:
|
| 134 |
+
vmin, vmax = np.min(values), np.max(values)
|
| 135 |
+
if vmax - vmin > 1e-10:
|
| 136 |
+
normalized = (values - vmin) / (vmax - vmin)
|
| 137 |
+
else:
|
| 138 |
+
normalized = np.zeros_like(values)
|
| 139 |
+
|
| 140 |
+
cmap = plt.cm.get_cmap(colormap)
|
| 141 |
+
colors = cmap(normalized)
|
| 142 |
+
|
| 143 |
+
views = [
|
| 144 |
+
(30, 45, "view1"),
|
| 145 |
+
(30, 135, "view2"),
|
| 146 |
+
(80, 45, "view3"),
|
| 147 |
+
]
|
| 148 |
+
|
| 149 |
+
image_paths = []
|
| 150 |
+
|
| 151 |
+
for elev, azim, view_name in views:
|
| 152 |
+
fig = plt.figure(figsize=(6, 6), facecolor="#1a1a1a")
|
| 153 |
+
ax = fig.add_subplot(111, projection="3d", facecolor="#1a1a1a")
|
| 154 |
+
|
| 155 |
+
ax.scatter(
|
| 156 |
+
coords[:, 0],
|
| 157 |
+
coords[:, 1],
|
| 158 |
+
coords[:, 2],
|
| 159 |
+
c=colors,
|
| 160 |
+
s=15,
|
| 161 |
+
alpha=0.9,
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
ax.view_init(elev=elev, azim=azim)
|
| 165 |
+
ax.set_xlim([-0.6, 0.6])
|
| 166 |
+
ax.set_ylim([-0.6, 0.6])
|
| 167 |
+
ax.set_zlim([-0.6, 0.6])
|
| 168 |
+
ax.set_axis_off()
|
| 169 |
+
ax.set_box_aspect([1, 1, 1])
|
| 170 |
+
|
| 171 |
+
output_path = os.path.join(output_dir, f"{property_name}_{view_name}.png")
|
| 172 |
+
plt.savefig(
|
| 173 |
+
output_path,
|
| 174 |
+
dpi=150,
|
| 175 |
+
bbox_inches="tight",
|
| 176 |
+
facecolor="#1a1a1a",
|
| 177 |
+
edgecolor="none",
|
| 178 |
+
)
|
| 179 |
+
plt.close()
|
| 180 |
+
|
| 181 |
+
image_paths.append(output_path)
|
| 182 |
+
|
| 183 |
+
return image_paths
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
def _create_material_visualizations(
|
| 187 |
+
material_file: str, output_dir: str
|
| 188 |
+
) -> Dict[str, Tuple[List[str], str]]:
|
| 189 |
+
result = {}
|
| 190 |
+
data = np.load(material_file, allow_pickle=True)
|
| 191 |
+
|
| 192 |
+
if "voxel_data" in data:
|
| 193 |
+
voxel_data = data["voxel_data"]
|
| 194 |
+
coords = np.column_stack([voxel_data["x"], voxel_data["y"], voxel_data["z"]])
|
| 195 |
+
properties = {
|
| 196 |
+
"youngs_modulus": voxel_data["youngs_modulus"],
|
| 197 |
+
"poissons_ratio": voxel_data["poissons_ratio"],
|
| 198 |
+
"density": voxel_data["density"],
|
| 199 |
+
}
|
| 200 |
+
else:
|
| 201 |
+
if "voxel_coords_world" in data:
|
| 202 |
+
coords = data["voxel_coords_world"]
|
| 203 |
+
elif "query_coords_world" in data:
|
| 204 |
+
coords = data["query_coords_world"]
|
| 205 |
+
elif "coords" in data:
|
| 206 |
+
coords = data["coords"]
|
| 207 |
+
else:
|
| 208 |
+
print(f"Warning: No coordinate data found in {material_file}")
|
| 209 |
+
return result
|
| 210 |
+
|
| 211 |
+
properties = {}
|
| 212 |
+
property_mapping = {
|
| 213 |
+
"youngs_modulus": ["youngs_modulus", "young_modulus"],
|
| 214 |
+
"poissons_ratio": ["poissons_ratio", "poisson_ratio"],
|
| 215 |
+
"density": ["density"],
|
| 216 |
+
}
|
| 217 |
+
for prop_name, possible_names in property_mapping.items():
|
| 218 |
+
for name in possible_names:
|
| 219 |
+
if name in data:
|
| 220 |
+
properties[prop_name] = data[name]
|
| 221 |
+
break
|
| 222 |
+
|
| 223 |
+
center = (np.min(coords, axis=0) + np.max(coords, axis=0)) / 2
|
| 224 |
+
max_range = np.max(np.max(coords, axis=0) - np.min(coords, axis=0))
|
| 225 |
+
if max_range > 1e-10:
|
| 226 |
+
coords_normalized = (coords - center) / max_range
|
| 227 |
+
else:
|
| 228 |
+
coords_normalized = coords - center
|
| 229 |
+
|
| 230 |
+
for prop_name, prop_data in properties.items():
|
| 231 |
+
if prop_data is not None:
|
| 232 |
+
view_paths = _render_point_cloud_views(
|
| 233 |
+
coords_normalized, prop_data, output_dir, prop_name
|
| 234 |
+
)
|
| 235 |
+
colorbar_path = os.path.join(output_dir, f"{prop_name}_colorbar.png")
|
| 236 |
+
_create_colorbar(prop_data, prop_name, colorbar_path)
|
| 237 |
+
result[prop_name] = (view_paths, colorbar_path)
|
| 238 |
+
print(f"Created visualization for {prop_name}: {len(view_paths)} views")
|
| 239 |
+
|
| 240 |
+
return result
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
@spaces.GPU()
|
| 244 |
+
@torch.no_grad()
|
| 245 |
+
def process_3d_model(input_file):
|
| 246 |
+
empty_result = (
|
| 247 |
+
None,
|
| 248 |
+
[],
|
| 249 |
+
None,
|
| 250 |
+
[],
|
| 251 |
+
None,
|
| 252 |
+
None,
|
| 253 |
+
[],
|
| 254 |
+
None,
|
| 255 |
+
None,
|
| 256 |
+
[],
|
| 257 |
+
None,
|
| 258 |
+
None,
|
| 259 |
+
)
|
| 260 |
+
if input_file is None:
|
| 261 |
+
return empty_result
|
| 262 |
+
output_dir = tempfile.mkdtemp(prefix="vomp_")
|
| 263 |
+
material_file = os.path.join(output_dir, "materials.npz")
|
| 264 |
+
try:
|
| 265 |
+
if _is_gaussian_splat(input_file):
|
| 266 |
+
print(f"Processing as Gaussian splat: {input_file}")
|
| 267 |
+
results = model.get_splat_materials(
|
| 268 |
+
input_file,
|
| 269 |
+
voxel_method="kaolin",
|
| 270 |
+
query_points="voxel_centers",
|
| 271 |
+
output_dir=output_dir,
|
| 272 |
+
)
|
| 273 |
+
else:
|
| 274 |
+
print(f"Processing as mesh: {input_file}")
|
| 275 |
+
_install_blender()
|
| 276 |
+
results = model.get_mesh_materials(
|
| 277 |
+
input_file,
|
| 278 |
+
blender_path=BLENDER_PATH,
|
| 279 |
+
query_points="voxel_centers",
|
| 280 |
+
output_dir=output_dir,
|
| 281 |
+
return_original_scale=True,
|
| 282 |
+
)
|
| 283 |
+
|
| 284 |
+
save_materials(results, material_file)
|
| 285 |
+
print(f"Materials saved to: {material_file}")
|
| 286 |
+
|
| 287 |
+
all_images = _get_render_images(output_dir)
|
| 288 |
+
first_image = all_images[0] if all_images else None
|
| 289 |
+
|
| 290 |
+
visualizations = _create_material_visualizations(material_file, output_dir)
|
| 291 |
+
|
| 292 |
+
youngs_views = visualizations.get("youngs_modulus", ([], None))[0]
|
| 293 |
+
youngs_colorbar = visualizations.get("youngs_modulus", ([], None))[1]
|
| 294 |
+
youngs_first = youngs_views[0] if youngs_views else None
|
| 295 |
+
|
| 296 |
+
poissons_views = visualizations.get("poissons_ratio", ([], None))[0]
|
| 297 |
+
poissons_colorbar = visualizations.get("poissons_ratio", ([], None))[1]
|
| 298 |
+
poissons_first = poissons_views[0] if poissons_views else None
|
| 299 |
+
|
| 300 |
+
density_views = visualizations.get("density", ([], None))[0]
|
| 301 |
+
density_colorbar = visualizations.get("density", ([], None))[1]
|
| 302 |
+
density_first = density_views[0] if density_views else None
|
| 303 |
+
|
| 304 |
+
return (
|
| 305 |
+
first_image,
|
| 306 |
+
all_images,
|
| 307 |
+
youngs_first,
|
| 308 |
+
youngs_views,
|
| 309 |
+
youngs_colorbar,
|
| 310 |
+
poissons_first,
|
| 311 |
+
poissons_views,
|
| 312 |
+
poissons_colorbar,
|
| 313 |
+
density_first,
|
| 314 |
+
density_views,
|
| 315 |
+
density_colorbar,
|
| 316 |
+
material_file,
|
| 317 |
+
)
|
| 318 |
+
|
| 319 |
+
except Exception as e:
|
| 320 |
+
print(f"Error processing 3D model: {e}")
|
| 321 |
+
raise gr.Error(f"Failed to process 3D model: {str(e)}")
|
| 322 |
+
|
| 323 |
+
|
| 324 |
+
def update_slider_image(slider_value: int, all_images: List[str]) -> Optional[str]:
|
| 325 |
+
if not all_images or slider_value < 0 or slider_value >= len(all_images):
|
| 326 |
+
return None
|
| 327 |
+
return all_images[slider_value]
|
| 328 |
+
|
| 329 |
+
|
| 330 |
+
def update_property_view(slider_value: int, views: List[str]) -> Optional[str]:
|
| 331 |
+
if not views or slider_value < 0 or slider_value >= len(views):
|
| 332 |
+
return None
|
| 333 |
+
return views[slider_value]
|
| 334 |
+
|
| 335 |
+
|
| 336 |
+
css = """
|
| 337 |
+
.gradio-container {
|
| 338 |
+
font-family: 'IBM Plex Sans', sans-serif;
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
.title-container {
|
| 342 |
+
text-align: center;
|
| 343 |
+
padding: 20px 0;
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
.badge-container {
|
| 347 |
+
display: flex;
|
| 348 |
+
justify-content: center;
|
| 349 |
+
gap: 8px;
|
| 350 |
+
flex-wrap: wrap;
|
| 351 |
+
margin-bottom: 20px;
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
.badge-container a img {
|
| 355 |
+
height: 22px;
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
h1 {
|
| 359 |
+
text-align: center;
|
| 360 |
+
font-size: 2.5rem;
|
| 361 |
+
margin-bottom: 0.5rem;
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
.subtitle {
|
| 365 |
+
text-align: center;
|
| 366 |
+
color: #666;
|
| 367 |
+
font-size: 1.1rem;
|
| 368 |
+
margin-bottom: 1.5rem;
|
| 369 |
+
}
|
| 370 |
+
|
| 371 |
+
.input-column, .output-column {
|
| 372 |
+
min-height: 400px;
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
.output-column .row {
|
| 376 |
+
display: flex !important;
|
| 377 |
+
flex-wrap: nowrap !important;
|
| 378 |
+
gap: 16px;
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
.output-column .row > .column {
|
| 382 |
+
flex: 1 1 50% !important;
|
| 383 |
+
min-width: 0 !important;
|
| 384 |
+
}
|
| 385 |
+
"""
|
| 386 |
+
|
| 387 |
+
title_md = """
|
| 388 |
+
<div class="title-container">
|
| 389 |
+
<h1>VoMP: Predicting Volumetric Mechanical Properties</h1>
|
| 390 |
+
<p class="subtitle">Feed-forward, fine-grained, physically based volumetric material properties from Splats, Meshes, NeRFs, and more.</p>
|
| 391 |
+
<div class="badge-container">
|
| 392 |
+
<a href="https://arxiv.org/abs/2510.22975"><img src='https://img.shields.io/badge/arXiv-VoMP-red' alt='Paper PDF'></a>
|
| 393 |
+
<a href='https://research.nvidia.com/labs/sil/projects/vomp/'><img src='https://img.shields.io/badge/Project_Page-VoMP-green' alt='Project Page'></a>
|
| 394 |
+
<a href='https://huggingface.co/nvidia/PhysicalAI-Simulation-VoMP-Model'><img src='https://img.shields.io/badge/%F0%9F%A4%97%20-Models-yellow'></a>
|
| 395 |
+
<a href='https://huggingface.co/datasets/nvidia/PhysicalAI-Robotics-PhysicalAssets-VoMP'><img src='https://img.shields.io/badge/%F0%9F%A4%97%20-GVM%20Dataset-yellow'></a>
|
| 396 |
+
</div>
|
| 397 |
+
</div>
|
| 398 |
+
"""
|
| 399 |
+
|
| 400 |
+
description_md = """
|
| 401 |
+
Upload a Gaussian Splat (.ply) or Mesh (.obj, .glb, .stl, .gltf) to predict volumetric mechanical properties (Young's modulus, Poisson's ratio, density) for realistic physics simulation.
|
| 402 |
+
"""
|
| 403 |
+
|
| 404 |
+
with gr.Blocks(css=css, title="VoMP") as demo:
|
| 405 |
+
all_images_state = gr.State([])
|
| 406 |
+
youngs_views_state = gr.State([])
|
| 407 |
+
poissons_views_state = gr.State([])
|
| 408 |
+
density_views_state = gr.State([])
|
| 409 |
+
|
| 410 |
+
gr.HTML(title_md)
|
| 411 |
+
gr.Markdown(description_md)
|
| 412 |
+
|
| 413 |
+
with gr.Row():
|
| 414 |
+
# Input Column (50%)
|
| 415 |
+
with gr.Column(scale=1, elem_classes="input-column"):
|
| 416 |
+
gr.Markdown("### 📤 Input")
|
| 417 |
+
input_model = gr.Model3D(
|
| 418 |
+
label="Upload 3D Model",
|
| 419 |
+
clear_color=[0.1, 0.1, 0.1, 1.0],
|
| 420 |
+
)
|
| 421 |
+
|
| 422 |
+
submit_btn = gr.Button(
|
| 423 |
+
"🚀 Generate Materials", variant="primary", size="lg"
|
| 424 |
+
)
|
| 425 |
+
|
| 426 |
+
gr.Markdown("#### 🎬 Rendered Views")
|
| 427 |
+
rendered_image = gr.Image(label="Rendered View", height=250)
|
| 428 |
+
|
| 429 |
+
view_slider = gr.Slider(
|
| 430 |
+
minimum=0,
|
| 431 |
+
maximum=NUM_VIEWS - 1,
|
| 432 |
+
step=1,
|
| 433 |
+
value=0,
|
| 434 |
+
label="Browse All Views",
|
| 435 |
+
info=f"Slide to view all {NUM_VIEWS} rendered views",
|
| 436 |
+
)
|
| 437 |
+
|
| 438 |
+
# Output Column (50%)
|
| 439 |
+
with gr.Column(scale=1, elem_classes="output-column"):
|
| 440 |
+
gr.Markdown("### 📥 Output - Material Properties")
|
| 441 |
+
|
| 442 |
+
# Row 1: Young's Modulus and Poisson's Ratio
|
| 443 |
+
with gr.Row():
|
| 444 |
+
with gr.Column(scale=1, min_width=200):
|
| 445 |
+
youngs_image = gr.Image(label="Young's Modulus", height=200)
|
| 446 |
+
youngs_slider = gr.Slider(
|
| 447 |
+
minimum=0,
|
| 448 |
+
maximum=2,
|
| 449 |
+
step=1,
|
| 450 |
+
value=0,
|
| 451 |
+
label="View",
|
| 452 |
+
info="Switch between 3 views",
|
| 453 |
+
)
|
| 454 |
+
youngs_colorbar = gr.Image(height=50, show_label=False)
|
| 455 |
+
|
| 456 |
+
with gr.Column(scale=1, min_width=200):
|
| 457 |
+
poissons_image = gr.Image(label="Poisson's Ratio", height=200)
|
| 458 |
+
poissons_slider = gr.Slider(
|
| 459 |
+
minimum=0,
|
| 460 |
+
maximum=2,
|
| 461 |
+
step=1,
|
| 462 |
+
value=0,
|
| 463 |
+
label="View",
|
| 464 |
+
info="Switch between 3 views",
|
| 465 |
+
)
|
| 466 |
+
poissons_colorbar = gr.Image(height=50, show_label=False)
|
| 467 |
+
|
| 468 |
+
# Row 2: Density and Download
|
| 469 |
+
with gr.Row():
|
| 470 |
+
with gr.Column(scale=1, min_width=200):
|
| 471 |
+
density_image = gr.Image(label="Density", height=200)
|
| 472 |
+
density_slider = gr.Slider(
|
| 473 |
+
minimum=0,
|
| 474 |
+
maximum=2,
|
| 475 |
+
step=1,
|
| 476 |
+
value=0,
|
| 477 |
+
label="View",
|
| 478 |
+
info="Switch between 3 views",
|
| 479 |
+
)
|
| 480 |
+
density_colorbar = gr.Image(height=50, show_label=False)
|
| 481 |
+
|
| 482 |
+
with gr.Column(scale=1, min_width=200):
|
| 483 |
+
gr.Markdown("#### 💾 Download")
|
| 484 |
+
output_file = gr.File(
|
| 485 |
+
label="Download Materials (.npz)",
|
| 486 |
+
file_count="single",
|
| 487 |
+
)
|
| 488 |
+
|
| 489 |
+
gr.Markdown("### 🎯 Examples")
|
| 490 |
+
gr.Examples(
|
| 491 |
+
examples=[
|
| 492 |
+
[os.path.join(EXAMPLES_DIR, "plant.ply")],
|
| 493 |
+
[os.path.join(EXAMPLES_DIR, "dog.ply")],
|
| 494 |
+
[os.path.join(EXAMPLES_DIR, "dozer.ply")],
|
| 495 |
+
[os.path.join(EXAMPLES_DIR, "fiscus.ply")],
|
| 496 |
+
],
|
| 497 |
+
inputs=[input_model],
|
| 498 |
+
outputs=[
|
| 499 |
+
rendered_image,
|
| 500 |
+
all_images_state,
|
| 501 |
+
youngs_image,
|
| 502 |
+
youngs_views_state,
|
| 503 |
+
youngs_colorbar,
|
| 504 |
+
poissons_image,
|
| 505 |
+
poissons_views_state,
|
| 506 |
+
poissons_colorbar,
|
| 507 |
+
density_image,
|
| 508 |
+
density_views_state,
|
| 509 |
+
density_colorbar,
|
| 510 |
+
output_file,
|
| 511 |
+
],
|
| 512 |
+
fn=process_3d_model,
|
| 513 |
+
cache_examples=False,
|
| 514 |
+
)
|
| 515 |
+
|
| 516 |
+
# Event handlers
|
| 517 |
+
submit_btn.click(
|
| 518 |
+
fn=process_3d_model,
|
| 519 |
+
inputs=[input_model],
|
| 520 |
+
outputs=[
|
| 521 |
+
rendered_image,
|
| 522 |
+
all_images_state,
|
| 523 |
+
youngs_image,
|
| 524 |
+
youngs_views_state,
|
| 525 |
+
youngs_colorbar,
|
| 526 |
+
poissons_image,
|
| 527 |
+
poissons_views_state,
|
| 528 |
+
poissons_colorbar,
|
| 529 |
+
density_image,
|
| 530 |
+
density_views_state,
|
| 531 |
+
density_colorbar,
|
| 532 |
+
output_file,
|
| 533 |
+
],
|
| 534 |
+
)
|
| 535 |
+
|
| 536 |
+
view_slider.change(
|
| 537 |
+
fn=update_slider_image,
|
| 538 |
+
inputs=[view_slider, all_images_state],
|
| 539 |
+
outputs=[rendered_image],
|
| 540 |
+
)
|
| 541 |
+
|
| 542 |
+
youngs_slider.change(
|
| 543 |
+
fn=update_property_view,
|
| 544 |
+
inputs=[youngs_slider, youngs_views_state],
|
| 545 |
+
outputs=[youngs_image],
|
| 546 |
+
)
|
| 547 |
+
|
| 548 |
+
poissons_slider.change(
|
| 549 |
+
fn=update_property_view,
|
| 550 |
+
inputs=[poissons_slider, poissons_views_state],
|
| 551 |
+
outputs=[poissons_image],
|
| 552 |
+
)
|
| 553 |
+
|
| 554 |
+
density_slider.change(
|
| 555 |
+
fn=update_property_view,
|
| 556 |
+
inputs=[density_slider, density_views_state],
|
| 557 |
+
outputs=[density_image],
|
| 558 |
+
)
|
| 559 |
+
|
| 560 |
+
if __name__ == "__main__":
|
| 561 |
+
demo.launch()
|
deps/vomp/.gitignore
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Byte-compiled / optimized / DLL files
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[codz]
|
| 4 |
+
*$py.class
|
| 5 |
+
|
| 6 |
+
# C extensions
|
| 7 |
+
*.so
|
| 8 |
+
|
| 9 |
+
# Distribution / packaging
|
| 10 |
+
.Python
|
| 11 |
+
build/
|
| 12 |
+
develop-eggs/
|
| 13 |
+
dist/
|
| 14 |
+
downloads/
|
| 15 |
+
eggs/
|
| 16 |
+
.eggs/
|
| 17 |
+
lib/
|
| 18 |
+
lib64/
|
| 19 |
+
parts/
|
| 20 |
+
sdist/
|
| 21 |
+
var/
|
| 22 |
+
wheels/
|
| 23 |
+
share/python-wheels/
|
| 24 |
+
*.egg-info/
|
| 25 |
+
.installed.cfg
|
| 26 |
+
*.egg
|
| 27 |
+
MANIFEST
|
| 28 |
+
|
| 29 |
+
# PyInstaller
|
| 30 |
+
# Usually these files are written by a python script from a template
|
| 31 |
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
| 32 |
+
*.manifest
|
| 33 |
+
*.spec
|
| 34 |
+
|
| 35 |
+
# Installer logs
|
| 36 |
+
pip-log.txt
|
| 37 |
+
pip-delete-this-directory.txt
|
| 38 |
+
|
| 39 |
+
# Unit test / coverage reports
|
| 40 |
+
htmlcov/
|
| 41 |
+
.tox/
|
| 42 |
+
.nox/
|
| 43 |
+
.coverage
|
| 44 |
+
.coverage.*
|
| 45 |
+
.cache
|
| 46 |
+
nosetests.xml
|
| 47 |
+
coverage.xml
|
| 48 |
+
*.cover
|
| 49 |
+
*.py.cover
|
| 50 |
+
.hypothesis/
|
| 51 |
+
.pytest_cache/
|
| 52 |
+
cover/
|
| 53 |
+
|
| 54 |
+
# Translations
|
| 55 |
+
*.mo
|
| 56 |
+
*.pot
|
| 57 |
+
|
| 58 |
+
# Django stuff:
|
| 59 |
+
*.log
|
| 60 |
+
local_settings.py
|
| 61 |
+
db.sqlite3
|
| 62 |
+
db.sqlite3-journal
|
| 63 |
+
|
| 64 |
+
# Flask stuff:
|
| 65 |
+
instance/
|
| 66 |
+
.webassets-cache
|
| 67 |
+
|
| 68 |
+
# Scrapy stuff:
|
| 69 |
+
.scrapy
|
| 70 |
+
|
| 71 |
+
# Sphinx documentation
|
| 72 |
+
docs/_build/
|
| 73 |
+
|
| 74 |
+
# PyBuilder
|
| 75 |
+
.pybuilder/
|
| 76 |
+
target/
|
| 77 |
+
|
| 78 |
+
# Jupyter Notebook
|
| 79 |
+
.ipynb_checkpoints
|
| 80 |
+
|
| 81 |
+
# IPython
|
| 82 |
+
profile_default/
|
| 83 |
+
ipython_config.py
|
| 84 |
+
|
| 85 |
+
# pyenv
|
| 86 |
+
# For a library or package, you might want to ignore these files since the code is
|
| 87 |
+
# intended to run in multiple environments; otherwise, check them in:
|
| 88 |
+
# .python-version
|
| 89 |
+
|
| 90 |
+
# pipenv
|
| 91 |
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
| 92 |
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
| 93 |
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
| 94 |
+
# install all needed dependencies.
|
| 95 |
+
# Pipfile.lock
|
| 96 |
+
|
| 97 |
+
# UV
|
| 98 |
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
| 99 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
| 100 |
+
# commonly ignored for libraries.
|
| 101 |
+
# uv.lock
|
| 102 |
+
|
| 103 |
+
# poetry
|
| 104 |
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
| 105 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
| 106 |
+
# commonly ignored for libraries.
|
| 107 |
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
| 108 |
+
# poetry.lock
|
| 109 |
+
# poetry.toml
|
| 110 |
+
|
| 111 |
+
# pdm
|
| 112 |
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
| 113 |
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
| 114 |
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
| 115 |
+
# pdm.lock
|
| 116 |
+
# pdm.toml
|
| 117 |
+
.pdm-python
|
| 118 |
+
.pdm-build/
|
| 119 |
+
|
| 120 |
+
# pixi
|
| 121 |
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
| 122 |
+
# pixi.lock
|
| 123 |
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
| 124 |
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
| 125 |
+
.pixi
|
| 126 |
+
|
| 127 |
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
| 128 |
+
__pypackages__/
|
| 129 |
+
|
| 130 |
+
# Celery stuff
|
| 131 |
+
celerybeat-schedule
|
| 132 |
+
celerybeat.pid
|
| 133 |
+
|
| 134 |
+
# Redis
|
| 135 |
+
*.rdb
|
| 136 |
+
*.aof
|
| 137 |
+
*.pid
|
| 138 |
+
|
| 139 |
+
# RabbitMQ
|
| 140 |
+
mnesia/
|
| 141 |
+
rabbitmq/
|
| 142 |
+
rabbitmq-data/
|
| 143 |
+
|
| 144 |
+
# ActiveMQ
|
| 145 |
+
activemq-data/
|
| 146 |
+
|
| 147 |
+
# SageMath parsed files
|
| 148 |
+
*.sage.py
|
| 149 |
+
|
| 150 |
+
# Environments
|
| 151 |
+
.env
|
| 152 |
+
.envrc
|
| 153 |
+
.venv
|
| 154 |
+
env/
|
| 155 |
+
venv/
|
| 156 |
+
ENV/
|
| 157 |
+
env.bak/
|
| 158 |
+
venv.bak/
|
| 159 |
+
|
| 160 |
+
# Spyder project settings
|
| 161 |
+
.spyderproject
|
| 162 |
+
.spyproject
|
| 163 |
+
|
| 164 |
+
# Rope project settings
|
| 165 |
+
.ropeproject
|
| 166 |
+
|
| 167 |
+
# mkdocs documentation
|
| 168 |
+
/site
|
| 169 |
+
|
| 170 |
+
# mypy
|
| 171 |
+
.mypy_cache/
|
| 172 |
+
.dmypy.json
|
| 173 |
+
dmypy.json
|
| 174 |
+
|
| 175 |
+
# Pyre type checker
|
| 176 |
+
.pyre/
|
| 177 |
+
|
| 178 |
+
# pytype static type analyzer
|
| 179 |
+
.pytype/
|
| 180 |
+
|
| 181 |
+
# Cython debug symbols
|
| 182 |
+
cython_debug/
|
| 183 |
+
|
| 184 |
+
# PyCharm
|
| 185 |
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
| 186 |
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
| 187 |
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
| 188 |
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
| 189 |
+
# .idea/
|
| 190 |
+
|
| 191 |
+
# Abstra
|
| 192 |
+
# Abstra is an AI-powered process automation framework.
|
| 193 |
+
# Ignore directories containing user credentials, local state, and settings.
|
| 194 |
+
# Learn more at https://abstra.io/docs
|
| 195 |
+
.abstra/
|
| 196 |
+
|
| 197 |
+
# Visual Studio Code
|
| 198 |
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
| 199 |
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
| 200 |
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
| 201 |
+
# you could uncomment the following to ignore the entire vscode folder
|
| 202 |
+
# .vscode/
|
| 203 |
+
|
| 204 |
+
# Ruff stuff:
|
| 205 |
+
.ruff_cache/
|
| 206 |
+
|
| 207 |
+
# PyPI configuration file
|
| 208 |
+
.pypirc
|
| 209 |
+
|
| 210 |
+
# Marimo
|
| 211 |
+
marimo/_static/
|
| 212 |
+
marimo/_lsp/
|
| 213 |
+
__marimo__/
|
| 214 |
+
|
| 215 |
+
# Streamlit
|
| 216 |
+
.streamlit/secrets.toml
|
deps/vomp/ATTRIBUTIONS.md
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
deps/vomp/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# How to Contribute
|
| 2 |
+
|
| 3 |
+
We'd love to receive your patches and contributions. Please keep your PRs as draft until such time that you would like us to review them.
|
| 4 |
+
|
| 5 |
+
## Code Reviews
|
| 6 |
+
|
| 7 |
+
All submissions, including submissions by project members, require review. We use GitHub pull requests for this purpose. Consult
|
| 8 |
+
[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more information on using pull requests.
|
| 9 |
+
|
| 10 |
+
## Signing Your Work
|
| 11 |
+
|
| 12 |
+
* We require that all contributors "sign-off" on their commits. This certifies that the contribution is your original work, or you have rights to submit it under the same license, or a compatible license.
|
| 13 |
+
|
| 14 |
+
* Any contribution which contains commits that are not Signed-Off will not be accepted.
|
| 15 |
+
|
| 16 |
+
* To sign off on a commit you simply use the `--signoff` (or `-s`) option when committing your changes:
|
| 17 |
+
```bash
|
| 18 |
+
$ git commit -s -m "Add cool feature."
|
| 19 |
+
```
|
| 20 |
+
This will append the following to your commit message:
|
| 21 |
+
```
|
| 22 |
+
Signed-off-by: Your Name <your@email.com>
|
| 23 |
+
```
|
| 24 |
+
|
| 25 |
+
* Full text of the DCO:
|
| 26 |
+
|
| 27 |
+
```
|
| 28 |
+
Developer Certificate of Origin
|
| 29 |
+
Version 1.1
|
| 30 |
+
|
| 31 |
+
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
|
| 32 |
+
1 Letterman Drive
|
| 33 |
+
Suite D4700
|
| 34 |
+
San Francisco, CA, 94129
|
| 35 |
+
|
| 36 |
+
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
|
| 37 |
+
```
|
| 38 |
+
|
| 39 |
+
```
|
| 40 |
+
Developer's Certificate of Origin 1.1
|
| 41 |
+
|
| 42 |
+
By making a contribution to this project, I certify that:
|
| 43 |
+
|
| 44 |
+
(a) The contribution was created in whole or in part by me and I have the right to submit it under the open source license indicated in the file; or
|
| 45 |
+
|
| 46 |
+
(b) The contribution is based upon previous work that, to the best of my knowledge, is covered under an appropriate open source license and I have the right under that license to submit that work with modifications, whether created in whole or in part by me, under the same open source license (unless I am permitted to submit under a different license), as indicated in the file; or
|
| 47 |
+
|
| 48 |
+
(c) The contribution was provided directly to me by some other person who certified (a), (b) or (c) and I have not modified it.
|
| 49 |
+
|
| 50 |
+
(d) I understand and agree that this project and the contribution are public and that a record of the contribution (including all personal information I submit with it, including my sign-off) is maintained indefinitely and may be redistributed consistent with this project or the open source license(s) involved.
|
| 51 |
+
```
|
deps/vomp/LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Apache License
|
| 2 |
+
Version 2.0, January 2004
|
| 3 |
+
http://www.apache.org/licenses/
|
| 4 |
+
|
| 5 |
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
| 6 |
+
|
| 7 |
+
1. Definitions.
|
| 8 |
+
|
| 9 |
+
"License" shall mean the terms and conditions for use, reproduction,
|
| 10 |
+
and distribution as defined by Sections 1 through 9 of this document.
|
| 11 |
+
|
| 12 |
+
"Licensor" shall mean the copyright owner or entity authorized by
|
| 13 |
+
the copyright owner that is granting the License.
|
| 14 |
+
|
| 15 |
+
"Legal Entity" shall mean the union of the acting entity and all
|
| 16 |
+
other entities that control, are controlled by, or are under common
|
| 17 |
+
control with that entity. For the purposes of this definition,
|
| 18 |
+
"control" means (i) the power, direct or indirect, to cause the
|
| 19 |
+
direction or management of such entity, whether by contract or
|
| 20 |
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
| 21 |
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
| 22 |
+
|
| 23 |
+
"You" (or "Your") shall mean an individual or Legal Entity
|
| 24 |
+
exercising permissions granted by this License.
|
| 25 |
+
|
| 26 |
+
"Source" form shall mean the preferred form for making modifications,
|
| 27 |
+
including but not limited to software source code, documentation
|
| 28 |
+
source, and configuration files.
|
| 29 |
+
|
| 30 |
+
"Object" form shall mean any form resulting from mechanical
|
| 31 |
+
transformation or translation of a Source form, including but
|
| 32 |
+
not limited to compiled object code, generated documentation,
|
| 33 |
+
and conversions to other media types.
|
| 34 |
+
|
| 35 |
+
"Work" shall mean the work of authorship, whether in Source or
|
| 36 |
+
Object form, made available under the License, as indicated by a
|
| 37 |
+
copyright notice that is included in or attached to the work
|
| 38 |
+
(an example is provided in the Appendix below).
|
| 39 |
+
|
| 40 |
+
"Derivative Works" shall mean any work, whether in Source or Object
|
| 41 |
+
form, that is based on (or derived from) the Work and for which the
|
| 42 |
+
editorial revisions, annotations, elaborations, or other modifications
|
| 43 |
+
represent, as a whole, an original work of authorship. For the purposes
|
| 44 |
+
of this License, Derivative Works shall not include works that remain
|
| 45 |
+
separable from, or merely link (or bind by name) to the interfaces of,
|
| 46 |
+
the Work and Derivative Works thereof.
|
| 47 |
+
|
| 48 |
+
"Contribution" shall mean any work of authorship, including
|
| 49 |
+
the original version of the Work and any modifications or additions
|
| 50 |
+
to that Work or Derivative Works thereof, that is intentionally
|
| 51 |
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
| 52 |
+
or by an individual or Legal Entity authorized to submit on behalf of
|
| 53 |
+
the copyright owner. For the purposes of this definition, "submitted"
|
| 54 |
+
means any form of electronic, verbal, or written communication sent
|
| 55 |
+
to the Licensor or its representatives, including but not limited to
|
| 56 |
+
communication on electronic mailing lists, source code control systems,
|
| 57 |
+
and issue tracking systems that are managed by, or on behalf of, the
|
| 58 |
+
Licensor for the purpose of discussing and improving the Work, but
|
| 59 |
+
excluding communication that is conspicuously marked or otherwise
|
| 60 |
+
designated in writing by the copyright owner as "Not a Contribution."
|
| 61 |
+
|
| 62 |
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
| 63 |
+
on behalf of whom a Contribution has been received by Licensor and
|
| 64 |
+
subsequently incorporated within the Work.
|
| 65 |
+
|
| 66 |
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
| 67 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 68 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 69 |
+
copyright license to reproduce, prepare Derivative Works of,
|
| 70 |
+
publicly display, publicly perform, sublicense, and distribute the
|
| 71 |
+
Work and such Derivative Works in Source or Object form.
|
| 72 |
+
|
| 73 |
+
3. Grant of Patent License. Subject to the terms and conditions of
|
| 74 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 75 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 76 |
+
(except as stated in this section) patent license to make, have made,
|
| 77 |
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
| 78 |
+
where such license applies only to those patent claims licensable
|
| 79 |
+
by such Contributor that are necessarily infringed by their
|
| 80 |
+
Contribution(s) alone or by combination of their Contribution(s)
|
| 81 |
+
with the Work to which such Contribution(s) was submitted. If You
|
| 82 |
+
institute patent litigation against any entity (including a
|
| 83 |
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
| 84 |
+
or a Contribution incorporated within the Work constitutes direct
|
| 85 |
+
or contributory patent infringement, then any patent licenses
|
| 86 |
+
granted to You under this License for that Work shall terminate
|
| 87 |
+
as of the date such litigation is filed.
|
| 88 |
+
|
| 89 |
+
4. Redistribution. You may reproduce and distribute copies of the
|
| 90 |
+
Work or Derivative Works thereof in any medium, with or without
|
| 91 |
+
modifications, and in Source or Object form, provided that You
|
| 92 |
+
meet the following conditions:
|
| 93 |
+
|
| 94 |
+
(a) You must give any other recipients of the Work or
|
| 95 |
+
Derivative Works a copy of this License; and
|
| 96 |
+
|
| 97 |
+
(b) You must cause any modified files to carry prominent notices
|
| 98 |
+
stating that You changed the files; and
|
| 99 |
+
|
| 100 |
+
(c) You must retain, in the Source form of any Derivative Works
|
| 101 |
+
that You distribute, all copyright, patent, trademark, and
|
| 102 |
+
attribution notices from the Source form of the Work,
|
| 103 |
+
excluding those notices that do not pertain to any part of
|
| 104 |
+
the Derivative Works; and
|
| 105 |
+
|
| 106 |
+
(d) If the Work includes a "NOTICE" text file as part of its
|
| 107 |
+
distribution, then any Derivative Works that You distribute must
|
| 108 |
+
include a readable copy of the attribution notices contained
|
| 109 |
+
within such NOTICE file, excluding those notices that do not
|
| 110 |
+
pertain to any part of the Derivative Works, in at least one
|
| 111 |
+
of the following places: within a NOTICE text file distributed
|
| 112 |
+
as part of the Derivative Works; within the Source form or
|
| 113 |
+
documentation, if provided along with the Derivative Works; or,
|
| 114 |
+
within a display generated by the Derivative Works, if and
|
| 115 |
+
wherever such third-party notices normally appear. The contents
|
| 116 |
+
of the NOTICE file are for informational purposes only and
|
| 117 |
+
do not modify the License. You may add Your own attribution
|
| 118 |
+
notices within Derivative Works that You distribute, alongside
|
| 119 |
+
or as an addendum to the NOTICE text from the Work, provided
|
| 120 |
+
that such additional attribution notices cannot be construed
|
| 121 |
+
as modifying the License.
|
| 122 |
+
|
| 123 |
+
You may add Your own copyright statement to Your modifications and
|
| 124 |
+
may provide additional or different license terms and conditions
|
| 125 |
+
for use, reproduction, or distribution of Your modifications, or
|
| 126 |
+
for any such Derivative Works as a whole, provided Your use,
|
| 127 |
+
reproduction, and distribution of the Work otherwise complies with
|
| 128 |
+
the conditions stated in this License.
|
| 129 |
+
|
| 130 |
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
| 131 |
+
any Contribution intentionally submitted for inclusion in the Work
|
| 132 |
+
by You to the Licensor shall be under the terms and conditions of
|
| 133 |
+
this License, without any additional terms or conditions.
|
| 134 |
+
Notwithstanding the above, nothing herein shall supersede or modify
|
| 135 |
+
the terms of any separate license agreement you may have executed
|
| 136 |
+
with Licensor regarding such Contributions.
|
| 137 |
+
|
| 138 |
+
6. Trademarks. This License does not grant permission to use the trade
|
| 139 |
+
names, trademarks, service marks, or product names of the Licensor,
|
| 140 |
+
except as required for reasonable and customary use in describing the
|
| 141 |
+
origin of the Work and reproducing the content of the NOTICE file.
|
| 142 |
+
|
| 143 |
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
| 144 |
+
agreed to in writing, Licensor provides the Work (and each
|
| 145 |
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
| 146 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
| 147 |
+
implied, including, without limitation, any warranties or conditions
|
| 148 |
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
| 149 |
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
| 150 |
+
appropriateness of using or redistributing the Work and assume any
|
| 151 |
+
risks associated with Your exercise of permissions under this License.
|
| 152 |
+
|
| 153 |
+
8. Limitation of Liability. In no event and under no legal theory,
|
| 154 |
+
whether in tort (including negligence), contract, or otherwise,
|
| 155 |
+
unless required by applicable law (such as deliberate and grossly
|
| 156 |
+
negligent acts) or agreed to in writing, shall any Contributor be
|
| 157 |
+
liable to You for damages, including any direct, indirect, special,
|
| 158 |
+
incidental, or consequential damages of any character arising as a
|
| 159 |
+
result of this License or out of the use or inability to use the
|
| 160 |
+
Work (including but not limited to damages for loss of goodwill,
|
| 161 |
+
work stoppage, computer failure or malfunction, or any and all
|
| 162 |
+
other commercial damages or losses), even if such Contributor
|
| 163 |
+
has been advised of the possibility of such damages.
|
| 164 |
+
|
| 165 |
+
9. Accepting Warranty or Additional Liability. While redistributing
|
| 166 |
+
the Work or Derivative Works thereof, You may choose to offer,
|
| 167 |
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
| 168 |
+
or other liability obligations and/or rights consistent with this
|
| 169 |
+
License. However, in accepting such obligations, You may act only
|
| 170 |
+
on Your own behalf and on Your sole responsibility, not on behalf
|
| 171 |
+
of any other Contributor, and only if You agree to indemnify,
|
| 172 |
+
defend, and hold each Contributor harmless for any liability
|
| 173 |
+
incurred by, or claims asserted against, such Contributor by reason
|
| 174 |
+
of your accepting any such warranty or additional liability.
|
| 175 |
+
|
| 176 |
+
END OF TERMS AND CONDITIONS
|
| 177 |
+
|
| 178 |
+
APPENDIX: How to apply the Apache License to your work.
|
| 179 |
+
|
| 180 |
+
To apply the Apache License to your work, attach the following
|
| 181 |
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
| 182 |
+
replaced with your own identifying information. (Don't include
|
| 183 |
+
the brackets!) The text should be enclosed in the appropriate
|
| 184 |
+
comment syntax for the file format. We also recommend that a
|
| 185 |
+
file or class name and description of purpose be included on the
|
| 186 |
+
same "printed page" as the copyright notice for easier
|
| 187 |
+
identification within third-party archives.
|
| 188 |
+
|
| 189 |
+
Copyright [yyyy] [name of copyright owner]
|
| 190 |
+
|
| 191 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
| 192 |
+
you may not use this file except in compliance with the License.
|
| 193 |
+
You may obtain a copy of the License at
|
| 194 |
+
|
| 195 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
| 196 |
+
|
| 197 |
+
Unless required by applicable law or agreed to in writing, software
|
| 198 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
| 199 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 200 |
+
See the License for the specific language governing permissions and
|
| 201 |
+
limitations under the License.
|
deps/vomp/README.md
ADDED
|
@@ -0,0 +1,665 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<div align="center">
|
| 2 |
+
<h2>VoMP: Predicting Volumetric Mechanical Properties</h2>
|
| 3 |
+
|
| 4 |
+
<a href="https://arxiv.org/abs/2510.22975"><img src='https://img.shields.io/badge/arXiv-VoMP-red' alt='Paper PDF'></a>
|
| 5 |
+
<a href='https://research.nvidia.com/labs/sil/projects/vomp/'><img src='https://img.shields.io/badge/Project_Page-VoMP-green' alt='Project Page'></a>
|
| 6 |
+
<a href='https://huggingface.co/nvidia/PhysicalAI-Simulation-VoMP-Model'><img src='https://img.shields.io/badge/%F0%9F%A4%97%20-Models-yellow'></a>
|
| 7 |
+
<a href='https://huggingface.co/datasets/nvidia/PhysicalAI-Robotics-PhysicalAssets-VoMP'><img src='https://img.shields.io/badge/%F0%9F%A4%97%20-GVM%20Dataset-yellow'></a>
|
| 8 |
+
</div>
|
| 9 |
+
|
| 10 |
+

|
| 11 |
+
|
| 12 |
+
This repository provides the implementation of **VoMP**. TL;DR: Feed-forward, fine-grained, physically based volumetric material properties from Splats, Meshes, NeRFs, etc. which can be used to produce realistic worlds. We recommend reading the [README_train.md](./README_train.md) if you need to fine-tune or train the model from scratch or know more details about the codebase.
|
| 13 |
+
|
| 14 |
+
---
|
| 15 |
+
|
| 16 |
+
## Contents
|
| 17 |
+
|
| 18 |
+
- [🔧 Dependencies and Installation](#-dependencies-and-installation)
|
| 19 |
+
* [Setup a Virtual Environment (Recommended)](#setup-a-virtual-environment--recommended-)
|
| 20 |
+
* [Install a Mesh Renderer (Required for Mesh Processing Only)](#install-a-mesh-renderer--required-for-mesh-processing-only-)
|
| 21 |
+
+ [Isaac Sim](#isaac-sim)
|
| 22 |
+
+ [Blender](#blender)
|
| 23 |
+
* [Setup a Conda Environment (Alternative)](#setup-a-conda-environment--alternative-)
|
| 24 |
+
* [Trained Models](#trained-models)
|
| 25 |
+
- [🌐 Quickstart: Web Demo](#-quickstart-web-demo)
|
| 26 |
+
- [📥 Loading the Model](#-loading-the-model)
|
| 27 |
+
* [Using Inference Config (Recommended)](#using-inference-config--recommended-)
|
| 28 |
+
* [Using Direct File Paths](#using-direct-file-paths)
|
| 29 |
+
* [Using Directories (use for fine-tuning)](#using-directories--use-for-fine-tuning-)
|
| 30 |
+
- [🎯 High-Level API](#-high-level-api)
|
| 31 |
+
* [Gaussian Splats](#gaussian-splats)
|
| 32 |
+
* [Meshes](#meshes)
|
| 33 |
+
* [USD Assets (including meshes)](#usd-assets--including-meshes-)
|
| 34 |
+
+ [General USD Formats](#general-usd-formats)
|
| 35 |
+
+ [SimReady Format USD](#simready-format-usd)
|
| 36 |
+
- [🎨 Visualizing Material Results](#-visualizing-material-results)
|
| 37 |
+
- [🔧 Low-Level API](#-low-level-api)
|
| 38 |
+
* [Gaussian Splats](#gaussian-splats-1)
|
| 39 |
+
* [Meshes](#meshes-1)
|
| 40 |
+
* [USD Assets](#usd-assets)
|
| 41 |
+
- [🧩 Custom 3D Representations](#-custom-3d-representations)
|
| 42 |
+
- [🧬 Material Upsampler](#-material-upsampler)
|
| 43 |
+
- [💾 Using our Benchmark](#-using-our-benchmark)
|
| 44 |
+
* [Reproducing results from the paper](#reproducing-results-from-the-paper)
|
| 45 |
+
- [📦 Simulation](#-simulation)
|
| 46 |
+
* [Simplicits simulation](#simplicits-simulation)
|
| 47 |
+
* [FEM simulation using warp.fem](#fem-simulation-using-warpfem)
|
| 48 |
+
* [FEM simulation using libuipc](#fem-simulation-using-libuipc)
|
| 49 |
+
* [Newton simulation](#newton-simulation)
|
| 50 |
+
- [🤗 Credits](#-credits)
|
| 51 |
+
- [📜 Citation](#-citation)
|
| 52 |
+
- [License and Contact](#license-and-contact)
|
| 53 |
+
|
| 54 |
+
## 🔧 Dependencies and Installation
|
| 55 |
+
|
| 56 |
+
All the instructions in this README are meant to be run from the root of the repository. Running simulations requires a separate set of dependencies than this setup which we mention later in the [📦 Simulation](#-simulation) section.
|
| 57 |
+
|
| 58 |
+
### Setup a Virtual Environment (Recommended)
|
| 59 |
+
|
| 60 |
+
First set up the environment. We recommend using Python>=3.10, PyTorch>=2.1.0, and CUDA>=11.8. It is okay if some packages show warnings or fail to install due to version conflicts. The version conflicts are not a problem for the functionalities we use.
|
| 61 |
+
|
| 62 |
+
```bash
|
| 63 |
+
git clone --recursive https://github.com/nv-tlabs/VoMP
|
| 64 |
+
cd VoMP
|
| 65 |
+
|
| 66 |
+
# Install dependencies using the provided script (Linux only)
|
| 67 |
+
chmod +x install_env.sh
|
| 68 |
+
./install_env.sh
|
| 69 |
+
```
|
| 70 |
+
|
| 71 |
+
> [!NOTE]
|
| 72 |
+
> Running install_env.sh without conda: The script includes optional conda-only steps (environment creation/activation, installing CUDA toolkit inside the env, and setting env vars). If you're using a Python `venv` and don't have conda, the script will fail when it tries to call `conda`. You can either install conda, or comment out the conda-specific lines (lines 93-115 and any `conda install` / `conda env config vars set` commands). The rest of the script relies on `pip` and standard bash commands and will work in a `venv`.
|
| 73 |
+
|
| 74 |
+
### Install a Mesh Renderer (Required for Mesh Processing Only)
|
| 75 |
+
|
| 76 |
+
We only need a mesh renderer so you can download any one of Isaac Sim or Blender. There is no need to install both.
|
| 77 |
+
|
| 78 |
+
#### Isaac Sim
|
| 79 |
+
|
| 80 |
+
For mesh material estimation, you need to install Isaac Sim or Blender manually. *This is not required for Gaussian splat processing.*
|
| 81 |
+
|
| 82 |
+
Download Isaac Sim from [here](https://docs.isaacsim.omniverse.nvidia.com/5.0.0/installation/index.html) and follow the instructions to install it. On Linux, you would have a `isaac-sim.sh` file in the path you installed it. For Windows, you would have a `isaac-sim.bat` file in the path you installed it. Note the path to the `isaac-sim.sh` or `isaac-sim.bat` file.
|
| 83 |
+
|
| 84 |
+
> [!NOTE]
|
| 85 |
+
> You'll need to provide the Isaac Sim binary path when using mesh APIs.
|
| 86 |
+
|
| 87 |
+
> [!WARNING]
|
| 88 |
+
> We use Replicator in Isaac Sim to render meshes. Replicator supports USD assets. If you want to use a USD asset, since USD files can contain many things in many formats we expect you to have used [existing tools](https://openusd.org/release/toolset.html) to convert it into an explicit mesh. If you want to use a mesh asset, you can use Replicator by also having a USD file of your mesh that you can make by using [existing tools](https://openusd.org/release/toolset.html).
|
| 89 |
+
|
| 90 |
+
#### Blender
|
| 91 |
+
|
| 92 |
+
For mesh material estimation, you need to install Blender 3.0+ manually. *This is not required for Gaussian splat processing.*
|
| 93 |
+
|
| 94 |
+
```bash
|
| 95 |
+
# Install system dependencies
|
| 96 |
+
sudo apt-get update
|
| 97 |
+
sudo apt-get install -y libxrender1 libxi6 libxkbcommon-x11-0 libsm6
|
| 98 |
+
|
| 99 |
+
# Download and install Blender 3.0.1
|
| 100 |
+
wget https://download.blender.org/release/Blender3.0/blender-3.0.1-linux-x64.tar.xz
|
| 101 |
+
tar -xvf blender-3.0.1-linux-x64.tar.xz
|
| 102 |
+
|
| 103 |
+
# Note the path: ./blender-3.0.1-linux-x64/blender
|
| 104 |
+
```
|
| 105 |
+
|
| 106 |
+
> [!NOTE]
|
| 107 |
+
> You'll need to provide the Blender binary path when using mesh APIs:
|
| 108 |
+
> ```python
|
| 109 |
+
> results = model.get_mesh_materials("mesh.obj", blender_path="/path/to/blender")
|
| 110 |
+
> ```
|
| 111 |
+
|
| 112 |
+
### Setup a Conda Environment (Alternative)
|
| 113 |
+
|
| 114 |
+
We also provide a conda environment file to install the dependencies. This automatically creates a new environment:
|
| 115 |
+
|
| 116 |
+
```bash
|
| 117 |
+
# Create and install environment from file (creates 'vomp' environment)
|
| 118 |
+
conda env create -f environment.yml
|
| 119 |
+
|
| 120 |
+
# Activate the environment
|
| 121 |
+
conda activate vomp
|
| 122 |
+
```
|
| 123 |
+
|
| 124 |
+
> [!WARNING]
|
| 125 |
+
> We do not recommend using this installation method. The conda environment file is accurate but it reflects the environment at its final stage and does not have the step-by-step process we use to install the dependencies.
|
| 126 |
+
|
| 127 |
+
### Trained Models
|
| 128 |
+
|
| 129 |
+
We provide the trained models (1.73 GB) in <a href='https://huggingface.co/nvidia/PhysicalAI-Simulation-VoMP-Model'><img src='https://img.shields.io/badge/%F0%9F%A4%97%20-Models-yellow'></a>. Download the models and place them in the `weights/` directory. The checkpoints we will use are the `weights/matvae.safetensors` and `weights/geometry_transformer.pt` files.
|
| 130 |
+
|
| 131 |
+
The above two files from the model repository contains the final checkpoint of the model. If you need to fine-tune the model, you can follow the same process but download the `ft` directory from the HuggingFace repo too and place them in the `weights/` directory.
|
| 132 |
+
|
| 133 |
+
| **File** | **Model** |
|
| 134 |
+
|------|----------------|
|
| 135 |
+
| `matvae.safetensors` | MatVAE |
|
| 136 |
+
| `geometry_transformer.pt` | Geometry Transformer |
|
| 137 |
+
| `normalization_params.json` | Normalization Parameters |
|
| 138 |
+
| `inference.json` | Inference Configuration |
|
| 139 |
+
| `ft` | MatVAE and Geometry Transformer checkpoints (same as above but in a directory structure compatible for fine-tuning) |
|
| 140 |
+
|
| 141 |
+
## 🌐 Quickstart: Web Demo
|
| 142 |
+
|
| 143 |
+
We provide a simple web demo to quickly test out VoMP in a GUI. The web-demo uses some additional dependecies over the base environment, see [`gradio/requirements.txt`](./gradio/requirements.txt). To start the web demo, run:
|
| 144 |
+
|
| 145 |
+
```bash
|
| 146 |
+
python gradio/app.py
|
| 147 |
+
```
|
| 148 |
+
|
| 149 |
+
Then, you can access the demo at the address shown in the terminal. The web demo allows you to run the model, visualize the outputs of the model and download an artifact which can directly be used for [📦 Simulation](#-simulation).
|
| 150 |
+
|
| 151 |
+
## 📥 Loading the Model
|
| 152 |
+
|
| 153 |
+
Before using any of the VoMP APIs, you need to load the model. We provide flexible loading options:
|
| 154 |
+
|
| 155 |
+
### Using Inference Config (Recommended)
|
| 156 |
+
|
| 157 |
+
The simplest way to load the model is using the inference configuration file:
|
| 158 |
+
|
| 159 |
+
```python
|
| 160 |
+
from vomp.inference import Vomp
|
| 161 |
+
|
| 162 |
+
# Load model using inference config (recommended - uses final_ckpt.zip weights)
|
| 163 |
+
model = Vomp.from_checkpoint(
|
| 164 |
+
config_path="weights/inference.json",
|
| 165 |
+
use_trt=False # Set to True to enable TensorRT acceleration (significantly faster but requires `torch-tensorrt`)
|
| 166 |
+
)
|
| 167 |
+
```
|
| 168 |
+
|
| 169 |
+
> [!NOTE]
|
| 170 |
+
> Using the `use_trt` flag will compile the DINO model with TensorRT. This makes the `from_checkpoint` function slower.
|
| 171 |
+
|
| 172 |
+
### Using Direct File Paths
|
| 173 |
+
|
| 174 |
+
For more control, you can specify exact checkpoint files, optionally overriding the inference config:
|
| 175 |
+
|
| 176 |
+
```python
|
| 177 |
+
# Load model using direct file paths
|
| 178 |
+
model = Vomp.from_checkpoint(
|
| 179 |
+
config_path="weights/inference.json",
|
| 180 |
+
geometry_checkpoint_dir="weights/geometry_transformer.pt",
|
| 181 |
+
matvae_checkpoint_dir="weights/matvae.safetensors",
|
| 182 |
+
normalization_params_path="weights/normalization_params.json"
|
| 183 |
+
)
|
| 184 |
+
|
| 185 |
+
# Or override specific paths from inference config
|
| 186 |
+
model = Vomp.from_checkpoint(
|
| 187 |
+
config_path="configs/materials/inference.json",
|
| 188 |
+
geometry_checkpoint_dir="custom/path/to/geometry_transformer.pt" # Override just this path
|
| 189 |
+
)
|
| 190 |
+
```
|
| 191 |
+
|
| 192 |
+
### Using Directories (use for fine-tuning)
|
| 193 |
+
|
| 194 |
+
Use this approach only if you are using the fine-tuning checkpoints i.e. the `ft/` directory in the model repository. This lets the model auto-find the latest checkpoints:
|
| 195 |
+
|
| 196 |
+
```python
|
| 197 |
+
from vomp.inference import Vomp
|
| 198 |
+
|
| 199 |
+
# Load model using directories (auto-finds latest checkpoints)
|
| 200 |
+
model = Vomp.from_checkpoint(
|
| 201 |
+
config_path="weights/inference.json",
|
| 202 |
+
geometry_checkpoint_dir="weights/ft/geometry_transformer",
|
| 203 |
+
matvae_checkpoint_dir="weights/ft/matvae",
|
| 204 |
+
normalization_params_path="weights/ft/matvae/normalization_params.json",
|
| 205 |
+
geometry_ckpt="latest" # Can also be a specific step number
|
| 206 |
+
)
|
| 207 |
+
```
|
| 208 |
+
|
| 209 |
+
We provide a flexible Python API with both high-level and low-level interfaces for material property estimation.
|
| 210 |
+
|
| 211 |
+
## 🎯 High-Level API
|
| 212 |
+
|
| 213 |
+
### Gaussian Splats
|
| 214 |
+
|
| 215 |
+
For Gaussian splats, use the high-level API for the easiest experience (see [Loading the Model](#-loading-the-model) section first):
|
| 216 |
+
|
| 217 |
+
```python
|
| 218 |
+
from vomp.inference.utils import save_materials
|
| 219 |
+
|
| 220 |
+
# Get materials directly from PLY (auto-handles Gaussian loading)
|
| 221 |
+
# By default, returns materials evaluated at each Gaussian splat center
|
| 222 |
+
results = model.get_splat_materials("path/to/your/gaussian_splat.ply")
|
| 223 |
+
|
| 224 |
+
# Or use Kaolin voxelizer for more accurate results
|
| 225 |
+
# results = model.get_splat_materials("path/to/your/gaussian_splat.ply", voxel_method="kaolin")
|
| 226 |
+
|
| 227 |
+
# Control where materials are evaluated using query_points:
|
| 228 |
+
# results = model.get_splat_materials("path/to/your/gaussian_splat.ply", query_points="splat_centers") # Default
|
| 229 |
+
# results = model.get_splat_materials("path/to/your/gaussian_splat.ply", query_points="voxel_centers") # Voxel centers (direct output of the model)
|
| 230 |
+
# results = model.get_splat_materials("path/to/your/gaussian_splat.ply", query_points=custom_points) # Custom (N,3) array
|
| 231 |
+
|
| 232 |
+
# Adjust DINO batch size for performance (higher values use more GPU memory)
|
| 233 |
+
# results = model.get_splat_materials("path/to/your/gaussian_splat.ply", dino_batch_size=32)
|
| 234 |
+
|
| 235 |
+
# Save results
|
| 236 |
+
save_materials(results, "materials.npz")
|
| 237 |
+
```
|
| 238 |
+
|
| 239 |
+
### Meshes
|
| 240 |
+
|
| 241 |
+
For mesh objects, use the equivalent high-level mesh API (see [Loading the Model](#-loading-the-model) section first):
|
| 242 |
+
|
| 243 |
+
```python
|
| 244 |
+
from vomp.inference.utils import save_materials
|
| 245 |
+
|
| 246 |
+
# Get materials directly from mesh file (supports OBJ, PLY, STL, USD)
|
| 247 |
+
# By default, returns materials evaluated at each mesh vertex (not recommended if you have vertices only on the surface)
|
| 248 |
+
# Note: Requires Blender installation (see Dependencies section)
|
| 249 |
+
results = model.get_mesh_materials(
|
| 250 |
+
"path/to/your/mesh.obj",
|
| 251 |
+
blender_path="/tmp/blender-3.0.1-linux-x64/blender" # Adjust path as needed
|
| 252 |
+
)
|
| 253 |
+
|
| 254 |
+
# Control where materials are evaluated using query_points:
|
| 255 |
+
# results = model.get_mesh_materials("path/to/your/mesh.obj", query_points="mesh_vertices") # Default
|
| 256 |
+
# results = model.get_mesh_materials("path/to/your/mesh.obj", query_points="voxel_centers") # Voxel centers (direct output of the model)
|
| 257 |
+
# results = model.get_mesh_materials("path/to/your/mesh.obj", query_points=custom_points) # Custom (N,3) array
|
| 258 |
+
|
| 259 |
+
# Use parallel rendering and adjust DINO batch size for better performance
|
| 260 |
+
# results = model.get_mesh_materials("path/to/your/mesh.obj", num_render_jobs=4, dino_batch_size=32, blender_path="/path/to/blender")
|
| 261 |
+
|
| 262 |
+
# Save results
|
| 263 |
+
save_materials(results, "materials.npz")
|
| 264 |
+
```
|
| 265 |
+
|
| 266 |
+
### USD Assets (including meshes)
|
| 267 |
+
|
| 268 |
+
USD files can come in many different formats with varying internal structures, materials, and organization. For USD assets, use the high-level USD API (see [Loading the Model](#-loading-the-model) section first):
|
| 269 |
+
|
| 270 |
+
#### General USD Formats
|
| 271 |
+
|
| 272 |
+
For USD files in any format, use [Isaac Sim Replicator](https://docs.isaacsim.omniverse.nvidia.com/5.1.0/replicator_tutorials/index.html) rendering with a separate mesh file for voxelization:
|
| 273 |
+
|
| 274 |
+
```python
|
| 275 |
+
from vomp.inference.utils import save_materials
|
| 276 |
+
|
| 277 |
+
# For general USD files - requires Isaac Sim and separate mesh
|
| 278 |
+
# Note: Requires Isaac Sim installation and a separate mesh file for voxelization
|
| 279 |
+
# Isaac Sim renders the USD while the mesh is used for voxelization
|
| 280 |
+
results = model.get_usd_materials(
|
| 281 |
+
usd_path="path/to/your/model.usd",
|
| 282 |
+
mesh_path="path/to/your/model.ply", # Mesh for voxelization (doesn't need to be normalized)
|
| 283 |
+
isaac_sim_path="~/isaac-sim/isaac-sim.sh", # Or set ISAAC_SIM_PATH environment variable
|
| 284 |
+
render_mode="path_tracing" # Options: "fast" or "path_tracing"
|
| 285 |
+
)
|
| 286 |
+
|
| 287 |
+
# Control where materials are evaluated using query_points:
|
| 288 |
+
# results = model.get_usd_materials(..., query_points="voxel_centers") # Default (direct output)
|
| 289 |
+
# results = model.get_usd_materials(..., query_points=custom_points) # Custom (N,3) array
|
| 290 |
+
|
| 291 |
+
# Adjust DINO batch size for performance (higher values use more GPU memory):
|
| 292 |
+
# results = model.get_usd_materials(..., dino_batch_size=32)
|
| 293 |
+
|
| 294 |
+
# Save results
|
| 295 |
+
save_materials(results, "materials.npz")
|
| 296 |
+
```
|
| 297 |
+
|
| 298 |
+
Isaac Sim Replicator provides flexible rendering modes:
|
| 299 |
+
|
| 300 |
+
```python
|
| 301 |
+
# Option 1: Fast Mode - Real-time ray tracing
|
| 302 |
+
results = model.get_usd_materials(
|
| 303 |
+
usd_path="model.usd",
|
| 304 |
+
mesh_path="model.ply",
|
| 305 |
+
isaac_sim_path="~/isaac-sim/isaac-sim.sh",
|
| 306 |
+
render_mode="fast" # Real-time ray tracing
|
| 307 |
+
)
|
| 308 |
+
|
| 309 |
+
# Option 2: Path Tracing - Higher quality
|
| 310 |
+
results = model.get_usd_materials(
|
| 311 |
+
usd_path="model.usd",
|
| 312 |
+
mesh_path="model.ply",
|
| 313 |
+
isaac_sim_path="~/isaac-sim/isaac-sim.sh",
|
| 314 |
+
render_mode="path_tracing" # 256 spp, 8 bounces, denoising enabled
|
| 315 |
+
)
|
| 316 |
+
|
| 317 |
+
# Option 3: start from a setting and override some RTX settings
|
| 318 |
+
from vomp.inference import RTX_PRESETS
|
| 319 |
+
print(RTX_PRESETS.keys()) # See available presets: ['fast', 'path_tracing']
|
| 320 |
+
|
| 321 |
+
results = model.get_usd_materials(
|
| 322 |
+
usd_path="model.usd",
|
| 323 |
+
mesh_path="model.ply",
|
| 324 |
+
isaac_sim_path="~/isaac-sim/isaac-sim.sh",
|
| 325 |
+
render_mode="path_tracing",
|
| 326 |
+
rtx_settings_override={
|
| 327 |
+
# Enable path tracing renderer
|
| 328 |
+
"/rtx/rendermode": "PathTracing",
|
| 329 |
+
|
| 330 |
+
# Path tracing quality settings
|
| 331 |
+
"/rtx/pathtracing/spp": 256, # Samples per pixel (higher = better quality, slower)
|
| 332 |
+
"/rtx/pathtracing/totalSpp": 256, # Total samples per pixel
|
| 333 |
+
"/rtx/pathtracing/maxBounces": 8, # Maximum light bounces
|
| 334 |
+
"/rtx/pathtracing/maxSpecularAndTransmissionBounces": 8,
|
| 335 |
+
|
| 336 |
+
# Additional quality settings
|
| 337 |
+
"/rtx/pathtracing/fireflyFilter/enable": True, # Reduce fireflies (bright pixels)
|
| 338 |
+
"/rtx/pathtracing/optixDenoiser/enabled": True, # Enable denoiser for clean renders
|
| 339 |
+
|
| 340 |
+
# ... other RTX settings you want to override
|
| 341 |
+
}
|
| 342 |
+
)
|
| 343 |
+
```
|
| 344 |
+
|
| 345 |
+
> [!WARNING]
|
| 346 |
+
> Please do not override the following RTX settings, as they are required for the model to work correctly:
|
| 347 |
+
> - "/rtx/post/backgroundZeroAlpha/enabled": True,
|
| 348 |
+
> - "/rtx/post/backgroundZeroAlpha/backgroundComposite": False,
|
| 349 |
+
> - "/rtx/post/backgroundZeroAlpha/outputAlphaInComposite": True,
|
| 350 |
+
|
| 351 |
+
#### SimReady Format USD
|
| 352 |
+
|
| 353 |
+
If your USD file is in the **SimReady format** (like the USD files in our dataset), you can use the following arguments:
|
| 354 |
+
|
| 355 |
+
```python
|
| 356 |
+
from vomp.inference.utils import save_materials
|
| 357 |
+
|
| 358 |
+
results = model.get_usd_materials(
|
| 359 |
+
usd_path="model.usd",
|
| 360 |
+
use_simready_usd_format=True,
|
| 361 |
+
blender_path="/path/to/blender",
|
| 362 |
+
seed=42
|
| 363 |
+
)
|
| 364 |
+
|
| 365 |
+
# Control where materials are evaluated using query_points:
|
| 366 |
+
# results = model.get_usd_materials(..., query_points="voxel_centers") # Default (direct output)
|
| 367 |
+
# results = model.get_usd_materials(..., query_points=custom_points) # Custom (N,3) array
|
| 368 |
+
|
| 369 |
+
# Save results
|
| 370 |
+
save_materials(results, "materials.npz")
|
| 371 |
+
```
|
| 372 |
+
|
| 373 |
+
## 🎨 Visualizing Material Results
|
| 374 |
+
|
| 375 |
+
After estimating material properties, you can visualize them using our interactive `polyscope`-based viewer.
|
| 376 |
+
|
| 377 |
+
```python
|
| 378 |
+
# After getting results from any API
|
| 379 |
+
from vomp.inference.utils import save_materials
|
| 380 |
+
|
| 381 |
+
# Save your results
|
| 382 |
+
save_materials(results, "my_materials.npz")
|
| 383 |
+
```
|
| 384 |
+
|
| 385 |
+
```bash
|
| 386 |
+
# Launch interactive property viewer
|
| 387 |
+
python scripts/viewer.py my_materials.npz
|
| 388 |
+
```
|
| 389 |
+
|
| 390 |
+
The viewer also saves the colorbars for visualizations as PNG images that look like this:
|
| 391 |
+
|
| 392 |
+

|
| 393 |
+
|
| 394 |
+
## 🔧 Low-Level API
|
| 395 |
+
|
| 396 |
+
### Gaussian Splats
|
| 397 |
+
|
| 398 |
+
For fine-grained control with Gaussian splats, use the low-level API (see [Loading the Model](#-loading-the-model) section first):
|
| 399 |
+
|
| 400 |
+
```python
|
| 401 |
+
from vomp.representations.gaussian import Gaussian
|
| 402 |
+
from vomp.inference.utils import save_materials
|
| 403 |
+
|
| 404 |
+
# Load Gaussian splat
|
| 405 |
+
gaussian = Gaussian(sh_degree=3, aabb=[-1,-1,-1,2,2,2], device="cuda")
|
| 406 |
+
gaussian.load_ply("path/to/your/gaussian_splat.ply")
|
| 407 |
+
|
| 408 |
+
# Step-by-step pipeline
|
| 409 |
+
output_dir = "outputs/materials"
|
| 410 |
+
renders_metadata = model.render_sampled_views(gaussian, output_dir, num_views=150)
|
| 411 |
+
voxel_centers = model._voxelize_gaussian(gaussian, output_dir)
|
| 412 |
+
coords, features = model._extract_dino_features(output_dir, voxel_centers, renders_metadata, save_features=True)
|
| 413 |
+
results = model.predict_materials(coords, features)
|
| 414 |
+
save_materials(results, "materials.npz")
|
| 415 |
+
```
|
| 416 |
+
|
| 417 |
+
### Meshes
|
| 418 |
+
|
| 419 |
+
For fine-grained control with meshes, use the equivalent low-level mesh API (see [Loading the Model](#-loading-the-model) section first):
|
| 420 |
+
|
| 421 |
+
```python
|
| 422 |
+
from vomp.inference.utils import save_materials
|
| 423 |
+
|
| 424 |
+
# Step-by-step pipeline for meshes
|
| 425 |
+
output_dir = "outputs/materials"
|
| 426 |
+
mesh_path = "path/to/your/mesh.obj"
|
| 427 |
+
blender_path = "/tmp/blender-3.0.1-linux-x64/blender" # Adjust for your installation
|
| 428 |
+
renders_metadata = model.render_mesh_views(mesh_path, output_dir, num_views=150, blender_path=blender_path)
|
| 429 |
+
voxel_centers = model._voxelize_mesh(mesh_path, output_dir)
|
| 430 |
+
coords, features = model._extract_dino_features(output_dir, voxel_centers, renders_metadata, save_features=True)
|
| 431 |
+
results = model.predict_materials(coords, features)
|
| 432 |
+
save_materials(results, "materials.npz")
|
| 433 |
+
```
|
| 434 |
+
|
| 435 |
+
### USD Assets
|
| 436 |
+
|
| 437 |
+
For fine-grained control with USD assets using Replicator rendering (see [Loading the Model](#-loading-the-model) section first):
|
| 438 |
+
|
| 439 |
+
```python
|
| 440 |
+
from vomp.inference.utils import save_materials
|
| 441 |
+
|
| 442 |
+
# Step-by-step pipeline for USD assets with Replicator
|
| 443 |
+
output_dir = "outputs/materials"
|
| 444 |
+
usd_path = "path/to/your/model.usd"
|
| 445 |
+
mesh_path = "path/to/your/model.ply" # For voxelization
|
| 446 |
+
isaac_sim_path = "~/isaac-sim/isaac-sim.sh"
|
| 447 |
+
|
| 448 |
+
# Render using Replicator (with custom settings)
|
| 449 |
+
renders_metadata = model.render_views_replicator(
|
| 450 |
+
asset_path=usd_path,
|
| 451 |
+
output_dir=output_dir,
|
| 452 |
+
num_views=150,
|
| 453 |
+
isaac_sim_path=isaac_sim_path,
|
| 454 |
+
render_mode="path_tracing", # or "fast"
|
| 455 |
+
rtx_settings_override={
|
| 456 |
+
"/rtx/pathtracing/spp": 512 # Optional: custom settings
|
| 457 |
+
}
|
| 458 |
+
)
|
| 459 |
+
|
| 460 |
+
# Voxelize and extract features
|
| 461 |
+
voxel_centers = model._voxelize_mesh(mesh_path, output_dir)
|
| 462 |
+
coords, features = model._extract_dino_features(output_dir, voxel_centers, renders_metadata, save_features=True)
|
| 463 |
+
results = model.predict_materials(coords, features)
|
| 464 |
+
save_materials(results, "materials.npz")
|
| 465 |
+
```
|
| 466 |
+
|
| 467 |
+
## 🧩 Custom 3D Representations
|
| 468 |
+
|
| 469 |
+
Bring your own 3D representation with custom render/voxelize functions (see [Loading the Model](#-loading-the-model) section first):
|
| 470 |
+
|
| 471 |
+
```python
|
| 472 |
+
from vomp.inference.utils import save_materials
|
| 473 |
+
|
| 474 |
+
def my_render_func(obj, output_dir, num_views, image_size, **kwargs):
|
| 475 |
+
# Your rendering code here - save images to output_dir/renders/
|
| 476 |
+
frames_metadata = []
|
| 477 |
+
for i in range(num_views):
|
| 478 |
+
# Your custom rendering logic
|
| 479 |
+
frames_metadata.append({
|
| 480 |
+
"file_path": f"frame_{i:04d}.png",
|
| 481 |
+
"transform_matrix": camera_matrix.tolist(), # 4x4 matrix
|
| 482 |
+
"camera_angle_x": fov_radians
|
| 483 |
+
})
|
| 484 |
+
return frames_metadata
|
| 485 |
+
|
| 486 |
+
def my_voxelize_func(obj, output_dir, **kwargs):
|
| 487 |
+
# Your voxelization code here
|
| 488 |
+
voxel_centers = your_voxelization_method(obj) # (N, 3) array
|
| 489 |
+
return voxel_centers
|
| 490 |
+
|
| 491 |
+
# Use with any 3D representation
|
| 492 |
+
coords, features = model.get_features(
|
| 493 |
+
obj_3d=your_mesh,
|
| 494 |
+
render_func=my_render_func,
|
| 495 |
+
voxelize_func=my_voxelize_func,
|
| 496 |
+
num_views=150
|
| 497 |
+
)
|
| 498 |
+
|
| 499 |
+
# Get materials
|
| 500 |
+
results = model.predict_materials(coords, features)
|
| 501 |
+
save_materials(results, "materials.npz")
|
| 502 |
+
```
|
| 503 |
+
|
| 504 |
+
## 🧬 Material Upsampler
|
| 505 |
+
|
| 506 |
+
The high-level splat API (`get_splat_materials`) automatically returns materials interpolated to splat centers. However, you may want to upsample materials to other locations like higher resolution grids or custom query points. We provide a utility class for these cases (see [Loading the Model](#-loading-the-model) section first).
|
| 507 |
+
|
| 508 |
+
```python
|
| 509 |
+
import numpy as np
|
| 510 |
+
from vomp.inference.utils import MaterialUpsampler
|
| 511 |
+
from vomp.representations.gaussian import Gaussian
|
| 512 |
+
|
| 513 |
+
# Get voxel-level materials (needed for upsampling to custom locations)
|
| 514 |
+
# Note: Use query_points="voxel_centers" to get voxel-level results
|
| 515 |
+
voxel_results = model.get_splat_materials("path/to/your/gaussian_splat.ply", query_points="voxel_centers")
|
| 516 |
+
# OR for meshes
|
| 517 |
+
# voxel_results = model.get_mesh_materials("path/to/your/mesh.obj", query_points="voxel_centers", blender_path="/path/to/blender")
|
| 518 |
+
|
| 519 |
+
# Create upsampler from voxel-level results
|
| 520 |
+
upsampler = MaterialUpsampler(
|
| 521 |
+
voxel_coords=voxel_results["voxel_coords_world"],
|
| 522 |
+
voxel_materials=np.column_stack([
|
| 523 |
+
voxel_results["youngs_modulus"],
|
| 524 |
+
voxel_results["poisson_ratio"],
|
| 525 |
+
voxel_results["density"]
|
| 526 |
+
])
|
| 527 |
+
)
|
| 528 |
+
|
| 529 |
+
# Example 1: Interpolate to Gaussian centers manually (usually not needed - high-level API does this)
|
| 530 |
+
gaussian = Gaussian(sh_degree=3, aabb=[-1,-1,-1,2,2,2], device="cuda")
|
| 531 |
+
gaussian.load_ply("path/to/your/gaussian_splat.ply")
|
| 532 |
+
gaussian_materials, gaussian_distances = upsampler.interpolate_to_gaussians(gaussian)
|
| 533 |
+
|
| 534 |
+
# Example 2: Interpolate to higher resolution grid (128x128x128) - main use case for manual upsampling
|
| 535 |
+
x = np.linspace(-0.5, 0.5, 128)
|
| 536 |
+
xx, yy, zz = np.meshgrid(x, x, x)
|
| 537 |
+
high_res_points = np.column_stack([xx.ravel(), yy.ravel(), zz.ravel()])
|
| 538 |
+
high_res_materials, high_res_distances = upsampler.interpolate(high_res_points)
|
| 539 |
+
|
| 540 |
+
# Example 3: Interpolate to custom query points - another main use case for manual upsampling
|
| 541 |
+
query_points = np.random.uniform(-0.4, 0.4, size=(1000, 3))
|
| 542 |
+
query_materials, query_distances = upsampler.interpolate(query_points)
|
| 543 |
+
|
| 544 |
+
# Save results
|
| 545 |
+
upsampler.save_results(gaussian.get_xyz.detach().cpu().numpy(), gaussian_materials,
|
| 546 |
+
gaussian_distances, "gaussian_materials.npz")
|
| 547 |
+
upsampler.save_results(high_res_points, high_res_materials, high_res_distances, "high_res_materials.npz")
|
| 548 |
+
upsampler.save_results(query_points, query_materials, query_distances, "custom_materials.npz")
|
| 549 |
+
```
|
| 550 |
+
|
| 551 |
+
## 💾 Using our Benchmark
|
| 552 |
+
|
| 553 |
+
> [!NOTE]
|
| 554 |
+
> Due to licenses we are unable to make the vegetation subset of the dataset public. Thus, when you compare outputs to the paper make sure to compare them to the listed results on the "public dataset" (Table 2 and Table 3).
|
| 555 |
+
|
| 556 |
+
We provide a dataset and a benchmark with fine-grained volumetric mechanical properties (65.9 GB) at <a href='https://huggingface.co/datasets/nvidia/PhysicalAI-Robotics-PhysicalAssets-VoMP'><img src='https://img.shields.io/badge/%F0%9F%A4%97%20-Dataset-yellow'></a> (or preprocess it yourself using the instructions in [README_train.md](./README_train.md)). We also provide code allowing the evaluation of models on this dataset.
|
| 557 |
+
|
| 558 |
+
### Reproducing results from the paper
|
| 559 |
+
|
| 560 |
+
Since our dataset is quite large, we provide a way to download only the test set by running the following command:
|
| 561 |
+
|
| 562 |
+
```bash
|
| 563 |
+
huggingface-cli download nvidia/PhysicalAI-Robotics-PhysicalAssets-VoMP-Eval --repo-type dataset --local-dir datasets/simready
|
| 564 |
+
```
|
| 565 |
+
|
| 566 |
+
We can now run VoMP on the test set:
|
| 567 |
+
|
| 568 |
+
```bash
|
| 569 |
+
python scripts/evaluate_geometry_encoder.py \
|
| 570 |
+
--config weights/inference.json \ # replace with your own config file
|
| 571 |
+
--checkpoint_dir weights/ft/geometry_transformer \ # replace with your own checkpoint directory
|
| 572 |
+
--data_dir datasets/simready \ # replace with your own data directory
|
| 573 |
+
# --ckpt latest \
|
| 574 |
+
# --results
|
| 575 |
+
```
|
| 576 |
+
|
| 577 |
+
This script requires loading the model in the ["Using Directories" method](#using-directories).
|
| 578 |
+
|
| 579 |
+
This prints out many detailed metrics. Particularly, you can also make sure you can reproduce the main results from the paper by comparing Table 2 and Appendix Table 3 from the paper with the outputs from Section 5 (SUMMARY TABLES) of the results printed out.
|
| 580 |
+
|
| 581 |
+
To build on top of our benchmark, you can replace the `load_model` and `evaluate_model` functions in the `scripts/evaluate_geometry_encoder.py` script with your own model and evaluation code.
|
| 582 |
+
|
| 583 |
+
## 📦 Simulation
|
| 584 |
+
|
| 585 |
+
Our properties are compatible with all simulators. We provide instructions to run a few kinds of simulations with the properties.
|
| 586 |
+
|
| 587 |
+
### Simplicits simulation
|
| 588 |
+
|
| 589 |
+
For the large-scale simulations that we perform with [Simplicits](https://research.nvidia.com/labs/toronto-ai/simplicits/), refer to the [Simplicits](https://kaolin.readthedocs.io/en/latest/notes/simplicits.html) documentation.
|
| 590 |
+
|
| 591 |
+
### FEM simulation using warp.fem
|
| 592 |
+
|
| 593 |
+
We provide a way to run FEM simulations using [`warp.fem`](https://nvidia.github.io/warp/modules/fem.html).
|
| 594 |
+
|
| 595 |
+
```bash
|
| 596 |
+
cd simulation/warp.fem
|
| 597 |
+
PYTHONPATH=./ python drop_tetmesh.py --mesh assets/cube_res20.msh --materials assets/cube_materials_two_halves.npz
|
| 598 |
+
```
|
| 599 |
+
|
| 600 |
+
This simple example has an artificially constructed NPZ file which can be used in `warp.fem`. This requires installing [`warp`](https://nvidia.github.io/warp/) and `meshio`.
|
| 601 |
+
|
| 602 |
+
### FEM simulation using libuipc
|
| 603 |
+
|
| 604 |
+
We provide a way to run FEM simulations using [`libuipc`](https://github.com/spiriMirror/libuipc/). These simulations use the config files in the `configs/sim/` directory and they can be run as,
|
| 605 |
+
|
| 606 |
+
```bash
|
| 607 |
+
python vomp/sim/main.py configs/sim/falling_oranges.json
|
| 608 |
+
```
|
| 609 |
+
|
| 610 |
+
This config runs a simulation of falling oranges (Figure 5 from the paper) with the NPZ files we generated from the VoMP model.
|
| 611 |
+
|
| 612 |
+
These simulations require a `.npz` file with the estimated mechanical properties of the object. This requires installing the Python version of `libuipc` using the instructions in the [`libuipc`](https://github.com/spiriMirror/libuipc/) repository. The command above will run the simulation, show it in a GUI, and save framewise surface meshes in the `outputs/simulation_output/falling_oranges` directory. The config also specifies a visual textured surface mesh so the per frame visualizations will use the high resolution visual mesh and also have textures.
|
| 613 |
+
|
| 614 |
+
### Newton simulation
|
| 615 |
+
|
| 616 |
+
We provide a way to run [Newton](https://github.com/newton-physics/newton/) simulations. Run an example simulation of a soft body cube with the NPZ files we generated from the VoMP model by running the following command:
|
| 617 |
+
|
| 618 |
+
```bash
|
| 619 |
+
cd simulation/newton
|
| 620 |
+
python mesh_falling_sim.py --grid_dim 16 --materials cube_high_E.npz
|
| 621 |
+
python mesh_falling_sim.py --grid_dim 16 --materials cube_low_E.npz
|
| 622 |
+
```
|
| 623 |
+
|
| 624 |
+
This simple example has two artificially constructed NPZ files which can be used in Newton. Observe the difference in simulation showing all Young's modulus, Poisson's ratio, and density values were properly applied. This requires installing [`newton`](https://github.com/newton-physics/newton/) and `meshio`.
|
| 625 |
+
|
| 626 |
+
> [!NOTE]
|
| 627 |
+
> Our properties are also compatible with [PhysX](https://developer.nvidia.com/physx-sdk) and rigid-body simulators. We plan to release some example code to do so at a later date. Until then, if you want to use our properties in PhysX, we recommend clustering the properties we produce, split the underlying meshes based on the clusters, and then add the averaged property for each such "connected part".
|
| 628 |
+
|
| 629 |
+
## 🤗 Credits
|
| 630 |
+
|
| 631 |
+
We are also grateful to several other open-source repositories that we drew inspiration from or built upon during the development of our pipeline:
|
| 632 |
+
|
| 633 |
+
- [DINOv2](https://github.com/facebookresearch/dinov2)
|
| 634 |
+
- [fTetWild](https://github.com/wildmeshing/fTetWild)
|
| 635 |
+
- [gaussian-splatting](https://github.com/graphdeco-inria/gaussian-splatting)
|
| 636 |
+
- [Isaac Sim](https://developer.nvidia.com/isaac/sim)
|
| 637 |
+
- [kaolin](https://github.com/NVIDIAGameWorks/kaolin)
|
| 638 |
+
- [libuipc](https://github.com/spiriMirror/libuipc)
|
| 639 |
+
- [newton](https://github.com/newton-physics/newton)
|
| 640 |
+
- [Simplicits](https://research.nvidia.com/labs/toronto-ai/simplicits/)
|
| 641 |
+
- [textgrad](https://github.com/zou-group/textgrad)
|
| 642 |
+
- [TRELLIS](https://github.com/microsoft/TRELLIS)
|
| 643 |
+
- [Warp](https://nvidia.github.io/warp/)
|
| 644 |
+
|
| 645 |
+
## 📜 Citation
|
| 646 |
+
|
| 647 |
+
If you find VoMP helpful, please consider citing:
|
| 648 |
+
|
| 649 |
+
```bibtex
|
| 650 |
+
@inproceedings{dagli2026vomp,
|
| 651 |
+
title={Vo{MP}: Predicting Volumetric Mechanical Property Fields},
|
| 652 |
+
author={Rishit Dagli and Donglai Xiang and Vismay Modi and Charles Loop and Clement Fuji Tsang and Anka He Chen and Anita Hu and Gavriel State and David Levin I.W. and Maria Shugrina},
|
| 653 |
+
booktitle={The Fourteenth International Conference on Learning Representations},
|
| 654 |
+
year={2026},
|
| 655 |
+
url={https://openreview.net/forum?id=aTP1IM6alo}
|
| 656 |
+
}
|
| 657 |
+
```
|
| 658 |
+
|
| 659 |
+
## License and Contact
|
| 660 |
+
|
| 661 |
+
This project will download and install additional third-party open source software projects. Review the license terms of these open source projects before use.
|
| 662 |
+
|
| 663 |
+
VoMP source code is released under the [Apache 2 License](https://www.apache.org/licenses/LICENSE-2.0).
|
| 664 |
+
|
| 665 |
+
VoMP models are released under the [NVIDIA Open Model License](https://www.nvidia.com/en-us/agreements/enterprise-software/nvidia-open-model-license). For a custom license, please visit our website and submit the form: [NVIDIA Research Licensing](https://www.nvidia.com/en-us/research/inquiries/).
|
deps/vomp/README_train.md
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<div align="center">
|
| 2 |
+
<h2>VoMP: Predicting Volumetric Mechanical Properties</h2>
|
| 3 |
+
|
| 4 |
+
<a href="https://arxiv.org/abs/2510.22975"><img src='https://img.shields.io/badge/arXiv-VoMP-red' alt='Paper PDF'></a>
|
| 5 |
+
<a href='https://research.nvidia.com/labs/sil/projects/vomp/'><img src='https://img.shields.io/badge/Project_Page-VoMP-green' alt='Project Page'></a>
|
| 6 |
+
<a href='https://huggingface.co/nvidia/PhysicalAI-Simulation-VoMP-Model'><img src='https://img.shields.io/badge/%F0%9F%A4%97%20-Models-yellow'></a>
|
| 7 |
+
<a href='https://huggingface.co/datasets/nvidia/PhysicalAI-Robotics-PhysicalAssets-VoMP'><img src='https://img.shields.io/badge/%F0%9F%A4%97%20-GVM%20Dataset-yellow'></a>
|
| 8 |
+
</div>
|
| 9 |
+
|
| 10 |
+

|
| 11 |
+
|
| 12 |
+
This repository provides the implementation of **VoMP**. TL;DR: Feed-forward, fine-grained, physically based volumetric material properties from Splats, Meshes, NeRFs, etc. which can be used to produce realistic worlds.
|
| 13 |
+
|
| 14 |
+
---
|
| 15 |
+
|
| 16 |
+
## Contents
|
| 17 |
+
|
| 18 |
+
- [🔧 Setup](#-setup)
|
| 19 |
+
- [📖 Overview of the codebase](#-overview-of-the-codebase)
|
| 20 |
+
- [📚 Create the dataset](#-create-the-dataset)
|
| 21 |
+
* [Preprocessed Datasets](#preprocessed-datasets)
|
| 22 |
+
* [Material Triplet Dataset (MTD)](#material-triplet-dataset--mtd-)
|
| 23 |
+
* [Geometry with Volumetric Materials (GVM)](#geometry-with-volumetric-materials--gvm-)
|
| 24 |
+
* [Preparing your own data for training the Geometry Transformer](#preparing-your-own-data-for-training-the-geometry-transformer)
|
| 25 |
+
- [💻 Training](#-training)
|
| 26 |
+
* [Training the MatVAE](#training-the-matvae)
|
| 27 |
+
* [Training the Geometry Transformer](#training-the-geometry-transformer)
|
| 28 |
+
* [Training on your own data](#training-on-your-own-data)
|
| 29 |
+
* [Fine-tuning](#fine-tuning)
|
| 30 |
+
- [💡 Tips](#-tips)
|
| 31 |
+
|
| 32 |
+
## 🔧 Setup
|
| 33 |
+
|
| 34 |
+
Follow the instructions in the [README.md](./README.md) file to set up the environment.
|
| 35 |
+
|
| 36 |
+
## 📖 Overview of the codebase
|
| 37 |
+
|
| 38 |
+

|
| 39 |
+
|
| 40 |
+
The codebase is organized as follows:
|
| 41 |
+
|
| 42 |
+
- `train_material_vae.py`: Main entry point for training the MatVAE.
|
| 43 |
+
- `train_geometry_encoder.py`: Main entry point for training the Geometry Transformer.
|
| 44 |
+
- `vomp/`: Main Python package containing all models and utilities.
|
| 45 |
+
- `models/`: Neural network architectures including MatVAE and Geometry Transformer.
|
| 46 |
+
- `geometry_encoder.py`: Geometry Transformer encoder.
|
| 47 |
+
- `material_vae/`: MatVAE model implementations.
|
| 48 |
+
- `structured_latent_vae/`: Structured latent VAE components.
|
| 49 |
+
- `trainers/`: Training frameworks for different model types.
|
| 50 |
+
- `modules/`: Neural network layer classes (sparse transformers, attention, etc.).
|
| 51 |
+
- `datasets/`: Dataset loaders (`SparseVoxelMaterials`, etc.).
|
| 52 |
+
- `representations/`: 3D representation handlers (Gaussian splats).
|
| 53 |
+
- `inference/`: Inference pipeline (`vomp.py`) and utilities.
|
| 54 |
+
- `utils/`: General utility functions and data processing tools.
|
| 55 |
+
- `dataset_toolkits/`: Tools for dataset creation and preprocessing.
|
| 56 |
+
- `material_objects/`: Material property rendering, voxelization, and VLM annotation tools.
|
| 57 |
+
- `datasets/`: Dataset loaders (simready, ABO, etc.).
|
| 58 |
+
- `configs/`: Configuration files for different experiments.
|
| 59 |
+
- `materials/`: MatVAE and Geometry Transformer configurations.
|
| 60 |
+
- `scripts/`: Visualization and evaluation scripts.
|
| 61 |
+
- `weights/`: Directory for storing pretrained model weights.
|
| 62 |
+
|
| 63 |
+
## 📚 Create the dataset
|
| 64 |
+
|
| 65 |
+
We provide toolkits for data preparation.
|
| 66 |
+
|
| 67 |
+

|
| 68 |
+
|
| 69 |
+
### Preprocessed Datasets
|
| 70 |
+
|
| 71 |
+
We provide the preprocessed datasets (with the vegetation subset removed) at: <a href='https://huggingface.co/datasets/nvidia/PhysicalAI-Robotics-PhysicalAssets-VoMP'><img src='https://img.shields.io/badge/%F0%9F%A4%97%20-GVM%20Dataset-yellow'></a>. We are unable to make the MTD dataset public due to licenses.
|
| 72 |
+
|
| 73 |
+
### Material Triplet Dataset (MTD)
|
| 74 |
+
|
| 75 |
+
First compile the `material_ranges.csv` file by extracting data from the following sources (and deduplicate the data):
|
| 76 |
+
|
| 77 |
+
- [MatWeb](https://matweb.com/)
|
| 78 |
+
- [Engineering Toolbox](https://www.engineeringtoolbox.com/engineering-materials-properties-d_1225.html)
|
| 79 |
+
- [Cambridge University Press](https://teaching.eng.cam.ac.uk/sites/teaching.eng.cam.ac.uk/files/Documents/Databooks/MATERIALS%20DATABOOK%20(2011)%20version%20for%20Moodle.pdf)
|
| 80 |
+
|
| 81 |
+
The Material Triplet Dataset (MTD) is used to train the MatVAE. Assuming you have the `material_ranges.csv` file in the `datasets/latent_space/` directory, you can create the MTD by running the following command:
|
| 82 |
+
|
| 83 |
+
```bash
|
| 84 |
+
python dataset_toolkits/latent_space/make_csv.py datasets/latent_space/
|
| 85 |
+
```
|
| 86 |
+
|
| 87 |
+
Due to the dataset licenses, we cannot provide the `material_ranges.csv` file.
|
| 88 |
+
|
| 89 |
+
### Geometry with Volumetric Materials (GVM)
|
| 90 |
+
|
| 91 |
+
The Geometry with Volumetric Materials (GVM) is used to train the Geometry Transformer. First, download the following datasets to `datasets/raw/`:
|
| 92 |
+
|
| 93 |
+
- [SimReady (13.9 GB + 20.5 GB + 9.4 GB + 21.4 GB + 20.6 GB)](https://docs.omniverse.nvidia.com/usd/latest/usd_content_samples/downloadable_packs.html#simready-warehouse-01-assets-pack)
|
| 94 |
+
- [Commercial (5.8 GB)](https://docs.omniverse.nvidia.com/usd/latest/usd_content_samples/downloadable_packs.html#commercial-assets-pack)
|
| 95 |
+
- [Residential (22.5 GB)](https://docs.omniverse.nvidia.com/usd/latest/usd_content_samples/downloadable_packs.html#residential-assets-pack)
|
| 96 |
+
- [Vegetation (2.7 GB)](https://docs.omniverse.nvidia.com/usd/latest/usd_content_samples/downloadable_packs.html#vegetation-assets-pack)
|
| 97 |
+
|
| 98 |
+
> [!NOTE]
|
| 99 |
+
> The SimReady dataset is split into 5 parts. You can download them all from the aforementioned URL.
|
| 100 |
+
|
| 101 |
+
Next, unzip these datasets to `datasets/raw/`, to create a directory structure like:
|
| 102 |
+
|
| 103 |
+
```
|
| 104 |
+
datasets/raw/
|
| 105 |
+
├── simready/
|
| 106 |
+
├── commercial/
|
| 107 |
+
├── residential/
|
| 108 |
+
├── vegetation/
|
| 109 |
+
```
|
| 110 |
+
|
| 111 |
+
Then, run the following command to create the GVM. This step takes ~2.5 days on 2 A100 GPUs, assuming you have enough CPU resources, as we perform significant CPU rendering.
|
| 112 |
+
|
| 113 |
+
```bash
|
| 114 |
+
mkdir -p /tmp/vlm
|
| 115 |
+
|
| 116 |
+
python dataset_toolkits/material_objects/vlm_annotations/main.py \
|
| 117 |
+
--dataset simready residential commercial vegetation \
|
| 118 |
+
-o datasets/raw/material_annotations.json \
|
| 119 |
+
--verbose
|
| 120 |
+
```
|
| 121 |
+
|
| 122 |
+
The VLM prompt is optimized using the `scripts/optimize_prompt.py` script which requires installing [textgrad](https://github.com/zou-group/textgrad).
|
| 123 |
+
|
| 124 |
+
This saves the annotations to `datasets/raw/material_annotations.json` in the following format.
|
| 125 |
+
|
| 126 |
+
```json
|
| 127 |
+
[
|
| 128 |
+
{
|
| 129 |
+
"object_name": "aluminumpallet_a01",
|
| 130 |
+
"category": "pallet",
|
| 131 |
+
"dataset_type": "simready",
|
| 132 |
+
"segments": {
|
| 133 |
+
"SM_AluminumPallet_A01_01": {
|
| 134 |
+
"name": "default__metal__aluminumpallet_a01",
|
| 135 |
+
"opacity": "opaque",
|
| 136 |
+
"material_type": "metal",
|
| 137 |
+
"semantic_usage": "aluminumpallet_a01",
|
| 138 |
+
"density": 2700.0,
|
| 139 |
+
"dynamic_friction": 0.1,
|
| 140 |
+
"static_friction": 0.1,
|
| 141 |
+
"restitution": 0.1,
|
| 142 |
+
"textures": {
|
| 143 |
+
"albedo": "datasets/raw/simready/common_assets/props/aluminumpallet_a01/textures/T_Aluminium_Brushed_A1_Albedo.png",
|
| 144 |
+
"orm": "datasets/raw/simready/common_assets/props/aluminumpallet_a01/textures/T_Aluminium_Brushed_A1_ORM.png",
|
| 145 |
+
"normal": "datasets/raw/simready/common_assets/props/aluminumpallet_a01/textures/T_Aluminium_Brushed_A1_Normal.png"
|
| 146 |
+
},
|
| 147 |
+
"vlm_analysis": "...",
|
| 148 |
+
"youngs_modulus": 70000000000.0,
|
| 149 |
+
"poissons_ratio": 0.33
|
| 150 |
+
}
|
| 151 |
+
},
|
| 152 |
+
"file_path": "datasets/raw/simready/common_assets/props/aluminumpallet_a01/aluminumpallet_a01_inst_base.usd"
|
| 153 |
+
},
|
| 154 |
+
...
|
| 155 |
+
]
|
| 156 |
+
```
|
| 157 |
+
|
| 158 |
+
### Preparing your own data for training the Geometry Transformer
|
| 159 |
+
|
| 160 |
+
To train VoMP on your own data, you need to prepare a dataset of 3D objects with volumetric materials. Particularly, you need to prepare a JSON file and USD files with the following format:
|
| 161 |
+
|
| 162 |
+
```json
|
| 163 |
+
[
|
| 164 |
+
{
|
| 165 |
+
"object_name": "[object name]",
|
| 166 |
+
"segments": {
|
| 167 |
+
"[segment name that matches the segment name in the USD file]": {
|
| 168 |
+
"density": 2700.0,
|
| 169 |
+
"youngs_modulus": 70000000000.0,
|
| 170 |
+
"poissons_ratio": 0.33
|
| 171 |
+
}
|
| 172 |
+
},
|
| 173 |
+
"file_path": "path/to/your/object.usd"
|
| 174 |
+
}
|
| 175 |
+
...
|
| 176 |
+
]
|
| 177 |
+
```
|
| 178 |
+
|
| 179 |
+
If you are preparing your own dataset make sure the individual segments you list in the JSON file match the segment names in the USD file and each segment is a mesh. Also make sure the object has appearance properties. The workflow would work even if you do not have appearance properties, but the estimated properties would be significantly worse.
|
| 180 |
+
|
| 181 |
+
## 💻 Training
|
| 182 |
+
|
| 183 |
+
### Training the MatVAE
|
| 184 |
+
|
| 185 |
+
First run `accelerate` config to create a config file, setting your hardware details and if you want to do distributed training. We highly recommend using a single GPU for training MatVAE. This step takes ~12 hours on a single A100 GPU.
|
| 186 |
+
|
| 187 |
+
Training hyperparameters and model architectures are defined in configuration files under the `configs/` directory. Example configuration files include:
|
| 188 |
+
|
| 189 |
+
| **Config** | **Description** |
|
| 190 |
+
|------------|-----------------|
|
| 191 |
+
| `configs/materials/material_vae/matvae.json` | Training configuration for MatVAE. |
|
| 192 |
+
| ... | Training configuration for ablations. |
|
| 193 |
+
|
| 194 |
+
Any configuration file can be used to start training (use `accelerate launch` instead of `python` if you want to do distributed training),
|
| 195 |
+
|
| 196 |
+
```bash
|
| 197 |
+
python train_material_vae.py --config ...
|
| 198 |
+
```
|
| 199 |
+
|
| 200 |
+
Train the MatVAE by running the following command:
|
| 201 |
+
|
| 202 |
+
```bash
|
| 203 |
+
python train_material_vae.py --config configs/materials/material_vae/matvae.json
|
| 204 |
+
```
|
| 205 |
+
|
| 206 |
+
This creates the `outputs/matvae/` directory, which contains the trained model and tensorboard logs.
|
| 207 |
+
|
| 208 |
+
### Training the Geometry Transformer
|
| 209 |
+
|
| 210 |
+
First, start by performing data preprocessing. This step takes ~2 days on an A100 GPU + ~1.5 days on an RTX6000 GPU (used for rendering).
|
| 211 |
+
|
| 212 |
+
```bash
|
| 213 |
+
# python dataset_toolkits/build_metadata.py simready --output_dir datasets/simready
|
| 214 |
+
python dataset_toolkits/build_metadata.py allmats --output_dir datasets/simready
|
| 215 |
+
|
| 216 |
+
# Render USD files to images (can be parallelized across GPUs)
|
| 217 |
+
# For multi-GPU: use --rank and --world_size arguments
|
| 218 |
+
# Example: python ... --rank 0 --world_size 4 (run on GPU 0)
|
| 219 |
+
# python ... --rank 1 --world_size 4 (run on GPU 1), etc.
|
| 220 |
+
python dataset_toolkits/material_objects/render_usd.py allmats --output_dir datasets/simready --quiet --max_workers 3
|
| 221 |
+
|
| 222 |
+
python dataset_toolkits/build_metadata.py allmats --output_dir datasets/simready --from_file
|
| 223 |
+
python dataset_toolkits/material_objects/voxelize.py --output_dir datasets/simready --max_voxels 72000 --force
|
| 224 |
+
python dataset_toolkits/build_metadata.py allmats --output_dir datasets/simready --from_file
|
| 225 |
+
|
| 226 |
+
python dataset_toolkits/extract_feature.py --output_dir datasets/simready --force
|
| 227 |
+
python dataset_toolkits/build_metadata.py allmats --output_dir datasets/simready
|
| 228 |
+
```
|
| 229 |
+
|
| 230 |
+
This creates the `datasets/simready/` directory, which contains the preprocessed data.
|
| 231 |
+
|
| 232 |
+
```bash
|
| 233 |
+
datasets/simready
|
| 234 |
+
├── features (outputs from DINOv2 feature aggregation)
|
| 235 |
+
├── merged_records
|
| 236 |
+
├── metadata.csv
|
| 237 |
+
├── renders (150 rendered images per object with camera poses)
|
| 238 |
+
├── splits (train/val/test splits)
|
| 239 |
+
├── statistics.txt (statistics of the dataset)
|
| 240 |
+
└── voxels (voxelized meshes and voxel-wise mechanical properties)
|
| 241 |
+
```
|
| 242 |
+
|
| 243 |
+
Next, run the following command to train the Geometry Transformer. This step takes ~5 days on 4 A100 GPUs.
|
| 244 |
+
|
| 245 |
+
```bash
|
| 246 |
+
python train_geometry_encoder.py --config configs/materials/geometry_encoder/train.json --output_dir outputs/geometry_encoder
|
| 247 |
+
```
|
| 248 |
+
|
| 249 |
+
This creates the `outputs/geometry_encoder/` directory, which contains the trained model and tensorboard logs.
|
| 250 |
+
|
| 251 |
+
### Training on your own data
|
| 252 |
+
|
| 253 |
+
Once you have prepared your dataset following the format above, training is straightforward.
|
| 254 |
+
|
| 255 |
+
```bash
|
| 256 |
+
python train_geometry_encoder.py --config ... --output_dir ...
|
| 257 |
+
```
|
| 258 |
+
|
| 259 |
+
Replace the config and output directory with your own. You can make a new config file by copying one of the existing ones in the `configs/` directory and modifying the hyperparameters and dataset paths.
|
| 260 |
+
|
| 261 |
+
### Fine-tuning
|
| 262 |
+
|
| 263 |
+
Fine-tuning from pre-trained checkpoints is built into the training pipeline, simply run the following command:
|
| 264 |
+
|
| 265 |
+
```bash
|
| 266 |
+
python train_geometry_encoder.py --config ... --output_dir ...
|
| 267 |
+
```
|
| 268 |
+
|
| 269 |
+
It searches for models in the `outputs/geometry_encoder/ckpts/` directory in the following format `geometry_encoder_step[0-9]+.pt` and uses it to continue training.
|
| 270 |
+
|
| 271 |
+
```bash
|
| 272 |
+
├── geometry_encoder_ema0.9999_step0060000.pt
|
| 273 |
+
├── geometry_encoder_ema0.9999_step0200000.pt
|
| 274 |
+
├── geometry_encoder_step0060000.pt
|
| 275 |
+
├── geometry_encoder_step0200000.pt
|
| 276 |
+
├── misc_step0060000.pt
|
| 277 |
+
└── misc_step0200000.pt
|
| 278 |
+
```
|
| 279 |
+
|
| 280 |
+
It also optionally searches for the `misc_step[0-9]+.pt` file to restore the optimizer state and scheduler state as well as `geometry_encoder_ema0.9999_step[0-9]+.pt` to restore the EMA model weights.
|
| 281 |
+
|
| 282 |
+
## 💡 Tips
|
| 283 |
+
|
| 284 |
+
- Running the model requires 40 GB VRAM. If you often run into out of memory errors, you can reduce the amount of voxels we use for the object.
|
| 285 |
+
- Dataset annotation with a VLM uses Qwen2.5-VL-72B which requires ~138 GB VRAM even when you load it in BF16 precision. The dataset annotation was done on 2 A100 GPUs. If you often run into out of memory errors, you can swap for a smaller version of Qwen2.5-VL or some other model, though the annotation would likely be degraded.
|
deps/vomp/configs/materials/geometry_encoder/train.json
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"matvae_checkpoint": "outputs/matvae2/checkpoints/checkpoint_853/model.safetensors",
|
| 3 |
+
"trellis_weights_path": "weights/TRELLIS-image-large",
|
| 4 |
+
"models": {
|
| 5 |
+
"geometry_encoder": {
|
| 6 |
+
"name": "geometry_encoder",
|
| 7 |
+
"args": {
|
| 8 |
+
"resolution": 64,
|
| 9 |
+
"in_channels": 1024,
|
| 10 |
+
"model_channels": 768,
|
| 11 |
+
"latent_channels": 2,
|
| 12 |
+
"num_blocks": 12,
|
| 13 |
+
"num_heads": 12,
|
| 14 |
+
"mlp_ratio": 4,
|
| 15 |
+
"attn_mode": "swin",
|
| 16 |
+
"window_size": 8,
|
| 17 |
+
"use_fp16": true
|
| 18 |
+
}
|
| 19 |
+
},
|
| 20 |
+
"matvae": {
|
| 21 |
+
"name": "matvae",
|
| 22 |
+
"args": {
|
| 23 |
+
"width": 256,
|
| 24 |
+
"depth": 3,
|
| 25 |
+
"z_dim": 2,
|
| 26 |
+
"p_drop": 0.05,
|
| 27 |
+
"use_learned_variances": false,
|
| 28 |
+
"use_additional_losses": true
|
| 29 |
+
}
|
| 30 |
+
}
|
| 31 |
+
},
|
| 32 |
+
"dataset": {
|
| 33 |
+
"name": "SparseVoxelMaterials",
|
| 34 |
+
"normalization_type": "log_minmax",
|
| 35 |
+
"args": {
|
| 36 |
+
"roots": "datasets/simready",
|
| 37 |
+
"image_size": 512,
|
| 38 |
+
"model": "dinov2_vitl14_reg",
|
| 39 |
+
"resolution": 64,
|
| 40 |
+
"min_aesthetic_score": 0.0,
|
| 41 |
+
"max_num_voxels": 32768,
|
| 42 |
+
"compute_material_stats": false
|
| 43 |
+
}
|
| 44 |
+
},
|
| 45 |
+
"trainer": {
|
| 46 |
+
"name": "SLatVaeMaterialsTrainer",
|
| 47 |
+
"args": {
|
| 48 |
+
"max_steps": 1000000,
|
| 49 |
+
"batch_size_per_gpu": 16,
|
| 50 |
+
"batch_split": 1,
|
| 51 |
+
"optimizer": {
|
| 52 |
+
"name": "AdamW",
|
| 53 |
+
"args": {
|
| 54 |
+
"lr": 1e-4,
|
| 55 |
+
"weight_decay": 0.0
|
| 56 |
+
}
|
| 57 |
+
},
|
| 58 |
+
"ema_rate": [
|
| 59 |
+
0.9999
|
| 60 |
+
],
|
| 61 |
+
"fp16_mode": "inflat_all",
|
| 62 |
+
"fp16_scale_growth": 0.001,
|
| 63 |
+
"elastic": {
|
| 64 |
+
"name": "LinearMemoryController",
|
| 65 |
+
"args": {
|
| 66 |
+
"target_ratio": 0.75,
|
| 67 |
+
"max_mem_ratio_start": 0.5
|
| 68 |
+
}
|
| 69 |
+
},
|
| 70 |
+
"grad_clip": {
|
| 71 |
+
"name": "AdaptiveGradClipper",
|
| 72 |
+
"args": {
|
| 73 |
+
"max_norm": 1.0,
|
| 74 |
+
"clip_percentile": 95
|
| 75 |
+
}
|
| 76 |
+
},
|
| 77 |
+
"i_log": 10,
|
| 78 |
+
"i_save": 2000,
|
| 79 |
+
"i_eval": 1000,
|
| 80 |
+
"loss_type": "l1"
|
| 81 |
+
}
|
| 82 |
+
}
|
| 83 |
+
}
|
deps/vomp/configs/materials/geometry_encoder/train_encoder_decoder_direct.json
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"training_mode": "encoder_decoder_direct",
|
| 3 |
+
"matvae_checkpoint": "outputs/matvae/checkpoints/checkpoint_821/model.safetensors",
|
| 4 |
+
"trellis_weights_path": "weights/TRELLIS-image-large",
|
| 5 |
+
"models": {
|
| 6 |
+
"geometry_encoder": {
|
| 7 |
+
"name": "geometry_encoder",
|
| 8 |
+
"args": {
|
| 9 |
+
"resolution": 64,
|
| 10 |
+
"in_channels": 1024,
|
| 11 |
+
"model_channels": 768,
|
| 12 |
+
"latent_channels": 8,
|
| 13 |
+
"num_blocks": 12,
|
| 14 |
+
"num_heads": 12,
|
| 15 |
+
"mlp_ratio": 4,
|
| 16 |
+
"attn_mode": "swin",
|
| 17 |
+
"window_size": 8,
|
| 18 |
+
"use_fp16": true
|
| 19 |
+
}
|
| 20 |
+
},
|
| 21 |
+
"decoder": {
|
| 22 |
+
"name": "decoder",
|
| 23 |
+
"args": {
|
| 24 |
+
"resolution": 64,
|
| 25 |
+
"model_channels": 768,
|
| 26 |
+
"latent_channels": 8,
|
| 27 |
+
"num_blocks": 12,
|
| 28 |
+
"out_channels": 3,
|
| 29 |
+
"num_heads": 12,
|
| 30 |
+
"mlp_ratio": 4,
|
| 31 |
+
"attn_mode": "swin",
|
| 32 |
+
"window_size": 8,
|
| 33 |
+
"use_fp16": true
|
| 34 |
+
}
|
| 35 |
+
},
|
| 36 |
+
"matvae": {
|
| 37 |
+
"name": "matvae",
|
| 38 |
+
"args": {
|
| 39 |
+
"width": 256,
|
| 40 |
+
"depth": 3,
|
| 41 |
+
"z_dim": 2,
|
| 42 |
+
"p_drop": 0.05,
|
| 43 |
+
"use_learned_variances": false,
|
| 44 |
+
"use_additional_losses": true
|
| 45 |
+
}
|
| 46 |
+
}
|
| 47 |
+
},
|
| 48 |
+
"dataset": {
|
| 49 |
+
"name": "SparseVoxelMaterials",
|
| 50 |
+
"normalization_type": "log_minmax",
|
| 51 |
+
"args": {
|
| 52 |
+
"roots": "datasets/simready",
|
| 53 |
+
"image_size": 512,
|
| 54 |
+
"model": "dinov2_vitl14_reg",
|
| 55 |
+
"resolution": 64,
|
| 56 |
+
"min_aesthetic_score": 0.0,
|
| 57 |
+
"max_num_voxels": 32768,
|
| 58 |
+
"compute_material_stats": false
|
| 59 |
+
}
|
| 60 |
+
},
|
| 61 |
+
"trainer": {
|
| 62 |
+
"name": "SLatVaeMaterialsTrainer",
|
| 63 |
+
"args": {
|
| 64 |
+
"max_steps": 1000000,
|
| 65 |
+
"batch_size_per_gpu": 16,
|
| 66 |
+
"batch_split": 1,
|
| 67 |
+
"optimizer": {
|
| 68 |
+
"name": "AdamW",
|
| 69 |
+
"args": {
|
| 70 |
+
"lr": 1e-4,
|
| 71 |
+
"weight_decay": 0.0
|
| 72 |
+
}
|
| 73 |
+
},
|
| 74 |
+
"ema_rate": [
|
| 75 |
+
0.9999
|
| 76 |
+
],
|
| 77 |
+
"fp16_mode": "inflat_all",
|
| 78 |
+
"fp16_scale_growth": 0.001,
|
| 79 |
+
"elastic": {
|
| 80 |
+
"name": "LinearMemoryController",
|
| 81 |
+
"args": {
|
| 82 |
+
"target_ratio": 0.75,
|
| 83 |
+
"max_mem_ratio_start": 0.5
|
| 84 |
+
}
|
| 85 |
+
},
|
| 86 |
+
"grad_clip": {
|
| 87 |
+
"name": "AdaptiveGradClipper",
|
| 88 |
+
"args": {
|
| 89 |
+
"max_norm": 1.0,
|
| 90 |
+
"clip_percentile": 95
|
| 91 |
+
}
|
| 92 |
+
},
|
| 93 |
+
"i_log": 10,
|
| 94 |
+
"i_save": 1000,
|
| 95 |
+
"i_eval": 1000,
|
| 96 |
+
"loss_type": "l1"
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
}
|
deps/vomp/configs/materials/geometry_encoder/train_encoder_decoder_matvae.json
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"training_mode": "encoder_decoder_matvae",
|
| 3 |
+
"matvae_checkpoint": "outputs/matvae/checkpoints/checkpoint_821/model.safetensors",
|
| 4 |
+
"trellis_weights_path": "weights/TRELLIS-image-large",
|
| 5 |
+
"models": {
|
| 6 |
+
"geometry_encoder": {
|
| 7 |
+
"name": "geometry_encoder",
|
| 8 |
+
"args": {
|
| 9 |
+
"resolution": 64,
|
| 10 |
+
"in_channels": 1024,
|
| 11 |
+
"model_channels": 768,
|
| 12 |
+
"latent_channels": 8,
|
| 13 |
+
"num_blocks": 12,
|
| 14 |
+
"num_heads": 12,
|
| 15 |
+
"mlp_ratio": 4,
|
| 16 |
+
"attn_mode": "swin",
|
| 17 |
+
"window_size": 8,
|
| 18 |
+
"use_fp16": true
|
| 19 |
+
}
|
| 20 |
+
},
|
| 21 |
+
"decoder": {
|
| 22 |
+
"name": "decoder",
|
| 23 |
+
"args": {
|
| 24 |
+
"resolution": 64,
|
| 25 |
+
"model_channels": 768,
|
| 26 |
+
"latent_channels": 8,
|
| 27 |
+
"num_blocks": 12,
|
| 28 |
+
"out_channels": 2,
|
| 29 |
+
"num_heads": 12,
|
| 30 |
+
"mlp_ratio": 4,
|
| 31 |
+
"attn_mode": "swin",
|
| 32 |
+
"window_size": 8,
|
| 33 |
+
"use_fp16": true
|
| 34 |
+
}
|
| 35 |
+
},
|
| 36 |
+
"matvae": {
|
| 37 |
+
"name": "matvae",
|
| 38 |
+
"args": {
|
| 39 |
+
"width": 256,
|
| 40 |
+
"depth": 3,
|
| 41 |
+
"z_dim": 2,
|
| 42 |
+
"p_drop": 0.05,
|
| 43 |
+
"use_learned_variances": false,
|
| 44 |
+
"use_additional_losses": true
|
| 45 |
+
}
|
| 46 |
+
}
|
| 47 |
+
},
|
| 48 |
+
"dataset": {
|
| 49 |
+
"name": "SparseVoxelMaterials",
|
| 50 |
+
"normalization_type": "log_minmax",
|
| 51 |
+
"args": {
|
| 52 |
+
"roots": "datasets/simready",
|
| 53 |
+
"image_size": 512,
|
| 54 |
+
"model": "dinov2_vitl14_reg",
|
| 55 |
+
"resolution": 64,
|
| 56 |
+
"min_aesthetic_score": 0.0,
|
| 57 |
+
"max_num_voxels": 32768,
|
| 58 |
+
"compute_material_stats": false
|
| 59 |
+
}
|
| 60 |
+
},
|
| 61 |
+
"trainer": {
|
| 62 |
+
"name": "SLatVaeMaterialsTrainer",
|
| 63 |
+
"args": {
|
| 64 |
+
"max_steps": 1000000,
|
| 65 |
+
"batch_size_per_gpu": 16,
|
| 66 |
+
"batch_split": 1,
|
| 67 |
+
"optimizer": {
|
| 68 |
+
"name": "AdamW",
|
| 69 |
+
"args": {
|
| 70 |
+
"lr": 1e-4,
|
| 71 |
+
"weight_decay": 0.0
|
| 72 |
+
}
|
| 73 |
+
},
|
| 74 |
+
"ema_rate": [
|
| 75 |
+
0.9999
|
| 76 |
+
],
|
| 77 |
+
"fp16_mode": "inflat_all",
|
| 78 |
+
"fp16_scale_growth": 0.001,
|
| 79 |
+
"elastic": {
|
| 80 |
+
"name": "LinearMemoryController",
|
| 81 |
+
"args": {
|
| 82 |
+
"target_ratio": 0.75,
|
| 83 |
+
"max_mem_ratio_start": 0.5
|
| 84 |
+
}
|
| 85 |
+
},
|
| 86 |
+
"grad_clip": {
|
| 87 |
+
"name": "AdaptiveGradClipper",
|
| 88 |
+
"args": {
|
| 89 |
+
"max_norm": 1.0,
|
| 90 |
+
"clip_percentile": 95
|
| 91 |
+
}
|
| 92 |
+
},
|
| 93 |
+
"i_log": 10,
|
| 94 |
+
"i_save": 1000,
|
| 95 |
+
"i_eval": 1000,
|
| 96 |
+
"loss_type": "l1"
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
}
|
deps/vomp/configs/materials/geometry_encoder/train_standard.json
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"matvae_checkpoint": "outputs/matvae2/checkpoints/checkpoint_853/model.safetensors",
|
| 3 |
+
"trellis_weights_path": "weights/TRELLIS-image-large",
|
| 4 |
+
"models": {
|
| 5 |
+
"geometry_encoder": {
|
| 6 |
+
"name": "geometry_encoder",
|
| 7 |
+
"args": {
|
| 8 |
+
"resolution": 64,
|
| 9 |
+
"in_channels": 1024,
|
| 10 |
+
"model_channels": 768,
|
| 11 |
+
"latent_channels": 2,
|
| 12 |
+
"num_blocks": 12,
|
| 13 |
+
"num_heads": 12,
|
| 14 |
+
"mlp_ratio": 4,
|
| 15 |
+
"attn_mode": "swin",
|
| 16 |
+
"window_size": 8,
|
| 17 |
+
"use_fp16": true
|
| 18 |
+
}
|
| 19 |
+
},
|
| 20 |
+
"matvae": {
|
| 21 |
+
"name": "matvae",
|
| 22 |
+
"args": {
|
| 23 |
+
"width": 256,
|
| 24 |
+
"depth": 3,
|
| 25 |
+
"z_dim": 2,
|
| 26 |
+
"p_drop": 0.05,
|
| 27 |
+
"use_learned_variances": false,
|
| 28 |
+
"use_additional_losses": true
|
| 29 |
+
}
|
| 30 |
+
}
|
| 31 |
+
},
|
| 32 |
+
"dataset": {
|
| 33 |
+
"name": "SparseVoxelMaterials",
|
| 34 |
+
"normalization_type": "standard",
|
| 35 |
+
"args": {
|
| 36 |
+
"roots": "datasets/simready",
|
| 37 |
+
"image_size": 512,
|
| 38 |
+
"model": "dinov2_vitl14_reg",
|
| 39 |
+
"resolution": 64,
|
| 40 |
+
"min_aesthetic_score": 0.0,
|
| 41 |
+
"max_num_voxels": 32768,
|
| 42 |
+
"compute_material_stats": false
|
| 43 |
+
}
|
| 44 |
+
},
|
| 45 |
+
"trainer": {
|
| 46 |
+
"name": "SLatVaeMaterialsTrainer",
|
| 47 |
+
"args": {
|
| 48 |
+
"max_steps": 1000000,
|
| 49 |
+
"batch_size_per_gpu": 16,
|
| 50 |
+
"batch_split": 1,
|
| 51 |
+
"optimizer": {
|
| 52 |
+
"name": "AdamW",
|
| 53 |
+
"args": {
|
| 54 |
+
"lr": 1e-4,
|
| 55 |
+
"weight_decay": 0.0
|
| 56 |
+
}
|
| 57 |
+
},
|
| 58 |
+
"ema_rate": [
|
| 59 |
+
0.9999
|
| 60 |
+
],
|
| 61 |
+
"fp16_mode": "inflat_all",
|
| 62 |
+
"fp16_scale_growth": 0.001,
|
| 63 |
+
"elastic": {
|
| 64 |
+
"name": "LinearMemoryController",
|
| 65 |
+
"args": {
|
| 66 |
+
"target_ratio": 0.75,
|
| 67 |
+
"max_mem_ratio_start": 0.5
|
| 68 |
+
}
|
| 69 |
+
},
|
| 70 |
+
"grad_clip": {
|
| 71 |
+
"name": "AdaptiveGradClipper",
|
| 72 |
+
"args": {
|
| 73 |
+
"max_norm": 1.0,
|
| 74 |
+
"clip_percentile": 95
|
| 75 |
+
}
|
| 76 |
+
},
|
| 77 |
+
"i_log": 10,
|
| 78 |
+
"i_save": 2000,
|
| 79 |
+
"i_eval": 1000,
|
| 80 |
+
"loss_type": "l1"
|
| 81 |
+
}
|
| 82 |
+
}
|
| 83 |
+
}
|
deps/vomp/configs/materials/inference.json
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"geometry_checkpoint_dir": "weights/geometry_transformer.pt",
|
| 3 |
+
"matvae_checkpoint_dir": "weights/matvae.safetensors",
|
| 4 |
+
"normalization_params_path": "weights/normalization_params.json",
|
| 5 |
+
"matvae_checkpoint": "weights/matvae.safetensors",
|
| 6 |
+
"trellis_weights_path": "weights/TRELLIS-image-large",
|
| 7 |
+
"models": {
|
| 8 |
+
"geometry_encoder": {
|
| 9 |
+
"name": "geometry_encoder",
|
| 10 |
+
"args": {
|
| 11 |
+
"resolution": 64,
|
| 12 |
+
"in_channels": 1024,
|
| 13 |
+
"model_channels": 768,
|
| 14 |
+
"latent_channels": 2,
|
| 15 |
+
"num_blocks": 12,
|
| 16 |
+
"num_heads": 12,
|
| 17 |
+
"mlp_ratio": 4,
|
| 18 |
+
"attn_mode": "swin",
|
| 19 |
+
"window_size": 8,
|
| 20 |
+
"use_fp16": true
|
| 21 |
+
}
|
| 22 |
+
},
|
| 23 |
+
"matvae": {
|
| 24 |
+
"name": "matvae",
|
| 25 |
+
"args": {
|
| 26 |
+
"width": 256,
|
| 27 |
+
"depth": 3,
|
| 28 |
+
"z_dim": 2,
|
| 29 |
+
"p_drop": 0.05,
|
| 30 |
+
"use_learned_variances": false,
|
| 31 |
+
"use_additional_losses": true
|
| 32 |
+
}
|
| 33 |
+
}
|
| 34 |
+
},
|
| 35 |
+
"dataset": {
|
| 36 |
+
"name": "SparseVoxelMaterials",
|
| 37 |
+
"normalization_type": "log_minmax",
|
| 38 |
+
"args": {
|
| 39 |
+
"roots": "datasets/simready",
|
| 40 |
+
"image_size": 512,
|
| 41 |
+
"model": "dinov2_vitl14_reg",
|
| 42 |
+
"resolution": 64,
|
| 43 |
+
"min_aesthetic_score": 0.0,
|
| 44 |
+
"max_num_voxels": 32768,
|
| 45 |
+
"compute_material_stats": false
|
| 46 |
+
}
|
| 47 |
+
},
|
| 48 |
+
"trainer": {
|
| 49 |
+
"name": "SLatVaeMaterialsTrainer",
|
| 50 |
+
"args": {
|
| 51 |
+
"max_steps": 1000000,
|
| 52 |
+
"batch_size_per_gpu": 16,
|
| 53 |
+
"batch_split": 1,
|
| 54 |
+
"optimizer": {
|
| 55 |
+
"name": "AdamW",
|
| 56 |
+
"args": {
|
| 57 |
+
"lr": 1e-4,
|
| 58 |
+
"weight_decay": 0.0
|
| 59 |
+
}
|
| 60 |
+
},
|
| 61 |
+
"ema_rate": [
|
| 62 |
+
0.9999
|
| 63 |
+
],
|
| 64 |
+
"fp16_mode": "inflat_all",
|
| 65 |
+
"fp16_scale_growth": 0.001,
|
| 66 |
+
"elastic": {
|
| 67 |
+
"name": "LinearMemoryController",
|
| 68 |
+
"args": {
|
| 69 |
+
"target_ratio": 0.75,
|
| 70 |
+
"max_mem_ratio_start": 0.5
|
| 71 |
+
}
|
| 72 |
+
},
|
| 73 |
+
"grad_clip": {
|
| 74 |
+
"name": "AdaptiveGradClipper",
|
| 75 |
+
"args": {
|
| 76 |
+
"max_norm": 1.0,
|
| 77 |
+
"clip_percentile": 95
|
| 78 |
+
}
|
| 79 |
+
},
|
| 80 |
+
"i_log": 10,
|
| 81 |
+
"i_save": 2000,
|
| 82 |
+
"i_eval": 1000,
|
| 83 |
+
"loss_type": "l1"
|
| 84 |
+
}
|
| 85 |
+
}
|
| 86 |
+
}
|
deps/vomp/configs/materials/material_vae/beta_tc_final.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"dry_run": false,
|
| 3 |
+
"standard_vae": false,
|
| 4 |
+
|
| 5 |
+
"data_csv": "datasets/latent_space/materials_filtered.csv",
|
| 6 |
+
"dataloader": {
|
| 7 |
+
"batch_size": 256,
|
| 8 |
+
"num_workers": 8,
|
| 9 |
+
"pin_memory": true,
|
| 10 |
+
"prefetch_factor": 4,
|
| 11 |
+
"persistent_workers": true
|
| 12 |
+
},
|
| 13 |
+
|
| 14 |
+
"project_dir": "./outputs/beta_tc",
|
| 15 |
+
"tracker_name": "tb_logs",
|
| 16 |
+
"log_with": "tensorboard",
|
| 17 |
+
|
| 18 |
+
"epochs": 25000,
|
| 19 |
+
"gradient_accumulation_steps": 1,
|
| 20 |
+
"keep_last_checkpoints": 3,
|
| 21 |
+
|
| 22 |
+
"mixed_precision": "no",
|
| 23 |
+
"use_stateful_dataloader": false,
|
| 24 |
+
"find_unused_parameters": false,
|
| 25 |
+
|
| 26 |
+
"compile": {
|
| 27 |
+
"enabled": false,
|
| 28 |
+
"backend": "inductor",
|
| 29 |
+
"mode": "default",
|
| 30 |
+
"fullgraph": true,
|
| 31 |
+
"dynamic": false
|
| 32 |
+
},
|
| 33 |
+
|
| 34 |
+
"optimizer": {
|
| 35 |
+
"lr": 5e-4,
|
| 36 |
+
"weight_decay": 1e-4,
|
| 37 |
+
"grad_clip_norm": 5.0
|
| 38 |
+
},
|
| 39 |
+
|
| 40 |
+
"lr_scheduler": {
|
| 41 |
+
"type": "cosine",
|
| 42 |
+
"eta_min": 1e-5
|
| 43 |
+
},
|
| 44 |
+
|
| 45 |
+
"free_nats": 0.1,
|
| 46 |
+
"alpha": 1.0,
|
| 47 |
+
"beta": 2.0,
|
| 48 |
+
"gamma": 1.0,
|
| 49 |
+
"iwae_K": 50,
|
| 50 |
+
|
| 51 |
+
"eval_interval": 1,
|
| 52 |
+
"save_interval": 1,
|
| 53 |
+
"visualization_interval": 1000,
|
| 54 |
+
"n_vis_samples": 5,
|
| 55 |
+
"n_vis_steps": 10,
|
| 56 |
+
|
| 57 |
+
"model": {
|
| 58 |
+
"width": 512,
|
| 59 |
+
"depth": 4,
|
| 60 |
+
"z_dim": 2,
|
| 61 |
+
"p_drop": 0.05,
|
| 62 |
+
"use_flow": false
|
| 63 |
+
},
|
| 64 |
+
|
| 65 |
+
"seed": 42,
|
| 66 |
+
|
| 67 |
+
"resume_from_checkpoint": null
|
| 68 |
+
}
|
deps/vomp/configs/materials/material_vae/matvae.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"dry_run": false,
|
| 3 |
+
"standard_vae": false,
|
| 4 |
+
|
| 5 |
+
"data_csv": "datasets/latent_space/materials_filtered.csv",
|
| 6 |
+
"dataloader": {
|
| 7 |
+
"batch_size": 256,
|
| 8 |
+
"num_workers": 8,
|
| 9 |
+
"pin_memory": true,
|
| 10 |
+
"prefetch_factor": 4,
|
| 11 |
+
"persistent_workers": true
|
| 12 |
+
},
|
| 13 |
+
|
| 14 |
+
"project_dir": "./outputs/matvae",
|
| 15 |
+
"tracker_name": "tb_logs",
|
| 16 |
+
"log_with": "tensorboard",
|
| 17 |
+
|
| 18 |
+
"epochs": 850,
|
| 19 |
+
"gradient_accumulation_steps": 1,
|
| 20 |
+
"keep_last_checkpoints": 3,
|
| 21 |
+
|
| 22 |
+
"mixed_precision": "no",
|
| 23 |
+
"use_stateful_dataloader": false,
|
| 24 |
+
"find_unused_parameters": false,
|
| 25 |
+
|
| 26 |
+
"compile": {
|
| 27 |
+
"enabled": false,
|
| 28 |
+
"backend": "inductor",
|
| 29 |
+
"mode": "default",
|
| 30 |
+
"fullgraph": true,
|
| 31 |
+
"dynamic": false
|
| 32 |
+
},
|
| 33 |
+
|
| 34 |
+
"optimizer": {
|
| 35 |
+
"lr": 1e-4,
|
| 36 |
+
"weight_decay": 1e-4,
|
| 37 |
+
"grad_clip_norm": 5.0
|
| 38 |
+
},
|
| 39 |
+
|
| 40 |
+
"lr_scheduler": {
|
| 41 |
+
"type": "cosine",
|
| 42 |
+
"eta_min": 1e-5
|
| 43 |
+
},
|
| 44 |
+
|
| 45 |
+
"free_nats": 0.1,
|
| 46 |
+
"kl_annealing": true,
|
| 47 |
+
"kl_annealing_epochs": 200,
|
| 48 |
+
"recon_scale": 1.0,
|
| 49 |
+
"kl_weight": 1.0,
|
| 50 |
+
"iwae_K": 50,
|
| 51 |
+
|
| 52 |
+
"alpha": 1.0,
|
| 53 |
+
"beta": 2.0,
|
| 54 |
+
"gamma": 1.0,
|
| 55 |
+
|
| 56 |
+
"normalization_type": "log_minmax",
|
| 57 |
+
|
| 58 |
+
"eval_interval": 1,
|
| 59 |
+
"save_interval": 1,
|
| 60 |
+
"visualization_interval": 1000,
|
| 61 |
+
"n_vis_samples": 5,
|
| 62 |
+
"n_vis_steps": 10,
|
| 63 |
+
|
| 64 |
+
"model": {
|
| 65 |
+
"width": 256,
|
| 66 |
+
"depth": 3,
|
| 67 |
+
"z_dim": 2,
|
| 68 |
+
"p_drop": 0.05,
|
| 69 |
+
"use_learned_variances": false,
|
| 70 |
+
"use_additional_losses": true
|
| 71 |
+
},
|
| 72 |
+
|
| 73 |
+
"seed": 42,
|
| 74 |
+
|
| 75 |
+
"resume_from_checkpoint": null
|
| 76 |
+
}
|
deps/vomp/configs/materials/material_vae/matvae_log_minmax_no_density.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"dry_run": false,
|
| 3 |
+
"standard_vae": false,
|
| 4 |
+
|
| 5 |
+
"data_csv": "datasets/latent_space/materials_filtered.csv",
|
| 6 |
+
"dataloader": {
|
| 7 |
+
"batch_size": 256,
|
| 8 |
+
"num_workers": 8,
|
| 9 |
+
"pin_memory": true,
|
| 10 |
+
"prefetch_factor": 4,
|
| 11 |
+
"persistent_workers": true
|
| 12 |
+
},
|
| 13 |
+
|
| 14 |
+
"project_dir": "./outputs/matvae",
|
| 15 |
+
"tracker_name": "tb_logs",
|
| 16 |
+
"log_with": "tensorboard",
|
| 17 |
+
|
| 18 |
+
"epochs": 850,
|
| 19 |
+
"gradient_accumulation_steps": 1,
|
| 20 |
+
"keep_last_checkpoints": 3,
|
| 21 |
+
|
| 22 |
+
"mixed_precision": "no",
|
| 23 |
+
"use_stateful_dataloader": false,
|
| 24 |
+
"find_unused_parameters": false,
|
| 25 |
+
|
| 26 |
+
"compile": {
|
| 27 |
+
"enabled": false,
|
| 28 |
+
"backend": "inductor",
|
| 29 |
+
"mode": "default",
|
| 30 |
+
"fullgraph": true,
|
| 31 |
+
"dynamic": false
|
| 32 |
+
},
|
| 33 |
+
|
| 34 |
+
"optimizer": {
|
| 35 |
+
"lr": 1e-4,
|
| 36 |
+
"weight_decay": 1e-4,
|
| 37 |
+
"grad_clip_norm": 5.0
|
| 38 |
+
},
|
| 39 |
+
|
| 40 |
+
"lr_scheduler": {
|
| 41 |
+
"type": "cosine",
|
| 42 |
+
"eta_min": 1e-5
|
| 43 |
+
},
|
| 44 |
+
|
| 45 |
+
"free_nats": 0.1,
|
| 46 |
+
"kl_annealing": true,
|
| 47 |
+
"kl_annealing_epochs": 200,
|
| 48 |
+
"recon_scale": 1.0,
|
| 49 |
+
"kl_weight": 1.0,
|
| 50 |
+
"iwae_K": 50,
|
| 51 |
+
|
| 52 |
+
"alpha": 1.0,
|
| 53 |
+
"beta": 2.0,
|
| 54 |
+
"gamma": 1.0,
|
| 55 |
+
|
| 56 |
+
"normalization_type": "log_minmax_no_density",
|
| 57 |
+
|
| 58 |
+
"eval_interval": 1,
|
| 59 |
+
"save_interval": 1,
|
| 60 |
+
"visualization_interval": 1000,
|
| 61 |
+
"n_vis_samples": 5,
|
| 62 |
+
"n_vis_steps": 10,
|
| 63 |
+
|
| 64 |
+
"model": {
|
| 65 |
+
"width": 256,
|
| 66 |
+
"depth": 3,
|
| 67 |
+
"z_dim": 2,
|
| 68 |
+
"p_drop": 0.05,
|
| 69 |
+
"use_learned_variances": false,
|
| 70 |
+
"use_additional_losses": true
|
| 71 |
+
},
|
| 72 |
+
|
| 73 |
+
"seed": 42,
|
| 74 |
+
|
| 75 |
+
"resume_from_checkpoint": null
|
| 76 |
+
}
|
deps/vomp/configs/materials/material_vae/matvae_no_beta_tc.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"dry_run": false,
|
| 3 |
+
"standard_vae": true,
|
| 4 |
+
|
| 5 |
+
"data_csv": "datasets/latent_space/materials_filtered.csv",
|
| 6 |
+
"dataloader": {
|
| 7 |
+
"batch_size": 256,
|
| 8 |
+
"num_workers": 8,
|
| 9 |
+
"pin_memory": true,
|
| 10 |
+
"prefetch_factor": 4,
|
| 11 |
+
"persistent_workers": true
|
| 12 |
+
},
|
| 13 |
+
|
| 14 |
+
"project_dir": "./outputs/matvae_ablations/no_beta_tc",
|
| 15 |
+
"tracker_name": "tb_logs",
|
| 16 |
+
"log_with": "tensorboard",
|
| 17 |
+
|
| 18 |
+
"epochs": 850,
|
| 19 |
+
"gradient_accumulation_steps": 1,
|
| 20 |
+
"keep_last_checkpoints": 3,
|
| 21 |
+
|
| 22 |
+
"mixed_precision": "no",
|
| 23 |
+
"use_stateful_dataloader": false,
|
| 24 |
+
"find_unused_parameters": false,
|
| 25 |
+
|
| 26 |
+
"compile": {
|
| 27 |
+
"enabled": false,
|
| 28 |
+
"backend": "inductor",
|
| 29 |
+
"mode": "default",
|
| 30 |
+
"fullgraph": true,
|
| 31 |
+
"dynamic": false
|
| 32 |
+
},
|
| 33 |
+
|
| 34 |
+
"optimizer": {
|
| 35 |
+
"lr": 1e-4,
|
| 36 |
+
"weight_decay": 1e-4,
|
| 37 |
+
"grad_clip_norm": 5.0
|
| 38 |
+
},
|
| 39 |
+
|
| 40 |
+
"lr_scheduler": {
|
| 41 |
+
"type": "cosine",
|
| 42 |
+
"eta_min": 1e-5
|
| 43 |
+
},
|
| 44 |
+
|
| 45 |
+
"free_nats": 0.1,
|
| 46 |
+
"kl_annealing": true,
|
| 47 |
+
"kl_annealing_epochs": 200,
|
| 48 |
+
"recon_scale": 1.0,
|
| 49 |
+
"kl_weight": 1.0,
|
| 50 |
+
"iwae_K": 50,
|
| 51 |
+
|
| 52 |
+
"alpha": 1.0,
|
| 53 |
+
"beta": 2.0,
|
| 54 |
+
"gamma": 1.0,
|
| 55 |
+
|
| 56 |
+
"normalization_type": "log_minmax",
|
| 57 |
+
|
| 58 |
+
"eval_interval": 1,
|
| 59 |
+
"save_interval": 1,
|
| 60 |
+
"visualization_interval": 1000,
|
| 61 |
+
"n_vis_samples": 5,
|
| 62 |
+
"n_vis_steps": 10,
|
| 63 |
+
|
| 64 |
+
"model": {
|
| 65 |
+
"width": 256,
|
| 66 |
+
"depth": 3,
|
| 67 |
+
"z_dim": 2,
|
| 68 |
+
"p_drop": 0.05,
|
| 69 |
+
"use_learned_variances": false,
|
| 70 |
+
"use_additional_losses": true,
|
| 71 |
+
"use_flow": true
|
| 72 |
+
},
|
| 73 |
+
|
| 74 |
+
"seed": 42,
|
| 75 |
+
|
| 76 |
+
"resume_from_checkpoint": null
|
| 77 |
+
}
|
deps/vomp/configs/materials/material_vae/matvae_no_flow.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"dry_run": false,
|
| 3 |
+
"standard_vae": false,
|
| 4 |
+
|
| 5 |
+
"data_csv": "datasets/latent_space/materials_filtered.csv",
|
| 6 |
+
"dataloader": {
|
| 7 |
+
"batch_size": 256,
|
| 8 |
+
"num_workers": 8,
|
| 9 |
+
"pin_memory": true,
|
| 10 |
+
"prefetch_factor": 4,
|
| 11 |
+
"persistent_workers": true
|
| 12 |
+
},
|
| 13 |
+
|
| 14 |
+
"project_dir": "./outputs/matvae_ablations/no_flow",
|
| 15 |
+
"tracker_name": "tb_logs",
|
| 16 |
+
"log_with": "tensorboard",
|
| 17 |
+
|
| 18 |
+
"epochs": 850,
|
| 19 |
+
"gradient_accumulation_steps": 1,
|
| 20 |
+
"keep_last_checkpoints": 3,
|
| 21 |
+
|
| 22 |
+
"mixed_precision": "no",
|
| 23 |
+
"use_stateful_dataloader": false,
|
| 24 |
+
"find_unused_parameters": false,
|
| 25 |
+
|
| 26 |
+
"compile": {
|
| 27 |
+
"enabled": false,
|
| 28 |
+
"backend": "inductor",
|
| 29 |
+
"mode": "default",
|
| 30 |
+
"fullgraph": true,
|
| 31 |
+
"dynamic": false
|
| 32 |
+
},
|
| 33 |
+
|
| 34 |
+
"optimizer": {
|
| 35 |
+
"lr": 1e-4,
|
| 36 |
+
"weight_decay": 1e-4,
|
| 37 |
+
"grad_clip_norm": 5.0
|
| 38 |
+
},
|
| 39 |
+
|
| 40 |
+
"lr_scheduler": {
|
| 41 |
+
"type": "cosine",
|
| 42 |
+
"eta_min": 1e-5
|
| 43 |
+
},
|
| 44 |
+
|
| 45 |
+
"free_nats": 0.1,
|
| 46 |
+
"kl_annealing": true,
|
| 47 |
+
"kl_annealing_epochs": 200,
|
| 48 |
+
"recon_scale": 1.0,
|
| 49 |
+
"kl_weight": 1.0,
|
| 50 |
+
"iwae_K": 50,
|
| 51 |
+
|
| 52 |
+
"alpha": 1.0,
|
| 53 |
+
"beta": 2.0,
|
| 54 |
+
"gamma": 1.0,
|
| 55 |
+
|
| 56 |
+
"normalization_type": "log_minmax",
|
| 57 |
+
|
| 58 |
+
"eval_interval": 1,
|
| 59 |
+
"save_interval": 1,
|
| 60 |
+
"visualization_interval": 1000,
|
| 61 |
+
"n_vis_samples": 5,
|
| 62 |
+
"n_vis_steps": 10,
|
| 63 |
+
|
| 64 |
+
"model": {
|
| 65 |
+
"width": 256,
|
| 66 |
+
"depth": 3,
|
| 67 |
+
"z_dim": 2,
|
| 68 |
+
"p_drop": 0.05,
|
| 69 |
+
"use_learned_variances": false,
|
| 70 |
+
"use_additional_losses": true,
|
| 71 |
+
"use_flow": false
|
| 72 |
+
},
|
| 73 |
+
|
| 74 |
+
"seed": 42,
|
| 75 |
+
|
| 76 |
+
"resume_from_checkpoint": null
|
| 77 |
+
}
|
deps/vomp/configs/materials/material_vae/matvae_no_free_nats.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"dry_run": false,
|
| 3 |
+
"standard_vae": false,
|
| 4 |
+
|
| 5 |
+
"data_csv": "datasets/latent_space/materials_filtered.csv",
|
| 6 |
+
"dataloader": {
|
| 7 |
+
"batch_size": 256,
|
| 8 |
+
"num_workers": 8,
|
| 9 |
+
"pin_memory": true,
|
| 10 |
+
"prefetch_factor": 4,
|
| 11 |
+
"persistent_workers": true
|
| 12 |
+
},
|
| 13 |
+
|
| 14 |
+
"project_dir": "./outputs/matvae_ablations/no_free_nats",
|
| 15 |
+
"tracker_name": "tb_logs",
|
| 16 |
+
"log_with": "tensorboard",
|
| 17 |
+
|
| 18 |
+
"epochs": 850,
|
| 19 |
+
"gradient_accumulation_steps": 1,
|
| 20 |
+
"keep_last_checkpoints": 3,
|
| 21 |
+
|
| 22 |
+
"mixed_precision": "no",
|
| 23 |
+
"use_stateful_dataloader": false,
|
| 24 |
+
"find_unused_parameters": false,
|
| 25 |
+
|
| 26 |
+
"compile": {
|
| 27 |
+
"enabled": false,
|
| 28 |
+
"backend": "inductor",
|
| 29 |
+
"mode": "default",
|
| 30 |
+
"fullgraph": true,
|
| 31 |
+
"dynamic": false
|
| 32 |
+
},
|
| 33 |
+
|
| 34 |
+
"optimizer": {
|
| 35 |
+
"lr": 1e-4,
|
| 36 |
+
"weight_decay": 1e-4,
|
| 37 |
+
"grad_clip_norm": 5.0
|
| 38 |
+
},
|
| 39 |
+
|
| 40 |
+
"lr_scheduler": {
|
| 41 |
+
"type": "cosine",
|
| 42 |
+
"eta_min": 1e-5
|
| 43 |
+
},
|
| 44 |
+
|
| 45 |
+
"free_nats": 0.0,
|
| 46 |
+
"kl_annealing": true,
|
| 47 |
+
"kl_annealing_epochs": 200,
|
| 48 |
+
"recon_scale": 1.0,
|
| 49 |
+
"kl_weight": 1.0,
|
| 50 |
+
"iwae_K": 50,
|
| 51 |
+
|
| 52 |
+
"alpha": 1.0,
|
| 53 |
+
"beta": 2.0,
|
| 54 |
+
"gamma": 1.0,
|
| 55 |
+
|
| 56 |
+
"normalization_type": "log_minmax",
|
| 57 |
+
|
| 58 |
+
"eval_interval": 1,
|
| 59 |
+
"save_interval": 1,
|
| 60 |
+
"visualization_interval": 1000,
|
| 61 |
+
"n_vis_samples": 5,
|
| 62 |
+
"n_vis_steps": 10,
|
| 63 |
+
|
| 64 |
+
"model": {
|
| 65 |
+
"width": 256,
|
| 66 |
+
"depth": 3,
|
| 67 |
+
"z_dim": 2,
|
| 68 |
+
"p_drop": 0.05,
|
| 69 |
+
"use_learned_variances": false,
|
| 70 |
+
"use_additional_losses": true
|
| 71 |
+
},
|
| 72 |
+
|
| 73 |
+
"seed": 42,
|
| 74 |
+
|
| 75 |
+
"resume_from_checkpoint": null
|
| 76 |
+
}
|
deps/vomp/configs/materials/material_vae/matvae_standard.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"dry_run": false,
|
| 3 |
+
"standard_vae": true,
|
| 4 |
+
|
| 5 |
+
"data_csv": "datasets/latent_space/materials_filtered.csv",
|
| 6 |
+
"dataloader": {
|
| 7 |
+
"batch_size": 256,
|
| 8 |
+
"num_workers": 8,
|
| 9 |
+
"pin_memory": true,
|
| 10 |
+
"prefetch_factor": 4,
|
| 11 |
+
"persistent_workers": true
|
| 12 |
+
},
|
| 13 |
+
|
| 14 |
+
"project_dir": "./outputs/matvae",
|
| 15 |
+
"tracker_name": "tb_logs",
|
| 16 |
+
"log_with": "tensorboard",
|
| 17 |
+
|
| 18 |
+
"epochs": 850,
|
| 19 |
+
"gradient_accumulation_steps": 1,
|
| 20 |
+
"keep_last_checkpoints": 3,
|
| 21 |
+
|
| 22 |
+
"mixed_precision": "no",
|
| 23 |
+
"use_stateful_dataloader": false,
|
| 24 |
+
"find_unused_parameters": false,
|
| 25 |
+
|
| 26 |
+
"compile": {
|
| 27 |
+
"enabled": false,
|
| 28 |
+
"backend": "inductor",
|
| 29 |
+
"mode": "default",
|
| 30 |
+
"fullgraph": true,
|
| 31 |
+
"dynamic": false
|
| 32 |
+
},
|
| 33 |
+
|
| 34 |
+
"optimizer": {
|
| 35 |
+
"lr": 1e-4,
|
| 36 |
+
"weight_decay": 1e-4,
|
| 37 |
+
"grad_clip_norm": 5.0
|
| 38 |
+
},
|
| 39 |
+
|
| 40 |
+
"lr_scheduler": {
|
| 41 |
+
"type": "cosine",
|
| 42 |
+
"eta_min": 1e-5
|
| 43 |
+
},
|
| 44 |
+
|
| 45 |
+
"free_nats": 0.1,
|
| 46 |
+
"kl_annealing": true,
|
| 47 |
+
"kl_annealing_epochs": 200,
|
| 48 |
+
"recon_scale": 1.0,
|
| 49 |
+
"kl_weight": 1.0,
|
| 50 |
+
"iwae_K": 50,
|
| 51 |
+
|
| 52 |
+
"alpha": 1.0,
|
| 53 |
+
"beta": 2.0,
|
| 54 |
+
"gamma": 1.0,
|
| 55 |
+
|
| 56 |
+
"normalization_type": "log_minmax",
|
| 57 |
+
|
| 58 |
+
"eval_interval": 1,
|
| 59 |
+
"save_interval": 1,
|
| 60 |
+
"visualization_interval": 1000,
|
| 61 |
+
"n_vis_samples": 5,
|
| 62 |
+
"n_vis_steps": 10,
|
| 63 |
+
|
| 64 |
+
"model": {
|
| 65 |
+
"width": 256,
|
| 66 |
+
"depth": 3,
|
| 67 |
+
"z_dim": 2,
|
| 68 |
+
"p_drop": 0.05,
|
| 69 |
+
"use_learned_variances": false,
|
| 70 |
+
"use_additional_losses": true
|
| 71 |
+
},
|
| 72 |
+
|
| 73 |
+
"seed": 42,
|
| 74 |
+
|
| 75 |
+
"resume_from_checkpoint": null
|
| 76 |
+
}
|
deps/vomp/configs/materials/material_vae/matvae_standard_norm.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"dry_run": false,
|
| 3 |
+
"standard_vae": true,
|
| 4 |
+
|
| 5 |
+
"data_csv": "datasets/latent_space/materials_filtered.csv",
|
| 6 |
+
"dataloader": {
|
| 7 |
+
"batch_size": 256,
|
| 8 |
+
"num_workers": 8,
|
| 9 |
+
"pin_memory": true,
|
| 10 |
+
"prefetch_factor": 4,
|
| 11 |
+
"persistent_workers": true
|
| 12 |
+
},
|
| 13 |
+
|
| 14 |
+
"project_dir": "./outputs/matvae",
|
| 15 |
+
"tracker_name": "tb_logs",
|
| 16 |
+
"log_with": "tensorboard",
|
| 17 |
+
|
| 18 |
+
"epochs": 850,
|
| 19 |
+
"gradient_accumulation_steps": 1,
|
| 20 |
+
"keep_last_checkpoints": 3,
|
| 21 |
+
|
| 22 |
+
"mixed_precision": "no",
|
| 23 |
+
"use_stateful_dataloader": false,
|
| 24 |
+
"find_unused_parameters": false,
|
| 25 |
+
|
| 26 |
+
"compile": {
|
| 27 |
+
"enabled": false,
|
| 28 |
+
"backend": "inductor",
|
| 29 |
+
"mode": "default",
|
| 30 |
+
"fullgraph": true,
|
| 31 |
+
"dynamic": false
|
| 32 |
+
},
|
| 33 |
+
|
| 34 |
+
"optimizer": {
|
| 35 |
+
"lr": 1e-4,
|
| 36 |
+
"weight_decay": 1e-4,
|
| 37 |
+
"grad_clip_norm": 5.0
|
| 38 |
+
},
|
| 39 |
+
|
| 40 |
+
"lr_scheduler": {
|
| 41 |
+
"type": "cosine",
|
| 42 |
+
"eta_min": 1e-5
|
| 43 |
+
},
|
| 44 |
+
|
| 45 |
+
"free_nats": 0.1,
|
| 46 |
+
"kl_annealing": true,
|
| 47 |
+
"kl_annealing_epochs": 200,
|
| 48 |
+
"recon_scale": 1.0,
|
| 49 |
+
"kl_weight": 1.0,
|
| 50 |
+
"iwae_K": 50,
|
| 51 |
+
|
| 52 |
+
"alpha": 1.0,
|
| 53 |
+
"beta": 2.0,
|
| 54 |
+
"gamma": 1.0,
|
| 55 |
+
|
| 56 |
+
"normalization_type": "standard",
|
| 57 |
+
|
| 58 |
+
"eval_interval": 1,
|
| 59 |
+
"save_interval": 1,
|
| 60 |
+
"visualization_interval": 1000,
|
| 61 |
+
"n_vis_samples": 5,
|
| 62 |
+
"n_vis_steps": 10,
|
| 63 |
+
|
| 64 |
+
"model": {
|
| 65 |
+
"width": 256,
|
| 66 |
+
"depth": 3,
|
| 67 |
+
"z_dim": 2,
|
| 68 |
+
"p_drop": 0.05,
|
| 69 |
+
"use_learned_variances": false,
|
| 70 |
+
"use_additional_losses": true
|
| 71 |
+
},
|
| 72 |
+
|
| 73 |
+
"seed": 42,
|
| 74 |
+
|
| 75 |
+
"resume_from_checkpoint": null
|
| 76 |
+
}
|
deps/vomp/configs/materials/material_vae/standard_vae_final.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"dry_run": false,
|
| 3 |
+
"standard_vae": true,
|
| 4 |
+
|
| 5 |
+
"data_csv": "datasets/latent_space/materials_filtered.csv",
|
| 6 |
+
"dataloader": {
|
| 7 |
+
"batch_size": 256,
|
| 8 |
+
"num_workers": 8,
|
| 9 |
+
"pin_memory": true,
|
| 10 |
+
"prefetch_factor": 4,
|
| 11 |
+
"persistent_workers": true
|
| 12 |
+
},
|
| 13 |
+
|
| 14 |
+
"project_dir": "./outputs/standard_vae",
|
| 15 |
+
"tracker_name": "tb_logs",
|
| 16 |
+
"log_with": "tensorboard",
|
| 17 |
+
|
| 18 |
+
"epochs": 25000,
|
| 19 |
+
"gradient_accumulation_steps": 1,
|
| 20 |
+
"keep_last_checkpoints": 3,
|
| 21 |
+
|
| 22 |
+
"mixed_precision": "no",
|
| 23 |
+
"use_stateful_dataloader": false,
|
| 24 |
+
"find_unused_parameters": false,
|
| 25 |
+
|
| 26 |
+
"compile": {
|
| 27 |
+
"enabled": false,
|
| 28 |
+
"backend": "inductor",
|
| 29 |
+
"mode": "default",
|
| 30 |
+
"fullgraph": true,
|
| 31 |
+
"dynamic": false
|
| 32 |
+
},
|
| 33 |
+
|
| 34 |
+
"optimizer": {
|
| 35 |
+
"lr": 1e-3,
|
| 36 |
+
"weight_decay": 1e-4,
|
| 37 |
+
"grad_clip_norm": 5.0
|
| 38 |
+
},
|
| 39 |
+
|
| 40 |
+
"lr_scheduler": {
|
| 41 |
+
"type": "cosine",
|
| 42 |
+
"eta_min": 1e-5
|
| 43 |
+
},
|
| 44 |
+
|
| 45 |
+
"free_nats": 0.1,
|
| 46 |
+
"kl_annealing": true,
|
| 47 |
+
"kl_annealing_epochs": 200,
|
| 48 |
+
"recon_scale": 1.0,
|
| 49 |
+
"iwae_K": 50,
|
| 50 |
+
|
| 51 |
+
"eval_interval": 1,
|
| 52 |
+
"save_interval": 1,
|
| 53 |
+
"visualization_interval": 1000,
|
| 54 |
+
"n_vis_samples": 5,
|
| 55 |
+
"n_vis_steps": 10,
|
| 56 |
+
|
| 57 |
+
"model": {
|
| 58 |
+
"width": 256,
|
| 59 |
+
"depth": 3,
|
| 60 |
+
"z_dim": 2,
|
| 61 |
+
"p_drop": 0.05
|
| 62 |
+
},
|
| 63 |
+
|
| 64 |
+
"seed": 42,
|
| 65 |
+
|
| 66 |
+
"resume_from_checkpoint": null
|
| 67 |
+
}
|
deps/vomp/configs/sim/armchair_and_orange.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"simulation": {
|
| 3 |
+
"dt": 0.01,
|
| 4 |
+
"gravity": [0.0, -9.8, 0.0],
|
| 5 |
+
"max_frames": 500,
|
| 6 |
+
"auto_start": false,
|
| 7 |
+
"contact": {
|
| 8 |
+
"friction_enable": true,
|
| 9 |
+
"d_hat": 0.005
|
| 10 |
+
}
|
| 11 |
+
},
|
| 12 |
+
"engine": {
|
| 13 |
+
"type": "cuda"
|
| 14 |
+
},
|
| 15 |
+
"contact_model": {
|
| 16 |
+
"friction": 0.5,
|
| 17 |
+
"contact_resistance": 2.0
|
| 18 |
+
},
|
| 19 |
+
"ground": {
|
| 20 |
+
"enable": true,
|
| 21 |
+
"height": 0.0
|
| 22 |
+
},
|
| 23 |
+
"objects": [
|
| 24 |
+
{
|
| 25 |
+
"name": "armchair_1",
|
| 26 |
+
"type": "msh",
|
| 27 |
+
"msh_path": "assets/armchair/armchair.msh",
|
| 28 |
+
"normalize_visual_mesh": false,
|
| 29 |
+
"scale": 1.0,
|
| 30 |
+
"translation": [0.0, 0.004, 0.0],
|
| 31 |
+
"rotation": [270.0, 0.0, 0.0],
|
| 32 |
+
"material": {
|
| 33 |
+
"file": "assets/armchair/materials_aligned.npz"
|
| 34 |
+
}
|
| 35 |
+
},
|
| 36 |
+
{
|
| 37 |
+
"name": "orange_1",
|
| 38 |
+
"type": "msh",
|
| 39 |
+
"msh_path": "assets/orange/orange_02_inst_base.msh",
|
| 40 |
+
"normalize_visual_mesh": false,
|
| 41 |
+
"scale": 3.0,
|
| 42 |
+
"translation": [0.0, 2.0, 0.0],
|
| 43 |
+
"rotation": [0.0, 0.0, 0.0],
|
| 44 |
+
"material": {
|
| 45 |
+
"file": "assets/orange/materials.npz"
|
| 46 |
+
}
|
| 47 |
+
}
|
| 48 |
+
],
|
| 49 |
+
"gui": {
|
| 50 |
+
"enable": true
|
| 51 |
+
},
|
| 52 |
+
"output": {
|
| 53 |
+
"directory": "./outputs/simulation_output/armchair_and_orange",
|
| 54 |
+
"save_meshes": true
|
| 55 |
+
},
|
| 56 |
+
"logging": {
|
| 57 |
+
"level": "warn"
|
| 58 |
+
}
|
| 59 |
+
}
|
deps/vomp/configs/sim/falling_armchair.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"simulation": {
|
| 3 |
+
"dt": 0.01,
|
| 4 |
+
"gravity": [0.0, -9.8, 0.0],
|
| 5 |
+
"max_frames": 500,
|
| 6 |
+
"auto_start": false,
|
| 7 |
+
"contact": {
|
| 8 |
+
"friction_enable": true,
|
| 9 |
+
"d_hat": 0.01
|
| 10 |
+
}
|
| 11 |
+
},
|
| 12 |
+
"engine": {
|
| 13 |
+
"type": "cuda"
|
| 14 |
+
},
|
| 15 |
+
"contact_model": {
|
| 16 |
+
"friction": 0.5,
|
| 17 |
+
"contact_resistance": 1.0
|
| 18 |
+
},
|
| 19 |
+
"ground": {
|
| 20 |
+
"enable": true,
|
| 21 |
+
"height": 0.0
|
| 22 |
+
},
|
| 23 |
+
"objects": [
|
| 24 |
+
{
|
| 25 |
+
"name": "armchair_1",
|
| 26 |
+
"type": "msh",
|
| 27 |
+
"msh_path": "assets/armchair/armchair.msh",
|
| 28 |
+
"visual_mesh": "assets/armchair/armchair_inst_base.obj",
|
| 29 |
+
"normalize_visual_mesh": false,
|
| 30 |
+
"scale": 1.0,
|
| 31 |
+
"translation": [0.0, 1.0, 0.0],
|
| 32 |
+
"rotation": [0.0, 0.0, 0.0],
|
| 33 |
+
"material": {
|
| 34 |
+
"file": "assets/armchair/materials_aligned.npz"
|
| 35 |
+
}
|
| 36 |
+
}
|
| 37 |
+
],
|
| 38 |
+
"gui": {
|
| 39 |
+
"enable": true
|
| 40 |
+
},
|
| 41 |
+
"output": {
|
| 42 |
+
"directory": "./outputs/simulation_output/falling_armchair",
|
| 43 |
+
"save_meshes": true
|
| 44 |
+
},
|
| 45 |
+
"logging": {
|
| 46 |
+
"level": "warn"
|
| 47 |
+
}
|
| 48 |
+
}
|
deps/vomp/configs/sim/falling_bar_stool.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"simulation": {
|
| 3 |
+
"dt": 0.01,
|
| 4 |
+
"gravity": [0.0, -9.8, 0.0],
|
| 5 |
+
"max_frames": 500,
|
| 6 |
+
"auto_start": false,
|
| 7 |
+
"contact": {
|
| 8 |
+
"friction_enable": true,
|
| 9 |
+
"d_hat": 0.01
|
| 10 |
+
}
|
| 11 |
+
},
|
| 12 |
+
"engine": {
|
| 13 |
+
"type": "cuda"
|
| 14 |
+
},
|
| 15 |
+
"contact_model": {
|
| 16 |
+
"friction": 0.5,
|
| 17 |
+
"contact_resistance": 1.0
|
| 18 |
+
},
|
| 19 |
+
"ground": {
|
| 20 |
+
"enable": true,
|
| 21 |
+
"height": 0.0
|
| 22 |
+
},
|
| 23 |
+
"objects": [
|
| 24 |
+
{
|
| 25 |
+
"name": "bar_stool_1",
|
| 26 |
+
"type": "msh",
|
| 27 |
+
"msh_path": "assets/bar_stool/bar_stool_inst_base.msh",
|
| 28 |
+
"visual_mesh": "assets/bar_stool/bar_stool_inst_base.obj",
|
| 29 |
+
"normalize_visual_mesh": false,
|
| 30 |
+
"scale": 1.0,
|
| 31 |
+
"translation": [0.0, 2.0, 0.0],
|
| 32 |
+
"rotation": [0.0, 0.0, 0.0],
|
| 33 |
+
"material": {
|
| 34 |
+
"youngs_modulus": 1e5,
|
| 35 |
+
"density": 900.0,
|
| 36 |
+
"poisson_ratio": 0.3
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
],
|
| 40 |
+
"gui": {
|
| 41 |
+
"enable": true
|
| 42 |
+
},
|
| 43 |
+
"output": {
|
| 44 |
+
"directory": "./outputs/simulation_output/falling_bar_stool",
|
| 45 |
+
"save_meshes": true
|
| 46 |
+
},
|
| 47 |
+
"logging": {
|
| 48 |
+
"level": "warn"
|
| 49 |
+
}
|
| 50 |
+
}
|
deps/vomp/configs/sim/falling_birch.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"simulation": {
|
| 3 |
+
"dt": 0.005,
|
| 4 |
+
"gravity": [0.0, -9.8, 0.0],
|
| 5 |
+
"max_frames": 500,
|
| 6 |
+
"auto_start": false,
|
| 7 |
+
"contact": {
|
| 8 |
+
"friction_enable": true,
|
| 9 |
+
"d_hat": 0.01
|
| 10 |
+
}
|
| 11 |
+
},
|
| 12 |
+
"engine": {
|
| 13 |
+
"type": "cuda"
|
| 14 |
+
},
|
| 15 |
+
"contact_model": {
|
| 16 |
+
"friction": 0.5,
|
| 17 |
+
"contact_resistance": 1.0
|
| 18 |
+
},
|
| 19 |
+
"ground": {
|
| 20 |
+
"enable": true,
|
| 21 |
+
"height": 0.0
|
| 22 |
+
},
|
| 23 |
+
"objects": [
|
| 24 |
+
{
|
| 25 |
+
"name": "birch_1",
|
| 26 |
+
"type": "msh",
|
| 27 |
+
"msh_path": "assets/birch/birch.msh",
|
| 28 |
+
"visual_mesh": "assets/birch/birch_lowbackseat_inst_base.obj",
|
| 29 |
+
"normalize_visual_mesh": false,
|
| 30 |
+
"scale": 1.0,
|
| 31 |
+
"translation": [0.0, 2.0, 0.0],
|
| 32 |
+
"rotation": [0.0, 0.0, 0.0],
|
| 33 |
+
"material": {
|
| 34 |
+
"youngs_modulus": 1e6,
|
| 35 |
+
"density": 1000.0,
|
| 36 |
+
"poisson_ratio": 0.45
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
],
|
| 40 |
+
"gui": {
|
| 41 |
+
"enable": true
|
| 42 |
+
},
|
| 43 |
+
"output": {
|
| 44 |
+
"directory": "./outputs/simulation_output/falling_birch",
|
| 45 |
+
"save_meshes": true
|
| 46 |
+
},
|
| 47 |
+
"logging": {
|
| 48 |
+
"level": "warn"
|
| 49 |
+
}
|
| 50 |
+
}
|
deps/vomp/configs/sim/falling_oranges.json
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"simulation": {
|
| 3 |
+
"dt": 0.02,
|
| 4 |
+
"gravity": [0.0, -9.8, 0.0],
|
| 5 |
+
"max_frames": 500,
|
| 6 |
+
"auto_start": false,
|
| 7 |
+
"contact": {
|
| 8 |
+
"friction_enable": true,
|
| 9 |
+
"d_hat": 0.01
|
| 10 |
+
}
|
| 11 |
+
},
|
| 12 |
+
"engine": {
|
| 13 |
+
"type": "cuda"
|
| 14 |
+
},
|
| 15 |
+
"contact_model": {
|
| 16 |
+
"friction": 0.5,
|
| 17 |
+
"contact_resistance": 1.0
|
| 18 |
+
},
|
| 19 |
+
"ground": {
|
| 20 |
+
"enable": true,
|
| 21 |
+
"height": 0.0
|
| 22 |
+
},
|
| 23 |
+
"objects": [
|
| 24 |
+
{
|
| 25 |
+
"name": "orange_1",
|
| 26 |
+
"type": "voxel",
|
| 27 |
+
"voxel_path": "assets/orange/voxels.ply",
|
| 28 |
+
"visual_mesh": "assets/orange/orange_02_inst_base.obj",
|
| 29 |
+
"normalize_visual_mesh": true,
|
| 30 |
+
"voxel_size": 1.0,
|
| 31 |
+
"scale": 1.0,
|
| 32 |
+
"max_voxels": 32000,
|
| 33 |
+
"translation": [0.0, 2.0, 0.0],
|
| 34 |
+
"rotation": [0.0, 0.0, 0.0],
|
| 35 |
+
"material": {
|
| 36 |
+
"file": "assets/orange/materials.npz"
|
| 37 |
+
}
|
| 38 |
+
},
|
| 39 |
+
{
|
| 40 |
+
"name": "orange_2",
|
| 41 |
+
"type": "voxel",
|
| 42 |
+
"voxel_path": "assets/orange/voxels.ply",
|
| 43 |
+
"visual_mesh": "assets/orange/orange_02_inst_base.obj",
|
| 44 |
+
"normalize_visual_mesh": true,
|
| 45 |
+
"voxel_size": 1.0,
|
| 46 |
+
"scale": 1.0,
|
| 47 |
+
"max_voxels": 32000,
|
| 48 |
+
"translation": [0.0, 3.5, 0.0],
|
| 49 |
+
"rotation": [0.0, 0.0, 0.0],
|
| 50 |
+
"material": {
|
| 51 |
+
"file": "assets/orange/materials.npz"
|
| 52 |
+
}
|
| 53 |
+
},
|
| 54 |
+
{
|
| 55 |
+
"name": "orange_3",
|
| 56 |
+
"type": "voxel",
|
| 57 |
+
"voxel_path": "assets/orange/voxels.ply",
|
| 58 |
+
"visual_mesh": "assets/orange/orange_02_inst_base.obj",
|
| 59 |
+
"normalize_visual_mesh": true,
|
| 60 |
+
"voxel_size": 1.0,
|
| 61 |
+
"scale": 1.0,
|
| 62 |
+
"max_voxels": 32000,
|
| 63 |
+
"translation": [0.0, 5.0, 0.0],
|
| 64 |
+
"rotation": [0.0, 0.0, 0.0],
|
| 65 |
+
"material": {
|
| 66 |
+
"file": "assets/orange/materials.npz"
|
| 67 |
+
}
|
| 68 |
+
}
|
| 69 |
+
],
|
| 70 |
+
"gui": {
|
| 71 |
+
"enable": true
|
| 72 |
+
},
|
| 73 |
+
"output": {
|
| 74 |
+
"directory": "./outputs/simulation_output/falling_oranges",
|
| 75 |
+
"save_meshes": true
|
| 76 |
+
},
|
| 77 |
+
"logging": {
|
| 78 |
+
"level": "warn"
|
| 79 |
+
}
|
| 80 |
+
}
|
deps/vomp/configs/sim/falling_sphere_soft.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"simulation": {
|
| 3 |
+
"dt": 0.005,
|
| 4 |
+
"gravity": [0.0, -9.8, 0.0],
|
| 5 |
+
"max_frames": 500,
|
| 6 |
+
"auto_start": false,
|
| 7 |
+
"contact": {
|
| 8 |
+
"friction_enable": false,
|
| 9 |
+
"d_hat": 0.005
|
| 10 |
+
}
|
| 11 |
+
},
|
| 12 |
+
"engine": {
|
| 13 |
+
"type": "cuda"
|
| 14 |
+
},
|
| 15 |
+
"contact_model": {
|
| 16 |
+
"friction": 0.5,
|
| 17 |
+
"contact_resistance": 0.01
|
| 18 |
+
},
|
| 19 |
+
"ground": {
|
| 20 |
+
"enable": true,
|
| 21 |
+
"height": 0.0
|
| 22 |
+
},
|
| 23 |
+
"objects": [
|
| 24 |
+
{
|
| 25 |
+
"name": "soft_sphere",
|
| 26 |
+
"type": "msh",
|
| 27 |
+
"msh_path": "assets/sphere/sphere_tetrahedral.msh",
|
| 28 |
+
"visual_mesh": "assets/sphere/sphere_visual.obj",
|
| 29 |
+
"normalize_visual_mesh": false,
|
| 30 |
+
"scale": 1.0,
|
| 31 |
+
"translation": [0.0, 0.3, 0.0],
|
| 32 |
+
"rotation": [0.0, 0.0, 0.0],
|
| 33 |
+
"material": {
|
| 34 |
+
"type": "StableNeoHookean",
|
| 35 |
+
"young_modulus": 1e4,
|
| 36 |
+
"poisson_ratio": 0.3,
|
| 37 |
+
"density": 1000
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
],
|
| 41 |
+
"gui": {
|
| 42 |
+
"enable": true
|
| 43 |
+
},
|
| 44 |
+
"output": {
|
| 45 |
+
"directory": "./outputs/simulation_output/fem",
|
| 46 |
+
"save_meshes": true
|
| 47 |
+
},
|
| 48 |
+
"logging": {
|
| 49 |
+
"level": "info"
|
| 50 |
+
}
|
| 51 |
+
}
|
deps/vomp/configs/sim/zag_and_falling_orange.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"simulation": {
|
| 3 |
+
"dt": 0.01,
|
| 4 |
+
"gravity": [0.0, -9.8, 0.0],
|
| 5 |
+
"max_frames": 500,
|
| 6 |
+
"auto_start": false,
|
| 7 |
+
"contact": {
|
| 8 |
+
"friction_enable": true,
|
| 9 |
+
"d_hat": 0.005
|
| 10 |
+
}
|
| 11 |
+
},
|
| 12 |
+
"engine": {
|
| 13 |
+
"type": "cuda"
|
| 14 |
+
},
|
| 15 |
+
"contact_model": {
|
| 16 |
+
"friction": 0.5,
|
| 17 |
+
"contact_resistance": 2.0
|
| 18 |
+
},
|
| 19 |
+
"ground": {
|
| 20 |
+
"enable": true,
|
| 21 |
+
"height": 0.0
|
| 22 |
+
},
|
| 23 |
+
"objects": [
|
| 24 |
+
{
|
| 25 |
+
"name": "zag_middle_base",
|
| 26 |
+
"type": "msh",
|
| 27 |
+
"msh_path": "assets/zag/zag_middle_inst_base.msh",
|
| 28 |
+
"normalize_visual_mesh": false,
|
| 29 |
+
"scale": 1.0,
|
| 30 |
+
"translation": [0.0, 0.001, 0.0],
|
| 31 |
+
"rotation": [270.0, 0.0, 0.0],
|
| 32 |
+
"material": {
|
| 33 |
+
"file": "assets/zag/materials.npz"
|
| 34 |
+
}
|
| 35 |
+
},
|
| 36 |
+
{
|
| 37 |
+
"name": "orange_1",
|
| 38 |
+
"type": "msh",
|
| 39 |
+
"msh_path": "assets/orange/orange_02_inst_base.msh",
|
| 40 |
+
"normalize_visual_mesh": false,
|
| 41 |
+
"scale": 3.0,
|
| 42 |
+
"translation": [0.0, 2.0, 0.0],
|
| 43 |
+
"rotation": [0.0, 0.0, 0.0],
|
| 44 |
+
"material": {
|
| 45 |
+
"file": "assets/orange/materials.npz"
|
| 46 |
+
}
|
| 47 |
+
}
|
| 48 |
+
],
|
| 49 |
+
"gui": {
|
| 50 |
+
"enable": true
|
| 51 |
+
},
|
| 52 |
+
"output": {
|
| 53 |
+
"directory": "./outputs/simulation_output/zag_and_falling_orange",
|
| 54 |
+
"save_meshes": true
|
| 55 |
+
},
|
| 56 |
+
"logging": {
|
| 57 |
+
"level": "warn"
|
| 58 |
+
}
|
| 59 |
+
}
|
deps/vomp/configs/sim/zag_and_falling_oranges.json
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"simulation": {
|
| 3 |
+
"dt": 0.01,
|
| 4 |
+
"gravity": [0.0, -9.8, 0.0],
|
| 5 |
+
"max_frames": 500,
|
| 6 |
+
"auto_start": false,
|
| 7 |
+
"contact": {
|
| 8 |
+
"friction_enable": true,
|
| 9 |
+
"d_hat": 0.005
|
| 10 |
+
}
|
| 11 |
+
},
|
| 12 |
+
"engine": {
|
| 13 |
+
"type": "cuda"
|
| 14 |
+
},
|
| 15 |
+
"contact_model": {
|
| 16 |
+
"friction": 0.5,
|
| 17 |
+
"contact_resistance": 2.0
|
| 18 |
+
},
|
| 19 |
+
"ground": {
|
| 20 |
+
"enable": true,
|
| 21 |
+
"height": 0.0
|
| 22 |
+
},
|
| 23 |
+
"objects": [
|
| 24 |
+
{
|
| 25 |
+
"name": "zag_middle_base",
|
| 26 |
+
"type": "msh",
|
| 27 |
+
"msh_path": "assets/zag/zag_middle_inst_base.msh",
|
| 28 |
+
"normalize_visual_mesh": false,
|
| 29 |
+
"scale": 1.0,
|
| 30 |
+
"translation": [0.0, 0.01, 0.0],
|
| 31 |
+
"rotation": [270.0, 0.0, 0.0],
|
| 32 |
+
"apply_boundary_conditions": true,
|
| 33 |
+
"boundary_fix_percentage": 0.15,
|
| 34 |
+
"material": {
|
| 35 |
+
"file": "assets/zag/materials.npz"
|
| 36 |
+
}
|
| 37 |
+
},
|
| 38 |
+
|
| 39 |
+
{
|
| 40 |
+
"name": "lemon_2",
|
| 41 |
+
"type": "msh",
|
| 42 |
+
"msh_path": "assets/orange/orange_02_inst_base.msh",
|
| 43 |
+
"normalize_visual_mesh": false,
|
| 44 |
+
"scale": 2.0,
|
| 45 |
+
"translation": [0.15, 1.0, 0.0],
|
| 46 |
+
"rotation": [0.0, 0.0, 0.0],
|
| 47 |
+
"material": {
|
| 48 |
+
"file": "assets/orange/materials.npz"
|
| 49 |
+
}
|
| 50 |
+
},
|
| 51 |
+
{
|
| 52 |
+
"name": "lemon_3",
|
| 53 |
+
"type": "msh",
|
| 54 |
+
"msh_path": "assets/orange/orange_02_inst_base.msh",
|
| 55 |
+
"normalize_visual_mesh": false,
|
| 56 |
+
"scale": 2.0,
|
| 57 |
+
"translation": [-0.15, 1.0, 0.0],
|
| 58 |
+
"rotation": [0.0, 0.0, 0.0],
|
| 59 |
+
"material": {
|
| 60 |
+
"file": "assets/orange/materials.npz"
|
| 61 |
+
}
|
| 62 |
+
},
|
| 63 |
+
{
|
| 64 |
+
"name": "lemon_4",
|
| 65 |
+
"type": "msh",
|
| 66 |
+
"msh_path": "assets/orange/orange_02_inst_base.msh",
|
| 67 |
+
"normalize_visual_mesh": false,
|
| 68 |
+
"scale": 2.0,
|
| 69 |
+
"translation": [0.0, 1.0, 0.15],
|
| 70 |
+
"rotation": [0.0, 0.0, 0.0],
|
| 71 |
+
"material": {
|
| 72 |
+
"file": "assets/orange/materials.npz"
|
| 73 |
+
}
|
| 74 |
+
},
|
| 75 |
+
{
|
| 76 |
+
"name": "lemon_5",
|
| 77 |
+
"type": "msh",
|
| 78 |
+
"msh_path": "assets/orange/orange_02_inst_base.msh",
|
| 79 |
+
"normalize_visual_mesh": false,
|
| 80 |
+
"scale": 2.0,
|
| 81 |
+
"translation": [0.0, 1.0, -0.15],
|
| 82 |
+
"rotation": [0.0, 0.0, 0.0],
|
| 83 |
+
"material": {
|
| 84 |
+
"file": "assets/orange/materials.npz"
|
| 85 |
+
}
|
| 86 |
+
}
|
| 87 |
+
],
|
| 88 |
+
"gui": {
|
| 89 |
+
"enable": true
|
| 90 |
+
},
|
| 91 |
+
"output": {
|
| 92 |
+
"directory": "./outputs/simulation_output/zag_and_falling_oranges",
|
| 93 |
+
"save_meshes": true
|
| 94 |
+
},
|
| 95 |
+
"logging": {
|
| 96 |
+
"level": "warn"
|
| 97 |
+
}
|
| 98 |
+
}
|
deps/vomp/dataset_toolkits/abo/ABO500.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
|
| 16 |
+
import os
|
| 17 |
+
import json
|
| 18 |
+
import argparse
|
| 19 |
+
import pandas as pd
|
| 20 |
+
from concurrent.futures import ThreadPoolExecutor
|
| 21 |
+
from tqdm import tqdm
|
| 22 |
+
import hashlib
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def add_args(parser: argparse.ArgumentParser):
|
| 26 |
+
parser.add_argument(
|
| 27 |
+
"--abo_500_dir",
|
| 28 |
+
type=str,
|
| 29 |
+
default="/home/rdagli/code/datasets/abo_500",
|
| 30 |
+
help="Path to the ABO 500 dataset directory",
|
| 31 |
+
)
|
| 32 |
+
parser.add_argument(
|
| 33 |
+
"--abo_3d_dir",
|
| 34 |
+
type=str,
|
| 35 |
+
default="/home/rdagli/code/datasets/abo-3dmodels/3dmodels",
|
| 36 |
+
help="Path to the ABO 3D models directory",
|
| 37 |
+
)
|
| 38 |
+
parser.add_argument(
|
| 39 |
+
"--split",
|
| 40 |
+
type=str,
|
| 41 |
+
default="all",
|
| 42 |
+
choices=["train", "val", "test", "all"],
|
| 43 |
+
help="Which split to process",
|
| 44 |
+
)
|
| 45 |
+
parser.add_argument(
|
| 46 |
+
"--limit",
|
| 47 |
+
type=int,
|
| 48 |
+
default=None,
|
| 49 |
+
help="Limit to first N objects for testing",
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def get_file_hash(file_path):
|
| 54 |
+
"""Get SHA256 hash of a file."""
|
| 55 |
+
hasher = hashlib.sha256()
|
| 56 |
+
with open(file_path, "rb") as f:
|
| 57 |
+
for chunk in iter(lambda: f.read(4096), b""):
|
| 58 |
+
hasher.update(chunk)
|
| 59 |
+
return hasher.hexdigest()
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def get_metadata(abo_500_dir, abo_3d_dir, split="all", limit=None, **kwargs):
|
| 63 |
+
"""Get metadata for ABO 500 dataset."""
|
| 64 |
+
splits_path = os.path.join(abo_500_dir, "splits.json")
|
| 65 |
+
|
| 66 |
+
if not os.path.exists(splits_path):
|
| 67 |
+
raise FileNotFoundError(f"Splits file not found at {splits_path}")
|
| 68 |
+
|
| 69 |
+
with open(splits_path, "r") as f:
|
| 70 |
+
splits_data = json.load(f)
|
| 71 |
+
|
| 72 |
+
if split == "all":
|
| 73 |
+
object_ids = splits_data["train"] + splits_data["val"] + splits_data["test"]
|
| 74 |
+
else:
|
| 75 |
+
object_ids = splits_data[split]
|
| 76 |
+
|
| 77 |
+
# Apply limit if specified
|
| 78 |
+
if limit is not None:
|
| 79 |
+
object_ids = object_ids[:limit]
|
| 80 |
+
|
| 81 |
+
print(f"Processing {len(object_ids)} objects from {split} split")
|
| 82 |
+
|
| 83 |
+
# Create metadata records
|
| 84 |
+
metadata_records = []
|
| 85 |
+
missing_files = []
|
| 86 |
+
|
| 87 |
+
for object_id in tqdm(object_ids, desc="Building metadata"):
|
| 88 |
+
# Extract base ID (remove suffix after underscore if present)
|
| 89 |
+
base_id = object_id.split("_")[0]
|
| 90 |
+
|
| 91 |
+
# Search for GLB file - try multiple patterns and locations
|
| 92 |
+
glb_path = None
|
| 93 |
+
|
| 94 |
+
# Pattern 1: Try with base_id in the directory based on first character
|
| 95 |
+
first_char = base_id[0]
|
| 96 |
+
candidate_path = os.path.join(
|
| 97 |
+
abo_3d_dir, "original", first_char, f"{base_id}.glb"
|
| 98 |
+
)
|
| 99 |
+
if os.path.exists(candidate_path):
|
| 100 |
+
glb_path = candidate_path
|
| 101 |
+
else:
|
| 102 |
+
# Pattern 2: Try with full object_id (without underscore splitting)
|
| 103 |
+
first_char_full = object_id[0]
|
| 104 |
+
candidate_path = os.path.join(
|
| 105 |
+
abo_3d_dir, "original", first_char_full, f"{object_id}.glb"
|
| 106 |
+
)
|
| 107 |
+
if os.path.exists(candidate_path):
|
| 108 |
+
glb_path = candidate_path
|
| 109 |
+
else:
|
| 110 |
+
# Pattern 3: Search in all directories for the base_id
|
| 111 |
+
for dir_name in os.listdir(os.path.join(abo_3d_dir, "original")):
|
| 112 |
+
dir_path = os.path.join(abo_3d_dir, "original", dir_name)
|
| 113 |
+
if os.path.isdir(dir_path):
|
| 114 |
+
candidate_path = os.path.join(dir_path, f"{base_id}.glb")
|
| 115 |
+
if os.path.exists(candidate_path):
|
| 116 |
+
glb_path = candidate_path
|
| 117 |
+
break
|
| 118 |
+
# Also try the full object_id
|
| 119 |
+
candidate_path = os.path.join(dir_path, f"{object_id}.glb")
|
| 120 |
+
if os.path.exists(candidate_path):
|
| 121 |
+
glb_path = candidate_path
|
| 122 |
+
break
|
| 123 |
+
|
| 124 |
+
if glb_path and os.path.exists(glb_path):
|
| 125 |
+
# Get file hash
|
| 126 |
+
try:
|
| 127 |
+
sha256 = get_file_hash(glb_path)
|
| 128 |
+
metadata_records.append(
|
| 129 |
+
{
|
| 130 |
+
"object_id": object_id,
|
| 131 |
+
"sha256": sha256,
|
| 132 |
+
"local_path": glb_path,
|
| 133 |
+
"file_type": "glb",
|
| 134 |
+
"split": split,
|
| 135 |
+
"dataset": "ABO500",
|
| 136 |
+
}
|
| 137 |
+
)
|
| 138 |
+
except Exception as e:
|
| 139 |
+
print(f"Error processing {object_id}: {e}")
|
| 140 |
+
missing_files.append(object_id)
|
| 141 |
+
else:
|
| 142 |
+
print(
|
| 143 |
+
f"Warning: GLB file not found for {object_id} (tried base_id: {base_id})"
|
| 144 |
+
)
|
| 145 |
+
missing_files.append(object_id)
|
| 146 |
+
|
| 147 |
+
if missing_files:
|
| 148 |
+
print(f"Warning: {len(missing_files)} objects have missing GLB files")
|
| 149 |
+
|
| 150 |
+
metadata = pd.DataFrame(metadata_records)
|
| 151 |
+
return metadata
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
def download(metadata, output_dir, **kwargs):
|
| 155 |
+
"""For ABO 500, files are already downloaded, so just return local paths."""
|
| 156 |
+
download_records = []
|
| 157 |
+
|
| 158 |
+
for _, row in metadata.iterrows():
|
| 159 |
+
download_records.append(
|
| 160 |
+
{"sha256": row["sha256"], "local_path": row["local_path"]}
|
| 161 |
+
)
|
| 162 |
+
|
| 163 |
+
return pd.DataFrame(download_records)
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
def foreach_instance(
|
| 167 |
+
metadata, output_dir, func, max_workers=None, desc="Processing objects"
|
| 168 |
+
) -> pd.DataFrame:
|
| 169 |
+
"""Process each instance in the metadata."""
|
| 170 |
+
import os
|
| 171 |
+
from concurrent.futures import ThreadPoolExecutor
|
| 172 |
+
from tqdm import tqdm
|
| 173 |
+
|
| 174 |
+
# Convert to list of records
|
| 175 |
+
metadata_records = metadata.to_dict("records")
|
| 176 |
+
|
| 177 |
+
# Processing objects
|
| 178 |
+
records = []
|
| 179 |
+
max_workers = max_workers or os.cpu_count()
|
| 180 |
+
|
| 181 |
+
try:
|
| 182 |
+
with (
|
| 183 |
+
ThreadPoolExecutor(max_workers=max_workers) as executor,
|
| 184 |
+
tqdm(total=len(metadata_records), desc=desc) as pbar,
|
| 185 |
+
):
|
| 186 |
+
|
| 187 |
+
def worker(metadatum):
|
| 188 |
+
try:
|
| 189 |
+
local_path = metadatum["local_path"]
|
| 190 |
+
sha256 = metadatum["sha256"]
|
| 191 |
+
record = func(local_path, sha256)
|
| 192 |
+
if record is not None:
|
| 193 |
+
records.append(record)
|
| 194 |
+
pbar.update()
|
| 195 |
+
except Exception as e:
|
| 196 |
+
print(f"Error processing object {sha256}: {e}")
|
| 197 |
+
pbar.update()
|
| 198 |
+
|
| 199 |
+
executor.map(worker, metadata_records)
|
| 200 |
+
executor.shutdown(wait=True)
|
| 201 |
+
except Exception as e:
|
| 202 |
+
print(f"Error happened during processing: {e}")
|
| 203 |
+
|
| 204 |
+
return pd.DataFrame.from_records(records)
|
deps/vomp/dataset_toolkits/abo/build_metadata.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
|
| 16 |
+
import os
|
| 17 |
+
import sys
|
| 18 |
+
import argparse
|
| 19 |
+
import pandas as pd
|
| 20 |
+
from easydict import EasyDict as edict
|
| 21 |
+
|
| 22 |
+
# Add current directory to path to import dataset modules
|
| 23 |
+
sys.path.insert(0, os.path.dirname(__file__))
|
| 24 |
+
|
| 25 |
+
# Import the local ABO500 module directly
|
| 26 |
+
import ABO500 as dataset_utils
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def main():
|
| 30 |
+
parser = argparse.ArgumentParser(description="Build metadata for ABO 500 dataset")
|
| 31 |
+
parser.add_argument(
|
| 32 |
+
"--output_dir",
|
| 33 |
+
type=str,
|
| 34 |
+
required=True,
|
| 35 |
+
help="Directory to save the metadata and processed files",
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
# Add dataset-specific arguments
|
| 39 |
+
dataset_utils.add_args(parser)
|
| 40 |
+
|
| 41 |
+
args = parser.parse_args()
|
| 42 |
+
opt = edict(vars(args))
|
| 43 |
+
|
| 44 |
+
# Create output directory
|
| 45 |
+
os.makedirs(opt.output_dir, exist_ok=True)
|
| 46 |
+
|
| 47 |
+
# Get metadata
|
| 48 |
+
print("Building metadata for ABO 500 dataset...")
|
| 49 |
+
metadata = dataset_utils.get_metadata(**opt)
|
| 50 |
+
|
| 51 |
+
# Add default columns for tracking processing status
|
| 52 |
+
metadata["rendered"] = False
|
| 53 |
+
metadata["voxelized"] = False
|
| 54 |
+
metadata["feature_dinov2_vitl14_reg"] = False
|
| 55 |
+
|
| 56 |
+
# Check for existing processed files and update flags
|
| 57 |
+
for idx, row in metadata.iterrows():
|
| 58 |
+
sha256 = row["sha256"]
|
| 59 |
+
|
| 60 |
+
# Check if voxel file exists
|
| 61 |
+
voxel_path = os.path.join(opt.output_dir, "voxels", f"{sha256}.ply")
|
| 62 |
+
if os.path.exists(voxel_path):
|
| 63 |
+
metadata.at[idx, "voxelized"] = True
|
| 64 |
+
|
| 65 |
+
# Check if render file exists (transforms.json)
|
| 66 |
+
render_path = os.path.join(opt.output_dir, "renders", sha256, "transforms.json")
|
| 67 |
+
if os.path.exists(render_path):
|
| 68 |
+
metadata.at[idx, "rendered"] = True
|
| 69 |
+
|
| 70 |
+
# Check if feature file exists
|
| 71 |
+
feature_path = os.path.join(
|
| 72 |
+
opt.output_dir, "features", "dinov2_vitl14_reg", f"{sha256}.npz"
|
| 73 |
+
)
|
| 74 |
+
if os.path.exists(feature_path):
|
| 75 |
+
metadata.at[idx, "feature_dinov2_vitl14_reg"] = True
|
| 76 |
+
|
| 77 |
+
# Save metadata
|
| 78 |
+
metadata_path = os.path.join(opt.output_dir, "metadata.csv")
|
| 79 |
+
metadata.to_csv(metadata_path, index=False)
|
| 80 |
+
|
| 81 |
+
print(f"Metadata saved to {metadata_path}")
|
| 82 |
+
print(f"Total objects: {len(metadata)}")
|
| 83 |
+
print(f"Objects by split:")
|
| 84 |
+
if "split" in metadata.columns:
|
| 85 |
+
print(metadata["split"].value_counts())
|
| 86 |
+
|
| 87 |
+
# Also save a summary file
|
| 88 |
+
summary = {
|
| 89 |
+
"total_objects": len(metadata),
|
| 90 |
+
"dataset": "ABO500",
|
| 91 |
+
"splits": (
|
| 92 |
+
metadata["split"].value_counts().to_dict()
|
| 93 |
+
if "split" in metadata.columns
|
| 94 |
+
else {}
|
| 95 |
+
),
|
| 96 |
+
"output_dir": opt.output_dir,
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
import json
|
| 100 |
+
|
| 101 |
+
with open(os.path.join(opt.output_dir, "dataset_summary.json"), "w") as f:
|
| 102 |
+
json.dump(summary, f, indent=2)
|
| 103 |
+
|
| 104 |
+
print("Dataset summary saved to dataset_summary.json")
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
if __name__ == "__main__":
|
| 108 |
+
main()
|
deps/vomp/dataset_toolkits/abo/extract_feature.py
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
|
| 16 |
+
import os
|
| 17 |
+
import copy
|
| 18 |
+
import sys
|
| 19 |
+
import json
|
| 20 |
+
import argparse
|
| 21 |
+
import torch
|
| 22 |
+
import torch.nn.functional as F
|
| 23 |
+
import numpy as np
|
| 24 |
+
import pandas as pd
|
| 25 |
+
import utils3d
|
| 26 |
+
from tqdm import tqdm
|
| 27 |
+
from easydict import EasyDict as edict
|
| 28 |
+
from torchvision import transforms
|
| 29 |
+
from PIL import Image
|
| 30 |
+
|
| 31 |
+
# Add current directory to path to import dataset modules
|
| 32 |
+
sys.path.insert(0, os.path.dirname(__file__))
|
| 33 |
+
import ABO500 as dataset_utils
|
| 34 |
+
|
| 35 |
+
torch.set_grad_enabled(False)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def get_data(frames, sha256, output_dir):
|
| 39 |
+
"""
|
| 40 |
+
Load and preprocess rendered images for feature extraction.
|
| 41 |
+
|
| 42 |
+
Args:
|
| 43 |
+
frames (list): List of frame data from transforms.json
|
| 44 |
+
sha256 (str): SHA256 hash of the object
|
| 45 |
+
output_dir (str): Output directory containing renders
|
| 46 |
+
|
| 47 |
+
Returns:
|
| 48 |
+
list: List of processed image data
|
| 49 |
+
"""
|
| 50 |
+
valid_data = []
|
| 51 |
+
|
| 52 |
+
for view in frames:
|
| 53 |
+
image_path = os.path.join(output_dir, "renders", sha256, view["file_path"])
|
| 54 |
+
try:
|
| 55 |
+
# Check if file exists before trying to open it
|
| 56 |
+
if not os.path.exists(image_path):
|
| 57 |
+
print(f"Warning: Image file {image_path} not found, skipping")
|
| 58 |
+
continue
|
| 59 |
+
|
| 60 |
+
image = Image.open(image_path)
|
| 61 |
+
except Exception as e:
|
| 62 |
+
print(f"Error loading image {image_path}: {e}")
|
| 63 |
+
continue
|
| 64 |
+
|
| 65 |
+
try:
|
| 66 |
+
# Resize and normalize image
|
| 67 |
+
image = image.resize((518, 518), Image.Resampling.LANCZOS)
|
| 68 |
+
image = np.array(image).astype(np.float32) / 255
|
| 69 |
+
image = image[:, :, :3] * image[:, :, 3:] # Apply alpha channel
|
| 70 |
+
image = torch.from_numpy(image).permute(2, 0, 1).float()
|
| 71 |
+
|
| 72 |
+
# Extract camera parameters
|
| 73 |
+
c2w = torch.tensor(view["transform_matrix"])
|
| 74 |
+
c2w[:3, 1:3] *= -1
|
| 75 |
+
extrinsics = torch.inverse(c2w)
|
| 76 |
+
fov = view["camera_angle_x"]
|
| 77 |
+
intrinsics = utils3d.torch.intrinsics_from_fov_xy(
|
| 78 |
+
torch.tensor(fov), torch.tensor(fov)
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
valid_data.append(
|
| 82 |
+
{"image": image, "extrinsics": extrinsics, "intrinsics": intrinsics}
|
| 83 |
+
)
|
| 84 |
+
except Exception as e:
|
| 85 |
+
print(f"Error processing image {image_path}: {e}")
|
| 86 |
+
continue
|
| 87 |
+
|
| 88 |
+
if len(valid_data) == 0:
|
| 89 |
+
print(f"Warning: No valid images found for {sha256}")
|
| 90 |
+
else:
|
| 91 |
+
print(f"Loaded {len(valid_data)}/{len(frames)} valid images for {sha256}")
|
| 92 |
+
|
| 93 |
+
return valid_data
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def extract_features(
|
| 97 |
+
file_path,
|
| 98 |
+
sha256,
|
| 99 |
+
output_dir=None,
|
| 100 |
+
model=None,
|
| 101 |
+
transform=None,
|
| 102 |
+
batch_size=16,
|
| 103 |
+
feature_name="dinov2_vitl14_reg",
|
| 104 |
+
):
|
| 105 |
+
"""
|
| 106 |
+
Extract features for a single object.
|
| 107 |
+
|
| 108 |
+
Args:
|
| 109 |
+
file_path (str): Path to the GLB file (not used directly, but needed for interface)
|
| 110 |
+
sha256 (str): SHA256 hash of the object
|
| 111 |
+
output_dir (str): Output directory
|
| 112 |
+
model: Pre-loaded feature extraction model
|
| 113 |
+
transform: Image transformation pipeline
|
| 114 |
+
batch_size (int): Batch size for processing
|
| 115 |
+
feature_name (str): Name of the feature extraction method
|
| 116 |
+
|
| 117 |
+
Returns:
|
| 118 |
+
dict: Result dictionary with processing info
|
| 119 |
+
"""
|
| 120 |
+
try:
|
| 121 |
+
# Load transforms.json
|
| 122 |
+
transforms_path = os.path.join(output_dir, "renders", sha256, "transforms.json")
|
| 123 |
+
if not os.path.exists(transforms_path):
|
| 124 |
+
print(f"transforms.json not found for {sha256}")
|
| 125 |
+
return {"sha256": sha256, f"feature_{feature_name}": False}
|
| 126 |
+
|
| 127 |
+
with open(transforms_path, "r") as f:
|
| 128 |
+
metadata_json = json.load(f)
|
| 129 |
+
|
| 130 |
+
frames = metadata_json["frames"]
|
| 131 |
+
data = get_data(frames, sha256, output_dir)
|
| 132 |
+
|
| 133 |
+
if len(data) == 0:
|
| 134 |
+
print(f"Skipping {sha256}: no valid image data")
|
| 135 |
+
return {"sha256": sha256, f"feature_{feature_name}": False}
|
| 136 |
+
|
| 137 |
+
# Apply transform to images
|
| 138 |
+
for datum in data:
|
| 139 |
+
datum["image"] = transform(datum["image"])
|
| 140 |
+
|
| 141 |
+
# Load voxel positions
|
| 142 |
+
voxel_path = os.path.join(output_dir, "voxels", f"{sha256}.ply")
|
| 143 |
+
if not os.path.exists(voxel_path):
|
| 144 |
+
print(f"Voxel file not found for {sha256}")
|
| 145 |
+
return {"sha256": sha256, f"feature_{feature_name}": False}
|
| 146 |
+
|
| 147 |
+
positions = utils3d.io.read_ply(voxel_path)[0]
|
| 148 |
+
positions = torch.from_numpy(positions).float().cuda()
|
| 149 |
+
indices = ((positions + 0.5) * 64).long()
|
| 150 |
+
# Clamp indices to valid range [0, 63] to handle floating point precision issues
|
| 151 |
+
indices = torch.clamp(indices, 0, 63)
|
| 152 |
+
|
| 153 |
+
n_views = len(data)
|
| 154 |
+
n_patch = 518 // 14
|
| 155 |
+
pack = {
|
| 156 |
+
"indices": indices.cpu().numpy().astype(np.uint8),
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
patchtokens_lst = []
|
| 160 |
+
uv_lst = []
|
| 161 |
+
|
| 162 |
+
# Process in batches
|
| 163 |
+
for i in range(0, n_views, batch_size):
|
| 164 |
+
batch_data = data[i : i + batch_size]
|
| 165 |
+
bs = len(batch_data)
|
| 166 |
+
batch_images = torch.stack([d["image"] for d in batch_data]).cuda()
|
| 167 |
+
batch_extrinsics = torch.stack([d["extrinsics"] for d in batch_data]).cuda()
|
| 168 |
+
batch_intrinsics = torch.stack([d["intrinsics"] for d in batch_data]).cuda()
|
| 169 |
+
|
| 170 |
+
# Extract features using the model
|
| 171 |
+
features = model(batch_images, is_training=True)
|
| 172 |
+
|
| 173 |
+
# Project 3D positions to 2D
|
| 174 |
+
uv = (
|
| 175 |
+
utils3d.torch.project_cv(positions, batch_extrinsics, batch_intrinsics)[
|
| 176 |
+
0
|
| 177 |
+
]
|
| 178 |
+
* 2
|
| 179 |
+
- 1
|
| 180 |
+
)
|
| 181 |
+
|
| 182 |
+
# Extract patch tokens
|
| 183 |
+
patchtokens = (
|
| 184 |
+
features["x_prenorm"][:, model.num_register_tokens + 1 :]
|
| 185 |
+
.permute(0, 2, 1)
|
| 186 |
+
.reshape(bs, 1024, n_patch, n_patch)
|
| 187 |
+
)
|
| 188 |
+
patchtokens_lst.append(patchtokens)
|
| 189 |
+
uv_lst.append(uv)
|
| 190 |
+
|
| 191 |
+
patchtokens = torch.cat(patchtokens_lst, dim=0)
|
| 192 |
+
uv = torch.cat(uv_lst, dim=0)
|
| 193 |
+
|
| 194 |
+
# Sample features at voxel positions
|
| 195 |
+
pack["patchtokens"] = (
|
| 196 |
+
F.grid_sample(
|
| 197 |
+
patchtokens,
|
| 198 |
+
uv.unsqueeze(1),
|
| 199 |
+
mode="bilinear",
|
| 200 |
+
align_corners=False,
|
| 201 |
+
)
|
| 202 |
+
.squeeze(2)
|
| 203 |
+
.permute(0, 2, 1)
|
| 204 |
+
.cpu()
|
| 205 |
+
.numpy()
|
| 206 |
+
)
|
| 207 |
+
pack["patchtokens"] = np.mean(pack["patchtokens"], axis=0).astype(np.float16)
|
| 208 |
+
|
| 209 |
+
# Save features
|
| 210 |
+
save_path = os.path.join(output_dir, "features", feature_name, f"{sha256}.npz")
|
| 211 |
+
os.makedirs(os.path.dirname(save_path), exist_ok=True)
|
| 212 |
+
np.savez_compressed(save_path, **pack)
|
| 213 |
+
|
| 214 |
+
return {"sha256": sha256, f"feature_{feature_name}": True}
|
| 215 |
+
|
| 216 |
+
except Exception as e:
|
| 217 |
+
print(f"Error processing {sha256}: {e}")
|
| 218 |
+
import traceback
|
| 219 |
+
|
| 220 |
+
traceback.print_exc()
|
| 221 |
+
return {"sha256": sha256, f"feature_{feature_name}": False}
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
if __name__ == "__main__":
|
| 225 |
+
parser = argparse.ArgumentParser(description="Extract features for ABO 500 dataset")
|
| 226 |
+
parser.add_argument(
|
| 227 |
+
"--output_dir",
|
| 228 |
+
type=str,
|
| 229 |
+
required=True,
|
| 230 |
+
help="Directory containing metadata and where to save features",
|
| 231 |
+
)
|
| 232 |
+
parser.add_argument(
|
| 233 |
+
"--model",
|
| 234 |
+
type=str,
|
| 235 |
+
default="dinov2_vitl14_reg",
|
| 236 |
+
help="Feature extraction model",
|
| 237 |
+
)
|
| 238 |
+
parser.add_argument(
|
| 239 |
+
"--instances",
|
| 240 |
+
type=str,
|
| 241 |
+
default=None,
|
| 242 |
+
help="Specific instances to process (comma-separated or file path)",
|
| 243 |
+
)
|
| 244 |
+
parser.add_argument("--batch_size", type=int, default=16)
|
| 245 |
+
parser.add_argument("--rank", type=int, default=0)
|
| 246 |
+
parser.add_argument("--world_size", type=int, default=1)
|
| 247 |
+
parser.add_argument(
|
| 248 |
+
"--force",
|
| 249 |
+
action="store_true",
|
| 250 |
+
help="Force feature extraction even if already processed",
|
| 251 |
+
)
|
| 252 |
+
parser.add_argument(
|
| 253 |
+
"--limit", type=int, default=None, help="Process only the first N objects"
|
| 254 |
+
)
|
| 255 |
+
|
| 256 |
+
args = parser.parse_args()
|
| 257 |
+
opt = edict(vars(args))
|
| 258 |
+
|
| 259 |
+
feature_name = opt.model
|
| 260 |
+
|
| 261 |
+
# Create features directory
|
| 262 |
+
os.makedirs(os.path.join(opt.output_dir, "features", feature_name), exist_ok=True)
|
| 263 |
+
|
| 264 |
+
# Load model
|
| 265 |
+
print(f"Loading model: {opt.model}")
|
| 266 |
+
dinov2_model = torch.hub.load("facebookresearch/dinov2", opt.model)
|
| 267 |
+
dinov2_model.eval().cuda()
|
| 268 |
+
transform = transforms.Compose(
|
| 269 |
+
[
|
| 270 |
+
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
|
| 271 |
+
]
|
| 272 |
+
)
|
| 273 |
+
|
| 274 |
+
# Load metadata
|
| 275 |
+
metadata_path = os.path.join(opt.output_dir, "metadata.csv")
|
| 276 |
+
if not os.path.exists(metadata_path):
|
| 277 |
+
raise ValueError(f"metadata.csv not found at {metadata_path}")
|
| 278 |
+
|
| 279 |
+
metadata = pd.read_csv(metadata_path)
|
| 280 |
+
|
| 281 |
+
# Filter instances if specified
|
| 282 |
+
if opt.instances is not None:
|
| 283 |
+
if os.path.exists(opt.instances):
|
| 284 |
+
with open(opt.instances, "r") as f:
|
| 285 |
+
instances = f.read().splitlines()
|
| 286 |
+
else:
|
| 287 |
+
instances = opt.instances.split(",")
|
| 288 |
+
metadata = metadata[metadata["sha256"].isin(instances)]
|
| 289 |
+
else:
|
| 290 |
+
# Only process objects that have been rendered and voxelized
|
| 291 |
+
if "rendered" in metadata.columns:
|
| 292 |
+
metadata = metadata[metadata["rendered"] == True]
|
| 293 |
+
if "voxelized" in metadata.columns:
|
| 294 |
+
metadata = metadata[metadata["voxelized"] == True]
|
| 295 |
+
|
| 296 |
+
# Only process objects that haven't had features extracted yet
|
| 297 |
+
if f"feature_{feature_name}" in metadata.columns and not opt.force:
|
| 298 |
+
metadata = metadata[metadata[f"feature_{feature_name}"] == False]
|
| 299 |
+
|
| 300 |
+
# Apply distributed processing
|
| 301 |
+
start = len(metadata) * opt.rank // opt.world_size
|
| 302 |
+
end = len(metadata) * (opt.rank + 1) // opt.world_size
|
| 303 |
+
metadata = metadata[start:end]
|
| 304 |
+
|
| 305 |
+
# Apply limit if specified
|
| 306 |
+
if opt.limit is not None:
|
| 307 |
+
metadata = metadata.head(opt.limit)
|
| 308 |
+
|
| 309 |
+
print(f"Processing {len(metadata)} objects...")
|
| 310 |
+
|
| 311 |
+
# Track already processed objects
|
| 312 |
+
records = []
|
| 313 |
+
sha256s = list(metadata["sha256"].values)
|
| 314 |
+
|
| 315 |
+
# Filter out objects that are already processed
|
| 316 |
+
if not opt.force:
|
| 317 |
+
for sha256 in copy.copy(sha256s):
|
| 318 |
+
feature_path = os.path.join(
|
| 319 |
+
opt.output_dir, "features", feature_name, f"{sha256}.npz"
|
| 320 |
+
)
|
| 321 |
+
if os.path.exists(feature_path):
|
| 322 |
+
records.append({"sha256": sha256, f"feature_{feature_name}": True})
|
| 323 |
+
sha256s.remove(sha256)
|
| 324 |
+
|
| 325 |
+
# Filter out objects that don't have required prerequisite files
|
| 326 |
+
initial_count = len(sha256s)
|
| 327 |
+
filtered_sha256s = []
|
| 328 |
+
|
| 329 |
+
for sha256 in sha256s:
|
| 330 |
+
# Check for voxel file
|
| 331 |
+
voxel_path = os.path.join(opt.output_dir, "voxels", f"{sha256}.ply")
|
| 332 |
+
if not os.path.exists(voxel_path):
|
| 333 |
+
print(f"Skipping {sha256}: voxel file not found")
|
| 334 |
+
continue
|
| 335 |
+
|
| 336 |
+
# Check for transforms.json
|
| 337 |
+
transforms_path = os.path.join(
|
| 338 |
+
opt.output_dir, "renders", sha256, "transforms.json"
|
| 339 |
+
)
|
| 340 |
+
if not os.path.exists(transforms_path):
|
| 341 |
+
print(f"Skipping {sha256}: transforms.json not found")
|
| 342 |
+
continue
|
| 343 |
+
|
| 344 |
+
filtered_sha256s.append(sha256)
|
| 345 |
+
|
| 346 |
+
sha256s = filtered_sha256s
|
| 347 |
+
print(
|
| 348 |
+
f"Filtered from {initial_count} to {len(sha256s)} objects with required files"
|
| 349 |
+
)
|
| 350 |
+
|
| 351 |
+
# Extract features for remaining objects
|
| 352 |
+
if len(sha256s) > 0:
|
| 353 |
+
for sha256 in tqdm(sha256s, desc="Extracting features"):
|
| 354 |
+
# Get the file path (not used directly but needed for interface consistency)
|
| 355 |
+
file_path = metadata[metadata["sha256"] == sha256]["local_path"].iloc[0]
|
| 356 |
+
|
| 357 |
+
result = extract_features(
|
| 358 |
+
file_path=file_path,
|
| 359 |
+
sha256=sha256,
|
| 360 |
+
output_dir=opt.output_dir,
|
| 361 |
+
model=dinov2_model,
|
| 362 |
+
transform=transform,
|
| 363 |
+
batch_size=opt.batch_size,
|
| 364 |
+
feature_name=feature_name,
|
| 365 |
+
)
|
| 366 |
+
|
| 367 |
+
if result is not None:
|
| 368 |
+
records.append(result)
|
| 369 |
+
|
| 370 |
+
# Save results
|
| 371 |
+
if len(records) > 0:
|
| 372 |
+
results_df = pd.DataFrame.from_records(records)
|
| 373 |
+
results_df.to_csv(
|
| 374 |
+
os.path.join(opt.output_dir, f"feature_{feature_name}_{opt.rank}.csv"),
|
| 375 |
+
index=False,
|
| 376 |
+
)
|
| 377 |
+
print(
|
| 378 |
+
f"Feature extraction complete. Results saved to feature_{feature_name}_{opt.rank}.csv"
|
| 379 |
+
)
|
| 380 |
+
else:
|
| 381 |
+
print("No objects processed.")
|
deps/vomp/dataset_toolkits/abo/render.py
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
|
| 16 |
+
import os
|
| 17 |
+
import json
|
| 18 |
+
import copy
|
| 19 |
+
import sys
|
| 20 |
+
import argparse
|
| 21 |
+
import pandas as pd
|
| 22 |
+
from easydict import EasyDict as edict
|
| 23 |
+
from functools import partial
|
| 24 |
+
from subprocess import DEVNULL, call
|
| 25 |
+
import numpy as np
|
| 26 |
+
|
| 27 |
+
# Add current directory to path to import dataset modules
|
| 28 |
+
sys.path.insert(0, os.path.dirname(__file__))
|
| 29 |
+
import ABO500 as dataset_utils
|
| 30 |
+
|
| 31 |
+
# Import from the existing render.py utils
|
| 32 |
+
sys.path.append(os.path.dirname(os.path.dirname(__file__)))
|
| 33 |
+
from utils import sphere_hammersley_sequence
|
| 34 |
+
|
| 35 |
+
BLENDER_LINK = (
|
| 36 |
+
"https://download.blender.org/release/Blender3.0/blender-3.0.1-linux-x64.tar.xz"
|
| 37 |
+
)
|
| 38 |
+
BLENDER_INSTALLATION_PATH = "/tmp"
|
| 39 |
+
BLENDER_PATH = f"{BLENDER_INSTALLATION_PATH}/blender-3.0.1-linux-x64/blender"
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def _install_blender():
|
| 43 |
+
"""Install Blender if not already installed."""
|
| 44 |
+
if not os.path.exists(BLENDER_PATH):
|
| 45 |
+
os.system("sudo apt-get update")
|
| 46 |
+
os.system(
|
| 47 |
+
"sudo apt-get install -y libxrender1 libxi6 libxkbcommon-x11-0 libsm6"
|
| 48 |
+
)
|
| 49 |
+
os.system(f"wget {BLENDER_LINK} -P {BLENDER_INSTALLATION_PATH}")
|
| 50 |
+
os.system(
|
| 51 |
+
f"tar -xvf {BLENDER_INSTALLATION_PATH}/blender-3.0.1-linux-x64.tar.xz -C {BLENDER_INSTALLATION_PATH}"
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def _render_glb(file_path, sha256, output_dir, num_views):
|
| 56 |
+
"""
|
| 57 |
+
Render a GLB file from multiple viewpoints.
|
| 58 |
+
|
| 59 |
+
Args:
|
| 60 |
+
file_path (str): Path to the GLB file
|
| 61 |
+
sha256 (str): SHA256 hash of the file
|
| 62 |
+
output_dir (str): Directory to save renders
|
| 63 |
+
num_views (int): Number of viewpoints to render
|
| 64 |
+
|
| 65 |
+
Returns:
|
| 66 |
+
dict: Result dictionary with rendering info
|
| 67 |
+
"""
|
| 68 |
+
# Convert to absolute path to avoid issues with relative paths
|
| 69 |
+
output_dir = os.path.abspath(output_dir)
|
| 70 |
+
output_folder = os.path.join(output_dir, "renders", sha256)
|
| 71 |
+
|
| 72 |
+
# Build camera parameters {yaw, pitch, radius, fov}
|
| 73 |
+
yaws = []
|
| 74 |
+
pitchs = []
|
| 75 |
+
offset = (np.random.rand(), np.random.rand())
|
| 76 |
+
for i in range(num_views):
|
| 77 |
+
y, p = sphere_hammersley_sequence(i, num_views, offset)
|
| 78 |
+
yaws.append(y)
|
| 79 |
+
pitchs.append(p)
|
| 80 |
+
|
| 81 |
+
radius = [2] * num_views
|
| 82 |
+
fov = [40 / 180 * np.pi] * num_views
|
| 83 |
+
views = [
|
| 84 |
+
{"yaw": y, "pitch": p, "radius": r, "fov": f}
|
| 85 |
+
for y, p, r, f in zip(yaws, pitchs, radius, fov)
|
| 86 |
+
]
|
| 87 |
+
|
| 88 |
+
# Construct Blender command
|
| 89 |
+
blender_script_path = os.path.join(
|
| 90 |
+
os.path.dirname(os.path.dirname(__file__)), "blender_script", "render.py"
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
args = [
|
| 94 |
+
BLENDER_PATH,
|
| 95 |
+
"-b",
|
| 96 |
+
"-P",
|
| 97 |
+
blender_script_path,
|
| 98 |
+
"--",
|
| 99 |
+
"--views",
|
| 100 |
+
json.dumps(views),
|
| 101 |
+
"--object",
|
| 102 |
+
os.path.expanduser(file_path),
|
| 103 |
+
"--resolution",
|
| 104 |
+
"512",
|
| 105 |
+
"--output_folder",
|
| 106 |
+
output_folder,
|
| 107 |
+
"--engine",
|
| 108 |
+
"CYCLES",
|
| 109 |
+
"--save_mesh",
|
| 110 |
+
]
|
| 111 |
+
|
| 112 |
+
try:
|
| 113 |
+
# Execute Blender rendering
|
| 114 |
+
result = call(args, stdout=DEVNULL, stderr=DEVNULL)
|
| 115 |
+
|
| 116 |
+
# Check if rendering was successful
|
| 117 |
+
if result == 0 and os.path.exists(
|
| 118 |
+
os.path.join(output_folder, "transforms.json")
|
| 119 |
+
):
|
| 120 |
+
return {"sha256": sha256, "rendered": True}
|
| 121 |
+
else:
|
| 122 |
+
print(f"Rendering failed for {sha256}")
|
| 123 |
+
return {"sha256": sha256, "rendered": False}
|
| 124 |
+
|
| 125 |
+
except Exception as e:
|
| 126 |
+
print(f"Error rendering {file_path}: {e}")
|
| 127 |
+
return {"sha256": sha256, "rendered": False}
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def _render(file_path, sha256, output_dir=None, num_views=150):
|
| 131 |
+
"""Wrapper function for rendering."""
|
| 132 |
+
return _render_glb(file_path, sha256, output_dir, num_views)
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
if __name__ == "__main__":
|
| 136 |
+
parser = argparse.ArgumentParser(description="Render ABO 500 dataset")
|
| 137 |
+
parser.add_argument(
|
| 138 |
+
"--output_dir",
|
| 139 |
+
type=str,
|
| 140 |
+
required=True,
|
| 141 |
+
help="Directory containing metadata and where to save renders",
|
| 142 |
+
)
|
| 143 |
+
parser.add_argument(
|
| 144 |
+
"--instances",
|
| 145 |
+
type=str,
|
| 146 |
+
default=None,
|
| 147 |
+
help="Specific instances to process (comma-separated or file path)",
|
| 148 |
+
)
|
| 149 |
+
parser.add_argument(
|
| 150 |
+
"--num_views", type=int, default=150, help="Number of views to render"
|
| 151 |
+
)
|
| 152 |
+
parser.add_argument(
|
| 153 |
+
"--force", action="store_true", help="Force rendering even if already processed"
|
| 154 |
+
)
|
| 155 |
+
parser.add_argument("--rank", type=int, default=0)
|
| 156 |
+
parser.add_argument("--world_size", type=int, default=1)
|
| 157 |
+
parser.add_argument("--max_workers", type=int, default=8)
|
| 158 |
+
parser.add_argument(
|
| 159 |
+
"--limit", type=int, default=None, help="Process only the first N objects"
|
| 160 |
+
)
|
| 161 |
+
|
| 162 |
+
args = parser.parse_args()
|
| 163 |
+
opt = edict(vars(args))
|
| 164 |
+
|
| 165 |
+
# Create renders directory
|
| 166 |
+
os.makedirs(os.path.join(opt.output_dir, "renders"), exist_ok=True)
|
| 167 |
+
|
| 168 |
+
# Install Blender if needed
|
| 169 |
+
print("Checking Blender installation...", flush=True)
|
| 170 |
+
_install_blender()
|
| 171 |
+
|
| 172 |
+
# Load metadata
|
| 173 |
+
metadata_path = os.path.join(opt.output_dir, "metadata.csv")
|
| 174 |
+
if not os.path.exists(metadata_path):
|
| 175 |
+
raise ValueError(f"metadata.csv not found at {metadata_path}")
|
| 176 |
+
|
| 177 |
+
metadata = pd.read_csv(metadata_path)
|
| 178 |
+
|
| 179 |
+
# Filter instances if specified
|
| 180 |
+
if opt.instances is not None:
|
| 181 |
+
if os.path.exists(opt.instances):
|
| 182 |
+
with open(opt.instances, "r") as f:
|
| 183 |
+
instances = f.read().splitlines()
|
| 184 |
+
else:
|
| 185 |
+
instances = opt.instances.split(",")
|
| 186 |
+
metadata = metadata[metadata["sha256"].isin(instances)]
|
| 187 |
+
else:
|
| 188 |
+
# Only process objects that have valid local paths
|
| 189 |
+
metadata = metadata[metadata["local_path"].notna()]
|
| 190 |
+
|
| 191 |
+
# Only process objects that haven't been rendered yet
|
| 192 |
+
if "rendered" in metadata.columns and not opt.force:
|
| 193 |
+
metadata = metadata[metadata["rendered"] == False]
|
| 194 |
+
|
| 195 |
+
# Apply distributed processing
|
| 196 |
+
start = len(metadata) * opt.rank // opt.world_size
|
| 197 |
+
end = len(metadata) * (opt.rank + 1) // opt.world_size
|
| 198 |
+
metadata = metadata[start:end]
|
| 199 |
+
|
| 200 |
+
# Apply limit if specified
|
| 201 |
+
if opt.limit is not None:
|
| 202 |
+
metadata = metadata.head(opt.limit)
|
| 203 |
+
|
| 204 |
+
print(f"Processing {len(metadata)} objects...")
|
| 205 |
+
|
| 206 |
+
# Track already processed objects
|
| 207 |
+
records = []
|
| 208 |
+
|
| 209 |
+
# Filter out objects that are already processed
|
| 210 |
+
if not opt.force:
|
| 211 |
+
for sha256 in copy.copy(metadata["sha256"].values):
|
| 212 |
+
transforms_path = os.path.join(
|
| 213 |
+
opt.output_dir, "renders", sha256, "transforms.json"
|
| 214 |
+
)
|
| 215 |
+
if os.path.exists(transforms_path):
|
| 216 |
+
records.append({"sha256": sha256, "rendered": True})
|
| 217 |
+
metadata = metadata[metadata["sha256"] != sha256]
|
| 218 |
+
|
| 219 |
+
# Process remaining objects
|
| 220 |
+
if len(metadata) > 0:
|
| 221 |
+
func = partial(_render, output_dir=opt.output_dir, num_views=opt.num_views)
|
| 222 |
+
rendered = dataset_utils.foreach_instance(
|
| 223 |
+
metadata,
|
| 224 |
+
opt.output_dir,
|
| 225 |
+
func,
|
| 226 |
+
max_workers=opt.max_workers,
|
| 227 |
+
desc="Rendering objects",
|
| 228 |
+
)
|
| 229 |
+
|
| 230 |
+
# Combine results
|
| 231 |
+
if len(records) > 0:
|
| 232 |
+
rendered = pd.concat([rendered, pd.DataFrame.from_records(records)])
|
| 233 |
+
|
| 234 |
+
# Save results
|
| 235 |
+
rendered.to_csv(
|
| 236 |
+
os.path.join(opt.output_dir, f"rendered_{opt.rank}.csv"), index=False
|
| 237 |
+
)
|
| 238 |
+
|
| 239 |
+
print(f"Rendering complete. Results saved to rendered_{opt.rank}.csv")
|
| 240 |
+
else:
|
| 241 |
+
print("No objects to process.")
|
deps/vomp/dataset_toolkits/abo/voxelize.py
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
|
| 16 |
+
import os
|
| 17 |
+
import copy
|
| 18 |
+
import sys
|
| 19 |
+
import argparse
|
| 20 |
+
import pandas as pd
|
| 21 |
+
from easydict import EasyDict as edict
|
| 22 |
+
from functools import partial
|
| 23 |
+
import numpy as np
|
| 24 |
+
import open3d as o3d
|
| 25 |
+
import utils3d
|
| 26 |
+
import trimesh
|
| 27 |
+
import tempfile
|
| 28 |
+
import shutil
|
| 29 |
+
|
| 30 |
+
# Add current directory to path to import dataset modules
|
| 31 |
+
sys.path.insert(0, os.path.dirname(__file__))
|
| 32 |
+
|
| 33 |
+
import ABO500 as dataset_utils
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def voxelize_mesh(
|
| 37 |
+
vertices, faces, voxel_size=1 / 64, center_scale=None, max_voxels=None
|
| 38 |
+
):
|
| 39 |
+
"""
|
| 40 |
+
Voxelize a mesh represented by vertices and faces using volumetric voxelization.
|
| 41 |
+
|
| 42 |
+
Args:
|
| 43 |
+
vertices (numpy.ndarray): Array of vertices
|
| 44 |
+
faces (numpy.ndarray): Array of faces
|
| 45 |
+
voxel_size (float): Size of each voxel
|
| 46 |
+
center_scale (tuple): Optional center and scale for normalization
|
| 47 |
+
max_voxels (int): Maximum number of voxels to return (will subsample if exceeded)
|
| 48 |
+
|
| 49 |
+
Returns:
|
| 50 |
+
tuple: (voxel_centers, voxel_grid) - center coordinates of voxels and Trimesh voxel grid
|
| 51 |
+
"""
|
| 52 |
+
# Create a Trimesh mesh
|
| 53 |
+
mesh = trimesh.Trimesh(vertices=vertices, faces=faces)
|
| 54 |
+
|
| 55 |
+
# Normalize the mesh to [-0.5, 0.5] range
|
| 56 |
+
vertices = mesh.vertices.copy()
|
| 57 |
+
|
| 58 |
+
if center_scale is None:
|
| 59 |
+
vertices_min = np.min(vertices, axis=0)
|
| 60 |
+
vertices_max = np.max(vertices, axis=0)
|
| 61 |
+
center = (vertices_min + vertices_max) / 2
|
| 62 |
+
scale = np.max(vertices_max - vertices_min)
|
| 63 |
+
else:
|
| 64 |
+
center, scale = center_scale
|
| 65 |
+
|
| 66 |
+
vertices = (vertices - center) / scale
|
| 67 |
+
vertices = np.clip(vertices, -0.5 + 1e-6, 0.5 - 1e-6)
|
| 68 |
+
|
| 69 |
+
# Update mesh with normalized vertices
|
| 70 |
+
mesh.vertices = vertices
|
| 71 |
+
|
| 72 |
+
# Create volumetric voxel grid using Trimesh
|
| 73 |
+
voxel_grid = mesh.voxelized(pitch=voxel_size).fill()
|
| 74 |
+
|
| 75 |
+
# Get voxel centers from the filled voxel grid
|
| 76 |
+
voxel_centers = voxel_grid.points
|
| 77 |
+
|
| 78 |
+
# Subsample if we have too many voxels
|
| 79 |
+
if max_voxels is not None and len(voxel_centers) > max_voxels:
|
| 80 |
+
print(f"Subsampling voxels: {len(voxel_centers):,} -> {max_voxels:,}")
|
| 81 |
+
# Use random sampling to maintain spatial distribution
|
| 82 |
+
np.random.seed(42) # For reproducibility
|
| 83 |
+
indices = np.random.choice(len(voxel_centers), max_voxels, replace=False)
|
| 84 |
+
voxel_centers = voxel_centers[indices]
|
| 85 |
+
|
| 86 |
+
return voxel_centers, voxel_grid
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def load_glb_mesh(glb_path):
|
| 90 |
+
"""
|
| 91 |
+
Load a GLB file and extract mesh data.
|
| 92 |
+
|
| 93 |
+
Args:
|
| 94 |
+
glb_path (str): Path to the GLB file
|
| 95 |
+
|
| 96 |
+
Returns:
|
| 97 |
+
tuple: (vertices, faces) - mesh vertices and faces
|
| 98 |
+
"""
|
| 99 |
+
try:
|
| 100 |
+
# Load the GLB file using trimesh
|
| 101 |
+
mesh = trimesh.load(glb_path)
|
| 102 |
+
|
| 103 |
+
# Handle different mesh types
|
| 104 |
+
if isinstance(mesh, trimesh.Scene):
|
| 105 |
+
# If it's a scene, combine all meshes
|
| 106 |
+
combined_mesh = trimesh.util.concatenate(
|
| 107 |
+
[
|
| 108 |
+
geometry
|
| 109 |
+
for geometry in mesh.geometry.values()
|
| 110 |
+
if isinstance(geometry, trimesh.Trimesh)
|
| 111 |
+
]
|
| 112 |
+
)
|
| 113 |
+
if combined_mesh is None:
|
| 114 |
+
raise ValueError("No valid meshes found in GLB file")
|
| 115 |
+
mesh = combined_mesh
|
| 116 |
+
elif not isinstance(mesh, trimesh.Trimesh):
|
| 117 |
+
raise ValueError("GLB file does not contain a valid mesh")
|
| 118 |
+
|
| 119 |
+
# Ensure the mesh has faces
|
| 120 |
+
if len(mesh.faces) == 0:
|
| 121 |
+
raise ValueError("Mesh has no faces")
|
| 122 |
+
|
| 123 |
+
return mesh.vertices, mesh.faces
|
| 124 |
+
|
| 125 |
+
except Exception as e:
|
| 126 |
+
print(f"Error loading GLB file {glb_path}: {e}")
|
| 127 |
+
return None, None
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def voxelize_glb(glb_path, sha256, output_dir, max_voxels=None):
|
| 131 |
+
"""
|
| 132 |
+
Voxelize a GLB file and save the result.
|
| 133 |
+
|
| 134 |
+
Args:
|
| 135 |
+
glb_path (str): Path to the GLB file
|
| 136 |
+
sha256 (str): SHA256 hash of the file
|
| 137 |
+
output_dir (str): Directory to save the voxelized data
|
| 138 |
+
max_voxels (int): Maximum number of voxels to generate
|
| 139 |
+
|
| 140 |
+
Returns:
|
| 141 |
+
dict: Result dictionary with processing info
|
| 142 |
+
"""
|
| 143 |
+
try:
|
| 144 |
+
# Load the GLB mesh
|
| 145 |
+
vertices, faces = load_glb_mesh(glb_path)
|
| 146 |
+
|
| 147 |
+
if vertices is None or faces is None:
|
| 148 |
+
print(f"Failed to load mesh from {glb_path}")
|
| 149 |
+
return {"sha256": sha256, "voxelized": False, "num_voxels": 0}
|
| 150 |
+
|
| 151 |
+
print(f"Loaded mesh with {len(vertices)} vertices and {len(faces)} faces")
|
| 152 |
+
|
| 153 |
+
# Voxelize the mesh
|
| 154 |
+
voxel_centers, voxel_grid = voxelize_mesh(
|
| 155 |
+
vertices, faces, max_voxels=max_voxels
|
| 156 |
+
)
|
| 157 |
+
|
| 158 |
+
if len(voxel_centers) == 0:
|
| 159 |
+
print(f"No voxels generated for {sha256}")
|
| 160 |
+
return {"sha256": sha256, "voxelized": False, "num_voxels": 0}
|
| 161 |
+
|
| 162 |
+
# Save voxel centers as PLY file
|
| 163 |
+
ply_output_path = os.path.join(output_dir, "voxels", f"{sha256}.ply")
|
| 164 |
+
save_ply(ply_output_path, voxel_centers)
|
| 165 |
+
|
| 166 |
+
print(f"Voxelized {sha256}: {len(voxel_centers)} voxels")
|
| 167 |
+
|
| 168 |
+
return {"sha256": sha256, "voxelized": True, "num_voxels": len(voxel_centers)}
|
| 169 |
+
|
| 170 |
+
except Exception as e:
|
| 171 |
+
print(f"Error voxelizing {glb_path}: {e}")
|
| 172 |
+
import traceback
|
| 173 |
+
|
| 174 |
+
traceback.print_exc()
|
| 175 |
+
return {"sha256": sha256, "voxelized": False, "num_voxels": 0}
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
def save_ply(filename, points):
|
| 179 |
+
"""
|
| 180 |
+
Save points as a PLY file.
|
| 181 |
+
|
| 182 |
+
Args:
|
| 183 |
+
filename (str): Output filename
|
| 184 |
+
points (numpy.ndarray): Array of 3D points
|
| 185 |
+
"""
|
| 186 |
+
os.makedirs(os.path.dirname(filename), exist_ok=True)
|
| 187 |
+
pcd = o3d.geometry.PointCloud()
|
| 188 |
+
pcd.points = o3d.utility.Vector3dVector(points)
|
| 189 |
+
o3d.io.write_point_cloud(filename, pcd)
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
def _voxelize(file_path, sha256, output_dir=None, max_voxels=None):
|
| 193 |
+
"""Wrapper function for voxelization."""
|
| 194 |
+
return voxelize_glb(file_path, sha256, output_dir, max_voxels=max_voxels)
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
if __name__ == "__main__":
|
| 198 |
+
parser = argparse.ArgumentParser(description="Voxelize ABO 500 dataset")
|
| 199 |
+
parser.add_argument(
|
| 200 |
+
"--output_dir",
|
| 201 |
+
type=str,
|
| 202 |
+
required=True,
|
| 203 |
+
help="Directory containing metadata and where to save voxelized data",
|
| 204 |
+
)
|
| 205 |
+
parser.add_argument(
|
| 206 |
+
"--instances",
|
| 207 |
+
type=str,
|
| 208 |
+
default=None,
|
| 209 |
+
help="Specific instances to process (comma-separated or file path)",
|
| 210 |
+
)
|
| 211 |
+
parser.add_argument(
|
| 212 |
+
"--force",
|
| 213 |
+
action="store_true",
|
| 214 |
+
help="Force voxelization even if already processed",
|
| 215 |
+
)
|
| 216 |
+
parser.add_argument("--rank", type=int, default=0)
|
| 217 |
+
parser.add_argument("--world_size", type=int, default=1)
|
| 218 |
+
parser.add_argument("--max_workers", type=int, default=None)
|
| 219 |
+
parser.add_argument(
|
| 220 |
+
"--limit", type=int, default=None, help="Process only the first N objects"
|
| 221 |
+
)
|
| 222 |
+
parser.add_argument(
|
| 223 |
+
"--max_voxels",
|
| 224 |
+
type=int,
|
| 225 |
+
default=70000,
|
| 226 |
+
help="Maximum number of voxels per asset",
|
| 227 |
+
)
|
| 228 |
+
|
| 229 |
+
args = parser.parse_args()
|
| 230 |
+
opt = edict(vars(args))
|
| 231 |
+
|
| 232 |
+
# Create voxels directory
|
| 233 |
+
os.makedirs(os.path.join(opt.output_dir, "voxels"), exist_ok=True)
|
| 234 |
+
|
| 235 |
+
# Load metadata
|
| 236 |
+
metadata_path = os.path.join(opt.output_dir, "metadata.csv")
|
| 237 |
+
if not os.path.exists(metadata_path):
|
| 238 |
+
raise ValueError(f"metadata.csv not found at {metadata_path}")
|
| 239 |
+
|
| 240 |
+
metadata = pd.read_csv(metadata_path)
|
| 241 |
+
|
| 242 |
+
# Filter instances if specified
|
| 243 |
+
if opt.instances is not None:
|
| 244 |
+
if os.path.exists(opt.instances):
|
| 245 |
+
with open(opt.instances, "r") as f:
|
| 246 |
+
instances = f.read().splitlines()
|
| 247 |
+
else:
|
| 248 |
+
instances = opt.instances.split(",")
|
| 249 |
+
metadata = metadata[metadata["sha256"].isin(instances)]
|
| 250 |
+
else:
|
| 251 |
+
# Only process objects that haven't been voxelized yet
|
| 252 |
+
if "voxelized" in metadata.columns and not opt.force:
|
| 253 |
+
metadata = metadata[metadata["voxelized"] == False]
|
| 254 |
+
|
| 255 |
+
# Apply distributed processing
|
| 256 |
+
start = len(metadata) * opt.rank // opt.world_size
|
| 257 |
+
end = len(metadata) * (opt.rank + 1) // opt.world_size
|
| 258 |
+
metadata = metadata[start:end]
|
| 259 |
+
|
| 260 |
+
# Apply limit if specified
|
| 261 |
+
if opt.limit is not None:
|
| 262 |
+
metadata = metadata.head(opt.limit)
|
| 263 |
+
|
| 264 |
+
print(f"Processing {len(metadata)} objects with max_voxels={opt.max_voxels:,}...")
|
| 265 |
+
|
| 266 |
+
# Track already processed objects
|
| 267 |
+
records = []
|
| 268 |
+
|
| 269 |
+
# Filter out objects that are already processed
|
| 270 |
+
if not opt.force:
|
| 271 |
+
for sha256 in copy.copy(metadata["sha256"].values):
|
| 272 |
+
ply_path = os.path.join(opt.output_dir, "voxels", f"{sha256}.ply")
|
| 273 |
+
if os.path.exists(ply_path):
|
| 274 |
+
try:
|
| 275 |
+
pts = utils3d.io.read_ply(ply_path)[0]
|
| 276 |
+
records.append(
|
| 277 |
+
{"sha256": sha256, "voxelized": True, "num_voxels": len(pts)}
|
| 278 |
+
)
|
| 279 |
+
metadata = metadata[metadata["sha256"] != sha256]
|
| 280 |
+
except:
|
| 281 |
+
# If file is corrupted, re-process it
|
| 282 |
+
pass
|
| 283 |
+
|
| 284 |
+
# Process remaining objects
|
| 285 |
+
if len(metadata) > 0:
|
| 286 |
+
func = partial(_voxelize, output_dir=opt.output_dir, max_voxels=opt.max_voxels)
|
| 287 |
+
voxelized = dataset_utils.foreach_instance(
|
| 288 |
+
metadata,
|
| 289 |
+
opt.output_dir,
|
| 290 |
+
func,
|
| 291 |
+
max_workers=opt.max_workers,
|
| 292 |
+
desc="Voxelizing",
|
| 293 |
+
)
|
| 294 |
+
|
| 295 |
+
# Combine results
|
| 296 |
+
if len(records) > 0:
|
| 297 |
+
voxelized = pd.concat([voxelized, pd.DataFrame.from_records(records)])
|
| 298 |
+
|
| 299 |
+
# Save results
|
| 300 |
+
voxelized.to_csv(
|
| 301 |
+
os.path.join(opt.output_dir, f"voxelized_{opt.rank}.csv"), index=False
|
| 302 |
+
)
|
| 303 |
+
|
| 304 |
+
print(f"Voxelization complete. Results saved to voxelized_{opt.rank}.csv")
|
| 305 |
+
else:
|
| 306 |
+
print("No objects to process.")
|
deps/vomp/dataset_toolkits/blender_script/render.py
ADDED
|
@@ -0,0 +1,695 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
|
| 16 |
+
import argparse, sys, os, math, re, glob
|
| 17 |
+
from typing import *
|
| 18 |
+
import bpy
|
| 19 |
+
from mathutils import Vector, Matrix
|
| 20 |
+
import numpy as np
|
| 21 |
+
import json
|
| 22 |
+
import glob
|
| 23 |
+
|
| 24 |
+
"""=============== BLENDER ==============="""
|
| 25 |
+
|
| 26 |
+
IMPORT_FUNCTIONS: Dict[str, Callable] = {
|
| 27 |
+
"obj": bpy.ops.import_scene.obj,
|
| 28 |
+
"glb": bpy.ops.import_scene.gltf,
|
| 29 |
+
"gltf": bpy.ops.import_scene.gltf,
|
| 30 |
+
"usd": bpy.ops.import_scene.usd,
|
| 31 |
+
"fbx": bpy.ops.import_scene.fbx,
|
| 32 |
+
"stl": bpy.ops.import_mesh.stl,
|
| 33 |
+
"usda": bpy.ops.import_scene.usda,
|
| 34 |
+
"dae": bpy.ops.wm.collada_import,
|
| 35 |
+
"ply": bpy.ops.import_mesh.ply,
|
| 36 |
+
"abc": bpy.ops.wm.alembic_import,
|
| 37 |
+
"blend": bpy.ops.wm.append,
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
EXT = {
|
| 41 |
+
"PNG": "png",
|
| 42 |
+
"JPEG": "jpg",
|
| 43 |
+
"OPEN_EXR": "exr",
|
| 44 |
+
"TIFF": "tiff",
|
| 45 |
+
"BMP": "bmp",
|
| 46 |
+
"HDR": "hdr",
|
| 47 |
+
"TARGA": "tga",
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def setup_gpu_devices():
|
| 52 |
+
"""Setup GPU devices for Cycles rendering using the same approach as viz_fields.py."""
|
| 53 |
+
try:
|
| 54 |
+
cycles_prefs = bpy.context.preferences.addons["cycles"].preferences
|
| 55 |
+
except KeyError:
|
| 56 |
+
print("[ERROR] Cycles addon not found or not enabled")
|
| 57 |
+
return False
|
| 58 |
+
|
| 59 |
+
# Get available device types and try them in order of preference
|
| 60 |
+
available_types = cycles_prefs.get_device_types(bpy.context)
|
| 61 |
+
available_type_names = [dt[0] for dt in available_types]
|
| 62 |
+
|
| 63 |
+
# Try device types in order of preference (same as viz_fields.py)
|
| 64 |
+
preferred_types = ["OPTIX", "CUDA", "HIP", "ONEAPI", "OPENCL", "METAL"]
|
| 65 |
+
available_preferred = [t for t in preferred_types if t in available_type_names]
|
| 66 |
+
|
| 67 |
+
best_type = None
|
| 68 |
+
for device_type in available_preferred:
|
| 69 |
+
try:
|
| 70 |
+
cycles_prefs.compute_device_type = device_type
|
| 71 |
+
cycles_prefs.get_devices()
|
| 72 |
+
|
| 73 |
+
gpu_devices = [
|
| 74 |
+
dev for dev in cycles_prefs.devices if dev.type == device_type
|
| 75 |
+
]
|
| 76 |
+
|
| 77 |
+
if gpu_devices:
|
| 78 |
+
best_type = device_type
|
| 79 |
+
print(f"[INFO] ✓ Using compute device type: {device_type}")
|
| 80 |
+
break
|
| 81 |
+
|
| 82 |
+
except Exception as e:
|
| 83 |
+
print(f"[WARNING] ⚠ Could not set {device_type}: {e}")
|
| 84 |
+
continue
|
| 85 |
+
|
| 86 |
+
if not best_type:
|
| 87 |
+
print("[ERROR] ✗ No GPU compute device type available")
|
| 88 |
+
return False
|
| 89 |
+
|
| 90 |
+
# Enable all GPU devices for the selected type
|
| 91 |
+
gpu_devices_enabled = 0
|
| 92 |
+
for device in cycles_prefs.devices:
|
| 93 |
+
if device.type == best_type:
|
| 94 |
+
device.use = True
|
| 95 |
+
gpu_devices_enabled += 1
|
| 96 |
+
print(f"[INFO] ✓ Enabled GPU device: {device.name}")
|
| 97 |
+
|
| 98 |
+
print(
|
| 99 |
+
f"[INFO] GPU setup complete: {gpu_devices_enabled} device(s) enabled with {best_type}"
|
| 100 |
+
)
|
| 101 |
+
return gpu_devices_enabled > 0
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def init_render(
|
| 105 |
+
engine="CYCLES", resolution=512, geo_mode=False, use_gpu=True, gpu_device="OPTIX"
|
| 106 |
+
):
|
| 107 |
+
bpy.context.scene.render.engine = engine
|
| 108 |
+
bpy.context.scene.render.resolution_x = resolution
|
| 109 |
+
bpy.context.scene.render.resolution_y = resolution
|
| 110 |
+
bpy.context.scene.render.resolution_percentage = 100
|
| 111 |
+
bpy.context.scene.render.image_settings.file_format = "PNG"
|
| 112 |
+
bpy.context.scene.render.image_settings.color_mode = "RGBA"
|
| 113 |
+
bpy.context.scene.render.film_transparent = True
|
| 114 |
+
|
| 115 |
+
# Enhanced GPU setup using the same approach as viz_fields.py
|
| 116 |
+
if use_gpu:
|
| 117 |
+
gpu_success = setup_gpu_devices()
|
| 118 |
+
if gpu_success:
|
| 119 |
+
bpy.context.scene.cycles.device = "GPU"
|
| 120 |
+
print("[INFO] ✅ GPU rendering enabled")
|
| 121 |
+
else:
|
| 122 |
+
bpy.context.scene.cycles.device = "CPU"
|
| 123 |
+
print("[WARNING] ⚠ GPU setup failed, using CPU rendering")
|
| 124 |
+
else:
|
| 125 |
+
bpy.context.scene.cycles.device = "CPU"
|
| 126 |
+
print("[INFO] CPU rendering requested")
|
| 127 |
+
|
| 128 |
+
bpy.context.scene.cycles.samples = 128 if not geo_mode else 1
|
| 129 |
+
bpy.context.scene.cycles.filter_type = "BOX"
|
| 130 |
+
bpy.context.scene.cycles.filter_width = 1
|
| 131 |
+
bpy.context.scene.cycles.diffuse_bounces = 1
|
| 132 |
+
bpy.context.scene.cycles.glossy_bounces = 1
|
| 133 |
+
bpy.context.scene.cycles.transparent_max_bounces = 3 if not geo_mode else 0
|
| 134 |
+
bpy.context.scene.cycles.transmission_bounces = 3 if not geo_mode else 1
|
| 135 |
+
bpy.context.scene.cycles.use_denoising = True
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
def init_nodes(save_depth=False, save_normal=False, save_albedo=False, save_mist=False):
|
| 139 |
+
if not any([save_depth, save_normal, save_albedo, save_mist]):
|
| 140 |
+
return {}, {}
|
| 141 |
+
outputs = {}
|
| 142 |
+
spec_nodes = {}
|
| 143 |
+
|
| 144 |
+
bpy.context.scene.use_nodes = True
|
| 145 |
+
bpy.context.scene.view_layers["View Layer"].use_pass_z = save_depth
|
| 146 |
+
bpy.context.scene.view_layers["View Layer"].use_pass_normal = save_normal
|
| 147 |
+
bpy.context.scene.view_layers["View Layer"].use_pass_diffuse_color = save_albedo
|
| 148 |
+
bpy.context.scene.view_layers["View Layer"].use_pass_mist = save_mist
|
| 149 |
+
|
| 150 |
+
nodes = bpy.context.scene.node_tree.nodes
|
| 151 |
+
links = bpy.context.scene.node_tree.links
|
| 152 |
+
for n in nodes:
|
| 153 |
+
nodes.remove(n)
|
| 154 |
+
|
| 155 |
+
render_layers = nodes.new("CompositorNodeRLayers")
|
| 156 |
+
|
| 157 |
+
if save_depth:
|
| 158 |
+
depth_file_output = nodes.new("CompositorNodeOutputFile")
|
| 159 |
+
depth_file_output.base_path = ""
|
| 160 |
+
depth_file_output.file_slots[0].use_node_format = True
|
| 161 |
+
depth_file_output.format.file_format = "PNG"
|
| 162 |
+
depth_file_output.format.color_depth = "16"
|
| 163 |
+
depth_file_output.format.color_mode = "BW"
|
| 164 |
+
# Remap to 0-1
|
| 165 |
+
map = nodes.new(type="CompositorNodeMapRange")
|
| 166 |
+
map.inputs[1].default_value = 0 # (min value you will be getting)
|
| 167 |
+
map.inputs[2].default_value = 10 # (max value you will be getting)
|
| 168 |
+
map.inputs[3].default_value = 0 # (min value you will map to)
|
| 169 |
+
map.inputs[4].default_value = 1 # (max value you will map to)
|
| 170 |
+
|
| 171 |
+
links.new(render_layers.outputs["Depth"], map.inputs[0])
|
| 172 |
+
links.new(map.outputs[0], depth_file_output.inputs[0])
|
| 173 |
+
|
| 174 |
+
outputs["depth"] = depth_file_output
|
| 175 |
+
spec_nodes["depth_map"] = map
|
| 176 |
+
|
| 177 |
+
if save_normal:
|
| 178 |
+
normal_file_output = nodes.new("CompositorNodeOutputFile")
|
| 179 |
+
normal_file_output.base_path = ""
|
| 180 |
+
normal_file_output.file_slots[0].use_node_format = True
|
| 181 |
+
normal_file_output.format.file_format = "OPEN_EXR"
|
| 182 |
+
normal_file_output.format.color_mode = "RGB"
|
| 183 |
+
normal_file_output.format.color_depth = "16"
|
| 184 |
+
|
| 185 |
+
links.new(render_layers.outputs["Normal"], normal_file_output.inputs[0])
|
| 186 |
+
|
| 187 |
+
outputs["normal"] = normal_file_output
|
| 188 |
+
|
| 189 |
+
if save_albedo:
|
| 190 |
+
albedo_file_output = nodes.new("CompositorNodeOutputFile")
|
| 191 |
+
albedo_file_output.base_path = ""
|
| 192 |
+
albedo_file_output.file_slots[0].use_node_format = True
|
| 193 |
+
albedo_file_output.format.file_format = "PNG"
|
| 194 |
+
albedo_file_output.format.color_mode = "RGBA"
|
| 195 |
+
albedo_file_output.format.color_depth = "8"
|
| 196 |
+
|
| 197 |
+
alpha_albedo = nodes.new("CompositorNodeSetAlpha")
|
| 198 |
+
|
| 199 |
+
links.new(render_layers.outputs["DiffCol"], alpha_albedo.inputs["Image"])
|
| 200 |
+
links.new(render_layers.outputs["Alpha"], alpha_albedo.inputs["Alpha"])
|
| 201 |
+
links.new(alpha_albedo.outputs["Image"], albedo_file_output.inputs[0])
|
| 202 |
+
|
| 203 |
+
outputs["albedo"] = albedo_file_output
|
| 204 |
+
|
| 205 |
+
if save_mist:
|
| 206 |
+
bpy.data.worlds["World"].mist_settings.start = 0
|
| 207 |
+
bpy.data.worlds["World"].mist_settings.depth = 10
|
| 208 |
+
|
| 209 |
+
mist_file_output = nodes.new("CompositorNodeOutputFile")
|
| 210 |
+
mist_file_output.base_path = ""
|
| 211 |
+
mist_file_output.file_slots[0].use_node_format = True
|
| 212 |
+
mist_file_output.format.file_format = "PNG"
|
| 213 |
+
mist_file_output.format.color_mode = "BW"
|
| 214 |
+
mist_file_output.format.color_depth = "16"
|
| 215 |
+
|
| 216 |
+
links.new(render_layers.outputs["Mist"], mist_file_output.inputs[0])
|
| 217 |
+
|
| 218 |
+
outputs["mist"] = mist_file_output
|
| 219 |
+
|
| 220 |
+
return outputs, spec_nodes
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
def init_scene() -> None:
|
| 224 |
+
"""Resets the scene to a clean state.
|
| 225 |
+
|
| 226 |
+
Returns:
|
| 227 |
+
None
|
| 228 |
+
"""
|
| 229 |
+
# delete everything
|
| 230 |
+
for obj in bpy.data.objects:
|
| 231 |
+
bpy.data.objects.remove(obj, do_unlink=True)
|
| 232 |
+
|
| 233 |
+
# delete all the materials
|
| 234 |
+
for material in bpy.data.materials:
|
| 235 |
+
bpy.data.materials.remove(material, do_unlink=True)
|
| 236 |
+
|
| 237 |
+
# delete all the textures
|
| 238 |
+
for texture in bpy.data.textures:
|
| 239 |
+
bpy.data.textures.remove(texture, do_unlink=True)
|
| 240 |
+
|
| 241 |
+
# delete all the images
|
| 242 |
+
for image in bpy.data.images:
|
| 243 |
+
bpy.data.images.remove(image, do_unlink=True)
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
def init_camera():
|
| 247 |
+
cam = bpy.data.objects.new("Camera", bpy.data.cameras.new("Camera"))
|
| 248 |
+
bpy.context.collection.objects.link(cam)
|
| 249 |
+
bpy.context.scene.camera = cam
|
| 250 |
+
cam.data.sensor_height = cam.data.sensor_width = 32
|
| 251 |
+
cam_constraint = cam.constraints.new(type="TRACK_TO")
|
| 252 |
+
cam_constraint.track_axis = "TRACK_NEGATIVE_Z"
|
| 253 |
+
cam_constraint.up_axis = "UP_Y"
|
| 254 |
+
cam_empty = bpy.data.objects.new("Empty", None)
|
| 255 |
+
cam_empty.location = (0, 0, 0)
|
| 256 |
+
bpy.context.scene.collection.objects.link(cam_empty)
|
| 257 |
+
cam_constraint.target = cam_empty
|
| 258 |
+
return cam
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
def init_lighting():
|
| 262 |
+
# Clear existing lights
|
| 263 |
+
bpy.ops.object.select_all(action="DESELECT")
|
| 264 |
+
bpy.ops.object.select_by_type(type="LIGHT")
|
| 265 |
+
bpy.ops.object.delete()
|
| 266 |
+
|
| 267 |
+
# Create key light
|
| 268 |
+
default_light = bpy.data.objects.new(
|
| 269 |
+
"Default_Light", bpy.data.lights.new("Default_Light", type="POINT")
|
| 270 |
+
)
|
| 271 |
+
bpy.context.collection.objects.link(default_light)
|
| 272 |
+
default_light.data.energy = 1000
|
| 273 |
+
default_light.location = (4, 1, 6)
|
| 274 |
+
default_light.rotation_euler = (0, 0, 0)
|
| 275 |
+
|
| 276 |
+
# create top light
|
| 277 |
+
top_light = bpy.data.objects.new(
|
| 278 |
+
"Top_Light", bpy.data.lights.new("Top_Light", type="AREA")
|
| 279 |
+
)
|
| 280 |
+
bpy.context.collection.objects.link(top_light)
|
| 281 |
+
top_light.data.energy = 10000
|
| 282 |
+
top_light.location = (0, 0, 10)
|
| 283 |
+
top_light.scale = (100, 100, 100)
|
| 284 |
+
|
| 285 |
+
# create bottom light
|
| 286 |
+
bottom_light = bpy.data.objects.new(
|
| 287 |
+
"Bottom_Light", bpy.data.lights.new("Bottom_Light", type="AREA")
|
| 288 |
+
)
|
| 289 |
+
bpy.context.collection.objects.link(bottom_light)
|
| 290 |
+
bottom_light.data.energy = 1000
|
| 291 |
+
bottom_light.location = (0, 0, -10)
|
| 292 |
+
bottom_light.rotation_euler = (0, 0, 0)
|
| 293 |
+
|
| 294 |
+
return {
|
| 295 |
+
"default_light": default_light,
|
| 296 |
+
"top_light": top_light,
|
| 297 |
+
"bottom_light": bottom_light,
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
|
| 301 |
+
def load_object(object_path: str) -> None:
|
| 302 |
+
"""Loads a model with a supported file extension into the scene.
|
| 303 |
+
|
| 304 |
+
Args:
|
| 305 |
+
object_path (str): Path to the model file.
|
| 306 |
+
|
| 307 |
+
Raises:
|
| 308 |
+
ValueError: If the file extension is not supported.
|
| 309 |
+
|
| 310 |
+
Returns:
|
| 311 |
+
None
|
| 312 |
+
"""
|
| 313 |
+
file_extension = object_path.split(".")[-1].lower()
|
| 314 |
+
if file_extension is None:
|
| 315 |
+
raise ValueError(f"Unsupported file type: {object_path}")
|
| 316 |
+
|
| 317 |
+
if file_extension == "usdz":
|
| 318 |
+
# install usdz io package
|
| 319 |
+
dirname = os.path.dirname(os.path.realpath(__file__))
|
| 320 |
+
usdz_package = os.path.join(dirname, "io_scene_usdz.zip")
|
| 321 |
+
bpy.ops.preferences.addon_install(filepath=usdz_package)
|
| 322 |
+
# enable it
|
| 323 |
+
addon_name = "io_scene_usdz"
|
| 324 |
+
bpy.ops.preferences.addon_enable(module=addon_name)
|
| 325 |
+
# import the usdz
|
| 326 |
+
from io_scene_usdz.import_usdz import import_usdz
|
| 327 |
+
|
| 328 |
+
import_usdz(context, filepath=object_path, materials=True, animations=True)
|
| 329 |
+
return None
|
| 330 |
+
|
| 331 |
+
# load from existing import functions
|
| 332 |
+
import_function = IMPORT_FUNCTIONS[file_extension]
|
| 333 |
+
|
| 334 |
+
print(f"Loading object from {object_path}")
|
| 335 |
+
if file_extension == "blend":
|
| 336 |
+
import_function(directory=object_path, link=False)
|
| 337 |
+
elif file_extension in {"glb", "gltf"}:
|
| 338 |
+
import_function(
|
| 339 |
+
filepath=object_path, merge_vertices=True, import_shading="NORMALS"
|
| 340 |
+
)
|
| 341 |
+
else:
|
| 342 |
+
import_function(filepath=object_path)
|
| 343 |
+
|
| 344 |
+
|
| 345 |
+
def delete_invisible_objects() -> None:
|
| 346 |
+
"""Deletes all invisible objects in the scene.
|
| 347 |
+
|
| 348 |
+
Returns:
|
| 349 |
+
None
|
| 350 |
+
"""
|
| 351 |
+
# bpy.ops.object.mode_set(mode="OBJECT")
|
| 352 |
+
bpy.ops.object.select_all(action="DESELECT")
|
| 353 |
+
for obj in bpy.context.scene.objects:
|
| 354 |
+
if obj.hide_viewport or obj.hide_render:
|
| 355 |
+
obj.hide_viewport = False
|
| 356 |
+
obj.hide_render = False
|
| 357 |
+
obj.hide_select = False
|
| 358 |
+
obj.select_set(True)
|
| 359 |
+
bpy.ops.object.delete()
|
| 360 |
+
|
| 361 |
+
# Delete invisible collections
|
| 362 |
+
invisible_collections = [col for col in bpy.data.collections if col.hide_viewport]
|
| 363 |
+
for col in invisible_collections:
|
| 364 |
+
bpy.data.collections.remove(col)
|
| 365 |
+
|
| 366 |
+
|
| 367 |
+
def split_mesh_normal():
|
| 368 |
+
bpy.ops.object.select_all(action="DESELECT")
|
| 369 |
+
objs = [obj for obj in bpy.context.scene.objects if obj.type == "MESH"]
|
| 370 |
+
bpy.context.view_layer.objects.active = objs[0]
|
| 371 |
+
for obj in objs:
|
| 372 |
+
obj.select_set(True)
|
| 373 |
+
bpy.ops.object.mode_set(mode="EDIT")
|
| 374 |
+
bpy.ops.mesh.select_all(action="SELECT")
|
| 375 |
+
bpy.ops.mesh.split_normals()
|
| 376 |
+
bpy.ops.object.mode_set(mode="OBJECT")
|
| 377 |
+
bpy.ops.object.select_all(action="DESELECT")
|
| 378 |
+
|
| 379 |
+
|
| 380 |
+
def delete_custom_normals():
|
| 381 |
+
for this_obj in bpy.data.objects:
|
| 382 |
+
if this_obj.type == "MESH":
|
| 383 |
+
bpy.context.view_layer.objects.active = this_obj
|
| 384 |
+
bpy.ops.mesh.customdata_custom_splitnormals_clear()
|
| 385 |
+
|
| 386 |
+
|
| 387 |
+
def override_material():
|
| 388 |
+
new_mat = bpy.data.materials.new(name="Override0123456789")
|
| 389 |
+
new_mat.use_nodes = True
|
| 390 |
+
new_mat.node_tree.nodes.clear()
|
| 391 |
+
bsdf = new_mat.node_tree.nodes.new("ShaderNodeBsdfDiffuse")
|
| 392 |
+
bsdf.inputs[0].default_value = (0.5, 0.5, 0.5, 1)
|
| 393 |
+
bsdf.inputs[1].default_value = 1
|
| 394 |
+
output = new_mat.node_tree.nodes.new("ShaderNodeOutputMaterial")
|
| 395 |
+
new_mat.node_tree.links.new(bsdf.outputs["BSDF"], output.inputs["Surface"])
|
| 396 |
+
bpy.context.scene.view_layers["View Layer"].material_override = new_mat
|
| 397 |
+
|
| 398 |
+
|
| 399 |
+
def unhide_all_objects() -> None:
|
| 400 |
+
"""Unhides all objects in the scene.
|
| 401 |
+
|
| 402 |
+
Returns:
|
| 403 |
+
None
|
| 404 |
+
"""
|
| 405 |
+
for obj in bpy.context.scene.objects:
|
| 406 |
+
obj.hide_set(False)
|
| 407 |
+
|
| 408 |
+
|
| 409 |
+
def convert_to_meshes() -> None:
|
| 410 |
+
"""Converts all objects in the scene to meshes.
|
| 411 |
+
|
| 412 |
+
Returns:
|
| 413 |
+
None
|
| 414 |
+
"""
|
| 415 |
+
bpy.ops.object.select_all(action="DESELECT")
|
| 416 |
+
bpy.context.view_layer.objects.active = [
|
| 417 |
+
obj for obj in bpy.context.scene.objects if obj.type == "MESH"
|
| 418 |
+
][0]
|
| 419 |
+
for obj in bpy.context.scene.objects:
|
| 420 |
+
obj.select_set(True)
|
| 421 |
+
bpy.ops.object.convert(target="MESH")
|
| 422 |
+
|
| 423 |
+
|
| 424 |
+
def triangulate_meshes() -> None:
|
| 425 |
+
"""Triangulates all meshes in the scene.
|
| 426 |
+
|
| 427 |
+
Returns:
|
| 428 |
+
None
|
| 429 |
+
"""
|
| 430 |
+
bpy.ops.object.select_all(action="DESELECT")
|
| 431 |
+
objs = [obj for obj in bpy.context.scene.objects if obj.type == "MESH"]
|
| 432 |
+
bpy.context.view_layer.objects.active = objs[0]
|
| 433 |
+
for obj in objs:
|
| 434 |
+
obj.select_set(True)
|
| 435 |
+
bpy.ops.object.mode_set(mode="EDIT")
|
| 436 |
+
bpy.ops.mesh.reveal()
|
| 437 |
+
bpy.ops.mesh.select_all(action="SELECT")
|
| 438 |
+
bpy.ops.mesh.quads_convert_to_tris(quad_method="BEAUTY", ngon_method="BEAUTY")
|
| 439 |
+
bpy.ops.object.mode_set(mode="OBJECT")
|
| 440 |
+
bpy.ops.object.select_all(action="DESELECT")
|
| 441 |
+
|
| 442 |
+
|
| 443 |
+
def scene_bbox() -> Tuple[Vector, Vector]:
|
| 444 |
+
"""Returns the bounding box of the scene.
|
| 445 |
+
|
| 446 |
+
Taken from Shap-E rendering script
|
| 447 |
+
(https://github.com/openai/shap-e/blob/main/shap_e/rendering/blender/blender_script.py#L68-L82)
|
| 448 |
+
|
| 449 |
+
Returns:
|
| 450 |
+
Tuple[Vector, Vector]: The minimum and maximum coordinates of the bounding box.
|
| 451 |
+
"""
|
| 452 |
+
bbox_min = (math.inf,) * 3
|
| 453 |
+
bbox_max = (-math.inf,) * 3
|
| 454 |
+
found = False
|
| 455 |
+
scene_meshes = [
|
| 456 |
+
obj
|
| 457 |
+
for obj in bpy.context.scene.objects.values()
|
| 458 |
+
if isinstance(obj.data, bpy.types.Mesh)
|
| 459 |
+
]
|
| 460 |
+
for obj in scene_meshes:
|
| 461 |
+
found = True
|
| 462 |
+
for coord in obj.bound_box:
|
| 463 |
+
coord = Vector(coord)
|
| 464 |
+
coord = obj.matrix_world @ coord
|
| 465 |
+
bbox_min = tuple(min(x, y) for x, y in zip(bbox_min, coord))
|
| 466 |
+
bbox_max = tuple(max(x, y) for x, y in zip(bbox_max, coord))
|
| 467 |
+
if not found:
|
| 468 |
+
raise RuntimeError("no objects in scene to compute bounding box for")
|
| 469 |
+
return Vector(bbox_min), Vector(bbox_max)
|
| 470 |
+
|
| 471 |
+
|
| 472 |
+
def normalize_scene() -> Tuple[float, Vector]:
|
| 473 |
+
"""Normalizes the scene by scaling and translating it to fit in a unit cube centered
|
| 474 |
+
at the origin.
|
| 475 |
+
|
| 476 |
+
Mostly taken from the Point-E / Shap-E rendering script
|
| 477 |
+
(https://github.com/openai/point-e/blob/main/point_e/evals/scripts/blender_script.py#L97-L112),
|
| 478 |
+
but fix for multiple root objects: (see bug report here:
|
| 479 |
+
https://github.com/openai/shap-e/pull/60).
|
| 480 |
+
|
| 481 |
+
Returns:
|
| 482 |
+
Tuple[float, Vector]: The scale factor and the offset applied to the scene.
|
| 483 |
+
"""
|
| 484 |
+
scene_root_objects = [
|
| 485 |
+
obj for obj in bpy.context.scene.objects.values() if not obj.parent
|
| 486 |
+
]
|
| 487 |
+
if len(scene_root_objects) > 1:
|
| 488 |
+
# create an empty object to be used as a parent for all root objects
|
| 489 |
+
scene = bpy.data.objects.new("ParentEmpty", None)
|
| 490 |
+
bpy.context.scene.collection.objects.link(scene)
|
| 491 |
+
|
| 492 |
+
# parent all root objects to the empty object
|
| 493 |
+
for obj in scene_root_objects:
|
| 494 |
+
obj.parent = scene
|
| 495 |
+
else:
|
| 496 |
+
scene = scene_root_objects[0]
|
| 497 |
+
|
| 498 |
+
bbox_min, bbox_max = scene_bbox()
|
| 499 |
+
scale = 1 / max(bbox_max - bbox_min)
|
| 500 |
+
scene.scale = scene.scale * scale
|
| 501 |
+
|
| 502 |
+
# Apply scale to matrix_world.
|
| 503 |
+
bpy.context.view_layer.update()
|
| 504 |
+
bbox_min, bbox_max = scene_bbox()
|
| 505 |
+
offset = -(bbox_min + bbox_max) / 2
|
| 506 |
+
scene.matrix_world.translation += offset
|
| 507 |
+
bpy.ops.object.select_all(action="DESELECT")
|
| 508 |
+
|
| 509 |
+
return scale, offset
|
| 510 |
+
|
| 511 |
+
|
| 512 |
+
def get_transform_matrix(obj: bpy.types.Object) -> list:
|
| 513 |
+
pos, rt, _ = obj.matrix_world.decompose()
|
| 514 |
+
rt = rt.to_matrix()
|
| 515 |
+
matrix = []
|
| 516 |
+
for ii in range(3):
|
| 517 |
+
a = []
|
| 518 |
+
for jj in range(3):
|
| 519 |
+
a.append(rt[ii][jj])
|
| 520 |
+
a.append(pos[ii])
|
| 521 |
+
matrix.append(a)
|
| 522 |
+
matrix.append([0, 0, 0, 1])
|
| 523 |
+
return matrix
|
| 524 |
+
|
| 525 |
+
|
| 526 |
+
def main(arg):
|
| 527 |
+
os.makedirs(arg.output_folder, exist_ok=True)
|
| 528 |
+
|
| 529 |
+
# Initialize context
|
| 530 |
+
init_render(
|
| 531 |
+
engine=arg.engine,
|
| 532 |
+
resolution=arg.resolution,
|
| 533 |
+
geo_mode=arg.geo_mode,
|
| 534 |
+
use_gpu=getattr(arg, "use_gpu", True),
|
| 535 |
+
gpu_device=getattr(arg, "gpu_device", "OPTIX"),
|
| 536 |
+
)
|
| 537 |
+
outputs, spec_nodes = init_nodes(
|
| 538 |
+
save_depth=arg.save_depth,
|
| 539 |
+
save_normal=arg.save_normal,
|
| 540 |
+
save_albedo=arg.save_albedo,
|
| 541 |
+
save_mist=arg.save_mist,
|
| 542 |
+
)
|
| 543 |
+
if arg.object.endswith(".blend"):
|
| 544 |
+
delete_invisible_objects()
|
| 545 |
+
else:
|
| 546 |
+
init_scene()
|
| 547 |
+
load_object(arg.object)
|
| 548 |
+
if arg.split_normal:
|
| 549 |
+
split_mesh_normal()
|
| 550 |
+
# delete_custom_normals()
|
| 551 |
+
print("[INFO] Scene initialized.")
|
| 552 |
+
|
| 553 |
+
# normalize scene
|
| 554 |
+
scale, offset = normalize_scene()
|
| 555 |
+
print("[INFO] Scene normalized.")
|
| 556 |
+
|
| 557 |
+
# Initialize camera and lighting
|
| 558 |
+
cam = init_camera()
|
| 559 |
+
init_lighting()
|
| 560 |
+
print("[INFO] Camera and lighting initialized.")
|
| 561 |
+
|
| 562 |
+
# Override material
|
| 563 |
+
if arg.geo_mode:
|
| 564 |
+
override_material()
|
| 565 |
+
|
| 566 |
+
# Create a list of views
|
| 567 |
+
to_export = {
|
| 568 |
+
"aabb": [[-0.5, -0.5, -0.5], [0.5, 0.5, 0.5]],
|
| 569 |
+
"scale": scale,
|
| 570 |
+
"offset": [offset.x, offset.y, offset.z],
|
| 571 |
+
"frames": [],
|
| 572 |
+
}
|
| 573 |
+
views = json.loads(arg.views)
|
| 574 |
+
for i, view in enumerate(views):
|
| 575 |
+
cam.location = (
|
| 576 |
+
view["radius"] * np.cos(view["yaw"]) * np.cos(view["pitch"]),
|
| 577 |
+
view["radius"] * np.sin(view["yaw"]) * np.cos(view["pitch"]),
|
| 578 |
+
view["radius"] * np.sin(view["pitch"]),
|
| 579 |
+
)
|
| 580 |
+
cam.data.lens = 16 / np.tan(view["fov"] / 2)
|
| 581 |
+
|
| 582 |
+
if arg.save_depth:
|
| 583 |
+
spec_nodes["depth_map"].inputs[1].default_value = view[
|
| 584 |
+
"radius"
|
| 585 |
+
] - 0.5 * np.sqrt(3)
|
| 586 |
+
spec_nodes["depth_map"].inputs[2].default_value = view[
|
| 587 |
+
"radius"
|
| 588 |
+
] + 0.5 * np.sqrt(3)
|
| 589 |
+
|
| 590 |
+
bpy.context.scene.render.filepath = os.path.join(
|
| 591 |
+
arg.output_folder, f"{i:03d}.png"
|
| 592 |
+
)
|
| 593 |
+
for name, output in outputs.items():
|
| 594 |
+
output.file_slots[0].path = os.path.join(
|
| 595 |
+
arg.output_folder, f"{i:03d}_{name}"
|
| 596 |
+
)
|
| 597 |
+
|
| 598 |
+
# Render the scene
|
| 599 |
+
bpy.ops.render.render(write_still=True)
|
| 600 |
+
bpy.context.view_layer.update()
|
| 601 |
+
for name, output in outputs.items():
|
| 602 |
+
ext = EXT[output.format.file_format]
|
| 603 |
+
path = glob.glob(f"{output.file_slots[0].path}*.{ext}")[0]
|
| 604 |
+
os.rename(path, f"{output.file_slots[0].path}.{ext}")
|
| 605 |
+
|
| 606 |
+
# Save camera parameters
|
| 607 |
+
metadata = {
|
| 608 |
+
"file_path": f"{i:03d}.png",
|
| 609 |
+
"camera_angle_x": view["fov"],
|
| 610 |
+
"transform_matrix": get_transform_matrix(cam),
|
| 611 |
+
}
|
| 612 |
+
if arg.save_depth:
|
| 613 |
+
metadata["depth"] = {
|
| 614 |
+
"min": view["radius"] - 0.5 * np.sqrt(3),
|
| 615 |
+
"max": view["radius"] + 0.5 * np.sqrt(3),
|
| 616 |
+
}
|
| 617 |
+
to_export["frames"].append(metadata)
|
| 618 |
+
|
| 619 |
+
# Save the camera parameters
|
| 620 |
+
with open(os.path.join(arg.output_folder, "transforms.json"), "w") as f:
|
| 621 |
+
json.dump(to_export, f, indent=4)
|
| 622 |
+
|
| 623 |
+
if arg.save_mesh:
|
| 624 |
+
# triangulate meshes
|
| 625 |
+
unhide_all_objects()
|
| 626 |
+
convert_to_meshes()
|
| 627 |
+
triangulate_meshes()
|
| 628 |
+
print("[INFO] Meshes triangulated.")
|
| 629 |
+
|
| 630 |
+
# export ply mesh
|
| 631 |
+
bpy.ops.export_mesh.ply(filepath=os.path.join(arg.output_folder, "mesh.ply"))
|
| 632 |
+
|
| 633 |
+
|
| 634 |
+
if __name__ == "__main__":
|
| 635 |
+
parser = argparse.ArgumentParser(
|
| 636 |
+
description="Renders given obj file by rotation a camera around it."
|
| 637 |
+
)
|
| 638 |
+
parser.add_argument(
|
| 639 |
+
"--views",
|
| 640 |
+
type=str,
|
| 641 |
+
help="JSON string of views. Contains a list of {yaw, pitch, radius, fov} object.",
|
| 642 |
+
)
|
| 643 |
+
parser.add_argument(
|
| 644 |
+
"--object", type=str, help="Path to the 3D model file to be rendered."
|
| 645 |
+
)
|
| 646 |
+
parser.add_argument(
|
| 647 |
+
"--output_folder",
|
| 648 |
+
type=str,
|
| 649 |
+
default="/tmp",
|
| 650 |
+
help="The path the output will be dumped to.",
|
| 651 |
+
)
|
| 652 |
+
parser.add_argument(
|
| 653 |
+
"--resolution", type=int, default=512, help="Resolution of the images."
|
| 654 |
+
)
|
| 655 |
+
parser.add_argument(
|
| 656 |
+
"--engine",
|
| 657 |
+
type=str,
|
| 658 |
+
default="CYCLES",
|
| 659 |
+
help="Blender internal engine for rendering. E.g. CYCLES, BLENDER_EEVEE, ...",
|
| 660 |
+
)
|
| 661 |
+
parser.add_argument(
|
| 662 |
+
"--geo_mode", action="store_true", help="Geometry mode for rendering."
|
| 663 |
+
)
|
| 664 |
+
parser.add_argument(
|
| 665 |
+
"--save_depth", action="store_true", help="Save the depth maps."
|
| 666 |
+
)
|
| 667 |
+
parser.add_argument(
|
| 668 |
+
"--save_normal", action="store_true", help="Save the normal maps."
|
| 669 |
+
)
|
| 670 |
+
parser.add_argument(
|
| 671 |
+
"--save_albedo", action="store_true", help="Save the albedo maps."
|
| 672 |
+
)
|
| 673 |
+
parser.add_argument(
|
| 674 |
+
"--save_mist", action="store_true", help="Save the mist distance maps."
|
| 675 |
+
)
|
| 676 |
+
parser.add_argument(
|
| 677 |
+
"--split_normal", action="store_true", help="Split the normals of the mesh."
|
| 678 |
+
)
|
| 679 |
+
parser.add_argument(
|
| 680 |
+
"--save_mesh", action="store_true", help="Save the mesh as a .ply file."
|
| 681 |
+
)
|
| 682 |
+
parser.add_argument(
|
| 683 |
+
"--use_gpu", action="store_true", help="Use GPU acceleration for rendering."
|
| 684 |
+
)
|
| 685 |
+
parser.add_argument(
|
| 686 |
+
"--gpu_device",
|
| 687 |
+
type=str,
|
| 688 |
+
default="OPTIX",
|
| 689 |
+
choices=["OPTIX", "CUDA", "OPENCL"],
|
| 690 |
+
help="GPU device type for rendering (OPTIX, CUDA, or OPENCL).",
|
| 691 |
+
)
|
| 692 |
+
argv = sys.argv[sys.argv.index("--") + 1 :]
|
| 693 |
+
args = parser.parse_args(argv)
|
| 694 |
+
|
| 695 |
+
main(args)
|
deps/vomp/dataset_toolkits/build_metadata.py
ADDED
|
@@ -0,0 +1,551 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
|
| 16 |
+
import os
|
| 17 |
+
import shutil
|
| 18 |
+
import sys
|
| 19 |
+
import time
|
| 20 |
+
import importlib
|
| 21 |
+
import argparse
|
| 22 |
+
import numpy as np
|
| 23 |
+
import pandas as pd
|
| 24 |
+
from tqdm import tqdm
|
| 25 |
+
from easydict import EasyDict as edict
|
| 26 |
+
from concurrent.futures import ThreadPoolExecutor
|
| 27 |
+
import utils3d
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def get_first_directory(path):
|
| 31 |
+
with os.scandir(path) as it:
|
| 32 |
+
for entry in it:
|
| 33 |
+
if entry.is_dir():
|
| 34 |
+
return entry.name
|
| 35 |
+
return None
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def need_process(key):
|
| 39 |
+
return key in opt.field or opt.field == ["all"]
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
if __name__ == "__main__":
|
| 43 |
+
dataset_utils = importlib.import_module(f"dataset_toolkits.datasets.{sys.argv[1]}")
|
| 44 |
+
|
| 45 |
+
parser = argparse.ArgumentParser()
|
| 46 |
+
parser.add_argument(
|
| 47 |
+
"--output_dir", type=str, required=True, help="Directory to save the metadata"
|
| 48 |
+
)
|
| 49 |
+
parser.add_argument(
|
| 50 |
+
"--field",
|
| 51 |
+
type=str,
|
| 52 |
+
default="all",
|
| 53 |
+
help="Fields to process, separated by commas",
|
| 54 |
+
)
|
| 55 |
+
parser.add_argument(
|
| 56 |
+
"--from_file",
|
| 57 |
+
action="store_true",
|
| 58 |
+
help="Build metadata from file instead of from records of processings."
|
| 59 |
+
+ "Useful when some processing fail to generate records but file already exists.",
|
| 60 |
+
)
|
| 61 |
+
parser.add_argument(
|
| 62 |
+
"--force_update_class_split",
|
| 63 |
+
action="store_true",
|
| 64 |
+
help="Force updating class and split information even if metadata file exists",
|
| 65 |
+
)
|
| 66 |
+
parser.add_argument(
|
| 67 |
+
"--skip_class_split_on_error",
|
| 68 |
+
action="store_true",
|
| 69 |
+
help="Skip updating class and split if an error occurs, instead of failing",
|
| 70 |
+
)
|
| 71 |
+
dataset_utils.add_args(parser)
|
| 72 |
+
opt = parser.parse_args(sys.argv[2:])
|
| 73 |
+
opt = edict(vars(opt))
|
| 74 |
+
|
| 75 |
+
os.makedirs(opt.output_dir, exist_ok=True)
|
| 76 |
+
os.makedirs(os.path.join(opt.output_dir, "merged_records"), exist_ok=True)
|
| 77 |
+
|
| 78 |
+
opt.field = opt.field.split(",")
|
| 79 |
+
|
| 80 |
+
timestamp = str(int(time.time()))
|
| 81 |
+
|
| 82 |
+
# Check if metadata file exists
|
| 83 |
+
metadata_exists = os.path.exists(os.path.join(opt.output_dir, "metadata.csv"))
|
| 84 |
+
|
| 85 |
+
# Load or create metadata
|
| 86 |
+
if metadata_exists:
|
| 87 |
+
print("Loading previous metadata...")
|
| 88 |
+
metadata = pd.read_csv(os.path.join(opt.output_dir, "metadata.csv"))
|
| 89 |
+
|
| 90 |
+
# Check if class and split information needs to be updated
|
| 91 |
+
requires_class_update = (
|
| 92 |
+
"class" not in metadata.columns or opt.force_update_class_split
|
| 93 |
+
)
|
| 94 |
+
requires_split_update = (
|
| 95 |
+
"split" not in metadata.columns or opt.force_update_class_split
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
if requires_class_update or requires_split_update:
|
| 99 |
+
# Generate fresh metadata with class and split information
|
| 100 |
+
print("Updating class and split information...")
|
| 101 |
+
try:
|
| 102 |
+
fresh_metadata = dataset_utils.get_metadata(**opt)
|
| 103 |
+
|
| 104 |
+
# Set index on sha256 for both DataFrames
|
| 105 |
+
metadata.set_index("sha256", inplace=True)
|
| 106 |
+
fresh_metadata.set_index("sha256", inplace=True)
|
| 107 |
+
|
| 108 |
+
# Update class information if needed
|
| 109 |
+
if requires_class_update and "class" in fresh_metadata.columns:
|
| 110 |
+
if "class" not in metadata.columns:
|
| 111 |
+
metadata["class"] = "unknown"
|
| 112 |
+
metadata.update(fresh_metadata[["class"]])
|
| 113 |
+
|
| 114 |
+
# Update split information if needed
|
| 115 |
+
if requires_split_update and "split" in fresh_metadata.columns:
|
| 116 |
+
if "split" not in metadata.columns:
|
| 117 |
+
metadata["split"] = "train" # Default value
|
| 118 |
+
metadata.update(fresh_metadata[["split"]])
|
| 119 |
+
except Exception as e:
|
| 120 |
+
if opt.skip_class_split_on_error:
|
| 121 |
+
print(f"Warning: Error updating class and split information: {e}")
|
| 122 |
+
print("Continuing with existing metadata...")
|
| 123 |
+
if "class" not in metadata.columns:
|
| 124 |
+
metadata["class"] = "unknown"
|
| 125 |
+
if "split" not in metadata.columns:
|
| 126 |
+
metadata["split"] = "train"
|
| 127 |
+
metadata.set_index("sha256", inplace=True)
|
| 128 |
+
else:
|
| 129 |
+
raise e
|
| 130 |
+
else:
|
| 131 |
+
metadata.set_index("sha256", inplace=True)
|
| 132 |
+
else:
|
| 133 |
+
# Create new metadata with all required information
|
| 134 |
+
print("Creating new metadata...")
|
| 135 |
+
try:
|
| 136 |
+
metadata = dataset_utils.get_metadata(**opt)
|
| 137 |
+
metadata.set_index("sha256", inplace=True)
|
| 138 |
+
except Exception as e:
|
| 139 |
+
if opt.skip_class_split_on_error:
|
| 140 |
+
print(
|
| 141 |
+
f"Warning: Error creating metadata with class and split information: {e}"
|
| 142 |
+
)
|
| 143 |
+
print("Creating basic metadata without class and split information...")
|
| 144 |
+
metadata = dataset_utils.get_metadata(skip_split=True, **opt)
|
| 145 |
+
metadata.set_index("sha256", inplace=True)
|
| 146 |
+
if "class" not in metadata.columns:
|
| 147 |
+
metadata["class"] = "unknown"
|
| 148 |
+
if "split" not in metadata.columns:
|
| 149 |
+
metadata["split"] = "train"
|
| 150 |
+
else:
|
| 151 |
+
raise e
|
| 152 |
+
|
| 153 |
+
# merge downloaded
|
| 154 |
+
df_files = [
|
| 155 |
+
f
|
| 156 |
+
for f in os.listdir(opt.output_dir)
|
| 157 |
+
if f.startswith("downloaded_") and f.endswith(".csv")
|
| 158 |
+
]
|
| 159 |
+
df_parts = []
|
| 160 |
+
for f in df_files:
|
| 161 |
+
try:
|
| 162 |
+
df_parts.append(pd.read_csv(os.path.join(opt.output_dir, f)))
|
| 163 |
+
except:
|
| 164 |
+
pass
|
| 165 |
+
if len(df_parts) > 0:
|
| 166 |
+
df = pd.concat(df_parts)
|
| 167 |
+
df.set_index("sha256", inplace=True)
|
| 168 |
+
if "local_path" in metadata.columns:
|
| 169 |
+
metadata.update(df, overwrite=True)
|
| 170 |
+
else:
|
| 171 |
+
metadata = metadata.join(df, on="sha256", how="left")
|
| 172 |
+
for f in df_files:
|
| 173 |
+
shutil.move(
|
| 174 |
+
os.path.join(opt.output_dir, f),
|
| 175 |
+
os.path.join(opt.output_dir, "merged_records", f"{timestamp}_{f}"),
|
| 176 |
+
)
|
| 177 |
+
|
| 178 |
+
# detect models
|
| 179 |
+
image_models = []
|
| 180 |
+
if os.path.exists(os.path.join(opt.output_dir, "features")):
|
| 181 |
+
image_models = os.listdir(os.path.join(opt.output_dir, "features"))
|
| 182 |
+
latent_models = []
|
| 183 |
+
if os.path.exists(os.path.join(opt.output_dir, "latents")):
|
| 184 |
+
latent_models = os.listdir(os.path.join(opt.output_dir, "latents"))
|
| 185 |
+
ss_latent_models = []
|
| 186 |
+
if os.path.exists(os.path.join(opt.output_dir, "ss_latents")):
|
| 187 |
+
ss_latent_models = os.listdir(os.path.join(opt.output_dir, "ss_latents"))
|
| 188 |
+
print(f"Image models: {image_models}")
|
| 189 |
+
print(f"Latent models: {latent_models}")
|
| 190 |
+
print(f"Sparse Structure latent models: {ss_latent_models}")
|
| 191 |
+
|
| 192 |
+
if "rendered" not in metadata.columns:
|
| 193 |
+
metadata["rendered"] = [False] * len(metadata)
|
| 194 |
+
if "voxelized" not in metadata.columns:
|
| 195 |
+
metadata["voxelized"] = [False] * len(metadata)
|
| 196 |
+
if "num_voxels" not in metadata.columns:
|
| 197 |
+
metadata["num_voxels"] = [0] * len(metadata)
|
| 198 |
+
if "cond_rendered" not in metadata.columns:
|
| 199 |
+
metadata["cond_rendered"] = [False] * len(metadata)
|
| 200 |
+
for model in image_models:
|
| 201 |
+
if f"feature_{model}" not in metadata.columns:
|
| 202 |
+
metadata[f"feature_{model}"] = [False] * len(metadata)
|
| 203 |
+
for model in latent_models:
|
| 204 |
+
if f"latent_{model}" not in metadata.columns:
|
| 205 |
+
metadata[f"latent_{model}"] = [False] * len(metadata)
|
| 206 |
+
for model in ss_latent_models:
|
| 207 |
+
if f"ss_latent_{model}" not in metadata.columns:
|
| 208 |
+
metadata[f"ss_latent_{model}"] = [False] * len(metadata)
|
| 209 |
+
|
| 210 |
+
# merge rendered
|
| 211 |
+
df_files = [
|
| 212 |
+
f
|
| 213 |
+
for f in os.listdir(opt.output_dir)
|
| 214 |
+
if f.startswith("rendered_") and f.endswith(".csv")
|
| 215 |
+
]
|
| 216 |
+
df_parts = []
|
| 217 |
+
for f in df_files:
|
| 218 |
+
try:
|
| 219 |
+
df_parts.append(pd.read_csv(os.path.join(opt.output_dir, f)))
|
| 220 |
+
except:
|
| 221 |
+
pass
|
| 222 |
+
if len(df_parts) > 0:
|
| 223 |
+
df = pd.concat(df_parts)
|
| 224 |
+
df.set_index("sha256", inplace=True)
|
| 225 |
+
metadata.update(df, overwrite=True)
|
| 226 |
+
for f in df_files:
|
| 227 |
+
shutil.move(
|
| 228 |
+
os.path.join(opt.output_dir, f),
|
| 229 |
+
os.path.join(opt.output_dir, "merged_records", f"{timestamp}_{f}"),
|
| 230 |
+
)
|
| 231 |
+
|
| 232 |
+
# merge voxelized
|
| 233 |
+
df_files = [
|
| 234 |
+
f
|
| 235 |
+
for f in os.listdir(opt.output_dir)
|
| 236 |
+
if f.startswith("voxelized_") and f.endswith(".csv")
|
| 237 |
+
]
|
| 238 |
+
df_parts = []
|
| 239 |
+
for f in df_files:
|
| 240 |
+
try:
|
| 241 |
+
df_parts.append(pd.read_csv(os.path.join(opt.output_dir, f)))
|
| 242 |
+
except:
|
| 243 |
+
pass
|
| 244 |
+
if len(df_parts) > 0:
|
| 245 |
+
df = pd.concat(df_parts)
|
| 246 |
+
df.set_index("sha256", inplace=True)
|
| 247 |
+
metadata.update(df, overwrite=True)
|
| 248 |
+
for f in df_files:
|
| 249 |
+
shutil.move(
|
| 250 |
+
os.path.join(opt.output_dir, f),
|
| 251 |
+
os.path.join(opt.output_dir, "merged_records", f"{timestamp}_{f}"),
|
| 252 |
+
)
|
| 253 |
+
|
| 254 |
+
# merge cond_rendered
|
| 255 |
+
df_files = [
|
| 256 |
+
f
|
| 257 |
+
for f in os.listdir(opt.output_dir)
|
| 258 |
+
if f.startswith("cond_rendered_") and f.endswith(".csv")
|
| 259 |
+
]
|
| 260 |
+
df_parts = []
|
| 261 |
+
for f in df_files:
|
| 262 |
+
try:
|
| 263 |
+
df_parts.append(pd.read_csv(os.path.join(opt.output_dir, f)))
|
| 264 |
+
except:
|
| 265 |
+
pass
|
| 266 |
+
if len(df_parts) > 0:
|
| 267 |
+
df = pd.concat(df_parts)
|
| 268 |
+
df.set_index("sha256", inplace=True)
|
| 269 |
+
metadata.update(df, overwrite=True)
|
| 270 |
+
for f in df_files:
|
| 271 |
+
shutil.move(
|
| 272 |
+
os.path.join(opt.output_dir, f),
|
| 273 |
+
os.path.join(opt.output_dir, "merged_records", f"{timestamp}_{f}"),
|
| 274 |
+
)
|
| 275 |
+
|
| 276 |
+
# merge features
|
| 277 |
+
for model in image_models:
|
| 278 |
+
df_files = [
|
| 279 |
+
f
|
| 280 |
+
for f in os.listdir(opt.output_dir)
|
| 281 |
+
if f.startswith(f"feature_{model}_") and f.endswith(".csv")
|
| 282 |
+
]
|
| 283 |
+
df_parts = []
|
| 284 |
+
for f in df_files:
|
| 285 |
+
try:
|
| 286 |
+
df_parts.append(pd.read_csv(os.path.join(opt.output_dir, f)))
|
| 287 |
+
except:
|
| 288 |
+
pass
|
| 289 |
+
if len(df_parts) > 0:
|
| 290 |
+
df = pd.concat(df_parts)
|
| 291 |
+
df.set_index("sha256", inplace=True)
|
| 292 |
+
metadata.update(df, overwrite=True)
|
| 293 |
+
for f in df_files:
|
| 294 |
+
shutil.move(
|
| 295 |
+
os.path.join(opt.output_dir, f),
|
| 296 |
+
os.path.join(opt.output_dir, "merged_records", f"{timestamp}_{f}"),
|
| 297 |
+
)
|
| 298 |
+
|
| 299 |
+
# merge latents
|
| 300 |
+
for model in latent_models:
|
| 301 |
+
df_files = [
|
| 302 |
+
f
|
| 303 |
+
for f in os.listdir(opt.output_dir)
|
| 304 |
+
if f.startswith(f"latent_{model}_") and f.endswith(".csv")
|
| 305 |
+
]
|
| 306 |
+
df_parts = []
|
| 307 |
+
for f in df_files:
|
| 308 |
+
try:
|
| 309 |
+
df_parts.append(pd.read_csv(os.path.join(opt.output_dir, f)))
|
| 310 |
+
except:
|
| 311 |
+
pass
|
| 312 |
+
if len(df_parts) > 0:
|
| 313 |
+
df = pd.concat(df_parts)
|
| 314 |
+
df.set_index("sha256", inplace=True)
|
| 315 |
+
metadata.update(df, overwrite=True)
|
| 316 |
+
for f in df_files:
|
| 317 |
+
shutil.move(
|
| 318 |
+
os.path.join(opt.output_dir, f),
|
| 319 |
+
os.path.join(opt.output_dir, "merged_records", f"{timestamp}_{f}"),
|
| 320 |
+
)
|
| 321 |
+
|
| 322 |
+
# merge sparse structure latents
|
| 323 |
+
for model in ss_latent_models:
|
| 324 |
+
df_files = [
|
| 325 |
+
f
|
| 326 |
+
for f in os.listdir(opt.output_dir)
|
| 327 |
+
if f.startswith(f"ss_latent_{model}_") and f.endswith(".csv")
|
| 328 |
+
]
|
| 329 |
+
df_parts = []
|
| 330 |
+
for f in df_files:
|
| 331 |
+
try:
|
| 332 |
+
df_parts.append(pd.read_csv(os.path.join(opt.output_dir, f)))
|
| 333 |
+
except:
|
| 334 |
+
pass
|
| 335 |
+
if len(df_parts) > 0:
|
| 336 |
+
df = pd.concat(df_parts)
|
| 337 |
+
df.set_index("sha256", inplace=True)
|
| 338 |
+
metadata.update(df, overwrite=True)
|
| 339 |
+
for f in df_files:
|
| 340 |
+
shutil.move(
|
| 341 |
+
os.path.join(opt.output_dir, f),
|
| 342 |
+
os.path.join(opt.output_dir, "merged_records", f"{timestamp}_{f}"),
|
| 343 |
+
)
|
| 344 |
+
|
| 345 |
+
# build metadata from files
|
| 346 |
+
if opt.from_file:
|
| 347 |
+
with (
|
| 348 |
+
ThreadPoolExecutor(max_workers=os.cpu_count()) as executor,
|
| 349 |
+
tqdm(total=len(metadata), desc="Building metadata") as pbar,
|
| 350 |
+
):
|
| 351 |
+
|
| 352 |
+
def worker(sha256):
|
| 353 |
+
try:
|
| 354 |
+
if (
|
| 355 |
+
need_process("rendered")
|
| 356 |
+
and metadata.loc[sha256, "rendered"] == False
|
| 357 |
+
and os.path.exists(
|
| 358 |
+
os.path.join(
|
| 359 |
+
opt.output_dir, "renders", sha256, "transforms.json"
|
| 360 |
+
)
|
| 361 |
+
)
|
| 362 |
+
):
|
| 363 |
+
metadata.loc[sha256, "rendered"] = True
|
| 364 |
+
if (
|
| 365 |
+
need_process("voxelized")
|
| 366 |
+
and metadata.loc[sha256, "rendered"] == True
|
| 367 |
+
and metadata.loc[sha256, "voxelized"] == False
|
| 368 |
+
and os.path.exists(
|
| 369 |
+
os.path.join(opt.output_dir, "voxels", f"{sha256}.ply")
|
| 370 |
+
)
|
| 371 |
+
):
|
| 372 |
+
try:
|
| 373 |
+
pts = utils3d.io.read_ply(
|
| 374 |
+
os.path.join(opt.output_dir, "voxels", f"{sha256}.ply")
|
| 375 |
+
)[0]
|
| 376 |
+
metadata.loc[sha256, "voxelized"] = True
|
| 377 |
+
metadata.loc[sha256, "num_voxels"] = len(pts)
|
| 378 |
+
except Exception as e:
|
| 379 |
+
pass
|
| 380 |
+
if (
|
| 381 |
+
need_process("cond_rendered")
|
| 382 |
+
and metadata.loc[sha256, "cond_rendered"] == False
|
| 383 |
+
and os.path.exists(
|
| 384 |
+
os.path.join(
|
| 385 |
+
opt.output_dir,
|
| 386 |
+
"renders_cond",
|
| 387 |
+
sha256,
|
| 388 |
+
"transforms.json",
|
| 389 |
+
)
|
| 390 |
+
)
|
| 391 |
+
):
|
| 392 |
+
metadata.loc[sha256, "cond_rendered"] = True
|
| 393 |
+
for model in image_models:
|
| 394 |
+
if (
|
| 395 |
+
need_process(f"feature_{model}")
|
| 396 |
+
and metadata.loc[sha256, f"feature_{model}"] == False
|
| 397 |
+
and metadata.loc[sha256, "rendered"] == True
|
| 398 |
+
and metadata.loc[sha256, "voxelized"] == True
|
| 399 |
+
and os.path.exists(
|
| 400 |
+
os.path.join(
|
| 401 |
+
opt.output_dir, "features", model, f"{sha256}.npz"
|
| 402 |
+
)
|
| 403 |
+
)
|
| 404 |
+
):
|
| 405 |
+
metadata.loc[sha256, f"feature_{model}"] = True
|
| 406 |
+
for model in latent_models:
|
| 407 |
+
if (
|
| 408 |
+
need_process(f"latent_{model}")
|
| 409 |
+
and metadata.loc[sha256, f"latent_{model}"] == False
|
| 410 |
+
and metadata.loc[sha256, "rendered"] == True
|
| 411 |
+
and metadata.loc[sha256, "voxelized"] == True
|
| 412 |
+
and os.path.exists(
|
| 413 |
+
os.path.join(
|
| 414 |
+
opt.output_dir, "latents", model, f"{sha256}.npz"
|
| 415 |
+
)
|
| 416 |
+
)
|
| 417 |
+
):
|
| 418 |
+
metadata.loc[sha256, f"latent_{model}"] = True
|
| 419 |
+
for model in ss_latent_models:
|
| 420 |
+
if (
|
| 421 |
+
need_process(f"ss_latent_{model}")
|
| 422 |
+
and metadata.loc[sha256, f"ss_latent_{model}"] == False
|
| 423 |
+
and metadata.loc[sha256, "voxelized"] == True
|
| 424 |
+
and os.path.exists(
|
| 425 |
+
os.path.join(
|
| 426 |
+
opt.output_dir, "ss_latents", model, f"{sha256}.npz"
|
| 427 |
+
)
|
| 428 |
+
)
|
| 429 |
+
):
|
| 430 |
+
metadata.loc[sha256, f"ss_latent_{model}"] = True
|
| 431 |
+
pbar.update()
|
| 432 |
+
except Exception as e:
|
| 433 |
+
print(f"Error processing {sha256}: {e}")
|
| 434 |
+
pbar.update()
|
| 435 |
+
|
| 436 |
+
executor.map(worker, metadata.index)
|
| 437 |
+
executor.shutdown(wait=True)
|
| 438 |
+
|
| 439 |
+
# Save dataset splits if we have split information
|
| 440 |
+
if "split" in metadata.columns:
|
| 441 |
+
os.makedirs(os.path.join(opt.output_dir, "splits"), exist_ok=True)
|
| 442 |
+
# Reset index to include sha256 in the exported files
|
| 443 |
+
metadata_export = metadata.reset_index()
|
| 444 |
+
for split in ["train", "val", "test"]:
|
| 445 |
+
split_df = metadata_export[metadata_export["split"] == split]
|
| 446 |
+
if not split_df.empty:
|
| 447 |
+
split_df.to_csv(
|
| 448 |
+
os.path.join(opt.output_dir, "splits", f"{split}.csv"), index=False
|
| 449 |
+
)
|
| 450 |
+
|
| 451 |
+
# statistics
|
| 452 |
+
metadata.to_csv(os.path.join(opt.output_dir, "metadata.csv"))
|
| 453 |
+
num_downloaded = (
|
| 454 |
+
metadata["local_path"].count() if "local_path" in metadata.columns else 0
|
| 455 |
+
)
|
| 456 |
+
|
| 457 |
+
# If from_file is True, update metadata to reflect actual files on disk before writing statistics
|
| 458 |
+
if opt.from_file:
|
| 459 |
+
print("Updating metadata to reflect actual files on disk...")
|
| 460 |
+
for model in image_models:
|
| 461 |
+
for sha256 in metadata.index:
|
| 462 |
+
actual_exists = os.path.exists(
|
| 463 |
+
os.path.join(opt.output_dir, "features", model, f"{sha256}.npz")
|
| 464 |
+
)
|
| 465 |
+
metadata.loc[sha256, f"feature_{model}"] = actual_exists
|
| 466 |
+
|
| 467 |
+
for model in latent_models:
|
| 468 |
+
for sha256 in metadata.index:
|
| 469 |
+
actual_exists = os.path.exists(
|
| 470 |
+
os.path.join(opt.output_dir, "latents", model, f"{sha256}.npz")
|
| 471 |
+
)
|
| 472 |
+
metadata.loc[sha256, f"latent_{model}"] = actual_exists
|
| 473 |
+
|
| 474 |
+
for model in ss_latent_models:
|
| 475 |
+
for sha256 in metadata.index:
|
| 476 |
+
actual_exists = os.path.exists(
|
| 477 |
+
os.path.join(opt.output_dir, "ss_latents", model, f"{sha256}.npz")
|
| 478 |
+
)
|
| 479 |
+
metadata.loc[sha256, f"ss_latent_{model}"] = actual_exists
|
| 480 |
+
|
| 481 |
+
# Save updated metadata
|
| 482 |
+
metadata.to_csv(os.path.join(opt.output_dir, "metadata.csv"))
|
| 483 |
+
|
| 484 |
+
with open(os.path.join(opt.output_dir, "statistics.txt"), "w") as f:
|
| 485 |
+
f.write("Statistics:\n")
|
| 486 |
+
f.write(f" - Number of assets: {len(metadata)}\n")
|
| 487 |
+
f.write(f" - Number of assets downloaded: {num_downloaded}\n")
|
| 488 |
+
f.write(f' - Number of assets rendered: {metadata["rendered"].sum()}\n')
|
| 489 |
+
f.write(f' - Number of assets voxelized: {metadata["voxelized"].sum()}\n')
|
| 490 |
+
if len(image_models) != 0:
|
| 491 |
+
f.write(f" - Number of assets with image features extracted:\n")
|
| 492 |
+
for model in image_models:
|
| 493 |
+
# Always use metadata counts since they're now accurate when from_file=True
|
| 494 |
+
f.write(f' - {model}: {metadata[f"feature_{model}"].sum()}\n')
|
| 495 |
+
if len(latent_models) != 0:
|
| 496 |
+
f.write(f" - Number of assets with latents extracted:\n")
|
| 497 |
+
for model in latent_models:
|
| 498 |
+
f.write(f' - {model}: {metadata[f"latent_{model}"].sum()}\n')
|
| 499 |
+
if len(ss_latent_models) != 0:
|
| 500 |
+
f.write(f" - Number of assets with sparse structure latents extracted:\n")
|
| 501 |
+
for model in ss_latent_models:
|
| 502 |
+
f.write(f' - {model}: {metadata[f"ss_latent_{model}"].sum()}\n')
|
| 503 |
+
|
| 504 |
+
# Only report captions if the column exists (it may not for Gaussian splats)
|
| 505 |
+
if "captions" in metadata.columns:
|
| 506 |
+
f.write(
|
| 507 |
+
f' - Number of assets with captions: {metadata["captions"].count()}\n'
|
| 508 |
+
)
|
| 509 |
+
else:
|
| 510 |
+
f.write(
|
| 511 |
+
f" - Number of assets with captions: N/A (no caption data available)\n"
|
| 512 |
+
)
|
| 513 |
+
|
| 514 |
+
f.write(
|
| 515 |
+
f' - Number of assets with image conditions: {metadata["cond_rendered"].sum()}\n'
|
| 516 |
+
)
|
| 517 |
+
|
| 518 |
+
# Add class distribution statistics
|
| 519 |
+
if "class" in metadata.columns:
|
| 520 |
+
f.write("\nClass distribution:\n")
|
| 521 |
+
class_counts = metadata["class"].value_counts()
|
| 522 |
+
for class_name, count in class_counts.items():
|
| 523 |
+
f.write(f" - {class_name}: {count} ({count/len(metadata)*100:.1f}%)\n")
|
| 524 |
+
|
| 525 |
+
# Add split statistics if split column exists
|
| 526 |
+
if "split" in metadata.columns:
|
| 527 |
+
f.write("\nDataset splits:\n")
|
| 528 |
+
split_counts = metadata["split"].value_counts()
|
| 529 |
+
for split_name, count in split_counts.items():
|
| 530 |
+
f.write(f" - {split_name}: {count} ({count/len(metadata)*100:.1f}%)\n")
|
| 531 |
+
|
| 532 |
+
# Add class distribution per split if both columns exist
|
| 533 |
+
if "class" in metadata.columns:
|
| 534 |
+
f.write("\nClass distribution per split:\n")
|
| 535 |
+
# Reset index to allow cross-tabulation
|
| 536 |
+
metadata_reset = metadata.reset_index()
|
| 537 |
+
# For each split, show class distribution
|
| 538 |
+
for split_name in ["train", "val", "test"]:
|
| 539 |
+
if split_name in split_counts:
|
| 540 |
+
f.write(f" {split_name.upper()}:\n")
|
| 541 |
+
split_data = metadata_reset[
|
| 542 |
+
metadata_reset["split"] == split_name
|
| 543 |
+
]
|
| 544 |
+
class_in_split = split_data["class"].value_counts()
|
| 545 |
+
for class_name, count in class_in_split.items():
|
| 546 |
+
f.write(
|
| 547 |
+
f" - {class_name}: {count} ({count/len(split_data)*100:.1f}%)\n"
|
| 548 |
+
)
|
| 549 |
+
|
| 550 |
+
with open(os.path.join(opt.output_dir, "statistics.txt"), "r") as f:
|
| 551 |
+
print(f.read())
|
deps/vomp/dataset_toolkits/datasets/ABO.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
|
| 16 |
+
import os
|
| 17 |
+
import re
|
| 18 |
+
import argparse
|
| 19 |
+
import tarfile
|
| 20 |
+
from concurrent.futures import ThreadPoolExecutor
|
| 21 |
+
from tqdm import tqdm
|
| 22 |
+
import pandas as pd
|
| 23 |
+
from utils import get_file_hash
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def add_args(parser: argparse.ArgumentParser):
|
| 27 |
+
pass
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def get_metadata(**kwargs):
|
| 31 |
+
metadata = pd.read_csv("hf://datasets/JeffreyXiang/TRELLIS-500K/ABO.csv")
|
| 32 |
+
return metadata
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def download(metadata, output_dir, **kwargs):
|
| 36 |
+
os.makedirs(os.path.join(output_dir, "raw"), exist_ok=True)
|
| 37 |
+
|
| 38 |
+
if not os.path.exists(os.path.join(output_dir, "raw", "abo-3dmodels.tar")):
|
| 39 |
+
try:
|
| 40 |
+
os.makedirs(os.path.join(output_dir, "raw"), exist_ok=True)
|
| 41 |
+
os.system(
|
| 42 |
+
f"wget -O {output_dir}/raw/abo-3dmodels.tar https://amazon-berkeley-objects.s3.amazonaws.com/archives/abo-3dmodels.tar"
|
| 43 |
+
)
|
| 44 |
+
except:
|
| 45 |
+
print("\033[93m")
|
| 46 |
+
print(
|
| 47 |
+
"Error downloading ABO dataset. Please check your internet connection and try again."
|
| 48 |
+
)
|
| 49 |
+
print(
|
| 50 |
+
"Or, you can manually download the abo-3dmodels.tar file and place it in the {output_dir}/raw directory"
|
| 51 |
+
)
|
| 52 |
+
print(
|
| 53 |
+
"Visit https://amazon-berkeley-objects.s3.amazonaws.com/index.html for more information"
|
| 54 |
+
)
|
| 55 |
+
print("\033[0m")
|
| 56 |
+
raise FileNotFoundError("Error downloading ABO dataset")
|
| 57 |
+
|
| 58 |
+
downloaded = {}
|
| 59 |
+
metadata = metadata.set_index("file_identifier")
|
| 60 |
+
with tarfile.open(os.path.join(output_dir, "raw", "abo-3dmodels.tar")) as tar:
|
| 61 |
+
with (
|
| 62 |
+
ThreadPoolExecutor(max_workers=1) as executor,
|
| 63 |
+
tqdm(total=len(metadata), desc="Extracting") as pbar,
|
| 64 |
+
):
|
| 65 |
+
|
| 66 |
+
def worker(instance: str) -> str:
|
| 67 |
+
try:
|
| 68 |
+
tar.extract(
|
| 69 |
+
f"3dmodels/original/{instance}",
|
| 70 |
+
path=os.path.join(output_dir, "raw"),
|
| 71 |
+
)
|
| 72 |
+
sha256 = get_file_hash(
|
| 73 |
+
os.path.join(output_dir, "raw/3dmodels/original", instance)
|
| 74 |
+
)
|
| 75 |
+
pbar.update()
|
| 76 |
+
return sha256
|
| 77 |
+
except Exception as e:
|
| 78 |
+
pbar.update()
|
| 79 |
+
print(f"Error extracting for {instance}: {e}")
|
| 80 |
+
return None
|
| 81 |
+
|
| 82 |
+
sha256s = executor.map(worker, metadata.index)
|
| 83 |
+
executor.shutdown(wait=True)
|
| 84 |
+
|
| 85 |
+
for k, sha256 in zip(metadata.index, sha256s):
|
| 86 |
+
if sha256 is not None:
|
| 87 |
+
if sha256 == metadata.loc[k, "sha256"]:
|
| 88 |
+
downloaded[sha256] = os.path.join("raw/3dmodels/original", k)
|
| 89 |
+
else:
|
| 90 |
+
print(f"Error downloading {k}: sha256s do not match")
|
| 91 |
+
|
| 92 |
+
return pd.DataFrame(downloaded.items(), columns=["sha256", "local_path"])
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def foreach_instance(
|
| 96 |
+
metadata, output_dir, func, max_workers=None, desc="Processing objects"
|
| 97 |
+
) -> pd.DataFrame:
|
| 98 |
+
import os
|
| 99 |
+
from concurrent.futures import ThreadPoolExecutor
|
| 100 |
+
from tqdm import tqdm
|
| 101 |
+
|
| 102 |
+
# load metadata
|
| 103 |
+
metadata = metadata.to_dict("records")
|
| 104 |
+
|
| 105 |
+
# processing objects
|
| 106 |
+
records = []
|
| 107 |
+
max_workers = max_workers or os.cpu_count()
|
| 108 |
+
try:
|
| 109 |
+
with (
|
| 110 |
+
ThreadPoolExecutor(max_workers=max_workers) as executor,
|
| 111 |
+
tqdm(total=len(metadata), desc=desc) as pbar,
|
| 112 |
+
):
|
| 113 |
+
|
| 114 |
+
def worker(metadatum):
|
| 115 |
+
try:
|
| 116 |
+
local_path = metadatum["local_path"]
|
| 117 |
+
sha256 = metadatum["sha256"]
|
| 118 |
+
file = os.path.join(output_dir, local_path)
|
| 119 |
+
record = func(file, sha256)
|
| 120 |
+
if record is not None:
|
| 121 |
+
records.append(record)
|
| 122 |
+
pbar.update()
|
| 123 |
+
except Exception as e:
|
| 124 |
+
print(f"Error processing object {sha256}: {e}")
|
| 125 |
+
pbar.update()
|
| 126 |
+
|
| 127 |
+
executor.map(worker, metadata)
|
| 128 |
+
executor.shutdown(wait=True)
|
| 129 |
+
except:
|
| 130 |
+
print("Error happened during processing.")
|
| 131 |
+
|
| 132 |
+
return pd.DataFrame.from_records(records)
|
deps/vomp/dataset_toolkits/datasets/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
|
| 16 |
+
# Dataset modules for TRELLIS preprocessing
|
deps/vomp/dataset_toolkits/datasets/allmats.py
ADDED
|
@@ -0,0 +1,510 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
import os
|
| 18 |
+
import json
|
| 19 |
+
import pandas as pd
|
| 20 |
+
import numpy as np
|
| 21 |
+
import hashlib
|
| 22 |
+
import random
|
| 23 |
+
from glob import glob
|
| 24 |
+
from sklearn.model_selection import train_test_split
|
| 25 |
+
from typing import Dict, List, Optional, Any, Union
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
from dataset_toolkits.material_objects.vlm_annotations.utils.utils import (
|
| 29 |
+
SIMREADY_PROPS_DIR,
|
| 30 |
+
COMMERCIAL_BASE_DIR,
|
| 31 |
+
RESIDENTIAL_BASE_DIR,
|
| 32 |
+
VEGETATION_BASE_DIR,
|
| 33 |
+
SIMREADY_ASSET_CLASS_MAPPING,
|
| 34 |
+
SIMREADY_ASSET_INFO_PATH,
|
| 35 |
+
)
|
| 36 |
+
from dataset_toolkits.material_objects.vlm_annotations.data_subsets import (
|
| 37 |
+
simready,
|
| 38 |
+
commercial,
|
| 39 |
+
vegetation,
|
| 40 |
+
residential,
|
| 41 |
+
common,
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def set_seeds(seed=42):
|
| 46 |
+
random.seed(seed)
|
| 47 |
+
np.random.seed(seed)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def add_args(parser):
|
| 51 |
+
parser.add_argument(
|
| 52 |
+
"--simready_dir",
|
| 53 |
+
type=str,
|
| 54 |
+
default=SIMREADY_PROPS_DIR,
|
| 55 |
+
help="Path to the SimReady props directory",
|
| 56 |
+
)
|
| 57 |
+
parser.add_argument(
|
| 58 |
+
"--commercial_dir",
|
| 59 |
+
type=str,
|
| 60 |
+
default=COMMERCIAL_BASE_DIR,
|
| 61 |
+
help="Path to the Commercial models directory",
|
| 62 |
+
)
|
| 63 |
+
parser.add_argument(
|
| 64 |
+
"--residential_dir",
|
| 65 |
+
type=str,
|
| 66 |
+
default=RESIDENTIAL_BASE_DIR,
|
| 67 |
+
help="Path to the Residential models directory",
|
| 68 |
+
)
|
| 69 |
+
parser.add_argument(
|
| 70 |
+
"--vegetation_dir",
|
| 71 |
+
type=str,
|
| 72 |
+
default=VEGETATION_BASE_DIR,
|
| 73 |
+
help="Path to the Vegetation models directory",
|
| 74 |
+
)
|
| 75 |
+
parser.add_argument(
|
| 76 |
+
"--asset_info_path",
|
| 77 |
+
type=str,
|
| 78 |
+
default=SIMREADY_ASSET_INFO_PATH,
|
| 79 |
+
help="Path to the SimReady asset_info.json file",
|
| 80 |
+
)
|
| 81 |
+
parser.add_argument(
|
| 82 |
+
"--seed",
|
| 83 |
+
type=int,
|
| 84 |
+
default=42,
|
| 85 |
+
help="Random seed for reproducibility",
|
| 86 |
+
)
|
| 87 |
+
parser.add_argument(
|
| 88 |
+
"--default_class",
|
| 89 |
+
type=str,
|
| 90 |
+
default="unknown",
|
| 91 |
+
help="Default class label to use when class information is not available",
|
| 92 |
+
)
|
| 93 |
+
parser.add_argument(
|
| 94 |
+
"--include_datasets",
|
| 95 |
+
type=str,
|
| 96 |
+
default="simready,commercial,residential,vegetation",
|
| 97 |
+
help="Comma-separated list of datasets to include",
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def split_dataset(metadata, seed=42):
|
| 102 |
+
|
| 103 |
+
np.random.seed(seed)
|
| 104 |
+
random.seed(seed)
|
| 105 |
+
|
| 106 |
+
metadata_copy = metadata.copy()
|
| 107 |
+
|
| 108 |
+
metadata_copy["split"] = "train"
|
| 109 |
+
|
| 110 |
+
classes = metadata_copy["class"].unique()
|
| 111 |
+
|
| 112 |
+
large_classes = []
|
| 113 |
+
small_classes = []
|
| 114 |
+
|
| 115 |
+
for cls in classes:
|
| 116 |
+
|
| 117 |
+
cls_indices = metadata_copy[metadata_copy["class"] == cls].index.tolist()
|
| 118 |
+
if len(cls_indices) >= 10:
|
| 119 |
+
large_classes.append(cls)
|
| 120 |
+
else:
|
| 121 |
+
small_classes.append(cls)
|
| 122 |
+
|
| 123 |
+
for cls in large_classes:
|
| 124 |
+
cls_indices = metadata_copy[metadata_copy["class"] == cls].index.tolist()
|
| 125 |
+
random.shuffle(cls_indices)
|
| 126 |
+
|
| 127 |
+
n_samples = len(cls_indices)
|
| 128 |
+
n_train = int(0.8 * n_samples)
|
| 129 |
+
n_val = int(0.1 * n_samples)
|
| 130 |
+
|
| 131 |
+
train_indices = cls_indices[:n_train]
|
| 132 |
+
val_indices = cls_indices[n_train : n_train + n_val]
|
| 133 |
+
test_indices = cls_indices[n_train + n_val :]
|
| 134 |
+
|
| 135 |
+
metadata_copy.loc[train_indices, "split"] = "train"
|
| 136 |
+
metadata_copy.loc[val_indices, "split"] = "val"
|
| 137 |
+
metadata_copy.loc[test_indices, "split"] = "test"
|
| 138 |
+
|
| 139 |
+
total_samples = len(metadata_copy)
|
| 140 |
+
goal_train = int(0.8 * total_samples)
|
| 141 |
+
goal_val = int(0.1 * total_samples)
|
| 142 |
+
goal_test = total_samples - goal_train - goal_val
|
| 143 |
+
|
| 144 |
+
current_train = (metadata_copy["split"] == "train").sum()
|
| 145 |
+
current_val = (metadata_copy["split"] == "val").sum()
|
| 146 |
+
current_test = (metadata_copy["split"] == "test").sum()
|
| 147 |
+
|
| 148 |
+
small_indices = []
|
| 149 |
+
for cls in small_classes:
|
| 150 |
+
cls_indices = metadata_copy[metadata_copy["class"] == cls].index.tolist()
|
| 151 |
+
small_indices.extend(cls_indices)
|
| 152 |
+
|
| 153 |
+
random.shuffle(small_indices)
|
| 154 |
+
|
| 155 |
+
need_train = max(0, goal_train - current_train)
|
| 156 |
+
need_val = max(0, goal_val - current_val)
|
| 157 |
+
need_test = max(0, goal_test - current_test)
|
| 158 |
+
|
| 159 |
+
idx = 0
|
| 160 |
+
while idx < len(small_indices):
|
| 161 |
+
if need_train > 0:
|
| 162 |
+
metadata_copy.loc[small_indices[idx], "split"] = "train"
|
| 163 |
+
need_train -= 1
|
| 164 |
+
idx += 1
|
| 165 |
+
elif need_val > 0:
|
| 166 |
+
metadata_copy.loc[small_indices[idx], "split"] = "val"
|
| 167 |
+
need_val -= 1
|
| 168 |
+
idx += 1
|
| 169 |
+
elif need_test > 0:
|
| 170 |
+
metadata_copy.loc[small_indices[idx], "split"] = "test"
|
| 171 |
+
need_test -= 1
|
| 172 |
+
idx += 1
|
| 173 |
+
else:
|
| 174 |
+
|
| 175 |
+
metadata_copy.loc[small_indices[idx:], "split"] = "train"
|
| 176 |
+
break
|
| 177 |
+
|
| 178 |
+
train_count = (metadata_copy["split"] == "train").sum()
|
| 179 |
+
val_count = (metadata_copy["split"] == "val").sum()
|
| 180 |
+
test_count = (metadata_copy["split"] == "test").sum()
|
| 181 |
+
|
| 182 |
+
print(
|
| 183 |
+
f"Dataset split: Train: {train_count} ({train_count/len(metadata_copy)*100:.1f}%), "
|
| 184 |
+
f"Val: {val_count} ({val_count/len(metadata_copy)*100:.1f}%), "
|
| 185 |
+
f"Test: {test_count} ({test_count/len(metadata_copy)*100:.1f}%)"
|
| 186 |
+
)
|
| 187 |
+
|
| 188 |
+
if small_classes:
|
| 189 |
+
print("\nSmall class distribution across splits:")
|
| 190 |
+
for cls in small_classes:
|
| 191 |
+
cls_data = metadata_copy[metadata_copy["class"] == cls]
|
| 192 |
+
cls_train = (cls_data["split"] == "train").sum()
|
| 193 |
+
cls_val = (cls_data["split"] == "val").sum()
|
| 194 |
+
cls_test = (cls_data["split"] == "test").sum()
|
| 195 |
+
cls_total = len(cls_data)
|
| 196 |
+
print(
|
| 197 |
+
f" - {cls} (total {cls_total}): Train: {cls_train}, Val: {cls_val}, Test: {cls_test}"
|
| 198 |
+
)
|
| 199 |
+
|
| 200 |
+
return metadata_copy
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
def get_simready_metadata(simready_dir, asset_info_path, default_class="unknown"):
|
| 204 |
+
|
| 205 |
+
asset_class_mapping = SIMREADY_ASSET_CLASS_MAPPING
|
| 206 |
+
|
| 207 |
+
if not asset_class_mapping and asset_info_path and os.path.exists(asset_info_path):
|
| 208 |
+
try:
|
| 209 |
+
with open(asset_info_path, "r") as f:
|
| 210 |
+
asset_info = json.load(f)
|
| 211 |
+
|
| 212 |
+
asset_class_mapping = {}
|
| 213 |
+
for asset in asset_info:
|
| 214 |
+
simple_name = asset.get("Simple Name")
|
| 215 |
+
if simple_name and "Labels" in asset and "Class" in asset["Labels"]:
|
| 216 |
+
asset_class_mapping[simple_name] = asset["Labels"]["Class"]
|
| 217 |
+
|
| 218 |
+
print(f"Loaded class information for {len(asset_class_mapping)} assets")
|
| 219 |
+
except Exception as e:
|
| 220 |
+
print(f"Error loading asset info: {e}")
|
| 221 |
+
|
| 222 |
+
prop_dirs = []
|
| 223 |
+
if os.path.exists(simready_dir):
|
| 224 |
+
prop_dirs = [
|
| 225 |
+
d
|
| 226 |
+
for d in os.listdir(simready_dir)
|
| 227 |
+
if os.path.isdir(os.path.join(simready_dir, d))
|
| 228 |
+
]
|
| 229 |
+
|
| 230 |
+
metadata = []
|
| 231 |
+
|
| 232 |
+
for prop_name in prop_dirs:
|
| 233 |
+
prop_dir = os.path.join(simready_dir, prop_name)
|
| 234 |
+
|
| 235 |
+
usd_files = glob(os.path.join(prop_dir, "*.usd"))
|
| 236 |
+
if not usd_files:
|
| 237 |
+
continue
|
| 238 |
+
|
| 239 |
+
inst_base_files = [f for f in usd_files if "_inst_base.usd" in f]
|
| 240 |
+
base_files = [f for f in usd_files if "_base.usd" in f]
|
| 241 |
+
|
| 242 |
+
if inst_base_files:
|
| 243 |
+
usd_file = inst_base_files[0]
|
| 244 |
+
elif base_files:
|
| 245 |
+
usd_file = base_files[0]
|
| 246 |
+
else:
|
| 247 |
+
usd_file = usd_files[0]
|
| 248 |
+
|
| 249 |
+
sha256 = hashlib.sha256(prop_name.encode()).hexdigest()
|
| 250 |
+
|
| 251 |
+
prop_class = asset_class_mapping.get(prop_name, default_class)
|
| 252 |
+
|
| 253 |
+
metadata.append(
|
| 254 |
+
{
|
| 255 |
+
"sha256": sha256,
|
| 256 |
+
"local_path": usd_file,
|
| 257 |
+
"original_name": prop_name,
|
| 258 |
+
"aesthetic_score": 1.0,
|
| 259 |
+
"rendered": False,
|
| 260 |
+
"class": prop_class,
|
| 261 |
+
"dataset": "simready",
|
| 262 |
+
}
|
| 263 |
+
)
|
| 264 |
+
|
| 265 |
+
return metadata
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
def get_commercial_metadata(commercial_dir, default_class="commercial"):
|
| 269 |
+
metadata = []
|
| 270 |
+
|
| 271 |
+
if not os.path.exists(commercial_dir):
|
| 272 |
+
print(f"Commercial directory not found: {commercial_dir}")
|
| 273 |
+
return metadata
|
| 274 |
+
|
| 275 |
+
for root, _, files in os.walk(commercial_dir):
|
| 276 |
+
for file in files:
|
| 277 |
+
if file.endswith(".usd") and not os.path.basename(root).startswith("."):
|
| 278 |
+
usd_file = os.path.join(root, file)
|
| 279 |
+
|
| 280 |
+
object_name = os.path.basename(os.path.dirname(usd_file))
|
| 281 |
+
|
| 282 |
+
sha256 = hashlib.sha256(f"{object_name}_{file}".encode()).hexdigest()
|
| 283 |
+
|
| 284 |
+
try:
|
| 285 |
+
material_info = common.extract_materials_from_usd(
|
| 286 |
+
usd_file, "commercial"
|
| 287 |
+
)
|
| 288 |
+
category = material_info.get("category", default_class)
|
| 289 |
+
except Exception:
|
| 290 |
+
category = default_class
|
| 291 |
+
|
| 292 |
+
metadata.append(
|
| 293 |
+
{
|
| 294 |
+
"sha256": sha256,
|
| 295 |
+
"local_path": usd_file,
|
| 296 |
+
"original_name": f"{object_name}/{file}",
|
| 297 |
+
"aesthetic_score": 1.0,
|
| 298 |
+
"rendered": False,
|
| 299 |
+
"class": category,
|
| 300 |
+
"dataset": "commercial",
|
| 301 |
+
}
|
| 302 |
+
)
|
| 303 |
+
|
| 304 |
+
return metadata
|
| 305 |
+
|
| 306 |
+
|
| 307 |
+
def get_residential_metadata(residential_dir, default_class="residential"):
|
| 308 |
+
metadata = []
|
| 309 |
+
|
| 310 |
+
if not os.path.exists(residential_dir):
|
| 311 |
+
print(f"Residential directory not found: {residential_dir}")
|
| 312 |
+
return metadata
|
| 313 |
+
|
| 314 |
+
for root, _, files in os.walk(residential_dir):
|
| 315 |
+
for file in files:
|
| 316 |
+
if file.endswith(".usd") and not os.path.basename(root).startswith("."):
|
| 317 |
+
usd_file = os.path.join(root, file)
|
| 318 |
+
|
| 319 |
+
object_name = os.path.basename(os.path.dirname(usd_file))
|
| 320 |
+
|
| 321 |
+
sha256 = hashlib.sha256(f"{object_name}_{file}".encode()).hexdigest()
|
| 322 |
+
|
| 323 |
+
try:
|
| 324 |
+
material_info = common.extract_materials_from_usd(
|
| 325 |
+
usd_file, "residential"
|
| 326 |
+
)
|
| 327 |
+
category = material_info.get("category", default_class)
|
| 328 |
+
except Exception:
|
| 329 |
+
category = default_class
|
| 330 |
+
|
| 331 |
+
metadata.append(
|
| 332 |
+
{
|
| 333 |
+
"sha256": sha256,
|
| 334 |
+
"local_path": usd_file,
|
| 335 |
+
"original_name": f"{object_name}/{file}",
|
| 336 |
+
"aesthetic_score": 1.0,
|
| 337 |
+
"rendered": False,
|
| 338 |
+
"class": category,
|
| 339 |
+
"dataset": "residential",
|
| 340 |
+
}
|
| 341 |
+
)
|
| 342 |
+
|
| 343 |
+
return metadata
|
| 344 |
+
|
| 345 |
+
|
| 346 |
+
def get_vegetation_metadata(vegetation_dir, default_class="vegetation"):
|
| 347 |
+
metadata = []
|
| 348 |
+
|
| 349 |
+
if not os.path.exists(vegetation_dir):
|
| 350 |
+
print(f"Vegetation directory not found: {vegetation_dir}")
|
| 351 |
+
return metadata
|
| 352 |
+
|
| 353 |
+
for root, _, files in os.walk(vegetation_dir):
|
| 354 |
+
for file in files:
|
| 355 |
+
if file.endswith(".usd") and not os.path.basename(root).startswith("."):
|
| 356 |
+
usd_file = os.path.join(root, file)
|
| 357 |
+
|
| 358 |
+
object_name = os.path.basename(os.path.dirname(usd_file))
|
| 359 |
+
|
| 360 |
+
sha256 = hashlib.sha256(f"{object_name}_{file}".encode()).hexdigest()
|
| 361 |
+
|
| 362 |
+
try:
|
| 363 |
+
material_info = common.extract_materials_from_usd(
|
| 364 |
+
usd_file, "vegetation"
|
| 365 |
+
)
|
| 366 |
+
category = material_info.get("category", default_class)
|
| 367 |
+
except Exception:
|
| 368 |
+
category = default_class
|
| 369 |
+
|
| 370 |
+
metadata.append(
|
| 371 |
+
{
|
| 372 |
+
"sha256": sha256,
|
| 373 |
+
"local_path": usd_file,
|
| 374 |
+
"original_name": f"{object_name}/{file}",
|
| 375 |
+
"aesthetic_score": 1.0,
|
| 376 |
+
"rendered": False,
|
| 377 |
+
"class": category,
|
| 378 |
+
"dataset": "vegetation",
|
| 379 |
+
}
|
| 380 |
+
)
|
| 381 |
+
|
| 382 |
+
return metadata
|
| 383 |
+
|
| 384 |
+
|
| 385 |
+
def get_metadata(
|
| 386 |
+
simready_dir=None,
|
| 387 |
+
commercial_dir=None,
|
| 388 |
+
residential_dir=None,
|
| 389 |
+
vegetation_dir=None,
|
| 390 |
+
output_dir=None,
|
| 391 |
+
asset_info_path=None,
|
| 392 |
+
include_datasets="simready,commercial,residential,vegetation",
|
| 393 |
+
seed=42,
|
| 394 |
+
default_class="unknown",
|
| 395 |
+
skip_split=False,
|
| 396 |
+
**kwargs,
|
| 397 |
+
):
|
| 398 |
+
|
| 399 |
+
set_seeds(seed)
|
| 400 |
+
|
| 401 |
+
if simready_dir is None:
|
| 402 |
+
simready_dir = SIMREADY_PROPS_DIR
|
| 403 |
+
if commercial_dir is None:
|
| 404 |
+
commercial_dir = COMMERCIAL_BASE_DIR
|
| 405 |
+
if residential_dir is None:
|
| 406 |
+
residential_dir = RESIDENTIAL_BASE_DIR
|
| 407 |
+
if vegetation_dir is None:
|
| 408 |
+
vegetation_dir = VEGETATION_BASE_DIR
|
| 409 |
+
if asset_info_path is None:
|
| 410 |
+
asset_info_path = SIMREADY_ASSET_INFO_PATH
|
| 411 |
+
|
| 412 |
+
datasets = [d.strip() for d in include_datasets.split(",")]
|
| 413 |
+
|
| 414 |
+
metadata = []
|
| 415 |
+
|
| 416 |
+
if "simready" in datasets:
|
| 417 |
+
print(f"Processing SimReady dataset from {simready_dir}")
|
| 418 |
+
simready_metadata = get_simready_metadata(
|
| 419 |
+
simready_dir, asset_info_path, default_class
|
| 420 |
+
)
|
| 421 |
+
metadata.extend(simready_metadata)
|
| 422 |
+
print(f"Added {len(simready_metadata)} items from SimReady dataset")
|
| 423 |
+
|
| 424 |
+
if "commercial" in datasets:
|
| 425 |
+
print(f"Processing Commercial dataset from {commercial_dir}")
|
| 426 |
+
commercial_metadata = get_commercial_metadata(commercial_dir)
|
| 427 |
+
metadata.extend(commercial_metadata)
|
| 428 |
+
print(f"Added {len(commercial_metadata)} items from Commercial dataset")
|
| 429 |
+
|
| 430 |
+
if "residential" in datasets:
|
| 431 |
+
print(f"Processing Residential dataset from {residential_dir}")
|
| 432 |
+
residential_metadata = get_residential_metadata(residential_dir)
|
| 433 |
+
metadata.extend(residential_metadata)
|
| 434 |
+
print(f"Added {len(residential_metadata)} items from Residential dataset")
|
| 435 |
+
|
| 436 |
+
if "vegetation" in datasets:
|
| 437 |
+
print(f"Processing Vegetation dataset from {vegetation_dir}")
|
| 438 |
+
vegetation_metadata = get_vegetation_metadata(vegetation_dir)
|
| 439 |
+
metadata.extend(vegetation_metadata)
|
| 440 |
+
print(f"Added {len(vegetation_metadata)} items from Vegetation dataset")
|
| 441 |
+
|
| 442 |
+
df = pd.DataFrame(metadata)
|
| 443 |
+
|
| 444 |
+
if df.empty:
|
| 445 |
+
print("Warning: No metadata collected from any dataset")
|
| 446 |
+
return df
|
| 447 |
+
|
| 448 |
+
class_counts = df["class"].value_counts()
|
| 449 |
+
print("\nClass distribution in combined dataset:")
|
| 450 |
+
for class_name, count in class_counts.items():
|
| 451 |
+
print(f" - {class_name}: {count} ({count/len(df)*100:.1f}%)")
|
| 452 |
+
|
| 453 |
+
dataset_counts = df["dataset"].value_counts()
|
| 454 |
+
print("\nDataset distribution:")
|
| 455 |
+
for dataset_name, count in dataset_counts.items():
|
| 456 |
+
print(f" - {dataset_name}: {count} ({count/len(df)*100:.1f}%)")
|
| 457 |
+
|
| 458 |
+
if not skip_split:
|
| 459 |
+
df = split_dataset(df, seed=seed)
|
| 460 |
+
else:
|
| 461 |
+
print("Skipping dataset splitting as requested")
|
| 462 |
+
df["split"] = "train"
|
| 463 |
+
|
| 464 |
+
if output_dir:
|
| 465 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 466 |
+
|
| 467 |
+
df.to_csv(os.path.join(output_dir, "metadata.csv"), index=False)
|
| 468 |
+
|
| 469 |
+
splits_dir = os.path.join(output_dir, "splits")
|
| 470 |
+
os.makedirs(splits_dir, exist_ok=True)
|
| 471 |
+
|
| 472 |
+
for split in ["train", "val", "test"]:
|
| 473 |
+
split_df = df[df["split"] == split]
|
| 474 |
+
if not split_df.empty:
|
| 475 |
+
split_df.to_csv(os.path.join(splits_dir, f"{split}.csv"), index=False)
|
| 476 |
+
|
| 477 |
+
class_stats = df.groupby(["class", "split"]).size().unstack(fill_value=0)
|
| 478 |
+
class_stats.to_csv(os.path.join(output_dir, "class_distribution.csv"))
|
| 479 |
+
|
| 480 |
+
dataset_stats = df.groupby(["dataset", "split"]).size().unstack(fill_value=0)
|
| 481 |
+
dataset_stats.to_csv(os.path.join(output_dir, "dataset_distribution.csv"))
|
| 482 |
+
|
| 483 |
+
return df
|
| 484 |
+
|
| 485 |
+
|
| 486 |
+
def foreach_instance(metadata, output_dir, func, max_workers=8, desc="Processing"):
|
| 487 |
+
from concurrent.futures import ThreadPoolExecutor
|
| 488 |
+
from tqdm import tqdm
|
| 489 |
+
import pandas as pd
|
| 490 |
+
|
| 491 |
+
results = []
|
| 492 |
+
|
| 493 |
+
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
| 494 |
+
futures = []
|
| 495 |
+
for _, row in metadata.iterrows():
|
| 496 |
+
sha256 = row["sha256"]
|
| 497 |
+
local_path = row["local_path"]
|
| 498 |
+
dataset = row.get("dataset", "unknown")
|
| 499 |
+
|
| 500 |
+
futures.append(executor.submit(func, local_path, sha256, dataset))
|
| 501 |
+
|
| 502 |
+
for future in tqdm(futures, desc=desc, total=len(futures)):
|
| 503 |
+
try:
|
| 504 |
+
result = future.result()
|
| 505 |
+
if result is not None:
|
| 506 |
+
results.append(result)
|
| 507 |
+
except Exception as e:
|
| 508 |
+
print(f"Error in worker: {e}")
|
| 509 |
+
|
| 510 |
+
return pd.DataFrame.from_records(results)
|
deps/vomp/dataset_toolkits/datasets/simready.py
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
import os
|
| 18 |
+
import json
|
| 19 |
+
import pandas as pd
|
| 20 |
+
import numpy as np
|
| 21 |
+
import hashlib
|
| 22 |
+
import random
|
| 23 |
+
from glob import glob
|
| 24 |
+
from sklearn.model_selection import train_test_split
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def set_seeds(seed=42):
|
| 28 |
+
random.seed(seed)
|
| 29 |
+
np.random.seed(seed)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def add_args(parser):
|
| 33 |
+
parser.add_argument(
|
| 34 |
+
"--simready_dir",
|
| 35 |
+
type=str,
|
| 36 |
+
default="datasets/raw/simready/common_assets/props",
|
| 37 |
+
help="Path to the SimReady props directory",
|
| 38 |
+
)
|
| 39 |
+
parser.add_argument(
|
| 40 |
+
"--asset_info_path",
|
| 41 |
+
type=str,
|
| 42 |
+
default="datasets/raw/simready/asset_info.json",
|
| 43 |
+
help="Path to the SimReady asset_info.json file",
|
| 44 |
+
)
|
| 45 |
+
parser.add_argument(
|
| 46 |
+
"--seed",
|
| 47 |
+
type=int,
|
| 48 |
+
default=42,
|
| 49 |
+
help="Random seed for reproducibility",
|
| 50 |
+
)
|
| 51 |
+
parser.add_argument(
|
| 52 |
+
"--default_class",
|
| 53 |
+
type=str,
|
| 54 |
+
default="unknown",
|
| 55 |
+
help="Default class label to use when class information is not available",
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def get_asset_class_mapping(asset_info_path):
|
| 60 |
+
if not os.path.exists(asset_info_path):
|
| 61 |
+
print(f"Warning: Asset info file not found at {asset_info_path}")
|
| 62 |
+
return {}
|
| 63 |
+
|
| 64 |
+
try:
|
| 65 |
+
with open(asset_info_path, "r") as f:
|
| 66 |
+
asset_info = json.load(f)
|
| 67 |
+
|
| 68 |
+
asset_class_mapping = {}
|
| 69 |
+
for asset in asset_info:
|
| 70 |
+
simple_name = asset.get("Simple Name")
|
| 71 |
+
if simple_name and "Labels" in asset and "Class" in asset["Labels"]:
|
| 72 |
+
asset_class_mapping[simple_name] = asset["Labels"]["Class"]
|
| 73 |
+
|
| 74 |
+
print(f"Loaded class information for {len(asset_class_mapping)} assets")
|
| 75 |
+
return asset_class_mapping
|
| 76 |
+
except Exception as e:
|
| 77 |
+
print(f"Error loading asset info: {e}")
|
| 78 |
+
return {}
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def split_dataset(metadata, seed=42):
|
| 82 |
+
|
| 83 |
+
np.random.seed(seed)
|
| 84 |
+
random.seed(seed)
|
| 85 |
+
|
| 86 |
+
metadata_copy = metadata.copy()
|
| 87 |
+
|
| 88 |
+
metadata_copy["split"] = "train"
|
| 89 |
+
|
| 90 |
+
classes = metadata_copy["class"].unique()
|
| 91 |
+
|
| 92 |
+
large_classes = []
|
| 93 |
+
small_classes = []
|
| 94 |
+
|
| 95 |
+
for cls in classes:
|
| 96 |
+
|
| 97 |
+
cls_indices = metadata_copy[metadata_copy["class"] == cls].index.tolist()
|
| 98 |
+
if len(cls_indices) >= 10:
|
| 99 |
+
large_classes.append(cls)
|
| 100 |
+
else:
|
| 101 |
+
small_classes.append(cls)
|
| 102 |
+
|
| 103 |
+
for cls in large_classes:
|
| 104 |
+
cls_indices = metadata_copy[metadata_copy["class"] == cls].index.tolist()
|
| 105 |
+
random.shuffle(cls_indices)
|
| 106 |
+
|
| 107 |
+
n_samples = len(cls_indices)
|
| 108 |
+
n_train = int(0.8 * n_samples)
|
| 109 |
+
n_val = int(0.1 * n_samples)
|
| 110 |
+
|
| 111 |
+
train_indices = cls_indices[:n_train]
|
| 112 |
+
val_indices = cls_indices[n_train : n_train + n_val]
|
| 113 |
+
test_indices = cls_indices[n_train + n_val :]
|
| 114 |
+
|
| 115 |
+
metadata_copy.loc[train_indices, "split"] = "train"
|
| 116 |
+
metadata_copy.loc[val_indices, "split"] = "val"
|
| 117 |
+
metadata_copy.loc[test_indices, "split"] = "test"
|
| 118 |
+
|
| 119 |
+
total_samples = len(metadata_copy)
|
| 120 |
+
goal_train = int(0.8 * total_samples)
|
| 121 |
+
goal_val = int(0.1 * total_samples)
|
| 122 |
+
goal_test = total_samples - goal_train - goal_val
|
| 123 |
+
|
| 124 |
+
current_train = (metadata_copy["split"] == "train").sum()
|
| 125 |
+
current_val = (metadata_copy["split"] == "val").sum()
|
| 126 |
+
current_test = (metadata_copy["split"] == "test").sum()
|
| 127 |
+
|
| 128 |
+
small_indices = []
|
| 129 |
+
for cls in small_classes:
|
| 130 |
+
cls_indices = metadata_copy[metadata_copy["class"] == cls].index.tolist()
|
| 131 |
+
small_indices.extend(cls_indices)
|
| 132 |
+
|
| 133 |
+
random.shuffle(small_indices)
|
| 134 |
+
|
| 135 |
+
need_train = max(0, goal_train - current_train)
|
| 136 |
+
need_val = max(0, goal_val - current_val)
|
| 137 |
+
need_test = max(0, goal_test - current_test)
|
| 138 |
+
|
| 139 |
+
idx = 0
|
| 140 |
+
while idx < len(small_indices):
|
| 141 |
+
if need_train > 0:
|
| 142 |
+
metadata_copy.loc[small_indices[idx], "split"] = "train"
|
| 143 |
+
need_train -= 1
|
| 144 |
+
idx += 1
|
| 145 |
+
elif need_val > 0:
|
| 146 |
+
metadata_copy.loc[small_indices[idx], "split"] = "val"
|
| 147 |
+
need_val -= 1
|
| 148 |
+
idx += 1
|
| 149 |
+
elif need_test > 0:
|
| 150 |
+
metadata_copy.loc[small_indices[idx], "split"] = "test"
|
| 151 |
+
need_test -= 1
|
| 152 |
+
idx += 1
|
| 153 |
+
else:
|
| 154 |
+
|
| 155 |
+
metadata_copy.loc[small_indices[idx:], "split"] = "train"
|
| 156 |
+
break
|
| 157 |
+
|
| 158 |
+
train_count = (metadata_copy["split"] == "train").sum()
|
| 159 |
+
val_count = (metadata_copy["split"] == "val").sum()
|
| 160 |
+
test_count = (metadata_copy["split"] == "test").sum()
|
| 161 |
+
|
| 162 |
+
print(
|
| 163 |
+
f"Dataset split: Train: {train_count} ({train_count/len(metadata_copy)*100:.1f}%), "
|
| 164 |
+
f"Val: {val_count} ({val_count/len(metadata_copy)*100:.1f}%), "
|
| 165 |
+
f"Test: {test_count} ({test_count/len(metadata_copy)*100:.1f}%)"
|
| 166 |
+
)
|
| 167 |
+
|
| 168 |
+
if small_classes:
|
| 169 |
+
print("\nSmall class distribution across splits:")
|
| 170 |
+
for cls in small_classes:
|
| 171 |
+
cls_data = metadata_copy[metadata_copy["class"] == cls]
|
| 172 |
+
cls_train = (cls_data["split"] == "train").sum()
|
| 173 |
+
cls_val = (cls_data["split"] == "val").sum()
|
| 174 |
+
cls_test = (cls_data["split"] == "test").sum()
|
| 175 |
+
cls_total = len(cls_data)
|
| 176 |
+
print(
|
| 177 |
+
f" - {cls} (total {cls_total}): Train: {cls_train}, Val: {cls_val}, Test: {cls_test}"
|
| 178 |
+
)
|
| 179 |
+
|
| 180 |
+
return metadata_copy
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
def get_metadata(
|
| 184 |
+
simready_dir=None,
|
| 185 |
+
output_dir=None,
|
| 186 |
+
asset_info_path=None,
|
| 187 |
+
seed=42,
|
| 188 |
+
default_class="unknown",
|
| 189 |
+
skip_split=False,
|
| 190 |
+
**kwargs,
|
| 191 |
+
):
|
| 192 |
+
|
| 193 |
+
set_seeds(seed)
|
| 194 |
+
|
| 195 |
+
if simready_dir is None:
|
| 196 |
+
simready_dir = "datasets/raw/simready/common_assets/props"
|
| 197 |
+
|
| 198 |
+
if asset_info_path is None:
|
| 199 |
+
asset_info_path = "datasets/raw/simready/asset_info.json"
|
| 200 |
+
|
| 201 |
+
asset_class_mapping = get_asset_class_mapping(asset_info_path)
|
| 202 |
+
|
| 203 |
+
prop_dirs = [
|
| 204 |
+
d
|
| 205 |
+
for d in os.listdir(simready_dir)
|
| 206 |
+
if os.path.isdir(os.path.join(simready_dir, d))
|
| 207 |
+
]
|
| 208 |
+
|
| 209 |
+
metadata = []
|
| 210 |
+
|
| 211 |
+
for prop_name in prop_dirs:
|
| 212 |
+
prop_dir = os.path.join(simready_dir, prop_name)
|
| 213 |
+
|
| 214 |
+
usd_files = glob(os.path.join(prop_dir, "*.usd"))
|
| 215 |
+
if not usd_files:
|
| 216 |
+
continue
|
| 217 |
+
|
| 218 |
+
inst_base_files = [f for f in usd_files if "_inst_base.usd" in f]
|
| 219 |
+
base_files = [f for f in usd_files if "_base.usd" in f]
|
| 220 |
+
|
| 221 |
+
if inst_base_files:
|
| 222 |
+
usd_file = inst_base_files[0]
|
| 223 |
+
elif base_files:
|
| 224 |
+
usd_file = base_files[0]
|
| 225 |
+
else:
|
| 226 |
+
usd_file = usd_files[0]
|
| 227 |
+
|
| 228 |
+
sha256 = hashlib.sha256(prop_name.encode()).hexdigest()
|
| 229 |
+
|
| 230 |
+
prop_class = asset_class_mapping.get(prop_name, default_class)
|
| 231 |
+
|
| 232 |
+
metadata.append(
|
| 233 |
+
{
|
| 234 |
+
"sha256": sha256,
|
| 235 |
+
"local_path": usd_file,
|
| 236 |
+
"original_name": prop_name,
|
| 237 |
+
"aesthetic_score": 1.0,
|
| 238 |
+
"rendered": False,
|
| 239 |
+
"class": prop_class,
|
| 240 |
+
}
|
| 241 |
+
)
|
| 242 |
+
|
| 243 |
+
df = pd.DataFrame(metadata)
|
| 244 |
+
|
| 245 |
+
class_counts = df["class"].value_counts()
|
| 246 |
+
print("\nClass distribution in dataset:")
|
| 247 |
+
for class_name, count in class_counts.items():
|
| 248 |
+
print(f" - {class_name}: {count} ({count/len(df)*100:.1f}%)")
|
| 249 |
+
|
| 250 |
+
if not skip_split:
|
| 251 |
+
df = split_dataset(df, seed=seed)
|
| 252 |
+
else:
|
| 253 |
+
print("Skipping dataset splitting as requested")
|
| 254 |
+
df["split"] = "train"
|
| 255 |
+
|
| 256 |
+
if output_dir:
|
| 257 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 258 |
+
|
| 259 |
+
df.to_csv(os.path.join(output_dir, "metadata.csv"), index=False)
|
| 260 |
+
|
| 261 |
+
splits_dir = os.path.join(output_dir, "splits")
|
| 262 |
+
os.makedirs(splits_dir, exist_ok=True)
|
| 263 |
+
|
| 264 |
+
for split in ["train", "val", "test"]:
|
| 265 |
+
split_df = df[df["split"] == split]
|
| 266 |
+
if not split_df.empty:
|
| 267 |
+
split_df.to_csv(os.path.join(splits_dir, f"{split}.csv"), index=False)
|
| 268 |
+
|
| 269 |
+
class_stats = df.groupby(["class", "split"]).size().unstack(fill_value=0)
|
| 270 |
+
class_stats.to_csv(os.path.join(output_dir, "class_distribution.csv"))
|
| 271 |
+
|
| 272 |
+
return df
|
| 273 |
+
|
| 274 |
+
|
| 275 |
+
def foreach_instance(metadata, output_dir, func, max_workers=8, desc="Processing"):
|
| 276 |
+
from concurrent.futures import ThreadPoolExecutor
|
| 277 |
+
from tqdm import tqdm
|
| 278 |
+
import pandas as pd
|
| 279 |
+
|
| 280 |
+
results = []
|
| 281 |
+
|
| 282 |
+
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
| 283 |
+
futures = []
|
| 284 |
+
for _, row in metadata.iterrows():
|
| 285 |
+
sha256 = row["sha256"]
|
| 286 |
+
local_path = row["local_path"]
|
| 287 |
+
futures.append(executor.submit(func, local_path, sha256))
|
| 288 |
+
|
| 289 |
+
for future in tqdm(futures, desc=desc, total=len(futures)):
|
| 290 |
+
try:
|
| 291 |
+
result = future.result()
|
| 292 |
+
if result is not None:
|
| 293 |
+
results.append(result)
|
| 294 |
+
except Exception as e:
|
| 295 |
+
print(f"Error in worker: {e}")
|
| 296 |
+
|
| 297 |
+
return pd.DataFrame.from_records(results)
|
deps/vomp/dataset_toolkits/extract_feature.py
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
|
| 16 |
+
import os
|
| 17 |
+
import copy
|
| 18 |
+
import sys
|
| 19 |
+
import json
|
| 20 |
+
import importlib
|
| 21 |
+
import argparse
|
| 22 |
+
import torch
|
| 23 |
+
import torch.nn.functional as F
|
| 24 |
+
import numpy as np
|
| 25 |
+
import pandas as pd
|
| 26 |
+
import utils3d
|
| 27 |
+
from tqdm import tqdm
|
| 28 |
+
from easydict import EasyDict as edict
|
| 29 |
+
from torchvision import transforms
|
| 30 |
+
from PIL import Image
|
| 31 |
+
|
| 32 |
+
torch.set_grad_enabled(False)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def get_data(frames, sha256):
|
| 36 |
+
valid_data = []
|
| 37 |
+
|
| 38 |
+
for view in frames:
|
| 39 |
+
image_path = os.path.join(opt.output_dir, "renders", sha256, view["file_path"])
|
| 40 |
+
try:
|
| 41 |
+
# Check if file exists before trying to open it
|
| 42 |
+
if not os.path.exists(image_path):
|
| 43 |
+
print(f"Warning: Image file {image_path} not found, skipping")
|
| 44 |
+
continue
|
| 45 |
+
|
| 46 |
+
image = Image.open(image_path)
|
| 47 |
+
except Exception as e:
|
| 48 |
+
print(f"Error loading image {image_path}: {e}")
|
| 49 |
+
continue
|
| 50 |
+
|
| 51 |
+
try:
|
| 52 |
+
image = image.resize((518, 518), Image.Resampling.LANCZOS)
|
| 53 |
+
image = np.array(image).astype(np.float32) / 255
|
| 54 |
+
image = image[:, :, :3] * image[:, :, 3:]
|
| 55 |
+
image = torch.from_numpy(image).permute(2, 0, 1).float()
|
| 56 |
+
|
| 57 |
+
c2w = torch.tensor(view["transform_matrix"])
|
| 58 |
+
c2w[:3, 1:3] *= -1
|
| 59 |
+
extrinsics = torch.inverse(c2w)
|
| 60 |
+
fov = view["camera_angle_x"]
|
| 61 |
+
intrinsics = utils3d.torch.intrinsics_from_fov_xy(
|
| 62 |
+
torch.tensor(fov), torch.tensor(fov)
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
valid_data.append(
|
| 66 |
+
{"image": image, "extrinsics": extrinsics, "intrinsics": intrinsics}
|
| 67 |
+
)
|
| 68 |
+
except Exception as e:
|
| 69 |
+
print(f"Error processing image {image_path}: {e}")
|
| 70 |
+
continue
|
| 71 |
+
|
| 72 |
+
if len(valid_data) == 0:
|
| 73 |
+
print(f"Warning: No valid images found for {sha256}")
|
| 74 |
+
else:
|
| 75 |
+
print(f"Loaded {len(valid_data)}/{len(frames)} valid images for {sha256}")
|
| 76 |
+
|
| 77 |
+
return valid_data
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
if __name__ == "__main__":
|
| 81 |
+
parser = argparse.ArgumentParser()
|
| 82 |
+
parser.add_argument(
|
| 83 |
+
"--output_dir", type=str, required=True, help="Directory to save the metadata"
|
| 84 |
+
)
|
| 85 |
+
parser.add_argument(
|
| 86 |
+
"--filter_low_aesthetic_score",
|
| 87 |
+
type=float,
|
| 88 |
+
default=None,
|
| 89 |
+
help="Filter objects with aesthetic score lower than this value",
|
| 90 |
+
)
|
| 91 |
+
parser.add_argument(
|
| 92 |
+
"--model",
|
| 93 |
+
type=str,
|
| 94 |
+
default="dinov2_vitl14_reg",
|
| 95 |
+
help="Feature extraction model",
|
| 96 |
+
)
|
| 97 |
+
parser.add_argument(
|
| 98 |
+
"--instances", type=str, default=None, help="Instances to process"
|
| 99 |
+
)
|
| 100 |
+
parser.add_argument("--batch_size", type=int, default=16)
|
| 101 |
+
parser.add_argument("--rank", type=int, default=0)
|
| 102 |
+
parser.add_argument("--world_size", type=int, default=1)
|
| 103 |
+
parser.add_argument(
|
| 104 |
+
"--force",
|
| 105 |
+
action="store_true",
|
| 106 |
+
help="Force feature extraction even if feature files already exist",
|
| 107 |
+
)
|
| 108 |
+
opt = parser.parse_args()
|
| 109 |
+
opt = edict(vars(opt))
|
| 110 |
+
|
| 111 |
+
feature_name = opt.model
|
| 112 |
+
os.makedirs(os.path.join(opt.output_dir, "features", feature_name), exist_ok=True)
|
| 113 |
+
|
| 114 |
+
# load model
|
| 115 |
+
dinov2_model = torch.hub.load("facebookresearch/dinov2", opt.model)
|
| 116 |
+
dinov2_model.eval().cuda()
|
| 117 |
+
transform = transforms.Compose(
|
| 118 |
+
[
|
| 119 |
+
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
|
| 120 |
+
]
|
| 121 |
+
)
|
| 122 |
+
n_patch = 518 // 14
|
| 123 |
+
|
| 124 |
+
# get file list
|
| 125 |
+
if os.path.exists(os.path.join(opt.output_dir, "metadata.csv")):
|
| 126 |
+
metadata = pd.read_csv(os.path.join(opt.output_dir, "metadata.csv"))
|
| 127 |
+
else:
|
| 128 |
+
raise ValueError("metadata.csv not found")
|
| 129 |
+
if opt.instances is not None:
|
| 130 |
+
with open(opt.instances, "r") as f:
|
| 131 |
+
instances = f.read().splitlines()
|
| 132 |
+
metadata = metadata[metadata["sha256"].isin(instances)]
|
| 133 |
+
else:
|
| 134 |
+
if opt.filter_low_aesthetic_score is not None:
|
| 135 |
+
metadata = metadata[
|
| 136 |
+
metadata["aesthetic_score"] >= opt.filter_low_aesthetic_score
|
| 137 |
+
]
|
| 138 |
+
if f"feature_{feature_name}" in metadata.columns and not opt.force:
|
| 139 |
+
metadata = metadata[metadata[f"feature_{feature_name}"] == False]
|
| 140 |
+
metadata = metadata[metadata["voxelized"] == True]
|
| 141 |
+
metadata = metadata[metadata["rendered"] == True]
|
| 142 |
+
|
| 143 |
+
start = len(metadata) * opt.rank // opt.world_size
|
| 144 |
+
end = len(metadata) * (opt.rank + 1) // opt.world_size
|
| 145 |
+
metadata = metadata[start:end]
|
| 146 |
+
records = []
|
| 147 |
+
|
| 148 |
+
# filter out objects that are already processed
|
| 149 |
+
sha256s = list(metadata["sha256"].values)
|
| 150 |
+
if not opt.force:
|
| 151 |
+
for sha256 in copy.copy(sha256s):
|
| 152 |
+
if os.path.exists(
|
| 153 |
+
os.path.join(opt.output_dir, "features", feature_name, f"{sha256}.npz")
|
| 154 |
+
):
|
| 155 |
+
records.append({"sha256": sha256, f"feature_{feature_name}": True})
|
| 156 |
+
sha256s.remove(sha256)
|
| 157 |
+
else:
|
| 158 |
+
print(
|
| 159 |
+
f"Force mode enabled. Processing all {len(sha256s)} objects regardless of existing features."
|
| 160 |
+
)
|
| 161 |
+
|
| 162 |
+
# filter out objects that don't have voxel files
|
| 163 |
+
initial_count = len(sha256s)
|
| 164 |
+
sha256s_with_voxels = []
|
| 165 |
+
for sha256 in sha256s:
|
| 166 |
+
voxel_path = os.path.join(opt.output_dir, "voxels", f"{sha256}.ply")
|
| 167 |
+
if os.path.exists(voxel_path):
|
| 168 |
+
sha256s_with_voxels.append(sha256)
|
| 169 |
+
else:
|
| 170 |
+
print(f"Skipping {sha256}: voxel file not found at {voxel_path}")
|
| 171 |
+
|
| 172 |
+
sha256s = sha256s_with_voxels
|
| 173 |
+
print(f"Filtered from {initial_count} to {len(sha256s)} objects with voxel files")
|
| 174 |
+
|
| 175 |
+
# extract features
|
| 176 |
+
for sha256 in tqdm(sha256s, desc="Extracting features"):
|
| 177 |
+
try:
|
| 178 |
+
# Load data
|
| 179 |
+
with open(
|
| 180 |
+
os.path.join(opt.output_dir, "renders", sha256, "transforms.json"),
|
| 181 |
+
"r",
|
| 182 |
+
) as f:
|
| 183 |
+
metadata_json = json.load(f)
|
| 184 |
+
frames = metadata_json["frames"]
|
| 185 |
+
data = get_data(frames, sha256)
|
| 186 |
+
|
| 187 |
+
if len(data) == 0:
|
| 188 |
+
print(f"Skipping {sha256}: no valid image data")
|
| 189 |
+
continue
|
| 190 |
+
|
| 191 |
+
# Apply transform to images
|
| 192 |
+
for datum in data:
|
| 193 |
+
datum["image"] = transform(datum["image"])
|
| 194 |
+
|
| 195 |
+
# Load positions
|
| 196 |
+
positions = utils3d.io.read_ply(
|
| 197 |
+
os.path.join(opt.output_dir, "voxels", f"{sha256}.ply")
|
| 198 |
+
)[0]
|
| 199 |
+
positions = torch.from_numpy(positions).float().cuda()
|
| 200 |
+
indices = ((positions + 0.5) * 64).long()
|
| 201 |
+
# Clamp indices to valid range [0, 63] to handle floating point precision issues
|
| 202 |
+
indices = torch.clamp(indices, 0, 63)
|
| 203 |
+
|
| 204 |
+
n_views = len(data)
|
| 205 |
+
N = positions.shape[0]
|
| 206 |
+
pack = {
|
| 207 |
+
"indices": indices.cpu().numpy().astype(np.uint8),
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
patchtokens_lst = []
|
| 211 |
+
uv_lst = []
|
| 212 |
+
|
| 213 |
+
# Process in batches
|
| 214 |
+
for i in range(0, n_views, opt.batch_size):
|
| 215 |
+
batch_data = data[i : i + opt.batch_size]
|
| 216 |
+
bs = len(batch_data)
|
| 217 |
+
batch_images = torch.stack([d["image"] for d in batch_data]).cuda()
|
| 218 |
+
batch_extrinsics = torch.stack(
|
| 219 |
+
[d["extrinsics"] for d in batch_data]
|
| 220 |
+
).cuda()
|
| 221 |
+
batch_intrinsics = torch.stack(
|
| 222 |
+
[d["intrinsics"] for d in batch_data]
|
| 223 |
+
).cuda()
|
| 224 |
+
features = dinov2_model(batch_images, is_training=True)
|
| 225 |
+
uv = (
|
| 226 |
+
utils3d.torch.project_cv(
|
| 227 |
+
positions, batch_extrinsics, batch_intrinsics
|
| 228 |
+
)[0]
|
| 229 |
+
* 2
|
| 230 |
+
- 1
|
| 231 |
+
)
|
| 232 |
+
patchtokens = (
|
| 233 |
+
features["x_prenorm"][:, dinov2_model.num_register_tokens + 1 :]
|
| 234 |
+
.permute(0, 2, 1)
|
| 235 |
+
.reshape(bs, 1024, n_patch, n_patch)
|
| 236 |
+
)
|
| 237 |
+
patchtokens_lst.append(patchtokens)
|
| 238 |
+
uv_lst.append(uv)
|
| 239 |
+
|
| 240 |
+
patchtokens = torch.cat(patchtokens_lst, dim=0)
|
| 241 |
+
uv = torch.cat(uv_lst, dim=0)
|
| 242 |
+
|
| 243 |
+
# Save features
|
| 244 |
+
pack["patchtokens"] = (
|
| 245 |
+
F.grid_sample(
|
| 246 |
+
patchtokens,
|
| 247 |
+
uv.unsqueeze(1),
|
| 248 |
+
mode="bilinear",
|
| 249 |
+
align_corners=False,
|
| 250 |
+
)
|
| 251 |
+
.squeeze(2)
|
| 252 |
+
.permute(0, 2, 1)
|
| 253 |
+
.cpu()
|
| 254 |
+
.numpy()
|
| 255 |
+
)
|
| 256 |
+
pack["patchtokens"] = np.mean(pack["patchtokens"], axis=0).astype(
|
| 257 |
+
np.float16
|
| 258 |
+
)
|
| 259 |
+
save_path = os.path.join(
|
| 260 |
+
opt.output_dir, "features", feature_name, f"{sha256}.npz"
|
| 261 |
+
)
|
| 262 |
+
np.savez_compressed(save_path, **pack)
|
| 263 |
+
records.append({"sha256": sha256, f"feature_{feature_name}": True})
|
| 264 |
+
|
| 265 |
+
except Exception as e:
|
| 266 |
+
print(f"Error processing {sha256}: {e}")
|
| 267 |
+
continue
|
| 268 |
+
|
| 269 |
+
records = pd.DataFrame.from_records(records)
|
| 270 |
+
records.to_csv(
|
| 271 |
+
os.path.join(opt.output_dir, f"feature_{feature_name}_{opt.rank}.csv"),
|
| 272 |
+
index=False,
|
| 273 |
+
)
|
deps/vomp/dataset_toolkits/latent_space/analyze_data_distribution.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
import pandas as pd
|
| 18 |
+
import numpy as np
|
| 19 |
+
import matplotlib.pyplot as plt
|
| 20 |
+
from pathlib import Path
|
| 21 |
+
|
| 22 |
+
df = pd.read_csv("datasets/latent_space/materials.csv")
|
| 23 |
+
|
| 24 |
+
print("Data shape:", df.shape)
|
| 25 |
+
print("\nColumn names:", df.columns.tolist())
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
for col in ["youngs_modulus", "poisson_ratio", "density"]:
|
| 29 |
+
print(f"\n{col}:")
|
| 30 |
+
print(f" Min: {df[col].min():.2e}")
|
| 31 |
+
print(f" Max: {df[col].max():.2e}")
|
| 32 |
+
print(f" Mean: {df[col].mean():.2e}")
|
| 33 |
+
print(f" Median: {df[col].median():.2e}")
|
| 34 |
+
print(f" Std: {df[col].std():.2e}")
|
| 35 |
+
|
| 36 |
+
Q1 = df[col].quantile(0.25)
|
| 37 |
+
Q3 = df[col].quantile(0.75)
|
| 38 |
+
IQR = Q3 - Q1
|
| 39 |
+
lower_bound = Q1 - 1.5 * IQR
|
| 40 |
+
upper_bound = Q3 + 1.5 * IQR
|
| 41 |
+
|
| 42 |
+
outliers = df[(df[col] < lower_bound) | (df[col] > upper_bound)]
|
| 43 |
+
print(
|
| 44 |
+
f" Outliers (IQR method): {len(outliers)} ({len(outliers)/len(df)*100:.1f}%)"
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
if col in ["youngs_modulus", "density"]:
|
| 48 |
+
log_vals = np.log10(df[col])
|
| 49 |
+
print(f" Log10 range: [{log_vals.min():.2f}, {log_vals.max():.2f}]")
|
| 50 |
+
print(
|
| 51 |
+
f" Log10 span: {log_vals.max() - log_vals.min():.2f} orders of magnitude"
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
print("\n\nMaterials with extreme Young's modulus (< 1e7 Pa):")
|
| 56 |
+
low_E = df[df["youngs_modulus"] < 1e7]
|
| 57 |
+
if len(low_E) > 0:
|
| 58 |
+
material_counts = low_E["material_name"].value_counts().head(10)
|
| 59 |
+
for mat, count in material_counts.items():
|
| 60 |
+
print(f" {mat}: {count} samples")
|
| 61 |
+
|
| 62 |
+
print("\n\nMaterials with extreme density (< 100 kg/m³):")
|
| 63 |
+
low_rho = df[df["density"] < 100]
|
| 64 |
+
if len(low_rho) > 0:
|
| 65 |
+
material_counts = low_rho["material_name"].value_counts().head(10)
|
| 66 |
+
for mat, count in material_counts.items():
|
| 67 |
+
print(f" {mat}: {count} samples")
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
print("\n\nPercentile analysis:")
|
| 71 |
+
percentiles = [1, 5, 10, 25, 50, 75, 90, 95, 99]
|
| 72 |
+
for col in ["youngs_modulus", "poisson_ratio", "density"]:
|
| 73 |
+
print(f"\n{col} percentiles:")
|
| 74 |
+
for p in percentiles:
|
| 75 |
+
val = df[col].quantile(p / 100)
|
| 76 |
+
print(f" {p}%: {val:.2e}")
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
print("\n\nCreating filtered dataset...")
|
| 80 |
+
|
| 81 |
+
filtered_df = df[
|
| 82 |
+
(df["youngs_modulus"] >= 1e5)
|
| 83 |
+
& (df["youngs_modulus"] <= 1e12)
|
| 84 |
+
& (df["density"] >= 100)
|
| 85 |
+
& (df["density"] <= 20000)
|
| 86 |
+
& (df["poisson_ratio"] >= 0.0)
|
| 87 |
+
& (df["poisson_ratio"] <= 0.49)
|
| 88 |
+
]
|
| 89 |
+
|
| 90 |
+
print(f"Original size: {len(df)}")
|
| 91 |
+
print(f"Filtered size: {len(filtered_df)}")
|
| 92 |
+
print(
|
| 93 |
+
f"Removed: {len(df) - len(filtered_df)} ({(len(df) - len(filtered_df))/len(df)*100:.1f}%)"
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
print("\nRanges in filtered dataset (only Poisson ratio filtering):")
|
| 98 |
+
for col in ["youngs_modulus", "poisson_ratio", "density"]:
|
| 99 |
+
print(f"\n{col}:")
|
| 100 |
+
print(f" Min: {filtered_df[col].min():.2e}")
|
| 101 |
+
print(f" Max: {filtered_df[col].max():.2e}")
|
| 102 |
+
print(f" Range span: {filtered_df[col].max() - filtered_df[col].min():.2e}")
|
| 103 |
+
|
| 104 |
+
if col in ["youngs_modulus", "density"]:
|
| 105 |
+
log_min = np.log10(filtered_df[col].min())
|
| 106 |
+
log_max = np.log10(filtered_df[col].max())
|
| 107 |
+
print(f" Log10 range: [{log_min:.2f}, {log_max:.2f}]")
|
| 108 |
+
print(f" Orders of magnitude: {log_max - log_min:.2f}")
|
| 109 |
+
|
| 110 |
+
filtered_df.to_csv("datasets/latent_space/materials_filtered.csv", index=False)
|
| 111 |
+
print("\nSaved filtered dataset to materials_filtered.csv")
|
deps/vomp/dataset_toolkits/latent_space/make_csv.py
ADDED
|
@@ -0,0 +1,411 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
import argparse
|
| 18 |
+
import json
|
| 19 |
+
import csv
|
| 20 |
+
import random
|
| 21 |
+
from pathlib import Path
|
| 22 |
+
from typing import Tuple, Set, List
|
| 23 |
+
import math
|
| 24 |
+
from dataset_toolkits.material_objects.vlm_annotations.utils.utils import (
|
| 25 |
+
parse_numerical_range_str,
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def parse_args() -> Path:
|
| 30 |
+
parser = argparse.ArgumentParser(
|
| 31 |
+
description="Generate a materials.csv file from material_ranges.csv in the provided directory."
|
| 32 |
+
)
|
| 33 |
+
parser.add_argument(
|
| 34 |
+
"directory",
|
| 35 |
+
type=str,
|
| 36 |
+
help="Path to the directory containing material_ranges.csv",
|
| 37 |
+
)
|
| 38 |
+
args = parser.parse_args()
|
| 39 |
+
directory = Path(args.directory).expanduser().resolve()
|
| 40 |
+
|
| 41 |
+
if not directory.is_dir():
|
| 42 |
+
parser.error(f"Provided path '{directory}' is not a directory.")
|
| 43 |
+
|
| 44 |
+
return directory
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def read_dataset(json_path: Path):
|
| 48 |
+
try:
|
| 49 |
+
with json_path.open("r", encoding="utf-8") as f:
|
| 50 |
+
return json.load(f)
|
| 51 |
+
except FileNotFoundError:
|
| 52 |
+
raise FileNotFoundError(
|
| 53 |
+
f"Could not find '{json_path}'. Ensure the directory contains the file."
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def extract_unique_rows(
|
| 58 |
+
dataset, unique_triplets: Set[Tuple[float, float, float]] | None = None
|
| 59 |
+
) -> tuple[list, Set[Tuple[float, float, float]]]:
|
| 60 |
+
if unique_triplets is None:
|
| 61 |
+
unique_triplets = set()
|
| 62 |
+
|
| 63 |
+
rows: List[Tuple[str, float, float, float]] = []
|
| 64 |
+
|
| 65 |
+
for obj in dataset:
|
| 66 |
+
segments = obj.get("segments", {})
|
| 67 |
+
for seg_key, seg_data in segments.items():
|
| 68 |
+
try:
|
| 69 |
+
youngs = float(seg_data["youngs_modulus"])
|
| 70 |
+
poisson = float(seg_data["poissons_ratio"])
|
| 71 |
+
density = float(seg_data["density"])
|
| 72 |
+
except (KeyError, ValueError, TypeError):
|
| 73 |
+
continue
|
| 74 |
+
|
| 75 |
+
if youngs <= 0 or youngs > 1e13:
|
| 76 |
+
print(
|
| 77 |
+
f"WARNING: Skipping material with invalid Young's modulus: {youngs}"
|
| 78 |
+
)
|
| 79 |
+
continue
|
| 80 |
+
|
| 81 |
+
if poisson < -1.0 or poisson > 0.5:
|
| 82 |
+
print(
|
| 83 |
+
f"WARNING: Skipping material with invalid Poisson's ratio: {poisson}"
|
| 84 |
+
)
|
| 85 |
+
continue
|
| 86 |
+
|
| 87 |
+
if density <= 0 or density > 50000:
|
| 88 |
+
print(f"WARNING: Skipping material with invalid density: {density}")
|
| 89 |
+
continue
|
| 90 |
+
|
| 91 |
+
triplet = (youngs, poisson, density)
|
| 92 |
+
if triplet in unique_triplets:
|
| 93 |
+
continue
|
| 94 |
+
|
| 95 |
+
unique_triplets.add(triplet)
|
| 96 |
+
material_name = seg_data.get("name", seg_key)
|
| 97 |
+
rows.append((material_name, youngs, poisson, density))
|
| 98 |
+
|
| 99 |
+
return rows, unique_triplets
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def sample_ranges(
|
| 103 |
+
csv_path: Path,
|
| 104 |
+
unique_triplets: Set[Tuple[float, float, float]],
|
| 105 |
+
min_samples_per_material: int = 100,
|
| 106 |
+
max_samples_per_material: int = 2500,
|
| 107 |
+
target_total_samples: int = 100_000,
|
| 108 |
+
) -> list:
|
| 109 |
+
if not csv_path.exists():
|
| 110 |
+
|
| 111 |
+
return []
|
| 112 |
+
|
| 113 |
+
parsed_rows: list[dict] = []
|
| 114 |
+
dynamic_indices: list[int] = []
|
| 115 |
+
|
| 116 |
+
with csv_path.open("r", encoding="utf-8") as f:
|
| 117 |
+
lines = f.readlines()
|
| 118 |
+
|
| 119 |
+
header = lines[0].strip().split(",")
|
| 120 |
+
|
| 121 |
+
for idx, line in enumerate(lines[1:], 0):
|
| 122 |
+
|
| 123 |
+
parts = []
|
| 124 |
+
current = ""
|
| 125 |
+
in_brackets = False
|
| 126 |
+
for char in line.strip() + ",":
|
| 127 |
+
if char == "," and not in_brackets:
|
| 128 |
+
parts.append(current)
|
| 129 |
+
current = ""
|
| 130 |
+
else:
|
| 131 |
+
if char == "[":
|
| 132 |
+
in_brackets = True
|
| 133 |
+
elif char == "]":
|
| 134 |
+
in_brackets = False
|
| 135 |
+
current += char
|
| 136 |
+
|
| 137 |
+
if len(parts) < 4:
|
| 138 |
+
print(f"WARNING: Line {idx+1} has incorrect format: {line.strip()}")
|
| 139 |
+
continue
|
| 140 |
+
|
| 141 |
+
material_name = parts[0].strip().strip('"')
|
| 142 |
+
|
| 143 |
+
y_range_str = parts[1].strip().strip('"')
|
| 144 |
+
p_range_str = parts[2].strip().strip('"')
|
| 145 |
+
d_range_str = parts[3].strip().strip('"')
|
| 146 |
+
|
| 147 |
+
try:
|
| 148 |
+
y_low, y_high = parse_numerical_range_str(y_range_str)
|
| 149 |
+
p_low, p_high = parse_numerical_range_str(p_range_str)
|
| 150 |
+
d_low, d_high = parse_numerical_range_str(d_range_str)
|
| 151 |
+
except ValueError as e:
|
| 152 |
+
print(
|
| 153 |
+
f"WARNING: Error parsing ranges for {material_name} on line {idx+1}: {e} - Skipping material."
|
| 154 |
+
)
|
| 155 |
+
continue
|
| 156 |
+
|
| 157 |
+
y_low *= 1e9
|
| 158 |
+
y_high *= 1e9
|
| 159 |
+
|
| 160 |
+
y_low = max(1e6, min(y_low, 1e13))
|
| 161 |
+
y_high = max(y_low, min(y_high, 1e13))
|
| 162 |
+
|
| 163 |
+
p_low = max(-0.999, min(p_low, 0.499))
|
| 164 |
+
p_high = max(p_low, min(p_high, 0.499))
|
| 165 |
+
|
| 166 |
+
d_low = max(10.0, min(d_low, 50000.0))
|
| 167 |
+
d_high = max(d_low, min(d_high, 50000.0))
|
| 168 |
+
|
| 169 |
+
y_has_range = abs(y_high - y_low) > 1e-6
|
| 170 |
+
p_has_range = abs(p_high - p_low) > 1e-6
|
| 171 |
+
d_has_range = abs(d_high - d_low) > 1e-6
|
| 172 |
+
|
| 173 |
+
has_range = y_has_range or p_has_range or d_has_range
|
| 174 |
+
|
| 175 |
+
y_width = max(y_high - y_low, 1.0) if y_has_range else 1.0
|
| 176 |
+
p_width = max(p_high - p_low, 0.001) if p_has_range else 0.001
|
| 177 |
+
d_width = max(d_high - d_low, 1.0) if d_has_range else 1.0
|
| 178 |
+
|
| 179 |
+
y_width_norm = y_width / 1e9
|
| 180 |
+
|
| 181 |
+
volume = y_width_norm * p_width * d_width
|
| 182 |
+
|
| 183 |
+
if has_range:
|
| 184 |
+
dynamic_indices.append(idx)
|
| 185 |
+
|
| 186 |
+
parsed_rows.append(
|
| 187 |
+
{
|
| 188 |
+
"material_name": material_name,
|
| 189 |
+
"y_low": y_low,
|
| 190 |
+
"y_high": y_high,
|
| 191 |
+
"p_low": p_low,
|
| 192 |
+
"p_high": p_high,
|
| 193 |
+
"d_low": d_low,
|
| 194 |
+
"d_high": d_high,
|
| 195 |
+
"has_range": has_range,
|
| 196 |
+
"y_has_range": y_has_range,
|
| 197 |
+
"p_has_range": p_has_range,
|
| 198 |
+
"d_has_range": d_has_range,
|
| 199 |
+
"volume": volume,
|
| 200 |
+
}
|
| 201 |
+
)
|
| 202 |
+
|
| 203 |
+
if not parsed_rows:
|
| 204 |
+
return []
|
| 205 |
+
|
| 206 |
+
print(
|
| 207 |
+
f"Found {len(dynamic_indices)} materials with ranges out of {len(parsed_rows)} total"
|
| 208 |
+
)
|
| 209 |
+
|
| 210 |
+
fixed_count = len(parsed_rows) - len(dynamic_indices)
|
| 211 |
+
print(f"Number of materials with fixed values: {fixed_count}")
|
| 212 |
+
|
| 213 |
+
if dynamic_indices:
|
| 214 |
+
print("\nExample materials with ranges:")
|
| 215 |
+
for i in range(min(5, len(dynamic_indices))):
|
| 216 |
+
idx = dynamic_indices[i]
|
| 217 |
+
info = parsed_rows[idx]
|
| 218 |
+
ranges_info = []
|
| 219 |
+
if info["y_has_range"]:
|
| 220 |
+
ranges_info.append(
|
| 221 |
+
f"Young's: {info['y_low']/1e9:.3f}-{info['y_high']/1e9:.3f} GPa"
|
| 222 |
+
)
|
| 223 |
+
if info["p_has_range"]:
|
| 224 |
+
ranges_info.append(
|
| 225 |
+
f"Poisson's: {info['p_low']:.3f}-{info['p_high']:.3f}"
|
| 226 |
+
)
|
| 227 |
+
if info["d_has_range"]:
|
| 228 |
+
ranges_info.append(
|
| 229 |
+
f"Density: {info['d_low']:.1f}-{info['d_high']:.1f} kg/m³"
|
| 230 |
+
)
|
| 231 |
+
|
| 232 |
+
print(
|
| 233 |
+
f" {info['material_name']}: {', '.join(ranges_info)} (volume: {info['volume']:.4f})"
|
| 234 |
+
)
|
| 235 |
+
|
| 236 |
+
total_volume = sum(parsed_rows[idx]["volume"] for idx in dynamic_indices)
|
| 237 |
+
print(f"\nTotal parameter space volume: {total_volume:.4f}")
|
| 238 |
+
|
| 239 |
+
volume_scale_factor = 13.0
|
| 240 |
+
|
| 241 |
+
samples_per_material = {}
|
| 242 |
+
|
| 243 |
+
for idx in dynamic_indices:
|
| 244 |
+
volume_ratio = parsed_rows[idx]["volume"] / total_volume
|
| 245 |
+
proportional_samples = max(
|
| 246 |
+
math.ceil(target_total_samples * volume_ratio * volume_scale_factor),
|
| 247 |
+
min_samples_per_material,
|
| 248 |
+
)
|
| 249 |
+
|
| 250 |
+
samples_per_material[idx] = min(proportional_samples, max_samples_per_material)
|
| 251 |
+
|
| 252 |
+
fixed_total = 0
|
| 253 |
+
|
| 254 |
+
dynamic_total = sum(samples_per_material.values())
|
| 255 |
+
total_planned = dynamic_total + fixed_total
|
| 256 |
+
|
| 257 |
+
print(f"\nSampling strategy (scaled by {volume_scale_factor}x):")
|
| 258 |
+
print(f" Minimum samples per material with ranges: {min_samples_per_material}")
|
| 259 |
+
print(f" Maximum samples per material: {max_samples_per_material}")
|
| 260 |
+
print(f" Planned total samples: {total_planned}")
|
| 261 |
+
|
| 262 |
+
sorted_materials = sorted(
|
| 263 |
+
[
|
| 264 |
+
(
|
| 265 |
+
idx,
|
| 266 |
+
parsed_rows[idx]["material_name"],
|
| 267 |
+
parsed_rows[idx]["volume"],
|
| 268 |
+
samples_per_material.get(idx, 1) if idx in dynamic_indices else 1,
|
| 269 |
+
)
|
| 270 |
+
for idx in range(len(parsed_rows))
|
| 271 |
+
],
|
| 272 |
+
key=lambda x: x[2],
|
| 273 |
+
reverse=True,
|
| 274 |
+
)
|
| 275 |
+
|
| 276 |
+
print("\nTop 15 highest volume materials:")
|
| 277 |
+
for idx, name, volume, samples in sorted_materials[:15]:
|
| 278 |
+
if idx in dynamic_indices:
|
| 279 |
+
volume_percent = volume / total_volume * 100
|
| 280 |
+
print(
|
| 281 |
+
f" {name}: volume {volume:.4f} ({volume_percent:.2f}%), {samples} samples"
|
| 282 |
+
)
|
| 283 |
+
else:
|
| 284 |
+
print(f" {name}: fixed values, 1 sample")
|
| 285 |
+
|
| 286 |
+
rows: list[Tuple[str, float, float, float]] = []
|
| 287 |
+
|
| 288 |
+
def _add_triplet(material: str, y: float, p: float, d: float):
|
| 289 |
+
|
| 290 |
+
if y <= 0 or y > 1e13:
|
| 291 |
+
return False
|
| 292 |
+
if p < -1.0 or p > 0.5:
|
| 293 |
+
return False
|
| 294 |
+
if d <= 0 or d > 50000:
|
| 295 |
+
return False
|
| 296 |
+
|
| 297 |
+
triplet = (y, p, d)
|
| 298 |
+
if triplet in unique_triplets:
|
| 299 |
+
return False
|
| 300 |
+
unique_triplets.add(triplet)
|
| 301 |
+
rows.append((material, y, p, d))
|
| 302 |
+
return True
|
| 303 |
+
|
| 304 |
+
total_generated = 0
|
| 305 |
+
duplicate_avoidance_failures = 0
|
| 306 |
+
|
| 307 |
+
for idx, info in enumerate(parsed_rows):
|
| 308 |
+
if not info["has_range"]:
|
| 309 |
+
name = info["material_name"]
|
| 310 |
+
y_val = info["y_low"]
|
| 311 |
+
p_val = info["p_low"]
|
| 312 |
+
d_val = info["d_low"]
|
| 313 |
+
|
| 314 |
+
if _add_triplet(name, y_val, p_val, d_val):
|
| 315 |
+
total_generated += 1
|
| 316 |
+
|
| 317 |
+
print(f"Added {total_generated} materials with fixed values")
|
| 318 |
+
|
| 319 |
+
for idx in dynamic_indices:
|
| 320 |
+
info = parsed_rows[idx]
|
| 321 |
+
name = info["material_name"]
|
| 322 |
+
y_low, y_high = info["y_low"], info["y_high"]
|
| 323 |
+
p_low, p_high = info["p_low"], info["p_high"]
|
| 324 |
+
d_low, d_high = info["d_low"], info["d_high"]
|
| 325 |
+
|
| 326 |
+
required = samples_per_material.get(idx, 0)
|
| 327 |
+
|
| 328 |
+
report_progress = required > 100
|
| 329 |
+
|
| 330 |
+
attempts = 0
|
| 331 |
+
generated = 0
|
| 332 |
+
|
| 333 |
+
max_attempts = required * 50
|
| 334 |
+
|
| 335 |
+
if info["volume"] > 10.0:
|
| 336 |
+
max_attempts *= 2
|
| 337 |
+
|
| 338 |
+
if report_progress:
|
| 339 |
+
print(
|
| 340 |
+
f"Generating {required} samples for {name} (volume: {info['volume']:.4f})"
|
| 341 |
+
)
|
| 342 |
+
|
| 343 |
+
while generated < required and attempts < max_attempts:
|
| 344 |
+
attempts += 1
|
| 345 |
+
|
| 346 |
+
y_val = random.uniform(y_low, y_high) if info["y_has_range"] else y_low
|
| 347 |
+
p_val = random.uniform(p_low, p_high) if info["p_has_range"] else p_low
|
| 348 |
+
d_val = random.uniform(d_low, d_high) if info["d_has_range"] else d_low
|
| 349 |
+
|
| 350 |
+
y_val = round(y_val, 10)
|
| 351 |
+
p_val = round(p_val, 10)
|
| 352 |
+
d_val = round(d_val, 10)
|
| 353 |
+
|
| 354 |
+
if _add_triplet(name, y_val, p_val, d_val):
|
| 355 |
+
generated += 1
|
| 356 |
+
total_generated += 1
|
| 357 |
+
else:
|
| 358 |
+
|
| 359 |
+
duplicate_avoidance_failures += 1
|
| 360 |
+
|
| 361 |
+
if report_progress and generated > 0 and generated % 100 == 0:
|
| 362 |
+
print(f" Generated {generated}/{required} samples for {name}")
|
| 363 |
+
|
| 364 |
+
if required > 0 and report_progress:
|
| 365 |
+
|
| 366 |
+
success_rate = (generated / attempts) * 100 if attempts > 0 else 0
|
| 367 |
+
print(
|
| 368 |
+
f"Material {name}: Generated {generated}/{required} samples after {attempts} attempts (success rate: {success_rate:.1f}%)"
|
| 369 |
+
)
|
| 370 |
+
|
| 371 |
+
print(f"Successfully generated {len(rows)} unique material property combinations")
|
| 372 |
+
print(
|
| 373 |
+
f"Duplicate avoidance prevented {duplicate_avoidance_failures} potential duplicates"
|
| 374 |
+
)
|
| 375 |
+
return rows
|
| 376 |
+
|
| 377 |
+
|
| 378 |
+
def write_csv(rows: list, csv_path: Path):
|
| 379 |
+
csv_path.parent.mkdir(parents=True, exist_ok=True)
|
| 380 |
+
with csv_path.open("w", newline="", encoding="utf-8") as csvfile:
|
| 381 |
+
writer = csv.writer(csvfile)
|
| 382 |
+
writer.writerow(["material_name", "youngs_modulus", "poisson_ratio", "density"])
|
| 383 |
+
for row in rows:
|
| 384 |
+
writer.writerow(row)
|
| 385 |
+
|
| 386 |
+
|
| 387 |
+
def main():
|
| 388 |
+
directory = parse_args()
|
| 389 |
+
csv_path = directory / "materials.csv"
|
| 390 |
+
|
| 391 |
+
print("Generating materials data from ranges only (skipping JSON file)...")
|
| 392 |
+
|
| 393 |
+
unique_triplets = set()
|
| 394 |
+
|
| 395 |
+
ranges_csv_path = directory / "material_ranges.csv"
|
| 396 |
+
if not ranges_csv_path.exists():
|
| 397 |
+
print(f"ERROR: material_ranges.csv not found at {ranges_csv_path}")
|
| 398 |
+
return
|
| 399 |
+
|
| 400 |
+
sampled_rows = sample_ranges(ranges_csv_path, unique_triplets)
|
| 401 |
+
|
| 402 |
+
write_csv(sampled_rows, csv_path)
|
| 403 |
+
|
| 404 |
+
print(
|
| 405 |
+
f"materials.csv generated with {len(sampled_rows)} unique rows at '{csv_path}'."
|
| 406 |
+
)
|
| 407 |
+
print("All data generated from material_ranges.csv with validation applied.")
|
| 408 |
+
|
| 409 |
+
|
| 410 |
+
if __name__ == "__main__":
|
| 411 |
+
main()
|
deps/vomp/dataset_toolkits/material_objects/render_usd.py
ADDED
|
@@ -0,0 +1,1176 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 3 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 4 |
+
#
|
| 5 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 6 |
+
# you may not use this file except in compliance with the License.
|
| 7 |
+
# You may obtain a copy of the License at
|
| 8 |
+
#
|
| 9 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 10 |
+
#
|
| 11 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 12 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 13 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 14 |
+
# See the License for the specific language governing permissions and
|
| 15 |
+
# limitations under the License.
|
| 16 |
+
|
| 17 |
+
"""
|
| 18 |
+
Clean USD rendering pipeline.
|
| 19 |
+
|
| 20 |
+
This script extracts meshes and textures directly from USD files,
|
| 21 |
+
similar to how Omniverse exports meshes. It does NOT search the filesystem
|
| 22 |
+
for textures - all texture paths come from the USD shaders themselves.
|
| 23 |
+
|
| 24 |
+
For vegetation datasets that use MDL materials, it parses the MDL files
|
| 25 |
+
to extract texture references.
|
| 26 |
+
"""
|
| 27 |
+
|
| 28 |
+
import os
|
| 29 |
+
import sys
|
| 30 |
+
import json
|
| 31 |
+
import re
|
| 32 |
+
import argparse
|
| 33 |
+
import tempfile
|
| 34 |
+
import shutil
|
| 35 |
+
from pathlib import Path
|
| 36 |
+
from typing import Dict, List, Optional, Tuple
|
| 37 |
+
from collections import defaultdict
|
| 38 |
+
from subprocess import call, DEVNULL
|
| 39 |
+
|
| 40 |
+
import numpy as np
|
| 41 |
+
import pandas as pd
|
| 42 |
+
from pxr import Usd, UsdGeom, UsdShade, Sdf, Gf
|
| 43 |
+
|
| 44 |
+
sys.path.append(os.path.dirname(os.path.dirname(__file__)))
|
| 45 |
+
from utils import sphere_hammersley_sequence
|
| 46 |
+
|
| 47 |
+
BLENDER_LINK = (
|
| 48 |
+
"https://download.blender.org/release/Blender3.0/blender-3.0.1-linux-x64.tar.xz"
|
| 49 |
+
)
|
| 50 |
+
BLENDER_INSTALLATION_PATH = "/tmp"
|
| 51 |
+
BLENDER_PATH = f"{BLENDER_INSTALLATION_PATH}/blender-3.0.1-linux-x64/blender"
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
class USDMaterialExtractor:
|
| 55 |
+
"""
|
| 56 |
+
Extracts materials and textures directly from USD files.
|
| 57 |
+
|
| 58 |
+
This class reads shader inputs from USD prims and resolves texture paths
|
| 59 |
+
relative to the USD file. For MDL materials (used in vegetation), it
|
| 60 |
+
parses the MDL files to extract texture references.
|
| 61 |
+
"""
|
| 62 |
+
|
| 63 |
+
# MDL texture patterns (for vegetation)
|
| 64 |
+
MDL_TEXTURE_PATTERNS = [
|
| 65 |
+
r'diffuse_texture:\s*texture_2d\("([^"]+)"',
|
| 66 |
+
r'normalmap_texture:\s*texture_2d\("([^"]+)"',
|
| 67 |
+
r'reflectionroughness_texture:\s*texture_2d\("([^"]+)"',
|
| 68 |
+
r'metallic_texture:\s*texture_2d\("([^"]+)"',
|
| 69 |
+
r'ORM_texture:\s*texture_2d\("([^"]+)"',
|
| 70 |
+
]
|
| 71 |
+
|
| 72 |
+
def __init__(self, usd_path: str, verbose: bool = False):
|
| 73 |
+
self.usd_path = Path(usd_path).resolve()
|
| 74 |
+
self.usd_dir = self.usd_path.parent
|
| 75 |
+
self.verbose = verbose
|
| 76 |
+
self.stage = None
|
| 77 |
+
|
| 78 |
+
# Extracted data
|
| 79 |
+
self.materials = {} # material_path -> {input_name: texture_path}
|
| 80 |
+
self.meshes = {} # mesh_path -> {name, material, vertices, faces, uvs}
|
| 81 |
+
self.mesh_materials = {} # mesh_path -> material_path
|
| 82 |
+
|
| 83 |
+
def _log(self, msg: str):
|
| 84 |
+
if self.verbose:
|
| 85 |
+
print(msg)
|
| 86 |
+
|
| 87 |
+
def _resolve_texture_path(self, texture_path: str) -> Optional[Path]:
|
| 88 |
+
texture_path = texture_path.strip("@")
|
| 89 |
+
|
| 90 |
+
# Handle UDIM textures - replace <UDIM> with first available tile
|
| 91 |
+
if "<UDIM>" in texture_path:
|
| 92 |
+
# Try common UDIM tile numbers
|
| 93 |
+
for udim in ["1001", "1002", "1003", "1004"]:
|
| 94 |
+
resolved = self._resolve_texture_path(
|
| 95 |
+
texture_path.replace("<UDIM>", udim)
|
| 96 |
+
)
|
| 97 |
+
if resolved:
|
| 98 |
+
return resolved
|
| 99 |
+
return None
|
| 100 |
+
|
| 101 |
+
# Already absolute
|
| 102 |
+
if Path(texture_path).is_absolute():
|
| 103 |
+
p = Path(texture_path)
|
| 104 |
+
return p if p.exists() else None
|
| 105 |
+
|
| 106 |
+
# Try relative to USD directory
|
| 107 |
+
candidates = [
|
| 108 |
+
self.usd_dir / texture_path,
|
| 109 |
+
self.usd_dir / "textures" / Path(texture_path).name,
|
| 110 |
+
self.usd_dir / "Textures" / Path(texture_path).name,
|
| 111 |
+
self.usd_dir / ".." / texture_path,
|
| 112 |
+
self.usd_dir / ".." / "textures" / Path(texture_path).name,
|
| 113 |
+
self.usd_dir / ".." / "materials" / "textures" / Path(texture_path).name,
|
| 114 |
+
]
|
| 115 |
+
|
| 116 |
+
for p in candidates:
|
| 117 |
+
if p.exists():
|
| 118 |
+
return p.resolve()
|
| 119 |
+
|
| 120 |
+
# Fuzzy matching: look for files containing the texture name
|
| 121 |
+
texture_name = Path(texture_path).stem # e.g., "Iron_BaseColor"
|
| 122 |
+
texture_ext = Path(texture_path).suffix # e.g., ".png"
|
| 123 |
+
|
| 124 |
+
# Search in Textures folders
|
| 125 |
+
search_dirs = [
|
| 126 |
+
self.usd_dir / "Textures",
|
| 127 |
+
self.usd_dir / "textures",
|
| 128 |
+
self.usd_dir / ".." / "Textures",
|
| 129 |
+
self.usd_dir / ".." / "textures",
|
| 130 |
+
]
|
| 131 |
+
|
| 132 |
+
for search_dir in search_dirs:
|
| 133 |
+
if search_dir.exists():
|
| 134 |
+
for f in search_dir.iterdir():
|
| 135 |
+
# Check if the file contains the texture name (fuzzy match)
|
| 136 |
+
if (
|
| 137 |
+
texture_name in f.stem
|
| 138 |
+
and f.suffix.lower() == texture_ext.lower()
|
| 139 |
+
):
|
| 140 |
+
self._log(f" (fuzzy match: {texture_name} -> {f.name})")
|
| 141 |
+
return f.resolve()
|
| 142 |
+
|
| 143 |
+
return None
|
| 144 |
+
|
| 145 |
+
def _categorize_input(self, name: str) -> str:
|
| 146 |
+
name_lower = name.lower()
|
| 147 |
+
|
| 148 |
+
# Check for texture/color type based on common patterns
|
| 149 |
+
if any(
|
| 150 |
+
x in name_lower for x in ["diffuse", "albedo", "basecolor", "base_color"]
|
| 151 |
+
):
|
| 152 |
+
return "diffuse"
|
| 153 |
+
elif any(x in name_lower for x in ["normal", "bump"]):
|
| 154 |
+
return "normal"
|
| 155 |
+
elif any(x in name_lower for x in ["rough"]):
|
| 156 |
+
return "roughness"
|
| 157 |
+
elif any(x in name_lower for x in ["metal"]):
|
| 158 |
+
return "metallic"
|
| 159 |
+
elif any(x in name_lower for x in ["orm", "occlusion"]):
|
| 160 |
+
return "orm"
|
| 161 |
+
elif any(x in name_lower for x in ["opacity", "alpha"]):
|
| 162 |
+
return "opacity"
|
| 163 |
+
else:
|
| 164 |
+
return name # Use original name if no match
|
| 165 |
+
|
| 166 |
+
def _find_fallback_textures(self, material_name: str) -> Dict[str, str]:
|
| 167 |
+
textures = {}
|
| 168 |
+
|
| 169 |
+
# Search in Textures folders
|
| 170 |
+
search_dirs = [
|
| 171 |
+
self.usd_dir / "Textures",
|
| 172 |
+
self.usd_dir / "textures",
|
| 173 |
+
]
|
| 174 |
+
|
| 175 |
+
for search_dir in search_dirs:
|
| 176 |
+
if not search_dir.exists():
|
| 177 |
+
continue
|
| 178 |
+
|
| 179 |
+
# Find all unique texture prefixes (e.g., BlueRug from BlueRug_BaseColor.png)
|
| 180 |
+
texture_files = list(search_dir.glob("*.png")) + list(
|
| 181 |
+
search_dir.glob("*.jpg")
|
| 182 |
+
)
|
| 183 |
+
if not texture_files:
|
| 184 |
+
continue
|
| 185 |
+
|
| 186 |
+
# Group by prefix (before _BaseColor, _N, _R, etc.)
|
| 187 |
+
prefixes = set()
|
| 188 |
+
for f in texture_files:
|
| 189 |
+
stem = f.stem
|
| 190 |
+
for suffix in [
|
| 191 |
+
"_BaseColor",
|
| 192 |
+
"_basecolor",
|
| 193 |
+
"_A",
|
| 194 |
+
"_albedo",
|
| 195 |
+
"_diffuse",
|
| 196 |
+
"_N",
|
| 197 |
+
"_Normal",
|
| 198 |
+
"_normal",
|
| 199 |
+
"_R",
|
| 200 |
+
"_Roughness",
|
| 201 |
+
"_roughness",
|
| 202 |
+
]:
|
| 203 |
+
if suffix in stem:
|
| 204 |
+
prefix = stem.split(suffix)[0]
|
| 205 |
+
prefixes.add(prefix)
|
| 206 |
+
break
|
| 207 |
+
|
| 208 |
+
# Use the first available texture set
|
| 209 |
+
if prefixes:
|
| 210 |
+
prefix = sorted(prefixes)[0] # Pick first alphabetically
|
| 211 |
+
self._log(f" (fallback: using {prefix}_* textures)")
|
| 212 |
+
|
| 213 |
+
# Find matching textures
|
| 214 |
+
for f in texture_files:
|
| 215 |
+
if f.stem.startswith(prefix):
|
| 216 |
+
stem_lower = f.stem.lower()
|
| 217 |
+
if any(
|
| 218 |
+
x in stem_lower
|
| 219 |
+
for x in ["basecolor", "_a", "albedo", "diffuse"]
|
| 220 |
+
):
|
| 221 |
+
textures["diffuse"] = str(f.resolve())
|
| 222 |
+
self._log(f" ✓ fallback diffuse: {f.name}")
|
| 223 |
+
elif any(x in stem_lower for x in ["normal", "_n"]):
|
| 224 |
+
textures["normal"] = str(f.resolve())
|
| 225 |
+
self._log(f" ✓ fallback normal: {f.name}")
|
| 226 |
+
elif any(x in stem_lower for x in ["rough", "_r"]):
|
| 227 |
+
textures["roughness"] = str(f.resolve())
|
| 228 |
+
self._log(f" ✓ fallback roughness: {f.name}")
|
| 229 |
+
|
| 230 |
+
if textures:
|
| 231 |
+
return textures
|
| 232 |
+
|
| 233 |
+
return textures
|
| 234 |
+
|
| 235 |
+
def _extract_textures_from_shader(self, shader: UsdShade.Shader) -> Dict[str, any]:
|
| 236 |
+
result = {}
|
| 237 |
+
|
| 238 |
+
for shader_input in shader.GetInputs():
|
| 239 |
+
val = shader_input.Get()
|
| 240 |
+
if val is None:
|
| 241 |
+
continue
|
| 242 |
+
|
| 243 |
+
input_name = shader_input.GetBaseName()
|
| 244 |
+
category = self._categorize_input(input_name)
|
| 245 |
+
|
| 246 |
+
# Texture path (AssetPath)
|
| 247 |
+
if isinstance(val, Sdf.AssetPath) and val.path:
|
| 248 |
+
texture_path = val.path.strip("@")
|
| 249 |
+
resolved = self._resolve_texture_path(texture_path)
|
| 250 |
+
if resolved:
|
| 251 |
+
result[category] = str(resolved)
|
| 252 |
+
self._log(f" ✓ {input_name} -> {category}: {resolved.name}")
|
| 253 |
+
else:
|
| 254 |
+
self._log(f" ✗ {input_name}: {texture_path} (not resolved)")
|
| 255 |
+
|
| 256 |
+
# Color value (Vec3)
|
| 257 |
+
elif (
|
| 258 |
+
hasattr(val, "__len__")
|
| 259 |
+
and len(val) == 3
|
| 260 |
+
and "color" in input_name.lower()
|
| 261 |
+
):
|
| 262 |
+
result[f"{category}_color"] = (
|
| 263 |
+
float(val[0]),
|
| 264 |
+
float(val[1]),
|
| 265 |
+
float(val[2]),
|
| 266 |
+
)
|
| 267 |
+
self._log(
|
| 268 |
+
f" ✓ {input_name} -> {category}_color: ({val[0]:.3f}, {val[1]:.3f}, {val[2]:.3f})"
|
| 269 |
+
)
|
| 270 |
+
|
| 271 |
+
return result
|
| 272 |
+
|
| 273 |
+
def _extract_textures_from_mdl(self, mdl_path: Path) -> Dict[str, str]:
|
| 274 |
+
textures = {}
|
| 275 |
+
|
| 276 |
+
if not mdl_path.exists():
|
| 277 |
+
return textures
|
| 278 |
+
|
| 279 |
+
try:
|
| 280 |
+
content = mdl_path.read_text()
|
| 281 |
+
|
| 282 |
+
# Parse texture references
|
| 283 |
+
type_mapping = {
|
| 284 |
+
"diffuse_texture": "diffuse",
|
| 285 |
+
"normalmap_texture": "normal",
|
| 286 |
+
"reflectionroughness_texture": "roughness",
|
| 287 |
+
"metallic_texture": "metallic",
|
| 288 |
+
"ORM_texture": "orm",
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
for tex_type, canonical_name in type_mapping.items():
|
| 292 |
+
pattern = rf'{tex_type}:\s*texture_2d\("([^"]+)"'
|
| 293 |
+
match = re.search(pattern, content)
|
| 294 |
+
if match:
|
| 295 |
+
rel_path = match.group(1)
|
| 296 |
+
# MDL paths are relative to the MDL file location
|
| 297 |
+
resolved = self._resolve_texture_path(rel_path)
|
| 298 |
+
if not resolved:
|
| 299 |
+
# Try relative to MDL file directory
|
| 300 |
+
mdl_dir = mdl_path.parent
|
| 301 |
+
candidates = [
|
| 302 |
+
mdl_dir / rel_path,
|
| 303 |
+
mdl_dir / "textures" / Path(rel_path).name,
|
| 304 |
+
]
|
| 305 |
+
for c in candidates:
|
| 306 |
+
if c.exists():
|
| 307 |
+
resolved = c.resolve()
|
| 308 |
+
break
|
| 309 |
+
|
| 310 |
+
if resolved:
|
| 311 |
+
textures[canonical_name] = str(resolved)
|
| 312 |
+
self._log(f" ✓ {canonical_name}: {resolved.name} (from MDL)")
|
| 313 |
+
else:
|
| 314 |
+
self._log(
|
| 315 |
+
f" ✗ {canonical_name}: {rel_path} (MDL, not resolved)"
|
| 316 |
+
)
|
| 317 |
+
|
| 318 |
+
except Exception as e:
|
| 319 |
+
self._log(f" Error parsing MDL {mdl_path}: {e}")
|
| 320 |
+
|
| 321 |
+
return textures
|
| 322 |
+
|
| 323 |
+
def _find_mdl_for_material(self, material_prim: Usd.Prim) -> Optional[Path]:
|
| 324 |
+
for child in material_prim.GetChildren():
|
| 325 |
+
if child.GetTypeName() == "Shader":
|
| 326 |
+
# Check for MDL source asset
|
| 327 |
+
mdl_attr = child.GetAttribute("info:mdl:sourceAsset")
|
| 328 |
+
if mdl_attr and mdl_attr.Get():
|
| 329 |
+
mdl_path_val = mdl_attr.Get()
|
| 330 |
+
if isinstance(mdl_path_val, Sdf.AssetPath) and mdl_path_val.path:
|
| 331 |
+
mdl_rel = mdl_path_val.path.strip("@")
|
| 332 |
+
|
| 333 |
+
# Try to resolve MDL path
|
| 334 |
+
candidates = [
|
| 335 |
+
self.usd_dir / mdl_rel,
|
| 336 |
+
self.usd_dir / "materials" / Path(mdl_rel).name,
|
| 337 |
+
self.usd_dir / ".." / "materials" / Path(mdl_rel).name,
|
| 338 |
+
]
|
| 339 |
+
|
| 340 |
+
for c in candidates:
|
| 341 |
+
if c.exists():
|
| 342 |
+
return c.resolve()
|
| 343 |
+
|
| 344 |
+
return None
|
| 345 |
+
|
| 346 |
+
def _get_geomsubset_bindings(
|
| 347 |
+
self, mesh_prim: Usd.Prim
|
| 348 |
+
) -> Dict[str, Tuple[str, List[int]]]:
|
| 349 |
+
bindings = {}
|
| 350 |
+
|
| 351 |
+
for child in mesh_prim.GetChildren():
|
| 352 |
+
if child.GetTypeName() == "GeomSubset":
|
| 353 |
+
subset_name = child.GetName()
|
| 354 |
+
|
| 355 |
+
# Get face indices for this subset
|
| 356 |
+
indices_attr = child.GetAttribute("indices")
|
| 357 |
+
face_indices = (
|
| 358 |
+
list(indices_attr.Get())
|
| 359 |
+
if indices_attr and indices_attr.Get()
|
| 360 |
+
else []
|
| 361 |
+
)
|
| 362 |
+
|
| 363 |
+
# Get material binding
|
| 364 |
+
mat_path = None
|
| 365 |
+
binding_rel = child.GetRelationship("material:binding")
|
| 366 |
+
if binding_rel:
|
| 367 |
+
targets = binding_rel.GetTargets()
|
| 368 |
+
if targets:
|
| 369 |
+
mat_path = str(targets[0])
|
| 370 |
+
|
| 371 |
+
if mat_path:
|
| 372 |
+
bindings[subset_name] = (mat_path, face_indices)
|
| 373 |
+
|
| 374 |
+
return bindings
|
| 375 |
+
|
| 376 |
+
def extract(self) -> bool:
|
| 377 |
+
try:
|
| 378 |
+
self.stage = Usd.Stage.Open(str(self.usd_path))
|
| 379 |
+
except Exception as e:
|
| 380 |
+
print(f"ERROR: Could not open USD: {self.usd_path}")
|
| 381 |
+
print(f" {e}")
|
| 382 |
+
return False
|
| 383 |
+
|
| 384 |
+
if not self.stage:
|
| 385 |
+
return False
|
| 386 |
+
|
| 387 |
+
self._log(f"\n=== Extracting from: {self.usd_path.name} ===")
|
| 388 |
+
|
| 389 |
+
# Step 1: Find all materials and extract textures
|
| 390 |
+
self._log("\n--- Materials ---")
|
| 391 |
+
for prim in self.stage.Traverse():
|
| 392 |
+
if prim.GetTypeName() == "Material":
|
| 393 |
+
mat_path = str(prim.GetPath())
|
| 394 |
+
self._log(f"\nMaterial: {prim.GetName()}")
|
| 395 |
+
|
| 396 |
+
textures = {}
|
| 397 |
+
|
| 398 |
+
# Try extracting from shader inputs
|
| 399 |
+
for child in prim.GetChildren():
|
| 400 |
+
if child.GetTypeName() == "Shader":
|
| 401 |
+
shader = UsdShade.Shader(child)
|
| 402 |
+
textures.update(self._extract_textures_from_shader(shader))
|
| 403 |
+
|
| 404 |
+
# If no textures found, try MDL
|
| 405 |
+
if not textures:
|
| 406 |
+
mdl_path = self._find_mdl_for_material(prim)
|
| 407 |
+
if mdl_path:
|
| 408 |
+
self._log(f" Using MDL: {mdl_path.name}")
|
| 409 |
+
textures = self._extract_textures_from_mdl(mdl_path)
|
| 410 |
+
|
| 411 |
+
# Fallback: if still no textures, search Textures folder for any available
|
| 412 |
+
if not textures:
|
| 413 |
+
textures = self._find_fallback_textures(prim.GetName())
|
| 414 |
+
|
| 415 |
+
self.materials[mat_path] = textures
|
| 416 |
+
|
| 417 |
+
# Step 2: Find all meshes and their material bindings
|
| 418 |
+
self._log("\n--- Meshes ---")
|
| 419 |
+
for prim in self.stage.Traverse():
|
| 420 |
+
if prim.GetTypeName() == "Mesh":
|
| 421 |
+
mesh_path = str(prim.GetPath())
|
| 422 |
+
mesh_name = prim.GetName()
|
| 423 |
+
|
| 424 |
+
# Get direct material binding first
|
| 425 |
+
binding_api = UsdShade.MaterialBindingAPI(prim)
|
| 426 |
+
bound_material = binding_api.ComputeBoundMaterial()[0]
|
| 427 |
+
mat_path = str(bound_material.GetPath()) if bound_material else None
|
| 428 |
+
|
| 429 |
+
# Check for GeomSubset bindings (per-face materials)
|
| 430 |
+
geomsubset_bindings = self._get_geomsubset_bindings(prim)
|
| 431 |
+
|
| 432 |
+
# If no direct binding but has GeomSubsets, use first one as default
|
| 433 |
+
if not mat_path and geomsubset_bindings:
|
| 434 |
+
first_subset = list(geomsubset_bindings.values())[0]
|
| 435 |
+
mat_path = first_subset[0]
|
| 436 |
+
|
| 437 |
+
self.mesh_materials[mesh_path] = mat_path
|
| 438 |
+
|
| 439 |
+
# Get mesh geometry
|
| 440 |
+
mesh = UsdGeom.Mesh(prim)
|
| 441 |
+
points_local = mesh.GetPointsAttr().Get()
|
| 442 |
+
face_counts = mesh.GetFaceVertexCountsAttr().Get()
|
| 443 |
+
face_indices = mesh.GetFaceVertexIndicesAttr().Get()
|
| 444 |
+
|
| 445 |
+
# Apply world transform to vertices
|
| 446 |
+
xformable = UsdGeom.Xformable(prim)
|
| 447 |
+
world_transform = xformable.ComputeLocalToWorldTransform(
|
| 448 |
+
Usd.TimeCode.Default()
|
| 449 |
+
)
|
| 450 |
+
|
| 451 |
+
# Transform points to world space
|
| 452 |
+
points = []
|
| 453 |
+
if points_local:
|
| 454 |
+
for p in points_local:
|
| 455 |
+
# Apply 4x4 transform matrix to point
|
| 456 |
+
p_world = world_transform.Transform(Gf.Vec3d(p[0], p[1], p[2]))
|
| 457 |
+
points.append(Gf.Vec3f(p_world[0], p_world[1], p_world[2]))
|
| 458 |
+
else:
|
| 459 |
+
points = None
|
| 460 |
+
|
| 461 |
+
# Get UVs and check interpolation
|
| 462 |
+
uvs = None
|
| 463 |
+
uv_interpolation = None
|
| 464 |
+
uv_indices = None
|
| 465 |
+
for primvar_name in ["st", "uvs", "uv", "UVMap", "texCoords"]:
|
| 466 |
+
primvar = UsdGeom.PrimvarsAPI(prim).GetPrimvar(primvar_name)
|
| 467 |
+
if primvar and primvar.Get():
|
| 468 |
+
uvs = primvar.Get()
|
| 469 |
+
uv_interpolation = primvar.GetInterpolation()
|
| 470 |
+
# For indexed primvars, get the indices
|
| 471 |
+
if primvar.IsIndexed():
|
| 472 |
+
uv_indices = primvar.GetIndices()
|
| 473 |
+
break
|
| 474 |
+
|
| 475 |
+
self.meshes[mesh_path] = {
|
| 476 |
+
"name": mesh_name,
|
| 477 |
+
"material": mat_path,
|
| 478 |
+
"points": points,
|
| 479 |
+
"face_counts": face_counts,
|
| 480 |
+
"face_indices": face_indices,
|
| 481 |
+
"uvs": uvs,
|
| 482 |
+
"uv_interpolation": uv_interpolation,
|
| 483 |
+
"uv_indices": uv_indices,
|
| 484 |
+
"geomsubsets": geomsubset_bindings, # Store GeomSubset bindings
|
| 485 |
+
}
|
| 486 |
+
|
| 487 |
+
has_tex = bool(self.materials.get(mat_path))
|
| 488 |
+
if geomsubset_bindings:
|
| 489 |
+
has_tex = any(
|
| 490 |
+
self.materials.get(m) for m, _ in geomsubset_bindings.values()
|
| 491 |
+
)
|
| 492 |
+
|
| 493 |
+
status = "✓" if has_tex else "○"
|
| 494 |
+
|
| 495 |
+
if geomsubset_bindings:
|
| 496 |
+
self._log(
|
| 497 |
+
f" {status} {mesh_name} (GeomSubsets: {len(geomsubset_bindings)})"
|
| 498 |
+
)
|
| 499 |
+
for subset_name, (sub_mat, _) in geomsubset_bindings.items():
|
| 500 |
+
sub_mat_name = sub_mat.split("/")[-1] if sub_mat else "none"
|
| 501 |
+
self._log(f" {subset_name} -> {sub_mat_name}")
|
| 502 |
+
else:
|
| 503 |
+
self._log(
|
| 504 |
+
f" {status} {mesh_name} -> {mat_path or '(no material)'}"
|
| 505 |
+
)
|
| 506 |
+
|
| 507 |
+
if not self.meshes:
|
| 508 |
+
self._log("WARNING: No meshes found in USD file")
|
| 509 |
+
return False
|
| 510 |
+
|
| 511 |
+
has_valid_mesh = any(
|
| 512 |
+
mesh_data.get("points") for mesh_data in self.meshes.values()
|
| 513 |
+
)
|
| 514 |
+
if not has_valid_mesh:
|
| 515 |
+
self._log("WARNING: No meshes with valid geometry found")
|
| 516 |
+
return False
|
| 517 |
+
|
| 518 |
+
return True
|
| 519 |
+
|
| 520 |
+
def export_obj(
|
| 521 |
+
self, output_dir: Path, normalize: bool = True
|
| 522 |
+
) -> Tuple[Optional[Path], Optional[Path]]:
|
| 523 |
+
output_dir = Path(output_dir)
|
| 524 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 525 |
+
|
| 526 |
+
obj_path = output_dir / "model.obj"
|
| 527 |
+
mtl_path = output_dir / "model.mtl"
|
| 528 |
+
|
| 529 |
+
# Collect all vertices, faces, UVs
|
| 530 |
+
all_vertices = []
|
| 531 |
+
all_faces = []
|
| 532 |
+
all_uvs = []
|
| 533 |
+
face_materials = []
|
| 534 |
+
vertex_offset = 0
|
| 535 |
+
uv_offset = 0
|
| 536 |
+
|
| 537 |
+
material_list = [] # List of unique materials used
|
| 538 |
+
material_map = {} # material_path -> index
|
| 539 |
+
|
| 540 |
+
for mesh_path, mesh_data in self.meshes.items():
|
| 541 |
+
if not mesh_data["points"]:
|
| 542 |
+
continue
|
| 543 |
+
|
| 544 |
+
points = mesh_data["points"]
|
| 545 |
+
face_counts = mesh_data["face_counts"]
|
| 546 |
+
face_indices = mesh_data["face_indices"]
|
| 547 |
+
uvs = mesh_data["uvs"]
|
| 548 |
+
uv_interpolation = mesh_data.get("uv_interpolation")
|
| 549 |
+
uv_indices = mesh_data.get("uv_indices")
|
| 550 |
+
mat_path = mesh_data["material"]
|
| 551 |
+
geomsubsets = mesh_data.get("geomsubsets", {})
|
| 552 |
+
|
| 553 |
+
# Add vertices
|
| 554 |
+
for p in points:
|
| 555 |
+
all_vertices.append((float(p[0]), float(p[1]), float(p[2])))
|
| 556 |
+
|
| 557 |
+
# Add UVs - handle different interpolation modes
|
| 558 |
+
mesh_uv_offset = len(all_uvs)
|
| 559 |
+
if uvs:
|
| 560 |
+
for uv in uvs:
|
| 561 |
+
all_uvs.append((float(uv[0]), float(uv[1])))
|
| 562 |
+
|
| 563 |
+
# Track UV mapping for this mesh
|
| 564 |
+
# For faceVarying, we need to map face-vertex index to UV index
|
| 565 |
+
mesh_data["_uv_offset"] = mesh_uv_offset
|
| 566 |
+
mesh_data["_has_uvs"] = uvs is not None and len(uvs) > 0
|
| 567 |
+
|
| 568 |
+
# Build face-to-material mapping for GeomSubsets
|
| 569 |
+
face_to_subset_mat = {}
|
| 570 |
+
if geomsubsets:
|
| 571 |
+
for subset_name, (
|
| 572 |
+
sub_mat_path,
|
| 573 |
+
sub_face_indices,
|
| 574 |
+
) in geomsubsets.items():
|
| 575 |
+
for face_idx in sub_face_indices:
|
| 576 |
+
face_to_subset_mat[face_idx] = sub_mat_path
|
| 577 |
+
# Ensure this material is in our list
|
| 578 |
+
if sub_mat_path and sub_mat_path not in material_map:
|
| 579 |
+
material_map[sub_mat_path] = len(material_list)
|
| 580 |
+
material_list.append(sub_mat_path)
|
| 581 |
+
|
| 582 |
+
# Track default material
|
| 583 |
+
if mat_path and mat_path not in material_map:
|
| 584 |
+
material_map[mat_path] = len(material_list)
|
| 585 |
+
material_list.append(mat_path)
|
| 586 |
+
|
| 587 |
+
# Add faces with proper UV indexing
|
| 588 |
+
idx = 0
|
| 589 |
+
face_num = 0
|
| 590 |
+
face_vertex_idx = 0 # Running index for faceVarying UVs
|
| 591 |
+
|
| 592 |
+
for count in face_counts:
|
| 593 |
+
# Determine material for this face
|
| 594 |
+
if face_num in face_to_subset_mat:
|
| 595 |
+
face_mat = face_to_subset_mat[face_num]
|
| 596 |
+
else:
|
| 597 |
+
face_mat = mat_path
|
| 598 |
+
|
| 599 |
+
mat_idx = material_map.get(face_mat, 0) if face_mat else 0
|
| 600 |
+
|
| 601 |
+
# Determine UV indices based on interpolation mode
|
| 602 |
+
def get_uv_idx(local_vert_idx, fv_offset):
|
| 603 |
+
if not mesh_data["_has_uvs"]:
|
| 604 |
+
return None
|
| 605 |
+
vertex_idx = face_indices[local_vert_idx]
|
| 606 |
+
|
| 607 |
+
if uv_interpolation == "faceVarying":
|
| 608 |
+
if uv_indices is not None:
|
| 609 |
+
# Indexed faceVarying: indices are per face-vertex
|
| 610 |
+
return mesh_uv_offset + int(uv_indices[fv_offset])
|
| 611 |
+
else:
|
| 612 |
+
# Non-indexed faceVarying: sequential per face-vertex
|
| 613 |
+
return mesh_uv_offset + fv_offset
|
| 614 |
+
else:
|
| 615 |
+
# vertex interpolation
|
| 616 |
+
if uv_indices is not None:
|
| 617 |
+
# Indexed vertex: indices are per-vertex
|
| 618 |
+
return mesh_uv_offset + int(uv_indices[vertex_idx])
|
| 619 |
+
else:
|
| 620 |
+
# Non-indexed vertex: UV index matches vertex index
|
| 621 |
+
return mesh_uv_offset + vertex_idx
|
| 622 |
+
|
| 623 |
+
if count == 3:
|
| 624 |
+
v_indices = [
|
| 625 |
+
face_indices[idx] + vertex_offset,
|
| 626 |
+
face_indices[idx + 1] + vertex_offset,
|
| 627 |
+
face_indices[idx + 2] + vertex_offset,
|
| 628 |
+
]
|
| 629 |
+
uv_idxs = [
|
| 630 |
+
get_uv_idx(idx, face_vertex_idx),
|
| 631 |
+
get_uv_idx(idx + 1, face_vertex_idx + 1),
|
| 632 |
+
get_uv_idx(idx + 2, face_vertex_idx + 2),
|
| 633 |
+
]
|
| 634 |
+
all_faces.append((v_indices, uv_idxs))
|
| 635 |
+
face_materials.append(mat_idx)
|
| 636 |
+
face_vertex_idx += 3
|
| 637 |
+
elif count == 4:
|
| 638 |
+
# Triangulate quad
|
| 639 |
+
v_indices1 = [
|
| 640 |
+
face_indices[idx] + vertex_offset,
|
| 641 |
+
face_indices[idx + 1] + vertex_offset,
|
| 642 |
+
face_indices[idx + 2] + vertex_offset,
|
| 643 |
+
]
|
| 644 |
+
v_indices2 = [
|
| 645 |
+
face_indices[idx] + vertex_offset,
|
| 646 |
+
face_indices[idx + 2] + vertex_offset,
|
| 647 |
+
face_indices[idx + 3] + vertex_offset,
|
| 648 |
+
]
|
| 649 |
+
uv_idxs1 = [
|
| 650 |
+
get_uv_idx(idx, face_vertex_idx),
|
| 651 |
+
get_uv_idx(idx + 1, face_vertex_idx + 1),
|
| 652 |
+
get_uv_idx(idx + 2, face_vertex_idx + 2),
|
| 653 |
+
]
|
| 654 |
+
uv_idxs2 = [
|
| 655 |
+
get_uv_idx(idx, face_vertex_idx),
|
| 656 |
+
get_uv_idx(idx + 2, face_vertex_idx + 2),
|
| 657 |
+
get_uv_idx(idx + 3, face_vertex_idx + 3),
|
| 658 |
+
]
|
| 659 |
+
all_faces.append((v_indices1, uv_idxs1))
|
| 660 |
+
all_faces.append((v_indices2, uv_idxs2))
|
| 661 |
+
face_materials.append(mat_idx)
|
| 662 |
+
face_materials.append(mat_idx)
|
| 663 |
+
face_vertex_idx += 4
|
| 664 |
+
else:
|
| 665 |
+
# Skip n-gons
|
| 666 |
+
face_vertex_idx += count
|
| 667 |
+
|
| 668 |
+
idx += count
|
| 669 |
+
face_num += 1
|
| 670 |
+
|
| 671 |
+
vertex_offset += len(points)
|
| 672 |
+
|
| 673 |
+
if not all_vertices:
|
| 674 |
+
return None, None
|
| 675 |
+
|
| 676 |
+
# Normalize vertices to fit in [-0.5, 0.5]^3 centered at origin
|
| 677 |
+
# Use margin factor to ensure object fits fully in camera frame at all angles
|
| 678 |
+
MARGIN_FACTOR = 0.85 # Scale to 85% of unit cube to leave padding
|
| 679 |
+
|
| 680 |
+
if normalize and all_vertices:
|
| 681 |
+
# Compute bounding box
|
| 682 |
+
xs = [v[0] for v in all_vertices]
|
| 683 |
+
ys = [v[1] for v in all_vertices]
|
| 684 |
+
zs = [v[2] for v in all_vertices]
|
| 685 |
+
|
| 686 |
+
min_x, max_x = min(xs), max(xs)
|
| 687 |
+
min_y, max_y = min(ys), max(ys)
|
| 688 |
+
min_z, max_z = min(zs), max(zs)
|
| 689 |
+
|
| 690 |
+
# Compute center and scale
|
| 691 |
+
center_x = (min_x + max_x) / 2
|
| 692 |
+
center_y = (min_y + max_y) / 2
|
| 693 |
+
center_z = (min_z + max_z) / 2
|
| 694 |
+
|
| 695 |
+
extent_x = max_x - min_x
|
| 696 |
+
extent_y = max_y - min_y
|
| 697 |
+
extent_z = max_z - min_z
|
| 698 |
+
max_extent = max(extent_x, extent_y, extent_z)
|
| 699 |
+
|
| 700 |
+
if max_extent > 0:
|
| 701 |
+
# Scale to fit in unit cube, with margin for camera framing
|
| 702 |
+
scale = MARGIN_FACTOR / max_extent
|
| 703 |
+
else:
|
| 704 |
+
scale = 1.0
|
| 705 |
+
|
| 706 |
+
# Apply normalization: center then scale
|
| 707 |
+
all_vertices = [
|
| 708 |
+
(
|
| 709 |
+
(v[0] - center_x) * scale,
|
| 710 |
+
(v[1] - center_y) * scale,
|
| 711 |
+
(v[2] - center_z) * scale,
|
| 712 |
+
)
|
| 713 |
+
for v in all_vertices
|
| 714 |
+
]
|
| 715 |
+
|
| 716 |
+
self._log(f"\nNormalization applied:")
|
| 717 |
+
self._log(
|
| 718 |
+
f" Original bounds: X[{min_x:.2f}, {max_x:.2f}], Y[{min_y:.2f}, {max_y:.2f}], Z[{min_z:.2f}, {max_z:.2f}]"
|
| 719 |
+
)
|
| 720 |
+
self._log(f" Scale factor: {scale:.6f} (with {MARGIN_FACTOR:.0%} margin)")
|
| 721 |
+
self._log(
|
| 722 |
+
f" Center offset: ({center_x:.2f}, {center_y:.2f}, {center_z:.2f})"
|
| 723 |
+
)
|
| 724 |
+
|
| 725 |
+
# Copy textures and write MTL
|
| 726 |
+
with open(mtl_path, "w") as f:
|
| 727 |
+
for mat_path in material_list:
|
| 728 |
+
mat_name = mat_path.split("/")[-1] if mat_path else "default_material"
|
| 729 |
+
textures = self.materials.get(mat_path, {})
|
| 730 |
+
|
| 731 |
+
f.write(f"newmtl {mat_name}\n")
|
| 732 |
+
f.write("Ka 0.2 0.2 0.2\n")
|
| 733 |
+
|
| 734 |
+
# Use diffuse color constant if available, otherwise default gray
|
| 735 |
+
if "diffuse_color" in textures:
|
| 736 |
+
color = textures["diffuse_color"]
|
| 737 |
+
f.write(f"Kd {color[0]:.6f} {color[1]:.6f} {color[2]:.6f}\n")
|
| 738 |
+
self._log(
|
| 739 |
+
f" Material {mat_name}: using diffuse color ({color[0]:.3f}, {color[1]:.3f}, {color[2]:.3f})"
|
| 740 |
+
)
|
| 741 |
+
else:
|
| 742 |
+
f.write("Kd 0.8 0.8 0.8\n")
|
| 743 |
+
|
| 744 |
+
f.write("Ks 0.2 0.2 0.2\n")
|
| 745 |
+
f.write("Ns 50.0\n")
|
| 746 |
+
f.write("d 1.0\n")
|
| 747 |
+
f.write("illum 2\n")
|
| 748 |
+
|
| 749 |
+
for tex_type, tex_value in textures.items():
|
| 750 |
+
# Skip color constants (they're tuples, not paths)
|
| 751 |
+
if isinstance(tex_value, tuple):
|
| 752 |
+
continue
|
| 753 |
+
|
| 754 |
+
tex_path = tex_value
|
| 755 |
+
if os.path.exists(tex_path):
|
| 756 |
+
# Copy texture to output dir
|
| 757 |
+
tex_name = os.path.basename(tex_path)
|
| 758 |
+
dest = output_dir / tex_name
|
| 759 |
+
if not dest.exists():
|
| 760 |
+
shutil.copy2(tex_path, dest)
|
| 761 |
+
|
| 762 |
+
# Write to MTL
|
| 763 |
+
if tex_type == "diffuse":
|
| 764 |
+
f.write(f"map_Kd {tex_name}\n")
|
| 765 |
+
elif tex_type == "normal":
|
| 766 |
+
f.write(f"map_Bump {tex_name}\n")
|
| 767 |
+
elif tex_type == "roughness":
|
| 768 |
+
f.write(f"map_Ns {tex_name}\n")
|
| 769 |
+
elif tex_type == "metallic":
|
| 770 |
+
f.write(f"map_Ks {tex_name}\n")
|
| 771 |
+
|
| 772 |
+
f.write("\n")
|
| 773 |
+
|
| 774 |
+
# Write OBJ
|
| 775 |
+
with open(obj_path, "w") as f:
|
| 776 |
+
f.write(f"mtllib model.mtl\n\n")
|
| 777 |
+
|
| 778 |
+
for v in all_vertices:
|
| 779 |
+
f.write(f"v {v[0]} {v[1]} {v[2]}\n")
|
| 780 |
+
|
| 781 |
+
f.write("\n")
|
| 782 |
+
for uv in all_uvs:
|
| 783 |
+
f.write(f"vt {uv[0]} {uv[1]}\n")
|
| 784 |
+
|
| 785 |
+
f.write("\n")
|
| 786 |
+
|
| 787 |
+
# Group faces by material
|
| 788 |
+
mat_faces = defaultdict(list)
|
| 789 |
+
for i, face_data in enumerate(all_faces):
|
| 790 |
+
mat_faces[face_materials[i]].append(face_data)
|
| 791 |
+
|
| 792 |
+
for mat_idx, faces in mat_faces.items():
|
| 793 |
+
mat_path = (
|
| 794 |
+
material_list[mat_idx] if mat_idx < len(material_list) else None
|
| 795 |
+
)
|
| 796 |
+
mat_name = mat_path.split("/")[-1] if mat_path else "default_material"
|
| 797 |
+
|
| 798 |
+
f.write(f"usemtl {mat_name}\n")
|
| 799 |
+
for face_data in faces:
|
| 800 |
+
v_indices, uv_indices = face_data
|
| 801 |
+
# OBJ indices are 1-based
|
| 802 |
+
if uv_indices[0] is not None:
|
| 803 |
+
# Include UV indices
|
| 804 |
+
f.write(
|
| 805 |
+
f"f {v_indices[0]+1}/{uv_indices[0]+1} {v_indices[1]+1}/{uv_indices[1]+1} {v_indices[2]+1}/{uv_indices[2]+1}\n"
|
| 806 |
+
)
|
| 807 |
+
else:
|
| 808 |
+
# No UVs, just vertex indices
|
| 809 |
+
f.write(
|
| 810 |
+
f"f {v_indices[0]+1} {v_indices[1]+1} {v_indices[2]+1}\n"
|
| 811 |
+
)
|
| 812 |
+
f.write("\n")
|
| 813 |
+
|
| 814 |
+
return obj_path, mtl_path
|
| 815 |
+
|
| 816 |
+
|
| 817 |
+
def _install_blender():
|
| 818 |
+
if not os.path.exists(BLENDER_PATH):
|
| 819 |
+
os.system("sudo apt-get update")
|
| 820 |
+
os.system(
|
| 821 |
+
"sudo apt-get install -y libxrender1 libxi6 libxkbcommon-x11-0 libsm6"
|
| 822 |
+
)
|
| 823 |
+
os.system(f"wget {BLENDER_LINK} -P {BLENDER_INSTALLATION_PATH}")
|
| 824 |
+
os.system(
|
| 825 |
+
f"tar -xvf {BLENDER_INSTALLATION_PATH}/blender-3.0.1-linux-x64.tar.xz -C {BLENDER_INSTALLATION_PATH}"
|
| 826 |
+
)
|
| 827 |
+
|
| 828 |
+
|
| 829 |
+
def render_usd(
|
| 830 |
+
usd_path: str,
|
| 831 |
+
output_dir: str,
|
| 832 |
+
num_views: int = 150,
|
| 833 |
+
resolution: int = 512,
|
| 834 |
+
verbose: bool = False,
|
| 835 |
+
) -> bool:
|
| 836 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 837 |
+
|
| 838 |
+
# Extract mesh and materials from USD
|
| 839 |
+
extractor = USDMaterialExtractor(usd_path, verbose=verbose)
|
| 840 |
+
if not extractor.extract():
|
| 841 |
+
print(f"Failed to extract from USD: {usd_path}")
|
| 842 |
+
return False
|
| 843 |
+
|
| 844 |
+
# Export to OBJ + MTL
|
| 845 |
+
temp_dir = tempfile.mkdtemp()
|
| 846 |
+
try:
|
| 847 |
+
# Don't normalize - let Blender's normalize_scene() handle it
|
| 848 |
+
obj_path, mtl_path = extractor.export_obj(Path(temp_dir), normalize=False)
|
| 849 |
+
if not obj_path:
|
| 850 |
+
print(f"Failed to export OBJ from USD: {usd_path}")
|
| 851 |
+
return False
|
| 852 |
+
|
| 853 |
+
if verbose:
|
| 854 |
+
print(f"\nExported to: {obj_path}")
|
| 855 |
+
# List textures
|
| 856 |
+
textures = list(Path(temp_dir).glob("*.png")) + list(
|
| 857 |
+
Path(temp_dir).glob("*.tga")
|
| 858 |
+
)
|
| 859 |
+
if textures:
|
| 860 |
+
print(f"Textures copied: {len(textures)}")
|
| 861 |
+
for t in textures:
|
| 862 |
+
print(f" - {t.name}")
|
| 863 |
+
|
| 864 |
+
# Generate camera views
|
| 865 |
+
yaws = []
|
| 866 |
+
pitchs = []
|
| 867 |
+
offset = (np.random.rand(), np.random.rand())
|
| 868 |
+
for i in range(num_views):
|
| 869 |
+
y, p = sphere_hammersley_sequence(i, num_views, offset)
|
| 870 |
+
yaws.append(y)
|
| 871 |
+
pitchs.append(p)
|
| 872 |
+
# Radius 2.5 ensures object corners fit in frame:
|
| 873 |
+
# Object diagonal at 0.866 from center, visible range at radius=2.5, FOV=40° is ±0.91
|
| 874 |
+
radius = [2.1] * num_views
|
| 875 |
+
fov = [40 / 180 * np.pi] * num_views
|
| 876 |
+
views = [
|
| 877 |
+
{"yaw": y, "pitch": p, "radius": r, "fov": f}
|
| 878 |
+
for y, p, r, f in zip(yaws, pitchs, radius, fov)
|
| 879 |
+
]
|
| 880 |
+
|
| 881 |
+
# Call Blender
|
| 882 |
+
blender_script = os.path.join(
|
| 883 |
+
os.path.dirname(os.path.dirname(os.path.dirname(__file__))),
|
| 884 |
+
"dataset_toolkits",
|
| 885 |
+
"blender_script",
|
| 886 |
+
"render.py",
|
| 887 |
+
)
|
| 888 |
+
|
| 889 |
+
args = [
|
| 890 |
+
BLENDER_PATH,
|
| 891 |
+
"-b",
|
| 892 |
+
"-P",
|
| 893 |
+
blender_script,
|
| 894 |
+
"--",
|
| 895 |
+
"--views",
|
| 896 |
+
json.dumps(views),
|
| 897 |
+
"--object",
|
| 898 |
+
str(obj_path),
|
| 899 |
+
"--resolution",
|
| 900 |
+
str(resolution),
|
| 901 |
+
"--output_folder",
|
| 902 |
+
output_dir,
|
| 903 |
+
"--engine",
|
| 904 |
+
"CYCLES",
|
| 905 |
+
"--save_mesh",
|
| 906 |
+
"--use_gpu", # Enable GPU acceleration
|
| 907 |
+
]
|
| 908 |
+
|
| 909 |
+
if verbose:
|
| 910 |
+
print(f"\nRunning Blender...")
|
| 911 |
+
|
| 912 |
+
call(
|
| 913 |
+
args,
|
| 914 |
+
stdout=DEVNULL if not verbose else None,
|
| 915 |
+
stderr=DEVNULL if not verbose else None,
|
| 916 |
+
)
|
| 917 |
+
|
| 918 |
+
success = os.path.exists(os.path.join(output_dir, "transforms.json"))
|
| 919 |
+
return success
|
| 920 |
+
|
| 921 |
+
finally:
|
| 922 |
+
shutil.rmtree(temp_dir, ignore_errors=True)
|
| 923 |
+
|
| 924 |
+
|
| 925 |
+
def _render_worker(
|
| 926 |
+
file_path: str,
|
| 927 |
+
sha256: str,
|
| 928 |
+
dataset: str,
|
| 929 |
+
output_dir: str,
|
| 930 |
+
num_views: int,
|
| 931 |
+
quiet: bool,
|
| 932 |
+
) -> Optional[Dict]:
|
| 933 |
+
output_folder = os.path.join(output_dir, "renders", sha256)
|
| 934 |
+
|
| 935 |
+
# Skip if already rendered
|
| 936 |
+
if os.path.exists(os.path.join(output_folder, "transforms.json")):
|
| 937 |
+
return {"sha256": sha256, "rendered": True}
|
| 938 |
+
|
| 939 |
+
success = render_usd(
|
| 940 |
+
file_path,
|
| 941 |
+
output_folder,
|
| 942 |
+
num_views=num_views,
|
| 943 |
+
resolution=512,
|
| 944 |
+
verbose=not quiet,
|
| 945 |
+
)
|
| 946 |
+
|
| 947 |
+
if success:
|
| 948 |
+
return {"sha256": sha256, "rendered": True}
|
| 949 |
+
else:
|
| 950 |
+
if not quiet:
|
| 951 |
+
print(f"Failed to render: {file_path}")
|
| 952 |
+
return None
|
| 953 |
+
|
| 954 |
+
|
| 955 |
+
def main_batch():
|
| 956 |
+
import importlib
|
| 957 |
+
import copy
|
| 958 |
+
from functools import partial
|
| 959 |
+
from easydict import EasyDict as edict
|
| 960 |
+
|
| 961 |
+
# First argument is dataset type (e.g., "allmats")
|
| 962 |
+
dataset_utils = importlib.import_module(f"dataset_toolkits.datasets.{sys.argv[1]}")
|
| 963 |
+
|
| 964 |
+
parser = argparse.ArgumentParser(
|
| 965 |
+
description="Batch render USD files with proper texture extraction"
|
| 966 |
+
)
|
| 967 |
+
parser.add_argument(
|
| 968 |
+
"--output_dir", type=str, required=True, help="Directory to save renders"
|
| 969 |
+
)
|
| 970 |
+
parser.add_argument(
|
| 971 |
+
"--filter_low_aesthetic_score",
|
| 972 |
+
type=float,
|
| 973 |
+
default=None,
|
| 974 |
+
help="Filter objects with aesthetic score lower than this value",
|
| 975 |
+
)
|
| 976 |
+
parser.add_argument(
|
| 977 |
+
"--instances",
|
| 978 |
+
type=str,
|
| 979 |
+
default=None,
|
| 980 |
+
help="Instances to process (comma-separated or file path)",
|
| 981 |
+
)
|
| 982 |
+
parser.add_argument(
|
| 983 |
+
"--num_views", type=int, default=150, help="Number of views to render"
|
| 984 |
+
)
|
| 985 |
+
parser.add_argument(
|
| 986 |
+
"--rank", type=int, default=0, help="Worker rank for distributed processing"
|
| 987 |
+
)
|
| 988 |
+
parser.add_argument(
|
| 989 |
+
"--world_size",
|
| 990 |
+
type=int,
|
| 991 |
+
default=1,
|
| 992 |
+
help="Total workers for distributed processing",
|
| 993 |
+
)
|
| 994 |
+
parser.add_argument(
|
| 995 |
+
"--max_workers", type=int, default=8, help="Number of parallel workers"
|
| 996 |
+
)
|
| 997 |
+
parser.add_argument("--quiet", action="store_true", help="Suppress verbose output")
|
| 998 |
+
|
| 999 |
+
# Add dataset-specific args
|
| 1000 |
+
dataset_utils.add_args(parser)
|
| 1001 |
+
|
| 1002 |
+
opt = parser.parse_args(sys.argv[2:])
|
| 1003 |
+
opt = edict(vars(opt))
|
| 1004 |
+
|
| 1005 |
+
os.makedirs(os.path.join(opt.output_dir, "renders"), exist_ok=True)
|
| 1006 |
+
|
| 1007 |
+
# Install blender
|
| 1008 |
+
if not opt.quiet:
|
| 1009 |
+
print("Checking blender...", flush=True)
|
| 1010 |
+
_install_blender()
|
| 1011 |
+
|
| 1012 |
+
# Get file list from metadata
|
| 1013 |
+
metadata_path = os.path.join(opt.output_dir, "metadata.csv")
|
| 1014 |
+
if not os.path.exists(metadata_path):
|
| 1015 |
+
raise ValueError(f"metadata.csv not found at {metadata_path}")
|
| 1016 |
+
|
| 1017 |
+
metadata = pd.read_csv(metadata_path)
|
| 1018 |
+
|
| 1019 |
+
if opt.instances is None:
|
| 1020 |
+
metadata = metadata[metadata["local_path"].notna()]
|
| 1021 |
+
if opt.filter_low_aesthetic_score is not None:
|
| 1022 |
+
metadata = metadata[
|
| 1023 |
+
metadata["aesthetic_score"] >= opt.filter_low_aesthetic_score
|
| 1024 |
+
]
|
| 1025 |
+
if "rendered" in metadata.columns:
|
| 1026 |
+
metadata = metadata[metadata["rendered"] == False]
|
| 1027 |
+
else:
|
| 1028 |
+
if os.path.exists(opt.instances):
|
| 1029 |
+
with open(opt.instances, "r") as f:
|
| 1030 |
+
instances = f.read().splitlines()
|
| 1031 |
+
else:
|
| 1032 |
+
instances = opt.instances.split(",")
|
| 1033 |
+
metadata = metadata[metadata["sha256"].isin(instances)]
|
| 1034 |
+
|
| 1035 |
+
# Distributed processing slice
|
| 1036 |
+
start = len(metadata) * opt.rank // opt.world_size
|
| 1037 |
+
end = len(metadata) * (opt.rank + 1) // opt.world_size
|
| 1038 |
+
metadata = metadata[start:end]
|
| 1039 |
+
records = []
|
| 1040 |
+
|
| 1041 |
+
# Filter already processed
|
| 1042 |
+
for sha256 in copy.copy(metadata["sha256"].values):
|
| 1043 |
+
if os.path.exists(
|
| 1044 |
+
os.path.join(opt.output_dir, "renders", sha256, "transforms.json")
|
| 1045 |
+
):
|
| 1046 |
+
records.append({"sha256": sha256, "rendered": True})
|
| 1047 |
+
metadata = metadata[metadata["sha256"] != sha256]
|
| 1048 |
+
|
| 1049 |
+
print(f"Processing {len(metadata)} objects (rank {opt.rank}/{opt.world_size})...")
|
| 1050 |
+
|
| 1051 |
+
# Process objects
|
| 1052 |
+
from concurrent.futures import ThreadPoolExecutor
|
| 1053 |
+
from tqdm import tqdm
|
| 1054 |
+
|
| 1055 |
+
results = []
|
| 1056 |
+
with ThreadPoolExecutor(max_workers=opt.max_workers) as executor:
|
| 1057 |
+
futures = []
|
| 1058 |
+
for _, row in metadata.iterrows():
|
| 1059 |
+
sha256 = row["sha256"]
|
| 1060 |
+
local_path = row["local_path"]
|
| 1061 |
+
dataset = row.get("dataset", "unknown")
|
| 1062 |
+
|
| 1063 |
+
futures.append(
|
| 1064 |
+
executor.submit(
|
| 1065 |
+
_render_worker,
|
| 1066 |
+
local_path,
|
| 1067 |
+
sha256,
|
| 1068 |
+
dataset,
|
| 1069 |
+
opt.output_dir,
|
| 1070 |
+
opt.num_views,
|
| 1071 |
+
opt.quiet,
|
| 1072 |
+
)
|
| 1073 |
+
)
|
| 1074 |
+
|
| 1075 |
+
for future in tqdm(futures, desc="Rendering", disable=opt.quiet):
|
| 1076 |
+
try:
|
| 1077 |
+
result = future.result()
|
| 1078 |
+
if result is not None:
|
| 1079 |
+
results.append(result)
|
| 1080 |
+
except Exception as e:
|
| 1081 |
+
if not opt.quiet:
|
| 1082 |
+
print(f"Error in worker: {e}")
|
| 1083 |
+
|
| 1084 |
+
# Save results
|
| 1085 |
+
rendered = pd.concat(
|
| 1086 |
+
[pd.DataFrame.from_records(results), pd.DataFrame.from_records(records)]
|
| 1087 |
+
)
|
| 1088 |
+
rendered.to_csv(
|
| 1089 |
+
os.path.join(opt.output_dir, f"rendered_{opt.rank}.csv"), index=False
|
| 1090 |
+
)
|
| 1091 |
+
|
| 1092 |
+
print(f"Done! Rendered {len(results)} objects.")
|
| 1093 |
+
|
| 1094 |
+
|
| 1095 |
+
def main_single():
|
| 1096 |
+
parser = argparse.ArgumentParser(
|
| 1097 |
+
description="Render a single USD file with proper texture extraction"
|
| 1098 |
+
)
|
| 1099 |
+
parser.add_argument("usd_file", help="Path to USD file")
|
| 1100 |
+
parser.add_argument(
|
| 1101 |
+
"--output_dir",
|
| 1102 |
+
"-o",
|
| 1103 |
+
default=None,
|
| 1104 |
+
help="Output directory (default: /tmp/render_<filename>)",
|
| 1105 |
+
)
|
| 1106 |
+
parser.add_argument(
|
| 1107 |
+
"--num_views", type=int, default=150, help="Number of views to render"
|
| 1108 |
+
)
|
| 1109 |
+
parser.add_argument("--resolution", type=int, default=512, help="Image resolution")
|
| 1110 |
+
parser.add_argument(
|
| 1111 |
+
"--verbose", "-v", action="store_true", help="Print detailed logs"
|
| 1112 |
+
)
|
| 1113 |
+
parser.add_argument(
|
| 1114 |
+
"--extract_only",
|
| 1115 |
+
action="store_true",
|
| 1116 |
+
help="Only extract materials (don't render)",
|
| 1117 |
+
)
|
| 1118 |
+
|
| 1119 |
+
args = parser.parse_args()
|
| 1120 |
+
|
| 1121 |
+
if not os.path.exists(args.usd_file):
|
| 1122 |
+
print(f"ERROR: USD file not found: {args.usd_file}")
|
| 1123 |
+
sys.exit(1)
|
| 1124 |
+
|
| 1125 |
+
if args.extract_only:
|
| 1126 |
+
extractor = USDMaterialExtractor(args.usd_file, verbose=True)
|
| 1127 |
+
extractor.extract()
|
| 1128 |
+
|
| 1129 |
+
print("\n=== SUMMARY ===")
|
| 1130 |
+
print(f"Meshes: {len(extractor.meshes)}")
|
| 1131 |
+
print(f"Materials: {len(extractor.materials)}")
|
| 1132 |
+
|
| 1133 |
+
total_textures = sum(len(t) for t in extractor.materials.values())
|
| 1134 |
+
print(f"Total textures: {total_textures}")
|
| 1135 |
+
|
| 1136 |
+
for mat_path, textures in extractor.materials.items():
|
| 1137 |
+
if textures:
|
| 1138 |
+
mat_name = mat_path.split("/")[-1] if mat_path else "unknown"
|
| 1139 |
+
print(f"\n{mat_name}:")
|
| 1140 |
+
for tex_type, tex_path in textures.items():
|
| 1141 |
+
print(f" {tex_type}: {os.path.basename(tex_path)}")
|
| 1142 |
+
else:
|
| 1143 |
+
_install_blender()
|
| 1144 |
+
|
| 1145 |
+
output_dir = args.output_dir
|
| 1146 |
+
if not output_dir:
|
| 1147 |
+
filename = Path(args.usd_file).stem
|
| 1148 |
+
output_dir = f"/tmp/render_{filename}"
|
| 1149 |
+
|
| 1150 |
+
success = render_usd(
|
| 1151 |
+
args.usd_file,
|
| 1152 |
+
output_dir,
|
| 1153 |
+
num_views=args.num_views,
|
| 1154 |
+
resolution=args.resolution,
|
| 1155 |
+
verbose=args.verbose,
|
| 1156 |
+
)
|
| 1157 |
+
|
| 1158 |
+
if success:
|
| 1159 |
+
print(f"\n✓ Rendered to: {output_dir}")
|
| 1160 |
+
else:
|
| 1161 |
+
print(f"\n✗ Rendering failed")
|
| 1162 |
+
sys.exit(1)
|
| 1163 |
+
|
| 1164 |
+
|
| 1165 |
+
if __name__ == "__main__":
|
| 1166 |
+
# Check if first arg is a dataset type (batch mode) or a file (single mode)
|
| 1167 |
+
if (
|
| 1168 |
+
len(sys.argv) > 1
|
| 1169 |
+
and not sys.argv[1].startswith("-")
|
| 1170 |
+
and not os.path.exists(sys.argv[1])
|
| 1171 |
+
):
|
| 1172 |
+
# Batch mode: first arg is dataset type like "allmats"
|
| 1173 |
+
main_batch()
|
| 1174 |
+
else:
|
| 1175 |
+
# Single file mode
|
| 1176 |
+
main_single()
|
deps/vomp/dataset_toolkits/material_objects/vlm_annotations/data_subsets/commercial.py
ADDED
|
@@ -0,0 +1,427 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
|
| 16 |
+
from dataset_toolkits.material_objects.vlm_annotations.utils.utils import (
|
| 17 |
+
COMMERCIAL_BASE_DIR,
|
| 18 |
+
)
|
| 19 |
+
from dataset_toolkits.material_objects.vlm_annotations.utils.render import (
|
| 20 |
+
render_sphere_with_texture,
|
| 21 |
+
)
|
| 22 |
+
from dataset_toolkits.material_objects.vlm_annotations.utils.vlm import (
|
| 23 |
+
analyze_material_with_vlm,
|
| 24 |
+
parse_vlm_properties,
|
| 25 |
+
)
|
| 26 |
+
from dataset_toolkits.material_objects.vlm_annotations.data_subsets.common import (
|
| 27 |
+
extract_materials_from_usd,
|
| 28 |
+
)
|
| 29 |
+
from dataset_toolkits.material_objects.vlm_annotations.data_subsets.residential import (
|
| 30 |
+
PROMPTS,
|
| 31 |
+
make_user_prompt,
|
| 32 |
+
)
|
| 33 |
+
import re
|
| 34 |
+
from tqdm import tqdm
|
| 35 |
+
import os
|
| 36 |
+
import logging
|
| 37 |
+
import copy
|
| 38 |
+
|
| 39 |
+
# Use the centralized parser function
|
| 40 |
+
parse_vlm_output = parse_vlm_properties
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def list_commercial_objects():
|
| 44 |
+
"""
|
| 45 |
+
List all available commercial objects in the commercial directory.
|
| 46 |
+
"""
|
| 47 |
+
usd_files = []
|
| 48 |
+
print("\nAvailable commercial objects:")
|
| 49 |
+
for root, _, files in os.walk(COMMERCIAL_BASE_DIR):
|
| 50 |
+
for file in files:
|
| 51 |
+
if file.endswith(".usd") and not os.path.basename(root).startswith("."):
|
| 52 |
+
usd_files.append(os.path.join(root, file))
|
| 53 |
+
print(f" - {os.path.basename(root)}/{file}")
|
| 54 |
+
print()
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def process_commercial(
|
| 58 |
+
vlm_model,
|
| 59 |
+
vlm_processor,
|
| 60 |
+
limit=None,
|
| 61 |
+
processed_objects=None,
|
| 62 |
+
output_file=None,
|
| 63 |
+
existing_results=None,
|
| 64 |
+
):
|
| 65 |
+
usd_files = []
|
| 66 |
+
for root, _, files in os.walk(COMMERCIAL_BASE_DIR):
|
| 67 |
+
for file in files:
|
| 68 |
+
if file.endswith(".usd") and not os.path.basename(root).startswith("."):
|
| 69 |
+
usd_files.append(os.path.join(root, file))
|
| 70 |
+
|
| 71 |
+
logging.info(f"Found {len(usd_files)} USD files in commercial dataset")
|
| 72 |
+
|
| 73 |
+
# Initialize tracking sets and results
|
| 74 |
+
processed_objects = set() if processed_objects is None else processed_objects
|
| 75 |
+
existing_results = [] if existing_results is None else existing_results
|
| 76 |
+
|
| 77 |
+
# Build a set of already processed object names from existing_results
|
| 78 |
+
existing_object_names = {
|
| 79 |
+
result.get("object_name")
|
| 80 |
+
for result in existing_results
|
| 81 |
+
if "object_name" in result
|
| 82 |
+
}
|
| 83 |
+
logging.info(
|
| 84 |
+
f"Found {len(existing_object_names)} already processed objects in existing results"
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
# Add names from existing_results to processed_objects to avoid reprocessing
|
| 88 |
+
processed_objects.update(existing_object_names)
|
| 89 |
+
|
| 90 |
+
# Create a copy of existing_results to avoid modifying the original
|
| 91 |
+
all_results = copy.deepcopy(existing_results)
|
| 92 |
+
|
| 93 |
+
usd_files.sort()
|
| 94 |
+
|
| 95 |
+
if limit and limit > 0:
|
| 96 |
+
usd_files = usd_files[:limit]
|
| 97 |
+
|
| 98 |
+
success_count = 0
|
| 99 |
+
failed_objects = []
|
| 100 |
+
total_segments = 0
|
| 101 |
+
unique_materials = set()
|
| 102 |
+
materials_per_object = {}
|
| 103 |
+
total_rendered_segments = 0
|
| 104 |
+
total_vlm_segments = 0
|
| 105 |
+
|
| 106 |
+
# Count total segments from existing results
|
| 107 |
+
for result in existing_results:
|
| 108 |
+
total_segments += len(result.get("segments", {}))
|
| 109 |
+
|
| 110 |
+
# Statistics for texture availability
|
| 111 |
+
segments_with_texture = 0
|
| 112 |
+
segments_without_texture = 0
|
| 113 |
+
segments_with_thumbnail_only = 0
|
| 114 |
+
|
| 115 |
+
# Track processed files to avoid duplicates from the same directory
|
| 116 |
+
processed_files = set()
|
| 117 |
+
|
| 118 |
+
for usd_file in tqdm(usd_files, desc=f"Processing commercial dataset"):
|
| 119 |
+
# Extract object name from path
|
| 120 |
+
object_name = os.path.basename(os.path.dirname(usd_file))
|
| 121 |
+
|
| 122 |
+
# Skip if we already processed this exact file
|
| 123 |
+
if usd_file in processed_files:
|
| 124 |
+
continue
|
| 125 |
+
|
| 126 |
+
# Skip objects that have already been processed
|
| 127 |
+
if object_name in processed_objects:
|
| 128 |
+
logging.info(f"Skipping already processed object: {object_name}")
|
| 129 |
+
continue
|
| 130 |
+
|
| 131 |
+
try:
|
| 132 |
+
directory = os.path.dirname(usd_file)
|
| 133 |
+
|
| 134 |
+
# Extract material information
|
| 135 |
+
result = extract_materials_from_usd(usd_file, "commercial")
|
| 136 |
+
|
| 137 |
+
if result:
|
| 138 |
+
# Add to processed_files to avoid duplicates
|
| 139 |
+
processed_files.add(usd_file)
|
| 140 |
+
|
| 141 |
+
# Track statistics
|
| 142 |
+
segments = result.get("segments", {})
|
| 143 |
+
total_segments += len(segments)
|
| 144 |
+
|
| 145 |
+
# Remove object_name and note fields from segments
|
| 146 |
+
for segment_key, segment_info in segments.items():
|
| 147 |
+
if "object_name" in segment_info:
|
| 148 |
+
del segment_info["object_name"]
|
| 149 |
+
if "note" in segment_info:
|
| 150 |
+
del segment_info["note"]
|
| 151 |
+
|
| 152 |
+
# Count unique materials for this object
|
| 153 |
+
object_materials = set()
|
| 154 |
+
for segment_name, segment_info in segments.items():
|
| 155 |
+
material_name = segment_info.get("material_type", "unknown")
|
| 156 |
+
unique_materials.add(material_name)
|
| 157 |
+
object_materials.add(material_name)
|
| 158 |
+
|
| 159 |
+
# Record materials per object
|
| 160 |
+
if len(segments) > 0:
|
| 161 |
+
materials_per_object[object_name] = len(object_materials)
|
| 162 |
+
|
| 163 |
+
# Get thumbnail path if available
|
| 164 |
+
thumb_path = None
|
| 165 |
+
# For commercial dataset, thumbnails are in .thumbs/256x256 directory
|
| 166 |
+
thumb_dir = os.path.join(
|
| 167 |
+
os.path.dirname(usd_file), ".thumbs", "256x256"
|
| 168 |
+
)
|
| 169 |
+
|
| 170 |
+
has_thumbnail = False
|
| 171 |
+
if os.path.exists(thumb_dir):
|
| 172 |
+
# Try to find a thumbnail matching the USD filename
|
| 173 |
+
usd_filename = os.path.basename(usd_file)
|
| 174 |
+
thumb_candidates = [
|
| 175 |
+
# Regular thumbnail
|
| 176 |
+
os.path.join(thumb_dir, f"{usd_filename}.png"),
|
| 177 |
+
# Auto-generated thumbnail
|
| 178 |
+
os.path.join(thumb_dir, f"{usd_filename}.auto.png"),
|
| 179 |
+
]
|
| 180 |
+
|
| 181 |
+
for candidate in thumb_candidates:
|
| 182 |
+
if os.path.exists(candidate):
|
| 183 |
+
thumb_path = candidate
|
| 184 |
+
has_thumbnail = True
|
| 185 |
+
logging.info(f"Found thumbnail: {thumb_path}")
|
| 186 |
+
break
|
| 187 |
+
|
| 188 |
+
# Process VLM for all segments if VLM model is provided
|
| 189 |
+
os.makedirs("/tmp/vlm", exist_ok=True)
|
| 190 |
+
|
| 191 |
+
if vlm_model and len(segments) > 0:
|
| 192 |
+
for segment_key, segment_info in segments.items():
|
| 193 |
+
textures = segment_info.get("textures", {})
|
| 194 |
+
|
| 195 |
+
# Log texture information for diagnostics
|
| 196 |
+
logging.info(
|
| 197 |
+
f"Segment {segment_key} has textures: {list(textures.keys())}"
|
| 198 |
+
)
|
| 199 |
+
|
| 200 |
+
# Check if we have either a normal or roughness texture for rendering
|
| 201 |
+
has_texture = (
|
| 202 |
+
"normal" in textures
|
| 203 |
+
or "roughness" in textures
|
| 204 |
+
or "diffuse" in textures
|
| 205 |
+
)
|
| 206 |
+
if has_texture:
|
| 207 |
+
# Has texture - render sphere and use with thumbnail
|
| 208 |
+
segments_with_texture += 1
|
| 209 |
+
logging.info(
|
| 210 |
+
f"Rendering texture sphere for {object_name}, segment {segment_key}"
|
| 211 |
+
)
|
| 212 |
+
|
| 213 |
+
# Set up file path for this segment's rendered sphere
|
| 214 |
+
segment_render_path = f"/tmp/vlm/texture_sphere_{object_name}_{segment_key}.png"
|
| 215 |
+
|
| 216 |
+
# Render the textured sphere
|
| 217 |
+
try:
|
| 218 |
+
rgb_buffer = render_sphere_with_texture(
|
| 219 |
+
textures, segment_render_path
|
| 220 |
+
)
|
| 221 |
+
logging.info(f"RGB buffer shape: {rgb_buffer.shape}")
|
| 222 |
+
except Exception as e:
|
| 223 |
+
logging.error(
|
| 224 |
+
f"Error rendering texture for {segment_key}: {str(e)}"
|
| 225 |
+
)
|
| 226 |
+
segment_render_path = None
|
| 227 |
+
else:
|
| 228 |
+
# No texture - just use thumbnail
|
| 229 |
+
segments_without_texture += 1
|
| 230 |
+
segment_render_path = None
|
| 231 |
+
logging.info(
|
| 232 |
+
f"No texture for {object_name}, segment {segment_key}. Using thumbnail only."
|
| 233 |
+
)
|
| 234 |
+
|
| 235 |
+
# Always try to process with VLM, even if no texture
|
| 236 |
+
try:
|
| 237 |
+
# If we have a thumbnail but no texture, still run VLM with just the thumbnail
|
| 238 |
+
if not has_texture and has_thumbnail:
|
| 239 |
+
segments_with_thumbnail_only += 1
|
| 240 |
+
logging.info(
|
| 241 |
+
f"Using thumbnail only for {object_name}, segment {segment_key}"
|
| 242 |
+
)
|
| 243 |
+
|
| 244 |
+
# Don't run VLM if we have neither texture nor thumbnail
|
| 245 |
+
if not segment_render_path and not has_thumbnail:
|
| 246 |
+
logging.warning(
|
| 247 |
+
f"Skipping VLM for {segment_key} - no texture or thumbnail available"
|
| 248 |
+
)
|
| 249 |
+
continue
|
| 250 |
+
|
| 251 |
+
# Set semantic usage to segment name but don't store in segment data
|
| 252 |
+
semantic_usage = segment_key
|
| 253 |
+
temp_object_name = object_name
|
| 254 |
+
|
| 255 |
+
# Create custom prompt based on texture availability
|
| 256 |
+
part1 = make_user_prompt(
|
| 257 |
+
segment_info["material_type"],
|
| 258 |
+
semantic_usage,
|
| 259 |
+
temp_object_name,
|
| 260 |
+
has_texture_sphere=segment_render_path is not None,
|
| 261 |
+
)
|
| 262 |
+
|
| 263 |
+
# Store the custom prompt in material_info but not object_name
|
| 264 |
+
segment_info["user_prompt"] = part1
|
| 265 |
+
|
| 266 |
+
# Debug: Log the prompt type based on texture availability
|
| 267 |
+
if segment_render_path is not None:
|
| 268 |
+
logging.info(
|
| 269 |
+
f"Using prompt WITH texture sphere for {object_name}, segment {segment_key}"
|
| 270 |
+
)
|
| 271 |
+
else:
|
| 272 |
+
logging.info(
|
| 273 |
+
f"Using prompt WITHOUT texture sphere for {object_name}, segment {segment_key}"
|
| 274 |
+
)
|
| 275 |
+
logging.info(
|
| 276 |
+
f"PROMPT: {part1[:100]}..."
|
| 277 |
+
) # Print just the beginning of the prompt
|
| 278 |
+
|
| 279 |
+
# Create a temporary segment_info with object_name for VLM but don't save to result
|
| 280 |
+
temp_segment_info = segment_info.copy()
|
| 281 |
+
temp_segment_info["semantic_usage"] = semantic_usage
|
| 282 |
+
temp_segment_info["object_name"] = temp_object_name
|
| 283 |
+
|
| 284 |
+
vlm_analysis = analyze_material_with_vlm(
|
| 285 |
+
segment_render_path, # This can be None, in which case only thumbnail is used
|
| 286 |
+
temp_segment_info, # Use temporary copy with object_name
|
| 287 |
+
vlm_model,
|
| 288 |
+
vlm_processor,
|
| 289 |
+
thumbnail_path=thumb_path,
|
| 290 |
+
dataset_name="commercial",
|
| 291 |
+
PROMPTS=PROMPTS,
|
| 292 |
+
make_user_prompt=make_user_prompt,
|
| 293 |
+
parse_vlm_output=parse_vlm_output,
|
| 294 |
+
)
|
| 295 |
+
|
| 296 |
+
# Add VLM analysis to segment info
|
| 297 |
+
if vlm_analysis and "error" not in vlm_analysis:
|
| 298 |
+
segment_info["vlm_analysis"] = vlm_analysis.get(
|
| 299 |
+
"vlm_analysis"
|
| 300 |
+
)
|
| 301 |
+
|
| 302 |
+
if vlm_analysis.get("youngs_modulus") is not None:
|
| 303 |
+
segment_info["youngs_modulus"] = vlm_analysis.get(
|
| 304 |
+
"youngs_modulus"
|
| 305 |
+
)
|
| 306 |
+
|
| 307 |
+
if vlm_analysis.get("poissons_ratio") is not None:
|
| 308 |
+
segment_info["poissons_ratio"] = vlm_analysis.get(
|
| 309 |
+
"poissons_ratio"
|
| 310 |
+
)
|
| 311 |
+
|
| 312 |
+
if vlm_analysis.get("density") is not None:
|
| 313 |
+
segment_info["density"] = vlm_analysis.get(
|
| 314 |
+
"density"
|
| 315 |
+
)
|
| 316 |
+
|
| 317 |
+
total_vlm_segments += 1
|
| 318 |
+
logging.info(
|
| 319 |
+
f"VLM analysis successful for {segment_key}:"
|
| 320 |
+
)
|
| 321 |
+
logging.info(
|
| 322 |
+
f" Young's modulus: {vlm_analysis.get('youngs_modulus')}"
|
| 323 |
+
)
|
| 324 |
+
logging.info(
|
| 325 |
+
f" Poisson's ratio: {vlm_analysis.get('poissons_ratio')}"
|
| 326 |
+
)
|
| 327 |
+
logging.info(
|
| 328 |
+
f" Density: {vlm_analysis.get('density')}"
|
| 329 |
+
)
|
| 330 |
+
else:
|
| 331 |
+
logging.error(
|
| 332 |
+
f"VLM analysis failed for {segment_key}: {vlm_analysis.get('error', 'Unknown error')}"
|
| 333 |
+
)
|
| 334 |
+
except Exception as e:
|
| 335 |
+
import traceback
|
| 336 |
+
|
| 337 |
+
logging.error(
|
| 338 |
+
f"Error during VLM analysis for {segment_key}: {str(e)}"
|
| 339 |
+
)
|
| 340 |
+
logging.error(traceback.format_exc())
|
| 341 |
+
|
| 342 |
+
total_rendered_segments += 1
|
| 343 |
+
|
| 344 |
+
all_results.append(result) # Add to our local copy of results
|
| 345 |
+
processed_objects.add(object_name) # Mark as processed
|
| 346 |
+
|
| 347 |
+
# Incremental save after each object if output file is provided
|
| 348 |
+
if output_file:
|
| 349 |
+
try:
|
| 350 |
+
with open(output_file, "w") as f:
|
| 351 |
+
import json
|
| 352 |
+
from dataset_toolkits.material_objects.vlm_annotations.data_subsets.common import (
|
| 353 |
+
UsdJsonEncoder,
|
| 354 |
+
)
|
| 355 |
+
|
| 356 |
+
# Debug save contents
|
| 357 |
+
logging.info(
|
| 358 |
+
f"Saving checkpoint with {len(all_results)} objects"
|
| 359 |
+
)
|
| 360 |
+
|
| 361 |
+
# Ensure result types are JSON serializable
|
| 362 |
+
for idx, item in enumerate(all_results):
|
| 363 |
+
if "segments" in item:
|
| 364 |
+
for seg_key, seg_info in item["segments"].items():
|
| 365 |
+
# Remove object_name and note fields if they exist
|
| 366 |
+
if "object_name" in seg_info:
|
| 367 |
+
del seg_info["object_name"]
|
| 368 |
+
if "note" in seg_info:
|
| 369 |
+
del seg_info["note"]
|
| 370 |
+
|
| 371 |
+
if "textures" in seg_info and isinstance(
|
| 372 |
+
seg_info["textures"], dict
|
| 373 |
+
):
|
| 374 |
+
# Convert any non-serializable texture paths to strings
|
| 375 |
+
serializable_textures = {}
|
| 376 |
+
for tex_type, tex_path in seg_info[
|
| 377 |
+
"textures"
|
| 378 |
+
].items():
|
| 379 |
+
serializable_textures[tex_type] = str(
|
| 380 |
+
tex_path
|
| 381 |
+
)
|
| 382 |
+
seg_info["textures"] = serializable_textures
|
| 383 |
+
|
| 384 |
+
# Dump to file
|
| 385 |
+
json.dump(all_results, f, indent=4, cls=UsdJsonEncoder)
|
| 386 |
+
|
| 387 |
+
except Exception as e:
|
| 388 |
+
logging.error(f"Error saving checkpoint: {str(e)}")
|
| 389 |
+
import traceback
|
| 390 |
+
|
| 391 |
+
logging.error(traceback.format_exc())
|
| 392 |
+
|
| 393 |
+
success_count += 1
|
| 394 |
+
else:
|
| 395 |
+
logging.warning(f"No material information extracted for {usd_file}")
|
| 396 |
+
failed_objects.append(object_name)
|
| 397 |
+
except Exception as e:
|
| 398 |
+
import traceback
|
| 399 |
+
|
| 400 |
+
logging.error(f"Error processing {usd_file}: {str(e)}")
|
| 401 |
+
logging.error(traceback.format_exc())
|
| 402 |
+
failed_objects.append(os.path.basename(os.path.dirname(usd_file)))
|
| 403 |
+
|
| 404 |
+
# Log texture statistics
|
| 405 |
+
logging.info("Texture Statistics:")
|
| 406 |
+
logging.info(f" Total segments processed: {total_segments}")
|
| 407 |
+
logging.info(f" Segments with textures: {segments_with_texture}")
|
| 408 |
+
logging.info(f" Segments without textures: {segments_without_texture}")
|
| 409 |
+
logging.info(f" Segments with thumbnail only: {segments_with_thumbnail_only}")
|
| 410 |
+
logging.info(f" Total VLM analyses completed: {total_vlm_segments}")
|
| 411 |
+
|
| 412 |
+
# Convert materials_per_object to list format for consistency with simready
|
| 413 |
+
materials_per_object_list = []
|
| 414 |
+
for obj_name, count in materials_per_object.items():
|
| 415 |
+
materials_per_object_list.append(obj_name)
|
| 416 |
+
|
| 417 |
+
return (
|
| 418 |
+
all_results,
|
| 419 |
+
len(usd_files),
|
| 420 |
+
success_count,
|
| 421 |
+
failed_objects,
|
| 422 |
+
total_segments,
|
| 423 |
+
total_rendered_segments,
|
| 424 |
+
total_vlm_segments,
|
| 425 |
+
list(unique_materials),
|
| 426 |
+
materials_per_object_list,
|
| 427 |
+
)
|
deps/vomp/dataset_toolkits/material_objects/vlm_annotations/data_subsets/common.py
ADDED
|
@@ -0,0 +1,1457 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
|
| 16 |
+
import os
|
| 17 |
+
import sys
|
| 18 |
+
import json
|
| 19 |
+
import glob
|
| 20 |
+
import argparse
|
| 21 |
+
import numpy as np
|
| 22 |
+
import logging
|
| 23 |
+
import copy
|
| 24 |
+
from PIL import Image
|
| 25 |
+
from tqdm import tqdm
|
| 26 |
+
from pxr import Usd, UsdGeom, UsdShade, Sdf, Ar
|
| 27 |
+
from dataset_toolkits.material_objects.vlm_annotations.utils.utils import (
|
| 28 |
+
COMMERCIAL_BASE_DIR,
|
| 29 |
+
RESIDENTIAL_BASE_DIR,
|
| 30 |
+
VEGETATION_BASE_DIR,
|
| 31 |
+
)
|
| 32 |
+
import datetime
|
| 33 |
+
import uuid
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class UsdJsonEncoder(json.JSONEncoder):
|
| 37 |
+
def default(self, obj):
|
| 38 |
+
if hasattr(obj, "__dict__"):
|
| 39 |
+
return obj.__dict__
|
| 40 |
+
return str(obj)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def find_textures_for_material(object_dir, texture_path):
|
| 44 |
+
"""
|
| 45 |
+
Find textures referenced by a material in a USD file.
|
| 46 |
+
|
| 47 |
+
Args:
|
| 48 |
+
object_dir (str): Directory containing the USD file
|
| 49 |
+
texture_path (str): Texture path from the USD file
|
| 50 |
+
|
| 51 |
+
Returns:
|
| 52 |
+
dict: Dictionary mapping texture types to full paths
|
| 53 |
+
"""
|
| 54 |
+
if not texture_path:
|
| 55 |
+
return {}
|
| 56 |
+
|
| 57 |
+
# Convert Sdf.AssetPath to string if needed
|
| 58 |
+
if (
|
| 59 |
+
hasattr(texture_path, "__class__")
|
| 60 |
+
and texture_path.__class__.__name__ == "AssetPath"
|
| 61 |
+
):
|
| 62 |
+
texture_path = str(texture_path)
|
| 63 |
+
|
| 64 |
+
# Handle absolute paths
|
| 65 |
+
if os.path.isabs(texture_path):
|
| 66 |
+
if os.path.exists(texture_path):
|
| 67 |
+
return {determine_texture_type(texture_path): texture_path}
|
| 68 |
+
return {}
|
| 69 |
+
|
| 70 |
+
# Handle relative paths with various prefixes
|
| 71 |
+
clean_path = texture_path.replace("@", "").replace("./", "")
|
| 72 |
+
|
| 73 |
+
# Try direct path
|
| 74 |
+
direct_path = os.path.join(object_dir, clean_path)
|
| 75 |
+
if os.path.exists(direct_path):
|
| 76 |
+
return {determine_texture_type(direct_path): direct_path}
|
| 77 |
+
|
| 78 |
+
# Try common texture directories
|
| 79 |
+
texture_dirs = []
|
| 80 |
+
for texture_dir_name in [
|
| 81 |
+
"textures",
|
| 82 |
+
"Textures",
|
| 83 |
+
"materials/textures",
|
| 84 |
+
"Materials/Textures",
|
| 85 |
+
]:
|
| 86 |
+
texture_dir = os.path.join(object_dir, texture_dir_name)
|
| 87 |
+
if os.path.isdir(texture_dir):
|
| 88 |
+
texture_dirs.append(texture_dir)
|
| 89 |
+
|
| 90 |
+
# Look in parent directory if object_dir doesn't have textures
|
| 91 |
+
if not texture_dirs:
|
| 92 |
+
parent_dir = os.path.dirname(object_dir)
|
| 93 |
+
for texture_dir_name in [
|
| 94 |
+
"textures",
|
| 95 |
+
"Textures",
|
| 96 |
+
"materials/textures",
|
| 97 |
+
"Materials/Textures",
|
| 98 |
+
]:
|
| 99 |
+
texture_dir = os.path.join(parent_dir, texture_dir_name)
|
| 100 |
+
if os.path.isdir(texture_dir):
|
| 101 |
+
texture_dirs.append(texture_dir)
|
| 102 |
+
|
| 103 |
+
# Check for texture in each texture directory
|
| 104 |
+
for texture_dir in texture_dirs:
|
| 105 |
+
texture_file = os.path.join(texture_dir, os.path.basename(clean_path))
|
| 106 |
+
if os.path.exists(texture_file):
|
| 107 |
+
return {determine_texture_type(texture_file): texture_file}
|
| 108 |
+
|
| 109 |
+
return {}
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def determine_texture_type(texture_path):
|
| 113 |
+
"""
|
| 114 |
+
Determine the type of texture based on its filename.
|
| 115 |
+
|
| 116 |
+
Args:
|
| 117 |
+
texture_path (str): Path to texture file
|
| 118 |
+
|
| 119 |
+
Returns:
|
| 120 |
+
str: Texture type (albedo, normal, roughness, metallic, orm)
|
| 121 |
+
"""
|
| 122 |
+
filename = os.path.basename(texture_path).lower()
|
| 123 |
+
|
| 124 |
+
# Check for common texture type indicators in filename
|
| 125 |
+
if any(
|
| 126 |
+
term in filename
|
| 127 |
+
for term in ["albedo", "basecolor", "color", "_a.", "_a_", "_diffuse", "_diff"]
|
| 128 |
+
):
|
| 129 |
+
return "albedo"
|
| 130 |
+
elif any(term in filename for term in ["normal", "nrm", "_n.", "_n_"]):
|
| 131 |
+
return "normal"
|
| 132 |
+
elif any(term in filename for term in ["roughness", "rough", "_r.", "_r_"]):
|
| 133 |
+
return "roughness"
|
| 134 |
+
elif any(term in filename for term in ["metallic", "metal", "_m.", "_m_"]):
|
| 135 |
+
return "metallic"
|
| 136 |
+
elif any(term in filename for term in ["orm", "arm", "occlusion"]):
|
| 137 |
+
return "orm"
|
| 138 |
+
elif any(term in filename for term in ["emissive", "emission", "_e."]):
|
| 139 |
+
return "emissive"
|
| 140 |
+
elif any(term in filename for term in ["opacity", "transparent", "alpha"]):
|
| 141 |
+
return "opacity"
|
| 142 |
+
elif any(term in filename for term in ["specular", "spec", "_s."]):
|
| 143 |
+
return "specular"
|
| 144 |
+
elif any(term in filename for term in ["displacement", "height", "bump"]):
|
| 145 |
+
return "displacement"
|
| 146 |
+
|
| 147 |
+
# If no specific type is identified, make an educated guess based on file extension
|
| 148 |
+
ext = os.path.splitext(filename)[1].lower()
|
| 149 |
+
if ext in [".jpg", ".jpeg", ".png", ".tga", ".tif", ".tiff"]:
|
| 150 |
+
return "albedo" # Default to albedo for unrecognized image files
|
| 151 |
+
|
| 152 |
+
return "unknown"
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
def copy_texture_to_output(
|
| 156 |
+
texture_path, output_dir, object_name, material_name, texture_type
|
| 157 |
+
):
|
| 158 |
+
"""
|
| 159 |
+
Copy a texture file to the output directory with a standardized name.
|
| 160 |
+
|
| 161 |
+
Args:
|
| 162 |
+
texture_path (str): Source texture path
|
| 163 |
+
output_dir (str): Output directory
|
| 164 |
+
object_name (str): Name of the object
|
| 165 |
+
material_name (str): Name of the material
|
| 166 |
+
texture_type (str): Type of texture
|
| 167 |
+
|
| 168 |
+
Returns:
|
| 169 |
+
str: Path to the copied texture file
|
| 170 |
+
"""
|
| 171 |
+
if not os.path.exists(texture_path):
|
| 172 |
+
return None
|
| 173 |
+
|
| 174 |
+
# Create output subdirectory for this object if it doesn't exist
|
| 175 |
+
object_output_dir = os.path.join(output_dir, object_name)
|
| 176 |
+
os.makedirs(object_output_dir, exist_ok=True)
|
| 177 |
+
|
| 178 |
+
# Create standardized output filename
|
| 179 |
+
texture_ext = os.path.splitext(texture_path)[1]
|
| 180 |
+
output_filename = f"{material_name}_{texture_type}{texture_ext}"
|
| 181 |
+
output_path = os.path.join(object_output_dir, output_filename)
|
| 182 |
+
|
| 183 |
+
try:
|
| 184 |
+
# Copy the texture file
|
| 185 |
+
import shutil
|
| 186 |
+
|
| 187 |
+
shutil.copy2(texture_path, output_path)
|
| 188 |
+
return output_path
|
| 189 |
+
except Exception as e:
|
| 190 |
+
logging.error(f"Error copying texture {texture_path}: {str(e)}")
|
| 191 |
+
return None
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
def extract_material_from_shader(shader_prim, object_dir, dataset_type=None):
|
| 195 |
+
"""
|
| 196 |
+
Extract material properties and textures from a shader prim.
|
| 197 |
+
|
| 198 |
+
Args:
|
| 199 |
+
shader_prim (UsdShade.Shader): Shader prim
|
| 200 |
+
object_dir (str): Directory containing the USD file
|
| 201 |
+
dataset_type (str, optional): Type of dataset (commercial, residential, vegetation)
|
| 202 |
+
|
| 203 |
+
Returns:
|
| 204 |
+
dict: Dictionary with material properties and textures
|
| 205 |
+
"""
|
| 206 |
+
material_info = {"textures": {}}
|
| 207 |
+
|
| 208 |
+
# Create a shader object from the prim
|
| 209 |
+
shader = UsdShade.Shader(shader_prim)
|
| 210 |
+
if not shader:
|
| 211 |
+
logging.warning(f"Failed to create shader from {shader_prim.GetPath()}")
|
| 212 |
+
return material_info
|
| 213 |
+
|
| 214 |
+
# Get material name from shader path
|
| 215 |
+
shader_path = str(shader_prim.GetPath())
|
| 216 |
+
material_name = None
|
| 217 |
+
if "/Looks/" in shader_path:
|
| 218 |
+
material_name = shader_path.split("/Looks/")[1].split("/")[0]
|
| 219 |
+
|
| 220 |
+
logging.info(f"Processing shader for material: {material_name}")
|
| 221 |
+
|
| 222 |
+
# For vegetation materials, try to find matching textures by material name
|
| 223 |
+
if dataset_type == "vegetation" and material_name:
|
| 224 |
+
# Find the materials/textures directory
|
| 225 |
+
object_dir_parts = object_dir.split(os.sep)
|
| 226 |
+
trees_dir = None
|
| 227 |
+
for i in range(len(object_dir_parts)):
|
| 228 |
+
if object_dir_parts[i] == "Trees":
|
| 229 |
+
trees_dir = os.sep.join(object_dir_parts[: i + 1])
|
| 230 |
+
break
|
| 231 |
+
|
| 232 |
+
if trees_dir:
|
| 233 |
+
textures_dir = os.path.join(trees_dir, "materials", "textures")
|
| 234 |
+
if os.path.exists(textures_dir):
|
| 235 |
+
material_name_lower = material_name.lower()
|
| 236 |
+
material_parts = material_name_lower.replace("_", " ").split()
|
| 237 |
+
|
| 238 |
+
# Get all texture files in the directory
|
| 239 |
+
texture_files = [
|
| 240 |
+
f
|
| 241 |
+
for f in os.listdir(textures_dir)
|
| 242 |
+
if f.lower().endswith((".png", ".jpg", ".jpeg", ".tif", ".tiff"))
|
| 243 |
+
]
|
| 244 |
+
|
| 245 |
+
# Track potential matches for different texture types
|
| 246 |
+
texture_matches = {
|
| 247 |
+
"diffuse": [],
|
| 248 |
+
"normal": [],
|
| 249 |
+
"roughness": [],
|
| 250 |
+
"metallic": [],
|
| 251 |
+
"orm": [],
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
# Categorize material into types
|
| 255 |
+
material_categories = {
|
| 256 |
+
"bark": [
|
| 257 |
+
"bark",
|
| 258 |
+
"trunk",
|
| 259 |
+
"wood",
|
| 260 |
+
"tree",
|
| 261 |
+
"log",
|
| 262 |
+
"stump",
|
| 263 |
+
"stem",
|
| 264 |
+
"branch",
|
| 265 |
+
"twig",
|
| 266 |
+
],
|
| 267 |
+
"leaf": ["leaf", "leaves", "foliage", "needle", "needles", "frond"],
|
| 268 |
+
"flower": [
|
| 269 |
+
"flower",
|
| 270 |
+
"flowers",
|
| 271 |
+
"petal",
|
| 272 |
+
"petals",
|
| 273 |
+
"bloom",
|
| 274 |
+
"blossom",
|
| 275 |
+
],
|
| 276 |
+
"fruit": [
|
| 277 |
+
"fruit",
|
| 278 |
+
"fruits",
|
| 279 |
+
"berry",
|
| 280 |
+
"berries",
|
| 281 |
+
"seed",
|
| 282 |
+
"seeds",
|
| 283 |
+
"cone",
|
| 284 |
+
"cones",
|
| 285 |
+
],
|
| 286 |
+
"grass": [
|
| 287 |
+
"grass",
|
| 288 |
+
"grasses",
|
| 289 |
+
"reed",
|
| 290 |
+
"reeds",
|
| 291 |
+
"sedge",
|
| 292 |
+
"rush",
|
| 293 |
+
"blade",
|
| 294 |
+
],
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
# Find all applicable categories
|
| 298 |
+
material_types = []
|
| 299 |
+
for category, keywords in material_categories.items():
|
| 300 |
+
if any(keyword in material_name_lower for keyword in keywords):
|
| 301 |
+
material_types.append(category)
|
| 302 |
+
|
| 303 |
+
# If we couldn't determine a category from material name, try from object name
|
| 304 |
+
if not material_types:
|
| 305 |
+
object_name = os.path.splitext(os.path.basename(object_dir))[
|
| 306 |
+
0
|
| 307 |
+
].lower()
|
| 308 |
+
for category, keywords in material_categories.items():
|
| 309 |
+
if any(keyword in object_name for keyword in keywords):
|
| 310 |
+
material_types.append(category)
|
| 311 |
+
|
| 312 |
+
# Still no category? Add generic fallbacks
|
| 313 |
+
if not material_types:
|
| 314 |
+
# Default to bark for most vegetation models
|
| 315 |
+
material_types = ["bark"]
|
| 316 |
+
|
| 317 |
+
logging.info(
|
| 318 |
+
f"Material categories for {material_name}: {material_types}"
|
| 319 |
+
)
|
| 320 |
+
|
| 321 |
+
# Scoring function for texture relevance to material name
|
| 322 |
+
def score_texture_for_material(texture_name, texture_type):
|
| 323 |
+
score = 0
|
| 324 |
+
texture_name_lower = texture_name.lower()
|
| 325 |
+
|
| 326 |
+
# Direct material name match (highest priority)
|
| 327 |
+
if material_name_lower in texture_name_lower:
|
| 328 |
+
score += 200
|
| 329 |
+
|
| 330 |
+
# Match individual parts of material name
|
| 331 |
+
for part in material_parts:
|
| 332 |
+
if len(part) > 2 and part in texture_name_lower:
|
| 333 |
+
score += 50
|
| 334 |
+
|
| 335 |
+
# Match material categories
|
| 336 |
+
for material_type in material_types:
|
| 337 |
+
# Match exact category name
|
| 338 |
+
if material_type in texture_name_lower:
|
| 339 |
+
score += 100
|
| 340 |
+
|
| 341 |
+
# Match keywords for this category
|
| 342 |
+
for keyword in material_categories.get(material_type, []):
|
| 343 |
+
if keyword in texture_name_lower:
|
| 344 |
+
score += 40
|
| 345 |
+
|
| 346 |
+
# Correct type suffix
|
| 347 |
+
type_suffixes = {
|
| 348 |
+
"diffuse": [
|
| 349 |
+
"basecolor",
|
| 350 |
+
"albedo",
|
| 351 |
+
"color",
|
| 352 |
+
"diffuse",
|
| 353 |
+
"_bc",
|
| 354 |
+
"_a",
|
| 355 |
+
"_d",
|
| 356 |
+
],
|
| 357 |
+
"normal": ["normal", "nrm", "_n", "nor"],
|
| 358 |
+
"roughness": ["roughness", "rough", "_r", "rgh"],
|
| 359 |
+
"metallic": ["metallic", "metal", "_m", "mtl"],
|
| 360 |
+
"orm": ["orm", "arm", "occlusion"],
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
for suffix in type_suffixes.get(texture_type, []):
|
| 364 |
+
if suffix in texture_name_lower:
|
| 365 |
+
score += 40
|
| 366 |
+
|
| 367 |
+
# Boost score for more specific matches (longer texture names probably more specific)
|
| 368 |
+
if len(texture_name_lower) > 15:
|
| 369 |
+
score += 10
|
| 370 |
+
|
| 371 |
+
# Exact matches for specific materials
|
| 372 |
+
if material_name_lower == "bark" and "bark" in texture_name_lower:
|
| 373 |
+
score += 50
|
| 374 |
+
elif (
|
| 375 |
+
material_name_lower == "leaves" and "leaf" in texture_name_lower
|
| 376 |
+
):
|
| 377 |
+
score += 50
|
| 378 |
+
elif (
|
| 379 |
+
material_name_lower == "needle"
|
| 380 |
+
and "needle" in texture_name_lower
|
| 381 |
+
):
|
| 382 |
+
score += 50
|
| 383 |
+
elif (
|
| 384 |
+
"trunk" in material_name_lower and "bark" in texture_name_lower
|
| 385 |
+
):
|
| 386 |
+
score += 30
|
| 387 |
+
|
| 388 |
+
return score
|
| 389 |
+
|
| 390 |
+
# Process each texture file
|
| 391 |
+
for texture_file in texture_files:
|
| 392 |
+
# Determine texture type
|
| 393 |
+
texture_type = determine_texture_type(texture_file)
|
| 394 |
+
|
| 395 |
+
# Don't process "unknown" textures
|
| 396 |
+
if texture_type == "unknown":
|
| 397 |
+
continue
|
| 398 |
+
|
| 399 |
+
# Score this texture for this material
|
| 400 |
+
score = score_texture_for_material(texture_file, texture_type)
|
| 401 |
+
|
| 402 |
+
# If it's a good match (score > 0), add to potential matches
|
| 403 |
+
if score > 0:
|
| 404 |
+
# Convert diffuse type to match our expected naming
|
| 405 |
+
if texture_type in ["albedo", "basecolor", "color"]:
|
| 406 |
+
texture_type = "diffuse"
|
| 407 |
+
|
| 408 |
+
# Add to matches with score
|
| 409 |
+
if texture_type in texture_matches:
|
| 410 |
+
texture_matches[texture_type].append((texture_file, score))
|
| 411 |
+
|
| 412 |
+
# Sort matches by score and select the best for each type
|
| 413 |
+
for texture_type, matches in texture_matches.items():
|
| 414 |
+
if matches:
|
| 415 |
+
# Sort by score (highest first)
|
| 416 |
+
matches.sort(key=lambda x: x[1], reverse=True)
|
| 417 |
+
best_match = matches[0][0]
|
| 418 |
+
|
| 419 |
+
# Add to material info
|
| 420 |
+
texture_path = os.path.join(textures_dir, best_match)
|
| 421 |
+
material_info["textures"][texture_type] = texture_path
|
| 422 |
+
logging.info(
|
| 423 |
+
f"Found {texture_type} texture for {material_name}: {best_match}"
|
| 424 |
+
)
|
| 425 |
+
|
| 426 |
+
# If we still don't have textures, use fallbacks from generic categories
|
| 427 |
+
if not any(material_info["textures"].values()):
|
| 428 |
+
logging.info(
|
| 429 |
+
f"No direct texture matches found for {material_name}, trying category fallbacks"
|
| 430 |
+
)
|
| 431 |
+
|
| 432 |
+
# Key textures we need
|
| 433 |
+
needed_types = ["diffuse", "normal", "roughness"]
|
| 434 |
+
|
| 435 |
+
# Generic fallbacks by category
|
| 436 |
+
fallbacks = {
|
| 437 |
+
"bark": {
|
| 438 |
+
"diffuse": "pinebark1_basecolor.png",
|
| 439 |
+
"normal": "pinebark1_normal.png",
|
| 440 |
+
"roughness": "pinebark1_roughness.png",
|
| 441 |
+
},
|
| 442 |
+
"leaf": {
|
| 443 |
+
"diffuse": "oakleaves1_basecolor.png",
|
| 444 |
+
"normal": "oakleaves1_normal.png",
|
| 445 |
+
"roughness": "oakleaves1_roughness.png",
|
| 446 |
+
},
|
| 447 |
+
"flower": {
|
| 448 |
+
"diffuse": "goldenchain_flowers_basecolor.png",
|
| 449 |
+
"normal": "goldenchain_flowers_normal.png",
|
| 450 |
+
"roughness": "goldenchain_flowers_roughness.png",
|
| 451 |
+
},
|
| 452 |
+
"grass": {
|
| 453 |
+
"diffuse": "ashleaves1_basecolor.png",
|
| 454 |
+
"normal": "ashleaves1_normal.png",
|
| 455 |
+
"roughness": "ashleaves1_roughness.png",
|
| 456 |
+
},
|
| 457 |
+
"needle": {
|
| 458 |
+
"diffuse": "spruceneedles_basecolor.png",
|
| 459 |
+
"normal": "spruceneedles_normal.png",
|
| 460 |
+
"roughness": "spruceneedles_roughness.png",
|
| 461 |
+
},
|
| 462 |
+
}
|
| 463 |
+
|
| 464 |
+
# Try each category we matched
|
| 465 |
+
for material_type in material_types:
|
| 466 |
+
if material_type in fallbacks:
|
| 467 |
+
for texture_type in needed_types:
|
| 468 |
+
if texture_type not in material_info[
|
| 469 |
+
"textures"
|
| 470 |
+
] and fallbacks[material_type].get(texture_type):
|
| 471 |
+
fallback_file = fallbacks[material_type][
|
| 472 |
+
texture_type
|
| 473 |
+
]
|
| 474 |
+
fallback_path = os.path.join(
|
| 475 |
+
textures_dir, fallback_file
|
| 476 |
+
)
|
| 477 |
+
if os.path.exists(fallback_path):
|
| 478 |
+
material_info["textures"][
|
| 479 |
+
texture_type
|
| 480 |
+
] = fallback_path
|
| 481 |
+
logging.info(
|
| 482 |
+
f"Using fallback {texture_type} texture for {material_name}: {fallback_file}"
|
| 483 |
+
)
|
| 484 |
+
|
| 485 |
+
# If still missing textures, use bark as an ultimate fallback (most common)
|
| 486 |
+
for texture_type in needed_types:
|
| 487 |
+
if texture_type not in material_info["textures"]:
|
| 488 |
+
fallback_file = fallbacks["bark"][texture_type]
|
| 489 |
+
fallback_path = os.path.join(textures_dir, fallback_file)
|
| 490 |
+
if os.path.exists(fallback_path):
|
| 491 |
+
material_info["textures"][texture_type] = fallback_path
|
| 492 |
+
logging.info(
|
| 493 |
+
f"Using ultimate fallback {texture_type} texture for {material_name}: {fallback_file}"
|
| 494 |
+
)
|
| 495 |
+
|
| 496 |
+
# Check for shader attributes
|
| 497 |
+
inputs_to_check = [
|
| 498 |
+
# Common texture inputs
|
| 499 |
+
"diffuse_color_texture",
|
| 500 |
+
"inputs:diffuse_color_texture",
|
| 501 |
+
"normalmap_texture",
|
| 502 |
+
"inputs:normalmap_texture",
|
| 503 |
+
"reflectionroughness_texture",
|
| 504 |
+
"inputs:reflectionroughness_texture",
|
| 505 |
+
"diffusecolor_texture",
|
| 506 |
+
"inputs:diffusecolor_texture",
|
| 507 |
+
"normal_texture",
|
| 508 |
+
"inputs:normal_texture",
|
| 509 |
+
"roughness_texture",
|
| 510 |
+
"inputs:roughness_texture",
|
| 511 |
+
# Common material constants
|
| 512 |
+
"diffuse_color_constant",
|
| 513 |
+
"inputs:diffuse_color_constant",
|
| 514 |
+
"reflection_roughness_constant",
|
| 515 |
+
"inputs:reflection_roughness_constant",
|
| 516 |
+
"metallic_constant",
|
| 517 |
+
"inputs:metallic_constant",
|
| 518 |
+
"opacity_constant",
|
| 519 |
+
"inputs:opacity_constant",
|
| 520 |
+
"emissive_color_constant",
|
| 521 |
+
"inputs:emissive_color_constant",
|
| 522 |
+
]
|
| 523 |
+
|
| 524 |
+
# Process each input attribute
|
| 525 |
+
for input_name in inputs_to_check:
|
| 526 |
+
# Remove "inputs:" prefix if present
|
| 527 |
+
input_name_clean = input_name.replace("inputs:", "")
|
| 528 |
+
|
| 529 |
+
# Try to get the input
|
| 530 |
+
shader_input = shader.GetInput(input_name_clean)
|
| 531 |
+
if not shader_input:
|
| 532 |
+
continue
|
| 533 |
+
|
| 534 |
+
# Get the value
|
| 535 |
+
value = shader_input.Get()
|
| 536 |
+
if value is None:
|
| 537 |
+
continue
|
| 538 |
+
|
| 539 |
+
# Format input name to standard form
|
| 540 |
+
standard_name = input_name_clean.lower()
|
| 541 |
+
|
| 542 |
+
# Check if this is a texture input
|
| 543 |
+
if "texture" in standard_name:
|
| 544 |
+
# Determine texture type
|
| 545 |
+
if "normal" in standard_name:
|
| 546 |
+
texture_type = "normal"
|
| 547 |
+
elif "rough" in standard_name:
|
| 548 |
+
texture_type = "roughness"
|
| 549 |
+
elif "diffuse" in standard_name or "color" in standard_name:
|
| 550 |
+
texture_type = "diffuse"
|
| 551 |
+
elif "specular" in standard_name:
|
| 552 |
+
texture_type = "specular"
|
| 553 |
+
elif "metallic" in standard_name:
|
| 554 |
+
texture_type = "metallic"
|
| 555 |
+
elif "opacity" in standard_name:
|
| 556 |
+
texture_type = "opacity"
|
| 557 |
+
elif "emissive" in standard_name:
|
| 558 |
+
texture_type = "emissive"
|
| 559 |
+
else:
|
| 560 |
+
texture_type = "other"
|
| 561 |
+
|
| 562 |
+
# Handle asset path values
|
| 563 |
+
if isinstance(value, Sdf.AssetPath):
|
| 564 |
+
texture_path = value.resolvedPath
|
| 565 |
+
if not texture_path:
|
| 566 |
+
# Try to resolve relative path
|
| 567 |
+
rel_path = value.path
|
| 568 |
+
if rel_path.startswith("./"):
|
| 569 |
+
rel_path = rel_path[2:]
|
| 570 |
+
texture_path = os.path.join(object_dir, rel_path)
|
| 571 |
+
|
| 572 |
+
if os.path.exists(texture_path):
|
| 573 |
+
# If we already found a texture through our material name matching,
|
| 574 |
+
# don't override it for vegetation materials
|
| 575 |
+
if (
|
| 576 |
+
dataset_type == "vegetation"
|
| 577 |
+
and texture_type in material_info["textures"]
|
| 578 |
+
):
|
| 579 |
+
logging.info(
|
| 580 |
+
f"Keeping already found {texture_type} texture for {material_name}"
|
| 581 |
+
)
|
| 582 |
+
else:
|
| 583 |
+
material_info["textures"][texture_type] = texture_path
|
| 584 |
+
|
| 585 |
+
# For vegetation, try to find exact textures by material name
|
| 586 |
+
if (
|
| 587 |
+
dataset_type == "vegetation"
|
| 588 |
+
and not material_info["textures"].get(texture_type)
|
| 589 |
+
and material_name
|
| 590 |
+
):
|
| 591 |
+
logging.info(
|
| 592 |
+
f"Looking for exact vegetation texture: {texture_type} for {material_name}"
|
| 593 |
+
)
|
| 594 |
+
|
| 595 |
+
# Find the materials/textures directory
|
| 596 |
+
object_dir_parts = object_dir.split(os.sep)
|
| 597 |
+
trees_dir = None
|
| 598 |
+
for i in range(len(object_dir_parts)):
|
| 599 |
+
if object_dir_parts[i] == "Trees":
|
| 600 |
+
trees_dir = os.sep.join(object_dir_parts[: i + 1])
|
| 601 |
+
break
|
| 602 |
+
|
| 603 |
+
if trees_dir:
|
| 604 |
+
materials_dir = os.path.join(trees_dir, "materials")
|
| 605 |
+
textures_dir = os.path.join(materials_dir, "textures")
|
| 606 |
+
|
| 607 |
+
logging.info(f"Looking for textures in: {textures_dir}")
|
| 608 |
+
|
| 609 |
+
if os.path.exists(textures_dir):
|
| 610 |
+
# Look for textures with material name
|
| 611 |
+
material_name_lower = material_name.lower()
|
| 612 |
+
|
| 613 |
+
# Build specific patterns for this material name
|
| 614 |
+
specific_patterns = [
|
| 615 |
+
f"{material_name_lower}_{texture_type}.png",
|
| 616 |
+
f"{material_name_lower.replace('_', '')}_{texture_type}.png",
|
| 617 |
+
]
|
| 618 |
+
|
| 619 |
+
# Try alternate texture type names for diffuse
|
| 620 |
+
if texture_type == "diffuse":
|
| 621 |
+
specific_patterns.extend(
|
| 622 |
+
[
|
| 623 |
+
f"{material_name_lower}_basecolor.png",
|
| 624 |
+
f"{material_name_lower.replace('_', '')}_basecolor.png",
|
| 625 |
+
f"{material_name_lower}_albedo.png",
|
| 626 |
+
f"{material_name_lower.replace('_', '')}_albedo.png",
|
| 627 |
+
]
|
| 628 |
+
)
|
| 629 |
+
|
| 630 |
+
# Search for exact matches only
|
| 631 |
+
for pattern in specific_patterns:
|
| 632 |
+
potential_file = os.path.join(textures_dir, pattern)
|
| 633 |
+
if os.path.exists(potential_file):
|
| 634 |
+
logging.info(
|
| 635 |
+
f"Found exact vegetation texture: {os.path.basename(potential_file)}"
|
| 636 |
+
)
|
| 637 |
+
material_info["textures"][texture_type] = potential_file
|
| 638 |
+
break
|
| 639 |
+
|
| 640 |
+
# If exact match not found, try partial matches
|
| 641 |
+
if not material_info["textures"].get(texture_type):
|
| 642 |
+
for file in os.listdir(textures_dir):
|
| 643 |
+
file_lower = file.lower()
|
| 644 |
+
if (
|
| 645 |
+
file_lower.endswith(".png")
|
| 646 |
+
and material_name_lower in file_lower
|
| 647 |
+
):
|
| 648 |
+
# Check for texture type in filename
|
| 649 |
+
if texture_type in file_lower or (
|
| 650 |
+
texture_type == "diffuse"
|
| 651 |
+
and "basecolor" in file_lower
|
| 652 |
+
):
|
| 653 |
+
full_path = os.path.join(textures_dir, file)
|
| 654 |
+
logging.info(
|
| 655 |
+
f"Found related vegetation texture: {file}"
|
| 656 |
+
)
|
| 657 |
+
material_info["textures"][
|
| 658 |
+
texture_type
|
| 659 |
+
] = full_path
|
| 660 |
+
break
|
| 661 |
+
else:
|
| 662 |
+
# Handle non-texture attributes
|
| 663 |
+
material_info[standard_name] = value
|
| 664 |
+
|
| 665 |
+
return material_info
|
| 666 |
+
|
| 667 |
+
|
| 668 |
+
def apply_generic_textures_to_segments(
|
| 669 |
+
segments, object_name, object_dir, output_textures_dir=None
|
| 670 |
+
):
|
| 671 |
+
"""
|
| 672 |
+
Apply generic textures to mesh segments that don't have textures.
|
| 673 |
+
|
| 674 |
+
Args:
|
| 675 |
+
segments (dict): Segments dictionary to update
|
| 676 |
+
object_name (str): Name of the object
|
| 677 |
+
object_dir (str): Directory containing the USD file
|
| 678 |
+
output_textures_dir (str, optional): Directory to save extracted textures
|
| 679 |
+
|
| 680 |
+
Returns:
|
| 681 |
+
dict: Updated segments dictionary
|
| 682 |
+
"""
|
| 683 |
+
# Skip if no segments
|
| 684 |
+
if not segments:
|
| 685 |
+
return segments
|
| 686 |
+
|
| 687 |
+
# Find the materials/textures directory
|
| 688 |
+
object_dir_parts = object_dir.split(os.sep)
|
| 689 |
+
trees_dir = None
|
| 690 |
+
shrub_dir = None
|
| 691 |
+
debris_dir = None
|
| 692 |
+
|
| 693 |
+
# Look for Trees directory
|
| 694 |
+
for i in range(len(object_dir_parts)):
|
| 695 |
+
if object_dir_parts[i] == "Trees":
|
| 696 |
+
trees_dir = os.sep.join(object_dir_parts[: i + 1])
|
| 697 |
+
break
|
| 698 |
+
|
| 699 |
+
# Look for Shrub directory
|
| 700 |
+
for i in range(len(object_dir_parts)):
|
| 701 |
+
if object_dir_parts[i] == "Shrub":
|
| 702 |
+
shrub_dir = os.sep.join(object_dir_parts[: i + 1])
|
| 703 |
+
break
|
| 704 |
+
|
| 705 |
+
# Look for Debris directory
|
| 706 |
+
for i in range(len(object_dir_parts)):
|
| 707 |
+
if object_dir_parts[i] == "Debris":
|
| 708 |
+
debris_dir = os.sep.join(object_dir_parts[: i + 1])
|
| 709 |
+
break
|
| 710 |
+
|
| 711 |
+
# Set up textures directory based on dataset subdirectory found
|
| 712 |
+
if trees_dir:
|
| 713 |
+
textures_dir = os.path.join(trees_dir, "materials", "textures")
|
| 714 |
+
elif shrub_dir:
|
| 715 |
+
textures_dir = os.path.join(shrub_dir, "materials", "textures")
|
| 716 |
+
elif debris_dir:
|
| 717 |
+
textures_dir = os.path.join(debris_dir, "materials", "textures")
|
| 718 |
+
else:
|
| 719 |
+
# Check for Plant_Tropical directory
|
| 720 |
+
tropical_dir = None
|
| 721 |
+
for i in range(len(object_dir_parts)):
|
| 722 |
+
if object_dir_parts[i] == "Plant_Tropical":
|
| 723 |
+
tropical_dir = os.sep.join(object_dir_parts[: i + 1])
|
| 724 |
+
break
|
| 725 |
+
|
| 726 |
+
if tropical_dir:
|
| 727 |
+
textures_dir = os.path.join(tropical_dir, "materials", "textures")
|
| 728 |
+
else:
|
| 729 |
+
# Try looking for material textures directory in current location
|
| 730 |
+
textures_dir = os.path.join(object_dir, "materials", "textures")
|
| 731 |
+
if not os.path.exists(textures_dir):
|
| 732 |
+
# Go up one directory and look there
|
| 733 |
+
parent_dir = os.path.dirname(object_dir)
|
| 734 |
+
textures_dir = os.path.join(parent_dir, "materials", "textures")
|
| 735 |
+
if not os.path.exists(textures_dir):
|
| 736 |
+
# Try root vegetation directory as a last resort
|
| 737 |
+
veg_root = None
|
| 738 |
+
for i in range(len(object_dir_parts)):
|
| 739 |
+
if object_dir_parts[i] == "vegetation":
|
| 740 |
+
veg_root = os.sep.join(object_dir_parts[: i + 1])
|
| 741 |
+
break
|
| 742 |
+
if veg_root:
|
| 743 |
+
textures_dir = os.path.join(veg_root, "materials", "textures")
|
| 744 |
+
|
| 745 |
+
# If no textures directory found, return segments unchanged
|
| 746 |
+
if not os.path.exists(textures_dir):
|
| 747 |
+
return segments
|
| 748 |
+
|
| 749 |
+
# Categorize object by name
|
| 750 |
+
object_name_lower = object_name.lower()
|
| 751 |
+
object_categories = []
|
| 752 |
+
|
| 753 |
+
# Common categories
|
| 754 |
+
category_keywords = {
|
| 755 |
+
"tree": [
|
| 756 |
+
"tree",
|
| 757 |
+
"pine",
|
| 758 |
+
"oak",
|
| 759 |
+
"maple",
|
| 760 |
+
"birch",
|
| 761 |
+
"cedar",
|
| 762 |
+
"ash",
|
| 763 |
+
"spruce",
|
| 764 |
+
"poplar",
|
| 765 |
+
"aspen",
|
| 766 |
+
"beech",
|
| 767 |
+
"dogwood",
|
| 768 |
+
"cypress",
|
| 769 |
+
"hemlock",
|
| 770 |
+
],
|
| 771 |
+
"palm": ["palm", "frond"],
|
| 772 |
+
"flower": ["flower", "bloom", "blossom", "rose", "tulip", "lily"],
|
| 773 |
+
"grass": [
|
| 774 |
+
"grass",
|
| 775 |
+
"reed",
|
| 776 |
+
"sedge",
|
| 777 |
+
"fern",
|
| 778 |
+
"bamboo",
|
| 779 |
+
"pampas",
|
| 780 |
+
"fountain",
|
| 781 |
+
"switchgrass",
|
| 782 |
+
],
|
| 783 |
+
"bush": [
|
| 784 |
+
"bush",
|
| 785 |
+
"shrub",
|
| 786 |
+
"boxwood",
|
| 787 |
+
"barberry",
|
| 788 |
+
"lilac",
|
| 789 |
+
"lupin",
|
| 790 |
+
"daphne",
|
| 791 |
+
"forsythia",
|
| 792 |
+
"vibernum",
|
| 793 |
+
"rhododendron",
|
| 794 |
+
],
|
| 795 |
+
}
|
| 796 |
+
|
| 797 |
+
# Determine categories
|
| 798 |
+
for category, keywords in category_keywords.items():
|
| 799 |
+
if any(keyword in object_name_lower for keyword in keywords):
|
| 800 |
+
object_categories.append(category)
|
| 801 |
+
|
| 802 |
+
if not object_categories:
|
| 803 |
+
# Default to tree if no other category matched
|
| 804 |
+
object_categories = ["tree"]
|
| 805 |
+
|
| 806 |
+
# Define generic texture sets for each category and part
|
| 807 |
+
generic_textures = {
|
| 808 |
+
"tree": {
|
| 809 |
+
"bark": {
|
| 810 |
+
"diffuse": "pinebark1_basecolor.png",
|
| 811 |
+
"normal": "pinebark1_normal.png",
|
| 812 |
+
"roughness": "pinebark1_roughness.png",
|
| 813 |
+
},
|
| 814 |
+
"leaf": {
|
| 815 |
+
"diffuse": "oakleaves1_basecolor.png",
|
| 816 |
+
"normal": "oakleaves1_normal.png",
|
| 817 |
+
"roughness": "oakleaves1_roughness.png",
|
| 818 |
+
},
|
| 819 |
+
},
|
| 820 |
+
"palm": {
|
| 821 |
+
"bark": {
|
| 822 |
+
"diffuse": "bark1_basecolor.png",
|
| 823 |
+
"normal": "bark1_normal.png",
|
| 824 |
+
"roughness": "bark1_roughness.png",
|
| 825 |
+
},
|
| 826 |
+
"leaf": {
|
| 827 |
+
"diffuse": "palmleaves_mat_basecolor.png",
|
| 828 |
+
"normal": "palmleaves_mat_normal.png",
|
| 829 |
+
"roughness": "palmleaves_mat_roughness.png",
|
| 830 |
+
},
|
| 831 |
+
},
|
| 832 |
+
"flower": {
|
| 833 |
+
"stem": {
|
| 834 |
+
"diffuse": "bark2_basecolor.png",
|
| 835 |
+
"normal": "bark2_normal.png",
|
| 836 |
+
"roughness": "bark2_roughness.png",
|
| 837 |
+
},
|
| 838 |
+
"petal": {
|
| 839 |
+
"diffuse": "goldenchain_flowers_basecolor.png",
|
| 840 |
+
"normal": "goldenchain_flowers_normal.png",
|
| 841 |
+
"roughness": "goldenchain_flowers_roughness.png",
|
| 842 |
+
},
|
| 843 |
+
},
|
| 844 |
+
"grass": {
|
| 845 |
+
"blade": {
|
| 846 |
+
"diffuse": "ashleaves1_basecolor.png",
|
| 847 |
+
"normal": "ashleaves1_normal.png",
|
| 848 |
+
"roughness": "ashleaves1_roughness.png",
|
| 849 |
+
}
|
| 850 |
+
},
|
| 851 |
+
"bush": {
|
| 852 |
+
"branch": {
|
| 853 |
+
"diffuse": "bark3_basecolor.png",
|
| 854 |
+
"normal": "bark3_normal.png",
|
| 855 |
+
"roughness": "bark3_roughness.png",
|
| 856 |
+
},
|
| 857 |
+
"leaf": {
|
| 858 |
+
"diffuse": "dogwood_leaf_basecolor.png",
|
| 859 |
+
"normal": "dogwood_leaf_normal.png",
|
| 860 |
+
"roughness": "dogwood_leaf_roughness.png",
|
| 861 |
+
},
|
| 862 |
+
},
|
| 863 |
+
}
|
| 864 |
+
|
| 865 |
+
# Special material name to texture mappings for problematic cases
|
| 866 |
+
special_material_textures = {
|
| 867 |
+
# Special material names
|
| 868 |
+
"Lupin_m": {
|
| 869 |
+
"diffuse": "lupin_basecolor.png",
|
| 870 |
+
"normal": "lupin_normal.png",
|
| 871 |
+
"roughness": "lupin_roughness.png",
|
| 872 |
+
},
|
| 873 |
+
"Dagger_M": {
|
| 874 |
+
"diffuse": "plantatlas1_basecolor.png",
|
| 875 |
+
"normal": "plantatlas1_normal.png",
|
| 876 |
+
"roughness": "plantatlas1_roughness.png",
|
| 877 |
+
},
|
| 878 |
+
"bark3": {
|
| 879 |
+
"diffuse": "bark3_basecolor.png",
|
| 880 |
+
"normal": "bark3_normal.png",
|
| 881 |
+
"roughness": "bark3_roughness.png",
|
| 882 |
+
},
|
| 883 |
+
"Pampas_flower": {
|
| 884 |
+
"diffuse": "pampas_flower.png",
|
| 885 |
+
"normal": "fanpalm_normal.png", # Fallback normal map
|
| 886 |
+
"roughness": "fanpalm_roughness.png", # Fallback roughness map
|
| 887 |
+
},
|
| 888 |
+
"FountainGrass": {
|
| 889 |
+
"diffuse": "fountaingrass_basecolor.png",
|
| 890 |
+
"normal": "pampas_grass_normal.png",
|
| 891 |
+
"roughness": "pampas_grass.png",
|
| 892 |
+
},
|
| 893 |
+
"TreeBark_01": {
|
| 894 |
+
"diffuse": "tree_bark_03_diff_2k.png",
|
| 895 |
+
"normal": "bark1_normal.png",
|
| 896 |
+
"roughness": "sycamorebark2_roughness.png",
|
| 897 |
+
},
|
| 898 |
+
"Barberry": {
|
| 899 |
+
"diffuse": "barberry_basecolor.png",
|
| 900 |
+
"normal": "bark3_normal.png", # Fallback
|
| 901 |
+
"roughness": "bark3_roughness.png", # Fallback
|
| 902 |
+
},
|
| 903 |
+
"Century_m": {
|
| 904 |
+
"diffuse": "century_m_basecolor.png",
|
| 905 |
+
"normal": "Century_m_Normal.png",
|
| 906 |
+
"roughness": "Century_m_Roughness.png",
|
| 907 |
+
},
|
| 908 |
+
"Rhododendron": {
|
| 909 |
+
"diffuse": "rhododendron_basecolor.png",
|
| 910 |
+
"normal": "rhododendron_normal.png",
|
| 911 |
+
"roughness": "rhododendron_roughness.png",
|
| 912 |
+
},
|
| 913 |
+
# Add more problematic materials
|
| 914 |
+
"Burning_Bush": {
|
| 915 |
+
"diffuse": "burningbush_leaf_basecolor.png",
|
| 916 |
+
"normal": "burningbush_leaf_normal.png",
|
| 917 |
+
"roughness": "burningbush_leaf_roughness.png",
|
| 918 |
+
},
|
| 919 |
+
"Cedar_Shrub": {
|
| 920 |
+
"diffuse": "pinebark1_basecolor.png",
|
| 921 |
+
"normal": "pinebark1_normal.png",
|
| 922 |
+
"roughness": "pinebark1_roughness.png",
|
| 923 |
+
},
|
| 924 |
+
"Japanese_Flame": {
|
| 925 |
+
"diffuse": "japaneseflame_basecolor.png",
|
| 926 |
+
"normal": "japaneseflame_normal.png",
|
| 927 |
+
"roughness": "japaneseflame_roughness.png",
|
| 928 |
+
},
|
| 929 |
+
"Honey_Myrtle": {
|
| 930 |
+
"diffuse": "honeymyrtle_basecolor.png",
|
| 931 |
+
"normal": "hollyprivet_normal.png", # Fallback
|
| 932 |
+
"roughness": "hollyprivet_roughness.png", # Fallback
|
| 933 |
+
},
|
| 934 |
+
"Hurricane_Palm_bark_Mat": {
|
| 935 |
+
"diffuse": "hurricanepalm_bark_basecolor.png",
|
| 936 |
+
"normal": "hurricanepalm_bark_normal.png",
|
| 937 |
+
"roughness": "hurricanepalm_bark_roughness.png",
|
| 938 |
+
},
|
| 939 |
+
"Australian_Fern_leaves_Mat": {
|
| 940 |
+
"diffuse": "australianfern_leaves_basecolor.png",
|
| 941 |
+
"normal": "australianfern_leaves_normal.png",
|
| 942 |
+
"roughness": "australianfern_leaves_roughness.png",
|
| 943 |
+
},
|
| 944 |
+
"Australian_Fern_trunk": {
|
| 945 |
+
"diffuse": "australianfern_trunk_basecolor.png",
|
| 946 |
+
"normal": "australianfern_trunk_normal.png",
|
| 947 |
+
"roughness": "australianfern_trunk_roughness.png",
|
| 948 |
+
},
|
| 949 |
+
"Agave_mat": {
|
| 950 |
+
"diffuse": "agave_basecolor.png",
|
| 951 |
+
"normal": "agave_normal.png",
|
| 952 |
+
"roughness": "Agave_Roughness.png",
|
| 953 |
+
},
|
| 954 |
+
"Bamboo_leaf_Mat1": {
|
| 955 |
+
"diffuse": "bambooleaf_basecolor.png",
|
| 956 |
+
"normal": "bambooleaf_normal.png",
|
| 957 |
+
"roughness": "bambooleaf_roughness.png",
|
| 958 |
+
},
|
| 959 |
+
"Bamboo_shoot_Mat1": {
|
| 960 |
+
"diffuse": "bambooshoot_basecolor.png",
|
| 961 |
+
"normal": "bambooshoot_normal.png",
|
| 962 |
+
"roughness": "bambooshoot_roughness.png",
|
| 963 |
+
},
|
| 964 |
+
"CraneLily_mat": {
|
| 965 |
+
"diffuse": "cranelily_basecolor.png",
|
| 966 |
+
"normal": "cranelily_normal.png",
|
| 967 |
+
"roughness": "cranelily_roughness.png",
|
| 968 |
+
},
|
| 969 |
+
"CraneLily_mat_2": {
|
| 970 |
+
"diffuse": "cranelily_basecolor.png",
|
| 971 |
+
"normal": "cranelily_normal.png",
|
| 972 |
+
"roughness": "cranelily_roughness.png",
|
| 973 |
+
},
|
| 974 |
+
"CraneLily_mat_3": {
|
| 975 |
+
"diffuse": "cranelily_basecolor.png",
|
| 976 |
+
"normal": "cranelily_normal.png",
|
| 977 |
+
"roughness": "cranelily_roughness.png",
|
| 978 |
+
},
|
| 979 |
+
"GrassPalm_bark": {
|
| 980 |
+
"diffuse": "grasspalm_bark_basecolor.png",
|
| 981 |
+
"normal": "grasspalm_bark_normal.png",
|
| 982 |
+
"roughness": "grasspalm_bark_roughness.png",
|
| 983 |
+
},
|
| 984 |
+
"GrassPalm_leaves": {
|
| 985 |
+
"diffuse": "grasspalm_leaves_basecolor.png",
|
| 986 |
+
"normal": "grasspalm_leaves_normal.png",
|
| 987 |
+
"roughness": "grasspalm_leaves_roughness.png",
|
| 988 |
+
},
|
| 989 |
+
}
|
| 990 |
+
|
| 991 |
+
# First try to apply special material textures based on material name in each segment
|
| 992 |
+
for segment_key, segment_info in segments.items():
|
| 993 |
+
if segment_info is None:
|
| 994 |
+
continue
|
| 995 |
+
|
| 996 |
+
# Skip segments that already have textures
|
| 997 |
+
if segment_info.get("textures") and len(segment_info["textures"]) > 0:
|
| 998 |
+
continue
|
| 999 |
+
|
| 1000 |
+
# Initialize textures dict if needed
|
| 1001 |
+
if "textures" not in segment_info:
|
| 1002 |
+
segment_info["textures"] = {}
|
| 1003 |
+
|
| 1004 |
+
# Get material name
|
| 1005 |
+
material_name = segment_info.get("name", "")
|
| 1006 |
+
|
| 1007 |
+
# Check for special material name mapping
|
| 1008 |
+
if material_name in special_material_textures:
|
| 1009 |
+
for texture_type, texture_file in special_material_textures[
|
| 1010 |
+
material_name
|
| 1011 |
+
].items():
|
| 1012 |
+
texture_path = os.path.join(textures_dir, texture_file)
|
| 1013 |
+
if os.path.exists(texture_path):
|
| 1014 |
+
segment_info["textures"][texture_type] = texture_path
|
| 1015 |
+
|
| 1016 |
+
# Copy texture if needed
|
| 1017 |
+
if output_textures_dir:
|
| 1018 |
+
copied_path = copy_texture_to_output(
|
| 1019 |
+
texture_path,
|
| 1020 |
+
output_textures_dir,
|
| 1021 |
+
object_name,
|
| 1022 |
+
material_name,
|
| 1023 |
+
texture_type,
|
| 1024 |
+
)
|
| 1025 |
+
if copied_path:
|
| 1026 |
+
segment_info["textures"][
|
| 1027 |
+
f"{texture_type}_copied"
|
| 1028 |
+
] = copied_path
|
| 1029 |
+
|
| 1030 |
+
# If we found specific textures for this segment, continue to next segment
|
| 1031 |
+
if segment_info.get("textures") and len(segment_info["textures"]) > 0:
|
| 1032 |
+
continue
|
| 1033 |
+
|
| 1034 |
+
# Apply category-based textures if specific ones weren't found
|
| 1035 |
+
material_type = segment_info.get("material_type", "")
|
| 1036 |
+
segment_type = "leaf" # Default
|
| 1037 |
+
|
| 1038 |
+
# Determine segment type
|
| 1039 |
+
if material_type in ["bark", "trunk", "stem", "branch", "stalk"]:
|
| 1040 |
+
segment_type = (
|
| 1041 |
+
"bark" if "bark" in generic_textures[object_categories[0]] else "branch"
|
| 1042 |
+
)
|
| 1043 |
+
elif material_type in ["leaf", "leaves", "foliage", "needle", "frond"]:
|
| 1044 |
+
segment_type = "leaf"
|
| 1045 |
+
elif material_type in ["petal", "flower", "bloom", "blossom"]:
|
| 1046 |
+
segment_type = "petal"
|
| 1047 |
+
elif material_type in ["blade", "grass"]:
|
| 1048 |
+
segment_type = "blade"
|
| 1049 |
+
|
| 1050 |
+
# Get the right texture set based on object category and segment type
|
| 1051 |
+
for category in object_categories:
|
| 1052 |
+
if (
|
| 1053 |
+
category in generic_textures
|
| 1054 |
+
and segment_type in generic_textures[category]
|
| 1055 |
+
):
|
| 1056 |
+
texture_set = generic_textures[category][segment_type]
|
| 1057 |
+
|
| 1058 |
+
# Apply textures from set
|
| 1059 |
+
for texture_type, texture_file in texture_set.items():
|
| 1060 |
+
texture_path = os.path.join(textures_dir, texture_file)
|
| 1061 |
+
if os.path.exists(texture_path):
|
| 1062 |
+
segment_info["textures"][texture_type] = texture_path
|
| 1063 |
+
|
| 1064 |
+
# Copy texture if needed
|
| 1065 |
+
if output_textures_dir:
|
| 1066 |
+
copied_path = copy_texture_to_output(
|
| 1067 |
+
texture_path,
|
| 1068 |
+
output_textures_dir,
|
| 1069 |
+
object_name,
|
| 1070 |
+
material_name or segment_key,
|
| 1071 |
+
texture_type,
|
| 1072 |
+
)
|
| 1073 |
+
if copied_path:
|
| 1074 |
+
segment_info["textures"][
|
| 1075 |
+
f"{texture_type}_copied"
|
| 1076 |
+
] = copied_path
|
| 1077 |
+
|
| 1078 |
+
# Break once we found a suitable texture set
|
| 1079 |
+
if segment_info.get("textures") and len(segment_info["textures"]) > 0:
|
| 1080 |
+
break
|
| 1081 |
+
|
| 1082 |
+
# If we still don't have textures, try to find them by looking for any textures that might match
|
| 1083 |
+
if not segment_info.get("textures") or len(segment_info["textures"]) == 0:
|
| 1084 |
+
# Try to find any textures that might match by name
|
| 1085 |
+
object_dir_lower = object_dir.lower()
|
| 1086 |
+
material_name_lower = material_name.lower() if material_name else ""
|
| 1087 |
+
segment_key_lower = segment_key.lower()
|
| 1088 |
+
object_name_lower = object_name.lower()
|
| 1089 |
+
|
| 1090 |
+
# Look in the textures directory for matching textures
|
| 1091 |
+
for texture_file in os.listdir(textures_dir):
|
| 1092 |
+
texture_lower = texture_file.lower()
|
| 1093 |
+
|
| 1094 |
+
# Try to find matches by object name, material name, or segment key
|
| 1095 |
+
if (
|
| 1096 |
+
object_name_lower in texture_lower
|
| 1097 |
+
or material_name_lower in texture_lower
|
| 1098 |
+
or segment_key_lower in texture_lower
|
| 1099 |
+
):
|
| 1100 |
+
|
| 1101 |
+
# Determine texture type
|
| 1102 |
+
texture_type = None
|
| 1103 |
+
if "basecolor" in texture_lower or "diffuse" in texture_lower:
|
| 1104 |
+
texture_type = "diffuse"
|
| 1105 |
+
elif "normal" in texture_lower:
|
| 1106 |
+
texture_type = "normal"
|
| 1107 |
+
elif "roughness" in texture_lower:
|
| 1108 |
+
texture_type = "roughness"
|
| 1109 |
+
|
| 1110 |
+
if texture_type:
|
| 1111 |
+
texture_path = os.path.join(textures_dir, texture_file)
|
| 1112 |
+
segment_info["textures"][texture_type] = texture_path
|
| 1113 |
+
|
| 1114 |
+
# Copy texture if needed
|
| 1115 |
+
if output_textures_dir:
|
| 1116 |
+
copied_path = copy_texture_to_output(
|
| 1117 |
+
texture_path,
|
| 1118 |
+
output_textures_dir,
|
| 1119 |
+
object_name,
|
| 1120 |
+
material_name or segment_key,
|
| 1121 |
+
texture_type,
|
| 1122 |
+
)
|
| 1123 |
+
if copied_path:
|
| 1124 |
+
segment_info["textures"][
|
| 1125 |
+
f"{texture_type}_copied"
|
| 1126 |
+
] = copied_path
|
| 1127 |
+
|
| 1128 |
+
# If still missing textures, apply default textures
|
| 1129 |
+
for segment_key, segment_info in segments.items():
|
| 1130 |
+
if segment_info is None:
|
| 1131 |
+
continue
|
| 1132 |
+
|
| 1133 |
+
if not segment_info.get("textures"):
|
| 1134 |
+
segment_info["textures"] = {}
|
| 1135 |
+
|
| 1136 |
+
# Check if we're missing any texture types
|
| 1137 |
+
missing_types = []
|
| 1138 |
+
for texture_type in ["diffuse", "normal", "roughness"]:
|
| 1139 |
+
if texture_type not in segment_info["textures"]:
|
| 1140 |
+
missing_types.append(texture_type)
|
| 1141 |
+
|
| 1142 |
+
if not missing_types:
|
| 1143 |
+
continue
|
| 1144 |
+
|
| 1145 |
+
# Determine segment type again
|
| 1146 |
+
material_type = segment_info.get("material_type", "")
|
| 1147 |
+
segment_type = "leaf" # Default
|
| 1148 |
+
|
| 1149 |
+
if material_type in ["bark", "trunk", "stem", "branch", "stalk"]:
|
| 1150 |
+
segment_type = (
|
| 1151 |
+
"bark" if "bark" in generic_textures[object_categories[0]] else "branch"
|
| 1152 |
+
)
|
| 1153 |
+
elif material_type in ["leaf", "leaves", "foliage", "needle", "frond"]:
|
| 1154 |
+
segment_type = "leaf"
|
| 1155 |
+
elif material_type in ["petal", "flower", "bloom", "blossom"]:
|
| 1156 |
+
segment_type = "petal"
|
| 1157 |
+
elif material_type in ["blade", "grass"]:
|
| 1158 |
+
segment_type = "blade"
|
| 1159 |
+
|
| 1160 |
+
# Apply default textures from the first applicable category
|
| 1161 |
+
for category in object_categories:
|
| 1162 |
+
if (
|
| 1163 |
+
category in generic_textures
|
| 1164 |
+
and segment_type in generic_textures[category]
|
| 1165 |
+
):
|
| 1166 |
+
for texture_type in missing_types:
|
| 1167 |
+
if texture_type in generic_textures[category][segment_type]:
|
| 1168 |
+
texture_file = generic_textures[category][segment_type][
|
| 1169 |
+
texture_type
|
| 1170 |
+
]
|
| 1171 |
+
texture_path = os.path.join(textures_dir, texture_file)
|
| 1172 |
+
|
| 1173 |
+
if os.path.exists(texture_path):
|
| 1174 |
+
segment_info["textures"][texture_type] = texture_path
|
| 1175 |
+
|
| 1176 |
+
# Copy texture if needed
|
| 1177 |
+
if output_textures_dir:
|
| 1178 |
+
copied_path = copy_texture_to_output(
|
| 1179 |
+
texture_path,
|
| 1180 |
+
output_textures_dir,
|
| 1181 |
+
object_name,
|
| 1182 |
+
segment_info.get("name", segment_key),
|
| 1183 |
+
texture_type,
|
| 1184 |
+
)
|
| 1185 |
+
if copied_path:
|
| 1186 |
+
segment_info["textures"][
|
| 1187 |
+
f"{texture_type}_copied"
|
| 1188 |
+
] = copied_path
|
| 1189 |
+
|
| 1190 |
+
# Break once we've applied textures from a category
|
| 1191 |
+
if all(
|
| 1192 |
+
texture_type in segment_info["textures"]
|
| 1193 |
+
for texture_type in missing_types
|
| 1194 |
+
):
|
| 1195 |
+
break
|
| 1196 |
+
|
| 1197 |
+
return segments
|
| 1198 |
+
|
| 1199 |
+
|
| 1200 |
+
def extract_materials_from_usd(
|
| 1201 |
+
usd_file_path, dataset_type=None, output_textures_dir=None
|
| 1202 |
+
):
|
| 1203 |
+
"""
|
| 1204 |
+
Extract material information from a USD file with improved handling of material bindings.
|
| 1205 |
+
|
| 1206 |
+
Args:
|
| 1207 |
+
usd_file_path: Path to the USD file
|
| 1208 |
+
dataset_type: Type of dataset (residential, commercial, etc.)
|
| 1209 |
+
|
| 1210 |
+
Returns:
|
| 1211 |
+
Dictionary with material information
|
| 1212 |
+
"""
|
| 1213 |
+
logging.info(f"Extracting materials from {usd_file_path}")
|
| 1214 |
+
result = {
|
| 1215 |
+
"object_name": os.path.splitext(os.path.basename(usd_file_path))[0],
|
| 1216 |
+
"dataset_type": dataset_type,
|
| 1217 |
+
"file_path": usd_file_path,
|
| 1218 |
+
"date_processed": datetime.datetime.now().isoformat(),
|
| 1219 |
+
"segments": {},
|
| 1220 |
+
}
|
| 1221 |
+
|
| 1222 |
+
# Open the USD stage
|
| 1223 |
+
try:
|
| 1224 |
+
stage = Usd.Stage.Open(usd_file_path)
|
| 1225 |
+
if not stage:
|
| 1226 |
+
logging.error(f"Could not open USD file: {usd_file_path}")
|
| 1227 |
+
return None
|
| 1228 |
+
except Exception as e:
|
| 1229 |
+
logging.error(f"Error opening USD file {usd_file_path}: {str(e)}")
|
| 1230 |
+
return None
|
| 1231 |
+
|
| 1232 |
+
# Track all materials we find in the stage
|
| 1233 |
+
all_materials = {}
|
| 1234 |
+
|
| 1235 |
+
# First pass: collect all materials and their properties
|
| 1236 |
+
logging.info("First pass: collecting all materials")
|
| 1237 |
+
for prim in stage.Traverse():
|
| 1238 |
+
if prim.IsA(UsdShade.Material):
|
| 1239 |
+
material = UsdShade.Material(prim)
|
| 1240 |
+
material_path = str(prim.GetPath())
|
| 1241 |
+
material_name = prim.GetName()
|
| 1242 |
+
|
| 1243 |
+
# Store material info with default values
|
| 1244 |
+
all_materials[material_path] = {
|
| 1245 |
+
"name": material_name,
|
| 1246 |
+
"material_type": material_name, # Default to name
|
| 1247 |
+
"textures": {},
|
| 1248 |
+
}
|
| 1249 |
+
|
| 1250 |
+
# Process material's shaders to find textures
|
| 1251 |
+
# Correctly get all the shader prims in this material
|
| 1252 |
+
shader_prims = []
|
| 1253 |
+
for child_prim in Usd.PrimRange(prim):
|
| 1254 |
+
if child_prim.IsA(UsdShade.Shader):
|
| 1255 |
+
shader_prims.append(child_prim)
|
| 1256 |
+
|
| 1257 |
+
for shader_prim in shader_prims:
|
| 1258 |
+
shader = UsdShade.Shader(shader_prim)
|
| 1259 |
+
if not shader:
|
| 1260 |
+
continue
|
| 1261 |
+
|
| 1262 |
+
# Inspect shader inputs for textures
|
| 1263 |
+
for input in shader.GetInputs():
|
| 1264 |
+
input_name = input.GetBaseName()
|
| 1265 |
+
|
| 1266 |
+
# Check if this input has a connected source that's an asset
|
| 1267 |
+
if input.HasConnectedSource():
|
| 1268 |
+
source = input.GetConnectedSource()
|
| 1269 |
+
if source:
|
| 1270 |
+
source_shader, source_output, _ = source
|
| 1271 |
+
source_prim = source_shader.GetPrim()
|
| 1272 |
+
|
| 1273 |
+
# Check if the source is a texture
|
| 1274 |
+
if source_prim.IsA(UsdShade.Shader):
|
| 1275 |
+
source_shader_id = UsdShade.Shader(
|
| 1276 |
+
source_prim
|
| 1277 |
+
).GetShaderId()
|
| 1278 |
+
if "texture" in str(source_shader_id).lower():
|
| 1279 |
+
# Try to find the file asset path
|
| 1280 |
+
for source_input in UsdShade.Shader(
|
| 1281 |
+
source_prim
|
| 1282 |
+
).GetInputs():
|
| 1283 |
+
if source_input.GetBaseName() in [
|
| 1284 |
+
"file",
|
| 1285 |
+
"filename",
|
| 1286 |
+
"filePath",
|
| 1287 |
+
"varname",
|
| 1288 |
+
]:
|
| 1289 |
+
asset_path = source_input.Get()
|
| 1290 |
+
if asset_path:
|
| 1291 |
+
# Determine texture type from connection patterns
|
| 1292 |
+
tex_type = "unknown"
|
| 1293 |
+
if (
|
| 1294 |
+
"diffuse" in input_name.lower()
|
| 1295 |
+
or "albedo" in input_name.lower()
|
| 1296 |
+
or "color" in input_name.lower()
|
| 1297 |
+
):
|
| 1298 |
+
tex_type = "diffuse"
|
| 1299 |
+
elif "normal" in input_name.lower():
|
| 1300 |
+
tex_type = "normal"
|
| 1301 |
+
elif "roughness" in input_name.lower():
|
| 1302 |
+
tex_type = "roughness"
|
| 1303 |
+
elif "metallic" in input_name.lower():
|
| 1304 |
+
tex_type = "metallic"
|
| 1305 |
+
elif "specular" in input_name.lower():
|
| 1306 |
+
tex_type = "specular"
|
| 1307 |
+
elif (
|
| 1308 |
+
"displacement" in input_name.lower()
|
| 1309 |
+
):
|
| 1310 |
+
tex_type = "displacement"
|
| 1311 |
+
|
| 1312 |
+
# Store texture path
|
| 1313 |
+
logging.info(
|
| 1314 |
+
f"Found texture: {tex_type} = {asset_path} for material {material_name}"
|
| 1315 |
+
)
|
| 1316 |
+
all_materials[material_path][
|
| 1317 |
+
"textures"
|
| 1318 |
+
][tex_type] = str(asset_path)
|
| 1319 |
+
|
| 1320 |
+
# Direct asset inputs (not connected through other shaders)
|
| 1321 |
+
elif input.GetTypeName() == "asset":
|
| 1322 |
+
asset_path = input.Get()
|
| 1323 |
+
if asset_path:
|
| 1324 |
+
# Determine texture type from input name
|
| 1325 |
+
tex_type = "unknown"
|
| 1326 |
+
if (
|
| 1327 |
+
"diffuse" in input_name.lower()
|
| 1328 |
+
or "albedo" in input_name.lower()
|
| 1329 |
+
or "color" in input_name.lower()
|
| 1330 |
+
):
|
| 1331 |
+
tex_type = "diffuse"
|
| 1332 |
+
elif "normal" in input_name.lower():
|
| 1333 |
+
tex_type = "normal"
|
| 1334 |
+
elif "roughness" in input_name.lower():
|
| 1335 |
+
tex_type = "roughness"
|
| 1336 |
+
elif "metallic" in input_name.lower():
|
| 1337 |
+
tex_type = "metallic"
|
| 1338 |
+
elif "specular" in input_name.lower():
|
| 1339 |
+
tex_type = "specular"
|
| 1340 |
+
elif "displacement" in input_name.lower():
|
| 1341 |
+
tex_type = "displacement"
|
| 1342 |
+
|
| 1343 |
+
# Store texture path
|
| 1344 |
+
logging.info(
|
| 1345 |
+
f"Found direct texture: {tex_type} = {asset_path} for material {material_name}"
|
| 1346 |
+
)
|
| 1347 |
+
all_materials[material_path]["textures"][tex_type] = str(
|
| 1348 |
+
asset_path
|
| 1349 |
+
)
|
| 1350 |
+
|
| 1351 |
+
# Second pass: find all material bindings
|
| 1352 |
+
logging.info("Second pass: finding material bindings")
|
| 1353 |
+
|
| 1354 |
+
# Process meshes and their subsets
|
| 1355 |
+
for prim in stage.Traverse():
|
| 1356 |
+
if prim.IsA(UsdGeom.Mesh):
|
| 1357 |
+
mesh = UsdGeom.Mesh(prim)
|
| 1358 |
+
mesh_name = prim.GetName()
|
| 1359 |
+
logging.info(f"Processing mesh: {mesh_name}")
|
| 1360 |
+
|
| 1361 |
+
# First check direct binding on the mesh
|
| 1362 |
+
binding_api = UsdShade.MaterialBindingAPI(prim)
|
| 1363 |
+
direct_binding = binding_api.GetDirectBinding()
|
| 1364 |
+
direct_material = None
|
| 1365 |
+
|
| 1366 |
+
if direct_binding.GetMaterial():
|
| 1367 |
+
direct_material = direct_binding.GetMaterial()
|
| 1368 |
+
material_path = str(direct_material.GetPath())
|
| 1369 |
+
logging.info(f" Found direct material binding: {material_path}")
|
| 1370 |
+
|
| 1371 |
+
if material_path in all_materials:
|
| 1372 |
+
# Create segment for the whole mesh
|
| 1373 |
+
segment_key = f"{mesh_name}_whole"
|
| 1374 |
+
material_info = all_materials[material_path].copy()
|
| 1375 |
+
material_info["semantic_usage"] = mesh_name
|
| 1376 |
+
|
| 1377 |
+
result["segments"][segment_key] = material_info
|
| 1378 |
+
logging.info(
|
| 1379 |
+
f" Created segment {segment_key} with material {material_path}"
|
| 1380 |
+
)
|
| 1381 |
+
|
| 1382 |
+
# Then check GeomSubsets - these are more specific material assignments
|
| 1383 |
+
imageable = UsdGeom.Imageable(prim)
|
| 1384 |
+
subsets = UsdGeom.Subset.GetGeomSubsets(imageable)
|
| 1385 |
+
|
| 1386 |
+
if subsets:
|
| 1387 |
+
logging.info(f" Found {len(subsets)} geom subsets for {mesh_name}")
|
| 1388 |
+
for subset in subsets:
|
| 1389 |
+
subset_prim = subset.GetPrim()
|
| 1390 |
+
subset_name = subset_prim.GetName()
|
| 1391 |
+
family = (
|
| 1392 |
+
subset.GetFamilyNameAttr().Get()
|
| 1393 |
+
if subset.GetFamilyNameAttr()
|
| 1394 |
+
else "unknown"
|
| 1395 |
+
)
|
| 1396 |
+
|
| 1397 |
+
logging.info(
|
| 1398 |
+
f" Processing subset: {subset_name} (Family: {family})"
|
| 1399 |
+
)
|
| 1400 |
+
|
| 1401 |
+
# Check material binding on subset
|
| 1402 |
+
subset_binding_api = UsdShade.MaterialBindingAPI(subset_prim)
|
| 1403 |
+
subset_direct_binding = subset_binding_api.GetDirectBinding()
|
| 1404 |
+
|
| 1405 |
+
if subset_direct_binding.GetMaterial():
|
| 1406 |
+
subset_material = subset_direct_binding.GetMaterial()
|
| 1407 |
+
subset_material_path = str(subset_material.GetPath())
|
| 1408 |
+
logging.info(
|
| 1409 |
+
f" Found subset material binding: {subset_material_path}"
|
| 1410 |
+
)
|
| 1411 |
+
|
| 1412 |
+
if subset_material_path in all_materials:
|
| 1413 |
+
# Create segment for this subset
|
| 1414 |
+
segment_key = subset_name
|
| 1415 |
+
material_info = all_materials[subset_material_path].copy()
|
| 1416 |
+
material_info["semantic_usage"] = subset_name
|
| 1417 |
+
|
| 1418 |
+
result["segments"][segment_key] = material_info
|
| 1419 |
+
logging.info(
|
| 1420 |
+
f" Created segment {segment_key} with material {subset_material_path}"
|
| 1421 |
+
)
|
| 1422 |
+
|
| 1423 |
+
# If no subsets but we have a direct material, use that
|
| 1424 |
+
if not subsets and direct_material:
|
| 1425 |
+
material_path = str(direct_material.GetPath())
|
| 1426 |
+
|
| 1427 |
+
if material_path in all_materials:
|
| 1428 |
+
# Create segment for the whole mesh
|
| 1429 |
+
segment_key = mesh_name
|
| 1430 |
+
material_info = all_materials[material_path].copy()
|
| 1431 |
+
material_info["semantic_usage"] = mesh_name
|
| 1432 |
+
|
| 1433 |
+
result["segments"][segment_key] = material_info
|
| 1434 |
+
logging.info(
|
| 1435 |
+
f" No subsets, created segment {segment_key} with material {material_path}"
|
| 1436 |
+
)
|
| 1437 |
+
|
| 1438 |
+
# Final check - make sure we have segments
|
| 1439 |
+
if not result["segments"]:
|
| 1440 |
+
logging.warning(f"No material segments found in {usd_file_path}")
|
| 1441 |
+
|
| 1442 |
+
# Last resort - add all materials as segments
|
| 1443 |
+
for material_path, material_info in all_materials.items():
|
| 1444 |
+
material_name = material_info["name"]
|
| 1445 |
+
segment_key = f"material_{material_name}"
|
| 1446 |
+
|
| 1447 |
+
result["segments"][segment_key] = material_info.copy()
|
| 1448 |
+
result["segments"][segment_key]["semantic_usage"] = material_name
|
| 1449 |
+
|
| 1450 |
+
logging.info(
|
| 1451 |
+
f"Added material {material_name} as segment {segment_key} (last resort)"
|
| 1452 |
+
)
|
| 1453 |
+
|
| 1454 |
+
logging.info(
|
| 1455 |
+
f"Extracted {len(result['segments'])} material segments from {usd_file_path}"
|
| 1456 |
+
)
|
| 1457 |
+
return result
|
deps/vomp/dataset_toolkits/material_objects/vlm_annotations/data_subsets/residential.py
ADDED
|
@@ -0,0 +1,582 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
|
| 16 |
+
from dataset_toolkits.material_objects.vlm_annotations.utils.utils import (
|
| 17 |
+
RESIDENTIAL_BASE_DIR,
|
| 18 |
+
)
|
| 19 |
+
from dataset_toolkits.material_objects.vlm_annotations.utils.render import (
|
| 20 |
+
render_sphere_with_texture,
|
| 21 |
+
)
|
| 22 |
+
from dataset_toolkits.material_objects.vlm_annotations.utils.vlm import (
|
| 23 |
+
analyze_material_with_vlm,
|
| 24 |
+
parse_vlm_properties,
|
| 25 |
+
)
|
| 26 |
+
from dataset_toolkits.material_objects.vlm_annotations.data_subsets.common import (
|
| 27 |
+
extract_materials_from_usd,
|
| 28 |
+
)
|
| 29 |
+
import re
|
| 30 |
+
from tqdm import tqdm
|
| 31 |
+
import os
|
| 32 |
+
import logging
|
| 33 |
+
import copy
|
| 34 |
+
|
| 35 |
+
PROMPTS = {
|
| 36 |
+
"few_shot_examples": (
|
| 37 |
+
"""
|
| 38 |
+
Example 1:
|
| 39 |
+
Material: metal
|
| 40 |
+
Usage: structural component
|
| 41 |
+
Object name: SteelBeam
|
| 42 |
+
|
| 43 |
+
Analysis:
|
| 44 |
+
Step 1: Based on the images, this appears to be a standard structural steel with a matte gray finish.
|
| 45 |
+
Step 2: The surface has medium roughness with some subtle texture visible in the reflection pattern.
|
| 46 |
+
Step 3: Considering its usage as a structural component, this is likely a carbon steel alloy.
|
| 47 |
+
Step 4: Comparing with reference materials, standard structural steel typically has:
|
| 48 |
+
- High stiffness (Young's modulus ~200 GPa)
|
| 49 |
+
- Medium Poisson's ratio typical of metals
|
| 50 |
+
- High density consistent with iron-based alloys
|
| 51 |
+
|
| 52 |
+
Young's modulus: 2.0e11 Pa
|
| 53 |
+
Poisson's ratio: 0.29
|
| 54 |
+
Density: 7800 kg/m^3
|
| 55 |
+
|
| 56 |
+
Example 2:
|
| 57 |
+
Material: plastic
|
| 58 |
+
Usage: household container
|
| 59 |
+
Object name: PlasticContainer
|
| 60 |
+
|
| 61 |
+
Analysis:
|
| 62 |
+
Step 1: The material shows the characteristic smooth, uniform appearance of a consumer plastic.
|
| 63 |
+
Step 2: It has moderate gloss with some translucency and a slight texture.
|
| 64 |
+
Step 3: Given its household container application, this is likely polypropylene.
|
| 65 |
+
Step 4: The visual and contextual evidence suggests:
|
| 66 |
+
- Medium-low stiffness typical of polyolefin plastics
|
| 67 |
+
- Higher Poisson's ratio indicating good lateral deformation
|
| 68 |
+
- Low-medium density typical of consumer thermoplastics
|
| 69 |
+
|
| 70 |
+
Young's modulus: 1.3e9 Pa
|
| 71 |
+
Poisson's ratio: 0.42
|
| 72 |
+
Density: 950 kg/m^3
|
| 73 |
+
|
| 74 |
+
Example 3:
|
| 75 |
+
Material: fabric
|
| 76 |
+
Usage: furniture covering
|
| 77 |
+
Object name: FabricCouch
|
| 78 |
+
|
| 79 |
+
Analysis:
|
| 80 |
+
Step 1: The material shows a woven textile structure with visible fibers.
|
| 81 |
+
Step 2: The surface has significant texture with a matte appearance and no specular highlights.
|
| 82 |
+
Step 3: As furniture upholstery, this is likely a synthetic or natural fiber blend.
|
| 83 |
+
Step 4: Based on the visual characteristics and usage:
|
| 84 |
+
- Low stiffness as expected for flexible textiles
|
| 85 |
+
- Medium-high Poisson's ratio from the woven structure
|
| 86 |
+
- Low density typical of fibrous materials
|
| 87 |
+
|
| 88 |
+
Young's modulus: 1.2e8 Pa
|
| 89 |
+
Poisson's ratio: 0.38
|
| 90 |
+
Density: 300 kg/m^3
|
| 91 |
+
|
| 92 |
+
Example 4:
|
| 93 |
+
Material: organic
|
| 94 |
+
Usage: decorative element
|
| 95 |
+
Object name: DriedLeaf
|
| 96 |
+
|
| 97 |
+
Analysis:
|
| 98 |
+
Step 1: This is an organic material with the characteristic shape and structure of a dried leaf.
|
| 99 |
+
Step 2: The surface shows visible veins, a matte finish, and a brittle, thin structure.
|
| 100 |
+
Step 3: As a dried leaf, it's a natural cellulose-based composite material.
|
| 101 |
+
Step 4: Considering similar organic materials like paper and dried plant fibers:
|
| 102 |
+
- Low-medium stiffness in the fiber direction
|
| 103 |
+
- Medium Poisson's ratio reflecting the fibrous structure
|
| 104 |
+
- Low density typical of dried plant matter
|
| 105 |
+
|
| 106 |
+
Young's modulus: 2.5e9 Pa
|
| 107 |
+
Poisson's ratio: 0.30
|
| 108 |
+
Density: 400 kg/m^3
|
| 109 |
+
"""
|
| 110 |
+
),
|
| 111 |
+
"query_prompt": (
|
| 112 |
+
"""
|
| 113 |
+
Based on the provided images and context information, analyze the material properties.
|
| 114 |
+
Note: The material segment might be internal to the object and not visible from the outside.
|
| 115 |
+
|
| 116 |
+
Respond using EXACTLY the following format (do not deviate from this structure):
|
| 117 |
+
|
| 118 |
+
Analysis:
|
| 119 |
+
Step 1: Identify the material class/type based on visual appearance
|
| 120 |
+
Step 2: Describe the surface characteristics (texture, reflectivity, color)
|
| 121 |
+
Step 3: Determine the specific material subtype considering its usage
|
| 122 |
+
Step 4: Reason through each property estimate based on visual and contextual clues
|
| 123 |
+
|
| 124 |
+
Young's modulus: <value in scientific notation> Pa
|
| 125 |
+
Poisson's ratio: <single decimal value between 0.0 and 0.5>
|
| 126 |
+
Density: <value in scientific notation> kg/m^3
|
| 127 |
+
|
| 128 |
+
Critical Instructions:
|
| 129 |
+
1. You MUST provide numerical estimates for ALL materials, including organic or unusual materials
|
| 130 |
+
2. For natural materials like leaves, wood, or paper, provide estimates based on similar materials with known properties
|
| 131 |
+
3. Never use "N/A", "unknown", or any non-numeric responses for the material properties
|
| 132 |
+
4. For Poisson's ratio, provide a simple decimal number (like 0.3 or 0.42)
|
| 133 |
+
5. Each property should be on its own line with exact formatting shown above
|
| 134 |
+
"""
|
| 135 |
+
),
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
def make_user_prompt(
|
| 140 |
+
material_type, semantic_usage, object_name, has_texture_sphere=True
|
| 141 |
+
):
|
| 142 |
+
intro_text = (
|
| 143 |
+
"""
|
| 144 |
+
You are a materials science expert analyzing two images:
|
| 145 |
+
1. A photo of the full object (showing how the material appears in context).
|
| 146 |
+
2. A sphere with the material's texture (showing color/roughness/reflectivity in isolation).
|
| 147 |
+
|
| 148 |
+
Using both images and the information below, identify the real-world material and estimate its mechanical properties.
|
| 149 |
+
"""
|
| 150 |
+
if has_texture_sphere
|
| 151 |
+
else """
|
| 152 |
+
You are a materials science expert analyzing an image of the full object (showing how the material appears in context).
|
| 153 |
+
|
| 154 |
+
Using this image and the information below, identify the real-world material and estimate its mechanical properties.
|
| 155 |
+
"""
|
| 156 |
+
)
|
| 157 |
+
|
| 158 |
+
return f"""{intro_text}
|
| 159 |
+
Material context:
|
| 160 |
+
* Material type: {material_type}
|
| 161 |
+
* Usage: {semantic_usage}
|
| 162 |
+
* Object name: {object_name}
|
| 163 |
+
|
| 164 |
+
Your task is to provide three specific properties:
|
| 165 |
+
1. Young's modulus (in Pa using scientific notation)
|
| 166 |
+
2. Poisson's ratio (a value between 0.0 and 0.5)
|
| 167 |
+
3. Density (in kg/m^3 using scientific notation)
|
| 168 |
+
"""
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
# Use the centralized parser function from vlm.py instead
|
| 172 |
+
parse_vlm_output = parse_vlm_properties
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
def list_residential_objects():
|
| 176 |
+
"""
|
| 177 |
+
List all available residential objects in the residential directory.
|
| 178 |
+
"""
|
| 179 |
+
usd_files = []
|
| 180 |
+
print("\nAvailable residential objects:")
|
| 181 |
+
for root, _, files in os.walk(RESIDENTIAL_BASE_DIR):
|
| 182 |
+
for file in files:
|
| 183 |
+
if file.endswith(".usd") and not os.path.basename(root).startswith("."):
|
| 184 |
+
usd_files.append(os.path.join(root, file))
|
| 185 |
+
print(f" - {os.path.basename(root)}/{file}")
|
| 186 |
+
print()
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
def process_residential(
|
| 190 |
+
vlm_model,
|
| 191 |
+
vlm_processor,
|
| 192 |
+
limit=None,
|
| 193 |
+
processed_objects=None,
|
| 194 |
+
output_file=None,
|
| 195 |
+
existing_results=None,
|
| 196 |
+
):
|
| 197 |
+
usd_files = []
|
| 198 |
+
for root, _, files in os.walk(RESIDENTIAL_BASE_DIR):
|
| 199 |
+
for file in files:
|
| 200 |
+
if file.endswith(".usd") and not os.path.basename(root).startswith("."):
|
| 201 |
+
usd_files.append(os.path.join(root, file))
|
| 202 |
+
|
| 203 |
+
logging.info(f"Found {len(usd_files)} USD files in residential dataset")
|
| 204 |
+
|
| 205 |
+
# Initialize tracking sets and results
|
| 206 |
+
processed_objects = set() if processed_objects is None else processed_objects
|
| 207 |
+
existing_results = [] if existing_results is None else existing_results
|
| 208 |
+
|
| 209 |
+
# Build a set of already processed object names from existing_results
|
| 210 |
+
existing_object_names = {
|
| 211 |
+
result.get("object_name")
|
| 212 |
+
for result in existing_results
|
| 213 |
+
if "object_name" in result
|
| 214 |
+
}
|
| 215 |
+
logging.info(
|
| 216 |
+
f"Found {len(existing_object_names)} already processed objects in existing results"
|
| 217 |
+
)
|
| 218 |
+
|
| 219 |
+
# Add names from existing_results to processed_objects to avoid reprocessing
|
| 220 |
+
processed_objects.update(existing_object_names)
|
| 221 |
+
|
| 222 |
+
# Create a copy of existing_results to avoid modifying the original
|
| 223 |
+
all_results = copy.deepcopy(existing_results)
|
| 224 |
+
|
| 225 |
+
usd_files.sort()
|
| 226 |
+
|
| 227 |
+
if limit and limit > 0:
|
| 228 |
+
usd_files = usd_files[:limit]
|
| 229 |
+
|
| 230 |
+
success_count = 0
|
| 231 |
+
failed_objects = []
|
| 232 |
+
total_segments = 0
|
| 233 |
+
unique_materials = set()
|
| 234 |
+
materials_per_object = {}
|
| 235 |
+
total_rendered_segments = 0
|
| 236 |
+
total_vlm_segments = 0
|
| 237 |
+
|
| 238 |
+
# Count total segments from existing results
|
| 239 |
+
for result in existing_results:
|
| 240 |
+
total_segments += len(result.get("segments", {}))
|
| 241 |
+
|
| 242 |
+
# Statistics for texture availability
|
| 243 |
+
segments_with_texture = 0
|
| 244 |
+
segments_without_texture = 0
|
| 245 |
+
segments_with_thumbnail_only = 0
|
| 246 |
+
|
| 247 |
+
# Track processed files to avoid duplicates from the same directory
|
| 248 |
+
processed_files = set()
|
| 249 |
+
|
| 250 |
+
for usd_file in tqdm(usd_files, desc=f"Processing residential dataset"):
|
| 251 |
+
# Extract object name from path
|
| 252 |
+
object_name = os.path.basename(os.path.dirname(usd_file))
|
| 253 |
+
|
| 254 |
+
# Skip if we already processed this exact file
|
| 255 |
+
if usd_file in processed_files:
|
| 256 |
+
continue
|
| 257 |
+
|
| 258 |
+
# Skip objects that have already been processed
|
| 259 |
+
if object_name in processed_objects:
|
| 260 |
+
logging.info(f"Skipping already processed object: {object_name}")
|
| 261 |
+
continue
|
| 262 |
+
|
| 263 |
+
try:
|
| 264 |
+
directory = os.path.dirname(usd_file)
|
| 265 |
+
|
| 266 |
+
# Extract material information
|
| 267 |
+
result = extract_materials_from_usd(usd_file, "residential")
|
| 268 |
+
|
| 269 |
+
if result:
|
| 270 |
+
# Add to processed_files to avoid duplicates
|
| 271 |
+
processed_files.add(usd_file)
|
| 272 |
+
|
| 273 |
+
# Track statistics
|
| 274 |
+
segments = result.get("segments", {})
|
| 275 |
+
total_segments += len(segments)
|
| 276 |
+
|
| 277 |
+
# Remove object_name and note fields from segments
|
| 278 |
+
for segment_key, segment_info in segments.items():
|
| 279 |
+
if "object_name" in segment_info:
|
| 280 |
+
del segment_info["object_name"]
|
| 281 |
+
if "note" in segment_info:
|
| 282 |
+
del segment_info["note"]
|
| 283 |
+
|
| 284 |
+
# Count unique materials for this object
|
| 285 |
+
object_materials = set()
|
| 286 |
+
for segment_name, segment_info in segments.items():
|
| 287 |
+
material_name = segment_info.get("material_type", "unknown")
|
| 288 |
+
unique_materials.add(material_name)
|
| 289 |
+
object_materials.add(material_name)
|
| 290 |
+
|
| 291 |
+
# Record materials per object
|
| 292 |
+
if len(segments) > 0:
|
| 293 |
+
materials_per_object[object_name] = len(object_materials)
|
| 294 |
+
|
| 295 |
+
# Get thumbnail path if available
|
| 296 |
+
thumb_path = None
|
| 297 |
+
# For residential dataset, thumbnails are in .thumbs/256x256 directory
|
| 298 |
+
thumb_dir = os.path.join(
|
| 299 |
+
os.path.dirname(usd_file), ".thumbs", "256x256"
|
| 300 |
+
)
|
| 301 |
+
|
| 302 |
+
has_thumbnail = False
|
| 303 |
+
if os.path.exists(thumb_dir):
|
| 304 |
+
# Try to find a thumbnail matching the USD filename
|
| 305 |
+
usd_filename = os.path.basename(usd_file)
|
| 306 |
+
thumb_candidates = [
|
| 307 |
+
# Regular thumbnail
|
| 308 |
+
os.path.join(thumb_dir, f"{usd_filename}.png"),
|
| 309 |
+
# Auto-generated thumbnail
|
| 310 |
+
os.path.join(thumb_dir, f"{usd_filename}.auto.png"),
|
| 311 |
+
]
|
| 312 |
+
|
| 313 |
+
for candidate in thumb_candidates:
|
| 314 |
+
if os.path.exists(candidate):
|
| 315 |
+
thumb_path = candidate
|
| 316 |
+
has_thumbnail = True
|
| 317 |
+
logging.info(f"Found thumbnail: {thumb_path}")
|
| 318 |
+
break
|
| 319 |
+
|
| 320 |
+
# Process VLM for all segments if VLM model is provided
|
| 321 |
+
os.makedirs("/tmp/vlm", exist_ok=True)
|
| 322 |
+
|
| 323 |
+
if vlm_model and len(segments) > 0:
|
| 324 |
+
for segment_key, segment_info in segments.items():
|
| 325 |
+
textures = segment_info.get("textures", {})
|
| 326 |
+
|
| 327 |
+
# Log texture information for diagnostics
|
| 328 |
+
logging.info(
|
| 329 |
+
f"Segment {segment_key} has textures: {list(textures.keys())}"
|
| 330 |
+
)
|
| 331 |
+
|
| 332 |
+
# Check if we have either a normal or roughness texture for rendering
|
| 333 |
+
has_texture = (
|
| 334 |
+
"normal" in textures
|
| 335 |
+
or "roughness" in textures
|
| 336 |
+
or "diffuse" in textures
|
| 337 |
+
)
|
| 338 |
+
if has_texture:
|
| 339 |
+
# Has texture - render sphere and use with thumbnail
|
| 340 |
+
segments_with_texture += 1
|
| 341 |
+
logging.info(
|
| 342 |
+
f"Rendering texture sphere for {object_name}, segment {segment_key}"
|
| 343 |
+
)
|
| 344 |
+
|
| 345 |
+
# Set up file path for this segment's rendered sphere
|
| 346 |
+
segment_render_path = f"/tmp/vlm/texture_sphere_{object_name}_{segment_key}.png"
|
| 347 |
+
|
| 348 |
+
# Render the textured sphere
|
| 349 |
+
try:
|
| 350 |
+
rgb_buffer = render_sphere_with_texture(
|
| 351 |
+
textures, segment_render_path
|
| 352 |
+
)
|
| 353 |
+
logging.info(f"RGB buffer shape: {rgb_buffer.shape}")
|
| 354 |
+
except Exception as e:
|
| 355 |
+
logging.error(
|
| 356 |
+
f"Error rendering texture for {segment_key}: {str(e)}"
|
| 357 |
+
)
|
| 358 |
+
segment_render_path = None
|
| 359 |
+
else:
|
| 360 |
+
# No texture - just use thumbnail
|
| 361 |
+
segments_without_texture += 1
|
| 362 |
+
segment_render_path = None
|
| 363 |
+
logging.info(
|
| 364 |
+
f"No texture for {object_name}, segment {segment_key}. Using thumbnail only."
|
| 365 |
+
)
|
| 366 |
+
|
| 367 |
+
# Always try to process with VLM, even if no texture
|
| 368 |
+
try:
|
| 369 |
+
# If we have a thumbnail but no texture, still run VLM with just the thumbnail
|
| 370 |
+
if not has_texture and has_thumbnail:
|
| 371 |
+
segments_with_thumbnail_only += 1
|
| 372 |
+
logging.info(
|
| 373 |
+
f"Using thumbnail only for {object_name}, segment {segment_key}"
|
| 374 |
+
)
|
| 375 |
+
|
| 376 |
+
# Don't run VLM if we have neither texture nor thumbnail
|
| 377 |
+
if not segment_render_path and not has_thumbnail:
|
| 378 |
+
logging.warning(
|
| 379 |
+
f"Skipping VLM for {segment_key} - no texture or thumbnail available"
|
| 380 |
+
)
|
| 381 |
+
continue
|
| 382 |
+
|
| 383 |
+
# Set semantic usage to segment name but don't store in segment data
|
| 384 |
+
semantic_usage = segment_key
|
| 385 |
+
temp_object_name = object_name
|
| 386 |
+
|
| 387 |
+
# Create custom prompt based on texture availability
|
| 388 |
+
custom_prompt = make_user_prompt(
|
| 389 |
+
segment_info["material_type"],
|
| 390 |
+
semantic_usage,
|
| 391 |
+
temp_object_name,
|
| 392 |
+
has_texture_sphere=segment_render_path is not None,
|
| 393 |
+
)
|
| 394 |
+
|
| 395 |
+
# Store the custom prompt in material_info but not object_name
|
| 396 |
+
segment_info["user_prompt"] = custom_prompt
|
| 397 |
+
|
| 398 |
+
# Debug: Log the prompt type based on texture availability
|
| 399 |
+
if segment_render_path is not None:
|
| 400 |
+
logging.info(
|
| 401 |
+
f"Using prompt WITH texture sphere for {object_name}, segment {segment_key}"
|
| 402 |
+
)
|
| 403 |
+
else:
|
| 404 |
+
logging.info(
|
| 405 |
+
f"Using prompt WITHOUT texture sphere for {object_name}, segment {segment_key}"
|
| 406 |
+
)
|
| 407 |
+
logging.info(
|
| 408 |
+
f"PROMPT: {custom_prompt[:100]}..."
|
| 409 |
+
) # Print just the beginning of the prompt
|
| 410 |
+
|
| 411 |
+
# Create a temporary segment_info with object_name for VLM but don't save to result
|
| 412 |
+
temp_segment_info = segment_info.copy()
|
| 413 |
+
temp_segment_info["semantic_usage"] = semantic_usage
|
| 414 |
+
temp_segment_info["object_name"] = temp_object_name
|
| 415 |
+
|
| 416 |
+
vlm_analysis = analyze_material_with_vlm(
|
| 417 |
+
segment_render_path, # This can be None, in which case only thumbnail is used
|
| 418 |
+
temp_segment_info, # Use temporary copy with object_name
|
| 419 |
+
vlm_model,
|
| 420 |
+
vlm_processor,
|
| 421 |
+
thumbnail_path=thumb_path,
|
| 422 |
+
dataset_name="residential",
|
| 423 |
+
PROMPTS=PROMPTS,
|
| 424 |
+
make_user_prompt=make_user_prompt,
|
| 425 |
+
parse_vlm_output=parse_vlm_output,
|
| 426 |
+
)
|
| 427 |
+
|
| 428 |
+
# Add VLM analysis to segment info
|
| 429 |
+
if vlm_analysis and "error" not in vlm_analysis:
|
| 430 |
+
segment_info["vlm_analysis"] = vlm_analysis.get(
|
| 431 |
+
"vlm_analysis"
|
| 432 |
+
)
|
| 433 |
+
|
| 434 |
+
if vlm_analysis.get("youngs_modulus") is not None:
|
| 435 |
+
segment_info["youngs_modulus"] = vlm_analysis.get(
|
| 436 |
+
"youngs_modulus"
|
| 437 |
+
)
|
| 438 |
+
|
| 439 |
+
if vlm_analysis.get("poissons_ratio") is not None:
|
| 440 |
+
segment_info["poissons_ratio"] = vlm_analysis.get(
|
| 441 |
+
"poissons_ratio"
|
| 442 |
+
)
|
| 443 |
+
|
| 444 |
+
if vlm_analysis.get("density") is not None:
|
| 445 |
+
segment_info["density"] = vlm_analysis.get(
|
| 446 |
+
"density"
|
| 447 |
+
)
|
| 448 |
+
|
| 449 |
+
total_vlm_segments += 1
|
| 450 |
+
logging.info(
|
| 451 |
+
f"VLM analysis successful for {segment_key}:"
|
| 452 |
+
)
|
| 453 |
+
logging.info(
|
| 454 |
+
f" Young's modulus: {vlm_analysis.get('youngs_modulus')}"
|
| 455 |
+
)
|
| 456 |
+
logging.info(
|
| 457 |
+
f" Poisson's ratio: {vlm_analysis.get('poissons_ratio')}"
|
| 458 |
+
)
|
| 459 |
+
logging.info(
|
| 460 |
+
f" Density: {vlm_analysis.get('density')}"
|
| 461 |
+
)
|
| 462 |
+
else:
|
| 463 |
+
logging.error(
|
| 464 |
+
f"VLM analysis failed for {segment_key}: {vlm_analysis.get('error', 'Unknown error')}"
|
| 465 |
+
)
|
| 466 |
+
except Exception as e:
|
| 467 |
+
import traceback
|
| 468 |
+
|
| 469 |
+
logging.error(
|
| 470 |
+
f"Error during VLM analysis for {segment_key}: {str(e)}"
|
| 471 |
+
)
|
| 472 |
+
logging.error(traceback.format_exc())
|
| 473 |
+
|
| 474 |
+
total_rendered_segments += 1
|
| 475 |
+
|
| 476 |
+
all_results.append(result) # Add to our local copy of results
|
| 477 |
+
processed_objects.add(object_name) # Mark as processed
|
| 478 |
+
|
| 479 |
+
# Incremental save after each object if output file is provided
|
| 480 |
+
if output_file:
|
| 481 |
+
try:
|
| 482 |
+
with open(output_file, "w") as f:
|
| 483 |
+
import json
|
| 484 |
+
from dataset_toolkits.material_objects.vlm_annotations.data_subsets.common import (
|
| 485 |
+
UsdJsonEncoder,
|
| 486 |
+
)
|
| 487 |
+
|
| 488 |
+
# Debug save contents
|
| 489 |
+
logging.info(
|
| 490 |
+
f"Saving checkpoint with {len(all_results)} objects"
|
| 491 |
+
)
|
| 492 |
+
|
| 493 |
+
# Ensure result types are JSON serializable
|
| 494 |
+
for idx, item in enumerate(all_results):
|
| 495 |
+
if "segments" in item:
|
| 496 |
+
for seg_key, seg_info in item["segments"].items():
|
| 497 |
+
# Remove object_name and note fields if they exist
|
| 498 |
+
if "object_name" in seg_info:
|
| 499 |
+
del seg_info["object_name"]
|
| 500 |
+
if "note" in seg_info:
|
| 501 |
+
del seg_info["note"]
|
| 502 |
+
|
| 503 |
+
if "textures" in seg_info and isinstance(
|
| 504 |
+
seg_info["textures"], dict
|
| 505 |
+
):
|
| 506 |
+
# Convert any non-serializable texture paths to strings
|
| 507 |
+
serializable_textures = {}
|
| 508 |
+
for tex_type, tex_path in seg_info[
|
| 509 |
+
"textures"
|
| 510 |
+
].items():
|
| 511 |
+
serializable_textures[tex_type] = str(
|
| 512 |
+
tex_path
|
| 513 |
+
)
|
| 514 |
+
seg_info["textures"] = serializable_textures
|
| 515 |
+
|
| 516 |
+
# Try to serialize to a string first to check for issues
|
| 517 |
+
try:
|
| 518 |
+
json_str = json.dumps(
|
| 519 |
+
all_results, cls=UsdJsonEncoder, indent=4
|
| 520 |
+
)
|
| 521 |
+
logging.info(
|
| 522 |
+
f"JSON serialization successful, string length: {len(json_str)}"
|
| 523 |
+
)
|
| 524 |
+
|
| 525 |
+
# Now write to file
|
| 526 |
+
f.write(json_str)
|
| 527 |
+
|
| 528 |
+
except Exception as json_err:
|
| 529 |
+
logging.error(
|
| 530 |
+
f"JSON serialization error: {str(json_err)}"
|
| 531 |
+
)
|
| 532 |
+
# Try to identify problematic objects
|
| 533 |
+
for i, item in enumerate(all_results):
|
| 534 |
+
try:
|
| 535 |
+
json.dumps(item, cls=UsdJsonEncoder)
|
| 536 |
+
except Exception as e:
|
| 537 |
+
logging.error(
|
| 538 |
+
f"Error serializing object {i}: {str(e)}"
|
| 539 |
+
)
|
| 540 |
+
raise json_err # Re-raise to be caught by outer exception handler
|
| 541 |
+
|
| 542 |
+
except Exception as e:
|
| 543 |
+
logging.error(f"Error saving checkpoint: {str(e)}")
|
| 544 |
+
import traceback
|
| 545 |
+
|
| 546 |
+
logging.error(traceback.format_exc())
|
| 547 |
+
|
| 548 |
+
success_count += 1
|
| 549 |
+
else:
|
| 550 |
+
logging.warning(f"No material information extracted for {usd_file}")
|
| 551 |
+
failed_objects.append(object_name)
|
| 552 |
+
except Exception as e:
|
| 553 |
+
import traceback
|
| 554 |
+
|
| 555 |
+
logging.error(f"Error processing {usd_file}: {str(e)}")
|
| 556 |
+
logging.error(traceback.format_exc())
|
| 557 |
+
failed_objects.append(os.path.basename(os.path.dirname(usd_file)))
|
| 558 |
+
|
| 559 |
+
# Convert materials_per_object to list format for consistency with simready
|
| 560 |
+
materials_per_object_list = []
|
| 561 |
+
for obj_name, count in materials_per_object.items():
|
| 562 |
+
materials_per_object_list.append(obj_name)
|
| 563 |
+
|
| 564 |
+
# Log texture statistics
|
| 565 |
+
logging.info("Texture Statistics:")
|
| 566 |
+
logging.info(f" Total segments processed: {total_segments}")
|
| 567 |
+
logging.info(f" Segments with textures: {segments_with_texture}")
|
| 568 |
+
logging.info(f" Segments without textures: {segments_without_texture}")
|
| 569 |
+
logging.info(f" Segments with thumbnail only: {segments_with_thumbnail_only}")
|
| 570 |
+
logging.info(f" Total VLM analyses completed: {total_vlm_segments}")
|
| 571 |
+
|
| 572 |
+
return (
|
| 573 |
+
all_results,
|
| 574 |
+
len(usd_files),
|
| 575 |
+
success_count,
|
| 576 |
+
failed_objects,
|
| 577 |
+
total_segments,
|
| 578 |
+
total_rendered_segments,
|
| 579 |
+
total_vlm_segments,
|
| 580 |
+
list(unique_materials),
|
| 581 |
+
materials_per_object_list,
|
| 582 |
+
)
|