Skip to content

Instantly share code, notes, and snippets.

@promto-c
Last active May 13, 2025 06:47
Show Gist options
  • Save promto-c/23f2cc747c90a5cb3a418d00c5e5f4c7 to your computer and use it in GitHub Desktop.
Save promto-c/23f2cc747c90a5cb3a418d00c5e5f4c7 to your computer and use it in GitHub Desktop.
import nuke
import csv
# Constant for the number of columns per track in Tracker4 node
COLUMNS_PER_TRACK = 31
CSV_HEADERS = ['Track Name', 'Frame', 'X', 'Y']
def get_total_tracks_count(tracks_knob):
"""Calculate the number of tracks based on the tracks knob.
Args:
tracks_knob (nuke.Knob): The knob object representing the tracks in a Tracker4 node.
Returns:
int: The total number of tracks.
"""
# Initialize the count of total tracks to 0.
total_tracks_count = 0
# Loop until the fully qualified name of the knob ends with '.tracks', indicating no more tracks are present.
while not tracks_knob.fullyQualifiedName(total_tracks_count * COLUMNS_PER_TRACK).endswith('.tracks'):
# Increment the total tracks count if the condition is not met
total_tracks_count += 1
# Return the total count of tracks found in the tracker node.
return total_tracks_count
def extract_tracks_data(tracker_node):
"""Extract tracking data from a tracker node.
Args:
tracker_node (nuke.Node): A nuke.Node object representing the Tracker4 node.
Returns:
List[Tuple[str, int, float, float]]: A list of tuples containing track data (track name, frame, x position, y position).
Raises:
ValueError: If the input node is not a Tracker4 node.
"""
# Raise a ValueError if the input node is not a Tracker4 node.
if tracker_node.Class() != 'Tracker4':
raise ValueError("Input node is not a Tracker4 node.")
# Retrieve the 'tracks' knob from the specified tracker node.
tracks_knob = tracker_node.knob('tracks')
# Calculate the total number of tracks in the tracker node.
total_tracks_count = get_total_tracks_count(tracks_knob)
# Initialize a list to hold the tracking data for export.
tracking_data_list = []
# Iterate through each track based on the total tracks count.
for index in range(total_tracks_count):
# Calculate the base index for the current track in the knob.
track_base_index = index * COLUMNS_PER_TRACK
# Calculate indices for the X and Y position within the track's knob.
x_position_index = track_base_index + 2
y_position_index = track_base_index + 3
# Get the number of keyframes for X and Y positions.
num_x_keys = tracks_knob.getNumKeys(x_position_index)
num_y_keys = tracks_knob.getNumKeys(y_position_index)
# Collect frame numbers from all X and Y position keyframes using set union.
frame_set = (
{tracks_knob.getKeyTime(key_index, x_position_index) for key_index in range(num_x_keys)} |
{tracks_knob.getKeyTime(key_index, y_position_index) for key_index in range(num_y_keys)}
)
# For each frame with keyframe data, retrieve the X and Y positions and append them to the tracking data list.
for frame in sorted(frame_set):
x_pos = tracks_knob.getValueAt(frame, x_position_index)
y_pos = tracks_knob.getValueAt(frame, y_position_index)
# Append track identifier (index), frame number, and positions to the list.
tracking_data_list.append((str(index), frame, x_pos, y_pos))
# Return the compiled list of tracking data for all tracks.
return tracking_data_list
def export_tracks_to_csv(tracker_node, csv_file_path, headers=CSV_HEADERS):
"""Export tracking data from a tracker node to a CSV file.
Args:
tracker_node (nuke.Node): A nuke.Node object representing the Tracker4 node.
csv_file_path (str): The file path to save the CSV file.
headers (List[str]): Optional. Custom headers for the CSV file. Defaults to CSV_HEADERS.
"""
# Extract tracking data from the tracker node.
tracking_data_list = extract_tracks_data(tracker_node)
# Open CSV file for writing and instantiate CSV writer.
with open(csv_file_path, 'w', newline='') as csvfile:
csv_writer = csv.writer(csvfile)
# Write header row.
csv_writer.writerow(headers)
# Write all tracking data to CSV.
csv_writer.writerows(tracking_data_list)
# Calculate the count of unique tracks exported.
exported_track_count = len({data[0] for data in tracking_data_list})
return exported_track_count
# Example usage
if __name__ == '__main__':
csv_file_path = '/path/to/your/tracking_data_list.csv'
export_tracks_to_csv(nuke.selectedNode(), csv_file_path)
import re
from typing import Dict, List, Tuple
def extract_tracks_from_syntheyes_text(text: str, frame_offset: int = 0) -> Dict[str, List[Tuple[int, float, float]]]:
"""Extract tracking data from a text string into a dict of keyframe lists.
Args:
text: The input string containing tracker-name headers and keyframe lines.
frame_offset: Frame offset to apply to each extracted keyframe.
Returns:
A dict mapping each tracker name to a list of (frame, x, y) tuples.
Examples:
>>> sample = '''
... Tracker1
... 1 100.0 200.0
... 2 110.0 210.0
...
... Tracker2
... 3 120.0 220.0
... 4 130.0 230.0
... '''
>>> extract_tracks_from_syntheyes_text(sample)
{'Tracker1': [(1, 100.0, 200.0), (2, 110.0, 210.0)], 'Tracker2': [(3, 120.0, 220.0), (4, 130.0, 230.0)]}
>>> extract_tracks_from_syntheyes_text(sample, frame_offset=5)
{'Tracker1': [(6, 100.0, 200.0), (7, 110.0, 210.0)], 'Tracker2': [(8, 120.0, 220.0), (9, 130.0, 230.0)]}
"""
# Pattern to capture each tracker block: header plus following non-header lines
BLOCK_RE = re.compile(
r'^(?P<name>\w+)\s*'
r'(?P<block>(?:(?!\w+\s*$).*(?:\n|$))*)',
re.MULTILINE
)
# Pattern to parse individual keyframe lines, multiline flag to allow ^/$ per line
KEYFRAME_RE = re.compile(
r'^\s*(?P<frame>\d+)\s+'
r'(?P<x>[+-]?\d+(?:\.\d+)?)\s+'
r'(?P<y>[+-]?\d+(?:\.\d+)?)\s*$',
re.MULTILINE
)
# Convert and store keyframes returned
return {
name: [
(int(f) + frame_offset, float(x), float(y))
for f, x, y in KEYFRAME_RE.findall(block)
]
for name, block in BLOCK_RE.findall(text)
}
if __name__ == '__main__':
import doctest
doctest.testmod()
import nuke
import csv
from collections import defaultdict
# The number of columns per track in Tracker4 node
COLUMNS_PER_TRACK = 31
def read_tracking_data_from_csv(csv_file_path, name_column=0, frame_column=1, x_column=2, y_column=3, is_skip_header=True):
"""Reads tracking data from a CSV file and organizes it into a defaultdict.
Args:
csv_file_path (str): The file path to the CSV file.
name_column (int): The column index for the track name.
frame_column (int): The column index for the frame number.
x_column (int): The column index for the X coordinate.
y_column (int): The column index for the Y coordinate.
is_skip_header (bool): Flag to determine whether to skip the first row (header) of the CSV.
Returns:
DefaultDict[str, List[Tuple[int, float, float]]]: A dictionary with track names as keys and lists of frame data as values.
"""
# Initialize a defaultdict for tracking data, with lists for each track name.
track_name_to_data = defaultdict(list)
# Open and read the CSV file. Optionally skip the header row based on is_skip_header.
with open(csv_file_path, 'r') as csvfile:
csvreader = csv.reader(csvfile)
# SZkip the header row if is_skip_header is True.
if is_skip_header:
next(csvreader)
# Extract and store frame data (frame, x, y) for each track name from the CSV rows.
for row in csvreader:
name, frame, x, y = row[name_column], int(row[frame_column]), float(row[x_column]), float(row[y_column])
track_name_to_data[name].append((frame, x, y))
# Return the dictionary with all track data organized by track name.
return track_name_to_data
def create_tracker_from_csv(csv_file_path, name_column=0, frame_column=1, x_column=2, y_column=3, is_skip_header=True):
"""Creates a Tracker node in Nuke from a CSV file containing tracking data,
with customizable column mappings and an option to skip or not skip the header row.
Args:
csv_file_path: String specifying the file path to the CSV containing tracking data.
name_column (int): The column index for the track name.
frame_column (int): The column index for the frame number.
x_column (int): The column index for the X coordinate.
y_column (int): The column index for the Y coordinate.
is_skip_header: Boolean indicating whether to skip the first row (header) of the CSV file. Default is True.
"""
# Load tracking data into a defaultdict mapping track names to lists of (frame, x, y) tuples.
track_name_to_data = read_tracking_data_from_csv(
csv_file_path,
name_column, frame_column, x_column, y_column,
is_skip_header
)
# Return early if the tracking data is empty.
if not track_name_to_data:
nuke.message("No tracking data found.")
return
# Get the total number of tracks to check progress.
total_tracks = len(track_name_to_data)
# Initialize a progress task for UI feedback if Nuke is running in GUI mode.
progress_task = nuke.ProgressTask("Importing Tracking Data") if nuke.GUI else None
# Create a new Tracker node
tracker_node = nuke.createNode('Tracker4')
tracks_knob = tracker_node.knob('tracks')
# Prepare to remove the automatically created keyframe if necessary
current_frame = nuke.frame()
# Iterate over each track in the track_name_to_data dictionary.
for index, (track_name, frames) in enumerate(track_name_to_data.items()):
# Allow the user to cancel the operation if in GUI mode
if progress_task and progress_task.isCancelled():
break
# Update the task progress if in GUI mode
if progress_task:
progress_task.setProgress(int(float(index) / total_tracks * 100))
progress_task.setMessage(f"Importing: {track_name}")
# Add a new track for each unique track name
tracker_node.knob('add_track').execute()
# Calculate the base index for the current track in the knob.
track_base_index = index * COLUMNS_PER_TRACK
# Calculate indices for the X and Y position within the track's knob.
x_position_index = track_base_index + 2
y_position_index = track_base_index + 3
for frame, x, y in frames:
# Set X and Y positions.
tracks_knob.setValueAt(x, frame, x_position_index)
tracks_knob.setValueAt(y, frame, y_position_index)
# Construct a list of frame numbers and remove the automatically created keyframe if it's not needed.
frame_numbers = [frame for frame, _, _ in frames]
if current_frame not in frame_numbers:
tracks_knob.removeKeyAt(current_frame, x_position_index)
tracks_knob.removeKeyAt(current_frame, y_position_index)
# Finalize the task if in GUI mode
if progress_task:
progress_task.setProgress(100)
# Example usage
if __name__ == '__main__':
csv_file_path = '/path/to/your/tracking_data.csv'
create_tracker_from_csv(csv_file_path)
import nuke
from import_track_from_csv import create_tracker_from_csv
from export_track_to_csv import export_tracks_to_csv
import os, webbrowser
def import_tracking_data_panel():
"""Presents a UI panel for importing tracking data from a CSV file into Nuke.
This function creates a UI panel that asks the user for the path to a CSV file,
column indices for the track name, frame number, X position, Y position, and
whether to skip the header row. It then calls `create_tracker_from_csv` with
these parameters to import the tracking data into a Tracker4 node in Nuke.
"""
# Create a panel to gather user input for CSV import parameters.
panel = nuke.Panel('Import Tracking Data')
# Remove the 'file://' prefix if the path starts with 'file://'.
csv_file_path = csv_file_path[7:] if csv_file_path.startswith('file://') else csv_file_path
panel.addFilenameSearch('CSV File Path', '')
panel.addSingleLineInput('Name Column', '0')
panel.addSingleLineInput('Frame Column', '1')
panel.addSingleLineInput('X Column', '2')
panel.addSingleLineInput('Y Column', '3')
panel.addBooleanCheckBox('Skip Header Row', True)
# Show the panel to the user and proceed only if the user confirm
if not panel.show():
# User cancelled, exit the function
return
# Retrieve values from the panel
csv_file_path = panel.value('CSV File Path')
name_column = int(panel.value('Name Column'))
frame_column = int(panel.value('Frame Column'))
x_column = int(panel.value('X Column'))
y_column = int(panel.value('Y Column'))
is_skip_header = panel.value('Skip Header Row')
try:
# Attempt to create a Tracker node from the provided CSV file
create_tracker_from_csv(csv_file_path, name_column, frame_column, x_column, y_column, is_skip_header)
except Exception as e:
# If an error occurs, display it in a message box
nuke.message("Failed to import tracking data: {}".format(e))
def export_tracking_data_panel():
"""Presents a UI panel for exporting tracking data from a selected Tracker4 node to a CSV file.
The default file path is set to the directory of the current Nuke script, with the filename
being the selected node's name followed by '_tracks.csv'. It checks if a node is selected
and verifies that it is a Tracker4 node.
"""
# Attempt to get the selected node and verify it's a Tracker4 node
try:
# NOTE: nuke.selectedNode() will raise a ValueError if no node selected in Nuke.
selected_node = nuke.selectedNode()
if selected_node.Class() != 'Tracker4':
raise ValueError
except ValueError:
nuke.message("Please select a Tracker4 node.")
return
# Get the directory of the current Nuke script
script_dir = os.path.dirname(nuke.root().name())
# Construct the default file path using the selected Tracker4 node's name
node_name = selected_node['name'].value()
default_filename = f"{node_name}_tracks.csv"
default_path = os.path.join(script_dir, default_filename)
# Create and display the panel
panel = nuke.Panel('Export Tracking Data')
panel.addFilenameSearch('CSV File Path', default_path)
if not panel.show():
return # User cancelled, exit the function
# Retrieve the file path from the panel
csv_file_path = panel.value('CSV File Path')
# Export tracking data from selected node to CSV file path
exported_track_count = export_tracks_to_csv(selected_node, csv_file_path)
# Custom panel for post-export options
post_export_panel = nuke.Panel("Export Complete")
post_export_panel.addSingleLineInput("Exported Tracks", str(exported_track_count))
post_export_panel.addButton("Open Exported Directory")
post_export_panel.addButton("Ok")
choice = post_export_panel.show()
# "Open Exported Directory" button has been clicked
if choice == 0:
# Open the directory containing the exported CSV file
webbrowser.open('file://' + os.path.dirname(csv_file_path))
if __name__ == '__main__':
# Add to Nuke menu
nuke.menu('Nuke').addCommand('MyTools/Import Tracking Data...', import_tracking_data_panel)
nuke.menu('Nuke').addCommand('MyTools/Export Tracking Data...', export_tracking_data_panel)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment