<div align="center">

# gsply

### Ultra-Fast Gaussian Splatting PLY I/O Library

[![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/downloads/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Tests](https://img.shields.io/badge/tests-passing-brightgreen.svg)](#testing)

**93M Gaussians/sec read | 57M Gaussians/sec write | Auto-optimized**

</div>

---

## What's New in v0.2.5

- **SOG Format Support**: `sogread()` - Read SOG (Splat Ordering Grid) format files
  - Returns `GSData` container (same as `plyread()`) for consistent API
  - In-memory ZIP extraction: Read directly from bytes without disk I/O
  - Uses `imagecodecs` (fastest WebP decoder) for optimal performance
  - Compatible with PlayCanvas splat-transform format
- **Object-Oriented I/O API**: Convenient save/load methods
  - `data.save(file_path, compressed=False)` - Instance method for saving GSData
  - `GSData.load(file_path)` - Classmethod for loading (auto-detects format)
  - `gstensor.save(file_path, compressed=True)` - GPU compression by default
  - `GSTensor.load(file_path, device='cuda')` - Direct GPU loading
- **Format Conversion API**: In-place operations for PLY format conversion
  - `normalize()` / `denormalize()` - Convert between linear and PLY formats
  - Available for both GSData (CPU) and GSTensor (GPU)
- **Color Conversion API**: In-place SH ↔ RGB conversion
  - `to_rgb()` / `to_sh()` - Convert sh0 between SH and RGB formats
  - Numba-optimized CPU and GPU-accelerated versions

**Previous versions:**
- v0.2.4: GPU I/O API (`plyread_gpu()`, `plywrite_gpu()`), GPU compression optimizations
- v0.2.2: Data Concatenation, GPU Concatenation, Performance Optimization, Mask Management

[Full API Reference](docs/API_REFERENCE.md) | [Changelog](docs/CHANGELOG.md)

---

## Quick Start

```python
from gsply import GSData, GSTensor

# Read PLY file (auto-detects format, zero-copy)
data = GSData.load("model.ply")  # Object-oriented API

# Or use functional API
from gsply import plyread
data = plyread("model.ply")

# Access fields
positions = data.means    # (N, 3) xyz coordinates
colors = data.sh0         # (N, 3) RGB colors
scales = data.scales      # (N, 3) scale parameters
rotations = data.quats    # (N, 4) quaternions

# Save PLY file (object-oriented API)
data.save("output.ply")  # Uncompressed
data.save("output.ply", compressed=True)  # Compressed (71-74% smaller)

# Or use functional API
from gsply import plywrite
plywrite("output.ply", data, compressed=True)

# GPU acceleration (optional)
gstensor = GSTensor.load("model.ply", device='cuda')  # Direct GPU loading
gstensor.save("output.compressed.ply")  # GPU compression (default)
```

**Performance:** 93M Gaussians/sec read, 57M Gaussians/sec write (400K Gaussians in 6-7ms)

[Installation](#installation) | [Examples](#examples) | [API Reference](docs/API_REFERENCE.md) | [Performance](#performance)

---

## Overview

Ultra-fast Gaussian Splatting PLY I/O for Python. Zero-copy reads, auto-optimized writes, optional GPU acceleration.

**Key Features:**
- Fast: 93M Gaussians/sec read, 57M Gaussians/sec write
- Auto-optimized: Writes are 2.6-2.8x faster automatically
- Pure Python: NumPy + Numba (no C++ compilation)
- Format support: Uncompressed PLY + PlayCanvas compressed (71-74% smaller) + SOG format
- Object-Oriented API: `data.save()`, `GSData.load()`, `gstensor.save()`, `GSTensor.load()`
- Format Conversion: `normalize()`, `denormalize()` for linear ↔ PLY format conversion
- Color Conversion: `to_rgb()`, `to_sh()` for SH ↔ RGB conversion
- GPU ready: Optional PyTorch integration with GSTensor

---

## Installation

### Basic Installation

```bash
pip install gsply
```

**Core dependencies:** NumPy and Numba (automatically installed)

### Optional Features

**GPU Acceleration (PyTorch):**
```bash
pip install torch  # For GSTensor GPU features
```

Enables:
- `GSTensor` - GPU-accelerated data container
- `plyread_gpu()`, `plywrite_gpu()` - Direct GPU I/O
- GPU compression/decompression (5-20x faster than CPU)
- GPU format conversion (`normalize()`, `denormalize()`, `to_rgb()`, `to_sh()`)

**SOG Format Support:**
```bash
pip install gsply[sogs]  # For SOG format support (sogread)
```

Enables:
- `sogread()` - Read SOG (Splat Ordering Grid) format files
- In-memory ZIP extraction
- PlayCanvas splat-transform compatibility

**Development:**
```bash
pip install -e .[dev]  # Install with dev dependencies (pytest, ruff, mypy)
```

### Installation Examples

```bash
# Basic installation (CPU only)
pip install gsply

# With GPU support
pip install gsply torch

# With SOG format support
pip install gsply[sogs]

# Full installation (GPU + SOG)
pip install gsply[sogs] torch

# Development installation
git clone https://github.com/OpsiClear/gsply.git
cd gsply
pip install -e .[dev]
```

---

## Examples

### Basic I/O

```python
from gsply import plyread, plywrite

# Read and access data
data = plyread("model.ply")
print(f"Loaded {len(data)} Gaussians")

# Unpack to individual arrays
means, scales, quats, opacities, sh0, shN = data.unpack()

# Write with individual arrays
plywrite("output.ply", means, scales, quats, opacities, sh0, shN)

# Or write directly from GSData
plywrite("output.ply", data)

# Or use object-oriented API
data.save("output.ply")  # Uncompressed
data.save("output.ply", compressed=True)  # Compressed

# Load using classmethod
data = GSData.load("scene.ply")  # Auto-detects format
```

### Format Detection

```python
from gsply import detect_format

is_compressed, sh_degree = detect_format("model.ply")
if is_compressed:
    print("Compressed PlayCanvas format")
else:
    print(f"Uncompressed format with SH degree {sh_degree}")
```

### In-Memory Compression

```python
from gsply import compress_to_bytes, decompress_from_bytes

# Compress for network transfer or storage
compressed_bytes = compress_to_bytes(data)

# Decompress from bytes
data_restored = decompress_from_bytes(compressed_bytes)
```

### GPU Acceleration

```python
from gsply import GSTensor, plyread_gpu, plywrite_gpu

# Direct GPU I/O (4x faster than CPU decompress + GPU transfer)
gstensor = plyread_gpu("model.compressed.ply", device='cuda')

# Access GPU tensors
positions_gpu = gstensor.means  # torch.Tensor on GPU
colors_gpu = gstensor.sh0       # torch.Tensor on GPU

# Filter on GPU
high_opacity = gstensor[gstensor.opacities > 0.5]

# Write back to compressed PLY (GPU compression)
plywrite_gpu("output.compressed.ply", gstensor)

# Or convert from CPU data
data = plyread("model.ply")
gstensor = GSTensor.from_gsdata(data, device='cuda')

# Or use object-oriented API
gstensor = GSTensor.load("model.ply", device='cuda')  # Auto-detects format
gstensor.save("output.compressed.ply")  # GPU compression (default)

# Convert back to CPU
data_cpu = gstensor.to_gsdata()
```

### Data Manipulation

```python
from gsply import GSData

# Slicing and indexing
subset = data[100:200]          # Slice
first = data[0]                 # Single Gaussian
filtered = data[data.opacities > 0.5]  # Boolean mask

# Concatenation
combined = data1.add(data2)     # Pairwise (1.9x faster)
combined = data1 + data2        # Or use + operator
merged = GSData.concatenate([data1, data2, data3])  # Bulk (6.15x faster)

# Optimize for faster operations
data = data.make_contiguous()   # 2-45x speedup for operations

# Copy and modify
bright = data.copy()
bright.sh0 *= 1.5  # Make brighter

# Format conversion before operations
data.denormalize()  # Convert to linear for easier manipulation
data.opacities = np.clip(data.opacities * 1.2, 0, 1)  # Modify in linear space
data.normalize()  # Convert back to PLY format before saving
data.save("modified.ply")
```

---

## Performance

### Benchmark Summary

**Uncompressed Format (400K Gaussians, SH0):**
- Read: 5.7ms (70M Gaussians/sec)
- Write: 19.3ms (21M Gaussians/sec)

**Compressed Format (400K Gaussians, SH0):**
- Read: 8.5ms (47M Gaussians/sec)
- Write: 15.0ms (27M Gaussians/sec)
- Size reduction: 71-74%

**Peak Performance:**
- Read: 78M Gaussians/sec (1M Gaussians, SH0, uncompressed)
- Write: 29M Gaussians/sec (100K Gaussians, SH0, compressed)

**GPU Transfer (400K Gaussians, RTX 3090 Ti):**
- With optimization: 1.99ms (11.4x faster)
- Without optimization: 22.78ms

See detailed [performance benchmarks](docs/API_REFERENCE.md#performance) for more information.

---

## Format Support

### Uncompressed PLY

Standard binary little-endian PLY format:

| SH Degree | Properties | Description |
|-----------|-----------|-------------|
| 0 | 14 | xyz, f_dc(3), opacity, scales(3), quats(4) |
| 1 | 23 | + 9 f_rest coefficients |
| 2 | 38 | + 24 f_rest coefficients |
| 3 | 59 | + 45 f_rest coefficients |

### Compressed PLY (PlayCanvas)

Chunk-based quantized format:
- Automatically saves as `.compressed.ply` when `compressed=True`
- Compression ratio: 71-74% size reduction
- Compatible with PlayCanvas, SuperSplat, other WebGL viewers
- Parallel compression/decompression

### SOG Format (Splat Ordering Grid) - Optional

WebP-based texture format for web deployment:
- Requires `gsply[sogs]` installation
- Uses WebP images for efficient storage
- Codebook-based compression for scales and colors
- Compatible with PlayCanvas splat-transform
- Supports both `.sog` ZIP bundles and folder formats
- **Returns GSData**: Same container as `plyread()` for consistent API
- **In-memory ZIP extraction**: Can read directly from bytes without disk I/O

```python
from gsply import sogread

# Read from file path - returns GSData (same as plyread)
data = sogread("model.sog")
positions = data.means  # Same API as GSData from plyread
colors = data.sh0

# Read from bytes (in-memory, no disk I/O)
with open("model.sog", "rb") as f:
    sog_bytes = f.read()
data = sogread(sog_bytes)  # Returns GSData - fully in-memory extraction and decoding
```

---

## API Reference

Complete API documentation is available in [docs/API_REFERENCE.md](docs/API_REFERENCE.md):

**Core I/O:**
- `plyread(file_path)` - Read PLY files
- `plywrite(file_path, ...)` - Write PLY files
- `detect_format(file_path)` - Detect format and SH degree
- `sogread(file_path | bytes)` - Read SOG files from path or bytes (requires `gsply[sogs]`)

**GSData Container:**
- `data.save(file_path, compressed=False)` - Save to PLY file
- `GSData.load(file_path)` - Load from PLY file (classmethod, auto-detects format)
- `data.unpack()` - Unpack to tuple
- `data.to_dict()` - Convert to dictionary
- `data.copy()` - Deep copy
- `data.consolidate()` - Optimize for slicing
- `data.normalize(inplace=True)` / `data.to_ply_format()` - Convert linear → PLY format (log/logit, modifies in-place by default)
- `data.denormalize(inplace=True)` / `data.from_ply_format()` / `data.to_linear()` - Convert PLY format → linear (modifies in-place by default)
- `data.to_rgb(inplace=True)` - Convert sh0 from SH format to RGB colors (Numba-optimized, modifies in-place by default)
- `data.to_sh(inplace=True)` - Convert sh0 from RGB format to SH coefficients (Numba-optimized, modifies in-place by default)
- `data[index]` - Indexing and slicing

**Compression:**
- `compress_to_bytes(data)` - Compress to bytes
- `compress_to_arrays(data)` - Compress to arrays
- `decompress_from_bytes(bytes)` - Decompress from bytes

**Utilities:**
- `sh2rgb(sh)` - SH to RGB conversion
- `rgb2sh(rgb)` - RGB to SH conversion
- `logit(x, eps=1e-6)` - Logit function (optimized CPU)
- `sigmoid(x)` - Sigmoid function (optimized CPU)
- `SH_C0` - Normalization constant

**GPU Support (PyTorch):**
- `GSTensor.load(file_path, device='cuda')` - Load from PLY file (classmethod, auto-detects format)
- `gstensor.save(file_path, compressed=True)` - Save to PLY file (GPU compression by default)
- `gstensor.save_compressed(file_path)` - Save compressed PLY (convenience alias)
- `GSTensor.from_gsdata(data, device='cuda')` - Convert to GPU
- `gstensor.to_gsdata()` - Convert to CPU
- `gstensor.normalize(inplace=True)` / `gstensor.to_ply_format()` - Convert linear → PLY format (GPU, modifies in-place by default)
- `gstensor.denormalize(inplace=True)` / `gstensor.from_ply_format()` / `gstensor.to_linear()` - Convert PLY format → linear (GPU, modifies in-place by default)
- `gstensor.to_rgb(inplace=True)` - Convert sh0 from SH format to RGB colors (GPU, modifies in-place by default)
- `gstensor.to_sh(inplace=True)` - Convert sh0 from RGB format to SH coefficients (GPU, modifies in-place by default)
- Device management: `.to()`, `.cpu()`, `.cuda()`
- Precision: `.half()`, `.float()`, `.double()`
- Full slicing and manipulation support

---

## Development

### Setup

```bash
# Clone repository
git clone https://github.com/OpsiClear/gsply.git
cd gsply

# Install in development mode
pip install -e .[dev]

# Run tests
pytest tests/ -v

# Run with coverage
pytest tests/ -v --cov=gsply --cov-report=html
```

### Project Structure

```
gsply/
├── src/gsply/          # Source code
│   ├── gsdata.py       # GSData dataclass
│   ├── reader.py       # PLY reading
│   ├── writer.py       # PLY writing
│   ├── formats.py      # Format detection
│   └── torch/          # PyTorch integration
│       └── gstensor.py # GSTensor GPU dataclass
├── tests/              # Unit tests (169 tests)
├── benchmarks/         # Performance benchmarks
├── docs/               # Documentation
└── pyproject.toml      # Package configuration
```

---

## Testing

gsply has comprehensive test coverage with 169 passing tests:

```bash
# Run all tests
pytest tests/ -v

# Run PyTorch tests (requires torch)
pytest tests/ -v -k "torch or gstensor"

# Run with coverage
pytest tests/ -v --cov=gsply --cov-report=html
```

---

## Benchmarking

Compare gsply performance against other PLY libraries:

```bash
# Install benchmark dependencies
pip install -e .[benchmark]

# Run benchmark
python benchmarks/benchmark.py

# Custom settings
python benchmarks/benchmark.py --config.file path/to/model.ply --config.iterations 20
```

---

## Documentation

- [API Reference](docs/API_REFERENCE.md) - Complete API documentation
- [Changelog](docs/CHANGELOG.md) - Version history and release notes
- [Contributing](.github/CONTRIBUTING.md) - Contribution guidelines

---

## CI/CD

Complete GitHub Actions pipeline:
- Multi-platform: Ubuntu, Windows, macOS
- Multi-version: Python 3.10, 3.11, 3.12, 3.13
- Core + PyTorch testing
- Automated benchmarking
- PyPI publishing on release

---

## Contributing

Contributions are welcome! Please see [.github/CONTRIBUTING.md](.github/CONTRIBUTING.md) for guidelines.

**Quick start:**
1. Fork the repository
2. Create a feature branch
3. Make your changes with tests
4. Run tests and benchmarks
5. Submit a pull request

---

## License

MIT License - see [LICENSE](LICENSE) file for details.

---

## Citation

If you use gsply in your research, please cite:

```bibtex
@software{gsply2024,
  author = {OpsiClear},
  title = {gsply: Ultra-Fast Gaussian Splatting PLY I/O},
  year = {2024},
  url = {https://github.com/OpsiClear/gsply}
}
```

---

## Related Projects

- **gsplat**: CUDA-accelerated Gaussian Splatting rasterizer
- **nerfstudio**: NeRF training framework with Gaussian Splatting support
- **PlayCanvas SuperSplat**: Web-based Gaussian Splatting viewer
- **3D Gaussian Splatting**: Original paper and implementation

---

<div align="center">

**Made with Python and numpy**

[Report Bug](https://github.com/OpsiClear/gsply/issues) | [Request Feature](https://github.com/OpsiClear/gsply/issues) | [Documentation](docs/API_REFERENCE.md)

</div>
