Nuke & Python: Making your first functional plugin: Node colors presets. Part 2/2

Last week I wrote about how to make your first little plugin for Nuke using Python.
It was a good start but it was missing a few functions to be fully done.

The biggest issue was that once a preset is created, you had no way to edit or delete it, without going to change it manually in the system files. That will be the first thing we will address today.
Secondly, I’d like to automatically name the colors the user picks, and present that as a name suggestion when creating a preset, instead of just “Color”. That might end up being a decent amount of work for not much, but it’s something I’ve been interested in for a while now.


Disclaimer: This is a pretty imposing amount of text and code for a beginner, and I understand if it scares you a bit. It stretched on more than I had originally hoped because I tried to explain as much as I could, and give examples that aren’t part of the final code. I hope you will forgive me and manage to get through.

Managing the Color Presets

The most instinctive and “nukest” way I can think about to manage the presets would be to open a Nuke Panel.

Before I even think about coding, I find it helpful to think about what my panel will contain and how it will look like. A good old paper notebook and pen help me lay that down.

A beautifully photographed Sketch of my future Panel

A beautifully photographed Sketch of my future Panel

The way I’m seeing it right now, is that each of the presets name will be displayed as an editable Text field, next to it will be a little color chip, and a checkbox for deleting.
At he bottom I may or may not add a “New Preset” button, I’m not too sure how I would handle that code wise. Finally, I’ll build that as a modalDialog, which will give me a Cancel and OK buttons for free.
If the user clicks OK, the changes will be applied, otherwise nothing will happen.

For coding, I will rely on this page from the documentation: http://docs.thefoundry.co.uk/nuke/80/pythondevguide/custom_panels.html
(I’m working in Nuke 8 at the moment)

The first issue I run into when thinking about my code, is that I’m not sure how many knobs I will have in my panel. There could be None, or there could be thousands..
The knobs building needs to be based on the same dictionary we used in part 1. Each 3 knobs (name, color, delete) also need to be linked to each other, so that when we run the code through at the end, we know which color it is that we would like to delete/edit.

It’s actually the first time I run into this issue, and I’m not too sure how to handle that.
When this happens, the best way to get unstuck is to browse the web for a solution, look for another tool using a similar function, or ask someone who knows.

(You can’t actually know, but at this point I paused the writing of the article for more than 24h. During these 24h, I did a lot of things completely unrelated, but I also spent a considerable amount of time thinking about the best way to approach this. I also sent an email to the nuke-python mail list to check the best way to approach this. No answers so far.)

I couldn’t find an example when someone had to do the same thing, so I just got into nuke and started coding a test Panel, based on the examples in the documentation.
I knew I would have to loop through my dictionary to create all the appropriate knobs. Since all the knobs for a specific dictionary entry would need to relate to each other, I decided to group them into a dictionary, then add this dictionary to a list.
Here’s my test code:

import nukescripts

dict = {"Test01": 50, "Test02": 120}


class Test(nukescripts.PythonPanel):
    def __init__(self, color_dict):
        super(Test, self).__init__('Test')
        # CREATE KNOBS DICTS IN A LISTS
        self.list = []
        for color in color_dict:
            knob_dict = {"name": nuke.String_Knob(color, '', color),
                         "value": nuke.Int_Knob('value_%s' % color, 'Value')}
            knob_dict["value"].setValue(color_dict[color])
            knob_dict["value"].clearFlag(nuke.STARTLINE)
            self.list.append(knob_dict)
            
        # ADD KNOBS
        for i in self.list:
            for k in i:
                self.addKnob( i[k] )

panel = Test(dict)
if panel.showModalDialog():
    print panel.list

I’m pretty happy with the way this behaves. Note the “self.” in front of my list variable. This makes the variable accessible at the end with “panel.list”. Without it I wouldn’t be able to access the list outside of my panel.

Note: I’m making a big mistake in this test code by naming my variable “dict”. This would “overwrite” the default python dict, which could be bad. I’m leaving it above as an example,  and to show everybody makes stupid mistakes sometimes. I will change the name of that variable going forward..

