Skip to content

fix floating point errors in ImageSequenceClip#2518

Open
trygvrad wants to merge 1 commit intoZulko:masterfrom
trygvrad:ImageSequenceClip_indexing_fix
Open

fix floating point errors in ImageSequenceClip#2518
trygvrad wants to merge 1 commit intoZulko:masterfrom
trygvrad:ImageSequenceClip_indexing_fix

Conversation

@trygvrad
Copy link
Copy Markdown

Both #2412 and #2507 relate to comparisons that fail due to floating point precision loss.
In #2412 this leads to the frames being included at the wrong timestep.
In #2507 this leads to the last frame not being included.


For #2412, self.images_starts is calculated in the following way:

            self.images_starts = [
                1.0 * i / fps - np.finfo(np.float32).eps for i in range(len(sequence))
            ]

The inclusion of np.finfo(np.float32).eps makes the dtype of each element 'float32', and this has lower precision than the number it is compared to. In the cases where there is sufficient precision in a float32 for np.finfo(np.float32).eps to impact the numerical value, self.images_starts[i] <= i/fps still holds true, but this is only true for numbers close to one.
Removing -np.finfo(np.float32).eps fixes this error.
Note: the -np.finfo(np.float32).eps was introduced to fix the same error (#464, 167db9f), but the fix was not sufficiently tested

The new test added to test #2412 is inspired by the code in #464


#2507 is related to the duration, calculated as:

            durations = [1.0 / fps for image in sequence]
        ...
        self.duration = sum(durations)

which is then compared to len(self.sequences)/self.fps, which results in a classical floating point error, similar to how np.sum([1.0/71 for i in range(71)]) == 1 is False.
Changing the calculation of duration to:

            self.duration = len(sequence) / fps

fixes the error, because then the calculation of the duration, and the check of the duration, is calculated in the same way.


There are two new tests:

def test_no_repeat_frames():
    # set of frames that are all different levels of grey
    frames = [np.ones((400, 400, 3)) * f for f in np.linspace(0, 1, 20)]

    for frames_per_second in range(1, 31):
        movie = ImageSequenceClip(frames, fps=frames_per_second)
        c = np.array([f[0, 0, 0] for f in movie.iter_frames()])
        assert np.all(c[1:] != c[:-1])


def test_correct_number_of_frames():
    # set of frames that are all different levels of grey
    frames = [np.ones((400, 400, 3)) * f for f in np.linspace(0, 1, 20)]

    for frames_per_second in range(1, 31):
        movie = ImageSequenceClip(frames, fps=frames_per_second)
        c = np.array([f[0, 0, 0] for f in movie.iter_frames()])
        assert len(c) == 20

Which fail on main but pass with this PR.

Note that the details of the failure is system-specific, and different systems will fail at different points (fps), which is why fps values from 1 to 30 are tested.

  • I have provided code that clearly demonstrates the bug and that only works correctly when applying this fix
  • I have added suitable tests demonstrating a fixed bug or new/changed feature to the test suite in tests/

@trygvrad trygvrad force-pushed the ImageSequenceClip_indexing_fix branch from aa37964 to b2b3041 Compare September 14, 2025 18:55
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.

1 participant