 # The incredibly challenging task of sorting colours

Let’s start with something trivial: sorting numbers. Regardless of the algorithm you’ll use, real numbers are naturally ordered. Mathematically speaking, they have a total order, in the sense that you can always decide if a number is greater than another one. There is no ambiguity in this, meaning you can actually sort them, and (excluding duplicates) this sort is unique. There are other fields which are not that lucky: colour, for instance, are very unlucky. Supposing you’re representing colours with their RGB values, there is no standard way to order triples in a line, since they are naturally not organised in a line fashion. The problem is even more complicated since colours have a meaning in the real world. How can we sort colours so that they look as continuous as possible? Which parameters affects the sorting order? Is azure closer to blue (similar hue) or to cyan (similar luminosity)? I can stop you all here and say that there is no solution to this problem. You can sort colours, but the overall result depends on what you are trying to achieve. This post will explore how colours can be sorted, and how this can lead to very different results.

### Part 1: Colour sorting

Let’s start by creating a bunch of random colours, sampling them from the RGB space. All the examples in this post will refer to this very list of random colours.

#### RGB sorting

The most trivial way in which we can sort colours is by directing sort its RGB values. Python has a very naive way of doing this: it sorts the first component, then the second and finally the third one. If two colours have the same quantity of red, the green channel will be used to determine which one is “bigger”. The result looks indeed very poor.

#### HSV sorting The RGB colour space works very well for monitors, but it does not represent how similar colours are. The HSV space attempts to overcome to this problem by introducing a parameter called hue. The hue is the base colour, made more intense or washed away by its saturation. The third component, simply called value, determined how “dark” the colour is. Since the hue has been arranged, by definition, in a rainbow fashion, sorting directly on the HSV values is likely to produce somehow visually pleasant results. Sorting using the HLS space, which organises colours in a similar fashion, produces seemingly indistinguishable results. Both these solutions, however, looks very noisy.

#### Luminosity sorting

The reason why sorting in HSV and HLS colour spaces produces noisy result is caused by a single factor. HSV believes that hue is more important than luminosity. Two visually different shades of blue are closer, compared two two different colours with the similar intensity. An attempt to compensate for this is by sorting directly for the perceived luminosity of a colour.

#### But this, unfortunately, still yields a very poor result.

#### Step sorting

If we want to sort colours in a visually pleasant way, we need to do something more complicated. We can, for instance, merge hue and luminosity information to obtain a smoother result. Another problem we encounter, however, is determined by how tuples are sorted in Python. To dampen the impact that sorting on the first component has, we can reduce the colour space from a float value between 0 to 1, to an integer from 0 to 7. By doing this, much of the noise is removed. Most of the noise is not removed, but the segments don’t look continuous any more. To fix this, we can invert the luminosity of every other segment. Colours looks now organised in a neater way. They are mostly continuous, with very little noise compared to the native HSV sorting. This, however, came at the expense of a monotonic luminosity. When it comes to colour sorting, we can’t have it all.

#### Hilbert sorting There is another way to sort colours which looks rather interesting. It is based on the concept of Hilbert curve. You can image a Hilbert curve as a way of mapping every point in a 2D space by using a 1D curve.

This is possible because the Hilbert curve is a fractal space-filling object. We can extend the same concept of space-filling to our three dimensional colour space. What if we use a Hilbert curve to connect all the colours in their RGB space? For this example I am using Steve Witham‘s implementation of a Hilbert walk, as suggested by Jan Pöschko. The result is indeed intriguing and, despite not following any intuitive colour distribution, it looks very homogeneous. It’s interesting to notice that while all the above mentioned technique would sort greyscale colours correctly, Hilbert sorting rearranges them in a very different way. #### Travelling Salesman sorting The travelling salesman problem refers to a very practical issues: visiting a certain number of cities minimising the overall distance and visiting each city only once. This sounds exactly what we want: visiting each colours only once, minimising the overall distance. Despite being such a critical issue, the travelling salesman problem is NP-complete, which is a fancy way of saying that is too computationally expensive to run over thousands of colours. The algorithm I’ll be using instead is a suboptimal version, called nearest neighbour. Without going too much into its details, it often finds solutions to the travelling salesman problem which are, on average, 25% worse. Which is not that bad, after all. I have used this version of the algorithm. Its result is very smooth, even though the colours are all over the place. This solution, however, should be the one that really minimises the distances between them.