Now, let’s re-arrange the code to match better what we’re actually trying to do.

import nukescripts

color_dict = {"Green": 16318464, "Pink" : 4288256256}


class ColorPresetPanel(nukescripts.PythonPanel):
    def __init__(self, colors):
        super(ColorPresetPanel, self).__init__('Manage Color Presets')
        # Create a list that will contain all the knobs for easy access.
        self.knob_list = []
        for color in colors:
            label_knob = nuke.String_Knob("name_%s" % color, '', color)
            value_knob = nuke.ColorChip_Knob('value_%s' % color, '')
            delete_checkbox = nuke.Boolean_Knob("delete_%s" % color, 'delete')

            value_knob.setValue(colors[color])
            value_knob.clearFlag(nuke.STARTLINE)
            delete_checkbox.clearFlag(nuke.STARTLINE)

            knob_dict = {"name": label_knob,
                         "value": value_knob,
                         "delete": delete_checkbox}

            self.knob_list.append(knob_dict)
            
            # Add the knobs to the panel
            for knob in [label_knob, value_knob, delete_checkbox]:
                self.addKnob(knob)


panel = ColorPresetPanel(color_dict)
if panel.showModalDialog():
    print panel.knob_list
It's starting to look like the original sketch. I should set an icon for the top left..

It’s starting to look like the original sketch. There is an ugly icon in the corner, but so far I haven’t found the way to get rid of it without making a full Qt panel. Only happens on windows..

In the original sketch, I had a button to add a new preset directly from this panel. I don’t think I actually want this in my final plugin, but since you’re now making the plugin yourself, if you do want it, feel free to code it as an exercise!

There are a few tricky things tu do that, and I wrote an example solution for it:

class ColorPresetPanel(nukescripts.PythonPanel):
    def __init__(self, colors):
        super(ColorPresetPanel, self).__init__('Manage Color Presets')
        # Create a list that will contain all the knobs for easy access.
        self.knob_list = []
        for color in colors:
            self.addColorKnobs(color, colors[color])

        # Create button knobs
        self.addButton = nuke.Script_Knob("+")
        self.addButton.setFlag(nuke.STARTLINE)
        self.okButton = nuke.Script_Knob("OK")
        self.cancelButton = nuke.Script_Knob("Cancel")

        # Also defining a counter, so that when we add new fields, we can number them.
        self.number = 1

        # Add buttons
        self.addButtons()

    def addButtons(self):
        """ Add the +, OK, and Cancel buttons """
        self.addKnob(self.addButton)
        self.addKnob(self.okButton)
        self.addKnob(self.cancelButton)

    def removeButtons(self):
        """ Remove the +, OK, and Cancel buttons """
        self.removeKnob(self.addButton)
        self.removeKnob(self.okButton)
        self.removeKnob(self.cancelButton)

    def addColorKnobs(self, color_name, color_value=None):
        """ Adds knobs for name, color and delete """
        label_knob = nuke.String_Knob("name_%s" % color_name, '', color_name)
        value_knob = nuke.ColorChip_Knob('value_%s' % color_name, '')
        delete_checkbox = nuke.Boolean_Knob("delete_%s" % color_name, 'delete')

        if color_value:
            value_knob.setValue(color_value)
        value_knob.clearFlag(nuke.STARTLINE)
        delete_checkbox.clearFlag(nuke.STARTLINE)

        knob_dict = {"name": label_knob,
                     "value": value_knob,
                     "delete": delete_checkbox}

        self.knob_list.append(knob_dict)

        # Add the knobs to the panel
        for knob in [label_knob, value_knob, delete_checkbox]:
            self.addKnob(knob)

    def addColor(self):
        # Remove 3 buttons: Without that, the new field would be added after them.
        self.removeButtons()
        # Add the new knobs
        self.addColorKnobs("new_color%i" % self.number)
        self.number += 1
        # Put back the 3 buttons we removed
        self.addButtons()

    def knobChanged(self, knob):
        if knob in[self.addButton, ]:
            self.addColor()

Processing the info from the panel

