Keyframe Reduction script for Nuke
The original version of this article can be viewed here
I came across this issue when importing tracking data from PFTrack into Nuke, but it is equally relevant to tracking data create with a nuke Tracker node, or any animation data that gets "baked". The curves of a simple animation can be created using a small number of keyframes in the Curve Editor. However sometimes the animation gets baked in i.e. a keyframe is created on every frame. If we want to adjust the animation by dragging keyframes in the Curve Editor it is a lot easier to do so with keyframes only occurring at zenith points. Here we see our original curves that require only 28 keyframes over a 100 frame range. Below it is the baked version, where we get 100 keyframes over 100 frames.
I wanted to find a way of un-baking the keyframes and returning to our animation curve being created using the minimal amount of keyframes possible. The only existing method I found was a script by Julik Tarkhanov called reducer.py. His script works by iterating through each keyframe and comparing its derivative to the derivative of the previous keyframe. Where the difference in angle is small, the keyframe is removed. This works well for sections of the animation curve that are straight lines, but has limitations＊. It did, however, give me a lot of help when it came to creating my own version. I looked into a lot of research papers and settled on "Keyframe Reduction Techniques for Motion Capture Data" as the method to replicate. The proposed "curve simplification" algorithm seemed like something I could recreate using Python.
Our script begins by launching a custom panel so that the user can input a range of frames in which the baked keyframes exist (tFrameRange) and a target error threshold (tErrorThreshold).
It then creates a new temporary knob (tTempKnob) with no keyframes in which to create our new animation curve (tTempCurve). It then places a keyframe on the first and last frame of the animation, creating a straight line in our tTempCurve.
Next we want to look at the frames in our original curve (tOriginalCurve) and find their distance from our new curve. This presented an interesting challenge as the distance had be to on a line perpendicular to our tTempCurve. Time to remember some basic maths from school. If we look at our Curve Editor and think of the frame number as our x axis and the value of the knob as the y axis, then we can take the triangle between our first and last frame and use trigonometry to calculate the angle of the slope in our new new curve (tMasterSlope). We can then create a triangle for each frame, taking the difference between the values on the y axis to give us a length of the 'hypotenuse' side, and use a 'sin' function to find the length of the 'opposite' side - which is the distance value needed.
We loop through each frame and find this distance, which is our "error value". Here we loop through 100 frames and frame 76 produces the highest error value.
The frame that produces the highest error value (76) then gets turned into a keyframe.
This divides our new tTempCurve into 2 sections - frames 1-76 and frames 76-100. Our script then takes each of these sections and recursively runs the same function on them, iterating through each frame and finding the greatest error value.
These two new error frames are turned into keyframes and our script continues on the 4 new sections created, recursively calling itself each time.
Each time a section of the curve is scanned for placing a new keyframe it is also checked for how much it differs from our original curve, with this value being represented as a percentage of the overall height. If this error percentage is below our tErrorThreshold, then this section is considered to be close enough to our original and is no longer sub-divided. Here I have set the target error threshold to be 1%, which results in the final curve below
We have reduced down to only 51 keyframes - not quite as low as our original 28 keyframes, but still a reduction to half of the number in our baked version. Playing with the error threshold value (i.e. increasing it to of 3%) results in a reduction to fewer keyframes (only 27), but areas of lower accuracy.
The script works well for most standard knobs that we find in most commonly used nodes. However it fails when we try to run it on animations that occur on Beziers or Layers in a RotoPaint node. Our script has a function called getKnobName which is designed to return the name of the knob on which we are running the script (i.e. "rotate", translate.x" etc.). The RotoPaint node has a knob called "curves", under which all transforms for Beziers and Layers fall, so the knob names returned tend to be called things like "curves.rotate" or "curves.Bezier1.translate.x". There should be a way of easily accessing these animations, but I need to look more into the _curvelib.AnimCTransform object as it gets a bit inconsistent with syntax.
The Python script
The formatting of the Python script was getting mangled in the Nukedpedia article editor, so the full script for reduceKeyframes.py can be found here in GitHub
The script needs to be installed under the "Animation" sub menu, so it should be copied into your .nuke folder as "reduceKeyframes.py" and your menu.py file should be modified with the following lines:
import reduceKeyframes m=nuke.menu( 'Animation' ) m.addCommand( 'Reduce Keyframes', "reduceKeyframes.doReduceKeyframes()" )
This makes the "Reduce Keyframes" option appear if your right-click on the knob in the "Properties" panel
** UPDATE 16/02/14 **
Apparently the newer Tracker node introduced in Nuke 8 will not run this script as the copyAnimation() function fails (the "tracks" knob isn't exposed as an Array_Knob, or a subclass thereof). I will write a workaround when I next have access to the latest copy of Nuke
＊Julik describes his method as being based on the concept of Curve-fitting compaction, specifically citing the paper Data Reduction of Piecewise Linear Curves by Erlend Arge and Morten Dæhlen. He points out that "curve fitting is never lossless, and for some values even small deviations from the baked values could produce wildly different images - depending to which parameter you apply it to. It can be disastrous for camera tracks for example, and normally when you have dense curves they are dense for a reason."
This has now been fixed in v1.15