rishitdagli commited on
Commit
42f0ff9
·
0 Parent(s):

Vendored VoMP with local debug edits; single commit (history rewritten to drop binary blobs for HF Hub).

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +37 -0
  2. .gitignore +2 -0
  3. README.md +15 -0
  4. app.py +561 -0
  5. deps/vomp/.gitignore +216 -0
  6. deps/vomp/ATTRIBUTIONS.md +0 -0
  7. deps/vomp/CONTRIBUTING.md +51 -0
  8. deps/vomp/LICENSE +201 -0
  9. deps/vomp/README.md +665 -0
  10. deps/vomp/README_train.md +285 -0
  11. deps/vomp/configs/materials/geometry_encoder/train.json +83 -0
  12. deps/vomp/configs/materials/geometry_encoder/train_encoder_decoder_direct.json +99 -0
  13. deps/vomp/configs/materials/geometry_encoder/train_encoder_decoder_matvae.json +99 -0
  14. deps/vomp/configs/materials/geometry_encoder/train_standard.json +83 -0
  15. deps/vomp/configs/materials/inference.json +86 -0
  16. deps/vomp/configs/materials/material_vae/beta_tc_final.json +68 -0
  17. deps/vomp/configs/materials/material_vae/matvae.json +76 -0
  18. deps/vomp/configs/materials/material_vae/matvae_log_minmax_no_density.json +76 -0
  19. deps/vomp/configs/materials/material_vae/matvae_no_beta_tc.json +77 -0
  20. deps/vomp/configs/materials/material_vae/matvae_no_flow.json +77 -0
  21. deps/vomp/configs/materials/material_vae/matvae_no_free_nats.json +76 -0
  22. deps/vomp/configs/materials/material_vae/matvae_standard.json +76 -0
  23. deps/vomp/configs/materials/material_vae/matvae_standard_norm.json +76 -0
  24. deps/vomp/configs/materials/material_vae/standard_vae_final.json +67 -0
  25. deps/vomp/configs/sim/armchair_and_orange.json +59 -0
  26. deps/vomp/configs/sim/falling_armchair.json +48 -0
  27. deps/vomp/configs/sim/falling_bar_stool.json +50 -0
  28. deps/vomp/configs/sim/falling_birch.json +50 -0
  29. deps/vomp/configs/sim/falling_oranges.json +80 -0
  30. deps/vomp/configs/sim/falling_sphere_soft.json +51 -0
  31. deps/vomp/configs/sim/zag_and_falling_orange.json +59 -0
  32. deps/vomp/configs/sim/zag_and_falling_oranges.json +98 -0
  33. deps/vomp/dataset_toolkits/abo/ABO500.py +204 -0
  34. deps/vomp/dataset_toolkits/abo/build_metadata.py +108 -0
  35. deps/vomp/dataset_toolkits/abo/extract_feature.py +381 -0
  36. deps/vomp/dataset_toolkits/abo/render.py +241 -0
  37. deps/vomp/dataset_toolkits/abo/voxelize.py +306 -0
  38. deps/vomp/dataset_toolkits/blender_script/render.py +695 -0
  39. deps/vomp/dataset_toolkits/build_metadata.py +551 -0
  40. deps/vomp/dataset_toolkits/datasets/ABO.py +132 -0
  41. deps/vomp/dataset_toolkits/datasets/__init__.py +16 -0
  42. deps/vomp/dataset_toolkits/datasets/allmats.py +510 -0
  43. deps/vomp/dataset_toolkits/datasets/simready.py +297 -0
  44. deps/vomp/dataset_toolkits/extract_feature.py +273 -0
  45. deps/vomp/dataset_toolkits/latent_space/analyze_data_distribution.py +111 -0
  46. deps/vomp/dataset_toolkits/latent_space/make_csv.py +411 -0
  47. deps/vomp/dataset_toolkits/material_objects/render_usd.py +1176 -0
  48. deps/vomp/dataset_toolkits/material_objects/vlm_annotations/data_subsets/commercial.py +427 -0
  49. deps/vomp/dataset_toolkits/material_objects/vlm_annotations/data_subsets/common.py +1457 -0
  50. 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
+ ![](./images/teaser.png)
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
+ ![Colorbar](images/youngs_modulus_colorbar_legend.png)
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
+ ![](./images/teaser.png)
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
+ ![](./images/method.png)
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
+ ![](./images/datacreation.png)
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
+ )