We now have a nicely working panel, but it isn’t actually doing anything yet.
Right now, we’re feeding a dictionary into the panel, it gets parsed into knobs. From the modified knobs, we should be able to parse a new updated dictionary.
For the test, I’m going to feed the panel a dictionary containing a wrong value (A magenta that has value of Cyan), and try to see if I can edit it.

color_dict = {"Green": 16318464, "Pink" : 4288256256}


class ColorPresetPanel(nukescripts.PythonPanel):
    def __init__(self, colors):
        super(ColorPresetPanel, self).__init__('Manage Color Presets')
        # Create a list that will contain all the knobs for easy access.
        self.knob_list = []
        for color in colors:
            label_knob = nuke.String_Knob("name_%s" % color, '', color)
            value_knob = nuke.ColorChip_Knob('value_%s' % color, '')
            delete_checkbox = nuke.Boolean_Knob("delete_%s" % color, 'delete')

            value_knob.setValue(colors[color])
            value_knob.clearFlag(nuke.STARTLINE)
            delete_checkbox.clearFlag(nuke.STARTLINE)

            knob_dict = {"name": label_knob,
                         "value": value_knob,
                         "delete": delete_checkbox}

            self.knob_list.append(knob_dict)
            
            # Add the knobs to the panel
            for knob in [label_knob, value_knob, delete_checkbox]:
                self.addKnob(knob)


panel = ColorPresetPanel(color_dict)
if panel.showModalDialog():
    new_colors = {}
    for preset in panel.knob_list:
        if not preset['delete'].value():
            new_colors[preset['name'].value()] = preset['value'].value()
    print new_colors
#I'm renaming Magenta to Cyan, and deleting Green
# Result:
{'Blue': 65280, 'Cyan': 16776960, 'Red': 4278190080L}

Cool, that worked. Notice how my ‘Red’ value got a L suffix. That’s just a way python 2 indicates this is a long int. It shouldn’t affect us at all.

Now let’s merge it all with our code from last week to get a final code (for this step).
Don’t forget to add a menu entry to open the panel.

import os
import json
import nuke
import nukescripts

# Settings
PATH = os.path.expanduser("~/.nuke/nodeColorPresets.json")
TARGET_MENU = nuke.toolbar("Nodes")


class ColorPresetPanel(nukescripts.PythonPanel):
    """ Custom Python Panel to manage color presets """
    def __init__(self, colors):
        super(ColorPresetPanel, self).__init__('Manage Color Presets')
        # Create a list that will contain all the knobs for easy access.
        self.knob_list = []
        for color in colors:
            label_knob = nuke.String_Knob("name_%s" % color, '', color)
            value_knob = nuke.ColorChip_Knob('value_%s' % color, '')
            delete_checkbox = nuke.Boolean_Knob("delete_%s" % color, 'delete')

            value_knob.setValue(colors[color])
            value_knob.clearFlag(nuke.STARTLINE)
            delete_checkbox.clearFlag(nuke.STARTLINE)

            knob_dict = {"name": label_knob,
                         "value": value_knob,
                         "delete": delete_checkbox}

            self.knob_list.append(knob_dict)

            # Add the knobs to the panel
            for knob in [label_knob, value_knob, delete_checkbox]:
                self.addKnob(knob)


def readColorPresets(path):
    """
    Read Color presets from Json file
    """
    if not os.path.isfile(path):
        print "No Color preset found "
        return {}
    else:
        try:
            f = open(path)
            colors = json.load(f)
            f.close()
            if isinstance(colors, dict):
                return colors
            else:
                print "The preset file doesn't contain a valid dictionary"
                return {}
        except:
            print "Error reading color preset file"
            return {}


def writeColorPresets(path, colors):
    """
    Write Color presets to Json file
    """
    try:
        f = open(path, "w")
        json.dump(colors, f)
        f.close()
        return True
    except:
        return False


#Function to change tile_color:
def setTileColor(value=None):
    """
    Changes the tile_color of multiple nodes at once
    """
    if value is None:
        value = nuke.getColor()
    for node in nuke.selectedNodes():
        node.knob('tile_color').setValue(value)