We can also use other colour spaces to calculate distance, such as the HSV (top) and the Lab (bottom) ones, although they all yields similar results:  ### Part 2: Colour distance

Colour sorting is deeply connected to another problem. Given two different colours, how distant are they? The concept of distance strongly depends on the space we are analysing them. Just to give you an indication, here there are some charts which will help you understand how distance is perceived in several colour space.

In the following diagrams, every (x,y) pixel indicates how distant the respective colours in the X and Y axes are. The X and Y axes arrange colours according to their hue value. White pixels indicate a perfect match: the colours have zero distance, meaning they are are identical. Dark pixels, instead, indicate a high distance. You can click on the charts to expand them.

 HSV (& HSL) RGB YIQ LAB The next diagrams replace the rainbow colours on the horizontal axis with a greyscale gradient. There won’t be any white pixels since no two colours are the same now.

 HSV & HSL RGB YIQ LAB ### Conclusion

Sorting by colours is a very common practice, especially in advertising and other form of media which requires to be visually pleasant. In a previous post I explained how I collected all the screenshots from #ScreenshotSaturday and sorted them, showing some interesting results over the predominant hues found in indie games. These mosaics are done using over 30.000 images, weighting almost 9Gb. You can download the full-size moisacs (16Mb, 71Mb, 40Mb, 13Mb) here on Patreon. Sceen size mosaics are also available in the tweet below.

Sorting colours is a pain. There isn’t a magic function which will order them nicely, simply because the way we perceived them is based on three different components. Any attempt to flatten them onto one single dimension will inevitably collapse some of the complexity. When it comes to sort colours, you should understand which features you want to highlight. Is it the hue? Is it the luminosity? Start from there, and create your own function.

#### Other resources

A new tutorial is released every week.

##### 💖 Support this blog

This websites exists thanks to the contribution of patrons on Patreon. If you think these posts have either helped or inspired you, please consider supporting this blog.  ##### 📝 Licensing

You are free to use, adapt and build upon this tutorial for your own projects (even commercially) as long as you credit me.

You are not allowed to redistribute the content of this tutorial on other platforms. Especially the parts that are only available on Patreon.

If the knowledge you have gained had a significant impact on your project, a mention in the credit would be very appreciated. ❤️🧔🏻

1. Dan

Great post! Very interesting.
I have a question :
Are your sure you have to return “lum” and not “lum2” on Step algorithm ?

Otherwise I didn’t understand why you compute a “lum2” variable!

• I didn’t understand anything and also I didn’t read it so… I m just looking for an Samsung app of sorting colors wish me luck…?

2. Dharma Saputra

Hei, very nice article. Sorting colors is very hard actually. Currenty i’m on a project that need to sorting colors :D. I have a question, in lum formula:
lum = math.sqrt( .241 * r + .691 * g + .068 * b ),

where is .241, .691, .068 come from?

• Alan Zucconi

Hey! Those are magic numbers that refers to how the human eye perceive R, G and B components.

3. Visitor

Hi there, would you please share the part of the code that displays these colours as vertical lines? thx

• Alan Zucconi

Nice! <3

4. Valdas

Thank you, good article 🙂

5. j

what do you use to plot the colors in the strip

• Alan Zucconi

I wrote some custom code to initialise a numpy array which I then save as a PNG.

• j

can you share it pls

• Alan Zucconi

Sure!

colours_length = 1000;
colours = [] # (0,1)

# Generates the colours
for i in range(0, colours_length):
colours.append (
[
random.random(),
random.random(),
random.random()
]
)

def generatePics (c_sorted):
# Generates the picture
height = 25;
img = np.zeros((height,colours_length,3), np.uint8) # (0,255)

