sightmachine/SimpleCV

Incorrect values returned by Color.hsv()

Opened this issue · 0 comments

A longer writeup is located at http://help.simplecv.org/question/1722/colorhsv-clarification-possible-bug/ but was unsure if my findings were a bug, but a commenter referred me to here, implying it was. Important details are copied here.

Problem

I believe Color.hsv() is returning incorrect values due to misuse of colorsys.rgb_to_hsv, which requires parameters in a 0-1 range, but is handed 'standard' RGB values (0-255), which results in incorrect results.

Details

In Color.py, the source for Color.hsv() reads:

def hsv(cls, tuple):
    hsv_float = colorsys.rgb_to_hsv(*tuple)
    return (hsv_float[0] * 180, hsv_float[1] * 255, hsv_float[2])

The documentation for colorsys reads:

All inputs and outputs are triples of floats in the range [0.0...1.0]
(with the exception of I and Q, which covers a slightly larger range).
Inputs outside the valid range may cause exceptions or invalid outputs.

In the docstring for Color.hsv(), an example usage (showing that the method wants an integral RGB vs. a scaled version) is:

    >>> c = Color.RED
    >>> hsvc = Color.hsv(c)

Below is a small script detailing how the existing test in the test suite passes for one color (which, incidentally, is wrong... Color.GREEN should be (0, 255, 0) and not (0, 128,0)), but fails for 20 of 36 of Color.colorlist colors. Included is a possible implementation that allows the test to pass for all Color.colorlist colors.

Also, note that while the Color.hsv() method is only called in a few places, there are multiple places where colorsys methods are called with unscaled RGB values. If the sample implementation below looks good, I'd be happy to make a proper patch to attempt fixing all of these issues.

Potential fix

#!/usr/bin/python

import colorsys

from SimpleCV import Color
from SimpleCV.tests import tests

DO_ALL = True

TRUE_GREEN = {'name': 'true green',
              'color': (0, 255, 0)}
STOCK_GREEN = {'name': 'stock green',
               'color': Color.GREEN}
MY_COLOR = {'name': 'problematic color',
                'color': (0, 45, 150)}

def color_test(colorinfo):
    Color.GREEN = colorinfo['color']
    print 'Running test with {0} ({1})'.format( colorinfo['name'],
                                                colorinfo['color'] )
    ret = 0
    try:
        tests.test_hsv_conversion()
    except AssertionError:
        print '  Failed!', Color.hsv(Color.GREEN)
        ret += 1
    return ret

def hsv(cls, tup):
    # This line is the most important    
    float_tup = tuple([i/255.0 for i in tup])

    # No change
    hsv_float = colorsys.rgb_to_hsv(*float_tup)

    # Only change is multiplying value by 255 and rounding...
    # the rounding is only to make the tests pass.  I don't know
    # whether the round() is correct or whether the tests should
    # be adapted to comparing floating point values
    return ( round(hsv_float[0] * 180),
             round(hsv_float[1] * 255),
             round(hsv_float[2] * 255) )

if __name__ == '__main__':
    if Color.GREEN != TRUE_GREEN['color']:
        print 'Stock green ({0}) is not true green ({1})'.format( Color.GREEN,
                                                                  TRUE_GREEN['color'] )
    failed = 0
    print '=' * 10, 'Regular Color.hsv', '=' * 10
    failed += color_test(STOCK_GREEN)
    failed += color_test(TRUE_GREEN)
    failed += color_test(MY_COLOR)
    if DO_ALL:
        for color in Color.colorlist:
            failed += color_test( {'color': color,
                                   'name': 'random color'} )
    print '=' * 10, 'Failed', failed, 'times', '=' * 10

    prev = Color.hsv
    Color.hsv = classmethod(hsv)

    failed = 0
    print '=' * 10, 'New Color.hsv', '=' * 10
    failed += color_test(STOCK_GREEN)
    failed += color_test(TRUE_GREEN)
    failed += color_test(MY_COLOR)
    if DO_ALL:
        for color in Color.colorlist:
            failed += color_test( {'color': color,
                                   'name': 'random color'} )
    print '=' * 10, 'Failed', failed, 'times', '=' * 10