Home > Back-end >  HSI to RGB color conversion
HSI to RGB color conversion

Time:09-27

I'm trying to implement HSI <=> RGB color conversion
There are formulas on the wiki https://en.wikipedia.org/wiki/HSL_and_HSV#HSI_to_RGB

RGB to HSI seems to work fine.
However, I have difficulties with HSI to RGB.

I will write in Ruby, the examples will be in Ruby, however if you write in JS/Python/etc I think it will be understandable too, since it's just math.
Online ruby Interpreter.

def hsi_to_rgb(hsi_arr)
  # to float
  hue, saturation, intensity = hsi_arr.map(&:to_f)

  hue /= 60
  z = 1 - (hue % 2 - 1).abs
  chroma = (3 * intensity * saturation) / (1   z)
  x = chroma * z
  point = case hue
          when 0..1 then [chroma, x, 0]
          when 1..2 then [x, chroma, 0]
          when 2..3 then [0, chroma, x]
          when 3..4 then [0, x, chroma]
          when 4..5 then [x, 0, chroma]
          when 5..6 then [chroma, 0, x]
          else [0, 0, 0]
          end

  # calculation rgb & scaling into range 0..255
  m = intensity * (1 - saturation)
  point.map { |channel| ((channel   m) * 255).round }
end

So, with simple html colors, everything seemed to work. Until I tried values like this:

p hsi_to_rgb([0,   1,   1])    # => [765, 0, 0]
p hsi_to_rgb([360, 1,   1])    # => [765, 0, 0]
p hsi_to_rgb([357, 1,   1])    # => [729, 0, 36]
p hsi_to_rgb([357, 1, 0.5])    # => [364, 0, 18]

The values obtained are clearly incorrect, outside the range 0..255.

I have also seen implementations using trigonometric functions:
https://hypjudy.github.io/images/dip/hsi2rgb.jpg
However, I didn't get the right results either.

The only online RGB to HSI converter I found: https://www.picturetopeople.org/color_converter.html
Just to have something to compare it to.

CodePudding user response:

Your implementation looks correct (assuming Wikipedia is correct).

The only missing part is clipping the RGB output to [0, 255].


In most color space conversion formulas there are values that are in the valid range of the source color space, but falls out of the valid range of the destination color space.
The common solution is clipping the result to the valid range.

In some cases there are undefined values.
Take a look at the first 3 rows of the examples table.
The Hue is marked N/A for white, black and gray colors.

All of the sample HSI values the you choose:
[0, 1, 1]
[360, 1, 1]
[357, 1, 1]
[357, 1, 0.5]
Falls out of the valid range of the RGB color space (after HSI to RGB conversion).


I suggest you to test the valid tuples from the examples table:

   H       S       I                R       G       B
  ---    ----    ----             ----    ----    ----
    0    100%   33.3%             100%      0%      0%
   60    100%     50%              75%     75%      0%
  120    100%   16.7%               0%     50%      0%
  180     40%   83.3%              50%    100%    100%
  240     25%   66.7%              50%     50%    100%
  300   57.1%   58.3%              75%     25%     75%
 61.8   69.9%   47.1%            62.8%   64.3%   14.2%
251.1   75.6%   42.6%            25.5%   10.4%   91.8%
134.9   66.7%   34.9%            11.6%   67.5%   25.5%
 49.5   91.1%   59.3%            94.1%   78.5%    5.3%
283.7   68.6%   59.6%            70.4%   18.7%   89.7%
 14.3   44.6%     57%            93.1%   46.3%   31.6%
 56.9   36.3%   83.5%            99.8%   97.4%   53.2%
162.4     80%   49.5%             9.9%   79.5%   59.1%
248.3   53.3%   31.9%            21.1%   14.9%   59.7%
240.5   13.5%     57%            49.5%   49.3%   72.1%

I don't know the syntax of Rubi programming language, but your implementation looks correct.

Here is a Python implementation that matches the conversion formula from Wikipedia:

def hsi_to_rgb(hsi):
    """
    Convert HSI tuple to RGB tuple (without scaling the result by 255) 
    Formula: https://en.wikipedia.org/wiki/HSL_and_HSV#HSI_to_RGB        
    H - Range [0, 360] (degrees)
    S - Range [0, 1]
    V - Range [0, 1]
    The R,G,B output range is [0, 1]
    """

    H, S, I = float(hsi[0]), float(hsi[1]), float(hsi[2])

    Htag = H / 60
    Z = 1 - abs(Htag % 2 - 1)
    C = (3 * I * S) / (1   Z)
    X = C * Z

    if 0 <= Htag <= 1:
        R1, G1, B1 = C, X, 0
    elif 1 <= Htag <= 2:
        R1, G1, B1 = X, C, 0
    elif 2 <= Htag <= 3:
        R1, G1, B1 = 0, C, X
    elif 3 <= Htag <= 4:
        R1, G1, B1 = 0, X, C
    elif 4 <= Htag <= 5:
        R1, G1, B1 = X, 0, C
    elif 5 <= Htag <= 6:
        R1, G1, B1 = C, 0, X
    else:
        R1, G1, B1 = 0, 0, 0  # Undefined

    # Calculation rgb
    m = I * (1 - S)
    R, G, B = R1   m, G1   m, B1   m

    # Limit R, G, B to valid range:
    R = max(min(R, 1), 0)
    G = max(min(G, 1), 0)
    B = max(min(B, 1), 0)

    return (R, G, B)


def rgb2percent(rgb):
    """ Convert rgb tuple to percentage with one decimal digit accuracy """
    rgb_per = (round(rgb[0]*1000.0)/10, round(rgb[1]*1000.0)/10, round(rgb[2]*1000.0)/10)
    return rgb_per


print(rgb2percent(hsi_to_rgb([    0,    100/100,   33.3/100])))  # => (99.9,   0.0,   0.0)   Wiki:  100%      0%      0%
print(rgb2percent(hsi_to_rgb([   60,    100/100,     50/100])))  # => (75.0,  75.0,   0.0)   Wiki:   75%     75%      0%
print(rgb2percent(hsi_to_rgb([  120,    100/100,   16.7/100])))  # => ( 0.0,  50.1,   0.0)   Wiki:    0%     50%      0%
print(rgb2percent(hsi_to_rgb([  180,     40/100,   83.3/100])))  # => (50.0, 100.0, 100.0)   Wiki:   50%    100%    100%
print(rgb2percent(hsi_to_rgb([  240,     25/100,   66.7/100])))  # => (50.0,  50.0, 100.0)   Wiki:   50%     50%    100%
print(rgb2percent(hsi_to_rgb([  300,   57.1/100,   58.3/100])))  # => (74.9,  25.0,  74.9)   Wiki:   75%     25%     75%
print(rgb2percent(hsi_to_rgb([ 61.8,   69.9/100,   47.1/100])))  # => (62.8,  64.3,  14.2)   Wiki: 62.8%   64.3%   14.2%
print(rgb2percent(hsi_to_rgb([251.1,   75.6/100,   42.6/100])))  # => (25.5,  10.4,  91.9)   Wiki: 25.5%   10.4%   91.8%
print(rgb2percent(hsi_to_rgb([134.9,   66.7/100,   34.9/100])))  # => (11.6,  67.6,  25.5)   Wiki: 11.6%   67.5%   25.5%
print(rgb2percent(hsi_to_rgb([ 49.5,   91.1/100,   59.3/100])))  # => (94.1,  78.5,   5.3)   Wiki: 94.1%   78.5%    5.3%
print(rgb2percent(hsi_to_rgb([283.7,   68.6/100,   59.6/100])))  # => (70.4,  18.7,  89.7)   Wiki: 70.4%   18.7%   89.7%
print(rgb2percent(hsi_to_rgb([ 14.3,   44.6/100,     57/100])))  # => (93.2,  46.3,  31.6)   Wiki: 93.1%   46.3%   31.6%
print(rgb2percent(hsi_to_rgb([ 56.9,   36.3/100,   83.5/100])))  # => (99.9,  97.4,  53.2)   Wiki: 99.8%   97.4%   53.2%
print(rgb2percent(hsi_to_rgb([162.4,     80/100,   49.5/100])))  # => ( 9.9,  79.5,  59.1)   Wiki:  9.9%   79.5%   59.1%
print(rgb2percent(hsi_to_rgb([248.3,   53.3/100,   31.9/100])))  # => (21.1,  14.9,  59.7)   Wiki: 21.1%   14.9%   59.7%
print(rgb2percent(hsi_to_rgb([240.5,   13.5/100,     57/100])))  # => (49.5,  49.3,  72.2)   Wiki: 49.5%   49.3%   72.1%