for x in range(0, colours_length-1):
c = [c_sorted[x] * 255, c_sorted[x] * 255, c_sorted[x] * 255]
img[:,x] = c

cv2.imwrite(“sort.png”, img)

generatePics(colours)

6. j

Thank you so much!

7. Daniel Walton

I did a lot of work recently to take pictures of a rubiks cube and extract the RGB values for each square. The next step was to examine the RGB values and figure out which of the six sides of the cube that color belonged to…your article was a big help with this, thank you for writing it. In order to correctly ID the colors I ended up converting to Lab and using cie2000 color distance.

In your article in the traveling salesman section you used Lab but it looks like you used the euclidean distance of the Lab values…did you try with cie2000? I tried that and got some decent results:
https://photos.smugmug.com/Misc/Rubiks-Cubes/Foo/i-gxZ6MKb/0/cb459fcf/X3/Screen%20Shot%202018-11-10%20at%202.02.31%20PM-X3.png

• Alan Zucconi

Thank you for sharing this!
It looks awesome, and I’m glad this helped! 🙂

8. Brett

Hey Alan,

Thanks for putting that together, it is exactly what we were looking for. There’s just one problem, I can’t code. How hard would it be to use this algorithm to sort colours in Corel .xml colour palette? Can someone help me?

Thanks

• Alan Zucconi

Hi Brett!

That really depends on what you want to do, exactly. All the code I used in this tutorial was written in Python, but there is no reason why you wouldn’t be able to re-make it in any other language.

Of course, you will need some basic programming skills.
If you are interested in playing with colours, it would be a very useful skill to have!

9. Aemie

Hello Alan !
The article is really helpful however I am stuck at the hilbert sort method .
I found a hilbert.py file which I imported however when I am trying to use the sort syntax , the same as yours , it’s showing me invalid syntax error .

• Alan Zucconi

Hi Aemie!

If you are using a different library, you will need to look at its documentation to know exactly how to use it. Unless it’s the exact file I have used, is unlikely to work in the exact same way.

Also, it is likely that a syntax error might be unrelated to that. Perhaps there is a typo of some kind that is also stopping your script from working!

• Aemie

Can you share the link of the exact file you used for hilbert as I am not able to open the Hilbert Walk from your article .

10. Aemie

Thank you , however when I am trying to do :
colors.sort(key=lambda(r,g,b) : step(r,g,b,8)) , it;s also giving a syntax error .
I couldn’t find any method on using lambda with nested list and hence , I am not able to comprehend .

• Aemie

I found out a method to do so , thankyou for your assistance .

• Aemie

I found out a method to do so , thankyou for your assistance .

11. Alex Hall

The most advanced color model to date, CIECAM02, can sort colors better than any other model. I wrote a python script which uses that model (and I library built on it, which someone else coded) to sort colors, and I was happy with the results. Example output and links to the script and library are over at a recent post at my blog, which this comment system seems bent on not letting me link directly to.

12. victoria janniffer

Using herbal natural remedy was what got me tested negative to HSV after been diagnosed for years, i got to know about it when i came across this website and i used the contact field which i got a message from his whatsApp channel +2349058026857 or email address (usmandrhazim@gmail.com) and let him know my pains. i am tested negative because i follow instruction on how to consume his herbal remedy and today i am celebrating one years of been free of HSV ,

13. Julie Rymer

Interesting article, in case anyone is interested, I wrote a script implementing the step sorting using numpy, complete with color array and final image generation:

import itertools
import warnings

import numpy as np
import cv2
from skimage.color import deltaE_ciede2000, rgb2lab, lab2rgb, rgb2hsv
from skimage import img_as_ubyte, img_as_float32
from PIL import Image

h, w = (20, 5)

# # produce all possible colors
all_colors = [color for color in itertools.product(range(256), repeat=3)]
# # equivalent but ~8% slower on my computer
# all_colors = [(r, g, b) for r in range(256) for g in range(256) for b in range(256)]

