Possible tweak to Colour floating point conversion?


#1

For various reasons, my application has floating point color components instead of integers.  Things looked good, but when I wrote some unit tests I discovered some tiny inconsistencies - I worked around them easily but wanted to put this forth here.

The issue is juce::Color::fromFloatRGBA which uses juce::ColourHelpers::floatToUInt8 - the key line of code is this one:

return n <= 0.0f ? 0 : (n >= 1.0f ? 255 : (uint8) (n * 255.0f));

If n is always hard-bounded above by 1.0f, you can only get 255 if n is exactly equal to 1.0f.  But as you know, if you are testing for equality with floating point numbers, you are in a state of sin...

And indeed, I ran into cases where I did a lot of calculations and got numbers like r=0.9999247, which gets truncated to 254.

I used the following tweak. It's not theoretically correct but rounding isn't either - and it definitely solves my issues and should give the right answer nearly all the time or at least not make things worse:

  static const float ROUNDING_ADJUSTMENT = 0.1f;
  static const float COLOUR_MODULUS = 255.0f + ROUNDING_ADJUSTMENT;
  static uint8 floatToUInt8(const float n) {
       return static_cast<uint8>(jmax(0.0f, jmin(255.0f, n * COLOUR_MODULUS));
  }

#2

Interesting, thanks! Yes, just giving it that little extra nudge is probably a wise plan..


#3

Well if you want to map the floating point range [0, 1] to the integers [0, 255] then I would think the most logical way to do it would be to scale it using floating point into the half open interval [0, 256) and then truncate. This can’t be done by simply multiplying by 256 because that gives us a closed interval. The closest we can get is an approximation. Multiplying by 255.99609375 (= 256 - 1/256) does the trick:

// Convert float to uint8. f must be in the range [0, 1]
template <class Float>
std::uint8_t float_to_uint8 (Float f)
{
  Float const c (255.99609375);
  return static_cast <std::uint8_t> (std::floor (f * c));
}

Rounding doesn’t really make sense here. In the original post the value r=0.9999247, produces 255.97681 which gets truncated to 255.

I hope my math is right, this is untested.

Looking over the original function in the JUCE codebase, it seems Jules is okay with extra branches to handle the case where the input value is out of bounds. That suggests this exact solution:

// Convert float to uint8. n will be clamped as if in the range [0, 1)
uint8 floatToUInt8 (float const f) noexcept
{
    return f <= 0.0f ? 0 : (f >= 1.0f ? 255 : static_cast <uint8> (f * 256));
}

#4

Thanks Vinnie! 

I went with Tom's suggestion because it wouldn't break any existing code behaviour, just in case anyone was expecting their numbers not to be rounded up, but TBH I think doing it properly with a multiple of 256 makes more sense, and is unlikely to cause any surprises.