def createTileColorMenu():
    """
    Create the Menu entry for the Tile Color Presets
    """
    # Loading out color presets
    colors = readColorPresets(PATH)

    # And creating the menu.
    color_menu = TARGET_MENU.addMenu('Color Nodes', icon="color_node.png")

    # Clear the menu if already populated
    color_menu.clearMenu()

    # Populating the Menu
    for color in sorted(colors):
        color_menu.addCommand(color, lambda x=colors[color]: setTileColor(x))

    color_menu.addCommand("Custom Color", lambda: setTileColor())
    color_menu.addCommand("-", "", "")
    color_menu.addCommand("Add New Preset", lambda: addNewColor())
    color_menu.addCommand("Manage Presets", lambda: manageColorPresets())


def addNewColor():
    """
    Add a new color preset to our presets.
    """
    #Open a color picker panel.
    color = nuke.getColor()
    if color:
        #Load our colors from the file
        colors = readColorPresets(PATH)
        valid_name = False
        while not valid_name:
            name = nuke.getInput("Give a name to your color", "color")
            if name:
                #Verify the name isn't taken. Reopen the panel if it's already taken
                if not name in colors.keys():
                    valid_name = True  # Get out of the loop
                    #add entry to the dictionary
                    colors[name] = color
                    success = writeColorPresets(PATH, colors)
                    if success:
                        createTileColorMenu()
                    else:
                        print "Error writing file %s" % PATH
                else:
                    nuke.message("The name already exists")
            elif name is None:
                # User Cancelled
                return


# Function to manage the presets:
def manageColorPresets():
    colors = readColorPresets(PATH)
    panel = ColorPresetPanel(colors)
    if panel.showModalDialog():
        new_colors = {}
        for preset in panel.knob_list:
            if not preset['delete'].value():
                new_colors[preset['name'].value()] = preset['value'].value()
        success = writeColorPresets(PATH, new_colors)
        if success:
            createTileColorMenu()
        else:
            print "Error writing file %s" % PATH


createTileColorMenu()

I did some very minor changes to the code, like adding a divider line to the Menu, and sorting the dictionary in the panel so the order of the colors is the same as the order in the menu.
I also moved the clearing of the menu to the function creating the menu rather than being in multiple functions, and added an elif in the addNewColor so it stops asking for a name if the user cancels.

Automatically Naming the Colors

We could argue that naming a color isn’t so painful to do Manually, but I’ve been interested for a while in algorithms to match colors. This would be a good exercise to practice using these algorithms.
It might be going a bit past a beginner level, but I shouldn’t be too hard to understand.

How to name the colors

My first intuition to name the colors would have been to get inspired by the way After Effects automatically names colors.
I don’t actually have a copy of After Effects so I’m not able to check exactly how it was behaving, but out of memory, it would name the basic color (Red, Green, Blue, Yellow, Purple, …) and add a Descriptive word if necessary (Dark, Pale, Bright, …).

HSV Cylinder – Source Wikipedia

Converting our color value to HSV, it would actually be relatively easy generate such a name. It’s quite boring to type all the if/else statements, but at the end you get a relatively accurate method, that is also extremely easy to understand for a beginner. It can also become more and more accurate by dividing your sections more. (You will see I got lazy on the blue colors and only categorized them as one tint)

def nameColor(hue, saturation, value):
    color_name = ''
    if value == 0:
        color_name = 'Black'
        return color_name
    if value == 1 and saturation == 0:
        color_name = 'White'
        return color_name
 
    if value <= 0.3: color_name += 'Dark ' elif value >= 0.9:
        color_name += 'Bright '
     
    if saturation == 0:
        color_name += 'Grey'
        return color_name
    elif saturation <= 0.15:
        color_name += 'Greyish '
    elif saturation <= 0.6: color_name += 'Pale ' elif saturation >= 0.9:
        color_name += 'Vivid '
 
    if hue <= 0.02 or hue >= 0.95:
        color_name += 'Red'
    elif hue <= 0.11:
        color_name += 'Orange'
    elif hue <= 0.17:
        color_name += 'Yellow'
    elif hue <= 0.29:
        color_name += 'Apple Green'
    elif hue <= 0.42:
        color_name += 'Green'
    elif hue <= 0.46:
        color_name += 'Cyan Green'
    elif hue <= 0.52:
        color_name += 'Cyan'
    elif hue <= 0.7:
        color_name += 'Blue'
    elif hue <= 0.79:
        color_name += 'Purple'
    elif hue <= 0.9:
        color_name += 'Pink'
    else:
        color_name += 'Magenta'
    return color_name
 
