Skip to content

Jackneill/spz

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

SPZ

Rust and Python implementation of the .SPZ file format (v3) and CLI tools.
 
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



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.

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::PackedGaussians,
};

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

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

	let pg = gs.to_packed_gaussians(
		&SaveOptions::builder()
			.coord_sys(CoordinateSystem::RightUpBack) // packed will be in RUB (OpenGL)
			.build(),
	)?;
	let bytes = pg.as_bytes_vec()?;
	let pg2 = PackedGaussians::from_bytes(bytes.as_slice())?;
	let _gs2 = GaussianSplat::new_from_packed_gaussians(
		&pg2,
		&LoadOptions::builder()
			.coord_sys(CoordinateSystem::LeftUpFront) // _gs2 will be in LUF (glTF)
			.build(),
	)?;
	Ok(())
}

API

Outline Overview

  • This outline is non-exhaustive.
// mod gaussian_splat ──────────────────────────────────────────────────────────

pub struct GaussianSplatBuilder { /* ... */ }

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 num_points: i32,
	pub spherical_harmonics_degree: i32, // 0-3
	pub antialiased: bool,
	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;
	pub fn load_packed_from_file<F: AsRef<Path>>(filepath: F, opts: &UnpackOptions) -> Result<Self>;
	pub fn load_packed<D: AsRef<[u8]>>(data: D) -> Result<PackedGaussians>;
	pub fn new_from_packed_gaussians(packed: &PackedGaussians, opts: &UnpackOptions) -> Result<Self>;

	// Serialization
	pub fn save_as_packed<F: AsRef<Path>>(&self, filepath: F, opts: &PackOptions) -> Result<()>;
	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, from: CoordinateSystem, to: 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;
}

pub struct LoadOptions {
	pub coord_sys: CoordinateSystem,
}

impl LoadOptions {
	pub fn builder() -> LoadOptionsBuilder;
}

pub struct LoadOptionsBuilder {
	coord_sys: CoordinateSystem,
}

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

pub struct SaveOptions {
	pub coord_sys: CoordinateSystem,
}

impl SaveOptions {
	pub fn builder() -> SaveOptionsBuilder;
}

pub struct SaveOptionsBuilder {
	coord_sys: CoordinateSystem,
}

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
}

impl CoordinateSystem {
	/// Computes the axis flip multipliers needed to convert from `self` to `target`.
	pub fn axis_flips_to(self, target: CoordinateSystem) -> AxisFlips;
	/// Compares axis orientations between two coordinate systems.
	pub fn axes_align(self, other: CoordinateSystem) -> (bool, bool, bool);
	/// Returns a short 3-letter abbreviation for the coordinate system.
	pub fn as_short_str(&self) -> &'static str;
	/// Returns an iterator over all coordinate system variants.
	pub fn iter() -> impl Iterator<Item = CoordinateSystem>;
}

/// Sign multipliers (+1.0 or -1.0) for transforming Gaussian splat data between
/// coordinate systems.
pub struct AxisFlips {
	/// Sign multipliers for XYZ position coordinates.
	pub position: [f32; 3],
	/// Sign multipliers for quaternion rotation components (X, Y, Z; W is unchanged).
	pub rotation: [f32; 3],
	/// Sign multipliers for spherical harmonic coefficients (15 values for degrees 1-3).
	pub spherical_harmonics: [f32; 15],
}

// mod packed ──────────────────────────────────────────────────────────────────

/// Represents a full splat with lower precision.
pub struct PackedGaussians {
	// Total number of points (gaussians).
	pub num_points: i32,
	// Degree of spherical harmonics.
	pub sh_degree: i32,
	// Number of bits used for fractional part of fixed-point coords.
	pub fractional_bits: i32,
	// Whether gaussians should be rendered with mip-splat antialiasing.
	pub antialiased: bool,
	// Whether gaussians use the smallest three method to store quaternions.
	pub uses_quaternion_smallest_three: bool,

	pub positions: Vec<u8>,
	pub scales: Vec<u8>,
	pub rotations: Vec<u8>,
	pub alphas: Vec<u8>,
	pub colors: Vec<u8>,
	pub spherical_harmonics: Vec<u8>,
}

impl PackedGaussians {
	/// Constructs an SPZ header from this packed data's metadata.
	pub fn construct_header(&self) -> PackedGaussiansHeader;
	/// Serializes to a complete SPZ file as a byte vector.
	pub fn as_bytes_vec(&self) -> Result<Vec<u8>>;
	/// Writes this packed data to a writer in SPZ format.
	pub fn write_self_to<W>(&self, stream: &mut W) -> Result<()>;
	/// Returns `true` if positions are stored as float16.
	pub fn uses_float16(&self) -> bool;
	/// Returns the packed data for a single splat at index `i`.
	pub fn at(&self, i: usize) -> Result<PackedGaussian>;
	/// Unpacks a single splat at index `i` with coordinate transformation.
	pub fn unpack(&self, i: usize, coord_flip: &AxisFlips) -> Result<UnpackedGaussian>;
	/// Validates that all internal arrays have the expected sizes.
	pub fn check_sizes(&self, num_points: usize, sh_dim: u8, uses_float16: bool) -> bool;
}

impl TryFrom<Vec<u8>> for PackedGaussians;
impl TryFrom<&[u8]> for PackedGaussians;

/// Represents a single low precision gaussian.
pub struct PackedGaussian {
	pub position: [u8; 9],
	pub rotation: [u8; 4],
	pub scale: [u8; 3],
	pub color: [u8; 3],
	pub alpha: u8,
	pub sh_r: [u8; 15],
	pub sh_g: [u8; 15],
	pub sh_b: [u8; 15],
}

impl PackedGaussian {
	/// Decompresses this packed Gaussian into full-precision values,
	/// [`UnpackedGaussian`].
	pub fn unpack(
		&self,
		uses_float16: bool,
		uses_quaternion_smallest_three: bool,
		fractional_bits: i32,
		coord_flip: &AxisFlips,
	) -> Result<UnpackedGaussian>;
}

// mod unpacked ────────────────────────────────────────────────────────────────

/// Represents a single inflated gaussian.
pub struct UnpackedGaussian {
	pub position: [f32; 3], // x, y, z
	pub rotation: [f32; 4], // x, y, z, w
	pub scale: [f32; 3],	// std::log(scale)
	pub color: [f32; 3],	// rgb sh0 encoding
	pub alpha: f32,		// inverse logistic
	pub sh_r: [f32; 15],
	pub sh_g: [f32; 15],
	pub sh_b: [f32; 15],
}

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

/// Fixed-size 16-byte header for SPZ (packed Gaussian splat) files.
#[repr(C)]
pub struct PackedGaussiansHeader {
	pub magic: i32,				// 0x5053474e "NGSP"
	pub version: i32,			// 3 (this lib only supports spz v3)
	pub num_points: i32,
	pub spherical_harmonics_degree: u8,	// 0-3
	pub fractional_bits: u8,
	pub flags: u8,				// 0x1 = antialiased
	pub reserved: u8,
}

impl PackedGaussiansHeader {
	/// Reads a header from the given reader.
	pub fn read_from<R: Read>(reader: &mut R) -> Result<Self>;
	/// Writes this header to the given writer.
	pub fn serialize_to<W: Write>(&self, writer: &mut W) -> Result<()>;
}

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

Further documentation is available under ./docs.

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