Skip to content

Commit 374e1f9

Browse files
Merge with Main
2 parents e16135e + 516fd7c commit 374e1f9

File tree

10 files changed

+201
-294
lines changed

10 files changed

+201
-294
lines changed

.github/workflows/build_docker_image.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ jobs:
4747
- name: Build and push Docker image
4848
id: push
4949
uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671
50+
if:
5051
with:
5152
context: .
5253
file: ./Dockerfile
@@ -55,8 +56,9 @@ jobs:
5556
labels: ${{ steps.meta.outputs.labels }}
5657
- name: Generate artifact attestation
5758
uses: actions/attest-build-provenance@v1
59+
if: ${{ github.event_name != 'pull_request' }}
5860
with:
5961
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
6062
subject-digest: ${{ steps.push.outputs.digest }}
61-
push-to-registry: ${{ github.event_name != 'pull_request' }}
63+
push-to-registry: true
6264

nerfstudio/configs/method_configs.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -663,8 +663,7 @@
663663
),
664664
model=SplatfactoModelConfig(
665665
cull_alpha_thresh=0.005,
666-
continue_cull_post_densification=False,
667-
densify_grad_thresh=0.0006,
666+
densify_grad_thresh=0.0005,
668667
),
669668
),
670669
optimizers={

nerfstudio/engine/trainer.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,8 @@ def train(self) -> None:
300300

301301
# Do not perform evaluation if there are no validation images
302302
if self.pipeline.datamanager.eval_dataset:
303-
self.eval_iteration(step)
303+
with self.train_lock:
304+
self.eval_iteration(step)
304305

305306
if step_check(step, self.config.steps_per_save):
306307
self.save_checkpoint(step)

nerfstudio/exporter/exporter_utils.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -165,11 +165,11 @@ def generate_point_cloud(
165165

166166
if crop_obb is not None:
167167
mask = crop_obb.within(point)
168-
point = point[mask]
169-
rgb = rgb[mask]
170-
view_direction = view_direction[mask]
171-
if normal is not None:
172-
normal = normal[mask]
168+
point = point[mask]
169+
rgb = rgb[mask]
170+
view_direction = view_direction[mask]
171+
if normal is not None:
172+
normal = normal[mask]
173173

174174
points.append(point)
175175
rgbs.append(rgb)

nerfstudio/models/splatfacto.py

Lines changed: 52 additions & 269 deletions
Large diffs are not rendered by default.

nerfstudio/process_data/process_data_utils.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"""Helper utils for processing data into the nerfstudio format."""
1616

1717
import math
18+
import random
1819
import re
1920
import shutil
2021
import sys
@@ -126,7 +127,7 @@ def convert_video_to_images(
126127
verbose: bool = False,
127128
image_prefix: str = "frame_",
128129
keep_image_dir: bool = False,
129-
random_seed: Optional[int] = None
130+
random_seed: Optional[int] = None,
130131
) -> Tuple[List[str], int]:
131132
"""Converts a video into a sequence of images.
132133
@@ -139,6 +140,7 @@ def convert_video_to_images(
139140
verbose: If True, logs the output of the command.
140141
image_prefix: Prefix to use for the image filenames.
141142
keep_image_dir: If True, don't delete the output directory if it already exists.
143+
random_seed: If set, the seed used to choose the frames t commit of the video
142144
Returns:
143145
A tuple containing summary of the conversion and the number of extracted frames.
144146
"""
@@ -178,7 +180,7 @@ def convert_video_to_images(
178180
start_x = crop_factor[2]
179181
start_y = crop_factor[0]
180182
crop_cmd = f"crop=w=iw*{width}:h=ih*{height}:x=iw*{start_x}:y=ih*{start_y},"
181-
183+
182184
downscale_chains = [f"[t{i}]scale=iw/{2**i}:ih/{2**i}[out{i}]" for i in range(num_downscales + 1)]
183185
downscale_dirs = [Path(str(image_dir) + (f"_{2**i}" if i > 0 else "")) for i in range(num_downscales + 1)]
184186
downscale_paths = [downscale_dirs[i] / f"{image_prefix}%05d.png" for i in range(num_downscales + 1)]
@@ -200,10 +202,10 @@ def convert_video_to_images(
200202
if random_seed:
201203
random.seed(random_seed)
202204
frame_indices = sorted(random.sample(range(num_frames), num_frames_target))
203-
select_cmd = f"select=\'" + "+".join([f"eq(n\,{idx})" for idx in frame_indices]) + "\',setpts=N/TB,"
204-
CONSOLE.print(f"Extracting {num_frames_target} frames using seed-based random selection.")
205+
select_cmd = "select='" + "+".join([f"eq(n\,{idx})" for idx in frame_indices]) + "',setpts=N/TB,"
206+
CONSOLE.print(f"Extracting {num_frames_target} frames using seed {random_seed} random selection.")
205207
elif spacing > 1:
206-
CONSOLE.print("Number of frames to extract:", math.ceil(num_frames / spacing))
208+
CONSOLE.print(f"Extracting {math.ceil(num_frames / spacing)} frames in evenly spaced intervals")
207209
select_cmd = f"thumbnail={spacing},setpts=N/TB,"
208210
else:
209211
CONSOLE.print("[bold red]Can't satisfy requested number of frames. Extracting all frames.")

nerfstudio/process_data/video_to_nerfstudio_dataset.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
import shutil
1818
from dataclasses import dataclass
19-
from typing import Literal
19+
from typing import Literal, Optional
2020

2121
from nerfstudio.process_data import equirect_utils, process_data_utils
2222
from nerfstudio.process_data.colmap_converter_to_nerfstudio_dataset import ColmapConverterToNerfstudioDataset
@@ -41,9 +41,9 @@ class VideoToNerfstudioDataset(ColmapConverterToNerfstudioDataset):
4141
"""Feature matching method to use. Vocab tree is recommended for a balance of speed
4242
and accuracy. Exhaustive is slower but more accurate. Sequential is faster but
4343
should only be used for videos."""
44-
random_seed: int = None
45-
"""Random seed to select video frames"""
46-
eval_random_seed: int = None
44+
random_seed: Optional[int] = None
45+
"""Random seed to select video frames for training set"""
46+
eval_random_seed: Optional[int] = None
4747
"""Random seed to select video frames for eval set"""
4848

4949
def main(self) -> None:
@@ -63,7 +63,7 @@ def main(self) -> None:
6363
num_downscales=0,
6464
crop_factor=(0.0, 0.0, 0.0, 0.0),
6565
verbose=self.verbose,
66-
random_seed = self.random_seed
66+
random_seed=self.random_seed,
6767
)
6868
else:
6969
# If we're not dealing with equirects we can downscale in one step.
@@ -76,7 +76,7 @@ def main(self) -> None:
7676
verbose=self.verbose,
7777
image_prefix="frame_train_" if self.eval_data is not None else "frame_",
7878
keep_image_dir=False,
79-
random_seed = self.random_seed
79+
random_seed=self.random_seed,
8080
)
8181
if self.eval_data is not None:
8282
summary_log_eval, num_extracted_frames_eval = process_data_utils.convert_video_to_images(
@@ -88,7 +88,7 @@ def main(self) -> None:
8888
verbose=self.verbose,
8989
image_prefix="frame_eval_",
9090
keep_image_dir=True,
91-
random_seed = self.eval_random_seed
91+
random_seed=self.eval_random_seed,
9292
)
9393
summary_log += summary_log_eval
9494
num_extracted_frames += num_extracted_frames_eval

nerfstudio/scripts/exporter.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -547,7 +547,7 @@ def main(self) -> None:
547547
if not self.output_dir.exists():
548548
self.output_dir.mkdir(parents=True)
549549

550-
_, pipeline, _, _ = eval_setup(self.load_config)
550+
_, pipeline, _, _ = eval_setup(self.load_config, test_mode="inference")
551551

552552
assert isinstance(pipeline.model, SplatfactoModel)
553553

@@ -620,9 +620,17 @@ def main(self) -> None:
620620
n_after = np.sum(select)
621621
if n_after < n_before:
622622
CONSOLE.print(f"{n_before - n_after} NaN/Inf elements in {k}")
623+
nan_count = np.sum(select) - n
624+
625+
# filter gaussians that have opacities < 1/255, because they are skipped in cuda rasterization
626+
low_opacity_gaussians = (map_to_tensors["opacity"]).squeeze(axis=-1) < -5.5373 # logit(1/255)
627+
lowopa_count = np.sum(low_opacity_gaussians)
628+
select[low_opacity_gaussians] = 0
623629

624630
if np.sum(select) < n:
625-
CONSOLE.print(f"values have NaN/Inf in map_to_tensors, only export {np.sum(select)}/{n}")
631+
CONSOLE.print(
632+
f"{nan_count} Gaussians have NaN/Inf and {lowopa_count} have low opacity, only export {np.sum(select)}/{n}"
633+
)
626634
for k, t in map_to_tensors.items():
627635
map_to_tensors[k] = map_to_tensors[k][select]
628636
count = np.sum(select)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ dependencies = [
6262
"xatlas",
6363
"trimesh>=3.20.2",
6464
"timm==0.6.7",
65-
"gsplat==1.0.0",
65+
"gsplat==1.3.0",
6666
"pytorch-msssim",
6767
"pathos",
6868
"packaging",

tests/process_data/test_misc.py

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,21 @@
22
Test misc data utils
33
"""
44

5+
import os
6+
import re
7+
from pathlib import Path
8+
from unittest import mock
9+
10+
import cv2
511
import numpy as np
12+
from PIL import Image
613
from pyquaternion import Quaternion
714
from scipy.spatial.transform import Rotation
815

916
# TODO(1480) use pycolmap instead of colmap_parsing_utils
1017
# import pycolmap
1118
from nerfstudio.data.utils.colmap_parsing_utils import qvec2rotmat
19+
from nerfstudio.process_data.process_data_utils import convert_video_to_images
1220

1321

1422
def test_scalar_first_scalar_last_quaternions():
@@ -39,7 +47,7 @@ def test_scalar_first_scalar_last_quaternions():
3947

4048
# Expected Rotation matrix
4149
# fmt: off
42-
R_expected = np.array(
50+
R_expected = np.array(
4351
[
4452
[ 0.81379768, -0.44096961, 0.37852231],
4553
[ 0.46984631, 0.88256412, 0.01802831],
@@ -61,3 +69,107 @@ def test_scalar_first_scalar_last_quaternions():
6169
# R = pycolmap.qvec_to_rotmat(wxyz)
6270
R = qvec2rotmat(wxyz)
6371
assert np.allclose(R, R_expected)
72+
73+
74+
def test_process_video_conversion_with_seed(tmp_path: Path):
75+
"""
76+
Test convert_video_to_images by creating a mock video and ensuring correct frame extraction with seed.
77+
"""
78+
79+
# Inner functions needed for the unit tests
80+
def create_mock_video(video_path: Path, frame_dir: Path, num_frames=10, frame_rate=1):
81+
"""Creates a mock video from a series of frames using OpenCV."""
82+
83+
first_frame = cv2.imread(str(frame_dir / "frame_0.png"))
84+
height, width, _ = first_frame.shape
85+
fourcc = cv2.VideoWriter_fourcc(*"mp4v")
86+
out = cv2.VideoWriter(str(video_path), fourcc, frame_rate, (width, height))
87+
88+
for i in range(num_frames):
89+
frame_path = frame_dir / f"frame_{i}.png"
90+
frame = cv2.imread(str(frame_path))
91+
out.write(frame)
92+
out.release()
93+
94+
def extract_frame_numbers(ffmpeg_command: str):
95+
"""Extracts the frame numbers from the ffmpeg command"""
96+
97+
pattern = r"eq\(n\\,(\d+)\)"
98+
matches = re.findall(pattern, ffmpeg_command)
99+
frame_numbers = [int(match) for match in matches]
100+
return frame_numbers
101+
102+
# Create a video directory with path video
103+
video_dir = tmp_path / "video"
104+
video_dir.mkdir(exist_ok=True)
105+
106+
# Set parameters for mock video
107+
video_path = video_dir / "mock_video.mp4"
108+
num_frames = 10
109+
frame_height = 150
110+
frame_width = 100
111+
frame_rate = 1
112+
113+
# Create the mock video
114+
for i in range(num_frames):
115+
img = Image.new("RGB", (frame_width, frame_height), (0, 0, 0))
116+
img.save(video_dir / f"frame_{i}.png")
117+
create_mock_video(video_path, video_dir, num_frames=num_frames, frame_rate=frame_rate)
118+
119+
# Call convert_video_to_images
120+
image_output_dir = tmp_path / "extracted_images"
121+
num_frames_target = 5
122+
num_downscales = 1
123+
crop_factor = (0.0, 0.0, 0.0, 0.0)
124+
125+
# Mock missing COLMAP and ffmpeg in the dev env
126+
old_path = os.environ.get("PATH", "")
127+
os.environ["PATH"] = str(tmp_path / "mocked_bin") + f":{old_path}"
128+
(tmp_path / "mocked_bin").mkdir()
129+
(tmp_path / "mocked_bin" / "colmap").touch(mode=0o777)
130+
(tmp_path / "mocked_bin" / "ffmpeg").touch(mode=0o777)
131+
132+
# Return value of 10 for the get_num_frames_in_video run_command call
133+
with mock.patch("nerfstudio.process_data.process_data_utils.run_command", return_value="10") as mock_run_func:
134+
summary_log, extracted_frame_count = convert_video_to_images(
135+
video_path=video_path,
136+
image_dir=image_output_dir,
137+
num_frames_target=num_frames_target,
138+
num_downscales=num_downscales,
139+
crop_factor=crop_factor,
140+
verbose=False,
141+
random_seed=42,
142+
)
143+
assert mock_run_func.call_count == 2, f"Expected 2 calls, but got {mock_run_func.call_count}"
144+
first_frames = extract_frame_numbers(mock_run_func.call_args[0][0])
145+
assert len(first_frames) == 5, f"Expected 5 frames, but got {len(first_frames)}"
146+
147+
summary_log, extracted_frame_count = convert_video_to_images(
148+
video_path=video_path,
149+
image_dir=image_output_dir,
150+
num_frames_target=num_frames_target,
151+
num_downscales=num_downscales,
152+
crop_factor=crop_factor,
153+
verbose=False,
154+
random_seed=42,
155+
)
156+
157+
assert mock_run_func.call_count == 4, f"Expected 4 total calls, but got {mock_run_func.call_count}"
158+
second_frames = extract_frame_numbers(mock_run_func.call_args[0][0])
159+
assert len(second_frames) == 5, f"Expected 5 frames, but got {len(first_frames)}"
160+
assert first_frames == second_frames
161+
162+
summary_log, extracted_frame_count = convert_video_to_images(
163+
video_path=video_path,
164+
image_dir=image_output_dir,
165+
num_frames_target=num_frames_target,
166+
num_downscales=num_downscales,
167+
crop_factor=crop_factor,
168+
verbose=False,
169+
random_seed=52,
170+
)
171+
172+
assert mock_run_func.call_count == 6, f"Expected 6 total calls, but got {mock_run_func.call_count}"
173+
third_frames = extract_frame_numbers(mock_run_func.call_args[0][0])
174+
assert len(third_frames) == 5, f"Expected 5 frames, but got {len(first_frames)}"
175+
assert first_frames != third_frames

0 commit comments

Comments
 (0)