Current section
Files
Jump to
Current section
Files
livebooks/06_spatial_codecs.livemd
# Spatial Codecs: Point Clouds and Gaussian Splats
```elixir
# Use this install to work with the source code
# Mix.install(
# [
# {:ex_codecs, path: Path.join(__DIR__, "..")},
# {:rustler, "~> 0.36"}
# ],
# config: [rustler_precompiled: [force_build: [ex_codecs: true]]]
# )
Mix.install([
{:ex_codecs, "~> 0.2.3"}
])
```
## Series
| # | Livebook |
| --- | -------------------------------------------------------------- |
| 01 | [Introduction](01_introduction.livemd) |
| 02 | [Compression Fundamentals](02_compression_fundamentals.livemd) |
| 03 | [Codec Comparison](03_codec_comparison.livemd) |
| 04 | [Building Storage Systems](04_building_storage_systems.livemd) |
| 05 | [Zarr-Style Workloads](05_zarr_style_workloads.livemd) |
| 06 | **Spatial Codecs** (you are here) |
## One Framework, Specialized Category APIs
Compression codecs and spatial formats share discovery metadata and
`%ExCodecs.Error{}` results. Their operation APIs differ because compression
maps binary to binary, while spatial codecs map domain structs to formats.
```elixir
ExCodecs.available_codecs(:spatial)
```
```elixir
for format <- ExCodecs.Spatial.available_formats() do
{:ok, info} = ExCodecs.codec_info(format)
%{
format: info.name,
interface: info.interface,
configurable?: info.configurable?,
version: info.version
}
end
```
## Build a Point Cloud
`Point.new/4` stores coordinates as floats and normalizes attribute keys to
strings. `PointCloud.new/2` computes axis-aligned bounds by default.
```elixir
alias ExCodecs.Spatial.{Gaussian, GaussianCloud, Point, PointCloud}
points = [
Point.new(0, 0, 0,
color: {255, 32, 32},
normal: {0.0, 0.0, 1.0},
attributes: %{intensity: 0.25}
),
Point.new(1, 0, 0,
color: {32, 255, 32},
normal: {0.0, 0.0, 1.0},
attributes: %{intensity: 0.5}
),
Point.new(0, 1, 0,
color: {32, 32, 255},
normal: {0.0, 0.0, 1.0},
attributes: %{intensity: 0.75}
)
]
cloud = PointCloud.new(points)
%{
points: PointCloud.size(cloud),
bounds: cloud.bounds,
attribute_keys: cloud.points |> hd() |> Map.fetch!(:attributes) |> Map.keys()
}
```
## PLY Interchange
PLY supports ASCII and binary little-/big-endian representations. The default
is binary little-endian.
```elixir
{:ok, ply_ascii} =
ExCodecs.Spatial.encode(cloud,
format: :ply,
ply_format: :ascii,
comments: ["created by the ExCodecs spatial Livebook"]
)
ply_ascii
|> String.split("\n")
|> Enum.take(14)
|> Enum.join("\n")
```
```elixir
{:ok, ply_binary} = ExCodecs.Spatial.encode(cloud, format: :ply)
{:ok, decoded_ply} = ExCodecs.Spatial.decode(ply_binary, format: :ply)
%{
encoded_bytes: byte_size(ply_binary),
decoded_points: PointCloud.size(decoded_ply),
first_point: hd(decoded_ply.points)
}
```
## Compact Point-Cloud Binary
`:spatial_binary` uses the versioned ExCodecs point format (`EXCP`). It is
intended for compact ExCodecs-to-ExCodecs interchange.
```elixir
{:ok, excp} = ExCodecs.Spatial.encode(cloud, format: :spatial_binary)
{:ok, decoded_excp} = ExCodecs.Spatial.decode(excp, format: :spatial_binary)
%{
magic: binary_part(excp, 0, 4),
encoded_bytes: byte_size(excp),
decoded_points: PointCloud.size(decoded_excp)
}
```
## Spatial Encoding Plus Compression
Spatial encoding chooses a geometry format. Compression is a separate,
composable binary step.
```elixir
{:ok, compressed_excp} = ExCodecs.encode(:zstd, excp, level: 7)
{:ok, restored_excp} = ExCodecs.decode(:zstd, compressed_excp)
{:ok, restored_cloud} =
ExCodecs.Spatial.decode(restored_excp, format: :spatial_binary)
%{
excp_bytes: byte_size(excp),
compressed_bytes: byte_size(compressed_excp),
round_trip_points: PointCloud.size(restored_cloud)
}
```
## Gaussian Splats
`Gaussian` uses scalar-first quaternions (`{w, x, y, z}`). `:gsplat` is the
versioned ExCodecs Gaussian format (`GSPL`).
```elixir
gaussian_cloud =
GaussianCloud.new([
Gaussian.new({0, 0, 0},
scale: {0.1, 0.2, 0.1},
opacity: 0.9,
color: {1.0, 0.2, 0.1}
),
Gaussian.new({1, 0.5, -0.25},
rotation: {0.9239, 0.0, 0.3827, 0.0},
scale: {0.25, 0.1, 0.15},
opacity: 0.65,
color: {0.1, 0.4, 1.0}
)
])
{:ok, gspl} = ExCodecs.Spatial.encode(gaussian_cloud, format: :gsplat)
{:ok, decoded_gaussians} = ExCodecs.Spatial.decode(gspl, format: :gsplat)
%{
magic: binary_part(gspl, 0, 4),
encoded_bytes: byte_size(gspl),
decoded_gaussians: GaussianCloud.size(decoded_gaussians)
}
```
## Enumerable Helpers
`stream_decode/2` with `source: :file` reads one record/vertex at a time
(bounded memory; mmap + Rust unpack when available). `stream_encode/2` still
collects the enumerable, then encodes once — use
`ExCodecs.Spatial.Stream.encode_to_file/3` with an explicit `:schema` for
EXCP/GSPL incremental writes.
```elixir
{:ok, streamed_payload} =
ExCodecs.Spatial.stream_encode(points, format: :spatial_binary)
streamed_points =
streamed_payload
|> ExCodecs.Spatial.stream_decode(format: :spatial_binary, source: :binary)
|> Enum.to_list()
length(streamed_points)
```
```elixir
path = Path.join(System.tmp_dir!(), "ex_codecs_spatial_#{System.unique_integer([:positive])}.excp")
:ok =
ExCodecs.Spatial.Stream.encode_to_file(points, path,
format: :spatial_binary,
schema: []
)
file_points =
path
|> ExCodecs.Spatial.stream_decode(format: :spatial_binary, source: :file)
|> Enum.to_list()
File.rm(path)
length(file_points)
```
## Category-Safe Dispatch
The shared catalog does not overload the binary API with struct inputs.
Choosing a spatial catalog entry through `ExCodecs.encode/3` returns guidance
to use the spatial category API.
```elixir
{:error, error} = ExCodecs.encode(:ply, ply_binary)
%{reason: error.reason, message: error.message}
```
## Further Reading
* [Understanding Spatial Codecs](https://hexdocs.pm/ex_codecs/understanding_spatial_codecs.html) — schema and format trade-offs
* [Spatial Wire Formats](https://hexdocs.pm/ex_codecs/spatial_formats.html) — frozen EXCP / GSPL / PLY layouts
* Add Zstd after spatial encoding when storage or transfer size matters.
## Navigation
**Previous:** [Zarr-Style Workloads](05_zarr_style_workloads.livemd)