Working with Occupancy Grids

isoext is built for scalar fields, but it also works on binary occupancy grids: voxel data where each sample is occupied (1) or empty (0), as produced by 3D scans, segmentation masks, or boolean voxel operations. Extraction works the same way; what changes is the choice of level and the need for smoothing.

import torch
import isoext
from isoext.sdf import SphereSDF
from isoext import viewer

Creating an Occupancy Grid

Let’s create a binary occupancy grid from a sphere SDF. Values > 0 are outside, so we threshold to get a binary mask:

# Create a grid and compute sphere SDF
grid = isoext.UniformGrid([64, 64, 64])
sdf_values = SphereSDF(radius=0.7)(grid.get_points())

# Convert to binary occupancy: 0 = outside, 1 = inside
occupancy = (sdf_values < 0.0).float()

print(f"Occupancy values: {occupancy.unique().tolist()}")
Occupancy values: [0.0, 1.0]

Direct Extraction

Marching cubes runs directly on the binary grid with level=0.5, halfway between empty and occupied. The result is jagged: with only two values there is nothing to interpolate, so every vertex lands exactly halfway across a voxel face.

grid.set_values(occupancy)
v_jagged, f_jagged = isoext.marching_cubes(grid, level=0.5)

print(f"Jagged mesh: {v_jagged.shape[0]} vertices, {f_jagged.shape[0]} faces")
viewer.embed(v_jagged, f_jagged, flat_shading=True)
Jagged mesh: 9168 vertices, 18332 faces

Smoothed Extraction

Gaussian smoothing before extraction turns the hard 0/1 steps into gradients the interpolation can work with. The mesh comes out smooth, at the cost of rounding any genuinely sharp corners in the data.

# Smooth the occupancy grid
smoothed = isoext.gaussian_smooth(occupancy, sigma=5.0)
grid.set_values(smoothed)
v_smooth, f_smooth = isoext.marching_cubes(grid, level=0.5)

print(f"Smooth mesh: {v_smooth.shape[0]} vertices, {f_smooth.shape[0]} faces")
viewer.embed(v_smooth, f_smooth)
Smooth mesh: 8232 vertices, 16460 faces