print nameColor(0.78, 0.42, 0.39)
print nameColor(0.35, 1, 0.95)

This is a totally valid approach, but in my case, I’d like to try something a little bit more complicated.
I would like my color to get matched to the closest known color taken from a list of colors.
Think about what happens when you export a GIF that has a limited amount of allowed colors. Each pixel gets recolored to the closest matching allowed color.
Python would be a bit slow to run such a script on each pixel of an image, but on a single color value why not?

It happens that Wikipedia has a pretty big list of colors with many different names here: List of colors (compact)
I also found a page by guy named Gauthier Lemoine who extracted the list from Wikipedia and made it into a JSON file for javascript, but the good news is, it’s compatible with Python as well.
He made an online tool to name a color that you can find by clicking here. His approach described in the page will be the base of what’s following.
With further reading (notably this link) I decided I’d do my matching in YUV colorspace instead of RGB or HSL. It is supposedly better matching human perception (yet far from perfect).

Doing the matching

Python has a colorsys module that can handle color conversions between different color systems like RGB, HSV and YIQ. It doesn’t have YUV, but according to wikipedia YIQ sounds pretty close to YUV, and a bit of googling around makes me believe it should work just as well, if not better than YUV for some tones, so I’ll go with that instead.

From now on, for the rest of this tutorial, I’ll be thinking of colors as a point in a 3D coordinate system. Better yet, we an consider each color to be a 3D vector.

0.1% of all RGB values represented as a point cloud.

0.1% of all RGB values represented as a point cloud.

The same color values, but in a YIQ space. Supposedly it will yield better results for the color matching.

The same color values, but in a YIQ space. Supposedly it will yield better results for the color matching.

To generate these point clouds, I also used python, with a code inspired by this: http://hagbarth.net/?p=663 (Check out this guy’s blog, he’s got some impressive tools in there).
It’s not needed for coding the tool, but I thought it would help you visualize.

Now remember we had a list of colors from Wikipedia/Gauthier earlier, containing RGB values. I’m going to run that through python and make it into a YIQ list. The RGB values also were based on a 0-255 range, but I need a float value, so I have to keep that in mind.

import colorsys, json
# Reading the list
file = open("C:\Users\Erwan\Desktop\colors.json")
data = json.load(file)
file.close()

yiqData = []
for color in data:
    rgb = (color['x']/255.0, color['y']/255.0, color['z']/255.0) #add .0 to make sure the math happens in float
    yiq = colorsys.rgb_to_yiq(rgb[0],rgb[1],rgb[2])
    newColor = {'x': yiq[0], 'y':yiq[1], 'z':yiq[2], 'label': color['label']}
    yiqData.append(newColor)

# Writing the new list
file = open(os.path.expanduser("~/.nuke/colorsYIQ.json"), "w")
json.dump(yiqData, file)
file.close()

One drawback is that the JSON file is much heavier in YIQ format than RGB 8bit because the format takes more characters to write (almost double).
ex:
{“x”:93,”y”:138,”z”:168,”label”:”Air Force blue”}
{“y”: -0.1435294117647059, “x”: 0.5011764705882353, “z”: -0.0005882352941176949, “label”: “Air Force blue”}
In our case the files are still quite small, so it won’t make much difference, but if we had a much bigger dataset, it would be something to consider.
Get the final YIQ JSON file

The samples for which we have a name, in RGB space.

The samples for which we have a name, in RGB space.

The same samples, in YIQ space.

The same samples, in YIQ space.

Now let’s try to match a single color to one of the labels.
I randomly picked a color 0.75, 0.4, 0.15 (rgb float) that I would probably have named “Pale Tangerine” if I was the one making the names.

