Color Palette Extraction

 

Intro

Browsing around online, I came across a movie still which had the dominant colors lined up at the bottom of the frame and started to think of a way to automate it using clustering techniques. After some research and brainstorming, I figured it wouldn’t be difficult to do using scikit-learn with Pillow, and came up with a couple of ideas to use it to generate fancy desktop backgrounds.

CodeDev

Loading image

The first thing was to load images to Python. I decided to use opencv-python due to its extensive documentation and support. One important thing was to notice that, by default, the image was not imported in RGB format, so it has to be converted (which can be done easily with opencv itself).

# File: aux.py
bgr = cv2.imread(imgPath)
img = cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB)

Reshape & clustering

After this, we do a slight reshaping to get the image in an RGB array of integers (8 bit). Now, if we think about the RGB points in 3D space, each point exists in the intersection of three (R,G,B) coordinates, so we can use unsupervised clustering techniques to get the dominant colors by obtaining the centroids of such agglomerations of colors. Although there are many clustering techniques that would get the job done in this situation, I chose to use K-Means because of its simplicity and the fact that we can provide it with the number of clusters we want as the output.

# File: aux.py
def calcDominantColors(img, cltsNumb=10, maxIter=1000):
    frame = img.reshape((img.shape[0] * img.shape[1], 3))
    # Cluster the colors for dominance detection
    kmeans = KMeans(n_clusters=cltsNumb, max_iter=maxIter).fit(frame)
    (palette, labels) = (kmeans.cluster_centers_, kmeans.labels_)
    # Rescale the colors for matplotlib
    colors = [reshapeColor(color) for color in palette]
    return (colors, labels)

Dominant colors

With this function in place, we are now able to calculate the dominant colors of the image, and then transform the resulting matplotlib colors into HEX and 8 bit RBG palettes:

# File: aux.py
(colors, labels) = calcDominantColors(
        img, cltsNumb=clstNum, maxIter=maxIters
    )
(hexColors, rgbColors) = calcHexAndRGBFromPalette(colors)

Assemble image

Finally, we assemble together the frame with the color palette on the top and bottom for aesthetic purposes:

# File: aux.py
# Create color swatch
colorsBars = genColorSwatch(img, colorBarHeight, colors)
# Put the image back together
whiteBar = genColorBar(
        width, round(height * bufferHeight), color=colorBuffer
    )
newImg = np.row_stack((
        whiteBar, colorsBars,
        img,
        colorsBars, whiteBar
    ))
palette = calcHexAndRGBFromPalette(colors)
swatch = Image.fromarray(colorsBars.astype('uint8'), 'RGB')
imgOut = Image.fromarray(newImg.astype('uint8'), 'RGB')
return (imgOut, swatch, palette)

The fully-documented code is available in the Github repo with clearly-defined functions and notes, with ruotines for single images, and batch export of frames.

Gallery

One final thing I did with this code, was to take the batch routine and use it to get the color palette of the frames of movies, and then to re-assemble them into grids for their use as fancy wallpapers.

Code repo