0
votes

I have small set of color data that I want to investigate. It is in the form of a list of RGB data.

[(255, 255, 255), (124, 144, 231), ...]

The image uses a restricted palette, and I would like to see how these colors are "distributed" by plotting them along the color wheel. As alternative, I tried histogram of individual channels, but this did not give me the insight I am interested in.

I googled and learned that HSL color more accurately maps to color wheel. However, after converting to HSL, I'm still confused. 3 pieces of data still make up the color: Hue, saturation, and luminosity. How do you map 3 piece of data onto a 2d plane?

I read about polar coordinates here: https://www.mathsisfun.com/polar-cartesian-coordinates.html. I try ignoring luminosity and plotting by treating HSL data as Polar coordinate (hue = angle, saturation = length of radius (scaled by some factor))

def polar2cartesian(hsl):
    color_circle_radius = 100
    radius = hsl.saturation * color_circle_radius
    x = radius * math.cos(math.radians(hsl.hue))
    y = radius * math.sin(math.radians(hsl.hue))
    return x, y


...

for hsl in colors: 
    x, y = polar2cartesian(hsl)
    im.point(x, y, hsl.to_rgb())

This is not correct result. As it shows same red color hue in multiple places like this example:

bad chart

What is the correct way to translate from RGB to a position on color wheel?

1
Have you looked at the colorsys module?martineau
How did you convert to HSL? Some routines return a hue value in the range 0-100, it seems you expect it to be 0-360, did you verify that this is the case?Cris Luengo
Also, what does im.point do with negative x, y values?Cris Luengo

1 Answers

8
votes

The problem of mapping a 3D (H, S, V) colour onto a 2D plane is a tough one to solve objectively, so I thought I'd give a crack at it and come up with results that I find pleasing.

My approach is as follows:

  1. For every (R, G, B) pixel in the image, convert it to (H, S, V).
  2. Convert the (H, S, V) colour to a 2D vector using the H value as the angle and the S value as the magnitude.
  3. Find the position of that vector in our 2D output image, and only write the pixel if the value (V) is greater than the value of what was previously in that pixel. (My reasoning is that since an image is likely to have multiple pixels of similar enough colours that they appear in the same place on our colour wheel, since we are not plotting using the value, we should give higher value pixels precedence to be shown.)

Now, in code: (Entire file)

Create a table to store the largest value in every particular position

highest_value = numpy.zeros((image_size, image_size))

Convert RGB to HSV

def rgb_to_point(rgb):
hsv = colorsys.rgb_to_hsv(*rgb)

Convert that to a vector

rads = math.tau * hsv[0] - math.pi
mag = hsv[1] * (image_size/2) - 1 

Convert that to a point on our image

x = int(math.cos(rads) * mag + (image_size/2))
y = int(math.sin(rads) * mag + (image_size/2))

If the value is higher, return the point, otherwise None

if(hsv[2] > highest_value[x][y]):
    highest_value[x][y] = hsv[2]
    return (x, y)

I called all that the rgb_to_point function, now we will use it for every pixel in our image:

for pixel in img.getdata():
    c = rgb_to_point(pixel)
    if(c):
        imgo.putpixel(c, pixel)

if(c) determines whether the value was higher, since c is None when it wasn't.

Here's the results:

mountainmountainwheelbirdbirdywheel

Note: Part of the reason I am dealing with value like this is because the alternatives I thought of were not as good. Ignoring value completely lead to darker pixels turning up on the output image, which usually lead to an ugly wheel. Turning the value up to 1 for every output pixel lead to very generic looking wheels that didn't really give a good idea of the original input image.