Skip to content
/ spz Public

SPZ file format handling for Rust and Python, and CLI tools.

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT
Notifications You must be signed in to change notification settings

Jackneill/spz

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

204 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

banner

SPZ

Rust implementation of the .SPZ file format and CLI tools.
Python and C bindings available.
 
WIP
 

Crates.io Version docs.rs lib.rs GitHub Tag
GitHub CI Deps GitHub Last Commit
CodSpeed CodeCov Codacy grade
Licenses FOSSA Status FOSSA Security
Python Version from PEP 621 TOML PyPI - Wheel


DeepWiki



Get it on Flathub Get it on Snapcraft Store

What is SPZ?

SPZ is a compressed file format for 3D Gaussian Splats, designed by Niantic. It provides efficient storage of Gaussian Splat data with configurable spherical harmonics degrees and coordinate system support.

About 10x smaller than the PLY equivalent with virtually no perceptible loss in visual quality.

See docs/SPZ_SPEC_v3.md for more information.

CLI

$ # install:
$ cargo install spz
$ # or
$ flatpak install io.github.jackneill.spz
$ # or
$ snap install spz
$
$ # run:
$ spz info assets/racoonfamily.spz
$ # or in container:
$ podman/docker run --rm -it -v "${PWD}:/app" -w /app spz \
$	info assets/racoonfamily.spz
GaussianSplat:
	Number of points:		932560
	Spherical harmonics degree:	3
	Antialiased:			true
	Median ellipsoid volume:	0.0000000046213082
	Bounding box:
		x: -281.779541 to 258.382568 (size 540.162109, center -11.698486)
		y: -240.000000 to 240.000000 (size 480.000000, center 0.000000)
		z: -240.000000 to 240.000000 (size 480.000000, center 0.000000)

Development

Open in GitHub Codespaces

Rust

Usage

[dependencies]
spz = { version = "0.0.7", default-features = false, features = [] }
use spz::prelude::*;

Examples

cargo run --example load_spz

Quick Start

// SPDX-License-Identifier: Apache-2.0 OR MIT

use std::path::PathBuf;

use anyhow::Result;
use spz::{
	coord::CoordinateSystem,
	gaussian_splat::GaussianSplat,
	gaussian_splat::{LoadOptions, SaveOptions},
	packed::PackedSpz,
};

fn main() -> Result<()> {
	let mut sample_spz = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
	sample_spz.push("assets/racoonfamily.spz");

	let gs = GaussianSplat::load(sample_spz.clone())?;

	let gs_cs = GaussianSplat::load_with(
		sample_spz,
		&LoadOptions::builder()
			.coord_sys(CoordinateSystem::LeftUpFront)
			.build(),
	)?;
	Ok(())
}

API

Outline Overview

  • This outline is non-exhaustive.
// SPDX-License-Identifier: Apache-2.0 OR MIT

// mod gaussian_splat ──────────────────────────────────────────────────────────

impl GaussianSplatBuilder {
	pub fn packed(self, packed: bool) -> Result<Self>;
	pub fn load_options(self, opts: LoadOptions) -> Self;

	pub fn load<P: AsRef<Path>>(self, filepath: P) -> Result<GaussianSplat>;
	pub async fn load_async<P: AsRef<Path>>(self, filepath: P) -> Result<GaussianSplat>;
}

#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, Arbitrary)]
pub struct GaussianSplat {
	pub header: Header,

	pub positions: Vec<f32>,	// flattened: [x0, y0, z0, x1, y1, z1, ...]
	pub scales: Vec<f32>,		// flattened: [x0, y0, z0, x1, y1, z1, ...], log-scale
	pub rotations: Vec<f32>,	// flattened: [x0, y0, z0, w0, x1, y1, z1, w1, ...]
	pub alphas: Vec<f32>,		// opacity (sigmoid-encoded)
	pub colors: Vec<f32>,		// flattened: [r0, g0, b0, r1, g1, b1, ...], DC color
	pub spherical_harmonics: Vec<f32>, // SH coefficients (degrees 1-3)
}

