Skip to content

Commit 1b9dfe8

Browse files
authored
Support COLMAP reconstructions with non-flat image dirs (#3006)
* Support COLMAP reconstructions with nested image dirs The code was previously looking for the file.name, which drops all directory information from the path. Instead, we want to get the file path relative to the data dir, which will only drop parts of the path above the data dir, allowing us to recurse into subdirectories within the images dir * Adhere to style guidelines * Add testcase to cover image dirs with subdirs
1 parent a990721 commit 1b9dfe8

File tree

3 files changed

+91
-2
lines changed

3 files changed

+91
-2
lines changed

nerfstudio/process_data/images_to_nerfstudio_dataset.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,9 @@ def main(self) -> None:
8787
)
8888
image_rename_map_paths.update(eval_image_rename_map_paths)
8989

90-
image_rename_map = dict((a.name, b.name) for a, b in image_rename_map_paths.items())
90+
image_rename_map = dict(
91+
(a.relative_to(self.data).as_posix(), b.name) for a, b in image_rename_map_paths.items()
92+
)
9193
num_frames = len(image_rename_map)
9294
summary_log.append(f"Starting with {num_frames} images")
9395

nerfstudio/process_data/process_data_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ class CameraModel(Enum):
6262
}
6363

6464

65-
def list_images(data: Path, recursive: bool = False) -> List[Path]:
65+
def list_images(data: Path, recursive: bool = True) -> List[Path]:
6666
"""Lists all supported images in a directory
6767
6868
Args:

tests/process_data/test_process_images.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,90 @@ def test_process_images_skip_colmap(tmp_path: Path):
121121
)
122122
dataparser_poses = np.linalg.inv(dataparser_poses)
123123
np.testing.assert_allclose(gt_poses, dataparser_poses, rtol=0, atol=1e-5)
124+
125+
126+
def test_process_images_recursively_skip_colmap(tmp_path: Path):
127+
"""
128+
Test ns-process-data images when images contains subdirectories"
129+
"""
130+
# Mock a colmap sparse model
131+
width = 100
132+
height = 150
133+
sparse_path = tmp_path / "sparse" / "0"
134+
sparse_path.mkdir(exist_ok=True, parents=True)
135+
(tmp_path / "images").mkdir(exist_ok=True, parents=True)
136+
write_cameras_binary(
137+
{1: Camera(1, "OPENCV", width, height, [110, 110, 50, 75, 0, 0, 0, 0, 0, 0])},
138+
sparse_path / "cameras.bin",
139+
)
140+
write_points3D_binary(
141+
{
142+
1: Point3D(
143+
id=1,
144+
xyz=np.array([0, 0, 0]),
145+
rgb=np.array([0, 0, 0]),
146+
error=np.array([0]),
147+
image_ids=np.array([1]),
148+
point2D_idxs=np.array([0]),
149+
),
150+
},
151+
sparse_path / "points3D.bin",
152+
)
153+
frames = {}
154+
num_frames = 9
155+
num_subdirs = 3
156+
qvecs = random_quaternion(num_frames)
157+
tvecs = np.random.uniform(size=(num_frames, 3))
158+
original_poses = np.concatenate(
159+
(
160+
np.concatenate(
161+
(
162+
np.stack(list(map(qvec2rotmat, qvecs))),
163+
tvecs[:, :, None],
164+
),
165+
-1,
166+
),
167+
np.array([[[0, 0, 0, 1]]], dtype=qvecs.dtype).repeat(num_frames, 0),
168+
),
169+
-2,
170+
)
171+
for i in range(num_frames):
172+
subdir = f"subdir_{num_frames // num_subdirs}"
173+
frames[i + 1] = ColmapImage(i + 1, qvecs[i], tvecs[i], 1, f"{subdir}/image_{i}.png", [], [])
174+
(tmp_path / "images" / subdir).mkdir(parents=True, exist_ok=True)
175+
Image.new("RGB", (width, height)).save(tmp_path / "images" / subdir / f"image_{i}.png")
176+
write_images_binary(frames, sparse_path / "images.bin")
177+
178+
# Mock missing COLMAP and ffmpeg in the dev env
179+
old_path = os.environ.get("PATH", "")
180+
os.environ["PATH"] = str(tmp_path / "mocked_bin") + f":{old_path}"
181+
(tmp_path / "mocked_bin").mkdir()
182+
(tmp_path / "mocked_bin" / "colmap").touch(mode=0o777)
183+
(tmp_path / "mocked_bin" / "ffmpeg").touch(mode=0o777)
184+
185+
# Convert images into a NerfStudio dataset
186+
cmd = ImagesToNerfstudioDataset(
187+
data=tmp_path / "images", output_dir=tmp_path / "nerfstudio", colmap_model_path=sparse_path, skip_colmap=True
188+
)
189+
cmd.main()
190+
os.environ["PATH"] = old_path
191+
192+
assert (tmp_path / "nerfstudio" / "transforms.json").exists()
193+
parser = NerfstudioDataParserConfig(
194+
data=tmp_path / "nerfstudio",
195+
downscale_factor=None,
196+
orientation_method="none", # orientation_method,
197+
center_method="none",
198+
auto_scale_poses=False,
199+
).setup()
200+
outputs = parser.get_dataparser_outputs("train")
201+
assert len(outputs.image_filenames) == 9
202+
assert torch.is_tensor(outputs.dataparser_transform)
203+
204+
# Test if the original poses can be obtained back
205+
dataparser_poses = outputs.transform_poses_to_original_space(outputs.cameras.camera_to_worlds, "opencv").numpy()
206+
dataparser_poses = np.concatenate(
207+
(dataparser_poses, np.array([[[0, 0, 0, 1]]]).repeat(len(dataparser_poses), 0)), 1
208+
)
209+
dataparser_poses = np.linalg.inv(dataparser_poses)
210+
np.testing.assert_allclose(original_poses, dataparser_poses, rtol=0, atol=1e-5)

0 commit comments

Comments
 (0)