{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# OpenTrack - Humanoid Motion Tracking Demo\n", "\n", "This notebook demonstrates OpenTrack, an open-source humanoid motion tracking system using MuJoCo.\n", "\n", "**Note**: Training can be resource-intensive. We'll use debug mode for quick testing." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 1. Setup Environment" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import os\n", "import subprocess\n", "import time\n", "import glob\n", "from pathlib import Path\n", "from IPython.display import Video, display, HTML\n", "import threading\n", "import queue\n", "\n", "# Define project directory structure\n", "PROJECT_BASE = Path(\"/data/workspaces/opentrack\")\n", "DATASETS_DIR = PROJECT_BASE / \"datasets\"\n", "MODELS_DIR = PROJECT_BASE / \"models\"\n", "VIDEOS_DIR = PROJECT_BASE / \"videos\"\n", "REPO_DIR = PROJECT_BASE / \"OpenTrack\"\n", "\n", "# Create directories\n", "for dir_path in [PROJECT_BASE, DATASETS_DIR, MODELS_DIR, VIDEOS_DIR]:\n", " dir_path.mkdir(parents=True, exist_ok=True)\n", " print(f\"✓ {dir_path}\")\n", "\n", "print(\"\\n✓ Environment setup complete!\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 2. Clone OpenTrack Repository" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Clone the repository if not already cloned\n", "if not REPO_DIR.exists():\n", " print(f\"Cloning OpenTrack to {REPO_DIR}...\")\n", " subprocess.run(\n", " ['git', 'clone', 'https://github.com/GalaxyGeneralRobotics/OpenTrack.git', str(REPO_DIR)],\n", " check=True\n", " )\n", " print(\"✓ Repository cloned successfully\")\n", "else:\n", " print(f\"✓ Repository already exists at {REPO_DIR}\")\n", "\n", "# Change to repository directory\n", "os.chdir(REPO_DIR)\n", "print(f\"\\n✓ Working directory: {os.getcwd()}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 3. Install Dependencies" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Install PyTorch (CPU version for compatibility)\n", "!pip install -q torch==2.5.1 torchvision==0.20.1 torchaudio==2.5.1 --index-url https://download.pytorch.org/whl/cpu\n", "\n", "# Install OpenTrack requirements\n", "!pip install -q -r requirements.txt\n", "\n", "# Install additional packages for video handling\n", "!pip install -q imageio imageio-ffmpeg\n", "\n", "print(\"✓ All dependencies installed\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 4. Download Motion Capture Data" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from huggingface_hub import snapshot_download\n", "\n", "# Define mocap directory in our datasets folder\n", "mocap_dir = DATASETS_DIR / \"lafan1\" / \"UnitreeG1\"\n", "mocap_dir.mkdir(parents=True, exist_ok=True)\n", "\n", "repo_id = \"robfiras/loco-mujoco-datasets\"\n", "\n", "print(\"Downloading all mocap data from Lafan1/mocap/UnitreeG1...\")\n", "print(f\"Target directory: {mocap_dir}\")\n", "print(\"This will download all .npz files concurrently.\\n\")\n", "\n", "try:\n", " # Use snapshot_download with allow_patterns to download only the files we need\n", " snapshot_path = snapshot_download(\n", " repo_id=repo_id,\n", " repo_type=\"dataset\",\n", " allow_patterns=\"Lafan1/mocap/UnitreeG1/*.npz\",\n", " local_dir=str(DATASETS_DIR),\n", " local_dir_use_symlinks=False\n", " )\n", " \n", " print(f\"\\n✓ Download complete! Files saved to: {snapshot_path}\")\n", " \n", " # Verify files\n", " npz_files = list(mocap_dir.glob(\"*.npz\"))\n", " print(f\"✓ Found {len(npz_files)} .npz files in {mocap_dir}\")\n", " \n", " if npz_files:\n", " print(\"\\nSample files:\")\n", " for f in sorted(npz_files)[:10]: # Show first 10 files\n", " print(f\" - {f.name}\")\n", " if len(npz_files) > 10:\n", " print(f\" ... and {len(npz_files) - 10} more files\")\n", " \n", " # Create symlink from OpenTrack's expected data directory to our datasets\n", " opentrack_data_dir = REPO_DIR / \"data\" / \"mocap\"\n", " opentrack_data_dir.parent.mkdir(parents=True, exist_ok=True)\n", " \n", " # Remove old symlink/directory if it exists\n", " if opentrack_data_dir.exists() or opentrack_data_dir.is_symlink():\n", " if opentrack_data_dir.is_symlink():\n", " opentrack_data_dir.unlink()\n", " else:\n", " import shutil\n", " shutil.rmtree(opentrack_data_dir)\n", " \n", " # Create symlink\n", " opentrack_data_dir.symlink_to(DATASETS_DIR, target_is_directory=True)\n", " print(f\"\\n✓ Created symlink: {opentrack_data_dir} -> {DATASETS_DIR}\")\n", " \n", "except Exception as e:\n", " print(f\"⚠ Error downloading mocap data: {e}\")\n", " print(\"\\nYou may need to download manually from:\")\n", " print(\"https://huggingface.co/datasets/robfiras/loco-mujoco-datasets/tree/main/Lafan1/mocap/UnitreeG1\")\n", " print(\"\\nOr check if you need to authenticate:\")\n", " print(\" huggingface-cli login\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 5. Helper Functions for Background Process Management" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def run_command_with_output(cmd, description=\"Running command\"):\n", " \"\"\"\n", " Run a command and display output in real-time\n", " \"\"\"\n", " print(f\"\\n{'='*60}\")\n", " print(f\"{description}\")\n", " print(f\"Command: {' '.join(cmd)}\")\n", " print(f\"{'='*60}\\n\")\n", " \n", " process = subprocess.Popen(\n", " cmd,\n", " stdout=subprocess.PIPE,\n", " stderr=subprocess.STDOUT,\n", " text=True,\n", " bufsize=1,\n", " universal_newlines=True\n", " )\n", " \n", " # Read output line by line\n", " for line in process.stdout:\n", " print(line, end='')\n", " \n", " process.wait()\n", " \n", " if process.returncode == 0:\n", " print(f\"\\n✓ {description} completed successfully\")\n", " else:\n", " print(f\"\\n⚠ {description} exited with code {process.returncode}\")\n", " \n", " return process.returncode\n", "\n", "\n", "def find_latest_experiment(exp_name_pattern):\n", " \"\"\"\n", " Find the latest experiment folder matching the pattern\n", " \"\"\"\n", " # Check both MODELS_DIR and REPO_DIR/logs\n", " search_dirs = [MODELS_DIR, REPO_DIR / \"logs\"]\n", " \n", " all_experiments = []\n", " for logs_dir in search_dirs:\n", " if not logs_dir.exists():\n", " continue\n", " \n", " # Find all matching experiments\n", " experiments = [\n", " d for d in logs_dir.iterdir() \n", " if d.is_dir() and exp_name_pattern in d.name\n", " ]\n", " all_experiments.extend(experiments)\n", " \n", " if not all_experiments:\n", " return None\n", " \n", " # Sort by modification time and return latest\n", " latest = sorted(all_experiments, key=lambda x: x.stat().st_mtime, reverse=True)[0]\n", " return latest.name\n", "\n", "\n", "def find_generated_videos(output_dir=None):\n", " \"\"\"\n", " Find all generated video files\n", " \"\"\"\n", " if output_dir is None:\n", " output_dir = VIDEOS_DIR\n", " else:\n", " output_dir = Path(output_dir)\n", " \n", " videos = []\n", " \n", " if output_dir.exists():\n", " videos = list(output_dir.glob(\"*.mp4\")) + list(output_dir.glob(\"*.gif\"))\n", " \n", " if not videos:\n", " # Try alternative locations\n", " alternative_dirs = [REPO_DIR, REPO_DIR / \"logs\", MODELS_DIR]\n", " for alt_dir in alternative_dirs:\n", " if alt_dir.exists():\n", " found = list(alt_dir.glob(\"**/*.mp4\")) + list(alt_dir.glob(\"**/*.gif\"))\n", " videos.extend(found)\n", " \n", " return sorted(videos, key=lambda x: x.stat().st_mtime, reverse=True)\n", "\n", "\n", "def setup_model_output_symlink():\n", " \"\"\"\n", " Create symlink from OpenTrack's logs directory to our models directory\n", " \"\"\"\n", " opentrack_logs_dir = REPO_DIR / \"logs\"\n", " \n", " # Remove old symlink/directory if it exists\n", " if opentrack_logs_dir.exists() or opentrack_logs_dir.is_symlink():\n", " if opentrack_logs_dir.is_symlink():\n", " opentrack_logs_dir.unlink()\n", " else:\n", " import shutil\n", " # Move existing logs to MODELS_DIR first\n", " if opentrack_logs_dir.is_dir():\n", " for item in opentrack_logs_dir.iterdir():\n", " shutil.move(str(item), str(MODELS_DIR))\n", " shutil.rmtree(opentrack_logs_dir)\n", " \n", " # Create symlink\n", " opentrack_logs_dir.symlink_to(MODELS_DIR, target_is_directory=True)\n", " print(f\"✓ Created symlink: {opentrack_logs_dir} -> {MODELS_DIR}\")\n", "\n", "print(\"✓ Helper functions loaded\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 6. Quick Training (Debug Mode)\n", "\n", "We'll run a quick training session in debug mode. This won't produce a well-trained model but will verify everything works." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Setup symlink for model outputs\n", "setup_model_output_symlink()\n", "\n", "print(f\"\\nModels will be saved to: {MODELS_DIR}\\n\")\n", "\n", "# Run quick training\n", "cmd = [\n", " 'python', 'train_policy.py',\n", " '--exp_name', 'debug',\n", " '--terrain_type', 'flat_terrain'\n", "]\n", "\n", "return_code = run_command_with_output(cmd, \"Training OpenTrack (debug mode)\")\n", "\n", "# Find the experiment folder\n", "exp_folder = find_latest_experiment('debug')\n", "if exp_folder:\n", " print(f\"\\n✓ Training completed! Experiment: {exp_folder}\")\n", " print(f\"✓ Model saved in: {MODELS_DIR / exp_folder}\")\n", "else:\n", " print(\"\\n⚠ Could not find experiment folder\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 7. Convert Checkpoint (Brax → PyTorch)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Get the latest experiment name\n", "exp_folder = find_latest_experiment('debug')\n", "\n", "if exp_folder:\n", " print(f\"Converting checkpoint for: {exp_folder}\")\n", " \n", " cmd = [\n", " 'python', 'brax2torch.py',\n", " '--exp_name', exp_folder\n", " ]\n", " \n", " return_code = run_command_with_output(cmd, \"Converting Brax checkpoint to PyTorch\")\n", "else:\n", " print(\"⚠ No experiment found. Run training first.\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 8. Generate Videos (Headless Rendering)\n", "\n", "This will run the policy and generate videos using MuJoCo's headless renderer." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Get the latest experiment name\n", "exp_folder = find_latest_experiment('debug')\n", "\n", "if exp_folder:\n", " print(f\"Generating videos for: {exp_folder}\")\n", " print(f\"Videos will be saved to: {VIDEOS_DIR}\\n\")\n", " \n", " # Use --use_renderer for headless video generation (NOT --use_viewer)\n", " cmd = [\n", " 'python', 'play_policy.py',\n", " '--exp_name', exp_folder,\n", " '--use_renderer',\n", " # '--play_ref_motion', # Uncomment to also show reference motion\n", " ]\n", " \n", " return_code = run_command_with_output(cmd, \"Generating videos with MuJoCo renderer\")\n", " \n", " print(\"\\n\" + \"=\"*60)\n", " print(\"Searching for generated videos...\")\n", " print(\"=\"*60)\n", " \n", " # Wait a moment for files to be written\n", " time.sleep(2)\n", " \n", " # Find generated videos\n", " videos = find_generated_videos()\n", " \n", " # Move videos to VIDEOS_DIR if they're not already there\n", " if videos:\n", " import shutil\n", " moved_videos = []\n", " for video in videos:\n", " if not video.is_relative_to(VIDEOS_DIR):\n", " dest = VIDEOS_DIR / video.name\n", " shutil.copy2(video, dest)\n", " moved_videos.append(dest)\n", " print(f\"Moved: {video.name} -> {dest}\")\n", " else:\n", " moved_videos.append(video)\n", " videos = moved_videos\n", " \n", " if videos:\n", " print(f\"\\n✓ Found {len(videos)} video(s) in {VIDEOS_DIR}:\")\n", " for video in videos:\n", " print(f\" - {video}\")\n", " else:\n", " print(\"\\n⚠ No videos found. Checking alternative locations...\")\n", " # Search more broadly\n", " all_videos = list(REPO_DIR.rglob(\"*.mp4\")) + list(REPO_DIR.rglob(\"*.gif\"))\n", " if all_videos:\n", " print(f\"Found {len(all_videos)} video(s) in project:\")\n", " for video in all_videos[:10]: # Show first 10\n", " print(f\" - {video}\")\n", "else:\n", " print(\"⚠ No experiment found. Run training first.\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 9. Display Generated Videos" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Find and display all generated videos from our videos directory\n", "videos = find_generated_videos()\n", "\n", "if not videos:\n", " # Try alternative search in the whole project\n", " videos = list(REPO_DIR.rglob(\"*.mp4\")) + list(REPO_DIR.rglob(\"*.gif\"))\n", "\n", "if videos:\n", " print(f\"Displaying {len(videos)} video(s) from {VIDEOS_DIR}:\\n\")\n", " \n", " for i, video_path in enumerate(videos[:5]): # Display first 5 videos\n", " print(f\"\\n{'='*60}\")\n", " print(f\"Video {i+1}: {video_path.name}\")\n", " print(f\"Location: {video_path}\")\n", " print(f\"{'='*60}\")\n", " \n", " try:\n", " if video_path.suffix == '.mp4':\n", " display(Video(str(video_path), width=800, embed=True))\n", " elif video_path.suffix == '.gif':\n", " display(HTML(f''))\n", " except Exception as e:\n", " print(f\"⚠ Error displaying video: {e}\")\n", " print(f\"You can download it from: {video_path}\")\n", "else:\n", " print(\"⚠ No videos found.\")\n", " print(\"\\nPossible reasons:\")\n", " print(\"1. Training didn't complete successfully\")\n", " print(\"2. Checkpoint conversion failed\")\n", " print(\"3. Video generation failed\")\n", " print(\"\\nCheck the output of previous cells for errors.\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 10. Optional: Generate Rough Terrain\n", "\n", "If you want to test on rough terrain, run this cell first to generate terrain data." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Generate rough terrain using Perlin noise\n", "cmd = ['python', 'generate_terrain.py']\n", "return_code = run_command_with_output(cmd, \"Generating rough terrain\")\n", "\n", "print(\"\\n✓ Terrain generated! You can now train with --terrain_type rough_terrain\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 11. Optional: Full Training (Takes Much Longer)\n", "\n", "⚠️ Warning: This will take significant time and resources. Only run if you have GPU access and time." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Full training (uncomment to run)\n", "# cmd = [\n", "# 'python', 'train_policy.py',\n", "# '--exp_name', 'flat_terrain_full',\n", "# '--terrain_type', 'flat_terrain'\n", "# ]\n", "# \n", "# return_code = run_command_with_output(cmd, \"Full training on flat terrain\")\n", "# \n", "# # Then convert and play\n", "# exp_folder = find_latest_experiment('flat_terrain_full')\n", "# if exp_folder:\n", "# !python brax2torch.py --exp_name {exp_folder}\n", "# !python play_policy.py --exp_name {exp_folder} --use_renderer\n", "\n", "print(\"Full training cell ready (currently commented out)\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Summary\n", "\n", "This notebook demonstrates:\n", "1. ✅ Setting up OpenTrack in a Jupyter environment\n", "2. ✅ Running training (debug mode for quick testing)\n", "3. ✅ Converting Brax checkpoints to PyTorch\n", "4. ✅ Generating videos using headless MuJoCo renderer\n", "5. ✅ Displaying videos in the notebook\n", "\n", "**Project Structure:**\n", "```\n", "/data/workspaces/opentrack/\n", "├── datasets/ # Motion capture data (.npz files)\n", "│ └── lafan1/\n", "│ └── UnitreeG1/\n", "├── models/ # Trained models and checkpoints\n", "│ └── _/\n", "├── videos/ # Generated videos (.mp4, .gif)\n", "└── OpenTrack/ # Cloned repository\n", " ├── data/mocap -> ../datasets/ (symlink)\n", " └── logs -> ../models/ (symlink)\n", "```\n", "\n", "**Next Steps:**\n", "- All mocap data is in `/data/workspaces/opentrack/datasets/`\n", "- Run full training with GPU support\n", "- Test on rough terrain\n", "- Experiment with reference motion playback\n", "- Trained models are saved in `/data/workspaces/opentrack/models/`\n", "- Generated videos are in `/data/workspaces/opentrack/videos/`\n", "\n", "**Troubleshooting:**\n", "- If videos aren't generated, check that `--use_renderer` flag is used (not `--use_viewer`)\n", "- Ensure MuJoCo can run headless (may need `xvfb` on some systems)\n", "- Check `/data/workspaces/opentrack/models/` directory for experiment outputs\n", "- All data persists in `/data/workspaces/opentrack/` across notebook sessions" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.0" } }, "nbformat": 4, "nbformat_minor": 4 }