impl GaussianSplat {
	// Construction & Loading
	pub fn builder() -> GaussianSplatBuilder;

	// Load
	pub async fn load_with_into_buf_async<F: AsRef<Path>>(
		filepath: F,
		opts: &LoadOptions,
		contents: &mut Vec<u8>,
	) -> Result<Self>;
	pub async fn load_with_async<F: AsRef<Path>>(filepath: F, opts: &LoadOptions) -> Result<Self>;
	pub fn load_with<F: AsRef<Path>>(filepath: F, opts: &LoadOptions) -> Result<Self>;
	pub fn load<F: AsRef<Path>>(filepath: F) -> Result<Self>;
	pub async fn load_async<F: AsRef<Path>>(filepath: F) -> Result<Self>;
	// Save
	pub async fn save_async<F: AsRef<Path>>(&self, filepath: F, opts: &SaveOptions) -> Result<()>;
	pub fn save<F: AsRef<Path>>(&self, filepath: F, opts: &SaveOptions) -> Result<()>;

	pub fn new_from_packed_gaussians(pg: &PackedGaussians, opts: &UnpackOptions) -> Result<Self>;

	pub fn serialize_as_packed_bytes(&self, opts: &PackOptions) -> Result<Vec<u8>>;
	pub fn to_packed_gaussians(&self, opts: &PackOptions) -> Result<PackedGaussians>;

	// Transforms
	pub fn convert_coordinates(&mut self, src: CoordinateSystem, target: CoordinateSystem);

	// Introspection
	pub fn bbox(&self) -> BoundingBox;
	/// Compute median ellipsoid volume.
	pub fn median_volume(&self) -> f32;
	/// Validates that all internal arrays have consistent sizes.
	pub fn check_sizes(&self) -> bool;
}

impl LoadOptionsBuilder {
	pub fn coord_sys(mut self, coord_sys: CoordinateSystem) -> Self;
	pub fn build(self) -> LoadOptions;
}

impl SaveOptionsBuilder {
	pub fn coord_sys(mut self, coord_sys: CoordinateSystem) -> Self;
	pub fn build(self) -> SaveOptions;
}

pub struct BoundingBox {
	pub x_min: f32, pub x_max: f32,
	pub y_min: f32, pub y_max: f32,
	pub z_min: f32, pub z_max: f32,
}

impl BoundingBox {
	pub fn size(&self) -> (f32, f32, f32);	 // (width, height, depth)
	pub fn center(&self) -> (f32, f32, f32); // (x, y, z)
}

// mod coord ───────────────────────────────────────────────────────────────────

pub enum CoordinateSystem {
	Unspecified = 0,

	/* LDB */ LeftDownBack = 1,
	/* RDB */ RightDownBack = 2,
	/* LUB */ LeftUpBack = 3,
	/* RUB */ RightUpBack = 4,	// SPZ Internal, Three.js coordinate system
	/* LDF */ LeftDownFront = 5,
	/* RDF */ RightDownFront = 6, 	// PLY coordinate system
	/* LUF */ LeftUpFront = 7,	// GLB coordinate system
	/* RUF */ RightUpFront = 8,	// Unity coordinate system
}

// mod header ──────────────────────────────────────────────────────────────────

/// 16-bytes header.
#[repr(C)]
pub struct Header {
	pub magic: i32,				// 0x5053474e "NGSP"
	pub version: Version,			// 2 or 3
	pub num_points: i32,
	pub spherical_harmonics_degree: u8,	// 0-3
	pub fractional_bits: u8,		// 12 by default
	// Currently there is only 1 flag: 0x1 = antialiased
	pub flags: Flags,
	pub reserved: u8,			// Must be `0`.
}

impl Header {
	pub fn from_compressed_bytes<C: AsRef<[u8]>>(compressed: C) -> Result<Self>;
	pub fn from_file<P: AsRef<Path>>(filepath: P) -> Result<Self>;
	pub fn read_from<R: Read>(reader: &mut R) -> Result<Self>;
	pub fn serialize_to<W: Write>(&self, writer: &mut W) -> Result<()>;
	pub fn is_valid(&self) -> bool;
}
impl From<Header> for [u8; 16];
impl TryFrom<[u8; 16]> for Header;
impl TryFrom<&[u8]> for Header;

