Skip to content

Commit 60be50a

Browse files
austinwitherspoonmarkreidvfx
authored andcommitted
Match MC bezier interpolation for degenerate cases (#131)
Don't preform handle scaling and clamping Add tests for keyframe interpolation
1 parent 049c706 commit 60be50a

File tree

4 files changed

+121
-16
lines changed

4 files changed

+121
-16
lines changed

src/aaf2/interpolation.py

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -147,26 +147,29 @@ def scale_handle(p0, p1, p2):
147147
y = (p1[1] - p0[1]) * (p2[0] - p0[0]) / (p1[0] - p0[0])
148148
return [p2[0], p0[1] + y]
149149

150-
def bezier_interpolate(p0, p1, p2, p3, x):
150+
def bezier_interpolate(p0, p1, p2, p3, x, scale_handles=False):
151151

152152
# degenerate cases 1
153153
# p0 is after p3
154154
if p0[0] >= p3[0]:
155-
return p[1]
156-
157-
# degenerate cases 2
158-
# p1 after p3 or before p0
159-
if p1[0] > p3[0]:
160-
p1 = scale_handle(p0, p1, p3)
161-
elif p1[0] < p0[0]:
162-
p1 = [p0[0], p1[1]]
163-
164-
# degenerate cases 3
165-
# p2 before p0 or after p3
166-
if p2[0] < p0[0]:
167-
p2 = scale_handle(p3, p2, p0)
168-
elif p2[0] > p3[0]:
169-
p2 = [p3[0], p2[1]]
155+
return p0[1]
156+
157+
# Media Composer doesn't perform any scaling or clamping of handles
158+
# if their x positions are out not between p0.x and p3.x.
159+
if scale_handles:
160+
# degenerate cases 2
161+
# p1 after p3 or before p0
162+
if p1[0] > p3[0]:
163+
p1 = scale_handle(p0, p1, p3)
164+
elif p1[0] < p0[0]:
165+
p1 = [p0[0], p1[1]]
166+
167+
# degenerate cases 3
168+
# p2 before p0 or after p3
169+
if p2[0] < p0[0]:
170+
p2 = scale_handle(p3, p2, p0)
171+
elif p2[0] > p3[0]:
172+
p2 = [p3[0], p2[1]]
170173

171174
# offset points so x is the x axis
172175
pa = p0[0] - x
Binary file not shown.
420 KB
Binary file not shown.

tests/test_interpolation.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
from __future__ import (
2+
unicode_literals,
3+
absolute_import,
4+
print_function,
5+
division,
6+
)
7+
import os
8+
import aaf2
9+
import common
10+
import unittest
11+
12+
KEYFRAME_VALUE_MARGIN_OF_ERROR = .1
13+
14+
15+
class TestKeyframeInterpolation(unittest.TestCase):
16+
def test_normal_bezier(self):
17+
file = os.path.join(common.test_files_dir(), 'keyframes/normal_bezier.aaf')
18+
# these values are transcribed by hand from media composer
19+
expected_result = {
20+
0: 0.0,
21+
1: 147.50,
22+
2: 257.19,
23+
3: 343.74,
24+
4: 413.91,
25+
5: 471.47,
26+
6: 518.72,
27+
7: 557.20,
28+
8: 587.92,
29+
9: 611.60,
30+
10: 628.70,
31+
11: 639.51,
32+
12: 644.18,
33+
13: 642.68,
34+
14: 634.87,
35+
15: 620.42,
36+
16: 598.80,
37+
17: 569.19,
38+
18: 530.32,
39+
19: 480.22,
40+
20: 415.59,
41+
21: 330.32,
42+
22: 210.33,
43+
23: 0.0,
44+
}
45+
with aaf2.open(file) as f:
46+
# Grab the Y position parameter from this AAF
47+
op_group = next(f.content.compositionmobs()).slots[8].segment.components[1]
48+
param_y_pos = next(
49+
param for param in op_group.parameters
50+
if isinstance(param, aaf2.misc.VaryingValue)
51+
and param.name == "DVE_POS_Y_U"
52+
)
53+
self.compare_interpolated_values(param_y_pos, expected_result)
54+
55+
def test_handle_past_last_keyframe(self):
56+
file = os.path.join(common.test_files_dir(), 'keyframes/bezier_handle_past_last_keyframe.aaf')
57+
# these values are transcribed by hand from media composer
58+
expected_result = {
59+
0: 0.0,
60+
1: 40.2,
61+
2: 79.92,
62+
3: 119.09,
63+
4: 157.63,
64+
5: 195.45,
65+
6: 232.43,
66+
7: 268.39,
67+
8: 303.14,
68+
9: 336.38,
69+
10: 367.72,
70+
11: 396.57,
71+
12: 422.01,
72+
13: 442.48,
73+
14: 454.99,
74+
15: 452.45,
75+
16: 409.92,
76+
17: 227.30,
77+
18: 107.82,
78+
19: 55.0,
79+
20: 26.25,
80+
21: 10.24,
81+
22: 2.3,
82+
23: 0.0,
83+
}
84+
with aaf2.open(file) as f:
85+
# Grab the Y position parameter from this AAF
86+
op_group = next(f.content.compositionmobs()).slots[8].segment.components[1]
87+
param_y_pos = next(
88+
param for param in op_group.parameters
89+
if isinstance(param, aaf2.misc.VaryingValue)
90+
and param.name == "DVE_POS_Y_U"
91+
)
92+
self.compare_interpolated_values(param_y_pos, expected_result)
93+
94+
def compare_interpolated_values(self, parameter, expected_values):
95+
# Generate the interpolated values
96+
for frame_num, expected_value in expected_values.items():
97+
actual_value = parameter.value_at(frame_num)
98+
difference = abs(actual_value - expected_value)
99+
assert difference < KEYFRAME_VALUE_MARGIN_OF_ERROR, \
100+
"Expected value of {:.02f} for frame {} but got {:.02f}. Difference: {:.02f}".format(
101+
expected_value, frame_num, actual_value, difference
102+
)

0 commit comments

Comments
 (0)