all_colors = img_as_float32(np.expand_dims(np.array(all_colors, dtype=np.uint8), axis=0))
# # equivalent but ~5% slower on my computer
# all_colors = np.expand_dims(np.array(all_colors, dtype=np.float32) / 255, axis=0)

all_colors = cv2.cvtColor(all_colors, cv2.COLOR_RGB2Lab)
# # equivalent but ~2400% slower on my computer
# all_colors = rgb2lab(all_colors)

# select only sufficiently dissimilar colors
final_colors = []
while all_colors.size > 0:
color = all_colors[np.random.choice(all_colors.shape)]
final_colors.append(color)
similarity = deltaE_ciede2000(color, all_colors)
all_colors = all_colors[similarity > 10]

# # deltaE_ciede2000 distance:
# # <= 1.0 Not perceptible by human eyes.
# # 1 – 2 Perceptible through close observation.
# # 2 – 10 Perceptible at a glance.
# # 11 – 49 Colors are more similar than opposite
# # 100 Colors are exact opposite

# # If you want all color instead, but beware of memory errors
# final_colors = all_colors

def transform_to_sorting_color_space(hsv_arr, hue_nb):
arr = hsv_arr.copy()
sat = arr[:, :, 1]
grey_threshold = 0.15

arr[:, :, 0] = (arr[:, :, 0] * (hue_nb – 2)).round() + 1

# # personal remix, to regroup color that are really a shade of grey to the left
indexes_greys = (sat < grey_threshold)
arr[:, :, 0][indexes_greys] = 0

# invert the luminosity of every other segment
indexes_odd_hue = (arr[:, :, 0] % 2 == 1)
arr[:, :, 1:][indexes_odd_hue] = 1 – arr[:, :, 1:][indexes_odd_hue]

arr[:, :, 1:] = arr[:, :, [2, 1]]

return arr

final_colors = np.expand_dims(np.array(final_colors, dtype=np.float32), axis=0)
final_colors = lab2rgb(final_colors)
hsv_colors = rgb2hsv(final_colors)

# Step sorting
sorting_colors = transform_to_sorting_color_space(hsv_colors, 12)
sort_indices = np.lexsort(sorting_colors[:, ::-1].T)
final_colors = final_colors[0, sort_indices]

# # If you prefer Hue sort
# final_colors = final_colors[np.lexsort(hsv_colors[:, ::-1].T)]

with warnings.catch_warnings():
warnings.simplefilter("ignore")
final_colors = img_as_ubyte(final_colors)

print(f'nb_colors : {final_colors.shape}')

# produce image
color_img = Image.new('RGB', (w * final_colors.shape, h))
final_colors = sum([[tuple(color)] * w for color in final_colors] * h, [])
color_img.putdata(final_colors)
color_img.save('./all_colors.png')

### Webmentions

• The incredibly challenging task of sorting colours - Marcus Jaschen January 24, 2020

Interesting article, in case anyone is interested, I wrote a script implementing the step sorting using numpy, complete with color array and final image generation:

import itertools
import warnings

import numpy as np
import cv2
from skimage.color import deltaE_ciede2000, rgb2lab, lab2rgb, rgb2hsv
from skimage import img_as_ubyte, img_as_float32
from PIL import Image

h, w = (20, 5)

# # produce all possible colors
all_colors = [color for color in itertools.product(range(256), repeat=3)]
# # equivalent but ~8% slower on my computer
# all_colors = [(r, g, b) for r in range(256) for g in range(256) for b in range(256)]

all_colors = img_as_float32(np.expand_dims(np.array(all_colors, dtype=np.uint8), axis=0))
# # equivalent but ~5% slower on my computer
# all_colors = np.expand_dims(np.array(all_colors, dtype=np.float32) / 255, axis=0)

all_colors = cv2.cvtColor(all_colors, cv2.COLOR_RGB2Lab)
# # equivalent but ~2400% slower on my computer
# all_colors = rgb2lab(all_colors)

