diff --git a/experiments/Convert_VTK_To_USD/convert_chop_alterra_valve_to_usd.ipynb b/experiments/Convert_VTK_To_USD/convert_chop_alterra_valve_to_usd.ipynb index 45d83c2..b31e91c 100644 --- a/experiments/Convert_VTK_To_USD/convert_chop_alterra_valve_to_usd.ipynb +++ b/experiments/Convert_VTK_To_USD/convert_chop_alterra_valve_to_usd.ipynb @@ -25,19 +25,6 @@ "5. Create multiple variations (full resolution, subsampled, etc.)" ] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from pathlib import Path\n", - "import re\n", - "import time as time_module\n", - "\n", - "import shutil" - ] - }, { "cell_type": "markdown", "metadata": {}, @@ -53,31 +40,13 @@ "metadata": {}, "outputs": [], "source": [ - "# Configuration: Control which conversions to run\n", - "# Set to True to compute full time series (all frames) - takes longer\n", - "# Set to False to only compute subsampled time series (faster, for preview)\n", - "COMPUTE_FULL_TIME_SERIES = True # Default: only subsampled\n", - "\n", - "print(\"Time Series Configuration:\")\n", - "print(f\" - Compute Full Time Series: {COMPUTE_FULL_TIME_SERIES}\")\n", - "print(\" - Compute Subsampled Time Series: Always enabled\")\n", - "print()\n", - "if not COMPUTE_FULL_TIME_SERIES:\n", - " print(\"⚠️ Full time series conversion is DISABLED for faster execution.\")\n", - " print(\" Set COMPUTE_FULL_TIME_SERIES = True to enable full conversion.\")\n", - "else:\n", - " print(\"✓ Full time series conversion is ENABLED (this will take longer).\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import logging\n", + "from pathlib import Path\n", + "import re\n", + "import time as time_module\n", + "\n", "import numpy as np\n", "\n", + "\n", "# Import the vtk_to_usd library\n", "from physiomotion4d.vtk_to_usd import (\n", " VTKToUSDConverter,\n", @@ -92,8 +61,8 @@ "from physiomotion4d.usd_tools import USDTools\n", "from physiomotion4d.usd_anatomy_tools import USDAnatomyTools\n", "\n", - "# Configure logging\n", - "logging.basicConfig(level=logging.INFO, format=\"%(levelname)s: %(message)s\")" + "# Use as a test\n", + "from physiomotion4d.notebook_utils import running_as_test" ] }, { @@ -109,16 +78,45 @@ "metadata": {}, "outputs": [], "source": [ + "# Set to True to use as a test. Automatically done by\n", + "# running_as_test() helper function.\n", + "quick_run = running_as_test()\n", + "quick_run_step = 4\n", + "\n", "# Define data directories (Alterra only)\n", "data_dir = Path.cwd().parent.parent / \"data\" / \"CHOP-Valve4D\"\n", - "Alterra_dir = data_dir / \"Alterra\"\n", - "output_dir = Path.cwd() / \"output\" / \"valve4d-alterra\"\n", - "output_dir.mkdir(parents=True, exist_ok=True)\n", + "alterra_dir = data_dir / \"Alterra\"\n", + "\n", + "output_dir = Path.cwd() / \"results\" / \"valve4d-alterra\"\n", + "if quick_run:\n", + " output_usd = output_dir / \"alterra_quick.usd\"\n", + "else:\n", + " output_usd = output_dir / \"alterra_full.usd\"\n", "\n", - "print(f\"Data directory: {data_dir}\")\n", - "print(f\"Output directory: {output_dir}\")\n", - "print(\"\\nDirectory status:\")\n", - "print(f\" Alterra: {'✓' if Alterra_dir.exists() else '✗'} {Alterra_dir}\")" + "colormap_primvar_substrs = [\"stress\", \"strain\"]\n", + "colormap_name = \"jet\" # matplotlib colormap name\n", + "colormap_range_min = 25\n", + "colormap_range_max = 200\n", + "\n", + "conversion_settings = ConversionSettings(\n", + " triangulate_meshes=True,\n", + " compute_normals=False, # Use existing normals if available\n", + " preserve_point_arrays=True,\n", + " preserve_cell_arrays=True,\n", + " separate_objects_by_cell_type=False,\n", + " separate_objects_by_connectivity=True, # Essential for alterra vtk file\n", + " up_axis=\"Y\",\n", + " times_per_second=60.0, # 60 FPS for smooth animation\n", + " use_time_samples=True,\n", + ")\n", + "\n", + "stent_material = MaterialData(\n", + " name=\"Alterra_valve\",\n", + " diffuse_color=(0.5, 0.5, 0.5),\n", + " roughness=0.4,\n", + " metallic=0.9,\n", + " use_vertex_colors=False,\n", + ")" ] }, { @@ -127,44 +125,21 @@ "metadata": {}, "outputs": [], "source": [ - "def discover_time_series(directory, pattern=r\"\\.t(\\d+)\\.vtk$\"):\n", - " \"\"\"Discover and sort time-series VTK files.\n", - "\n", - " Args:\n", - " directory: Directory containing VTK files\n", - " pattern: Regex pattern to extract time step number\n", - "\n", - " Returns:\n", - " list: Sorted list of (time_step, file_path) tuples\n", - " \"\"\"\n", - " vtk_files = list(Path(directory).glob(\"*.vtk\"))\n", - "\n", - " # Extract time step numbers and pair with files\n", - " time_series = []\n", - " for vtk_file in vtk_files:\n", - " match = re.search(pattern, vtk_file.name)\n", - " if match:\n", - " time_step = int(match.group(1))\n", - " time_series.append((time_step, vtk_file))\n", - "\n", - " # Sort by time step\n", - " time_series.sort(key=lambda x: x[0])\n", - "\n", - " return time_series\n", - "\n", - "\n", - "# Discover Alterra time series\n", - "Alterra_series = discover_time_series(Alterra_dir)\n", - "\n", - "print(\"=\" * 60)\n", - "print(\"Time-Series Discovery (Alterra)\")\n", - "print(\"=\" * 60)\n", - "print(\"\\nAlterra:\")\n", - "print(f\" Files found: {len(Alterra_series)}\")\n", - "if Alterra_series:\n", - " print(f\" Time range: t{Alterra_series[0][0]} to t{Alterra_series[-1][0]}\")\n", - " print(f\" First file: {Alterra_series[0][1].name}\")\n", - " print(f\" Last file: {Alterra_series[-1][1].name}\")" + "output_dir.mkdir(parents=True, exist_ok=True)\n", + "\n", + "vtk_files = list(Path(alterra_dir).glob(\"*.vtk\"))\n", + "pattern = r\"\\.t(\\d+)\\.vtk$\"\n", + "\n", + "# Extract time step numbers and pair with files\n", + "alterra_series = []\n", + "for vtk_file in vtk_files:\n", + " match = re.search(pattern, vtk_file.name)\n", + " if match:\n", + " time_step = int(match.group(1))\n", + " alterra_series.append((time_step, vtk_file))\n", + "\n", + "# Sort by time step\n", + "alterra_series.sort(key=lambda x: x[0])" ] }, { @@ -182,106 +157,49 @@ "metadata": {}, "outputs": [], "source": [ - "# Read first frame of Alterra\n", - "if Alterra_series:\n", - " print(\"=\" * 60)\n", - " print(\"Alterra - First Frame Analysis\")\n", - " print(\"=\" * 60)\n", - "\n", - " first_file = Alterra_series[0][1]\n", - " mesh_data = read_vtk_file(first_file, extract_surface=True)\n", - "\n", - " print(f\"\\nFile: {first_file.name}\")\n", - " print(\"\\nGeometry:\")\n", - " print(f\" Points: {len(mesh_data.points):,}\")\n", - " print(f\" Faces: {len(mesh_data.face_vertex_counts):,}\")\n", - " print(f\" Normals: {'Yes' if mesh_data.normals is not None else 'No'}\")\n", - " print(f\" Colors: {'Yes' if mesh_data.colors is not None else 'No'}\")\n", - "\n", - " # Bounding box\n", - " bbox_min = np.min(mesh_data.points, axis=0)\n", - " bbox_max = np.max(mesh_data.points, axis=0)\n", - " bbox_size = bbox_max - bbox_min\n", - " print(\"\\nBounding Box:\")\n", - " print(f\" Min: [{bbox_min[0]:.3f}, {bbox_min[1]:.3f}, {bbox_min[2]:.3f}]\")\n", - " print(f\" Max: [{bbox_max[0]:.3f}, {bbox_max[1]:.3f}, {bbox_max[2]:.3f}]\")\n", - " print(f\" Size: [{bbox_size[0]:.3f}, {bbox_size[1]:.3f}, {bbox_size[2]:.3f}]\")\n", - "\n", - " print(f\"\\nData Arrays ({len(mesh_data.generic_arrays)}):\")\n", - " for i, array in enumerate(mesh_data.generic_arrays, 1):\n", - " print(f\" {i}. {array.name}:\")\n", - " print(f\" - Type: {array.data_type.value}\")\n", - " print(f\" - Components: {array.num_components}\")\n", - " print(f\" - Interpolation: {array.interpolation}\")\n", - " print(f\" - Elements: {len(array.data):,}\")\n", - " if array.data.size > 0:\n", - " print(f\" - Range: [{np.min(array.data):.6f}, {np.max(array.data):.6f}]\")\n", - "\n", - " # Cell types (face vertex count) - TPV data has multiple cell types (triangle, quad, etc.)\n", - " unique_counts, num_each = np.unique(\n", - " mesh_data.face_vertex_counts, return_counts=True\n", - " )\n", - " print(\"\\nCell types (faces by vertex count):\")\n", - " for u, n in zip(unique_counts, num_each):\n", - " name = cell_type_name_for_vertex_count(int(u))\n", - " print(f\" {name} ({u} vertices): {n:,} faces\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Note: Helper functions removed - now using USDTools for primvar inspection and colorization\n", - "# The workflow has changed to: convert to USD first, then apply colormap post-processing\n", - "\n", - "# Configuration: choose colormap for visualization\n", - "DEFAULT_COLORMAP = \"viridis\" # matplotlib colormap name\n", - "\n", - "# Enable automatic colorization (will pick strain/stress primvars if available)\n", - "ENABLE_AUTO_COLORIZATION = True\n", - "\n", - "print(\"Colorization will be applied after USD conversion using USDTools methods\")\n", - "print(\" - USDTools.list_mesh_primvars() for inspection\")\n", - "print(\" - USDTools.pick_color_primvar() for selection\")\n", - "print(\" - USDTools.apply_colormap_from_primvar() for coloring\")\n", - "print(f\" - Colormap: {DEFAULT_COLORMAP}\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "## 2. Configure Conversion Settings\n", - "\n", - "# Create converter settings\n", - "settings = ConversionSettings(\n", - " triangulate_meshes=True,\n", - " compute_normals=False, # Use existing normals if available\n", - " preserve_point_arrays=True,\n", - " preserve_cell_arrays=True,\n", - " separate_objects_by_cell_type=False,\n", - " separate_objects_by_connectivity=True,\n", - " up_axis=\"Y\",\n", - " times_per_second=60.0, # 60 FPS for smooth animation\n", - " use_time_samples=True,\n", - ")\n", - "\n", - "print(\"Conversion settings configured\")\n", - "print(f\" - Triangulate: {settings.triangulate_meshes}\")\n", - "print(f\" - Separate objects by cell type: {settings.separate_objects_by_cell_type}\")\n", - "print(f\" - FPS: {settings.times_per_second}\")\n", - "print(f\" - Up axis: {settings.up_axis}\")" + "# Debuggin\n", + "first_file = alterra_series[0][1]\n", + "mesh_data = read_vtk_file(first_file, extract_surface=True)\n", + "\n", + "print(f\"\\nFile: {first_file.name}\")\n", + "print(\"\\nGeometry:\")\n", + "print(f\" Points: {len(mesh_data.points):,}\")\n", + "print(f\" Faces: {len(mesh_data.face_vertex_counts):,}\")\n", + "print(f\" Normals: {'Yes' if mesh_data.normals is not None else 'No'}\")\n", + "print(f\" Colors: {'Yes' if mesh_data.colors is not None else 'No'}\")\n", + "\n", + "# Bounding box\n", + "bbox_min = np.min(mesh_data.points, axis=0)\n", + "bbox_max = np.max(mesh_data.points, axis=0)\n", + "bbox_size = bbox_max - bbox_min\n", + "print(\"\\nBounding Box:\")\n", + "print(f\" Min: [{bbox_min[0]:.3f}, {bbox_min[1]:.3f}, {bbox_min[2]:.3f}]\")\n", + "print(f\" Max: [{bbox_max[0]:.3f}, {bbox_max[1]:.3f}, {bbox_max[2]:.3f}]\")\n", + "print(f\" Size: [{bbox_size[0]:.3f}, {bbox_size[1]:.3f}, {bbox_size[2]:.3f}]\")\n", + "\n", + "print(f\"\\nData Arrays ({len(mesh_data.generic_arrays)}):\")\n", + "for i, array in enumerate(mesh_data.generic_arrays, 1):\n", + " print(f\" {i}. {array.name}:\")\n", + " print(f\" - Type: {array.data_type.value}\")\n", + " print(f\" - Components: {array.num_components}\")\n", + " print(f\" - Interpolation: {array.interpolation}\")\n", + " print(f\" - Elements: {len(array.data):,}\")\n", + " if array.data.size > 0:\n", + " print(f\" - Range: [{np.min(array.data):.6f}, {np.max(array.data):.6f}]\")\n", + "\n", + "# Cell types (face vertex count = triangle, quad, etc.)\n", + "unique_counts, num_each = np.unique(mesh_data.face_vertex_counts, return_counts=True)\n", + "print(\"\\nCell types (faces by vertex count):\")\n", + "for u, n in zip(unique_counts, num_each):\n", + " name = cell_type_name_for_vertex_count(int(u))\n", + " print(f\" {name} ({u} vertices): {n:,} faces\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## 3. Convert Full Time Series - TPV25" + "## 3. Convert TPV25" ] }, { @@ -290,62 +208,45 @@ "metadata": {}, "outputs": [], "source": [ - "# Create material for Alterra\n", - "# Note: Vertex colors will be applied post-conversion by USDTools\n", - "Alterra_material = MaterialData(\n", - " name=\"Alterra_valve\",\n", - " diffuse_color=(0.85, 0.4, 0.4),\n", - " roughness=0.4,\n", - " metallic=0.0,\n", - " use_vertex_colors=False, # USDTools will bind vertex color material during colorization\n", - ")\n", - "\n", - "print(\"=\" * 60)\n", - "print(\"Converting Alterra Time Series\")\n", - "print(\"=\" * 60)\n", - "print(f\"Dataset: {len(Alterra_series)} frames\")\n", - "\n", - "# Convert Alterra (full resolution)\n", - "if COMPUTE_FULL_TIME_SERIES and Alterra_series:\n", - " converter = VTKToUSDConverter(settings)\n", + "converter = VTKToUSDConverter(conversion_settings)\n", "\n", - " Alterra_files = [file_path for _, file_path in Alterra_series]\n", - " Alterra_times = [float(time_step) for time_step, _ in Alterra_series]\n", + "alterra_files = [file_path for _, file_path in alterra_series]\n", + "alterra_times = [float(time_step) for time_step, _ in alterra_series]\n", "\n", - " output_usd = output_dir / \"Alterra_full.usd\"\n", + "if quick_run:\n", + " alterra_files = alterra_files[::quick_run_step]\n", + " alterra_times = alterra_times[::quick_run_step]\n", "\n", - " print(f\"\\nConverting to: {output_usd}\")\n", - " print(f\"Time codes: {Alterra_times[0]:.1f} to {Alterra_times[-1]:.1f}\")\n", - " print(\"\\nThis may take several minutes...\\n\")\n", + "print(f\"\\nConverting to: {output_usd}\")\n", + "print(f\"Number of time steps: {len(alterra_times)}\")\n", + "print(\"\\nThis may take several minutes...\\n\")\n", "\n", - " start_time = time_module.time()\n", + "start_time = time_module.time()\n", "\n", - " # Read MeshData\n", - " mesh_data_sequence = [read_vtk_file(f, extract_surface=True) for f in Alterra_files]\n", + "# Read MeshData\n", + "mesh_data_sequence = [read_vtk_file(f, extract_surface=True) for f in alterra_files]\n", "\n", - " # Validate topology consistency across time series\n", - " validation_report = validate_time_series_topology(\n", - " mesh_data_sequence, filenames=Alterra_files\n", + "# Validate topology consistency across time series\n", + "validation_report = validate_time_series_topology(\n", + " mesh_data_sequence, filenames=alterra_files\n", + ")\n", + "if not validation_report[\"is_consistent\"]:\n", + " print(\n", + " f\"Warning: Found {len(validation_report['warnings'])} topology/primvar issues\"\n", " )\n", - " if not validation_report[\"is_consistent\"]:\n", + " if validation_report[\"topology_changes\"]:\n", " print(\n", - " f\"Warning: Found {len(validation_report['warnings'])} topology/primvar issues\"\n", + " f\" Topology changes in {len(validation_report['topology_changes'])} frames\"\n", " )\n", - " if validation_report[\"topology_changes\"]:\n", - " print(\n", - " f\" Topology changes in {len(validation_report['topology_changes'])} frames\"\n", - " )\n", - "\n", - " # Convert to USD (preserves all primvars from VTK)\n", - " stage = converter.convert_mesh_data_sequence(\n", - " mesh_data_sequence=mesh_data_sequence,\n", - " output_usd=output_usd,\n", - " mesh_name=\"AlterraValve\",\n", - " time_codes=Alterra_times,\n", - " material=Alterra_material,\n", - " )\n", "\n", - " shutil.copy(output_usd, output_usd.with_suffix(\".save.usd\"))" + "# Convert to USD (preserves all primvars from VTK)\n", + "stage = converter.convert_mesh_data_sequence(\n", + " mesh_data_sequence=mesh_data_sequence,\n", + " output_usd=output_usd,\n", + " mesh_name=\"AlterraValve\",\n", + " time_codes=alterra_times,\n", + " material=stent_material,\n", + ")" ] }, { @@ -354,59 +255,33 @@ "metadata": {}, "outputs": [], "source": [ - "if COMPUTE_FULL_TIME_SERIES and Alterra_series:\n", - " # Post-process: apply colormap visualization using USDTools\n", - " if ENABLE_AUTO_COLORIZATION:\n", - " usd_tools = USDTools()\n", - " usd_anatomy_tools = USDAnatomyTools(stage)\n", - " if settings.separate_objects_by_connectivity is True:\n", - " mesh_path1 = \"/World/Meshes/AlterraValve_object3\"\n", - " mesh_path2 = \"/World/Meshes/AlterraValve_object4\"\n", - " elif settings.separate_objects_by_cell_type is True:\n", - " mesh_path1 = \"/World/Meshes/AlterraValve_triangle1\"\n", - " mesh_path2 = \"/World/Meshes/AlterraValve_triangle1\"\n", - " else:\n", - " mesh_path1 = \"/World/Meshes/AlterraValve\"\n", - " mesh_path2 = None\n", - "\n", - " # Inspect and select primvar for coloring\n", - " primvars = usd_tools.list_mesh_primvars(str(output_usd), mesh_path1)\n", - " print(primvars)\n", - " color_primvar = usd_tools.pick_color_primvar(\n", - " primvars, keywords=(\"strain\", \"stress\")\n", - " )\n", - "\n", - " if color_primvar:\n", - " print(f\"\\nApplying colormap to '{color_primvar}'\")\n", - " usd_tools.apply_colormap_from_primvar(\n", - " str(output_usd),\n", - " mesh_path1,\n", - " color_primvar,\n", - " # intensity_range=(0, 300),\n", - " cmap=\"hot\",\n", - " # use_sigmoid_scale=True,\n", - " bind_vertex_color_material=True,\n", - " )\n", - " if mesh_path2 is not None:\n", - " mesh_prim = stage.GetPrimAtPath(mesh_path2)\n", - " usd_anatomy_tools.apply_anatomy_material_to_prim(\n", - " mesh_prim, usd_anatomy_tools.bone_params\n", - " )\n", - "\n", - " if not validation_report[\"is_consistent\"]:\n", - " print(\n", - " f\"Warning: Found {len(validation_report['warnings'])} topology/primvar issues\"\n", - " )\n", - " if validation_report[\"topology_changes\"]:\n", - " print(\"\\nNo strain/stress primvar found for coloring\")\n", + "usd_tools = USDTools()\n", + "usd_anatomy_tools = USDAnatomyTools(stage)\n", + "if conversion_settings.separate_objects_by_connectivity is True:\n", + " vessel_path = \"/World/Meshes/AlterraValve_object3\"\n", + "elif conversion_settings.separate_objects_by_cell_type is True:\n", + " vessel_path = \"/World/Meshes/AlterraValve_triangle1\"\n", + "else:\n", + " vessel_path = \"/World/Meshes/AlterraValve\"\n", "\n", - " print(f\" Size: {output_usd.stat().st_size / (1024 * 1024):.2f} MB\")\n", - " print(f\" Time range: {stage.GetStartTimeCode()} - {stage.GetEndTimeCode()}\")\n", - " print(\n", - " f\" Duration: {(stage.GetEndTimeCode() - stage.GetStartTimeCode()) / settings.times_per_second:.2f} seconds @ {settings.times_per_second} FPS\"\n", - " )\n", - "elif not COMPUTE_FULL_TIME_SERIES:\n", - " print(\"⏭️ Skipping Alterra full time series (COMPUTE_FULL_TIME_SERIES = False)\")" + "# Select primvar for coloring\n", + "primvars = usd_tools.list_mesh_primvars(str(output_usd), vessel_path)\n", + "color_primvar = usd_tools.pick_color_primvar(\n", + " primvars, keywords=tuple(colormap_primvar_substrs)\n", + ")\n", + "print(f\"Chosen primvar = {color_primvar}\")\n", + "\n", + "if color_primvar:\n", + " print(f\"\\nApplying colormap to '{color_primvar}' using {colormap_name}\")\n", + " usd_tools.apply_colormap_from_primvar(\n", + " str(output_usd),\n", + " vessel_path,\n", + " color_primvar,\n", + " intensity_range=(colormap_range_min, colormap_range_max),\n", + " cmap=colormap_name,\n", + " use_sigmoid_scale=True,\n", + " bind_vertex_color_material=True,\n", + " )" ] } ], diff --git a/experiments/Convert_VTK_To_USD/convert_chop_tpv25_valve_to_usd.ipynb b/experiments/Convert_VTK_To_USD/convert_chop_tpv25_valve_to_usd.ipynb index 9fa4b66..b8f05fe 100644 --- a/experiments/Convert_VTK_To_USD/convert_chop_tpv25_valve_to_usd.ipynb +++ b/experiments/Convert_VTK_To_USD/convert_chop_tpv25_valve_to_usd.ipynb @@ -25,19 +25,6 @@ "5. Create multiple variations (full resolution, subsampled, etc.)" ] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from pathlib import Path\n", - "import re\n", - "import time as time_module\n", - "\n", - "import shutil" - ] - }, { "cell_type": "markdown", "metadata": {}, @@ -53,31 +40,12 @@ "metadata": {}, "outputs": [], "source": [ - "# Configuration: Control which conversions to run\n", - "# Set to True to compute full time series (all frames) - takes longer\n", - "# Set to False to only compute subsampled time series (faster, for preview)\n", - "COMPUTE_FULL_TIME_SERIES = True # Default: only subsampled\n", - "\n", - "print(\"Time Series Configuration:\")\n", - "print(f\" - Compute Full Time Series: {COMPUTE_FULL_TIME_SERIES}\")\n", - "print(\" - Compute Subsampled Time Series: Always enabled\")\n", - "print()\n", - "if not COMPUTE_FULL_TIME_SERIES:\n", - " print(\"⚠️ Full time series conversion is DISABLED for faster execution.\")\n", - " print(\" Set COMPUTE_FULL_TIME_SERIES = True to enable full conversion.\")\n", - "else:\n", - " print(\"✓ Full time series conversion is ENABLED (this will take longer).\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import logging\n", + "from pathlib import Path\n", + "import re\n", + "import time as time_module\n", + "\n", "import numpy as np\n", - "from pxr import Usd, UsdGeom\n", + "\n", "\n", "# Import the vtk_to_usd library\n", "from physiomotion4d.vtk_to_usd import (\n", @@ -92,9 +60,7 @@ "# Import USDTools for post-processing colormap\n", "from physiomotion4d.usd_tools import USDTools\n", "from physiomotion4d.usd_anatomy_tools import USDAnatomyTools\n", - "\n", - "# Configure logging\n", - "logging.basicConfig(level=logging.INFO, format=\"%(levelname)s: %(message)s\")" + "from physiomotion4d.notebook_utils import running_as_test" ] }, { @@ -110,315 +76,45 @@ "metadata": {}, "outputs": [], "source": [ + "# Set to True to use as a test. Automatically done by\n", + "# running_as_test() helper function.\n", + "quick_run = running_as_test()\n", + "quick_run_step = 4\n", + "\n", "# Define data directories (TPV25 only)\n", "data_dir = Path.cwd().parent.parent / \"data\" / \"CHOP-Valve4D\"\n", "tpv25_dir = data_dir / \"TPV25\"\n", - "output_dir = Path.cwd() / \"output\" / \"valve4d\"\n", - "output_dir.mkdir(parents=True, exist_ok=True)\n", - "\n", - "print(f\"Data directory: {data_dir}\")\n", - "print(f\"Output directory: {output_dir}\")\n", - "print(\"\\nDirectory status:\")\n", - "print(f\" TPV25: {'✓' if tpv25_dir.exists() else '✗'} {tpv25_dir}\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def discover_time_series(directory, pattern=r\"\\.t(\\d+)\\.vtk$\"):\n", - " \"\"\"Discover and sort time-series VTK files.\n", - "\n", - " Args:\n", - " directory: Directory containing VTK files\n", - " pattern: Regex pattern to extract time step number\n", - "\n", - " Returns:\n", - " list: Sorted list of (time_step, file_path) tuples\n", - " \"\"\"\n", - " vtk_files = list(Path(directory).glob(\"*.vtk\"))\n", - "\n", - " # Extract time step numbers and pair with files\n", - " time_series = []\n", - " for vtk_file in vtk_files:\n", - " match = re.search(pattern, vtk_file.name)\n", - " if match:\n", - " time_step = int(match.group(1))\n", - " time_series.append((time_step, vtk_file))\n", - "\n", - " # Sort by time step\n", - " time_series.sort(key=lambda x: x[0])\n", - "\n", - " return time_series\n", - "\n", - "\n", - "# Discover TPV25 time series\n", - "tpv25_series = discover_time_series(tpv25_dir)\n", - "\n", - "print(\"=\" * 60)\n", - "print(\"Time-Series Discovery (TPV25)\")\n", - "print(\"=\" * 60)\n", - "print(\"\\nTPV25:\")\n", - "print(f\" Files found: {len(tpv25_series)}\")\n", - "if tpv25_series:\n", - " print(f\" Time range: t{tpv25_series[0][0]} to t{tpv25_series[-1][0]}\")\n", - " print(f\" First file: {tpv25_series[0][1].name}\")\n", - " print(f\" Last file: {tpv25_series[-1][1].name}\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 2. Inspect First Frame\n", - "\n", - "Examine the first time step to understand the data structure." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Read first frame of TPV25\n", - "if tpv25_series:\n", - " print(\"=\" * 60)\n", - " print(\"TPV25 - First Frame Analysis\")\n", - " print(\"=\" * 60)\n", - "\n", - " first_file = tpv25_series[0][1]\n", - " mesh_data = read_vtk_file(first_file, extract_surface=True)\n", - "\n", - " print(f\"\\nFile: {first_file.name}\")\n", - " print(\"\\nGeometry:\")\n", - " print(f\" Points: {len(mesh_data.points):,}\")\n", - " print(f\" Faces: {len(mesh_data.face_vertex_counts):,}\")\n", - " print(f\" Normals: {'Yes' if mesh_data.normals is not None else 'No'}\")\n", - " print(f\" Colors: {'Yes' if mesh_data.colors is not None else 'No'}\")\n", - "\n", - " # Bounding box\n", - " bbox_min = np.min(mesh_data.points, axis=0)\n", - " bbox_max = np.max(mesh_data.points, axis=0)\n", - " bbox_size = bbox_max - bbox_min\n", - " print(\"\\nBounding Box:\")\n", - " print(f\" Min: [{bbox_min[0]:.3f}, {bbox_min[1]:.3f}, {bbox_min[2]:.3f}]\")\n", - " print(f\" Max: [{bbox_max[0]:.3f}, {bbox_max[1]:.3f}, {bbox_max[2]:.3f}]\")\n", - " print(f\" Size: [{bbox_size[0]:.3f}, {bbox_size[1]:.3f}, {bbox_size[2]:.3f}]\")\n", - "\n", - " print(f\"\\nData Arrays ({len(mesh_data.generic_arrays)}):\")\n", - " for i, array in enumerate(mesh_data.generic_arrays, 1):\n", - " print(f\" {i}. {array.name}:\")\n", - " print(f\" - Type: {array.data_type.value}\")\n", - " print(f\" - Components: {array.num_components}\")\n", - " print(f\" - Interpolation: {array.interpolation}\")\n", - " print(f\" - Elements: {len(array.data):,}\")\n", - " if array.data.size > 0:\n", - " print(f\" - Range: [{np.min(array.data):.6f}, {np.max(array.data):.6f}]\")\n", - "\n", - " # Cell types (face vertex count) - TPV data has multiple cell types (triangle, quad, etc.)\n", - " unique_counts, num_each = np.unique(\n", - " mesh_data.face_vertex_counts, return_counts=True\n", - " )\n", - " print(\"\\nCell types (faces by vertex count):\")\n", - " for u, n in zip(unique_counts, num_each):\n", - " name = cell_type_name_for_vertex_count(int(u))\n", - " print(f\" {name} ({u} vertices): {n:,} faces\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Note: Helper functions removed - now using USDTools for primvar inspection and colorization\n", - "# The workflow has changed to: convert to USD first, then apply colormap post-processing\n", - "\n", - "# Configuration: choose colormap for visualization\n", - "DEFAULT_COLORMAP = \"viridis\" # matplotlib colormap name\n", "\n", - "# Enable automatic colorization (will pick strain/stress primvars if available)\n", - "ENABLE_AUTO_COLORIZATION = True\n", + "output_dir = Path.cwd() / \"results\" / \"valve4d-tpv25\"\n", + "if quick_run:\n", + " output_usd = output_dir / \"tpv25_quick.usd\"\n", + "else:\n", + " output_usd = output_dir / \"tpv25_full.usd\"\n", "\n", - "print(\"Colorization will be applied after USD conversion using USDTools methods\")\n", - "print(\" - USDTools.list_mesh_primvars() for inspection\")\n", - "print(\" - USDTools.pick_color_primvar() for selection\")\n", - "print(\" - USDTools.apply_colormap_from_primvar() for coloring\")\n", - "print(f\" - Colormap: {DEFAULT_COLORMAP}\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "## 2. Configure Conversion Settings\n", + "colormap_primvar_substrs = [\"stress\", \"strain\"]\n", + "colormap_name = \"jet\" # matplotlib colormap name\n", + "colormap_range_min = 25\n", + "colormap_range_max = 200\n", "\n", - "# Create converter settings\n", - "settings = ConversionSettings(\n", + "conversion_settings = ConversionSettings(\n", " triangulate_meshes=True,\n", " compute_normals=False, # Use existing normals if available\n", " preserve_point_arrays=True,\n", " preserve_cell_arrays=True,\n", " separate_objects_by_cell_type=False,\n", - " separate_objects_by_connectivity=True,\n", + " separate_objects_by_connectivity=True, # Essential for tpv25 vtk file\n", " up_axis=\"Y\",\n", " times_per_second=60.0, # 60 FPS for smooth animation\n", " use_time_samples=True,\n", ")\n", "\n", - "print(\"Conversion settings configured\")\n", - "print(f\" - Triangulate: {settings.triangulate_meshes}\")\n", - "print(f\" - Separate objects by cell type: {settings.separate_objects_by_cell_type}\")\n", - "print(f\" - FPS: {settings.times_per_second}\")\n", - "print(f\" - Up axis: {settings.up_axis}\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 3. Convert Full Time Series - TPV25" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Create material for TPV25\n", - "# Note: Vertex colors will be applied post-conversion by USDTools\n", - "# Create material for TPV25\n", - "# Note: Vertex colors will be applied post-conversion by USDTools\n", - "tpv25_material = MaterialData(\n", + "stent_material = MaterialData(\n", " name=\"tpv25_valve\",\n", - " diffuse_color=(0.85, 0.4, 0.4),\n", + " diffuse_color=(0.5, 0.5, 0.5),\n", " roughness=0.4,\n", - " metallic=0.0,\n", - " use_vertex_colors=False, # USDTools will bind vertex color material during colorization\n", - ")\n", - "\n", - "print(\"=\" * 60)\n", - "print(\"Converting TPV25 Time Series\")\n", - "print(\"=\" * 60)\n", - "print(f\"Dataset: {len(tpv25_series)} frames\")\n", - "\n", - "# Convert TPV25 (full resolution)\n", - "if COMPUTE_FULL_TIME_SERIES and tpv25_series:\n", - " converter = VTKToUSDConverter(settings)\n", - "\n", - " tpv25_files = [file_path for _, file_path in tpv25_series]\n", - " tpv25_times = [float(time_step) for time_step, _ in tpv25_series]\n", - "\n", - " output_usd = output_dir / \"tpv25_full.usd\"\n", - "\n", - " print(f\"\\nConverting to: {output_usd}\")\n", - " print(f\"Time codes: {tpv25_times[0]:.1f} to {tpv25_times[-1]:.1f}\")\n", - " print(\"\\nThis may take several minutes...\\n\")\n", - "\n", - " start_time = time_module.time()\n", - "\n", - " # Read MeshData\n", - " mesh_data_sequence = [read_vtk_file(f, extract_surface=True) for f in tpv25_files]\n", - "\n", - " # Validate topology consistency across time series\n", - " validation_report = validate_time_series_topology(\n", - " mesh_data_sequence, filenames=tpv25_files\n", - " )\n", - " if not validation_report[\"is_consistent\"]:\n", - " print(\n", - " f\"Warning: Found {len(validation_report['warnings'])} topology/primvar issues\"\n", - " )\n", - " if validation_report[\"topology_changes\"]:\n", - " print(\n", - " f\" Topology changes in {len(validation_report['topology_changes'])} frames\"\n", - " )\n", - "\n", - " # Convert to USD (preserves all primvars from VTK)\n", - " stage = converter.convert_mesh_data_sequence(\n", - " mesh_data_sequence=mesh_data_sequence,\n", - " output_usd=output_usd,\n", - " mesh_name=\"TPV25Valve\",\n", - " time_codes=tpv25_times,\n", - " material=tpv25_material,\n", - " )\n", - "\n", - " shutil.copy(output_usd, output_usd.with_suffix(\".save.usd\"))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "if COMPUTE_FULL_TIME_SERIES and tpv25_series:\n", - " # Post-process: apply colormap visualization using USDTools\n", - " if ENABLE_AUTO_COLORIZATION:\n", - " usd_tools = USDTools()\n", - " usd_anatomy_tools = USDAnatomyTools(stage)\n", - " if settings.separate_objects_by_connectivity is True:\n", - " mesh_path1 = \"/World/Meshes/TPV25Valve_object4\"\n", - " mesh_path2 = \"/World/Meshes/TPV25Valve_object3\"\n", - " elif settings.separate_objects_by_cell_type is True:\n", - " mesh_path1 = \"/World/Meshes/TPV25Valve_triangle1\"\n", - " mesh_path2 = \"/World/Meshes/TPV25Valve_triangle1\"\n", - " else:\n", - " mesh_path1 = \"/World/Meshes/TPV25Valve\"\n", - " mesh_path2 = None\n", - "\n", - " # Inspect and select primvar for coloring\n", - " primvars = usd_tools.list_mesh_primvars(str(output_usd), mesh_path1)\n", - " print(primvars)\n", - " color_primvar = usd_tools.pick_color_primvar(\n", - " primvars, keywords=(\"strain\", \"stress\")\n", - " )\n", - "\n", - " if color_primvar:\n", - " print(f\"\\nApplying colormap to '{color_primvar}' using {DEFAULT_COLORMAP}\")\n", - " usd_tools.apply_colormap_from_primvar(\n", - " str(output_usd),\n", - " mesh_path1,\n", - " color_primvar,\n", - " # intensity_range=(75, 200),\n", - " cmap=\"hot\",\n", - " # use_sigmoid_scale=True,\n", - " bind_vertex_color_material=True,\n", - " )\n", - " if mesh_path2 is not None:\n", - " mesh_prim = stage.GetPrimAtPath(mesh_path2)\n", - " usd_anatomy_tools.apply_anatomy_material_to_prim(\n", - " mesh_prim, usd_anatomy_tools.bone_params\n", - " )\n", - "\n", - " if not validation_report[\"is_consistent\"]:\n", - " print(\n", - " f\"Warning: Found {len(validation_report['warnings'])} topology/primvar issues\"\n", - " )\n", - " if validation_report[\"topology_changes\"]:\n", - " print(\"\\nNo strain/stress primvar found for coloring\")\n", - "\n", - " print(f\" Size: {output_usd.stat().st_size / (1024 * 1024):.2f} MB\")\n", - " print(f\" Time range: {stage.GetStartTimeCode()} - {stage.GetEndTimeCode()}\")\n", - " print(\n", - " f\" Duration: {(stage.GetEndTimeCode() - stage.GetStartTimeCode()) / settings.times_per_second:.2f} seconds @ {settings.times_per_second} FPS\"\n", - " )\n", - "elif not COMPUTE_FULL_TIME_SERIES:\n", - " print(\"⏭️ Skipping TPV25 full time series (COMPUTE_FULL_TIME_SERIES = False)\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 4. Convert Subsampled Time Series - TPV25 (single mesh)\n", - "\n", - "Convert TPV25 with every 5th frame to **tpv25_subsample_5x.usd**. Uses default settings (no split); one mesh prim `TPV25Valve`." + " metallic=0.9,\n", + " use_vertex_colors=False,\n", + ")" ] }, { @@ -427,220 +123,30 @@ "metadata": {}, "outputs": [], "source": [ - "# Subsample TPV25 (every 5th frame)\n", - "if tpv25_series:\n", - " subsample_rate = 5\n", - " tpv25_subsampled = tpv25_series[::subsample_rate]\n", - "\n", - " print(\"=\" * 60)\n", - " print(f\"Converting Subsampled TPV25 (every {subsample_rate}th frame)\")\n", - " print(\"=\" * 60)\n", - " print(f\"Frames: {len(tpv25_series)} → {len(tpv25_subsampled)}\")\n", - "\n", - " converter = VTKToUSDConverter(settings)\n", - "\n", - " tpv25_files_sub = [file_path for _, file_path in tpv25_subsampled]\n", - " tpv25_times_sub = [float(time_step) for time_step, _ in tpv25_subsampled]\n", - "\n", - " output_usd_sub = output_dir / f\"tpv25_subsample_{subsample_rate}x.usd\"\n", - "\n", - " print(f\"\\nConverting to: {output_usd_sub}\")\n", - "\n", - " start_time = time_module.time()\n", - "\n", - " # Read MeshData\n", - " mesh_data_sequence = [\n", - " read_vtk_file(f, extract_surface=True) for f in tpv25_files_sub\n", - " ]\n", - "\n", - " # Validate topology consistency across time series\n", - " validation_report = validate_time_series_topology(\n", - " mesh_data_sequence, filenames=tpv25_files_sub\n", - " )\n", - " if not validation_report[\"is_consistent\"]:\n", - " print(\n", - " f\"Warning: Found {len(validation_report['warnings'])} topology/primvar issues\"\n", - " )\n", - " if validation_report[\"topology_changes\"]:\n", - " print(\n", - " f\" Topology changes in {len(validation_report['topology_changes'])} frames\"\n", - " )\n", - "\n", - " # Convert to USD (preserves all primvars from VTK)\n", - " stage_sub = converter.convert_mesh_data_sequence(\n", - " mesh_data_sequence=mesh_data_sequence,\n", - " output_usd=output_usd_sub,\n", - " mesh_name=\"TPV25Valve\",\n", - " time_codes=tpv25_times_sub,\n", - " material=tpv25_material,\n", - " )\n", - "\n", - " # Post-process: apply colormap visualization using USDTools\n", - " if ENABLE_AUTO_COLORIZATION:\n", - " usd_tools = USDTools()\n", - " if settings.separate_objects_by_connectivity is True:\n", - " mesh_path = \"/World/Meshes/object3\"\n", - " elif settings.separate_objects_by_cell_type is True:\n", - " mesh_path = \"/World/Meshes/triangle1\"\n", - " else:\n", - " mesh_path = \"/World/Meshes/TPV25Valve\"\n", - "\n", - " # Inspect and select primvar for coloring\n", - " primvars = usd_tools.list_mesh_primvars(str(output_usd_sub), mesh_path)\n", - " color_primvar = usd_tools.pick_color_primvar(\n", - " primvars, keywords=(\"strain\", \"stress\")\n", - " )\n", - "\n", - " if color_primvar:\n", - " print(f\"\\nApplying colormap to '{color_primvar}' using {DEFAULT_COLORMAP}\")\n", - " usd_tools.apply_colormap_from_primvar(\n", - " str(output_usd_sub),\n", - " mesh_path,\n", - " color_primvar,\n", - " cmap=DEFAULT_COLORMAP,\n", - " bind_vertex_color_material=True,\n", - " )\n", - " else:\n", - " print(\"\\nNo strain/stress primvar found for coloring\")\n", - "\n", - " elapsed = time_module.time() - start_time\n", - "\n", - " print(f\"\\n✓ Conversion completed in {elapsed:.1f} seconds\")\n", - " print(f\" Output: {output_usd_sub}\")\n", - " print(f\" Size: {output_usd_sub.stat().st_size / (1024 * 1024):.2f} MB\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 5. TPV25 Subsampled — Split by Cell Type\n", - "\n", - "When `separate_objects_by_cell_type=True`, the converter splits the mesh into **separate USD prims** by cell type (triangle, quad, etc.). Output: **tpv25_subsample_5x_by_cell_type.usd** (distinct from the single-mesh subsample).\n", - "\n", - "TPV data contains multiple cell types (see first-frame analysis). Here we convert the same subsampled TPV25 sequence with triangulation off so quads remain quads; the stage has one mesh per cell type (e.g. `Triangle_0`, `Quad_0`)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Convert TPV25 subsampled with separate meshes per cell type (triangulate=False to preserve quads)\n", - "if tpv25_series:\n", - " settings_by_cell_type = ConversionSettings(\n", - " triangulate_meshes=False, # Keep quads so we get both Triangle_0 and Quad_0\n", - " compute_normals=False,\n", - " preserve_point_arrays=True,\n", - " preserve_cell_arrays=True,\n", - " separate_objects_by_cell_type=True,\n", - " separate_objects_by_connectivity=False,\n", - " up_axis=\"Y\",\n", - " times_per_second=60.0,\n", - " use_time_samples=True,\n", - " )\n", + "output_dir.mkdir(parents=True, exist_ok=True)\n", "\n", - " subsample_rate = 5\n", - " tpv25_subsampled = tpv25_series[::subsample_rate]\n", - " tpv25_files_sub = [file_path for _, file_path in tpv25_subsampled]\n", - " tpv25_times_sub = [float(t) for t, _ in tpv25_subsampled]\n", + "vtk_files = list(Path(tpv25_dir).glob(\"*.vtk\"))\n", + "pattern = r\"\\.t(\\d+)\\.vtk$\"\n", "\n", - " output_by_cell_type = output_dir / \"tpv25_subsample_5x_by_cell_type.usd\"\n", - " print(\"Converting TPV25 (subsampled) with separate objects by cell type...\")\n", - " print(\n", - " \" triangulate_meshes=False → triangles and quads preserved as separate meshes\"\n", - " )\n", - " print(f\" Output: {output_by_cell_type.name}\")\n", - "\n", - " converter_ct = VTKToUSDConverter(settings_by_cell_type)\n", - " mesh_data_sequence = [\n", - " read_vtk_file(f, extract_surface=True) for f in tpv25_files_sub\n", - " ]\n", - " stage_ct = converter_ct.convert_mesh_data_sequence(\n", - " mesh_data_sequence=mesh_data_sequence,\n", - " output_usd=output_by_cell_type,\n", - " mesh_name=\"TPV25Valve\", # base name when not splitting; ignored when splitting\n", - " time_codes=tpv25_times_sub,\n", - " material=tpv25_material,\n", - " )\n", + "# Extract time step numbers and pair with files\n", + "tpv25_series = []\n", + "for vtk_file in vtk_files:\n", + " match = re.search(pattern, vtk_file.name)\n", + " if match:\n", + " time_step = int(match.group(1))\n", + " tpv25_series.append((time_step, vtk_file))\n", "\n", - " # List mesh prims under /World/Meshes (each cell type is a separate prim)\n", - " meshes_prim = stage_ct.GetPrimAtPath(\"/World/Meshes\")\n", - " if meshes_prim:\n", - " children = meshes_prim.GetChildren()\n", - " print(f\"\\nMesh prims created (by cell type): {len(children)}\")\n", - " for child in children:\n", - " print(f\" - {child.GetPath().pathString}\")\n", - " print(f\"\\n✓ Saved: {output_by_cell_type}\")" + "# Sort by time step\n", + "tpv25_series.sort(key=lambda x: x[0])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## 6. TPV25 Subsampled — Split by Connectivity\n", - "\n", - "When `separate_objects_by_connectivity=True`, the converter splits the mesh into **separate USD prims** by connected component (object1, object2, ...). Output: **tpv25_subsample_5x_by_connectivity.usd** (distinct from single-mesh and by-cell-type).\n", - "\n", - "Only one of `separate_objects_by_cell_type` and `separate_objects_by_connectivity` can be enabled at a time." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Convert TPV25 subsampled with separate meshes per connected component\n", - "if tpv25_series:\n", - " settings_by_connectivity = ConversionSettings(\n", - " triangulate_meshes=True,\n", - " compute_normals=False,\n", - " preserve_point_arrays=True,\n", - " preserve_cell_arrays=True,\n", - " separate_objects_by_cell_type=False,\n", - " separate_objects_by_connectivity=True,\n", - " up_axis=\"Y\",\n", - " times_per_second=60.0,\n", - " use_time_samples=True,\n", - " )\n", - "\n", - " subsample_rate = 5\n", - " tpv25_subsampled = tpv25_series[::subsample_rate]\n", - " tpv25_files_sub = [file_path for _, file_path in tpv25_subsampled]\n", - " tpv25_times_sub = [float(t) for t, _ in tpv25_subsampled]\n", - "\n", - " output_by_connectivity = output_dir / \"tpv25_subsample_5x_by_connectivity.usd\"\n", - " print(\"Converting TPV25 (subsampled) with separate objects by connectivity...\")\n", - " print(f\" Output: {output_by_connectivity.name}\")\n", - "\n", - " converter_conn = VTKToUSDConverter(settings_by_connectivity)\n", - " mesh_data_sequence = [\n", - " read_vtk_file(f, extract_surface=True) for f in tpv25_files_sub\n", - " ]\n", - " stage_conn = converter_conn.convert_mesh_data_sequence(\n", - " mesh_data_sequence=mesh_data_sequence,\n", - " output_usd=output_by_connectivity,\n", - " mesh_name=\"TPV25Valve\",\n", - " time_codes=tpv25_times_sub,\n", - " material=tpv25_material,\n", - " )\n", + "## 2. Inspect First Frame\n", "\n", - " meshes_prim = stage_conn.GetPrimAtPath(\"/World/Meshes\")\n", - " if meshes_prim:\n", - " children = meshes_prim.GetChildren()\n", - " print(f\"\\nMesh prims created (by connectivity): {len(children)}\")\n", - " for child in children:\n", - " print(f\" - {child.GetPath().pathString}\")\n", - " print(f\"\\n✓ Saved: {output_by_connectivity}\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 7. Summary and File Inspection" + "Examine the first time step to understand the data structure." ] }, { @@ -649,64 +155,49 @@ "metadata": {}, "outputs": [], "source": [ - "import os\n", - "\n", - "print(\"=\" * 60)\n", - "print(\"Conversion Summary\")\n", - "print(\"=\" * 60)\n", - "\n", - "# List all generated USD files\n", - "usd_files = list(output_dir.glob(\"*.usd\"))\n", - "usd_files.extend(output_dir.glob(\"*.usda\"))\n", - "usd_files.extend(output_dir.glob(\"*.usdc\"))\n", - "\n", - "total_size = 0\n", - "\n", - "for usd_file in sorted(usd_files):\n", - " size_mb = os.path.getsize(usd_file) / (1024 * 1024)\n", - " total_size += size_mb\n", - "\n", - " print(f\"\\n{usd_file.name}:\")\n", - " print(f\" Size: {size_mb:.2f} MB\")\n", - "\n", - " # Open and inspect\n", - " stage = Usd.Stage.Open(str(usd_file))\n", - " if stage:\n", - " if stage.HasAuthoredTimeCodeRange():\n", - " duration = (\n", - " stage.GetEndTimeCode() - stage.GetStartTimeCode()\n", - " ) / stage.GetTimeCodesPerSecond()\n", - " print(\n", - " f\" Time range: {stage.GetStartTimeCode():.0f} - {stage.GetEndTimeCode():.0f}\"\n", - " )\n", - " print(\n", - " f\" Duration: {duration:.2f} seconds @ {stage.GetTimeCodesPerSecond():.0f} FPS\"\n", - " )\n", - " print(\n", - " f\" Frames: {int(stage.GetEndTimeCode() - stage.GetStartTimeCode() + 1)}\"\n", - " )\n", - "\n", - " # Count meshes\n", - " mesh_count = 0\n", - " for prim in stage.Traverse():\n", - " if prim.IsA(UsdGeom.Mesh):\n", - " mesh_count += 1\n", - " print(f\" Meshes: {mesh_count}\")\n", - "\n", - "print(f\"\\n{'=' * 60}\")\n", - "print(f\"Total size: {total_size:.2f} MB\")\n", - "print(f\"Total files: {len(usd_files)}\")\n", - "print(f\"Output directory: {output_dir}\")\n", - "print(f\"{'=' * 60}\")" + "# Debuggin\n", + "first_file = tpv25_series[0][1]\n", + "mesh_data = read_vtk_file(first_file, extract_surface=True)\n", + "\n", + "print(f\"\\nFile: {first_file.name}\")\n", + "print(\"\\nGeometry:\")\n", + "print(f\" Points: {len(mesh_data.points):,}\")\n", + "print(f\" Faces: {len(mesh_data.face_vertex_counts):,}\")\n", + "print(f\" Normals: {'Yes' if mesh_data.normals is not None else 'No'}\")\n", + "print(f\" Colors: {'Yes' if mesh_data.colors is not None else 'No'}\")\n", + "\n", + "# Bounding box\n", + "bbox_min = np.min(mesh_data.points, axis=0)\n", + "bbox_max = np.max(mesh_data.points, axis=0)\n", + "bbox_size = bbox_max - bbox_min\n", + "print(\"\\nBounding Box:\")\n", + "print(f\" Min: [{bbox_min[0]:.3f}, {bbox_min[1]:.3f}, {bbox_min[2]:.3f}]\")\n", + "print(f\" Max: [{bbox_max[0]:.3f}, {bbox_max[1]:.3f}, {bbox_max[2]:.3f}]\")\n", + "print(f\" Size: [{bbox_size[0]:.3f}, {bbox_size[1]:.3f}, {bbox_size[2]:.3f}]\")\n", + "\n", + "print(f\"\\nData Arrays ({len(mesh_data.generic_arrays)}):\")\n", + "for i, array in enumerate(mesh_data.generic_arrays, 1):\n", + " print(f\" {i}. {array.name}:\")\n", + " print(f\" - Type: {array.data_type.value}\")\n", + " print(f\" - Components: {array.num_components}\")\n", + " print(f\" - Interpolation: {array.interpolation}\")\n", + " print(f\" - Elements: {len(array.data):,}\")\n", + " if array.data.size > 0:\n", + " print(f\" - Range: [{np.min(array.data):.6f}, {np.max(array.data):.6f}]\")\n", + "\n", + "# Cell types (face vertex count = triangle, quad, etc.)\n", + "unique_counts, num_each = np.unique(mesh_data.face_vertex_counts, return_counts=True)\n", + "print(\"\\nCell types (faces by vertex count):\")\n", + "for u, n in zip(unique_counts, num_each):\n", + " name = cell_type_name_for_vertex_count(int(u))\n", + " print(f\" {name} ({u} vertices): {n:,} faces\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## 8. Detailed USD Inspection\n", - "\n", - "Examine the converted USD files to verify data preservation." + "## 3. Convert TPV25" ] }, { @@ -715,161 +206,45 @@ "metadata": {}, "outputs": [], "source": [ - "# Inspect one of the converted files in detail\n", - "inspect_file = output_dir / \"tpv25_subsample_5x.usd\"\n", - "\n", - "if inspect_file.exists():\n", - " print(\"=\" * 60)\n", - " print(f\"Detailed Inspection: {inspect_file.name}\")\n", - " print(\"=\" * 60)\n", - "\n", - " stage = Usd.Stage.Open(str(inspect_file))\n", - "\n", - " # Find mesh prim\n", - " mesh_prim = None\n", - " for prim in stage.Traverse():\n", - " if prim.IsA(UsdGeom.Mesh):\n", - " mesh_prim = prim\n", - " break\n", - "\n", - " if mesh_prim:\n", - " mesh = UsdGeom.Mesh(mesh_prim)\n", - "\n", - " print(f\"\\nMesh: {mesh_prim.GetPath()}\")\n", - "\n", - " # Geometry at first frame\n", - " first_time = stage.GetStartTimeCode()\n", - " points = mesh.GetPointsAttr().Get(first_time)\n", - " faces = mesh.GetFaceVertexCountsAttr().Get()\n", - "\n", - " print(f\"\\nGeometry (at t={first_time:.0f}):\")\n", - " print(f\" Points: {len(points):,}\")\n", - " print(f\" Faces: {len(faces):,}\")\n", - "\n", - " # Check time-varying attributes\n", - " print(\"\\nTime-Varying Attributes:\")\n", - " points_attr = mesh.GetPointsAttr()\n", - " if points_attr.GetNumTimeSamples() > 0:\n", - " print(f\" Points: {points_attr.GetNumTimeSamples()} time samples\")\n", - "\n", - " # List primvars\n", - " primvars_api = UsdGeom.PrimvarsAPI(mesh)\n", - " primvars = primvars_api.GetPrimvars()\n", - "\n", - " print(f\"\\nPrimvars ({len(primvars)}):\")\n", - " for primvar in primvars:\n", - " name = primvar.GetPrimvarName()\n", - " interpolation = primvar.GetInterpolation()\n", - " type_name = primvar.GetTypeName()\n", - " value = primvar.Get(first_time)\n", - " size = len(value) if value else 0\n", - "\n", - " print(f\" - {name}:\")\n", - " print(f\" Type: {type_name}\")\n", - " print(f\" Interpolation: {interpolation}\")\n", - " print(f\" Elements: {size:,}\")\n", - "\n", - " # Check if time-varying\n", - " if primvar.GetAttr().GetNumTimeSamples() > 0:\n", - " print(f\" Time samples: {primvar.GetAttr().GetNumTimeSamples()}\")\n", - "\n", - " # Material binding\n", - " from pxr import UsdShade\n", - "\n", - " binding_api = UsdShade.MaterialBindingAPI(mesh)\n", - " material_binding = binding_api.GetDirectBinding()\n", - " if material_binding:\n", - " print(f\"\\nMaterial: {material_binding.GetMaterialPath()}\")\n", - "else:\n", - " print(f\"File not found: {inspect_file}\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 8.5. Post-Process USD with USDTools\n", + "converter = VTKToUSDConverter(conversion_settings)\n", "\n", - "Demonstrate using the new `USDTools` methods to inspect primvars and apply colormap visualization to existing USD files." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Example: Post-process an existing USD file to add colormap visualization\n", - "from physiomotion4d.usd_tools import USDTools\n", + "tpv25_files = [file_path for _, file_path in tpv25_series]\n", + "tpv25_times = [float(time_step) for time_step, _ in tpv25_series]\n", "\n", - "usd_tools = USDTools()\n", + "if quick_run:\n", + " tpv25_files = tpv25_files[::quick_run_step]\n", + " tpv25_times = tpv25_times[::quick_run_step]\n", "\n", - "# Pick a USD file to post-process\n", - "postprocess_file = output_dir / \"tpv25_subsample_5x.usd\"\n", + "print(f\"\\nConverting to: {output_usd}\")\n", + "print(f\"Number of time steps: {len(tpv25_times)}\")\n", + "print(\"\\nThis may take several minutes...\\n\")\n", "\n", - "if postprocess_file.exists():\n", - " print(\"=\" * 60)\n", - " print(f\"Post-Processing: {postprocess_file.name}\")\n", - " print(\"=\" * 60)\n", + "start_time = time_module.time()\n", "\n", - " # 1. List available primvars on the mesh\n", - " mesh_path = \"/World/Meshes/AlterraValve\"\n", - " primvars = usd_tools.list_mesh_primvars(str(postprocess_file), mesh_path)\n", + "# Read MeshData\n", + "mesh_data_sequence = [read_vtk_file(f, extract_surface=True) for f in tpv25_files]\n", "\n", - " print(f\"\\nAvailable primvars on {mesh_path}:\")\n", - " for pv in primvars:\n", - " time_info = (\n", - " f\", {pv['num_time_samples']} time samples\"\n", - " if pv[\"num_time_samples\"] > 0\n", - " else \"\"\n", - " )\n", - " range_info = (\n", - " f\", range={pv['range'][0]:.3g}..{pv['range'][1]:.3g}\" if pv[\"range\"] else \"\"\n", - " )\n", + "# Validate topology consistency across time series\n", + "validation_report = validate_time_series_topology(\n", + " mesh_data_sequence, filenames=tpv25_files\n", + ")\n", + "if not validation_report[\"is_consistent\"]:\n", + " print(\n", + " f\"Warning: Found {len(validation_report['warnings'])} topology/primvar issues\"\n", + " )\n", + " if validation_report[\"topology_changes\"]:\n", " print(\n", - " f\" - {pv['name']}: {pv['interpolation']}, {pv['elements']} elements{time_info}{range_info}\"\n", + " f\" Topology changes in {len(validation_report['topology_changes'])} frames\"\n", " )\n", "\n", - " # 2. Pick best primvar for coloring (prefer strain/stress)\n", - " color_primvar = usd_tools.pick_color_primvar(primvars)\n", - " print(f\"\\nAuto-selected for coloring: {color_primvar}\")\n", - "\n", - " # 3. Apply colormap to create displayColor visualization\n", - " # Note: This modifies the USD file in-place\n", - " if color_primvar:\n", - " print(f\"\\nApplying 'plasma' colormap to '{color_primvar}'...\")\n", - "\n", - " # Create a copy for demonstration (optional)\n", - " demo_file = output_dir / f\"{postprocess_file.stem}_colored.usd\"\n", - " import shutil\n", - "\n", - " shutil.copy(postprocess_file, demo_file)\n", - "\n", - " usd_tools.apply_colormap_from_primvar(\n", - " str(demo_file),\n", - " mesh_path,\n", - " color_primvar,\n", - " cmap=\"plasma\",\n", - " write_default_at_t0=True,\n", - " bind_vertex_color_material=True,\n", - " )\n", - "\n", - " print(f\"\\n✓ Created colored visualization: {demo_file.name}\")\n", - " print(f\" - displayColor primvar added with colormap from {color_primvar}\")\n", - " print(\" - Vertex color material bound for immediate visualization\")\n", - " print(\" - Ready to open in Omniverse with default coloring\")\n", - " else:\n", - " print(\"\\n⚠️ No suitable primvar found for coloring\")\n", - "else:\n", - " print(f\"File not found: {postprocess_file}\")\n", - " print(\"Run the conversion cells first to generate USD files.\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 9. Performance Analysis" + "# Convert to USD (preserves all primvars from VTK)\n", + "stage = converter.convert_mesh_data_sequence(\n", + " mesh_data_sequence=mesh_data_sequence,\n", + " output_usd=output_usd,\n", + " mesh_name=\"TPV25Valve\",\n", + " time_codes=tpv25_times,\n", + " material=stent_material,\n", + ")" ] }, { @@ -878,88 +253,33 @@ "metadata": {}, "outputs": [], "source": [ - "# Analyze conversion performance\n", - "print(\"=\" * 60)\n", - "print(\"Performance Analysis\")\n", - "print(\"=\" * 60)\n", - "\n", - "# Read a few frames to estimate per-frame metrics\n", - "if tpv25_series:\n", - " sample_files = [\n", - " tpv25_series[0][1],\n", - " tpv25_series[len(tpv25_series) // 2][1],\n", - " tpv25_series[-1][1],\n", - " ]\n", - "\n", - " total_points = 0\n", - " total_faces = 0\n", - " total_arrays = 0\n", - "\n", - " for sample_file in sample_files:\n", - " mesh_data = read_vtk_file(sample_file, extract_surface=True)\n", - " total_points += len(mesh_data.points)\n", - " total_faces += len(mesh_data.face_vertex_counts)\n", - " total_arrays += len(mesh_data.generic_arrays)\n", - "\n", - " avg_points = total_points / len(sample_files)\n", - " avg_faces = total_faces / len(sample_files)\n", - " avg_arrays = total_arrays / len(sample_files)\n", - "\n", - " print(\"\\nTPV25 Dataset:\")\n", - " print(f\" Average points per frame: {avg_points:,.0f}\")\n", - " print(f\" Average faces per frame: {avg_faces:,.0f}\")\n", - " print(f\" Average data arrays per frame: {avg_arrays:.0f}\")\n", - " print(f\" Total frames: {len(tpv25_series)}\")\n", - " print(f\" Estimated total points: {avg_points * len(tpv25_series):,.0f}\")\n", - " print(f\" Estimated total faces: {avg_faces * len(tpv25_series):,.0f}\")\n", - "\n", - "print(f\"\\n{'=' * 60}\")\n", - "print(\"\\n✓ All conversions completed!\")\n", - "print(\"\\nView the results:\")\n", - "print(\" - USDView: usdview .usd\")\n", - "print(\" - Omniverse: Open in Create/View/Composer\")\n", - "print(f\"\\nOutput files: {output_dir}\")\n", - "print(\"=\" * 60)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Conclusion\n", - "\n", - "This notebook demonstrated converting large-scale time-varying cardiac valve simulation data to USD:\n", - "\n", - "### Key Accomplishments\n", - "\n", - "1. **Discovered and organized** 200+ frame time-series datasets\n", - "2. **Converted full-resolution** datasets to animated USD\n", - "3. **Created subsampled versions** for faster preview\n", - "4. **Preserved all simulation data** as USD primvars\n", - "5. **Applied custom materials** for visualization\n", - "6. **Handled coordinate systems** (RAS → Y-up)\n", - "\n", - "### File Outputs\n", - "\n", - "- `tpv25_full.usd` - Complete 265-frame animation (single mesh)\n", - "- `tpv25_subsample_5x.usd` - Subsampled, single mesh\n", - "- `tpv25_subsample_5x_by_cell_type.usd` - Subsampled, split by cell type (Triangle_0, Quad_0, ...)\n", - "- `tpv25_subsample_5x_by_connectivity.usd` - Subsampled, split by connectivity (object1, object2, ...)\n", - "\n", - "### Performance Notes\n", - "\n", - "- Full conversions may take several minutes due to large frame counts\n", - "- Subsampling provides faster iteration during development\n", - "- All VTK point and cell data arrays are preserved as primvars\n", - "- Time-sampled attributes enable efficient animation\n", - "\n", - "### Next Steps\n", + "usd_tools = USDTools()\n", + "usd_anatomy_tools = USDAnatomyTools(stage)\n", + "if conversion_settings.separate_objects_by_connectivity is True:\n", + " vessel_path = \"/World/Meshes/TPV25Valve_object4\"\n", + "elif conversion_settings.separate_objects_by_cell_type is True:\n", + " vessel_path = \"/World/Meshes/TPV25Valve_triangle1\"\n", + "else:\n", + " vessel_path = \"/World/Meshes/TPV25Valve\"\n", "\n", - "1. **View animations** in USDView or Omniverse\n", - "2. **Analyze primvars** to visualize simulation data\n", - "3. **Create custom materials** based on data arrays\n", - "4. **Compose scenes** or add multiple assets for comparison\n", - "5. **Add cameras and lighting** for publication-quality renders" + "# Select primvar for coloring\n", + "primvars = usd_tools.list_mesh_primvars(str(output_usd), vessel_path)\n", + "color_primvar = usd_tools.pick_color_primvar(\n", + " primvars, keywords=tuple(colormap_primvar_substrs)\n", + ")\n", + "print(f\"Chosen primvar = {color_primvar}\")\n", + "\n", + "if color_primvar:\n", + " print(f\"\\nApplying colormap to '{color_primvar}' using {colormap_name}\")\n", + " usd_tools.apply_colormap_from_primvar(\n", + " str(output_usd),\n", + " vessel_path,\n", + " color_primvar,\n", + " intensity_range=(colormap_range_min, colormap_range_max),\n", + " cmap=colormap_name,\n", + " use_sigmoid_scale=True,\n", + " bind_vertex_color_material=True,\n", + " )" ] } ],