From 91a1ddd4208ac991a28ada9b2e43287f2d77c7ed Mon Sep 17 00:00:00 2001 From: Afloat Date: Sat, 7 Nov 2026 22:34:07 +0800 Subject: [PATCH 1/4] Preserve OpenSfM intrinsics when importing ODM cameras --- nerfstudio/process_data/odm_utils.py | 36 +++++++++++++++------------- tests/process_data/test_odm_utils.py | 29 ++++++++++++++++++++++ 2 files changed, 49 insertions(+), 16 deletions(-) create mode 100644 tests/process_data/test_odm_utils.py diff --git a/nerfstudio/process_data/odm_utils.py b/nerfstudio/process_data/odm_utils.py index 70ad1b7f8..f2379ae0d 100644 --- a/nerfstudio/process_data/odm_utils.py +++ b/nerfstudio/process_data/odm_utils.py @@ -104,35 +104,39 @@ def cameras2nerfds( cameras = json.loads(f.read()) camera_ids = list(cameras.keys()) - if len(camera_ids) > 1: - raise ValueError("Only one camera is supported") + if len(camera_ids) != 1: + raise ValueError("Expected exactly one ODM camera") camera_id = camera_ids[0] camera = cameras[camera_id] data = {} - if camera["projection_type"] in ["brown", "perspective"]: + projection_type = camera["projection_type"] + if projection_type in ["brown", "perspective"]: data["camera_model"] = CAMERA_MODELS["perspective"].value - elif camera["projection_type"] in ["fisheye", "fisheye_opencv"]: + elif projection_type in ["fisheye", "fisheye_opencv"]: data["camera_model"] = CAMERA_MODELS["fisheye"].value - elif camera["projection_type"] in ["spherical", "equirectangular"]: + elif projection_type in ["spherical", "equirectangular"]: data["camera_model"] = CAMERA_MODELS["equirectangular"].value else: - raise ValueError("Unsupported ODM camera model: " + data["camera_model"]) + raise ValueError("Unsupported ODM camera model: " + projection_type) - sensor_dict = {} s = {"w": int(camera["width"]), "h": int(camera["height"])} - s["fl_x"] = camera.get("focal_x", camera.get("focal")) * max(s["w"], s["h"]) - s["fl_y"] = camera.get("focal_y", camera.get("focal")) * max(s["w"], s["h"]) + if projection_type in ["spherical", "equirectangular"]: + s["fl_x"] = s["w"] / 2.0 + s["fl_y"] = s["h"] + s["cx"] = s["w"] / 2.0 + s["cy"] = s["h"] / 2.0 + else: + scale = max(s["w"], s["h"]) + s["fl_x"] = camera.get("focal_x", camera.get("focal")) * scale + s["fl_y"] = camera.get("focal_y", camera.get("focal")) * scale + s["cx"] = camera.get("c_x", 0.0) * scale + (s["w"] - 1.0) / 2.0 + s["cy"] = camera.get("c_y", 0.0) * scale + (s["h"] - 1.0) / 2.0 - s["cx"] = camera["c_x"] + (s["w"] - 1.0) / 2.0 - s["cy"] = camera["c_y"] + (s["h"] - 1.0) / 2.0 - - for p in ["k1", "k2", "p1", "p2", "k3"]: + for p in ["k1", "k2", "p1", "p2", "k3", "k4"]: if p in camera: s[p] = camera[p] - sensor_dict[camera_id] = s - shots_dict = {} reconstruction = None @@ -182,7 +186,7 @@ def cameras2nerfds( frame = {} frame["file_path"] = image_filename_map[fname].as_posix() - frame.update(sensor_dict[camera_id]) + frame.update(s) frame["transform_matrix"] = transform.tolist() frames.append(frame) diff --git a/tests/process_data/test_odm_utils.py b/tests/process_data/test_odm_utils.py new file mode 100644 index 000000000..9b4aed6fd --- /dev/null +++ b/tests/process_data/test_odm_utils.py @@ -0,0 +1,29 @@ +"""Regression tests for OpenSfM camera intrinsics in ODM conversions.""" + +import json +from pathlib import Path + +import pytest + +from nerfstudio.process_data.odm_utils import cameras2nerfds + + +@pytest.mark.parametrize( + "camera, expected", + [ + ({"projection_type": "perspective", "focal": 0.5}, {"fl_x": 400, "cx": 399.5, "cy": 299.5}), + ({"projection_type": "brown", "focal": 0.5, "c_x": 0.1, "c_y": -0.05}, {"cx": 479.5, "cy": 259.5}), + ({"projection_type": "fisheye_opencv", "focal": 0.5, "k4": 0.004}, {"k4": 0.004}), + ({"projection_type": "spherical"}, {"fl_x": 400, "fl_y": 600, "cx": 400, "cy": 300}), + ], +) +def test_odm_intrinsics(tmp_path, camera, expected): + cameras_path = tmp_path / "cameras.json" + cameras_path.write_text(json.dumps({"camera": {"width": 800, "height": 600, **camera}})) + reconstruction_path = tmp_path / "reconstruction.json" + shot = {"rotation": [0, 0, 0], "translation": [0, 0, 0]} + reconstruction_path.write_text(json.dumps([{"shots": {"sample.jpg": shot}}])) + cameras2nerfds({"sample": Path("sample.jpg")}, cameras_path, tmp_path / "shots.geojson", reconstruction_path, tmp_path) + frame = json.loads((tmp_path / "transforms.json").read_text())["frames"][0] + for key, value in expected.items(): + assert frame[key] == pytest.approx(value) From fb39bf3fbe2e88149153cfb569180589c8d2744a Mon Sep 17 00:00:00 2001 From: Afloat Date: Sun, 8 Nov 2026 02:23:10 +0800 Subject: [PATCH 2/4] Apply Ruff formatting to the ODM intrinsics test --- tests/process_data/test_odm_utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/process_data/test_odm_utils.py b/tests/process_data/test_odm_utils.py index 9b4aed6fd..f770f40ff 100644 --- a/tests/process_data/test_odm_utils.py +++ b/tests/process_data/test_odm_utils.py @@ -23,7 +23,9 @@ def test_odm_intrinsics(tmp_path, camera, expected): reconstruction_path = tmp_path / "reconstruction.json" shot = {"rotation": [0, 0, 0], "translation": [0, 0, 0]} reconstruction_path.write_text(json.dumps([{"shots": {"sample.jpg": shot}}])) - cameras2nerfds({"sample": Path("sample.jpg")}, cameras_path, tmp_path / "shots.geojson", reconstruction_path, tmp_path) + cameras2nerfds( + {"sample": Path("sample.jpg")}, cameras_path, tmp_path / "shots.geojson", reconstruction_path, tmp_path + ) frame = json.loads((tmp_path / "transforms.json").read_text())["frames"][0] for key, value in expected.items(): assert frame[key] == pytest.approx(value) From ef8afd671cbfe4df2caf4afb4c60aa2b4f72d641 Mon Sep 17 00:00:00 2001 From: Afloat Date: Sun, 8 Nov 2026 02:26:09 +0800 Subject: [PATCH 3/4] Type ODM camera intrinsics to allow fractional values --- nerfstudio/process_data/odm_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nerfstudio/process_data/odm_utils.py b/nerfstudio/process_data/odm_utils.py index f2379ae0d..b9a4bffc8 100644 --- a/nerfstudio/process_data/odm_utils.py +++ b/nerfstudio/process_data/odm_utils.py @@ -119,7 +119,7 @@ def cameras2nerfds( else: raise ValueError("Unsupported ODM camera model: " + projection_type) - s = {"w": int(camera["width"]), "h": int(camera["height"])} + s: Dict[str, float] = {"w": int(camera["width"]), "h": int(camera["height"])} if projection_type in ["spherical", "equirectangular"]: s["fl_x"] = s["w"] / 2.0 From 8f9443f84e094ee57e55984daf6c4cf0f37014b6 Mon Sep 17 00:00:00 2001 From: Afloat Date: Mon, 9 Nov 2026 10:07:18 +0800 Subject: [PATCH 4/4] Cover normalized ODM offsets in a portrait calibration Use a 600 x 800 input to exercise the height as the normalization scale. The expected horizontal principal point is 379.5 pixels, while main 50e0e3c returns 299.6. All five fixtures fail on main and pass with the patch using the real importer. Ruff 0.12.2 lint and format checks pass. The earlier CI run reports the same 19 Pyright diagnostic locations and messages as PR #3797 at the same base. Both Docker runs stop building tiny-cuda-nn because pkg_resources is unavailable. https://github.com/nerfstudio-project/nerfstudio/actions/runs/42401285154 https://github.com/nerfstudio-project/nerfstudio/actions/runs/37900213951 https://github.com/nerfstudio-project/nerfstudio/actions/runs/42401284860 https://github.com/nerfstudio-project/nerfstudio/actions/runs/37900214031 --- tests/process_data/test_odm_utils.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/process_data/test_odm_utils.py b/tests/process_data/test_odm_utils.py index f770f40ff..41b51bc60 100644 --- a/tests/process_data/test_odm_utils.py +++ b/tests/process_data/test_odm_utils.py @@ -13,6 +13,10 @@ from nerfstudio.process_data.odm_utils import cameras2nerfds [ ({"projection_type": "perspective", "focal": 0.5}, {"fl_x": 400, "cx": 399.5, "cy": 299.5}), ({"projection_type": "brown", "focal": 0.5, "c_x": 0.1, "c_y": -0.05}, {"cx": 479.5, "cy": 259.5}), + ( + {"projection_type": "brown", "width": 600, "height": 800, "focal": 0.5, "c_x": 0.1, "c_y": -0.05}, + {"fl_x": 400, "fl_y": 400, "cx": 379.5, "cy": 359.5}, + ), ({"projection_type": "fisheye_opencv", "focal": 0.5, "k4": 0.004}, {"k4": 0.004}), ({"projection_type": "spherical"}, {"fl_x": 400, "fl_y": 600, "cx": 400, "cy": 300}), ],