Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 2 additions & 1 deletion df3d/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ def run(args):

if args.video_2d:
video.make_pose2d_video(
core.plot_2d, core.num_images, core.input_folder, core.output_folder
core.plot_2d, core.num_images, core.input_folder, core.output_folder, fps=core.fps
)

if args.video_3d:
Expand All @@ -307,6 +307,7 @@ def run(args):
core.num_images,
core.input_folder,
core.output_folder,
fps=core.fps,
)

if args.delete_images:
Expand Down
16 changes: 16 additions & 0 deletions df3d/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ def __init__(
self.output_folder = output_folder

self.expand_videos() # turn .mp4 into .jpg
self.fps = self.get_fps()
self.num_images_max = num_images_max if num_images_max is not None else 0
self.max_img_id = get_max_img_id(self.input_folder)
if self.num_images_max > 0:
Expand Down Expand Up @@ -412,6 +413,21 @@ def setup_camera_ordering(self, camera_ordering) -> np.ndarray:
# self.cidread2cid, self.cid2cidread = read_camera_order(self.output_folder)
return np.array(camera_ordering)

def get_fps(self):

Copilot AI Jul 16, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrap the subprocess.check_output call in a try/except to handle situations where ffprobe fails, and log a warning rather than letting the exception crash the run.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with Copilot here - if this goes wrong, it can be very hard to debug because ffprobe runs in a different process. This can be especially chaotic when someone runs df3d on a cluster or in the background because different STDOUT/STDERR streams from different processes can be redirected to the same file but with different buffering configurations. Potentially this can make the location of the error message in the log very confusing.

rates = []
for vid in glob.glob(os.path.join(self.input_folder, "camera_?.mp4")):
cmd = ["ffprobe", "-v", "error", "-select_streams", "v:0",
"-show_entries", "stream=avg_frame_rate", "-of",
"default=noprint_wrappers=1:nokey=1", vid]
rates.append(subprocess.check_output(cmd, text=True))
if len(rates) == 0:
return None
if any(rate != rates[0] for rate in rates):

Copilot AI Jul 16, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When returning None due to inconsistent frame rates, consider logging a warning so users know why the default FPS fallback is used.

Suggested change
if any(rate != rates[0] for rate in rates):
if any(rate != rates[0] for rate in rates):
logger.warning("Inconsistent frame rates detected across videos. Falling back to default FPS (None).")

Copilot uses AI. Check for mistakes.
return None
# All videos returned the same rate, so we return that
numerator, denominator = map(int, rates[0].split('/'))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the default output if no FPS information is saved in the video metadata? In other words, will this line fail because "/" is not detected?

return numerator / denominator if denominator != 0 else None

def expand_videos(self):
"""expands video camera_x.mp4 into set of images camera_x_img_y.jpg"""
for vid in glob.glob(os.path.join(self.input_folder, "camera_?.mp4")):
Expand Down
16 changes: 10 additions & 6 deletions df3d/video.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@
img3d_aspect = (2, 2) # this is the aspect ration for one image on the 3d video's grid
img2d_aspect = (2, 1) # this is the aspect ration for one image on the 3d video's grid
video_width = 5000 # total width of the 2d and 3d videos
default_fps = 30


def make_pose2d_video(plot_2d, num_images, input_folder, output_folder):
def make_pose2d_video(plot_2d, num_images, input_folder,
output_folder, fps=default_fps):
"""Creates pose2d estimation videos and writes it to output_folder.

Parameters:
Expand All @@ -43,10 +45,11 @@ def stack(img_id):

video_name = 'video_pose2d_' + input_folder.replace('/', '_') + '.mp4'
video_path = os.path.join(input_folder, output_folder, video_name)
_make_video(video_path, generator)
_make_video(video_path, generator, fps=fps)


def make_pose3d_video(points3d, plot_2d, num_images, input_folder, output_folder):
def make_pose3d_video(points3d, plot_2d, num_images, input_folder,
output_folder, fps=default_fps):
"""Creates pose3d estimation videos and writes it to output_folder.

Parameters:
Expand All @@ -72,24 +75,25 @@ def stack(img_id):
generator = imgs_generator()
video_name = 'video_pose3d_' + input_folder.replace('/', '_') + '.mp4'
video_path = os.path.join(input_folder, output_folder, video_name)
_make_video(video_path, generator)
_make_video(video_path, generator, fps=fps)


def _make_video(video_path, imgs):
def _make_video(video_path, imgs, fps=default_fps):
"""Code used to generate a video using cv2.

Parameters:
video_path: a path ending with .mp4, for instance: "/results/pose2d.mp4"
imgs: an iterable or generator with the images to turn into a video
"""
if fps is None:
fps = default_fps

first_frame = next(imgs)
imgs = itertools.chain([first_frame], imgs)

shape = int(first_frame.shape[1]), int(first_frame.shape[0])
logger.debug('Saving video to: ' + video_path)
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
fps = 30
output_shape = _resize(current_shape=shape, new_width=video_width)
logger.debug('Video size is: {}'.format(output_shape))
video_writer = cv2.VideoWriter(video_path, fourcc, fps, output_shape)
Expand Down