Skip to main content

Color Library HSV Bug

While experimenting with terminal color schemes, I have been using the Color library. This library is pretty new! When I last experimented with color schemes, I used the colour library. I would like to compare colors using the ΔE* CIEDE2000 algorithm, however, and I switched to the Color library because the colour library does not support the CIELAB color space. It is a nice library! Referencing the source code while I use it, I spotted a bug in the HSV color model implementation.

The bug is in the rgb2hsv function, which converts from the RGB color model to the HSV color model.

rgb2hsv :: (Ord e, Fractional e) => Color RGB e -> Color HSV e
rgb2hsv (ColorRGB r g b) = ColorHSV h s v
  where
    !max' = max r (max g b)
    !min' = min r (min g b)
    !h' | max' == r = (    (g - b) / (max' - min')) / 6
        | max' == g = (2 + (b - r) / (max' - min')) / 6
        | max' == b = (4 + (r - g) / (max' - min')) / 6
        | otherwise = 0
    !h
      | h' < 0 = h' + 1
      | otherwise = h'
    !s
      | max' == 0 = 0
      | otherwise = (max' - min') / max'
    !v = max'
{-# INLINE rgb2hsv #-}

The correct equations for conversion from RGB to HSV are as follows, where \(R, G, B \in [0,1]\).

First, the maximum and minimum RGB component is computed.

\[ X_{max} = \max(R, G, B) \]

\[ X_{min} = \min(R, G, B) \]

The chroma is then computed as the difference of these values.

\[ C = X_{max} - X_{min} \]

The hue (H) is generally measured in degrees, as it is the angle component of the cylindrical geometry of the HSV color model. The calculation depends on which RGB component is greatest, and the zero case is special.

\[ H = \left\{ \begin{array}{ll} 0, & \text{if } C = 0 \\ 60^{\circ} \times (0 + \frac{G - B}{C}), & \text{if } X_{max} = R \\ 60^{\circ} \times (2 + \frac{B - R}{C}), & \text{if } X_{max} = G \\ 60^{\circ} \times (4 + \frac{R - G}{C}), & \text{if } X_{max} = B \\ \end{array} \right. \]

The saturation (S) is generally measured as a percentage, representing the distance from the center (0%) to the edge (100%) of the cylindrical geometry of the HSV color model. The zero case is special.

\[ S = \left\{ \begin{array}{ll} 0, & \text{if } C = 0 \\ \frac{C}{X_{max}}, & \text{otherwise} \\ \end{array} \right. \]

The value (V) is generally measured as a percentage as well, representing the distance from the bottom (0%) to the top (100%) of the cylindrical geometry of the HSV color model. It is simply the value of the maximum RGB component.

\[ V = X_{max} \]

The bug in the above implementation is in the calculation of the hue. The special case is not handled! When converting a greyscale color, all of the RGB components are equal, so the chroma is zero. The first guard (max' == r) is true because both values being compared are the same, and there is a division by zero as a result.

I wrote a test program (available on GitHub) that demonstrates the bug.

blackSRGB :: Color (SRGB 'NonLinear) Word8
blackSRGB = ColorSRGB 0x00 0x00 0x00

blackHSV :: Color (HSV (SRGB 'NonLinear)) Float
blackHSV = convert blackSRGB

main :: IO ()
main = print blackHSV

Running this program prints the following. The hue is NaN, but the correct value is 0.

<HSV-SRGB 'NonLinear:(        NaN, 0.00000000, 0.00000000)>

The fix is simple: handle the special case first. Note that the otherwise case is still required in order to satisfy the type checker.

rgb2hsv :: (Ord e, Fractional e) => Color RGB e -> Color HSV e
rgb2hsv (ColorRGB r g b) = ColorHSV h s v
  where
    !max' = max r (max g b)
    !min' = min r (min g b)
    !c' = max' - min'
    !h' | c'   == 0 = 0
        | max' == r = (    (g - b) / c') / 6
        | max' == g = (2 + (b - r) / c') / 6
        | max' == b = (4 + (r - g) / c') / 6
        | otherwise = 0
    !h
      | h' < 0 = h' + 1
      | otherwise = h'
    !s
      | max' == 0 = 0
      | otherwise = c' / max'
    !v = max'
{-# INLINE rgb2hsv #-}

The rgb2hsl function, which converts from the RGB color model to the HSL color model, has a similar bug. I fixed it in the same way.

I submitted a pull request to the library repository.

Author

Travis Cardwell

Published

Revised

Tags