paper_3d__utonia
Utonia trains a single self-supervised Point Transformer V3 (PTv3) encoder across five diverse point cloud domains (remote sensing, outdoor LiDAR, indoor RGB-D, object CAD, RGB-video-lifted), producing universal 3D representations that transfer to segmentation, robotic manipulation, and multimodal reasoning.
Paper Info
Method Overview
Utonia is a cross-domain pre-trained PTv3 encoder that harmonizes point clouds with distinct sensing geometries, densities, and modality priors. The pipeline is:
- Data preparation: Raw point clouds (coord, color, normal) are normalized and grid-sampled at 0.01m resolution. A domain-dependent scale parameter controls Perceptual Granularity Rescale -- the key mechanism for unifying different point cloud densities.
- Serialization: Grid coordinates are serialized via z-order or Hilbert curves to create 1D sequences for efficient attention.
- Encoder: A 5-stage hierarchical PTv3 with serialized patch attention, 3D RoPE positional encoding, and sparse convolution CPE. Stages are connected by GridPooling (stride-2 downsampling).
- Output: The encoder produces a hierarchical
Point object; features can be unpooled back to the original resolution via pooling_inverse and pooling_parent chains.
Key insight: A single encoder can learn universal 3D representations across wildly different domains by rescaling point cloud granularity through a per-domain scale factor applied before grid sampling.
Scale / Domain Table (CRITICAL)
The scale parameter in the transform pipeline controls the effective voxel resolution. Choosing the right scale per domain is essential:
| Domain |
scale |
apply_z_positive |
normalize_coord |
Notes |
| Indoor scene (ScanNet) |
0.5 |
True |
False |
Default for rooms |
| Robotic manipulation |
4.0 |
True |
False |
Tabletop scenes need finer grain |
| Outdoor LiDAR |
0.2 |
False |
False |
Keep ego at origin, road on xy-plane |
| Object-centric (CAD) |
1.0 (default) |
True |
True |
Enable NormalizeCoord for objects |
| Hong Kong scene |
0.5 |
True |
False |
Similar to indoor |
Paper-Code Mapping
| Paper Concept |
Code Location |
Notes |
| PTv3 Encoder |
utonia/model.py:PointTransformerV3 |
Main model class, also PyTorchModelHubMixin |
| 3D RoPE |
utonia/model.py:Point3DRoPE |
Splits head_dim into 3 chunks for x,y,z rotation; head_dim must be divisible by 3 |
| Serialized Attention |
utonia/model.py:SerializedAttention |
Patch-based attention with flash_attn varlen support |
| Transformer Block |
utonia/model.py:Block |
CPE (SubMConv3d) + SerializedAttention + MLP with DropPath |
| Grid Pooling (downsampling) |
utonia/model.py:GridPooling |
stride-2, max-reduce, traceable for unpooling |
| Grid Unpooling |
utonia/model.py:GridUnpooling |
Skip-connection based upsampling (decoder only) |
| Embedding |
utonia/model.py:Embedding |
Linear(in_channels -> enc_channels[0]) + LN + GELU, optional mask token |
| Point data structure |
utonia/structure.py:Point |
addict.Dict subclass with serialization() and sparsify() methods |
| Serialization (z-order, Hilbert) |
utonia/serialization/default.py:encode |
Orders: "z", "z-trans", "hilbert", "hilbert-trans" |
| Perceptual Granularity Rescale |
utonia/transform.py:RandomScale + GridSample |
scale param controls effective resolution |
| Transform pipeline |
utonia/transform.py:Compose, default() |
Configurable transform chain via Registry |
| Data loading |
utonia/data.py:load |
Loads sample .npz from HuggingFace pointcept/demo |
| Batch collation |
utonia/data.py:collate_fn |
Handles offset-based batching for variable-size point clouds |
| Model loading |
utonia/model.py:load |
Downloads from HuggingFace Pointcept/Utonia, builds model from stored config |
| Linear probe head |
demo/2_sem_seg.py:SegHead |
Simple nn.Linear for ScanNet 20-class segmentation |
| RPE (relative pos enc) |
utonia/model.py:RPE |
Table-based RPE (disabled when flash_attn enabled) |
| LayerScale |
utonia/model.py:LayerScale |
Per-channel learnable scaling (optional, controlled by layer_scale param) |
Setup
Dependencies
- Python 3.10+
- PyTorch 2.5.0+ with CUDA 12.4+
- FlashAttention (strongly recommended, required for default config)
- Key packages:
spconv-cu124, torch-scatter, flash-attn, huggingface_hub, timm, addict, scipy
- Demo extras:
open3d (requires numpy<=1.26.4), fast_pytorch_kmeans, psutil, camtools, trimesh, gradio, natsort, opencv-python
Installation
Standalone mode (conda env with everything):
git clone https://github.com/Pointcept/Utonia.git
cd Utonia
# unset CUDA_PATH if you have local CUDA installed
conda env create -f environment.yml --verbose
conda activate utonia
Package mode (inject into existing env):
# CUDA_VERSION e.g. 124, TORCH_VERSION e.g. 2.5.0
pip install spconv-cu${CUDA_VERSION}
pip install torch-scatter -f https://data.pyg.org/whl/torch-${TORCH_VERSION}+cu${CUDA_VERSION}.html
pip install git+https://github.com/Dao-AILab/flash-attention.git
pip install huggingface_hub timm addict scipy
# Install utonia as a package (or just copy utonia/ folder to your project)
cd Utonia
python setup.py install
Pre-trained Weights
Weights are auto-downloaded from HuggingFace on first utonia.model.load() call.
Available models on Pointcept/Utonia:
utonia.pth -- main encoder (enc_mode=True, encoder-only PTv3)
utonia_linear_prob_head_sc.pth -- linear probe segmentation head for ScanNet 20-class
Cache location: ~/.cache/utonia/ckpt (customizable via download_root param).
Usage Scenarios
Scenario 1: Feature Extraction (Indoor Scene)
export PYTHONPATH=./
python demo/0_pca_indoor.py
# With --wo_color or --wo_normal to test modality ablation
Scenario 2: Semantic Segmentation (Linear Probe)
python demo/2_sem_seg.py
Scenario 3: Batched Inference
python demo/3_batch_forward.py
Scenario 4: Outdoor LiDAR
python demo/7_pca_outdoor.py
Scenario 5: Object-level
python demo/6_pca_object.py
Scenario 6: Robotic Manipulation
python demo/5_pca_manipulation.py
Scenario 7: Video-lifted Point Clouds (requires VGGT)
git clone https://github.com/facebookresearch/vggt.git && cd vggt && pip install -e . && cd ..
python demo/8_pca_video.py --input_video ${VIDEO_PATH} --conf_thres 0 \
--prediction_mode "Depthmap and Camera Branch" --if_TSDF --pca_start 1 --pca_brightness 1.2
Key Config Flags
| Flag / Param |
Default |
Description |
enc_mode |
True (in checkpoint) |
Encoder-only mode (no decoder); always True for Utonia weights |
enable_flash |
True |
Use FlashAttention; set False if flash_attn not installed |
enc_patch_size |
(1024,1024,1024,1024,1024) |
Patch size per stage; reduce if OOM |
in_channels |
6 |
Input feature dim (set by checkpoint config) |
freeze_encoder |
False |
Freeze embedding+encoder params for downstream fine-tuning |
scale (transform) |
1.0 |
Domain-dependent granularity rescale factor |
apply_z_positive (transform) |
True |
CenterShift; set False for outdoor LiDAR |
normalize_coord (transform) |
False |
NormalizeCoord; set True for object-centric data |
custom_config (load) |
None |
Dict to override any checkpoint config key |
ckpt_only (load) |
False |
Return raw checkpoint dict instead of model |
--wo_color |
False |
Zero out color input (demo scripts) |
--wo_normal |
False |
Zero out normal input (demo scripts) |
Code Integration Guide
Minimal Imports
import utonia
# or individually:
from utonia.model import PointTransformerV3, load
from utonia.transform import Compose, default
from utonia.data import collate_fn
from utonia.structure import Point
from utonia.utils import offset2batch, batch2offset, offset2bincount, set_seed
Model Instantiation & Inference
import torch
import utonia
# Load pre-trained encoder from HuggingFace
model = utonia.model.load("utonia", repo_id="Pointcept/Utonia").cuda()
model.eval()
# Alternative: HuggingFace Hub API
# from utonia.model import PointTransformerV3
# model = PointTransformerV3.from_pretrained("Pointcept/Utonia").cuda()
# Without flash_attn:
model = utonia.model.load("utonia", repo_id="Pointcept/Utonia",
custom_config=dict(enable_flash=False, enc_patch_size=[1024]*5)).cuda()
# From local checkpoint:
model = utonia.model.load("path/to/utonia.pth").cuda()
# Inspect checkpoint config:
ckpt = utonia.model.load("utonia", ckpt_only=True)
print(ckpt["config"]) # dict with all PointTransformerV3 constructor args
Data Format
Input (before transform): a dict with numpy arrays.
| Field |
Shape / Type |
Description |
coord |
(N, 3) float |
XYZ coordinates in meters (required) |
color |
(N, 3) float |
RGB values in [0, 255]; use zeros if unavailable |
normal |
(N, 3) float |
Surface normals; use zeros if unavailable |
segment |
(N,) int |
Optional ground-truth labels |
batch |
(N,) int |
Optional batch index per point (for pre-batched data) |
Input (after transform, ready for model): a dict with torch tensors.
| Field |
Shape / Type |
Description |
coord |
(M, 3) float32 |
Grid-sampled, scaled coordinates |
grid_coord |
(M, 3) int32 |
Integer grid coordinates for serialization/sparsification |
color |
(M, 3) float32 |
Normalized to [0, 1] (via NormalizeColor) |
feat |
(M, D) float32 |
Concatenation of feat_keys, e.g. [coord, color, normal] -> D=9 |
offset |
(1,) int64 |
Cumulative point count per sample |
inverse |
(N,) int64 |
Maps original points to grid-sampled points (for upsampling back) |
Output: a Point object (subclass of addict.Dict).
| Field |
Shape / Type |
Description |
point.feat |
(M', D_enc) float32 |
Encoded features at deepest encoder stage (D_enc=512) |
point.coord |
(M', 3) float32 |
Coordinates at deepest stage |
point.offset |
(B,) int64 |
Batch offsets |
point.batch |
(M',) int64 |
Batch index per point |
point.pooling_parent |
Point |
Parent point cloud from previous (higher-res) stage |
point.pooling_inverse |
(M',) int64 |
Maps each deepest-stage point to its parent |
Default encoder channel dims per stage: enc_channels=(48, 96, 192, 384, 512).
Feature Unpooling (CRITICAL)
The encoder output is at the deepest (most downsampled) stage. To recover features at higher resolutions:
# Unpool with concatenation (first K levels for richer features)
for _ in range(K): # K=2 for visualization, K=4 for full unpool
parent = point.pop("pooling_parent")
inverse = point.pop("pooling_inverse")
parent.feat = torch.cat([parent.feat, point.feat[inverse]], dim=-1)
point = parent
# Unpool remaining levels with simple nearest-neighbor copy
while "pooling_parent" in point.keys():
parent = point.pop("pooling_parent")
inverse = point.pop("pooling_inverse")
parent.feat = point.feat[inverse]
point = parent
# Map back to original (pre-GridSample) resolution
feat_original = point.feat[point.inverse] # (N, D) at original point count
Feature dims after concat-unpooling:
- K=2: 48 + 96 + 192 = 336 (stage0_feat + stage1_upcast + stage2_upcast)
- K=4 (all stages): 48 + 96 + 192 + 384 + 512 = 1232
Global Feature Extraction (for RL, classification)
For tasks needing a single feature vector per point cloud:
# After model forward (before unpooling), max-pool over deepest features
point = model(data_dict)
# point.feat is (M', 512) at deepest stage
# Simple max-pool for single sample:
global_feat = point.feat.max(dim=0).values # (512,)
# Batched max-pool using offset:
from utonia.utils import offset2bincount
import torch_scatter
bincount = offset2bincount(point.offset)
idx_ptr = torch.cat([bincount.new_zeros(1), torch.cumsum(bincount, dim=0)])
_, indices = torch.sort(torch.arange(point.feat.shape[0], device=point.feat.device))
global_feat = torch_scatter.segment_csr(point.feat[indices], idx_ptr, reduce="max")
# global_feat: (B, 512)
Integration Notes
- The
utonia package is self-contained (no dependency on full Pointcept). Just copy the utonia/ folder or pip install via setup.py.
feat_keys=("coord", "color", "normal") in the Collect transform concatenates these into the feat tensor (9-dim). The model's in_channels in the checkpoint config determines what the embedding layer expects.
- When color or normal is absent, fill with zeros (same shape as coord). Do NOT omit the key.
- The model uses
spconv.SubMConv3d for the convolutional position encoding (CPE). This is handled internally by Point.sparsify().
- FlashAttention is strongly recommended. Without it, set
enable_flash=False via custom_config and optionally reduce enc_patch_size.
- The
Point class inherits from addict.Dict -- attribute access works like dict access (point.feat == point["feat"]).
GridPooling sets traceable=True by default, which stores pooling_parent and pooling_inverse for unpooling.
Core Architecture
Input dict {coord, color, normal}
|
v
[Embedding] Linear(in_channels -> 48) + LN + GELU
|
v
[Serialization] z-order / z-trans curve encoding
[Sparsify] -> SparseConvTensor for CPE
|
v
+-- Enc Stage 0: 3x Block(48, heads=3, patch=1024) ----> pooling_parent chain
| Block = CPE(SubMConv3d) + SerializedAttention(3D_RoPE) + MLP(ratio=4)
v
[GridPooling stride=2, max-reduce] 48 -> 96
|
+-- Enc Stage 1: 3x Block(96, heads=6) ----> pooling_parent chain
v
[GridPooling stride=2] 96 -> 192
|
+-- Enc Stage 2: 3x Block(192, heads=12) ----> pooling_parent chain
v
[GridPooling stride=2] 192 -> 384
|
+-- Enc Stage 3: 12x Block(384, heads=24) ----> pooling_parent chain
v
[GridPooling stride=2] 384 -> 512
|
+-- Enc Stage 4: 3x Block(512, heads=32)
|
v
Output: Point with feat(512), pooling_parent/pooling_inverse chain
(enc_mode=True: no decoder, manual unpool via chain)
Repo Structure
| Path |
Purpose |
utonia/__init__.py |
Package init; exports load and all submodules |
utonia/model.py |
PointTransformerV3, load(), Point3DRoPE, SerializedAttention, Block, GridPooling, GridUnpooling, Embedding |
utonia/module.py |
PointModule, PointSequential -- base classes for Point-aware nn.Module dispatch |
utonia/structure.py |
Point class (addict.Dict subclass) with serialization() and sparsify() |
utonia/transform.py |
All transforms (GridSample, RandomScale, NormalizeColor, NormalizeCoord, CenterShift, Collect, ToTensor, etc.), Compose, default() |
utonia/data.py |
load() for sample data from HuggingFace (pointcept/demo), collate_fn() for offset-based batching |
utonia/utils.py |
offset2batch, batch2offset, offset2bincount, set_seed |
utonia/registry.py |
OpenMMLab-style Registry for building transforms from config dicts |
utonia/serialization/ |
Z-order (z_order.py) and Hilbert (hilbert.py) curve encoding/decoding; default.py dispatches by order name |
demo/0_pca_indoor.py |
PCA visualization on indoor ScanNet scene (scale=0.5) |
demo/1_similarity.py |
Similarity heatmap visualization |
demo/2_sem_seg.py |
Semantic segmentation with utonia_linear_prob_head_sc linear probe |
demo/3_batch_forward.py |
Batched inference example using utonia.data.collate_fn |
demo/4_pca_hk.py |
PCA on Hong Kong outdoor scene |
demo/5_pca_manipulation.py |
PCA on robotic manipulation scene (scale=4.0) |
demo/6_pca_object.py |
PCA on object-centric data with estimated normals |
demo/7_pca_outdoor.py |
PCA on outdoor LiDAR (scale=0.2, apply_z_positive=False) |
demo/8_pca_video.py |
PCA on video-lifted point clouds (requires VGGT) |
demo/visual_util.py |
Visualization utilities |
setup.py |
Package installation (name="utonia", version="1.0") |
environment.yml |
Conda env spec (Python 3.10, PyTorch 2.5.0, CUDA 12.4) |
Tips & Gotchas
- Scale is the most important hyperparameter. Wrong scale = garbage features. See the Scale / Domain Table above. Indoor=0.5, manipulation=4.0, outdoor=0.2, object=1.0.
- Outdoor LiDAR: Must set
apply_z_positive=False so CenterShift is skipped. Ensure ego-vehicle is at origin with road on xy-plane.
- Object data: Set
normalize_coord=True to enable NormalizeCoord transform (centers and normalizes to unit sphere).
- Missing modalities: When color or normal is unavailable, pass
np.zeros_like(coord). Do NOT omit the key from the dict. The model was trained with Causal Modality Blinding and handles missing inputs gracefully.
- FlashAttention required by default: The checkpoint config has
enable_flash=True. If flash_attn is not installed, pass custom_config=dict(enable_flash=False) to load().
- Memory: Large scenes can OOM. Reduce
enc_patch_size (e.g., [512]*5 or [256]*5) to lower memory usage at the cost of attention window size.
- Unpooling depth matters: Use
range(2) concat + remaining copy for visualization. Use range(4) concat for quantitative evaluation (all skip features concatenated). The demo/2_sem_seg.py uses full concat (while loop with concat at every level).
- Feature dims after unpool: Concat over K=2 stages gives 48+96+192=336 dims. Full K=4 gives 48+96+192+384+512=1232 dims.
- Grid size: Fixed at 0.01m in the default transform. Changing this is not recommended without retraining.
- Batch collation: Use
utonia.data.collate_fn() for batching multiple transformed point clouds. It handles offset-based batching correctly (concatenates coords, builds cumulative offset). Do NOT use PyTorch default collation.
- Weights license: CC-BY-NC 4.0 (non-commercial due to HM3D/ArkitScenes dataset restrictions). Code is Apache 2.0.
- Fine-tuning: This repo is inference-only. For training/fine-tuning, use the full Pointcept codebase with Utonia weights as initialization.
enc_mode=True: The Utonia checkpoint is encoder-only. The decoder path in PointTransformerV3 exists in code but is not used. All feature recovery is done via manual unpooling with pooling_parent/pooling_inverse.
- 3D RoPE constraint:
head_dim = channels // num_heads must be divisible by 3. This is enforced by assertion in Point3DRoPE.__init__.
- Serialization orders: Default is
("z", "z-trans"). Blocks alternate between orders via order_index = i % len(self.order).
drop_path: Default 0.3 stochastic depth, linearly increasing across encoder depth.
rope_base: Default 10 (not the typical 10000 used in NLP RoPE).
1---2name: paper-3d-utonia3description: Utonia - unified cross-domain point cloud encoder (PTv3-based, self-supervised, 5 domains)4---56# paper_3d__utonia78Utonia trains a single self-supervised Point Transformer V3 (PTv3) encoder across five diverse point cloud domains (remote sensing, outdoor LiDAR, indoor RGB-D, object CAD, RGB-video-lifted), producing universal 3D representations that transfer to segmentation, robotic manipulation, and multimodal reasoning.910## Paper Info1112| Field | Value |13|-------|-------|14| Title | Utonia: Toward One Encoder for All Point Clouds |15| Authors | Yujia Zhang, Xiaoyang Wu et al. |16| Year | 2026 |17| Venue | arXiv (cs.CV) |18| Paper | https://arxiv.org/abs/2603.03283 |19| Code | https://github.com/Pointcept/Utonia |20| Weights | https://huggingface.co/Pointcept/Utonia (model), https://huggingface.co/datasets/pointcept/demo (sample data) |2122## Method Overview2324Utonia is a cross-domain pre-trained PTv3 encoder that harmonizes point clouds with distinct sensing geometries, densities, and modality priors. The pipeline is:25261. **Data preparation**: Raw point clouds (coord, color, normal) are normalized and grid-sampled at 0.01m resolution. A domain-dependent **scale** parameter controls Perceptual Granularity Rescale -- the key mechanism for unifying different point cloud densities.272. **Serialization**: Grid coordinates are serialized via z-order or Hilbert curves to create 1D sequences for efficient attention.283. **Encoder**: A 5-stage hierarchical PTv3 with serialized patch attention, 3D RoPE positional encoding, and sparse convolution CPE. Stages are connected by GridPooling (stride-2 downsampling).294. **Output**: The encoder produces a hierarchical `Point` object; features can be unpooled back to the original resolution via `pooling_inverse` and `pooling_parent` chains.3031Key insight: A single encoder can learn universal 3D representations across wildly different domains by rescaling point cloud granularity through a per-domain scale factor applied before grid sampling.3233## Scale / Domain Table (CRITICAL)3435The `scale` parameter in the transform pipeline controls the effective voxel resolution. Choosing the right scale per domain is essential:3637| Domain | scale | `apply_z_positive` | `normalize_coord` | Notes |38|--------|-------|--------------------|--------------------|-------|39| Indoor scene (ScanNet) | 0.5 | True | False | Default for rooms |40| Robotic manipulation | 4.0 | True | False | Tabletop scenes need finer grain |41| Outdoor LiDAR | 0.2 | **False** | False | Keep ego at origin, road on xy-plane |42| Object-centric (CAD) | 1.0 (default) | True | **True** | Enable NormalizeCoord for objects |43| Hong Kong scene | 0.5 | True | False | Similar to indoor |4445## Paper-Code Mapping4647| Paper Concept | Code Location | Notes |48|---------------|---------------|-------|49| PTv3 Encoder | `utonia/model.py:PointTransformerV3` | Main model class, also `PyTorchModelHubMixin` |50| 3D RoPE | `utonia/model.py:Point3DRoPE` | Splits head_dim into 3 chunks for x,y,z rotation; head_dim must be divisible by 3 |51| Serialized Attention | `utonia/model.py:SerializedAttention` | Patch-based attention with flash_attn varlen support |52| Transformer Block | `utonia/model.py:Block` | CPE (SubMConv3d) + SerializedAttention + MLP with DropPath |53| Grid Pooling (downsampling) | `utonia/model.py:GridPooling` | stride-2, max-reduce, traceable for unpooling |54| Grid Unpooling | `utonia/model.py:GridUnpooling` | Skip-connection based upsampling (decoder only) |55| Embedding | `utonia/model.py:Embedding` | Linear(in_channels -> enc_channels[0]) + LN + GELU, optional mask token |56| Point data structure | `utonia/structure.py:Point` | `addict.Dict` subclass with `serialization()` and `sparsify()` methods |57| Serialization (z-order, Hilbert) | `utonia/serialization/default.py:encode` | Orders: "z", "z-trans", "hilbert", "hilbert-trans" |58| Perceptual Granularity Rescale | `utonia/transform.py:RandomScale` + `GridSample` | scale param controls effective resolution |59| Transform pipeline | `utonia/transform.py:Compose`, `default()` | Configurable transform chain via Registry |60| Data loading | `utonia/data.py:load` | Loads sample .npz from HuggingFace `pointcept/demo` |61| Batch collation | `utonia/data.py:collate_fn` | Handles offset-based batching for variable-size point clouds |62| Model loading | `utonia/model.py:load` | Downloads from HuggingFace `Pointcept/Utonia`, builds model from stored config |63| Linear probe head | `demo/2_sem_seg.py:SegHead` | Simple `nn.Linear` for ScanNet 20-class segmentation |64| RPE (relative pos enc) | `utonia/model.py:RPE` | Table-based RPE (disabled when flash_attn enabled) |65| LayerScale | `utonia/model.py:LayerScale` | Per-channel learnable scaling (optional, controlled by `layer_scale` param) |6667## Setup6869### Dependencies7071- Python 3.10+72- PyTorch 2.5.0+ with CUDA 12.4+73- FlashAttention (strongly recommended, required for default config)74- Key packages: `spconv-cu124`, `torch-scatter`, `flash-attn`, `huggingface_hub`, `timm`, `addict`, `scipy`75- Demo extras: `open3d` (requires numpy<=1.26.4), `fast_pytorch_kmeans`, `psutil`, `camtools`, `trimesh`, `gradio`, `natsort`, `opencv-python`7677### Installation7879**Standalone mode** (conda env with everything):80```bash81git clone https://github.com/Pointcept/Utonia.git82cd Utonia8384# unset CUDA_PATH if you have local CUDA installed85conda env create -f environment.yml --verbose86conda activate utonia87```8889**Package mode** (inject into existing env):90```bash91# CUDA_VERSION e.g. 124, TORCH_VERSION e.g. 2.5.092pip install spconv-cu${CUDA_VERSION}93pip install torch-scatter -f https://data.pyg.org/whl/torch-${TORCH_VERSION}+cu${CUDA_VERSION}.html94pip install git+https://github.com/Dao-AILab/flash-attention.git95pip install huggingface_hub timm addict scipy9697# Install utonia as a package (or just copy utonia/ folder to your project)98cd Utonia99python setup.py install100```101102### Pre-trained Weights103104Weights are auto-downloaded from HuggingFace on first `utonia.model.load()` call.105106Available models on `Pointcept/Utonia`:107- `utonia.pth` -- main encoder (enc_mode=True, encoder-only PTv3)108- `utonia_linear_prob_head_sc.pth` -- linear probe segmentation head for ScanNet 20-class109110Cache location: `~/.cache/utonia/ckpt` (customizable via `download_root` param).111112## Usage Scenarios113114### Scenario 1: Feature Extraction (Indoor Scene)115```bash116export PYTHONPATH=./117python demo/0_pca_indoor.py118# With --wo_color or --wo_normal to test modality ablation119```120121### Scenario 2: Semantic Segmentation (Linear Probe)122```bash123python demo/2_sem_seg.py124```125126### Scenario 3: Batched Inference127```bash128python demo/3_batch_forward.py129```130131### Scenario 4: Outdoor LiDAR132```bash133python demo/7_pca_outdoor.py134```135136### Scenario 5: Object-level137```bash138python demo/6_pca_object.py139```140141### Scenario 6: Robotic Manipulation142```bash143python demo/5_pca_manipulation.py144```145146### Scenario 7: Video-lifted Point Clouds (requires VGGT)147```bash148git clone https://github.com/facebookresearch/vggt.git && cd vggt && pip install -e . && cd ..149python demo/8_pca_video.py --input_video ${VIDEO_PATH} --conf_thres 0 \150 --prediction_mode "Depthmap and Camera Branch" --if_TSDF --pca_start 1 --pca_brightness 1.2151```152153### Key Config Flags154155| Flag / Param | Default | Description |156|------|---------|-------------|157| `enc_mode` | True (in checkpoint) | Encoder-only mode (no decoder); always True for Utonia weights |158| `enable_flash` | True | Use FlashAttention; set False if flash_attn not installed |159| `enc_patch_size` | (1024,1024,1024,1024,1024) | Patch size per stage; reduce if OOM |160| `in_channels` | 6 | Input feature dim (set by checkpoint config) |161| `freeze_encoder` | False | Freeze embedding+encoder params for downstream fine-tuning |162| `scale` (transform) | 1.0 | **Domain-dependent** granularity rescale factor |163| `apply_z_positive` (transform) | True | CenterShift; set False for outdoor LiDAR |164| `normalize_coord` (transform) | False | NormalizeCoord; set True for object-centric data |165| `custom_config` (load) | None | Dict to override any checkpoint config key |166| `ckpt_only` (load) | False | Return raw checkpoint dict instead of model |167| `--wo_color` | False | Zero out color input (demo scripts) |168| `--wo_normal` | False | Zero out normal input (demo scripts) |169170## Code Integration Guide171172### Minimal Imports173174```python175import utonia176# or individually:177from utonia.model import PointTransformerV3, load178from utonia.transform import Compose, default179from utonia.data import collate_fn180from utonia.structure import Point181from utonia.utils import offset2batch, batch2offset, offset2bincount, set_seed182```183184### Model Instantiation & Inference185186```python187import torch188import utonia189190# Load pre-trained encoder from HuggingFace191model = utonia.model.load("utonia", repo_id="Pointcept/Utonia").cuda()192model.eval()193194# Alternative: HuggingFace Hub API195# from utonia.model import PointTransformerV3196# model = PointTransformerV3.from_pretrained("Pointcept/Utonia").cuda()197198# Without flash_attn:199model = utonia.model.load("utonia", repo_id="Pointcept/Utonia",200 custom_config=dict(enable_flash=False, enc_patch_size=[1024]*5)).cuda()201202# From local checkpoint:203model = utonia.model.load("path/to/utonia.pth").cuda()204205# Inspect checkpoint config:206ckpt = utonia.model.load("utonia", ckpt_only=True)207print(ckpt["config"]) # dict with all PointTransformerV3 constructor args208```209210### Data Format211212**Input** (before transform): a dict with numpy arrays.213214| Field | Shape / Type | Description |215|-------|-------------|-------------|216| `coord` | `(N, 3)` float | XYZ coordinates in meters (required) |217| `color` | `(N, 3)` float | RGB values in [0, 255]; use zeros if unavailable |218| `normal` | `(N, 3)` float | Surface normals; use zeros if unavailable |219| `segment` | `(N,)` int | Optional ground-truth labels |220| `batch` | `(N,)` int | Optional batch index per point (for pre-batched data) |221222**Input** (after transform, ready for model): a dict with torch tensors.223224| Field | Shape / Type | Description |225|-------|-------------|-------------|226| `coord` | `(M, 3)` float32 | Grid-sampled, scaled coordinates |227| `grid_coord` | `(M, 3)` int32 | Integer grid coordinates for serialization/sparsification |228| `color` | `(M, 3)` float32 | Normalized to [0, 1] (via NormalizeColor) |229| `feat` | `(M, D)` float32 | Concatenation of feat_keys, e.g. [coord, color, normal] -> D=9 |230| `offset` | `(1,)` int64 | Cumulative point count per sample |231| `inverse` | `(N,)` int64 | Maps original points to grid-sampled points (for upsampling back) |232233**Output**: a `Point` object (subclass of `addict.Dict`).234235| Field | Shape / Type | Description |236|-------|-------------|-------------|237| `point.feat` | `(M', D_enc)` float32 | Encoded features at deepest encoder stage (D_enc=512) |238| `point.coord` | `(M', 3)` float32 | Coordinates at deepest stage |239| `point.offset` | `(B,)` int64 | Batch offsets |240| `point.batch` | `(M',)` int64 | Batch index per point |241| `point.pooling_parent` | `Point` | Parent point cloud from previous (higher-res) stage |242| `point.pooling_inverse` | `(M',)` int64 | Maps each deepest-stage point to its parent |243244Default encoder channel dims per stage: `enc_channels=(48, 96, 192, 384, 512)`.245246### Feature Unpooling (CRITICAL)247248The encoder output is at the deepest (most downsampled) stage. To recover features at higher resolutions:249250```python251# Unpool with concatenation (first K levels for richer features)252for _ in range(K): # K=2 for visualization, K=4 for full unpool253 parent = point.pop("pooling_parent")254 inverse = point.pop("pooling_inverse")255 parent.feat = torch.cat([parent.feat, point.feat[inverse]], dim=-1)256 point = parent257258# Unpool remaining levels with simple nearest-neighbor copy259while "pooling_parent" in point.keys():260 parent = point.pop("pooling_parent")261 inverse = point.pop("pooling_inverse")262 parent.feat = point.feat[inverse]263 point = parent264265# Map back to original (pre-GridSample) resolution266feat_original = point.feat[point.inverse] # (N, D) at original point count267```268269Feature dims after concat-unpooling:270- K=2: 48 + 96 + 192 = 336 (stage0_feat + stage1_upcast + stage2_upcast)271- K=4 (all stages): 48 + 96 + 192 + 384 + 512 = 1232272273### Global Feature Extraction (for RL, classification)274275For tasks needing a single feature vector per point cloud:276277```python278# After model forward (before unpooling), max-pool over deepest features279point = model(data_dict)280# point.feat is (M', 512) at deepest stage281282# Simple max-pool for single sample:283global_feat = point.feat.max(dim=0).values # (512,)284285# Batched max-pool using offset:286from utonia.utils import offset2bincount287import torch_scatter288bincount = offset2bincount(point.offset)289idx_ptr = torch.cat([bincount.new_zeros(1), torch.cumsum(bincount, dim=0)])290_, indices = torch.sort(torch.arange(point.feat.shape[0], device=point.feat.device))291global_feat = torch_scatter.segment_csr(point.feat[indices], idx_ptr, reduce="max")292# global_feat: (B, 512)293```294295### Integration Notes296297- The `utonia` package is self-contained (no dependency on full Pointcept). Just copy the `utonia/` folder or `pip install` via setup.py.298- `feat_keys=("coord", "color", "normal")` in the `Collect` transform concatenates these into the `feat` tensor (9-dim). The model's `in_channels` in the checkpoint config determines what the embedding layer expects.299- When color or normal is absent, **fill with zeros** (same shape as coord). Do NOT omit the key.300- The model uses `spconv.SubMConv3d` for the convolutional position encoding (CPE). This is handled internally by `Point.sparsify()`.301- FlashAttention is strongly recommended. Without it, set `enable_flash=False` via `custom_config` and optionally reduce `enc_patch_size`.302- The `Point` class inherits from `addict.Dict` -- attribute access works like dict access (`point.feat` == `point["feat"]`).303- `GridPooling` sets `traceable=True` by default, which stores `pooling_parent` and `pooling_inverse` for unpooling.304305## Core Architecture306307```308Input dict {coord, color, normal}309 |310 v311[Embedding] Linear(in_channels -> 48) + LN + GELU312 |313 v314[Serialization] z-order / z-trans curve encoding315[Sparsify] -> SparseConvTensor for CPE316 |317 v318+-- Enc Stage 0: 3x Block(48, heads=3, patch=1024) ----> pooling_parent chain319 | Block = CPE(SubMConv3d) + SerializedAttention(3D_RoPE) + MLP(ratio=4)320 v321[GridPooling stride=2, max-reduce] 48 -> 96322 |323+-- Enc Stage 1: 3x Block(96, heads=6) ----> pooling_parent chain324 v325[GridPooling stride=2] 96 -> 192326 |327+-- Enc Stage 2: 3x Block(192, heads=12) ----> pooling_parent chain328 v329[GridPooling stride=2] 192 -> 384330 |331+-- Enc Stage 3: 12x Block(384, heads=24) ----> pooling_parent chain332 v333[GridPooling stride=2] 384 -> 512334 |335+-- Enc Stage 4: 3x Block(512, heads=32)336 |337 v338Output: Point with feat(512), pooling_parent/pooling_inverse chain339 (enc_mode=True: no decoder, manual unpool via chain)340```341342## Repo Structure343344| Path | Purpose |345|------|---------|346| `utonia/__init__.py` | Package init; exports `load` and all submodules |347| `utonia/model.py` | `PointTransformerV3`, `load()`, `Point3DRoPE`, `SerializedAttention`, `Block`, `GridPooling`, `GridUnpooling`, `Embedding` |348| `utonia/module.py` | `PointModule`, `PointSequential` -- base classes for Point-aware nn.Module dispatch |349| `utonia/structure.py` | `Point` class (addict.Dict subclass) with `serialization()` and `sparsify()` |350| `utonia/transform.py` | All transforms (`GridSample`, `RandomScale`, `NormalizeColor`, `NormalizeCoord`, `CenterShift`, `Collect`, `ToTensor`, etc.), `Compose`, `default()` |351| `utonia/data.py` | `load()` for sample data from HuggingFace (`pointcept/demo`), `collate_fn()` for offset-based batching |352| `utonia/utils.py` | `offset2batch`, `batch2offset`, `offset2bincount`, `set_seed` |353| `utonia/registry.py` | OpenMMLab-style `Registry` for building transforms from config dicts |354| `utonia/serialization/` | Z-order (`z_order.py`) and Hilbert (`hilbert.py`) curve encoding/decoding; `default.py` dispatches by order name |355| `demo/0_pca_indoor.py` | PCA visualization on indoor ScanNet scene (scale=0.5) |356| `demo/1_similarity.py` | Similarity heatmap visualization |357| `demo/2_sem_seg.py` | Semantic segmentation with `utonia_linear_prob_head_sc` linear probe |358| `demo/3_batch_forward.py` | Batched inference example using `utonia.data.collate_fn` |359| `demo/4_pca_hk.py` | PCA on Hong Kong outdoor scene |360| `demo/5_pca_manipulation.py` | PCA on robotic manipulation scene (scale=4.0) |361| `demo/6_pca_object.py` | PCA on object-centric data with estimated normals |362| `demo/7_pca_outdoor.py` | PCA on outdoor LiDAR (scale=0.2, apply_z_positive=False) |363| `demo/8_pca_video.py` | PCA on video-lifted point clouds (requires VGGT) |364| `demo/visual_util.py` | Visualization utilities |365| `setup.py` | Package installation (name="utonia", version="1.0") |366| `environment.yml` | Conda env spec (Python 3.10, PyTorch 2.5.0, CUDA 12.4) |367368## Tips & Gotchas369370- **Scale is the most important hyperparameter**. Wrong scale = garbage features. See the Scale / Domain Table above. Indoor=0.5, manipulation=4.0, outdoor=0.2, object=1.0.371- **Outdoor LiDAR**: Must set `apply_z_positive=False` so CenterShift is skipped. Ensure ego-vehicle is at origin with road on xy-plane.372- **Object data**: Set `normalize_coord=True` to enable `NormalizeCoord` transform (centers and normalizes to unit sphere).373- **Missing modalities**: When color or normal is unavailable, pass `np.zeros_like(coord)`. Do NOT omit the key from the dict. The model was trained with Causal Modality Blinding and handles missing inputs gracefully.374- **FlashAttention required by default**: The checkpoint config has `enable_flash=True`. If flash_attn is not installed, pass `custom_config=dict(enable_flash=False)` to `load()`.375- **Memory**: Large scenes can OOM. Reduce `enc_patch_size` (e.g., `[512]*5` or `[256]*5`) to lower memory usage at the cost of attention window size.376- **Unpooling depth matters**: Use `range(2)` concat + remaining copy for visualization. Use `range(4)` concat for quantitative evaluation (all skip features concatenated). The `demo/2_sem_seg.py` uses full concat (`while` loop with concat at every level).377- **Feature dims after unpool**: Concat over K=2 stages gives 48+96+192=336 dims. Full K=4 gives 48+96+192+384+512=1232 dims.378- **Grid size**: Fixed at 0.01m in the default transform. Changing this is not recommended without retraining.379- **Batch collation**: Use `utonia.data.collate_fn()` for batching multiple transformed point clouds. It handles offset-based batching correctly (concatenates coords, builds cumulative offset). Do NOT use PyTorch default collation.380- **Weights license**: CC-BY-NC 4.0 (non-commercial due to HM3D/ArkitScenes dataset restrictions). Code is Apache 2.0.381- **Fine-tuning**: This repo is inference-only. For training/fine-tuning, use the full [Pointcept](https://github.com/Pointcept/Pointcept) codebase with Utonia weights as initialization.382- **`enc_mode=True`**: The Utonia checkpoint is encoder-only. The decoder path in `PointTransformerV3` exists in code but is not used. All feature recovery is done via manual unpooling with `pooling_parent`/`pooling_inverse`.383- **3D RoPE constraint**: `head_dim = channels // num_heads` must be divisible by 3. This is enforced by assertion in `Point3DRoPE.__init__`.384- **Serialization orders**: Default is `("z", "z-trans")`. Blocks alternate between orders via `order_index = i % len(self.order)`.385- **`drop_path`**: Default 0.3 stochastic depth, linearly increasing across encoder depth.386- **`rope_base`**: Default 10 (not the typical 10000 used in NLP RoPE).