Skip to content

Pull true fps from input videos and use it when making new videos#63

Merged
Dominic-DallOsto merged 5 commits intomasterfrom
use-real-fps
Jul 21, 2025
Merged

Pull true fps from input videos and use it when making new videos#63
Dominic-DallOsto merged 5 commits intomasterfrom
use-real-fps

Conversation

@jasper-tms
Copy link
Member

  • df3d/video.py previously had a hardcoded line fps=30 in _make_video(). I un-harcoded this by making it a keyword argument of _make_video(), so now other functions can specify what fps they want when calling _make_video().
  • I added an attribute core.fps which is read from the input video files during __init__. (Note that I'm using ffprobe to request the avg_frame_rate, where r_frame_rate is another possible field that could have been pulled instead - these are supposed to be the same for constant framerate videos, but might be slightly different for variable framerate videos.)
  • when the cli is used with --video-2d or --video-3d flags, core.fps is used to produce a video at the same framerate that the input videos play at.

In summary, this eliminates the discrepancy between the output videos previously always being made at 30 fps regardless of the input video framerate.

@jasper-tms jasper-tms requested a review from Copilot July 16, 2025 13:48
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR removes the hardcoded 30 fps in video creation, reads the actual frame rate from input videos, and applies it when generating new 2D/3D videos.

  • Un-hardcoded fps in _make_video, make_pose2d_video, and make_pose3d_video by adding a fps parameter with a default_fps fallback.
  • Introduced get_fps() in core.py to probe input videos for their average frame rate.
  • Updated the CLI to pass core.fps when invoking video-generation functions.

Reviewed Changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
df3d/video.py Added default_fps, propagated fps through APIs, and removed hardcoded fps = 30.
df3d/core.py Added get_fps() to extract frame rates via ffprobe and store in self.fps.
df3d/cli.py Updated CLI calls to pass the extracted core.fps to the video functions.
Comments suppressed due to low confidence (3)

df3d/video.py:18

  • [nitpick] Per PEP 8, module-level constants should be uppercase. Consider renaming default_fps to DEFAULT_FPS.
default_fps = 30

df3d/video.py:21

  • The new fps parameter should be documented in the function docstring (e.g., fps: frames per second to use for the output video).
def make_pose2d_video(plot_2d, num_images, input_folder,

df3d/core.py:416

  • Add unit tests for get_fps covering scenarios: no videos, consistent FPS, inconsistent FPS, and denominator=0 to ensure accurate behavior.
    def get_fps(self):

# self.cidread2cid, self.cid2cidread = read_camera_order(self.output_folder)
return np.array(camera_ordering)

def get_fps(self):
Copy link

Copilot AI Jul 16, 2025

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
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.append(subprocess.check_output(cmd, text=True))
if len(rates) == 0:
return None
if any(rate != rates[0] for rate in rates):
Copy link

Copilot AI Jul 16, 2025

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.
@jasper-tms
Copy link
Member Author

Well, this revealed the fascinating fact that the sample videos provided in DeepFly3D/tests/data/reference/*mp4 all have their framerate set to a blazing fast 0.2 fps. I... suppose we want to change that? Presumably the true fps of that recording was either 100 or 30, but perhaps it's desirable for the sample output videos to be played back slower than real speed to make it easier for users to inspect the results. This means we could:

  • Set the sample video's framerate to like 5 so that the output videos have that framerate
  • Set the sample video's framerate to 100, which is most likely the true recording speed, and then add an --output-fps flag to df3d-cli to allow users to override the input video framerate and specify what fps they want their output videos to be made at.

@sibocw sibocw self-requested a review July 16, 2025 14:08
Copy link
Contributor

@sibocw sibocw left a comment

Choose a reason for hiding this comment

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

Just two small comments

# self.cidread2cid, self.cid2cidread = read_camera_order(self.output_folder)
return np.array(camera_ordering)

def get_fps(self):
Copy link
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.

df3d/core.py Outdated
if any(rate != rates[0] for rate in rates):
return None
# All videos returned the same rate, so we return that
numerator, denominator = map(int, rates[0].split('/'))
Copy link
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?

Copy link
Contributor

@Dominic-DallOsto Dominic-DallOsto left a comment

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?

I think rates[0].split('/') will give a list with an empty string, and int will throw an error when trying to parse it.

Copy link
Contributor

@Dominic-DallOsto Dominic-DallOsto left a comment

Choose a reason for hiding this comment

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

Well, this revealed the fascinating fact that the sample videos provided in DeepFly3D/tests/data/reference/*mp4 all have their framerate set to a blazing fast 0.2 fps. I... suppose we want to change that? Presumably the true fps of that recording was either 100 or 30, but perhaps it's desirable for the sample output videos to be played back slower than real speed to make it easier for users to inspect the results. This means we could:

  • Set the sample video's framerate to like 5 so that the output videos have that framerate
  • Set the sample video's framerate to 100, which is most likely the true recording speed, and then add an --output-fps flag to df3d-cli to allow users to override the input video framerate and specify what fps they want their output videos to be made at.

I think the CLI param would be nice to have, yeah. Then maybe the second option makes the most sense

@jasper-tms
Copy link
Member Author

Thanks for the comments! --output-fps added and possible errors during fps extraction handled and logged. I have the test/run_df3d_on_sample_data.sh script output the sample videos at 5fps, which looks reasonable for viewing the small 15 frame sample dataset.

The only small change in behavior I made to the get_fps() logic was that if the input videos have different framerates, I go ahead and set core.fps to the first video's framerate instead of None, but I log a warning so the user knows about the discrepancies in input framerates.

When the tests complete, if this looks good to you please feel free to merge the PR, otherwise I will in a day or two.

@jasper-tms
Copy link
Member Author

The test failure looks like a pretty minor issue:

FAIL: test_video_3d (tests.test_df3d.TestDeepFly3D)
Test that we can generate a video of the 3D pose estimation results
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/runner/work/DeepFly3D/DeepFly3D/tests/test_df3d.py", line 324, in test_video_3d
    np.testing.assert_almost_equal(
  File "/opt/hostedtoolcache/Python/3.10.18/x64/lib/python3.10/site-packages/numpy/testing/_private/utils.py", line 611, in assert_almost_equal
    return assert_array_almost_equal(actual, desired, decimal, err_msg)
  File "/opt/hostedtoolcache/Python/3.10.18/x64/lib/python3.10/site-packages/numpy/_utils/__init__.py", line 86, in wrapper
    return fun(*args, **kwargs)
  File "/opt/hostedtoolcache/Python/3.10.18/x64/lib/python3.10/site-packages/numpy/testing/_private/utils.py", line 1172, in assert_array_almost_equal
    assert_array_compare(compare, actual, desired, err_msg=err_msg,
  File "/opt/hostedtoolcache/Python/3.10.18/x64/lib/python3.10/site-packages/numpy/testing/_private/utils.py", line 921, in assert_array_compare
    raise AssertionError(msg)
AssertionError: 
Arrays are not almost equal to 7 decimals
Frame 3 of 3D video doesn't match what it should
Mismatched elements: 1758 / 49980000 (0.00352%)
Max absolute difference among violations: 13
Max relative difference among violations: 0.104
 ACTUAL: array([[[129, 132, 130],
        [129, 132, 130],
        [129, 132, 130],...
 DESIRED: array([[[129, 132, 130],
        [129, 132, 130],
        [129, 132, 130],...

It looks like there are tiny pixel value differences between the video frames that the test function generates and the video frames that the test function believes it should generate. @Dominic-DallOsto do you know what this could be about? I pushed new versions of video_pose2d.mp4 and video_pose3d.mp4, but is there something else that needs to be done?

Copy link
Contributor

@Dominic-DallOsto Dominic-DallOsto left a comment

Choose a reason for hiding this comment

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

I updated the test data output video framerates using this approach and I think it works all good now

Copy link
Contributor

@Dominic-DallOsto Dominic-DallOsto left a comment

Choose a reason for hiding this comment

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

Yeah, maybe the video data just got mangled / reencoded somehow when updating the fps last time. Looks all good now so I'll merge it.

@Dominic-DallOsto Dominic-DallOsto merged commit 070e753 into master Jul 21, 2025
7 checks passed
@jasper-tms
Copy link
Member Author

jasper-tms commented Jul 21, 2025

That's strange... In d960d82 I had df3d re-generate those videos. So I would expect that the test suite, which also re-generates the videos from input images plus df3d predictions, would produce the same ones. I'm confused as to how the old 0.2fps videos with modified framerates matches the output of the test suite but the output of my local df3d run doesn't. Any ideas? I feel like we're in a bad spot if users and the test suite are generating slightly different outputs! Maybe it just comes down to something small like a different version of a library that I have installed vs the test suite installs? If you run run_df3d_on_sample_data.sh yourself do you get data that matches the current file in the repo? I don't, but if you do, it's probably a version thing:

In [3]: vid1, fps1 = npimage.load_video('video_pose3d.mp4', return_framerate=True)
Loading video:  93%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▎         | 14/15 [00:00<00:00, 19.47it/s]

In [4]: vid1.shape
Out[4]: (15, 3332, 5000, 3)

In [5]: fps1
Out[5]: 5.294117647058823

In [6]: vid2, fps2 = npimage.load_video('video_pose3d_me.mp4', return_framerate=True)
Loading video:  93%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▎         | 14/15 [00:00<00:00, 19.37it/s]

In [7]: vid2.shape
Out[7]: (15, 3332, 5000, 3)

In [8]: fps2
Out[8]: 5.0

In [10]: (vid1.astype(np.int16) - vid2).max()
Out[10]: np.int16(15)

In [11]: (vid1.astype(np.int16) - vid2).min()
Out[11]: np.int16(-21)

Also I see the fps of the current video_3d.mp4 in the repo as 5.294. If we can get a user-df3d-generated video to have the correct values I suggest we push it since it'd have the correct fps.

Copy link
Contributor

@Dominic-DallOsto Dominic-DallOsto left a comment

Choose a reason for hiding this comment

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

I get the same framerates as you - not sure why the ffmpeg edited one is mangled, but for me the two videos are identifcal.

I'll make a PR to update the videos.

In case you want to check - I have these library versions:

numpy                      1.26.4
matplotlib                 3.9.1
matplotlib-inline          0.1.7
opencv-python              4.10.0.84
opencv-python-headless     4.10.0.84

@jasper-tms jasper-tms deleted the use-real-fps branch November 20, 2025 14:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants