Training & Mesh Extraction

June 24, 2026 · View on GitHub

From Blobs to Spokes: High-Fidelity Surface Reconstruction via Oriented Gaussians

Diego Gomez*1Antoine Guédon*1Nissim Maruani2Bingchen Gong1Maks Ovsjanikov1
1Ecole Polytechnique, France    2Inria, Université Côte d'Azur, France

* Equal contribution


Project Page | arXiv | Download Meshes


Teaser

Gaussian Wrapping reconstructs watertight, textured surface meshes of full 3D scenes—including extremely thin structures such as bicycle spokes—at a fraction of the mesh size of concurrent works, by interpreting 3D Gaussians as stochastic oriented surface elements.

Zoom Bike

Updates

  • 01/05/2026 — Update: Added example bounding volumes for Primal Adaptive Meshing and launch scripts for the NeRF Synthetic dataset.
  • 07/04/2026 — Code Release: Initial release of the Gaussian Wrapping codebase.

Installation

git clone --recurse-submodules https://github.com/diego1401/GaussianWrapping
cd GaussianWrapping
conda create -n gaussian_wrapping python=3.9 -y
conda activate gaussian_wrapping

# Set CUDA paths (adjust version as needed)
CUDA_VERSION=12.1 # or 11.8 (12.1 is best for 4090RTX)

export CPATH=/usr/local/cuda-${CUDA_VERSION}/targets/x86_64-linux/include:$CPATH
export LD_LIBRARY_PATH=/usr/local/cuda-${CUDA_VERSION}/targets/x86_64-linux/lib:$LD_LIBRARY_PATH
export PATH=/usr/local/cuda-${CUDA_VERSION}/bin:$PATH

python install.py --cuda_version ${CUDA_VERSION}

Training & Mesh Extraction

Gaussian Wrapping expects a COLMAP-formatted dataset (images + sparse reconstruction). Use -s for the dataset root and -m for the output directory.

We provide two end-to-end scripts that run training followed by mesh extraction:

# RaDeGS rasterizer (Best looking meshes! A bit slower to train, script used for qualitative results on Mipnerf360)
python gaussian_wrapping/scripts/train_and_extract_gw_radegs.py \
    -s <PATH_TO_COLMAP_DATASET> \
    -m <OUTPUT_DIR>

# Our median-depth rasterizer (faster, better metrics)
python gaussian_wrapping/scripts/train_and_extract_gw_ours.py \
    -s <PATH_TO_COLMAP_DATASET> \
    -m <OUTPUT_DIR> \
    -r 2 # Flag added to remain comparable to heavier alternatives. This flag was used to obtain the metrics on the paper

Each script runs three steps in sequence: training, mesh extraction, and texture refinement. The refined mesh is saved alongside the extracted mesh with a _texture_refined suffix.

Key options

FlagDescription
-rDownsample input images (-r 2 used for metrics and comparisons)
--isosurface_valueOffset added to the base transmittance threshold (0.5) to control the surface boundary. Use 0.2 if the mesh is missing fine details (default: 0.0).
--N_max_gaussiansCaps the total number of Gaussians to prevent Out-of-Memory (OOM) errors.

Primal Adaptive Meshing

An alternative mesh extraction method that samples candidate points from an existing mesh (e.g. extracted by the standard pipeline), refines them onto the Gaussian occupancy isosurface via gradient descent, and reconstructs the surface with a Delaunay-based approach.

python gaussian_wrapping/primal_adaptive_meshing_extraction.py \
    -s <PATH_TO_COLMAP_DATASET> \
    -m <OUTPUT_DIR> \
    --input_mesh <PATH_TO_INPUT_MESH> \
    --output_mesh <PATH_TO_OUTPUT_MESH.ply> \
    --max_points <NUMBER_OF_MESH_VERTICES>