# select only sufficiently dissimilar colors
final_colors = []
while all_colors.size > 0:
color = all_colors[np.random.choice(all_colors.shape)]
final_colors.append(color)
similarity = deltaE_ciede2000(color, all_colors)
all_colors = all_colors[similarity > 10]

# # deltaE_ciede2000 distance:
# # <= 1.0 Not perceptible by human eyes.
# # 1 – 2 Perceptible through close observation.
# # 2 – 10 Perceptible at a glance.
# # 11 – 49 Colors are more similar than opposite
# # 100 Colors are exact opposite

# # If you want all color instead, but beware of memory errors
# final_colors = all_colors

def transform_to_sorting_color_space(hsv_arr, hue_nb):
arr = hsv_arr.copy()
sat = arr[:, :, 1]
grey_threshold = 0.15

arr[:, :, 0] = (arr[:, :, 0] * (hue_nb – 2)).round() + 1

# # personal remix, to regroup color that are really a shade of grey to the left
indexes_greys = (sat < grey_threshold)
arr[:, :, 0][indexes_greys] = 0

# invert the luminosity of every other segment
indexes_odd_hue = (arr[:, :, 0] % 2 == 1)
arr[:, :, 1:][indexes_odd_hue] = 1 – arr[:, :, 1:][indexes_odd_hue]

arr[:, :, 1:] = arr[:, :, [2, 1]]

return arr

final_colors = np.expand_dims(np.array(final_colors, dtype=np.float32), axis=0)
final_colors = lab2rgb(final_colors)
hsv_colors = rgb2hsv(final_colors)

# Step sorting
sorting_colors = transform_to_sorting_color_space(hsv_colors, 12)
sort_indices = np.lexsort(sorting_colors[:, ::-1].T)
final_colors = final_colors[0, sort_indices]

# # If you prefer Hue sort
# final_colors = final_colors[np.lexsort(hsv_colors[:, ::-1].T)]

with warnings.catch_warnings():
warnings.simplefilter("ignore")
final_colors = img_as_ubyte(final_colors)

print(f'nb_colors : {final_colors.shape}')

# produce image
color_img = Image.new('RGB', (w * final_colors.shape, h))
final_colors = sum([[tuple(color)] * w for color in final_colors] * h, [])
color_img.putdata(final_colors)
color_img.save('./all_colors.png')

• Game Barcode: A Study of Colours in Games - Alan Zucconi January 24, 2020

Interesting article, in case anyone is interested, I wrote a script implementing the step sorting using numpy, complete with color array and final image generation:

import itertools
import warnings

import numpy as np
import cv2
from skimage.color import deltaE_ciede2000, rgb2lab, lab2rgb, rgb2hsv
from skimage import img_as_ubyte, img_as_float32
from PIL import Image

h, w = (20, 5)

# # produce all possible colors
all_colors = [color for color in itertools.product(range(256), repeat=3)]
# # equivalent but ~8% slower on my computer
# all_colors = [(r, g, b) for r in range(256) for g in range(256) for b in range(256)]

all_colors = img_as_float32(np.expand_dims(np.array(all_colors, dtype=np.uint8), axis=0))
# # equivalent but ~5% slower on my computer
# all_colors = np.expand_dims(np.array(all_colors, dtype=np.float32) / 255, axis=0)

all_colors = cv2.cvtColor(all_colors, cv2.COLOR_RGB2Lab)
# # equivalent but ~2400% slower on my computer
# all_colors = rgb2lab(all_colors)

# select only sufficiently dissimilar colors
final_colors = []
while all_colors.size > 0:
color = all_colors[np.random.choice(all_colors.shape)]
final_colors.append(color)
similarity = deltaE_ciede2000(color, all_colors)
all_colors = all_colors[similarity > 10]

# # deltaE_ciede2000 distance:
# # <= 1.0 Not perceptible by human eyes.
# # 1 – 2 Perceptible through close observation.
# # 2 – 10 Perceptible at a glance.
# # 11 – 49 Colors are more similar than opposite
# # 100 Colors are exact opposite

# # If you want all color instead, but beware of memory errors
# final_colors = all_colors

