|
| 1 | +## Python Bindings |
| 2 | + |
| 3 | +### API Overview |
| 4 | + |
| 5 | +The Python API closely mirrors the C++ API with additional Python-friendly features: |
| 6 | + |
| 7 | +- **Automatic type conversion**: Accepts numpy arrays with various dtypes (int32, float64, etc.) and converts them to float32 |
| 8 | +- **Memory safety**: All data is safely copied between Python and C++ |
| 9 | + |
| 10 | +### Basic Usage Examples |
| 11 | + |
| 12 | +#### Loading and Saving SPZ Files |
| 13 | + |
| 14 | +```python |
| 15 | +import spz |
| 16 | + |
| 17 | +# Load a .spz file |
| 18 | +cloud = spz.load_spz("samples/hornedlizard.spz") |
| 19 | +print(f"Loaded {cloud.num_points} gaussians with SH degree {cloud.sh_degree}") |
| 20 | + |
| 21 | +# Save to a new file |
| 22 | +options = spz.PackOptions() |
| 23 | +options.from_coord = spz.CoordinateSystem.RUB |
| 24 | +spz.save_spz(cloud, options, "output.spz") |
| 25 | +``` |
| 26 | + |
| 27 | +#### Converting PLY to SPZ |
| 28 | + |
| 29 | +```python |
| 30 | +import spz |
| 31 | + |
| 32 | +# Load a PLY file and convert to compressed SPZ format |
| 33 | +# PLY files typically use RDF coordinates (right-handed, Y-down, Z-forward) |
| 34 | +unpack_options = spz.UnpackOptions() |
| 35 | +unpack_options.to_coord = spz.CoordinateSystem.RUB # Convert to RUB for Three.js compatibility |
| 36 | + |
| 37 | +# Load the PLY file |
| 38 | +cloud = spz.load_splat_from_ply("input.ply", unpack_options) |
| 39 | +print(f"Loaded {cloud.num_points} gaussians from PLY") |
| 40 | + |
| 41 | +# Save as compressed SPZ format |
| 42 | +pack_options = spz.PackOptions() |
| 43 | +pack_options.from_coord = spz.CoordinateSystem.RUB # Data is now in RUB coordinates |
| 44 | +spz.save_spz(cloud, pack_options, "output.spz") |
| 45 | + |
| 46 | +# Check compression ratio |
| 47 | +import os |
| 48 | +ply_size = os.path.getsize("input.ply") |
| 49 | +spz_size = os.path.getsize("output.spz") |
| 50 | +compression_ratio = ply_size / spz_size |
| 51 | +print(f"Compression ratio: {compression_ratio:.1f}x smaller ({ply_size} → {spz_size} bytes)") |
| 52 | +``` |
| 53 | + |
| 54 | +#### Creating and Manipulating Gaussian Clouds |
| 55 | + |
| 56 | +```python |
| 57 | +import spz |
| 58 | +import numpy as np |
| 59 | + |
| 60 | +# Create a new Gaussian cloud |
| 61 | +cloud = spz.GaussianCloud() |
| 62 | +cloud.sh_degree = 1 |
| 63 | +cloud.antialiased = True |
| 64 | + |
| 65 | +# Choose point count for data you will assign |
| 66 | +num_points = 100 |
| 67 | + |
| 68 | +# Set positions first (defines num_points) |
| 69 | +positions = np.random.randn(num_points * 3).astype(np.float32) |
| 70 | +cloud.positions = positions |
| 71 | +assert cloud.num_points == num_points # num_points is derived, read‑only |
| 72 | + |
| 73 | +# Set scales (3 floats per point: log-scale factors) |
| 74 | +scales = np.random.randn(num_points * 3).astype(np.float32) |
| 75 | +cloud.scales = scales |
| 76 | + |
| 77 | +# Set rotations (4 floats per point: quaternion x, y, z, w) |
| 78 | +rotations = np.random.randn(num_points * 4).astype(np.float32) |
| 79 | +cloud.rotations = rotations |
| 80 | + |
| 81 | +# Set alphas (1 float per point: opacity before sigmoid) |
| 82 | +alphas = np.random.randn(num_points).astype(np.float32) |
| 83 | +cloud.alphas = alphas |
| 84 | + |
| 85 | +# Set colors (3 floats per point: RGB) |
| 86 | +colors = np.random.rand(num_points * 3).astype(np.float32) |
| 87 | +cloud.colors = colors |
| 88 | + |
| 89 | +# Set spherical harmonics (9 floats per point for degree 1) |
| 90 | +sh_coeffs = np.random.randn(num_points * 9).astype(np.float32) |
| 91 | +cloud.sh = sh_coeffs |
| 92 | + |
| 93 | +# Calculate median volume |
| 94 | +median_vol = cloud.median_volume() |
| 95 | +print(f"Median volume: {median_vol}") |
| 96 | + |
| 97 | +# Apply coordinate transformation |
| 98 | +cloud.rotate_180_deg_about_x() # Converts between RUB and RDF coordinates |
| 99 | +``` |
| 100 | + |
| 101 | +#### Coordinate System Conversions |
| 102 | + |
| 103 | +```python |
| 104 | +import spz |
| 105 | + |
| 106 | +# All available coordinate systems |
| 107 | +print("Available coordinate systems:") |
| 108 | +for coord_sys in [spz.CoordinateSystem.UNSPECIFIED, spz.CoordinateSystem.LDB, |
| 109 | + spz.CoordinateSystem.RDB, spz.CoordinateSystem.LUB, |
| 110 | + spz.CoordinateSystem.RUB, spz.CoordinateSystem.LDF, |
| 111 | + spz.CoordinateSystem.RDF, spz.CoordinateSystem.LUF, |
| 112 | + spz.CoordinateSystem.RUF]: |
| 113 | + print(f" {coord_sys}") |
| 114 | + |
| 115 | +# Load PLY (typically RDF) and convert to Unity coordinates (RUF) |
| 116 | +unpack_options = spz.UnpackOptions() |
| 117 | +unpack_options.to_coord = spz.CoordinateSystem.RUF |
| 118 | +cloud = spz.load_splat_from_ply("ply_file.ply", unpack_options) |
| 119 | + |
| 120 | +# Save for Three.js (RUB coordinates) |
| 121 | +pack_options = spz.PackOptions() |
| 122 | +pack_options.from_coord = spz.CoordinateSystem.RUF # Current data is in RUF |
| 123 | +spz.save_spz(cloud, pack_options, "threejs_output.spz") # Will be converted to RUB internally |
| 124 | +``` |
| 125 | + |
| 126 | +#### In-place conversions on an existing GaussianCloud |
| 127 | + |
| 128 | +```python |
| 129 | +import spz |
| 130 | +import numpy as np |
| 131 | + |
| 132 | +cloud = spz.GaussianCloud() |
| 133 | +cloud.positions = np.array([1.0, 2.0, 3.0], dtype=np.float32) |
| 134 | +cloud.rotations = np.array([0.1, 0.2, 0.3, 0.9], dtype=np.float32) |
| 135 | + |
| 136 | +# Convert from RUB to RDF (flips Y and Z axes) |
| 137 | +cloud.convert_coordinates(spz.CoordinateSystem.RUB, spz.CoordinateSystem.RDF) |
| 138 | + |
| 139 | +# Convert back to RUB |
| 140 | +cloud.convert_coordinates(spz.CoordinateSystem.RDF, spz.CoordinateSystem.RUB) |
| 141 | +``` |
| 142 | + |
| 143 | +#### Advanced Usage with NumPy Integration |
| 144 | + |
| 145 | +```python |
| 146 | +import spz |
| 147 | +import numpy as np |
| 148 | + |
| 149 | +# Load existing cloud |
| 150 | +cloud = spz.load_spz("samples/hornedlizard.spz") |
| 151 | + |
| 152 | +# Access data as NumPy arrays (always returns float32) |
| 153 | +positions = cloud.positions # Shape: (num_points * 3,) |
| 154 | +scales = cloud.scales # Shape: (num_points * 3,) |
| 155 | +rotations = cloud.rotations # Shape: (num_points * 4,) |
| 156 | +alphas = cloud.alphas # Shape: (num_points,) |
| 157 | +colors = cloud.colors # Shape: (num_points * 3,) |
| 158 | +sh_coeffs = cloud.sh # Shape: (num_points * sh_coeffs_per_point,) |
| 159 | + |
| 160 | +# Reshape for easier manipulation |
| 161 | +positions_3d = positions.reshape(-1, 3) # Shape: (num_points, 3) |
| 162 | +colors_rgb = colors.reshape(-1, 3) # Shape: (num_points, 3) |
| 163 | + |
| 164 | +# Modify data |
| 165 | +positions_3d[:, 2] += 1.0 # Move all points up by 1 unit in Z |
| 166 | +colors_rgb[:, 0] *= 0.5 # Reduce red channel by half |
| 167 | + |
| 168 | +# Update the cloud (automatic type conversion) |
| 169 | +cloud.positions = positions_3d.flatten() |
| 170 | +cloud.colors = colors_rgb.flatten() |
| 171 | + |
| 172 | +# Save modified cloud |
| 173 | +spz.save_spz(cloud, spz.PackOptions(), "modified.spz") |
| 174 | +``` |
| 175 | + |
| 176 | +### Python API invariants and validations |
| 177 | + |
| 178 | +The Python bindings enforce consistency across fields and provide clear errors: |
| 179 | + |
| 180 | +- num_points is read‑only and derived from positions.size() / 3. |
| 181 | + - Set positions first to establish the point count. |
| 182 | +- positions: length must be a multiple of 3. Setting positions updates num_points. |
| 183 | +- scales: length must be a multiple of 3; if num_points > 0, length must equal num_points * 3. |
| 184 | +- rotations: length must be a multiple of 4; if num_points > 0, length must equal num_points * 4. |
| 185 | +- alphas: if num_points > 0, length must equal num_points. |
| 186 | +- colors: length must be a multiple of 3; if num_points > 0, length must equal num_points * 3. |
| 187 | +- sh_degree: must be in [0, 3]. Set sh_degree before assigning sh. |
| 188 | +- sh: |
| 189 | + - If sh_degree == 0, sh must be empty. |
| 190 | + - Otherwise, length must be a multiple of (((sh_degree + 1)^2 − 1) * 3). |
| 191 | + - If num_points > 0, length must equal num_points * (((sh_degree + 1)^2 − 1) * 3). |
| 192 | +- Dtypes: numeric arrays are accepted and converted to float32; non‑numeric arrays raise TypeError. |
| 193 | +- All arrays must be C‑contiguous; non‑contiguous inputs will be copied by nanobind. |
| 194 | + |
| 195 | +### Data Layout |
| 196 | + |
| 197 | +The Python bindings maintain the same data layout as the C++ library: |
| 198 | + |
| 199 | +- **Positions**: `[x1, y1, z1, x2, y2, z2, ...]` |
| 200 | +- **Scales**: `[sx1, sy1, sz1, sx2, sy2, sz2, ...]` (log-scale) |
| 201 | +- **Rotations**: `[x1, y1, z1, w1, x2, y2, z2, w2, ...]` (quaternions) |
| 202 | +- **Alphas**: `[a1, a2, a3, ...]` (before sigmoid activation) |
| 203 | +- **Colors**: `[r1, g1, b1, r2, g2, b2, ...]` (base RGB) |
| 204 | +- **Spherical Harmonics**: Coefficient-major order, e.g., for degree 1: |
| 205 | + `[sh1n1_r, sh1n1_g, sh1n1_b, sh10_r, sh10_g, sh10_b, sh1p1_r, sh1p1_g, sh1p1_b, ...]` |
| 206 | + |
| 207 | +### Type Safety |
| 208 | + |
| 209 | +The Python bindings provide automatic type conversion while maintaining safety: |
| 210 | + |
| 211 | +- ✅ **Accepts**: `int32`, `float64`, `uint8`, etc. → automatically converts to `float32` |
| 212 | +- ❌ **Rejects**: `string`, `complex`, `object` arrays → raises `TypeError` |
| 213 | +- 🔄 **Preserves**: `float32` arrays → no conversion needed |
| 214 | + |
| 215 | +### Testing and Development |
| 216 | + |
| 217 | +The Python bindings include a comprehensive test suite that covers all API functionality: |
| 218 | + |
| 219 | +```bash |
| 220 | + |
| 221 | +# Install pytest and scipy |
| 222 | +pip install pytest scipy |
| 223 | + |
| 224 | +# Run the test suite |
| 225 | +python -m pytest tests/python/ |
| 226 | + |
| 227 | +### Requirements |
| 228 | + |
| 229 | +- Python 3.8+ |
| 230 | +- NumPy (automatically installed) |
| 231 | +- For development: pytest, scipy (for testing) |
0 commit comments