Skip to content

How to reduce the color count in a SVG file ?

Midjourney, AI, Python

Today I wanted to add an illustration for Advanced-Stack.com. I am amazed by the results of Midjourney and I said to myself : let's give it a try.

I generated these results using a basic prompt.

But when I wanted to integrate images in the home page, I realized that Midjourney is not capable of generating transparent background.

I quickly realized that I would need to vectorize and then remove the background - which can be a complex task when the image has a lot of details.

I end up using Figma working on the SVG but I had hundreds of unique colors for the dark background !

Introduction

In this tutorial, we will learn how to reduce the color count in an SVG file using Python. We will use the scikit-learn library's KMeans algorithm to cluster the colors in the SVG file and replace them with a reduced set of colors. We will use the svgpathtools library to parse the SVG file and modify its attributes.

Step 1: Import the Required Libraries

Start by importing the necessary libraries: sklearn.cluster.KMeans for color clustering and svgpathtools.svg2paths and svgpathtools.wsvg for parsing and modifying the SVG file.

python
from sklearn.cluster import KMeans
from svgpathtools import svg2paths, wsvg

Step 2: Define the Helper Functions

We define 2 helper functions extract_colors and replace_colors.

python
def extract_colors(paths, attrs):
    colors = []
    for attr in attrs:
        if 'fill' in attr:
            color = attr['fill']
            if color.startswith('#'):
                r, g, b = (
                    int(color[1:3], 16),
                    int(color[3:5], 16),
                    int(color[5:7], 16),
                )
                colors.append([r, g, b])
    return colors


def replace_colors(paths, attrs, kmeans):
    new_attrs = []
    for attr in attrs:
        new_attr = attr.copy()
        if 'fill' in attr:
            color = attr['fill']
            if color.startswith('#'):
                r, g, b = (
                    int(color[1:3], 16),
                    int(color[3:5], 16),
                    int(color[5:7], 16),
                )
                new_color = kmeans.predict([[r, g, b]])
                new_color_hex = '#{:02x}{:02x}{:02x}'.format(
                    int(kmeans.cluster_centers_[new_color][0][0]),
                    int(kmeans.cluster_centers_[new_color][0][1]),
                    int(kmeans.cluster_centers_[new_color][0][2]),
                )
                new_attr['fill'] = new_color_hex
        new_attrs.append(new_attr)
    return new_attrs

Step 3: Main Function

The main function reduce_colors that takes the input SVG file path, output SVG file path, and the desired number of colors as input.

python
def reduce_colors(input_svg, output_svg, n_colors):
    paths, attrs = svg2paths(input_svg)
    colors = extract_colors(paths, attrs)
    kmeans = KMeans(n_clusters=n_colors)
    kmeans.fit(colors)
    new_attrs = replace_colors(paths, attrs, kmeans)
    wsvg(paths, attributes=new_attrs, filename=output_svg)

Inside the reduce_colors function, we first parse the input SVG file using svg2paths to get the paths and attributes. We then extract the colors from the attributes using the extract_colors function. Next, we initialize a KMeans model with the desired number of colors and fit it to the extracted colors. We use the replace_colors function to replace the colors in the SVG file with the colors predicted by the KMeans model. Finally, we write the modified SVG file using wsvg.

Step 4: Specify the Input and Output Files

Specify the paths of the input SVG file and the desired output SVG file, as well as the desired number of colors.

python
input_svg = 'input.svg'
output_svg = 'output.svg'
n_colors = 8

Step 5: Call the Main Function

Call the reduce_colors function with the specified input SVG file, output SVG file, and number of colors.

python
reduce_colors(input_svg, output_svg, n_colors)

Complete code

python
from sklearn.cluster import KMeans
from svgpathtools import svg2paths, wsvg


def extract_colors(paths, attrs):
    colors = []
    for attr in attrs:
        if 'fill' in attr:
            color = attr['fill']
            if color.startswith('#'):
                r, g, b = (
                    int(color[1:3], 16),
                    int(color[3:5], 16),
                    int(color[5:7], 16),
                )
                colors.append([r, g, b])
    return colors


def replace_colors(paths, attrs, kmeans):
    new_attrs = []
    for attr in attrs:
        new_attr = attr.copy()
        if 'fill' in attr:
            color = attr['fill']
            if color.startswith('#'):
                r, g, b = (
                    int(color[1:3], 16),
                    int(color[3:5], 16),
                    int(color[5:7], 16),
                )
                new_color = kmeans.predict([[r, g, b]])
                new_color_hex = '#{:02x}{:02x}{:02x}'.format(
                    int(kmeans.cluster_centers_[new_color][0][0]),
                    int(kmeans.cluster_centers_[new_color][0][1]),
                    int(kmeans.cluster_centers_[new_color][0][2]),
                )
                new_attr['fill'] = new_color_hex
        new_attrs.append(new_attr)
    return new_attrs


def reduce_colors(input_svg, output_svg, n_colors):
    paths, attrs = svg2paths(input_svg)
    colors = extract_colors(paths, attrs)
    kmeans = KMeans(n_clusters=n_colors)
    kmeans.fit(colors)
    new_attrs = replace_colors(paths, attrs, kmeans)
    wsvg(paths, attributes=new_attrs, filename=output_svg)


input_svg = 'input.svg'
output_svg = 'ouput.svg'
n_colors = 8

reduce_colors(input_svg, output_svg, n_colors)

Discover more content ?

Do you want to learn more and faster with dense and tailored technical resources ?

Advanced Stack