def transform_to_sorting_color_space(hsv_arr, hue_nb):
arr = hsv_arr.copy()
sat = arr[:, :, 1]
grey_threshold = 0.15

arr[:, :, 0] = (arr[:, :, 0] * (hue_nb – 2)).round() + 1

# # personal remix, to regroup color that are really a shade of grey to the left
indexes_greys = (sat < grey_threshold)
arr[:, :, 0][indexes_greys] = 0

# invert the luminosity of every other segment
indexes_odd_hue = (arr[:, :, 0] % 2 == 1)
arr[:, :, 1:][indexes_odd_hue] = 1 – arr[:, :, 1:][indexes_odd_hue]

arr[:, :, 1:] = arr[:, :, [2, 1]]

return arr

final_colors = np.expand_dims(np.array(final_colors, dtype=np.float32), axis=0)
final_colors = lab2rgb(final_colors)
hsv_colors = rgb2hsv(final_colors)

# Step sorting
sorting_colors = transform_to_sorting_color_space(hsv_colors, 12)
sort_indices = np.lexsort(sorting_colors[:, ::-1].T)
final_colors = final_colors[0, sort_indices]

# # If you prefer Hue sort
# final_colors = final_colors[np.lexsort(hsv_colors[:, ::-1].T)]

with warnings.catch_warnings():
warnings.simplefilter("ignore")
final_colors = img_as_ubyte(final_colors)

print(f'nb_colors : {final_colors.shape}')

# produce image
color_img = Image.new('RGB', (w * final_colors.shape, h))
final_colors = sum([[tuple(color)] * w for color in final_colors] * h, [])
color_img.putdata(final_colors)
color_img.save('./all_colors.png')

• Tech News / The challenging task of sorting colours January 24, 2020

Interesting article, in case anyone is interested, I wrote a script implementing the step sorting using numpy, complete with color array and final image generation:

import itertools
import warnings

import numpy as np
import cv2
from skimage.color import deltaE_ciede2000, rgb2lab, lab2rgb, rgb2hsv
from skimage import img_as_ubyte, img_as_float32
from PIL import Image

h, w = (20, 5)

# # produce all possible colors
all_colors = [color for color in itertools.product(range(256), repeat=3)]
# # equivalent but ~8% slower on my computer
# all_colors = [(r, g, b) for r in range(256) for g in range(256) for b in range(256)]

all_colors = img_as_float32(np.expand_dims(np.array(all_colors, dtype=np.uint8), axis=0))
# # equivalent but ~5% slower on my computer
# all_colors = np.expand_dims(np.array(all_colors, dtype=np.float32) / 255, axis=0)

all_colors = cv2.cvtColor(all_colors, cv2.COLOR_RGB2Lab)
# # equivalent but ~2400% slower on my computer
# all_colors = rgb2lab(all_colors)

# select only sufficiently dissimilar colors
final_colors = []
while all_colors.size > 0:
color = all_colors[np.random.choice(all_colors.shape)]
final_colors.append(color)
similarity = deltaE_ciede2000(color, all_colors)
all_colors = all_colors[similarity > 10]

# # deltaE_ciede2000 distance:
# # <= 1.0 Not perceptible by human eyes.
# # 1 – 2 Perceptible through close observation.
# # 2 – 10 Perceptible at a glance.
# # 11 – 49 Colors are more similar than opposite
# # 100 Colors are exact opposite

# # If you want all color instead, but beware of memory errors
# final_colors = all_colors

def transform_to_sorting_color_space(hsv_arr, hue_nb):
arr = hsv_arr.copy()
sat = arr[:, :, 1]
grey_threshold = 0.15

arr[:, :, 0] = (arr[:, :, 0] * (hue_nb – 2)).round() + 1

# # personal remix, to regroup color that are really a shade of grey to the left
indexes_greys = (sat < grey_threshold)
arr[:, :, 0][indexes_greys] = 0

# invert the luminosity of every other segment
indexes_odd_hue = (arr[:, :, 0] % 2 == 1)
arr[:, :, 1:][indexes_odd_hue] = 1 – arr[:, :, 1:][indexes_odd_hue]

