4
0
mirror of https://github.com/nerfstudio-project/nerfstudio.git synced 2026-11-10 18:17:31 +08:00

Add image tiling option to ColmapDataParser to use less memory (#3685)

Add tiling option to ColmapDataParser
This commit is contained in:
couperle 2025-07-28 19:28:48 -07:00 committed by GitHub
parent 04a6cb6268
commit 50e0e3c70c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 163 additions and 12 deletions

View File

@ -1021,3 +1021,34 @@ class Cameras(TensorDataclass):
self.width = torch.ceil(self.width * scaling_factor).to(torch.int64)
else:
raise ValueError("Scale rounding mode must be 'floor', 'round' or 'ceil'.")
def update_tiling_intrinsics(self, tiling_factor: int) -> None:
"""
Update camera intrinsics based on tiling_factor.
Must match tiling logic as defined in dataparser.
Args:
tiling_factor: Tiling factor to apply to the camera intrinsics.
"""
if tiling_factor == 1:
return
num_tiles = tiling_factor**2
# Compute tile sizes
base_tile_w, remainder_w = self.width // tiling_factor, self.width % tiling_factor
base_tile_h, remainder_h = self.height // tiling_factor, self.height % tiling_factor
tile_indices = torch.arange(len(self.cx), device=self.cx.device).unsqueeze(1) % num_tiles
row_indices, col_indices = tile_indices // tiling_factor, tile_indices % tiling_factor
x_offsets = col_indices * base_tile_w + torch.minimum(col_indices, remainder_w)
y_offsets = row_indices * base_tile_h + torch.minimum(row_indices, remainder_h)
# Adjust principal points
self.cx = self.cx - x_offsets
self.cy = self.cy - y_offsets
# Adjust height/width
self.width = base_tile_w + (col_indices < remainder_w).to(torch.int)
self.height = base_tile_h + (row_indices < remainder_h).to(torch.int)

View File

@ -59,6 +59,8 @@ class ColmapDataParserConfig(DataParserConfig):
"""How much to downscale images. If not set, images are chosen such that the max dimension is <1600px."""
downscale_rounding_mode: Literal["floor", "round", "ceil"] = "floor"
"""How to round downscale image height and Image width."""
tiling_factor: int = 1
"""Tile images into n^2 equal-resolution images, where n is this number. n | H, n | W for image with resolution WxH"""
scene_scale: float = 1.0
"""How much to scale the region of interest by."""
orientation_method: Literal["pca", "up", "vertical", "none"] = "up"
@ -115,7 +117,8 @@ class ColmapDataParser(DataParser):
The dataparser loads the downscaled images from folders with `_{downscale_factor}` suffix.
If these folders do not exist, the user can choose to automatically downscale the images and
create these folders.
create these folders. If tiling_factor > 1, the images are instead loaded from folders with
`_tiled_{tiling_factor}` suffix.
The loader is compatible with the datasets processed using the ns-process-data script and
can be used as a drop-in replacement. It further supports datasets like Mip-NeRF 360 (although
@ -327,13 +330,26 @@ class ColmapDataParser(DataParser):
image_filenames, mask_filenames, depth_filenames, downscale_factor = self._setup_downscale_factor(
image_filenames, mask_filenames, depth_filenames
)
image_filenames, mask_filenames, depth_filenames = self._setup_tiling(
image_filenames, mask_filenames, depth_filenames
)
image_filenames = [image_filenames[i] for i in indices]
mask_filenames = [mask_filenames[i] for i in indices] if len(mask_filenames) > 0 else []
depth_filenames = [depth_filenames[i] for i in indices] if len(depth_filenames) > 0 else []
num_tiles = self.config.tiling_factor**2
image_filenames = [image_filenames[i * num_tiles + j] for i in indices for j in range(num_tiles)]
mask_filenames = (
[mask_filenames[i * num_tiles + j] for i in indices for j in range(num_tiles)]
if len(mask_filenames) > 0
else []
)
depth_filenames = (
[depth_filenames[i * num_tiles + j] for i in indices for j in range(num_tiles)]
if len(depth_filenames) > 0
else []
)
idx_tensor = torch.tensor(indices, dtype=torch.long)
poses = poses[idx_tensor]
poses = poses[idx_tensor].repeat_interleave(num_tiles, dim=0)
# in x,y,z order
# assumes that the scene is centered at the origin
@ -344,13 +360,13 @@ class ColmapDataParser(DataParser):
)
)
fx = torch.tensor(fx, dtype=torch.float32)[idx_tensor]
fy = torch.tensor(fy, dtype=torch.float32)[idx_tensor]
cx = torch.tensor(cx, dtype=torch.float32)[idx_tensor]
cy = torch.tensor(cy, dtype=torch.float32)[idx_tensor]
height = torch.tensor(height, dtype=torch.int32)[idx_tensor]
width = torch.tensor(width, dtype=torch.int32)[idx_tensor]
distortion_params = torch.stack(distort, dim=0)[idx_tensor]
fx = torch.tensor(fx, dtype=torch.float32)[idx_tensor].repeat_interleave(num_tiles)
fy = torch.tensor(fy, dtype=torch.float32)[idx_tensor].repeat_interleave(num_tiles)
cx = torch.tensor(cx, dtype=torch.float32)[idx_tensor].repeat_interleave(num_tiles)
cy = torch.tensor(cy, dtype=torch.float32)[idx_tensor].repeat_interleave(num_tiles)
height = torch.tensor(height, dtype=torch.int32)[idx_tensor].repeat_interleave(num_tiles)
width = torch.tensor(width, dtype=torch.int32)[idx_tensor].repeat_interleave(num_tiles)
distortion_params = torch.stack(distort, dim=0)[idx_tensor].repeat_interleave(num_tiles, dim=0)
cameras = Cameras(
fx=fx,
@ -364,6 +380,7 @@ class ColmapDataParser(DataParser):
camera_type=camera_type,
)
cameras.update_tiling_intrinsics(tiling_factor=self.config.tiling_factor)
cameras.rescale_output_resolution(
scaling_factor=1.0 / downscale_factor, scale_rounding_mode=self.config.downscale_rounding_mode
)
@ -464,6 +481,109 @@ class ColmapDataParser(DataParser):
out["points3D_points2D_xy"] = torch.stack(points3D_image_xy, dim=0)
return out
def _tile_images(self, paths, get_fname, tiling_factor):
"""
Tile images into self.tiling_factor^2 tiles.
Logic must match intrinsics update in Cameras object.
"""
with status(msg="[bold yellow]Tiling images...", spinner="growVertical"):
assert isinstance(tiling_factor, int)
assert tiling_factor > 1
for path in paths:
img = Image.open(path)
w, h = img.size
base_tile_w, remainder_w = divmod(w, tiling_factor)
base_tile_h, remainder_h = divmod(h, tiling_factor)
path_out_base = get_fname(path)
path_out_base.parent.mkdir(parents=True, exist_ok=True)
for row in range(tiling_factor):
for col in range(tiling_factor):
idx = row * tiling_factor + col
# Distribute the remainder among the first remainder_w columns and remainder_h rows
tile_w = base_tile_w + int(col < remainder_w)
tile_h = base_tile_h + int(row < remainder_h)
x_offset = col * base_tile_w + min(col, remainder_w)
y_offset = row * base_tile_h + min(row, remainder_h)
tile = img.crop(
(
x_offset,
y_offset,
x_offset + tile_w,
y_offset + tile_h,
)
)
output_path = path_out_base.with_stem(path_out_base.stem + f"_{idx}")
tile.save(output_path)
CONSOLE.log("[bold green]:tada: Done tiling images.")
def _setup_tiling(self, image_filenames: List[Path], mask_filenames: List[Path], depth_filenames: List[Path]):
"""
Wrapper around self._tile_images() to handle tiling of image, mask, and depth files.
"""
if self.config.tiling_factor == 1:
return image_filenames, mask_filenames, depth_filenames
assert self._downscale_factor == 1, "Tiling not supported with downscaling, please set --downscale_factor=1"
def get_fname(parent: Path, filepath: Path) -> Path:
"""Returns transformed file name when tiling factor is applied"""
rel_part = filepath.relative_to(parent)
base_part = parent.parent / (str(parent.name) + f"_tiled_{self.config.tiling_factor}")
return base_part / rel_part
if not all(get_fname(self.config.data / self.config.images_path, fp).parent.exists() for fp in image_filenames):
self._tile_images(
image_filenames,
partial(get_fname, self.config.data / self.config.images_path),
self.config.tiling_factor,
)
if len(mask_filenames) > 0:
assert self.config.masks_path is not None
self._tile_images(
mask_filenames,
partial(get_fname, self.config.data / self.config.masks_path),
self.config.tiling_factor,
)
if len(depth_filenames) > 0:
assert self.config.depths_path is not None
self._tile_images(
depth_filenames,
partial(get_fname, self.config.data / self.config.depths_path),
self.config.tiling_factor,
)
num_tiles = self.config.tiling_factor**2
image_filenames = [
get_fname(self.config.data / self.config.images_path, fp.with_stem(fp.stem + f"_{i}"))
for fp in image_filenames
for i in range(num_tiles)
]
if len(mask_filenames) > 0:
assert self.config.masks_path is not None
mask_filenames = [
get_fname(self.config.data / self.config.masks_path, fp.with_stem(fp.stem + f"_{i}"))
for fp in mask_filenames
for i in range(num_tiles)
]
if len(depth_filenames) > 0:
assert self.config.depths_path is not None
depth_filenames = [
get_fname(self.config.data / self.config.depths_path, fp.with_stem(fp.stem + f"_{i}"))
for fp in depth_filenames
for i in range(num_tiles)
]
return image_filenames, mask_filenames, depth_filenames
def _downscale_images(
self,
paths,