FlagDefaultDescription
--input_meshrequiredInput mesh to sample candidate points from (.ply)
--output_meshrequiredOutput mesh path (must end in .ply)
--max_points1e6Number of candidate points to sample from the input mesh
--bounding_box_methodscenescene uses the camera extent; ground_truth uses the TNT ground truth volume (if available); blender uses a custom volume exported from the Blender add-on (see below)
--bounding_box_scaling1.0Scale factor applied to the scene bounding box
--bounding_box_filePath to the .json bounding volume exported from Blender (required when --bounding_box_method blender)
Advanced options
FlagDefaultDescription
--n_steps10Number of refinement steps
--vacancy_threshold0.1Points with vacancy further than this from 0.5 are filtered out
--mesh_sampling_methodproportional_to_cameraproportional_to_camera weights sampling by proximity to cameras; surface_even samples uniformly on the cropped section of the input mesh.
--p_per_tet10Points sampled per tetrahedron for occupancy estimation
--oversampling_factor2Mostly useful when max_points is small. Sample this multiple of max_points upfront per pass to reduce the number of expensive occupancy evaluations
--post_processoffApply post-processing to the output mesh
--save_candidate_pointsoffSave the refined candidate point cloud alongside the mesh
--plot_vacancy_histogramoffSave per-step vacancy histograms to the output directory

Using the Provided Bounding Volumes

We provide ready-to-use bounding volume files for several scenes in assets/bounding_volumes_examples/. These can be used directly with PAM — no Blender setup required:

FileScene
Barn_bounding_volume.jsonTanks and Temples — Barn
bicycle_bounding_volume.jsonMipNeRF 360 — Bicycle
bonsai_bounding_volume.jsonMipNeRF 360 — Bonsai
garden_bounding_volume.jsonMipNeRF 360 — Garden
kitchen_bounding_volume.jsonMipNeRF 360 — Kitchen

To run PAM with one of these bounding volumes:

python gaussian_wrapping/primal_adaptive_meshing_extraction.py \
    -s <PATH_TO_COLMAP_DATASET> \
    -m <OUTPUT_DIR> \
    --input_mesh <PATH_TO_INPUT_MESH> \
    --output_mesh <PATH_TO_OUTPUT_MESH.ply> \
    --bounding_box_method blender \
    --bounding_box_file assets/bounding_volumes_examples/Barn_bounding_volume.json

To create your own bounding volumes for other scenes, see the Blender add-on below.

Blender Bounding Volume Add-on

If you want to extract an object of your scene at abritary resolution easily we propose this blender-add on to easily define the section of the scene to extract.

Installation

  1. Open Blender.
  2. Go to Edit → Preferences → Add-ons → Install…
  3. Select gaussian_wrapping/blender/bounding_volume_addon.py.
  4. Enable the GW Bounding Volume Exporter add-on in the list.

The add-on is now accessible as the GW Bounds tab in the 3D Viewport's N-panel (press N to open it).

Usage

  1. Import your input mesh into Blender as a visual reference (e.g. File → Import → Stanford PLY and select the mesh you plan to pass to --input_mesh).

    Step 1

  2. Create a bounding mesh — add any mesh (e.g. a Cube via Shift+A → Mesh → Cube) and transform (e.g edit mode → move vertices of cube) it so it encloses exactly the region of interest.

    Step 2

  3. Open the GW Bounds tab in the N-panel:

    • Set Bounding Mesh to the object you just created.
    • Set Export Path to the .json file you want to write.
    • Click Export Bounding Volume.
  4. Run the extraction script with:

python gaussian_wrapping/primal_adaptive_meshing_extraction.py \
    -s <PATH_TO_COLMAP_DATASET> \
    -m <OUTPUT_DIR> \
    --input_mesh <PATH_TO_INPUT_MESH> \
    --output_mesh <PATH_TO_OUTPUT_MESH.ply> \
    --bounding_box_method blender \
    --bounding_box_file <PATH_TO_EXPORTED.json>