arr[:, :, 1:] = arr[:, :, [2, 1]]

return arr

final_colors = np.expand_dims(np.array(final_colors, dtype=np.float32), axis=0)
final_colors = lab2rgb(final_colors)
hsv_colors = rgb2hsv(final_colors)

# Step sorting
sorting_colors = transform_to_sorting_color_space(hsv_colors, 12)
sort_indices = np.lexsort(sorting_colors[:, ::-1].T)
final_colors = final_colors[0, sort_indices]

# # If you prefer Hue sort
# final_colors = final_colors[np.lexsort(hsv_colors[:, ::-1].T)]

with warnings.catch_warnings():
warnings.simplefilter("ignore")
final_colors = img_as_ubyte(final_colors)

print(f'nb_colors : {final_colors.shape}')

# produce image
color_img = Image.new('RGB', (w * final_colors.shape, h))
final_colors = sum([[tuple(color)] * w for color in final_colors] * h, [])
color_img.putdata(final_colors)
color_img.save('./all_colors.png')

• How to find the main colours in an image - Alan Zucconi January 24, 2020

Interesting article, in case anyone is interested, I wrote a script implementing the step sorting using numpy, complete with color array and final image generation:

import itertools
import warnings

import numpy as np
import cv2
from skimage.color import deltaE_ciede2000, rgb2lab, lab2rgb, rgb2hsv
from skimage import img_as_ubyte, img_as_float32
from PIL import Image

h, w = (20, 5)

# # produce all possible colors
all_colors = [color for color in itertools.product(range(256), repeat=3)]
# # equivalent but ~8% slower on my computer
# all_colors = [(r, g, b) for r in range(256) for g in range(256) for b in range(256)]

all_colors = img_as_float32(np.expand_dims(np.array(all_colors, dtype=np.uint8), axis=0))
# # equivalent but ~5% slower on my computer
# all_colors = np.expand_dims(np.array(all_colors, dtype=np.float32) / 255, axis=0)

all_colors = cv2.cvtColor(all_colors, cv2.COLOR_RGB2Lab)
# # equivalent but ~2400% slower on my computer
# all_colors = rgb2lab(all_colors)

# select only sufficiently dissimilar colors
final_colors = []
while all_colors.size > 0:
color = all_colors[np.random.choice(all_colors.shape)]
final_colors.append(color)
similarity = deltaE_ciede2000(color, all_colors)
all_colors = all_colors[similarity > 10]

# # deltaE_ciede2000 distance:
# # <= 1.0 Not perceptible by human eyes.
# # 1 – 2 Perceptible through close observation.
# # 2 – 10 Perceptible at a glance.
# # 11 – 49 Colors are more similar than opposite
# # 100 Colors are exact opposite

# # If you want all color instead, but beware of memory errors
# final_colors = all_colors

def transform_to_sorting_color_space(hsv_arr, hue_nb):
arr = hsv_arr.copy()
sat = arr[:, :, 1]
grey_threshold = 0.15

arr[:, :, 0] = (arr[:, :, 0] * (hue_nb – 2)).round() + 1

# # personal remix, to regroup color that are really a shade of grey to the left
indexes_greys = (sat < grey_threshold)
arr[:, :, 0][indexes_greys] = 0

# invert the luminosity of every other segment
indexes_odd_hue = (arr[:, :, 0] % 2 == 1)
arr[:, :, 1:][indexes_odd_hue] = 1 – arr[:, :, 1:][indexes_odd_hue]

arr[:, :, 1:] = arr[:, :, [2, 1]]

return arr

final_colors = np.expand_dims(np.array(final_colors, dtype=np.float32), axis=0)
final_colors = lab2rgb(final_colors)
hsv_colors = rgb2hsv(final_colors)

# Step sorting
sorting_colors = transform_to_sorting_color_space(hsv_colors, 12)
sort_indices = np.lexsort(sorting_colors[:, ::-1].T)
final_colors = final_colors[0, sort_indices]

