This code was done by Jed Smith and it is really useful to create Roto or Translate nodes linked to a Tracker node. The hotkeys I use for this tool are the ones Jed had it written for: alt+o for a linked Roto, and alt+l for a linked Transform.

If you want you can see the code (and many other amazing ones from Jed) on his GitHub.
Expand Link Tools
import nuke, nukescripts, nuke.rotopaint as rp, _curvelib as cl
'''
## LINK TOOLS
Utility functions for creating linked nodes. This script can create roto linked to trackers,
transform linked to trackers, or cameras linked to each other, or held on a projection frame.
Installation:
Put link_tools.py somewhere in your nuke path.
You can add the script as commands to your Nodes panel. This code creates a custom menu entry called "Scripts".
I have them set to the shortcuts Alt-O for roto and Alt-L for generalized link (works with tracker, transform, and camera nodes).
import link_tools
nuke.toolbar('Nodes').addMenu('Scripts').addCommand('Link Roto', 'link_tools.link("roto")', 'alt+o')
nuke.toolbar('Nodes').addMenu('Scripts').addCommand('Link Tool', 'link_tools.link()', 'alt+l')
'''
def link_transform(target_node):
"""
Utility function for link_transform: creates linked transform node
"""
target_name = target_node.name()
nclass = target_node.Class()
if "Tracker" not in nclass and "Transform" not in nclass:
print "Must select Tracker or Transform node!"
return
grid_x = int(nuke.toNode('preferences').knob('GridWidth').value())
grid_y = int(nuke.toNode('preferences').knob('GridHeight').value())
target_node.setSelected(False)
# Create linked Transform Node
trans = nuke.nodes.Transform()
trans.setName('TransformLink')
trans.setSelected(True)
trans.knob('help').setValue("<b>TransformLink</b>\n\nA Transform node with options for linking to a Tracker or a Transform node. \n\nAllows you to set a seperate identity transform frame from the linked Tracker. Select the link target and click 'Set Target', or set the link target by name. You can also bake the expression link into keyframes if you want it independant from the target node. \n\nThe transform Matchmoves or Stabilizes depending on what the parent tracker node is set to, but you can invert this by enabling the 'invert' checkbox.")
trans.knob('label').setValue('Matchmove: [value link_target]')
trans.setXYpos(target_node.xpos()-grid_x*0, target_node.ypos()+grid_y*2)
trans.addKnob(nuke.Tab_Knob('TrackLink'))
trans.addKnob(nuke.Int_Knob('identity_frame', 'Identity Frame'))
if 'Tracker' in nclass:
trans.knob('identity_frame').setValue( int(target_node.knob('reference_frame').value()) )
else:
trans.knob('identity_frame').setValue(nuke.frame())
trans.addKnob(nuke.PyScript_Knob('identity_to_curframe', 'Set to Current Frame'))
trans.knob('identity_to_curframe').setTooltip("Set identity frame to current frame.")
trans.knob('identity_to_curframe').setCommand("nuke.thisNode().knob('identity_frame').setValue(nuke.frame())")
trans.addKnob(nuke.PyScript_Knob('del_relative', 'Del Relative'))
trans.knob('del_relative').setTooltip('Delete relative transformation.\n\nUse original Transform values.')
trans.knob('del_relative').setCommand("# Delete Rel - remove identity frame transformations on transform link\ndef del_relative():\n n = nuke.thisNode()\n # Get target node name\n target_name = n['translate'].animation(0).expression().split(' - ')[0].split('parent.')[1].split('.translate')[0]\n n.knob('translate').clearAnimated()\n n.knob('translate').setExpression('parent.{0}.translate'.format(target_name))\n n.knob('rotate').clearAnimated()\n n.knob('rotate').setExpression('parent.{0}.rotate'.format(target_name))\n n.knob('scale').clearAnimated()\n n.knob('scale').setExpression('parent.{0}.scale'.format(target_name))\n n.knob('skewX').clearAnimated()\n n.knob('skewX').setExpression('parent.{0}.skewX'.format(target_name))\n n.knob('skewY').clearAnimated()\n n.knob('skewY').setExpression('parent.{0}.skewY'.format(target_name))\n n.knob('center').clearAnimated()\n n.knob('center').setExpression('parent.{0}.center'.format(target_name))\nif __name__ == '__main__':\n del_relative()")
trans.addKnob(nuke.String_Knob('link_target', 'Link Target', target_name))
trans.knob('link_target').setTooltip('Type in a node name to link to.')
trans.addKnob(nuke.PyScript_Knob('set_target', 'Set Target'))
trans.knob('set_target').clearFlag(nuke.STARTLINE)
trans.knob('set_target').setTooltip('Sets the link target to the selected node.\n\nIf no node is selected, set target node to the value of Link Target.')
trans.knob('set_target').setCommand("# Set Target Button\ndef link_to_target():\n trans = nuke.thisNode()\n selected_nodes = nuke.selectedNodes()\n if len(selected_nodes) is 0:\n target_name = nuke.thisNode().knob('link_target').getValue()\n target_node = nuke.toNode(target_name)\n if target_node is None:\n nuke.message('Target node does not exist!')\n return\n if len(selected_nodes) is 1:\n target_node = selected_nodes[0]\n target_name = target_node.name()\n trans.knob('link_target').setValue(target_name)\n if len(selected_nodes) > 1:\n print 'Error: Exactly 1 target node must be specified.'\n return\n target_class = target_node.Class()\n if target_class in ['Tracker3', 'Tracker4', 'Transform', 'TransformMasked']:\n trans.knob('translate').clearAnimated()\n trans.knob('translate').setExpression('parent.{0}.translate - parent.{0}.translate(identity_frame)'.format(target_name))\n trans.knob('rotate').clearAnimated()\n trans.knob('rotate').setExpression('parent.{0}.rotate - parent.{0}.rotate(identity_frame)'.format(target_name))\n trans.knob('scale').clearAnimated()\n trans.knob('scale').setExpression('parent.{0}.scale / parent.{0}.scale(identity_frame)'.format(target_name))\n trans.knob('skewX').clearAnimated()\n trans.knob('skewX').setExpression('parent.{0}.skewX - parent.{0}.skewX(identity_frame)'.format(target_name))\n trans.knob('skewY').clearAnimated()\n trans.knob('skewY').setExpression('parent.{0}.skewY - parent.{0}.skewY(identity_frame)'.format(target_name))\n trans.knob('center').clearAnimated()\n trans.knob('center').setExpression('parent.{0}.center+parent.{0}.translate(identity_frame)'.format(target_name))\n if target_class in ['Tracker3', 'Tracker4']:\n trans.knob('identity_frame').setValue( int(nuke.toNode(target_name).knob('reference_frame').getValue()) )\n else:\n print 'Error: Must select a Tracker or Transform class node.'\nif __name__ == '__main__':\n link_to_target()")
trans.addKnob(nuke.PyScript_Knob('bake_link', 'Bake Expression Links'))
trans.knob('bake_link').setFlag(nuke.STARTLINE)
trans.knob('bake_link').setTooltip('Bake expression links to keyframes')
trans.knob('bake_link').setCommand("# Bake Expression Links Button\ndef bake_expression_links():\n trans = nuke.thisNode()\n link_target_name = trans.knob('link_target').getValue()\n target_node = nuke.toNode(link_target_name)\n if target_node.knob('translate').isAnimated():\n # Get animation framerange from translate knob of target tracker node\n target_anims = target_node.knob('translate').animations()\n first_key = target_anims[0].keys()[0].x\n last_key = target_anims[0].keys()[-1].x\n else:\n nuke.message('No Keys on Source!')\n return\n # Bake knob values\n def bake_knob( node, knob, first_key, last_key):\n first_key = str(first_key)\n last_key = str(last_key)\n def horizontal_ends(knob, i):\n # Set start and end keyframe to horizontal interpolation so it matches expression before and after animation\n anim = knob.animation(i)\n anim.changeInterpolation( [anim.keys()[0], anim.keys()[-1]], nuke.HORIZONTAL)\n return\n if knob.width() == 1:\n nuke.animation('{0}.{1}'.format(node.name(), knob.name()), 'generate', (first_key, last_key, '1', 'y', '{0}'.format(knob.name())))\n horizontal_ends(knob, 0)\n if knob.width() == 2: \n if knob.name() == 'scale':\n if knob.singleValue() == True:\n nuke.animation('{0}.scale'.format(node.name()), 'generate', (first_key, last_key, '1', 'y', 'scale'))\n horizontal_ends(knob, 0)\n else:\n for i, dim in enumerate(['w', 'h']):\n nuke.animation('{0}.{1}.{2}'.format(node.name(), knob.name(), dim), 'generate', (first_key, last_key, '1', 'y', '{0}.{1}'.format(knob.name(), dim)))\n horizontal_ends(knob, i)\n else:\n for i, dim in enumerate(['x', 'y']):\n nuke.animation('{0}.{1}.{2}'.format(node.name(), knob.name(), dim), 'generate', (first_key, last_key, '1', 'y', '{0}.{1}'.format(knob.name(), dim)))\n horizontal_ends(knob, i)\n for knob_name, knob in trans.knobs().iteritems():\n if knob_name in ['translate', 'rotate', 'scale', 'skewX', 'skewY', 'center']:\n print 'Baking {0}'.format(knob_name)\n bake_knob(trans, knob, first_key, last_key)\nif __name__ == '__main__':\n bake_expression_links()")
trans.addKnob(nuke.Text_Knob(""))
trans.addKnob(nuke.Double_Knob('motionblur_gui', 'gui mblur'))
trans.knob('motionblur_gui').setValue(0)
trans.knob('motionblur_gui').setTooltip('Set the $gui motion blur value')
# Link knobs
trans.knob('translate').setExpression('parent.{0}.translate - parent.{0}.translate(identity_frame)'.format(target_name))
trans.knob('rotate').setExpression('parent.{0}.rotate - parent.{0}.rotate(identity_frame)'.format(target_name))
trans.knob('scale').setExpression('parent.{0}.scale / parent.{0}.scale(identity_frame)'.format(target_name))
trans.knob('skewX').setExpression('parent.{0}.skewX - parent.{0}.skewX(identity_frame)'.format(target_name))
trans.knob('skewY').setExpression('parent.{0}.skewY - parent.{0}.skewY(identity_frame)'.format(target_name))
trans.knob('center').setExpression('parent.{0}.center+parent.{0}.translate(identity_frame)'.format(target_name))
trans.knob('motionblur').setExpression('$gui ? motionblur_gui : 4')
trans.knob('shutteroffset').setValue('centred')
def link_roto(tracker_node, roto_node=False):
'''
Utility function: Creates a layer in roto_node linked to tracker_node
if roto_node is False, creates a roto node next to tracker node to link to
'''
grid_x = int(nuke.toNode('preferences').knob('GridWidth').value())
grid_y = int(nuke.toNode('preferences').knob('GridHeight').value())
tracker_name = tracker_node.name()
tracker_node.setSelected(False)
# If Roto node not selected, create one.
if not roto_node:
roto_node = nuke.nodes.Roto()
roto_node.setXYpos(tracker_node.xpos()-grid_x*0, tracker_node.ypos()+grid_y*2)
roto_node.setSelected(True)
# Create linked layer in Roto Node
curves_knob = roto_node["curves"]
stab_layer = rp.Layer(curves_knob)
stab_layer.name = "stab_"+tracker_name
trans_curve_x = cl.AnimCurve()
trans_curve_y = cl.AnimCurve()
trans_curve_x.expressionString = "parent.{0}.translate.x".format(tracker_name)
trans_curve_y.expressionString = "parent.{0}.translate.y".format(tracker_name)
trans_curve_x.useExpression = True
trans_curve_y.useExpression = True
rot_curve = cl.AnimCurve()
rot_curve.expressionString = "parent.{0}.rotate".format(tracker_name)
rot_curve.useExpression = True
scale_curve = cl.AnimCurve()
scale_curve.expressionString = "parent.{0}.scale".format(tracker_name)
scale_curve.useExpression = True
center_curve_x = cl.AnimCurve()
center_curve_y = cl.AnimCurve()
center_curve_x.expressionString = "parent.{0}.center.x".format(tracker_name)
center_curve_y.expressionString = "parent.{0}.center.y".format(tracker_name)
center_curve_x.useExpression = True
center_curve_y.useExpression = True
# Define variable for accessing the getTransform()
transform_attr = stab_layer.getTransform()
# Set the Animation Curve for the Translation attribute to the value of the previously defined curve, for both x and y
transform_attr.setTranslationAnimCurve(0, trans_curve_x)
transform_attr.setTranslationAnimCurve(1, trans_curve_y)
# Index value of setRotationAnimCurve is 2 even though there is only 1 parameter...
# http://www.mail-archive.com/nuke-python@support.thefoundry.co.uk/msg02295.html
transform_attr.setRotationAnimCurve(2, rot_curve)
transform_attr.setScaleAnimCurve(0, scale_curve)
transform_attr.setScaleAnimCurve(1, scale_curve)
transform_attr.setPivotPointAnimCurve(0, center_curve_x)
transform_attr.setPivotPointAnimCurve(1, center_curve_y)
curves_knob.rootLayer.append(stab_layer)
def link_camera(src_cam, proj_frame, expr_link, clone, index):
"""
Create a linked camera to src_cam -- this camera can be frozen on a projection frame
"""
# Default grid size is 110x24. This enables moving by grid increments.
# http://forums.thefoundry.co.uk/phpBB2/viewtopic.php?t=3739&sid=c40e65b1f575ba9166583faf807184ee
# Offset by 1 grid in x for each additional iteration
grid_x = int(nuke.toNode('preferences').knob('GridWidth').value())
grid_y = int(nuke.toNode('preferences').knob('GridHeight').value())*index
src_cam.setSelected(False)
# Create Projection Camera
proj_cam = nuke.nodes.Camera2()
# Create Projection Frame knob
frame_tab = nuke.Tab_Knob('Frame')
proj_cam.addKnob(frame_tab)
proj_frame_knob = nuke.Double_Knob('proj_frame')
if clone:
proj_frame_knob.setExpression('t')
else:
proj_frame_knob.setValue(proj_frame)
proj_cam.addKnob(proj_frame_knob)
## Copy the knob values of the Source Camera
for knob_name, knob in src_cam.knobs().iteritems():
# For Animated knobs, copy or link the values depending on if Expression Links are enabled.
if knob.isAnimated():
#print "setting animated knob", knob_name
if expr_link == True:
#print "setting expression for animated knobs"
for index in range(src_cam.knob(knob_name).arraySize()):
if src_cam.knob(knob_name).isAnimated(index):
proj_cam[knob_name].copyAnimation(index, src_cam[knob_name].animation(index))
proj_cam[knob_name].setExpression('parent.' + src_cam.name() + "." + knob_name + "(proj_frame)")
# http://www.nukepedia.com/python/knob-animation-and-python-a-primer/
else:
for index in range(src_cam.knob(knob_name).arraySize()):
if src_cam.knob(knob_name).isAnimated(index):
proj_cam[knob_name].copyAnimation(index, src_cam[knob_name].animation(index))
proj_cam[knob_name].setExpression('curve(proj_frame)')
# For all non-animated knobs that are not set to default values, match value from src_cam
elif hasattr(knob, "notDefault") and knob.notDefault():
try:
#print "changing ", knob_name
proj_cam[knob_name].setValue(knob.value())
except TypeError:
pass
# Set label, color, name, and position
proj_cam.setXYpos(src_cam.xpos()-grid_x, src_cam.ypos()-grid_y*4)
if clone:
proj_cam.setName("{0}_CLONE_".format(src_cam.name()))
proj_cam["gl_color"].setValue(0xff5f00ff)
proj_cam["tile_color"].setValue(0xff5f00ff)
else:
proj_cam.setName("{0}_PROJ_".format(src_cam.name()))
proj_cam["gl_color"].setValue(0xffff)
proj_cam["tile_color"].setValue(0xffff)
proj_cam["label"].setValue("FRAME [value proj_frame]")
def link(link_type=None):
nodes = nuke.selectedNodes()
track_nodes = [n for n in nodes if 'Tracker' in n.Class()]
cam_nodes = [n for n in nodes if n.Class() in ['Camera', 'Camera2']]
roto_nodes = [n for n in nodes if n.Class() in ['Roto', 'RotoPaint', 'SplineWarp3']]
xform_nodes = [n for n in nodes if n.Class() == 'Transform']
if link_type == 'roto':
if len(track_nodes) == 0:
print "Error: At least one Tracker node must be selected."
return
if len(roto_nodes) > 1 and len(track_nodes) > 1:
print "Error: if multiple roto nodes are selected, exactly 1 Tracker node must be selected."
return
for track_node in track_nodes:
if len(roto_nodes) is 0:
link_roto(track_node)
if len(roto_nodes) >= 1:
for roto_node in roto_nodes:
link_roto(track_node, roto_node)
if link_type == None:
for track_node in track_nodes:
link_transform(track_node)
for xform_node in xform_nodes:
link_transform(xform_node)
for cam_node in cam_nodes:
## Set up camera linker panel
p = nuke.Panel('Camera Linker: {0}'.format(cam_node.name()))
p.setWidth(450)
p.addSingleLineInput('Frame', str(nuke.frame()))
p.addBooleanCheckBox("Clone", False)
p.addBooleanCheckBox('Link', True)
if p.show():
clone = p.value('Clone')
framestring = p.value('Frame')
expr_link = p.value('Link')
else:
# Cancelled
return
## Parse frame string
if "," in framestring:
framelist = map(int, framestring.split(','))
for i, frame in enumerate(framelist):
link_camera(cam_node, frame, expr_link, clone, i)
else:
try:
framestring = int(framestring)
except:
print "Error converting frame to integer!"
return
link_camera(cam_node, framestring, expr_link, clone, 0)