Step 3

  1. Decimate the output mesh — the extracted mesh tends to be very dense. We recommend decimating it to reduce its size with no perceptible quality loss. This can be done via the script:
blender --background --python gaussian_wrapping/mesh_decimate.py -- \
    --in <PATH_TO_OUTPUT_MESH.ply> \
    --out <PATH_TO_DECIMATED_MESH.ply> \
    --ratio 0.3

Alternatively, you can decimate directly in Blender: import the mesh (File → Import → Stanford PLY), select it, go to the Properties panel → Modifier tab, add a Decimate modifier, set the Ratio (e.g. 0.3), and click Apply. Then export via File → Export → Stanford PLY.

Benchmarks

Tanks and Temples

Note on _post_ meshes: The meshes that are evaluated with the following metrics are passed through a post-processing procedure similar to GGGS and PGSR. This procedure yields in practice better metrics. However, we find that it can sometimes remove objects on the scene (e.g chandelier in the Meeting Room scene). Thus, for the textured meshes we recommend using non post-processed meshes.

To reproduce our full Tanks and Temples results, run the end-to-end benchmark scripts from the project root. Each script trains all 6 scenes, extracts meshes, and runs all three evaluations (uniform sampling, virtual scan sampling, and legacy TNT).

# Ours rasterizer
python gaussian_wrapping/scripts/benchmark_tnt_gw_ours.py \
    --data_dir <PATH_TO_TNT_SCENES> \
    --gt_dir <PATH_TO_TNT_GT> \
    --output_dir <OUTPUT_DIR>

# RaDeGS rasterizer
python gaussian_wrapping/scripts/benchmark_tnt_gw_radegs.py \
    --data_dir <PATH_TO_TNT_SCENES> \
    --gt_dir <PATH_TO_TNT_GT> \
    --output_dir <OUTPUT_DIR>

<PATH_TO_TNT_SCENES> should contain one subdirectory per scene (Barn/, Caterpillar/, etc.) in COLMAP format. <PATH_TO_TNT_GT> should contain the ground truth files for each scene (<Scene>.ply, <Scene>.json, <Scene>_trans.txt, <Scene>_COLMAP_SfM.log).

FlagDefaultDescription
--gpu_device0CUDA device index
--data_on_gpuoffLoad dataset on GPU instead of CPU
--depth_orderoffEnable depth-order regularization (see note below)
--depth_order_configConfig name under configs/depth_order/ (e.g. default, strong); only used with --depth_order

Note on --depth_order: This flag enables depth-order regularization using a pre-trained monocular depth model. It is not used in the paper, but can yield better results. It requires monocular depth priors to be pre-computed.

Per-scene results are written to <OUTPUT_DIR>/<Scene>/, with evaluation outputs in eval_uniform/, eval_virtual_scan/, and eval_legacy/ subdirectories.

Uniform and Virtual Scan Sampling Evaluations

We provide evaluation scripts using two sampling strategies: uniform sampling and virtual scan sampling. Full documentation and the standalone toolbox are available at diego1401/TNTUniScanEvals.

The scripts live under gaussian_wrapping/eval/TNTUniScanEvals/. See that directory's README for dataset setup and installation instructions.

Uniform Sampling Evaluation

Evaluates a mesh or point cloud against the ground truth using surface-based sampling and a 3-step ICP alignment pipeline.

python gaussian_wrapping/eval/TNTUniScanEvals/uniform_sampling_eval.py \
    --dataset-dir <path/to/scene_dir> \
    --traj-path <path/to/scene_dir>/<Scene>_COLMAP_SfM.log \
    --ply-path <path/to/mesh.ply> \
    --out-dir <output_dir>

Virtual Scan Sampling Evaluation

Evaluates a mesh by rendering depth from each training camera and projecting to world-space point clouds. Requires a COLMAP dataset and a CUDA-enabled GPU.