My single color in a YIQ lonely space.

My single color in a YIQ lonely space.

Even though all this may sound a bit complex at first, now finding the closest value is pretty straight forward.
We need to measure the distance between this point and each of the known points, and return the closest one.

To measure the distance, it’s basic vector math. We need to subtract our two vectors, then calculate the magnitude of the resulting vector. https://www.mathsisfun.com/algebra/vectors.html
We can either do it manually:

from math import sqrt, pow
x1, y1, z1 = 15,12,45 #my first vector
x2, y2, z2 = 75,12,4 #my second vector

new_x = x1-x2
new_y = y1-y2
new_z = z1-z2

distance = sqrt(pow(new_x,2)+pow(new_y,2)+pow(new_z,2))
print distance
# Result:
72.6704891961

Or we can use the Nuke Math module, which is not very well documented, but has a good tutorial on nukepedia here: http://www.nukepedia.com/written-tutorials/using-the-nukemath-python-module-to-do-vector-and-matrix-operations/

vectorA = nuke.math.Vector3(15,12,45)
vectorB = nuke.math.Vector3(75,12,4)

distance = vectorA.distanceBetween(vectorB)
print distance
# Result:
72.6704864502

As you can see, both of these methods have a slightly different result, most likely a rounding error. It’s very little and won’t affect us. I’m going to use the nuke vector3 object for it’s ease of use.

import colorsys
import json


def nameColor(r, g, b):
    path_to_color_names = os.path.expanduser("~/.nuke/colorsYIQ.json")

    yiq = colorsys.rgb_to_yiq(r, g, b)
    x, y, z = yiq[0], yiq[1], yiq[2]
    vector_a = nuke.math.Vector3(x, y, z)
    name = ''
    smallest_distance = None
    try:
        with open(path_to_color_names) as color_names_file:
            color_names = json.load(color_names_file)
    except:
        return "Unknown Color"
    for color in color_names:
        vector_b = nuke.math.Vector3(color['x'], color['y'] ,color['z'])
        distance = vector_a.distanceBetween(vector_b)
        if smallest_distance is None or distance < smallest_distance:
            smallest_distance = distance
            name = color['label']
    return name


print nameColor(0.75,0.4,0.15)
# Result:
Ruddy brown

That code works, and despite the fact that it has to run once for each value, it’s still almost instantaneous to get the result.
Now, I’ll integrate it with the rest of the plugin:

Final code:

import os
import json
import colorsys
import nuke
import nukescripts

# Settings
PATH = os.path.expanduser("~/.nuke/nodeColorPresets.json")
PATH_TO_COLOR_NAMES = os.path.expanduser("~/.nuke/colorsYIQ.json")
TARGET_MENU = nuke.toolbar("Nodes")


class ColorPresetPanel(nukescripts.PythonPanel):
    """
    Custom Python Panel to manage color presets
    """
    def __init__(self, colors):
        super(ColorPresetPanel, self).__init__('Manage Color Presets')
        # Create a list that will contain all the knobs for easy access.
        self.knob_list = []
        for color in colors:
            label_knob = nuke.String_Knob("name_%s" % color, '', color)
            value_knob = nuke.ColorChip_Knob('value_%s' % color, '')
            delete_checkbox = nuke.Boolean_Knob("delete_%s" % color, 'delete')

            value_knob.setValue(colors[color])
            value_knob.clearFlag(nuke.STARTLINE)
            delete_checkbox.clearFlag(nuke.STARTLINE)

            knob_dict = {"name": label_knob,
                         "value": value_knob,
                         "delete": delete_checkbox}

            self.knob_list.append(knob_dict)

            # Add the knobs to the panel
            for knob in [label_knob, value_knob, delete_checkbox]:
                self.addKnob(knob)


def readColorPresets(path):
    """
    Read Color presets from Json file
    """
    if not os.path.isfile(path):
        print "No Color preset found "
        return {}
    else:
        try:
            f = open(path)
            colors = json.load(f)
            f.close()
            if isinstance(colors, dict):
                return colors
            else:
                print "The preset file doesn't contain a valid dictionary"
                return {}
        except:
            print "Error reading color preset file"
            return {}


