Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion nerfstudio/process_data/colmap_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,24 @@ def colmap_to_json(

name = im_data.name
if image_rename_map is not None:
name = image_rename_map[name]
# COLMAP stores im_data.name relative to the image_path it was
# given (e.g. "rig1/front/img.jpg"). The rename map is keyed by
# the source path relative to self.data, which can include an
# extra "images/" prefix when --data points at a parent
# containing an images/ subdirectory (common with --skip-colmap
# workflows). Try the bare name first, then the prefixed form.
# See https://github.com/nerfstudio-project/nerfstudio/issues/3759
if name in image_rename_map:
name = image_rename_map[name]
elif f"images/{name}" in image_rename_map:
name = image_rename_map[f"images/{name}"]
else:
raise KeyError(
f"COLMAP image name {name!r} not in image_rename_map. "
f"Tried {name!r} and 'images/' + {name!r}. "
f"The map has {len(image_rename_map)} entries; "
f"first 3 keys: {list(image_rename_map.keys())[:3]}."
)
name = Path(f"./images/{name}")

frame = {
Expand Down
72 changes: 72 additions & 0 deletions tests/process_data/test_process_images.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,3 +209,75 @@ def test_process_images_recursively_skip_colmap(tmp_path: Path):
)
dataparser_poses = np.linalg.inv(dataparser_poses)
np.testing.assert_allclose(original_poses, dataparser_poses, rtol=0, atol=1e-5)


def test_process_images_data_parent_skip_colmap(tmp_path: Path):
"""
Regression test for #3759: when --data points at a directory that contains
an `images/` subdirectory (a common layout for users running COLMAP externally
against `<data>/images/...` with --skip-colmap), the image_rename_map keys
were prefixed with `images/` while COLMAP's im_data.name was the bare path,
producing a KeyError in colmap_to_json. The fix accepts both key forms.
"""
width = 100
height = 150
sparse_path = tmp_path / "sparse" / "0"
sparse_path.mkdir(exist_ok=True, parents=True)
# IMPORTANT: images live under <tmp_path>/images/ AND --data is <tmp_path>
# (the parent). This is the layout that triggered #3759.
(tmp_path / "images").mkdir(exist_ok=True, parents=True)
write_cameras_binary(
{1: Camera(1, "OPENCV", width, height, [110, 110, 50, 75, 0, 0, 0, 0, 0, 0])},
sparse_path / "cameras.bin",
)
write_points3D_binary(
{
1: Point3D(
id=1,
xyz=np.array([0, 0, 0]),
rgb=np.array([0, 0, 0]),
error=np.array([0]),
image_ids=np.array([1]),
point2D_idxs=np.array([0]),
),
},
sparse_path / "points3D.bin",
)
frames = {}
num_frames = 5
qvecs = random_quaternion(num_frames)
tvecs = np.random.uniform(size=(num_frames, 3))
for i in range(num_frames):
# COLMAP stores the bare relative name (no "images/" prefix) because
# the user ran COLMAP with image_path=<tmp_path>/images.
frames[i + 1] = ColmapImage(i + 1, qvecs[i], tvecs[i], 1, f"image_{i}.png", [], [])
Image.new("RGB", (width, height)).save(tmp_path / "images" / f"image_{i}.png")
write_images_binary(frames, sparse_path / "images.bin")

# Mock missing COLMAP and ffmpeg in the dev env
old_path = os.environ.get("PATH", "")
os.environ["PATH"] = str(tmp_path / "mocked_bin") + f":{old_path}"
(tmp_path / "mocked_bin").mkdir()
(tmp_path / "mocked_bin" / "colmap").touch(mode=0o777)
(tmp_path / "mocked_bin" / "ffmpeg").touch(mode=0o777)

# Convert: --data points at <tmp_path> (the parent of images/), NOT at
# <tmp_path>/images/ like the existing tests do.
cmd = ImagesToNerfstudioDataset(
data=tmp_path, output_dir=tmp_path / "nerfstudio", colmap_model_path=sparse_path, skip_colmap=True
)
cmd.main()
os.environ["PATH"] = old_path

# Without the fix: KeyError before transforms.json is written.
assert (tmp_path / "nerfstudio" / "transforms.json").exists()
parser = NerfstudioDataParserConfig(
data=tmp_path / "nerfstudio",
downscale_factor=None,
orientation_method="none",
center_method="none",
auto_scale_poses=False,
).setup()
outputs = parser.get_dataparser_outputs("train")
# All 5 frames should round-trip (allowing for the parser's train/eval split).
assert len(outputs.image_filenames) >= 1
Loading