Tests

Pre-Requisites

Run

just test
just fuzz
just mutants

Benches

Pre-Requisites

  • cargo install cargo-criterion
  • Install gnuplot for html reports.

Run

just bench
  • The html report of the benchmark can be found under ./target/criterion/report/index.html.
  • View Benchmark and Profiling data on CodSpeed, (from CI runs).

Test Code Coverage

CodeCov Grid

Build

Pre-Requisites

Python

Usage

uvx pip install spz
# pyproject.toml

[project]
dependencies = [
    "spz",
]

Examples

import numpy as np
import spz

# Load from file
splat = spz.load("scene.spz")  # -> GaussianSplat
# or
splat = spz.GaussianSplat.load(
    "scene.spz", coordinate_system=spz.CoordinateSystem.RUB
)  # -> GaussianSplat
# or
with spz.SplatReader("scene.spz") as ctx:
    splat2 = ctx.splat  # -> GaussianSplat

with spz.temp_save(splat) as tmp_path:
    import subprocess

    subprocess.run(["viewer", str(tmp_path)])

# Access properties
print(f"{splat.num_points:,} points")
print(f"center: {splat.bbox.center}")
print(f"size: {splat.bbox.size}")

# Access data as numpy arrays
positions = splat.positions  # shape: (num_points, 3)
scales = splat.scales  # shape: (num_points, 3)
rotations = splat.rotations  # shape: (num_points, 4)
alphas = splat.alphas  # shape: (num_points,)
colors = splat.colors  # shape: (num_points, 3)
sh = splat.spherical_harmonics  # shape: (num_points, sh_dim * 3)

# Serialize
data = splat.to_bytes()  # -> bytes
splat2 = spz.GaussianSplat.from_bytes(data)  # -> GaussianSplat

# Create from numpy arrays
new_splat = spz.GaussianSplat(
    positions=np.zeros((2, 3), dtype=np.float32),
    scales=np.full((2, 3), -5.0, dtype=np.float32),
    rotations=np.tile([1.0, 0.0, 0.0, 0.0], (2, 1)).astype(np.float32),
    alphas=np.array([0.5, 0.8], dtype=np.float32),
    colors=np.array([[255.0, 0.0, 0.0], [0.0, 255.0, 0.0]], dtype=np.float32),
)  # -> GaussianSplat

# Save to file
new_splat.save("output.spz")

with spz.SplatWriter("output2.spz") as writer:
    writer.splat = splat2

# Coordinate conversion
with spz.modified_splat("scene.spz", "scene_converted.spz") as splat:
    splat.convert_coordinates(spz.CoordinateSystem.RUB, spz.CoordinateSystem.RDF)

C Bindings

Documentation

Overview

An SPZ file consists of:

  1. Outer compression: The entire payload is gzip-compressed.
  2. Inner binary data: A header followed by non-interleaved arrays of gaussian attributes.

File Structure

┌─────────────────────────────────────────┐
│           GZIP Compressed Data          │
│  ┌───────────────────────────────────┐  │
│  │         Header (16 bytes)         │  │
│  ├───────────────────────────────────┤  │
│  │         Positions Array           │  │
│  ├───────────────────────────────────┤  │
│  │          Alphas Array             │  │
│  ├───────────────────────────────────┤  │
│  │          Colors Array             │  │
│  ├───────────────────────────────────┤  │
│  │          Scales Array             │  │
│  ├───────────────────────────────────┤  │
│  │         Rotations Array           │  │
│  ├───────────────────────────────────┤  │
│  │    Spherical Harmonics Array      │  │
│  └───────────────────────────────────┘  │
└─────────────────────────────────────────┘
  • The data is organized by attribute (Structure of Arrays),
    • rather than by gaussian (Array of Structures) for better compression ratios.

License

Licensed under either of

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

FOSSA Scan