def writeColorPresets(path, colors):
    """
    Write Color presets to Json file
    """
    try:
        f = open(path, "w")
        json.dump(colors, f)
        f.close()
        return True
    except:
        return False


#Function to change tile_color:
def setTileColor(value=None):
    """
    Changes the tile_color of multiple nodes at once
    """
    if value is None:
        value = nuke.getColor()
    for node in nuke.selectedNodes():
        node.knob('tile_color').setValue(value)


def createTileColorMenu():
    """
    Create the Menu entry for the Tile Color Presets
    """
    # Loading out color presets
    colors = readColorPresets(PATH)

    # And creating the menu.
    color_menu = TARGET_MENU.addMenu('Color Nodes', icon="color_node.png")

    # Clear the menu if already populated
    color_menu.clearMenu()

    # Populating the Menu
    for color in sorted(colors):
        color_menu.addCommand(color, lambda x=colors[color]: setTileColor(x))

    color_menu.addCommand("Custom Color", lambda: setTileColor())
    color_menu.addCommand("-", "", "")
    color_menu.addCommand("Add New Preset", lambda: addNewColor())
    color_menu.addCommand("Manage Presets", lambda: manageColorPresets())


def addNewColor():
    """
    Add a new color preset to our presets.
    """
    #Open a color picker panel.
    color = nuke.getColor()
    if color:
        #Load our colors from the file
        colors = readColorPresets(PATH)
        valid_name = False
        r, g, b = hexToRgb(color)
        default_name = nameColor(r, g, b)
        while not valid_name:
            name = nuke.getInput("Give a name to your color", default_name)
            if name:
                #Verify the name isn't taken. Reopen the panel if it's already taken
                if not name in colors.keys():
                    valid_name = True  # Get out of the loop
                    #add entry to the dictionary
                    colors[name] = color
                    success = writeColorPresets(PATH, colors)
                    if success:
                        createTileColorMenu()
                    else:
                        print "Error writing file %s" % PATH
                else:
                    nuke.message("The name already exists")
            elif name is None:
                # User Cancelled
                return


def nameColor(r, g, b):
    """
    Picks a name from the closest known color in a color name list
    """
    yiq = colorsys.rgb_to_yiq(r, g, b)
    x, y, z = yiq[0], yiq[1], yiq[2]
    vector_a = nuke.math.Vector3(x, y, z)
    name = ''
    smallest_distance = None
    try:
        with open(PATH_TO_COLOR_NAMES) as color_names_file:
            color_names = json.load(color_names_file)
    except:
        return "Unknown Color"
    for color in color_names:
        vector_b = nuke.math.Vector3(color['x'], color['y'] ,color['z'])
        distance = vector_a.distanceBetween(vector_b)
        if smallest_distance is None or distance < smallest_distance:
            smallest_distance = distance
            name = color['label']
    return name


def hexToRgb(nuke_hex):
    """
    Convert HEX color value to RGB
    """
    try:
        real_hex = '%08x' % nuke_hex
        r = int(real_hex[0:2], 16)/255.0
        g = int(real_hex[2:4], 16)/255.0
        b = int(real_hex[4:6], 16)/255.0
    except:
        return None, None, None
    return r, g, b


def manageColorPresets():
    """ Edit and Delete color presets """
    colors = readColorPresets(PATH)
    panel = ColorPresetPanel(colors)
    if panel.showModalDialog():
        new_colors = {}
        for preset in panel.knob_list:
            if not preset['delete'].value():
                new_colors[preset['name'].value()] = preset['value'].value()
        success = writeColorPresets(PATH, new_colors)
        if success:
            createTileColorMenu()
        else:
            print "Error writing file %s" % PATH


createTileColorMenu()

I had to do some slight modifications to the original functions, as well as adding a function to convert from the HEX value nuke gives us to RGB values.

Thanks for reading through.

By |2018-09-05T01:44:24+00:00May 11th, 2015|Nuke, Python, Tutorials|0 Comments

Leave A Comment