python gaussian_wrapping/eval/TNTUniScanEvals/virtual_scan_sampling_eval.py \
    -s <path/to/colmap_dataset> \
    -r 2 \
    --dataset-dir <path/to/scene_dir> \
    --traj-path <path/to/scene_dir>/<Scene>_COLMAP_SfM.log \
    --ply-path <path/to/mesh.ply> \
    --out-dir <output_dir>

Both scripts output precision, recall, and F-score to the console, along with precision/recall curve plots. Pass --save_point_clouds to also write color-coded error clouds.

DTU

Coming soon.

MipNeRF360

To reproduce our full MipNeRF 360 results, run the end-to-end benchmark scripts from the project root. Each script trains all 7 scenes, extracts meshes, and evaluates novel view synthesis quality.
# Ours rasterizer
python gaussian_wrapping/scripts/benchmark_mip360_gw_ours.py \
    --data_dir <PATH_TO_MIP360_SCENES> \
    --output_dir <OUTPUT_DIR>

# RaDeGS rasterizer
python gaussian_wrapping/scripts/benchmark_mip360_gw_radegs.py \
    --data_dir <PATH_TO_MIP360_SCENES> \
    --output_dir <OUTPUT_DIR>

<PATH_TO_MIP360_SCENES> should contain one subdirectory per scene (bicycle/, bonsai/, counter/, garden/, kitchen/, room/, stump/) in COLMAP format.

FlagDefaultDescription
--gpu_device0CUDA device index
--data_on_gpuoffLoad dataset on GPU instead of CPU
--depth_orderoffEnable depth-order regularization (see note below)
--depth_order_configConfig name under configs/depth_order/ (e.g. default, strong); only used with --depth_order

Note on --depth_order: This flag enables depth-order regularization using a pre-trained monocular depth model. It is not used in the paper, but can yield better results. It requires monocular depth priors to be pre-computed.

Per-scene results are written to <OUTPUT_DIR>/<scene>/. Rendered images and metrics (PSNR / SSIM / LPIPS) are saved in the standard Gaussian Splatting output structure.

Mesh-Based Novel View Synthesis

We evaluate mesh-based novel view synthesis on the MipNeRF360 and Tanks and Temples datasets. Use the training and mesh extraction scripts described above to produce meshes for these datasets, then refer to [MILo](https://github.com/Anttwo/MILo.git) for the evaluation pipeline.

NeRF Synthetic

Details

Note: We only launch the training and extraction with the RaDe-GS rasterizer since it leads to smoother meshes. There are no metrics in this script. We provide it just as an example of our method being applied to synthetic scenes.

To run the NeRF Synthetic benchmark using the RaDe-GS rasterizer:

python gaussian_wrapping/scripts/launch_nerf_synthetic.py \
    --data_dir "${DATA_DIR}" \
    --output_dir "${OUTPUT_DIR}" \
    --gpu_device "${GPU_DEVICE}"
FlagDefaultDescription
--data_dirrequiredPath to the NeRF synthetic scenes
--output_dirrequiredPath to output the results
--gpu_device0CUDA device index

Acknowledgements

We build this project based on Gaussian Splatting.

For rasterization and mesh extraction, we modified CUDA kernels from GGGS and SOF.

We also propose to use rasterization techniques from RaDe-GS.

We finally use Nvdiffrast for mesh rasterization.

We thank the authors of all the above projects for their great works!

Citation

@misc{gomez2026blobsspokeshighfidelitysurface,
      title={From Blobs to Spokes: High-Fidelity Surface Reconstruction via Oriented Gaussians}, 
      author={Diego Gomez and Antoine Guédon and Nissim Maruani and Bingchen Gong and Maks Ovsjanikov},
      year={2026},
      eprint={2604.07337},
      archivePrefix={arXiv},
      primaryClass={cs.CV},
      url={https://arxiv.org/abs/2604.07337}, 
}