# # If you prefer Hue sort
# final_colors = final_colors[np.lexsort(hsv_colors[:, ::-1].T)]

with warnings.catch_warnings():
warnings.simplefilter("ignore")
final_colors = img_as_ubyte(final_colors)

print(f'nb_colors : {final_colors.shape}')

# produce image
color_img = Image.new('RGB', (w * final_colors.shape, h))
final_colors = sum([[tuple(color)] * w for color in final_colors] * h, [])
color_img.putdata(final_colors)
color_img.save('./all_colors.png')

• How to retrieve all the images from a website - Alan Zucconi January 24, 2020

Interesting article, in case anyone is interested, I wrote a script implementing the step sorting using numpy, complete with color array and final image generation:

import itertools
import warnings

import numpy as np
import cv2
from skimage.color import deltaE_ciede2000, rgb2lab, lab2rgb, rgb2hsv
from skimage import img_as_ubyte, img_as_float32
from PIL import Image

h, w = (20, 5)

# # produce all possible colors
all_colors = [color for color in itertools.product(range(256), repeat=3)]
# # equivalent but ~8% slower on my computer
# all_colors = [(r, g, b) for r in range(256) for g in range(256) for b in range(256)]

all_colors = img_as_float32(np.expand_dims(np.array(all_colors, dtype=np.uint8), axis=0))
# # equivalent but ~5% slower on my computer
# all_colors = np.expand_dims(np.array(all_colors, dtype=np.float32) / 255, axis=0)

all_colors = cv2.cvtColor(all_colors, cv2.COLOR_RGB2Lab)
# # equivalent but ~2400% slower on my computer
# all_colors = rgb2lab(all_colors)

# select only sufficiently dissimilar colors
final_colors = []
while all_colors.size > 0:
color = all_colors[np.random.choice(all_colors.shape)]
final_colors.append(color)
similarity = deltaE_ciede2000(color, all_colors)
all_colors = all_colors[similarity > 10]

# # deltaE_ciede2000 distance:
# # <= 1.0 Not perceptible by human eyes.
# # 1 – 2 Perceptible through close observation.
# # 2 – 10 Perceptible at a glance.
# # 11 – 49 Colors are more similar than opposite
# # 100 Colors are exact opposite

# # If you want all color instead, but beware of memory errors
# final_colors = all_colors

def transform_to_sorting_color_space(hsv_arr, hue_nb):
arr = hsv_arr.copy()
sat = arr[:, :, 1]
grey_threshold = 0.15

arr[:, :, 0] = (arr[:, :, 0] * (hue_nb – 2)).round() + 1

# # personal remix, to regroup color that are really a shade of grey to the left
indexes_greys = (sat < grey_threshold)
arr[:, :, 0][indexes_greys] = 0

# invert the luminosity of every other segment
indexes_odd_hue = (arr[:, :, 0] % 2 == 1)
arr[:, :, 1:][indexes_odd_hue] = 1 – arr[:, :, 1:][indexes_odd_hue]

arr[:, :, 1:] = arr[:, :, [2, 1]]

return arr

final_colors = np.expand_dims(np.array(final_colors, dtype=np.float32), axis=0)
final_colors = lab2rgb(final_colors)
hsv_colors = rgb2hsv(final_colors)

# Step sorting
sorting_colors = transform_to_sorting_color_space(hsv_colors, 12)
sort_indices = np.lexsort(sorting_colors[:, ::-1].T)
final_colors = final_colors[0, sort_indices]

# # If you prefer Hue sort
# final_colors = final_colors[np.lexsort(hsv_colors[:, ::-1].T)]

with warnings.catch_warnings():
warnings.simplefilter("ignore")
final_colors = img_as_ubyte(final_colors)

print(f'nb_colors : {final_colors.shape}')

# produce image
color_img = Image.new('RGB', (w * final_colors.shape, h))
final_colors = sum([[tuple(color)] * w for color in final_colors] * h, [])
color_img.putdata(final_colors)
color_img.save('./all_colors.png')