Last active
May 13, 2025 06:47
-
-
Save promto-c/23f2cc747c90a5cb3a418d00c5e5f4c7 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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