nvs2colmap 0.4.1__tar.gz → 0.5.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {nvs2colmap-0.4.1 → nvs2colmap-0.5.2}/PKG-INFO +1 -1
- nvs2colmap-0.5.2/nvs2colmap/blender/__main__.py +54 -0
- nvs2colmap-0.5.2/nvs2colmap/blender/render.py +42 -0
- nvs2colmap-0.5.2/nvs2colmap/blender/sample_cameras.py +101 -0
- {nvs2colmap-0.4.1 → nvs2colmap-0.5.2}/nvs2colmap.egg-info/PKG-INFO +1 -1
- {nvs2colmap-0.4.1 → nvs2colmap-0.5.2}/nvs2colmap.egg-info/SOURCES.txt +6 -0
- {nvs2colmap-0.4.1 → nvs2colmap-0.5.2}/nvs2colmap.egg-info/entry_points.txt +2 -0
- {nvs2colmap-0.4.1 → nvs2colmap-0.5.2}/pyproject.toml +3 -1
- {nvs2colmap-0.4.1 → nvs2colmap-0.5.2}/LICENSE +0 -0
- {nvs2colmap-0.4.1 → nvs2colmap-0.5.2}/README.md +0 -0
- {nvs2colmap-0.4.1 → nvs2colmap-0.5.2}/nvs2colmap/__init__.py +0 -0
- {nvs2colmap-0.4.1 → nvs2colmap-0.5.2}/nvs2colmap/colmap.py +0 -0
- {nvs2colmap-0.4.1 → nvs2colmap-0.5.2}/nvs2colmap/dynamic3dgs/__main__.py +0 -0
- {nvs2colmap-0.4.1 → nvs2colmap-0.5.2}/nvs2colmap/dynamic3dgs/camera_meta.py +0 -0
- {nvs2colmap-0.4.1 → nvs2colmap-0.5.2}/nvs2colmap/dynamic3dgs/link_frames.py +0 -0
- {nvs2colmap-0.4.1 → nvs2colmap-0.5.2}/nvs2colmap/dynamic3dgs/points.py +0 -0
- {nvs2colmap-0.4.1 → nvs2colmap-0.5.2}/nvs2colmap/extract_videos.py +0 -0
- {nvs2colmap-0.4.1 → nvs2colmap-0.5.2}/nvs2colmap/n3dv/__main__.py +0 -0
- {nvs2colmap-0.4.1 → nvs2colmap-0.5.2}/nvs2colmap/n3dv/extract_videos.py +0 -0
- {nvs2colmap-0.4.1 → nvs2colmap-0.5.2}/nvs2colmap/n3dv/poses_bounds.py +0 -0
- {nvs2colmap-0.4.1 → nvs2colmap-0.5.2}/nvs2colmap/stnerf/__init__.py +0 -0
- {nvs2colmap-0.4.1 → nvs2colmap-0.5.2}/nvs2colmap/utils/__init__.py +0 -0
- {nvs2colmap-0.4.1 → nvs2colmap-0.5.2}/nvs2colmap/utils/colmap.py +0 -0
- {nvs2colmap-0.4.1 → nvs2colmap-0.5.2}/nvs2colmap/utils/ffmpeg.py +0 -0
- {nvs2colmap-0.4.1 → nvs2colmap-0.5.2}/nvs2colmap/utils/rotation.py +0 -0
- {nvs2colmap-0.4.1 → nvs2colmap-0.5.2}/nvs2colmap/write_model.py +0 -0
- {nvs2colmap-0.4.1 → nvs2colmap-0.5.2}/nvs2colmap/write_pcd.py +0 -0
- {nvs2colmap-0.4.1 → nvs2colmap-0.5.2}/nvs2colmap.egg-info/dependency_links.txt +0 -0
- {nvs2colmap-0.4.1 → nvs2colmap-0.5.2}/nvs2colmap.egg-info/requires.txt +0 -0
- {nvs2colmap-0.4.1 → nvs2colmap-0.5.2}/nvs2colmap.egg-info/top_level.txt +0 -0
- {nvs2colmap-0.4.1 → nvs2colmap-0.5.2}/setup.cfg +0 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Render a Blender file into an n3dv scene directory."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from nvs2colmap.n3dv.poses_bounds import read_camera_meta_n3dv
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main() -> None:
|
|
14
|
+
parser = argparse.ArgumentParser(
|
|
15
|
+
description="Render a .blend file into camera videos next to poses_bounds.npy."
|
|
16
|
+
)
|
|
17
|
+
parser.add_argument("--path", type=Path, required=True, help="Scene directory containing poses_bounds.npy.")
|
|
18
|
+
parser.add_argument("--blend", type=Path, required=True, help="Blender file to render.")
|
|
19
|
+
parser.add_argument("--blender", default="blender", help="Blender executable.")
|
|
20
|
+
args = parser.parse_args()
|
|
21
|
+
|
|
22
|
+
folder = args.path.resolve()
|
|
23
|
+
n_cameras, Rs, Ts, hwf, _ = read_camera_meta_n3dv(folder)
|
|
24
|
+
w2c = np.tile(np.eye(4), (n_cameras, 1, 1))
|
|
25
|
+
w2c[:, :3, :3] = Rs.numpy()
|
|
26
|
+
w2c[:, :3, 3] = Ts.numpy()
|
|
27
|
+
# OpenCV camera (+Z forward, +Y down) to Blender camera (-Z forward, +Y up).
|
|
28
|
+
c2w = np.linalg.inv(w2c) @ np.diag([1.0, -1.0, -1.0, 1.0])
|
|
29
|
+
hwf = hwf.numpy()
|
|
30
|
+
pad = max(2, len(str(n_cameras - 1)))
|
|
31
|
+
cameras = [
|
|
32
|
+
[f"cam{i:0{pad}d}", int(round(float(hwf[i, 0]))), int(round(float(hwf[i, 1]))), float(hwf[i, 2]), c2w[i].tolist()]
|
|
33
|
+
for i in range(n_cameras)
|
|
34
|
+
]
|
|
35
|
+
subprocess.run(
|
|
36
|
+
[
|
|
37
|
+
args.blender,
|
|
38
|
+
str(args.blend.resolve()),
|
|
39
|
+
"--background",
|
|
40
|
+
"--python",
|
|
41
|
+
str(Path(__file__).with_name("render.py")),
|
|
42
|
+
"--",
|
|
43
|
+
"--output",
|
|
44
|
+
str(folder),
|
|
45
|
+
"--cameras",
|
|
46
|
+
json.dumps(cameras, separators=(",", ":")),
|
|
47
|
+
],
|
|
48
|
+
check=True,
|
|
49
|
+
)
|
|
50
|
+
print(f"Done: {folder}")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
if __name__ == "__main__":
|
|
54
|
+
main()
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Render one video per camera. Executed by Blender, not by the project interpreter."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import bpy
|
|
9
|
+
import mathutils
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main() -> None:
|
|
13
|
+
parser = argparse.ArgumentParser()
|
|
14
|
+
parser.add_argument("--output", type=Path, required=True)
|
|
15
|
+
parser.add_argument("--cameras", type=json.loads, required=True)
|
|
16
|
+
args = parser.parse_args(sys.argv[sys.argv.index("--") + 1:])
|
|
17
|
+
|
|
18
|
+
scene = bpy.context.scene
|
|
19
|
+
cam_obj = scene.camera
|
|
20
|
+
cam_obj.animation_data_clear()
|
|
21
|
+
cam = cam_obj.data
|
|
22
|
+
cam.sensor_fit = "HORIZONTAL"
|
|
23
|
+
cam.sensor_width = 36.0
|
|
24
|
+
scene.render.resolution_percentage = 100
|
|
25
|
+
scene.render.image_settings.file_format = "FFMPEG"
|
|
26
|
+
scene.render.ffmpeg.format = "MPEG4"
|
|
27
|
+
scene.render.ffmpeg.codec = "H264"
|
|
28
|
+
scene.render.ffmpeg.constant_rate_factor = "MEDIUM"
|
|
29
|
+
scene.render.ffmpeg.audio_codec = "NONE"
|
|
30
|
+
scene.render.use_file_extension = False
|
|
31
|
+
|
|
32
|
+
for name, height, width, focal, c2w in args.cameras:
|
|
33
|
+
scene.render.resolution_x = width
|
|
34
|
+
scene.render.resolution_y = height
|
|
35
|
+
cam.lens = focal * cam.sensor_width / width
|
|
36
|
+
cam_obj.matrix_world = mathutils.Matrix(c2w)
|
|
37
|
+
scene.render.filepath = str(args.output / f"{name}.mp4")
|
|
38
|
+
bpy.ops.render.render(animation=True)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
if __name__ == "__main__":
|
|
42
|
+
main()
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Sample cameras on a spherical cap and write poses_bounds.npy."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def cap_direction(axis, cap_angle, rng):
|
|
11
|
+
z = rng.uniform(np.cos(cap_angle), 1.0)
|
|
12
|
+
phi = rng.uniform(0, 2 * np.pi)
|
|
13
|
+
xy = np.sqrt(1 - z * z)
|
|
14
|
+
local = np.array([xy * np.cos(phi), xy * np.sin(phi), z])
|
|
15
|
+
helper = np.array([0.0, 1.0, 0.0]) if abs(axis[2]) > 0.999 else np.array([0.0, 0.0, 1.0])
|
|
16
|
+
tangent = np.cross(helper, axis)
|
|
17
|
+
tangent /= np.linalg.norm(tangent)
|
|
18
|
+
bitangent = np.cross(axis, tangent)
|
|
19
|
+
return tangent * local[0] + bitangent * local[1] + axis * local[2]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def main() -> None:
|
|
23
|
+
parser = argparse.ArgumentParser()
|
|
24
|
+
parser.add_argument("--output", type=Path, required=True)
|
|
25
|
+
parser.add_argument("--n-cameras", type=int, required=True)
|
|
26
|
+
parser.add_argument("--height", type=int, required=True)
|
|
27
|
+
parser.add_argument("--width", type=int, required=True)
|
|
28
|
+
parser.add_argument("--focal", type=float, required=True, help="Focal length in pixels. fx = fy, principal point at the image center.")
|
|
29
|
+
parser.add_argument("--center", type=float, nargs=3, required=True, help="Shell center and look-at point.")
|
|
30
|
+
parser.add_argument("--radius-min", type=float, default=10.0)
|
|
31
|
+
parser.add_argument("--radius-max", type=float, default=12.0)
|
|
32
|
+
parser.add_argument("--azimuth", type=float, default=0.0, help="Cap axis azimuth in degrees.")
|
|
33
|
+
parser.add_argument("--elevation", type=float, default=90.0, help="Cap axis elevation in degrees. 90 looks straight up.")
|
|
34
|
+
parser.add_argument("--cap-angle", type=float, default=90.0, help="Cap half-angle in degrees. 90 is a hemisphere; smaller clusters cameras on one side.")
|
|
35
|
+
parser.add_argument("--min-angle", type=float, default=10.0, help="Minimum angle in degrees between a camera and its nearest neighbor.")
|
|
36
|
+
parser.add_argument("--max-angle", type=float, default=45.0, help="Maximum angle in degrees between a camera and its nearest neighbor.")
|
|
37
|
+
argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else sys.argv[1:]
|
|
38
|
+
args = parser.parse_args(argv)
|
|
39
|
+
|
|
40
|
+
azimuth = np.deg2rad(args.azimuth)
|
|
41
|
+
elevation = np.deg2rad(args.elevation)
|
|
42
|
+
axis = np.array([
|
|
43
|
+
np.cos(elevation) * np.cos(azimuth),
|
|
44
|
+
np.cos(elevation) * np.sin(azimuth),
|
|
45
|
+
np.sin(elevation),
|
|
46
|
+
])
|
|
47
|
+
cap_angle = np.deg2rad(args.cap_angle)
|
|
48
|
+
min_angle = np.deg2rad(args.min_angle)
|
|
49
|
+
max_angle = np.deg2rad(args.max_angle)
|
|
50
|
+
center = np.array(args.center)
|
|
51
|
+
rng = np.random.default_rng()
|
|
52
|
+
|
|
53
|
+
directions = []
|
|
54
|
+
radii = []
|
|
55
|
+
tries = 0
|
|
56
|
+
while len(directions) < args.n_cameras:
|
|
57
|
+
tries += 1
|
|
58
|
+
if tries > 100:
|
|
59
|
+
raise RuntimeError("Could not place that many cameras on this cap.")
|
|
60
|
+
direction = cap_direction(axis, cap_angle, rng)
|
|
61
|
+
if directions:
|
|
62
|
+
nearest = np.arccos(np.clip(np.stack(directions) @ direction, -1.0, 1.0)).min()
|
|
63
|
+
if nearest < min_angle or nearest > max_angle:
|
|
64
|
+
continue
|
|
65
|
+
directions.append(direction)
|
|
66
|
+
radii.append(rng.uniform(args.radius_min, args.radius_max))
|
|
67
|
+
|
|
68
|
+
rows = []
|
|
69
|
+
for direction, radius in zip(directions, radii):
|
|
70
|
+
position = center + direction * radius
|
|
71
|
+
forward = center - position
|
|
72
|
+
forward /= np.linalg.norm(forward)
|
|
73
|
+
up = np.array([0.0, 0.0, 1.0])
|
|
74
|
+
right = np.cross(forward, up)
|
|
75
|
+
if np.dot(right, right) == 0:
|
|
76
|
+
up = np.array([0.0, 1.0, 0.0])
|
|
77
|
+
right = np.cross(forward, up)
|
|
78
|
+
right /= np.linalg.norm(right)
|
|
79
|
+
down = np.cross(forward, right)
|
|
80
|
+
c2w = np.eye(4)
|
|
81
|
+
c2w[:3, 0] = right
|
|
82
|
+
c2w[:3, 1] = down
|
|
83
|
+
c2w[:3, 2] = forward
|
|
84
|
+
c2w[:3, 3] = position
|
|
85
|
+
# Inverse of read_camera_meta_n3dv: poses_bounds columns are [down, right, back, position, hwf].
|
|
86
|
+
pose = np.zeros((3, 5))
|
|
87
|
+
pose[:3, 0] = c2w[:3, 1]
|
|
88
|
+
pose[:3, 1] = c2w[:3, 0]
|
|
89
|
+
pose[:3, 2] = -c2w[:3, 2]
|
|
90
|
+
pose[:3, 3] = c2w[:3, 3]
|
|
91
|
+
pose[:, 4] = (args.height, args.width, args.focal)
|
|
92
|
+
rows.append(np.concatenate([pose.reshape(-1), [radius * 0.1, radius * 2]]))
|
|
93
|
+
|
|
94
|
+
args.output.mkdir(parents=True, exist_ok=True)
|
|
95
|
+
path = args.output / "poses_bounds.npy"
|
|
96
|
+
np.save(path, np.stack(rows))
|
|
97
|
+
print(f"Done: {path}")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
if __name__ == "__main__":
|
|
101
|
+
main()
|
|
@@ -6,6 +6,9 @@ pyproject.toml
|
|
|
6
6
|
./nvs2colmap/extract_videos.py
|
|
7
7
|
./nvs2colmap/write_model.py
|
|
8
8
|
./nvs2colmap/write_pcd.py
|
|
9
|
+
./nvs2colmap/blender/__main__.py
|
|
10
|
+
./nvs2colmap/blender/render.py
|
|
11
|
+
./nvs2colmap/blender/sample_cameras.py
|
|
9
12
|
./nvs2colmap/dynamic3dgs/__main__.py
|
|
10
13
|
./nvs2colmap/dynamic3dgs/camera_meta.py
|
|
11
14
|
./nvs2colmap/dynamic3dgs/link_frames.py
|
|
@@ -29,6 +32,9 @@ nvs2colmap.egg-info/dependency_links.txt
|
|
|
29
32
|
nvs2colmap.egg-info/entry_points.txt
|
|
30
33
|
nvs2colmap.egg-info/requires.txt
|
|
31
34
|
nvs2colmap.egg-info/top_level.txt
|
|
35
|
+
nvs2colmap/blender/__main__.py
|
|
36
|
+
nvs2colmap/blender/render.py
|
|
37
|
+
nvs2colmap/blender/sample_cameras.py
|
|
32
38
|
nvs2colmap/dynamic3dgs/__main__.py
|
|
33
39
|
nvs2colmap/dynamic3dgs/camera_meta.py
|
|
34
40
|
nvs2colmap/dynamic3dgs/link_frames.py
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
[console_scripts]
|
|
2
|
+
nvs2colmap-blender = nvs2colmap.blender.__main__:main
|
|
2
3
|
nvs2colmap-dynamic3dgs = nvs2colmap.dynamic3dgs.__main__:main
|
|
3
4
|
nvs2colmap-extract-videos = nvs2colmap.extract_videos:main
|
|
4
5
|
nvs2colmap-n3dv = nvs2colmap.n3dv.__main__:main
|
|
6
|
+
nvs2colmap-sample-cameras = nvs2colmap.blender.sample_cameras:main
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "nvs2colmap"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.5.2"
|
|
8
8
|
description = "Utilities for converting novel view synthesis datasets to COLMAP format."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [{ name = "Howard Yin", email = "yindaheng98@gmail.com" }]
|
|
@@ -45,6 +45,8 @@ Issues = "https://github.com/yindaheng98/NVS2COLMAP/issues"
|
|
|
45
45
|
nvs2colmap-extract-videos = "nvs2colmap.extract_videos:main"
|
|
46
46
|
nvs2colmap-n3dv = "nvs2colmap.n3dv.__main__:main"
|
|
47
47
|
nvs2colmap-dynamic3dgs = "nvs2colmap.dynamic3dgs.__main__:main"
|
|
48
|
+
nvs2colmap-blender = "nvs2colmap.blender.__main__:main"
|
|
49
|
+
nvs2colmap-sample-cameras = "nvs2colmap.blender.sample_cameras:main"
|
|
48
50
|
|
|
49
51
|
[tool.setuptools]
|
|
50
52
|
packages = { find = { include = ["nvs2colmap*"] } }
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|