As you can see, the results matches the examples table from Wikipedia.

CodePudding user response:

Comparisons with the WIKI color table:

def print_rgb(rgb)
  puts "[%s]" % rgb.map {|val| "%5.1f" % ((val / 255.0) * 100) }.join(", ")
end

print_rgb hsi_to_rgb([    0,    100/100.0,   33.3/100.0])  # => [100.0,   0.0,   0.0]   Wiki:  100%      0%      0%
print_rgb hsi_to_rgb([   60,    100/100.0,     50/100.0])  # => [ 74.9,  74.9,   0.0]   Wiki:   75%     75%      0%
print_rgb hsi_to_rgb([  120,    100/100.0,   16.7/100.0])  # => [  0.0,  50.2,   0.0]   Wiki:    0%     50%      0%
print_rgb hsi_to_rgb([  180,     40/100.0,   83.3/100.0])  # => [ 49.8, 100.0, 100.0]   Wiki:   50%    100%    100%
print_rgb hsi_to_rgb([  240,     25/100.0,   66.7/100.0])  # => [ 50.2,  50.2, 100.0]   Wiki:   50%     50%    100%
print_rgb hsi_to_rgb([  300,   57.1/100.0,   58.3/100.0])  # => [ 74.9,  25.1,  74.9]   Wiki:   75%     25%     75%
print_rgb hsi_to_rgb([ 61.8,   69.9/100.0,   47.1/100.0])  # => [ 62.7,  64.3,  14.1]   Wiki: 62.8%   64.3%   14.2%
print_rgb hsi_to_rgb([251.1,   75.6/100.0,   42.6/100.0])  # => [ 25.5,  10.6,  91.8]   Wiki: 25.5%   10.4%   91.8%
print_rgb hsi_to_rgb([134.9,   66.7/100.0,   34.9/100.0])  # => [ 11.8,  67.5,  25.5]   Wiki: 11.6%   67.5%   25.5%
print_rgb hsi_to_rgb([ 49.5,   91.1/100.0,   59.3/100.0])  # => [ 94.1,  78.4,   5.1]   Wiki: 94.1%   78.5%    5.3%
print_rgb hsi_to_rgb([283.7,   68.6/100.0,   59.6/100.0])  # => [ 70.6,  18.8,  89.8]   Wiki: 70.4%   18.7%   89.7%
print_rgb hsi_to_rgb([ 14.3,   44.6/100.0,     57/100.0])  # => [ 93.3,  46.3,  31.8]   Wiki: 93.1%   46.3%   31.6%
print_rgb hsi_to_rgb([ 56.9,   36.3/100.0,   83.5/100.0])  # => [100.0,  97.3,  53.3]   Wiki: 99.8%   97.4%   53.2%
print_rgb hsi_to_rgb([162.4,     80/100.0,   49.5/100.0])  # => [  9.8,  79.6,  59.2]   Wiki:  9.9%   79.5%   59.1%
print_rgb hsi_to_rgb([248.3,   53.3/100.0,   31.9/100.0])  # => [ 21.2,  14.9,  59.6]   Wiki: 21.1%   14.9%   59.7%
print_rgb hsi_to_rgb([240.5,   13.5/100.0,     57/100.0])  # => [ 49.4,  49.4,  72.2]   Wiki: 49.5%   49.3%   72.1%

The values are slightly different, since the method returns integer RGB values from the range 0..255

As Rotem said, the HSI values I tried to convert to RGB are out of RGB range.
All other RGB values of 16.7M colors are converted correctly.

  • Related