This repository is the official implementation of the paper "MSVS: Multi-Shell Viewpoint Sampling for Comprehensive Evaluation of 3D Watermarking", accepted at ICASSP 2026.
Reliable copyright protection for 3D assets requires watermark verification across arbitrary viewpoints. However, existing evaluations rely on dataset splits or ad-hoc camera samplings that overlook failure cases. We introduce Multi-Shell Viewpoint Sampling (MSVS), which ensures uniform, distance-aware coverage via concentric, visibility-bounded shells and spherical sampling. MSVS exposes significantly low bit accuracy even on high-quality renderings that an adversary would prefer. Based on this evaluation standard, we further propose a greedy subsampling strategy that selects training views guided by a locality-aware kernel. For 3D-GSW, greedy subsampling improves MSVS bit accuracy by +0.020 on the Blender dataset and +0.056 on the Stanford-ORB dataset over random selection, and the gains persist under common image attacks. MSVS establishes a comprehensive benchmark for 3D watermark evaluation, while greedy subsampling provides an efficient strategy to enhance watermark protection.
Table of Contents
MSVS is a comprehensive evaluation algorithm for 3D watermarking methods based on bit-string embedding into 3D assets.
Unlike existing evaluation methods that rely on dataset splits or ad-hoc camera samplings, MSVS samples viewpoints uniformly in 3D space, which helps to evaluate the watermarking robustness more comprehensively.
uv: Python package manager- CUDA 11.8
Note
This code has been tested with CUDA 11.8. For other versions, please modify
the tool.uv.index in pyproject.toml accordingly.
-
Clone this repository:
git clone https://github.com/Tomoya-Matsubara/msvs.git cd msvs -
Install the required packages using
uv:uv sync
Alternatively, you can add MSVS to an existing uv environment with:
uv add git+https://github.com/Tomoya-Matsubara/msvsFollowing the original 3DGS implementation, the evaluation pipeline consists of the following steps:
- Render the scene with Gaussian splatting.
- Compute the bit accuracy of the rendered images.
Scenes can be rendered using the
render_and_save_scene_images_with_gaussians() function provided in the
render_gaussians module:
import pathlib
import torch
from msvs.render import render_gaussians
from msvs.sampling import point_sampler
# TODO: Replace with actual paths
DATASET_DIRECTORY: pathlib.Path = ...
"""Path to the dataset directory (e.g., dataset/nerf_synthetic/chair)."""
MODEL_DIRECTORY: pathlib.Path = ...
"""Path to the model directory (e.g., gaussian_models/nerf_synthetic/chair)."""
SAVE_DIRECTORY: pathlib.Path = ...
"""Path to the directory where rendered images will be saved (e.g., outputs)."""
device = torch.device("cuda")
sampling_config = point_sampler.MultiShellSampleConfig(
seed=0, device=device, auto_radius=True
)
render_gaussians.render_and_save_scene_images_with_gaussians(
dataset_directory=DATASET_DIRECTORY,
model_directory=MODEL_DIRECTORY,
save_directory=SAVE_DIRECTORY,
scale_spec=1,
load_iteration=7000,
evaluate=True,
debug=False,
compensate_exposure=False,
resolution_scales=[1.0],
inverse_depth_directory_name=None,
device=device,
background_color="black",
compute_3d_covariances_python=False,
convert_shs_python=False,
anti_aliasing=False,
separate_sh=False,
sample_config=sampling_config,
)This function renders the scene using the specified Gaussian splatting
parameters and saves the output images to the designated directory.
By setting sampling_config, an additional dataset split named sampled is
created, allowing for more exhaustive coverage of the scene during rendering
and evaluation.
Once the scene has been rendered, the next step is to compute the bit accuracy of the rendered images.
This can be done using the evaluate_and_save_model_metrics() function in the
metric module:
import torch
from msvs import metrics
def compute_bit_accuracy(image: torch.Tensor) -> float:
# Define your bit accuracy computation logic here
bit_accuracy = ...
return bit_accuracy
metrics.evaluate_and_save_model_metrics(
model_output_directory=SAVE_DIRECTORY,
compute_bit_accuracy=compute_bit_accuracy,
)This function computes the bit accuracy of the rendered images and saves the evaluation metrics to the specified directory.
Note
Because watermark decoders (architectures, parameters, and I/O formats) vary
by method, you need to implement compute_bit_accuracy() accordingly.
The two steps presented above will save rendered images as well as evaluation metrics to the specified directory for easier inspection and debugging. However, in most cases, the involvement of disk I/O can slow down the evaluation process.
If you prefer a faster evaluation without saving intermediate images to disk,
you can use the evaluate_and_save_model_metrics_fast() function in the
metrics_fast module:
import pathlib
import torch
from msvs import metrics_fast
from msvs.sampling import point_sampler
# TODO: Replace with actual paths
DATASET_DIRECTORY: pathlib.Path = ...
"""Path to the dataset directory (e.g., dataset/nerf_synthetic/chair)."""
MODEL_DIRECTORY: pathlib.Path = ...
"""Path to the model directory (e.g., gaussian_models/nerf_synthetic/chair)."""
SAVE_DIRECTORY: pathlib.Path = ...
"""Path to the directory where evaluation metrics will be saved (e.g., outputs)."""
def compute_bit_accuracy(image: torch.Tensor) -> float:
# Define your bit accuracy computation logic here
bit_accuracy = ...
return bit_accuracy
device = torch.device("cuda")
sampling_config = point_sampler.MultiShellSampleConfig(
seed=0, device=device, auto_radius=True
)
metrics_fast.evaluate_and_save_model_metrics_fast(
dataset_directory=DATASET_DIRECTORY,
model_directory=MODEL_DIRECTORY,
save_directory=SAVE_DIRECTORY,
scale_spec=1,
load_iteration=7000,
compute_bit_accuracy=compute_bit_accuracy,
evaluate=True,
debug=False,
compensate_exposure=False,
resolution_scales=[1.0],
inverse_depth_directory_name=None,
device=device,
background_color="black",
compute_3d_covariances_python=False,
convert_shs_python=False,
anti_aliasing=False,
separate_sh=False,
sample_config=sampling_config,
save_sampled_cameras=False,
)The current implementation targets Gaussian splatting, but it can be extended to other neural rendering techniques (e.g., NeRF, mip-NeRF) by subclassing the following base classes:
renderer_base.RendererBase: Base class for all renderer implementations.scene.Scene: Base class for all scene representations.
After providing concrete subclasses, you can render scenes using
render_and_save_scene_images() from the render module:
from msvs import scene
from msvs.render import render, renderer_base
# Your concrete implementations must subclass the base classes and
# fulfill their contracts.
class MyMethodRenderer(renderer_base.RendererBase):
...
# Implement all required abstract methods defined by RendererBase.
class MyMethodScene(scene.Scene):
...
# Scene is not an abstract class. Just make sure to implement member variables.
renderer: renderer_base.RendererBase = MyMethodRenderer()
target_scene: scene.Scene = MyMethodScene()
render.render_and_save_scene_images(
renderer=renderer, target_scene=target_scene, save_directory=SAVE_DIRECTORY
)Bit accuracy computation follows the same process described above.
In addition to the evaluation pipeline, this repository provides utilities for subsampling cameras to augment the training set and efficiently improve watermark coverage.
Once the sampled split is prepared, call subsample_cameras() to choose a
subset of cameras for training.
from msvs.sampling import subsampler
subsampler.subsample_cameras(
sampled_directory=SAVE_DIRECTORY / "sampled",
dataset_directory=DATASET_DIRECTORY,
scale_spec=1,
num_subsample=200
)The subsampled cameras can be loaded using load_subsampled_cameras()
in the camera_loader module:
from msvs.sampling import camera_loader
loaded_cameras = camera_loader.load_subsampled_cameras(
sampled_directory=SAVE_DIRECTORY / "sampled",
is_nerf_synthetic=True,
device=torch.device("cuda"),
)As a simple baseline, random subsampling is also supported. To randomly select
a subset of cameras from the sampled split, use the
random_load_sampled_cameras() function in the camera_loader module:
from msvs.sampling import camera_loader
loaded_cameras = camera_loader.random_load_sampled_cameras(
sampled_directory=SAVE_DIRECTORY / "sampled",
num_sample=200,
is_nerf_synthetic=True,
device=torch.device("cuda"),
seed=42,
)Sometimes, a dataset consists of multiple scenes (e.g., the NeRF synthetic dataset). In such cases, you may want to aggregate evaluation metrics across all scenes.
Suppose you have evaluated multiple scenes and stored their results in a common directory structure like this:
results/
├── chair/
│ ├── sampled/
│ │ └── result.json
│ ├── test
│ │ └── result.json
│ └── train
│ └── result.json
├── drums
│ └── ... (similar structure as chair)
...
└── ship
└── ...Then, you can use the aggregate_dataset_results() function in the
results_utils module to aggregate the metrics:
from msvs.utils import results_utils
dataset_results_directory = ...
results_utils.aggregate_dataset_results(dataset_results_directory)This function reads the JSON files from each split of each scene, computes the average metrics, and saves the aggregated results in the same results directory.
For more information on how to use this repository, please refer to the
examples/ directory, which contains an example script for
MSVS evaluation.