shawnrice / alfred-bundler

a utility / workflow to handle dependencies
MIT License
21 stars 3 forks source link

Error with color conversion Algorithms #53

Closed shawnrice closed 10 years ago

shawnrice commented 10 years ago

So, I decided to look further into the color conversion algorithms that I was using.

I went back to the source (equations) and rewrote them. They work much better now, especially because if you convert hex->rgb->hsv->rgb->hex, you'll get the same output as input. One major issue that I found was that PHP's % plays only with integers, so I had to switch to fmod. Regardless, I think everything is cleaner and nicer. We need to make sure that the logic in the other languages can spit out the same hex after those conversions.

As a bonus, I wrote a little test script for PHP (to test the functions), you'll find it under tests/generic/color_conversion_tests.php. When you run it, you're asked for a hex color, and then it shows you the conversions from the hex to hsv and back again.

deanishe commented 10 years ago

In order to test this in the other versions, could you explain more precisely how to verify if it's working correctly?

If I convert hex1->rbg->hsv->rgb->hex2 and hex1 == hex2, does that mean it's correct?

shawnrice commented 10 years ago

Yes. Well,

But Hex is the most important.

I also think that the conversion functions are a bit easier to read now, especially when in the color_conversion_tests.php file.

Also, look at the luminance function: I think it's a bit more of an elegant standard to use to test light/dark than the older one.

The initial implementation of luminance was similar, but this returns a nice value between 0 and 1 where .5 is the neutral point. So, the function is (r(.39) + g(.59) + b(.11)) / 255.

So, the luminance test for the icon should simply be:

$rgb = $this->rgb_to_hex( $hex );
if ( ( ( (rgb['r'] * .39) + (rgb['g'] * .59) + (rgb['b'] * .11) ) / 255 ) > .5 )
   return 'light';
else
  return 'dark';

The colors are weighted in how the human eye perceives light. Green light is "brighter" than blue light, brighter in that the human eye picks it up better. Cool, right? If you want to test this out for experience, then create an HTML document:

<html style='padding:0;'>
<body style='padding:0; margin:0'>
<div style='background-color: rgb(255, 0, 0); width: 100%; height: 100%'>red</div>
<div style='background-color: rgb(0, 255, 0); width: 100%; height: 100%'>green;</div>
<div style='background-color: rgb(0, 0, 255); width: 100%; height: 100%'>blue</div>
</body>
</html>

Then, open it in a browser, go in a dark room, and scroll through them, feeling how your eyes adjust/react. It's an interesting feeling.

deanishe commented 10 years ago

The values won't round-trip properly. Sometimes they're just a little off, due to rounding errors between int and float, but other times they're a fair way off.

I'm guessing this is because the conversion is lossy. What's your experience?

shawnrice commented 10 years ago

The first algorithm made it so that they never came out the same. Close, but not the same.

With the new one, they came out differently only once, and then I adjusted it a tiny bit (round instead of floor) and haven't seen it come out differently since. (Although, I've run the test script through only about 30 values, so, by no means exhaustive).

I think that at least some of the improved accuracy this time comes from that I'm not using floor. Also, the best discovery was that the % operator in PHP didn't play so well with floats.

And, there will be loss, but I think this new one makes it less lossy. Anytime we're converting from a ten decimal float to an integer will have some loss (mostly).

deanishe commented 10 years ago

How does round() work in PHP?

I was testing it with a function that generates random colours. In some instances, the round-tripped values were off by 30-40, which isn't a rounding error.

Or that might have been the colours round-tripped through alter.

shawnrice commented 10 years ago

While unit testing, I just ran this code:

  $color = generate_hex();
  $hex1 = $i->color( $color );
  $rgb1 = $i->hexToRgb( $hex1 );
  $hsv  = $i->rgbToHsv( $rgb1 );
  $rgb2 = $i->hsvToRgb( $hsv );
  $hex2 = $i->rgbToHex($rgb2);
  if ( $hex1 == $hex2 ) {
    echo "$hex1 == $hex2" . PHP_EOL;
    return TRUE;
  }

$i is an AlfredBundlerIcon object instantiated on its own. generate_hex(), well, generates a random hex. And I ran it on a loop 100,000 times. Results:

.... (truncated) ....
Calling color inverse test 99988: 6bc37f == 6bc37f
Calling color inverse test 99989: e61667 == e61667
Calling color inverse test 99990: 715442 == 715442
Calling color inverse test 99991: 77ec02 == 77ec02
Calling color inverse test 99992: 992dfb == 992dfb
Calling color inverse test 99993: d2bf60 == d2bf60
Calling color inverse test 99994: 4389fa == 4389fa
Calling color inverse test 99995: 2751bb == 2751bb
Calling color inverse test 99996: 8be9c4 == 8be9c4
Calling color inverse test 99997: 46fe3e == 46fe3e
Calling color inverse test 99998: c99162 == c99162
Calling color inverse test 99999: 2171c6 == 2171c6
Calling color inverse test 100000: bfca60 == bfca60
======================================================
Completed 100000 tests. For 100000 tests, we passed 100000 (100%)

So... it seems like the PHP version is pretty good.

shawnrice commented 10 years ago

The only time I rounded was at the end of hsvToRgb:

    $r = round( ( $r + $min ) * 255 );
    $g = round( ( $g + $min ) * 255 );
    $b = round( ( $b + $min ) * 255 );

PHP's round():

Returns the rounded value of val to specified precision (number of digits after the decimal point)

There are optional flags to round 0.5 either up or down. But default is to round 0.5 >= up.

shawnrice commented 10 years ago

I put it back to floor() instead of round(), and ran it again with fewer iterations. The results were much less stunning:

.... (truncated) ....
Calling color inverse test 991: e1254b != e1244a
Calling color inverse test 992: 179267 != 169266
Calling color inverse test 993: 7193b7 == 7193b7
Calling color inverse test 994: 91784c == 91784c
Calling color inverse test 995: 791e44 != 791d44
Calling color inverse test 996: 1fc79c == 1fc79c
Calling color inverse test 997: 17a527 != 17a526
Calling color inverse test 998: bfc86e != bfc86d
Calling color inverse test 999: c17ffd == c17ffd
Calling color inverse test 1000: da1e6b != da1d6b
======================================================
Completed 1000 tests. For 1000 tests, we passed 477 (47.7%)
deanishe commented 10 years ago

I've got the CSS -> RGB -> HSV -> RGB -> CSS working perfectly, but not light -> dark -> light.

Is that expected?

shawnrice commented 10 years ago

Do you mean to alter a color to dark and then alter it back and get the same?

deanishe commented 10 years ago

Yes.

shawnrice commented 10 years ago

Okay, yes. It's not as precise.

643658 -> 9b5488 -> 643658
Completed: 996 of 1000
3c96f1 -> 03090e -> 349bf1
Completed: 997 of 1000
f30c09 -> 0c0100 -> f31400
Completed: 998 of 1000
e11175 -> 1e0210 -> e10f78
Completed: 999 of 1000
fcf6d6 -> 030303 -> fcfcfc
Completed: 1000 of 1000
6be5cd -> 0c1a17 -> 6ae5cb
======================================================
Completed 1000 tests in 31.484 seconds.
For 1000 tests, we passed 243 (24.3%)

Here, passed just means a perfect conversion.

deanishe commented 10 years ago

Splendid. The Python version is in line with that…

I got the CSS -> RGB -> HSV -> RGB -> CSS working 100% correctly, but it was my suspicion that invert(invert(CSS)) was inherently lossy.

Closing this issue. Feel free to reopen.

shawnrice commented 10 years ago

Just a bit more on the accuracy... I wrote up some functions to test the difference of colors between the recursive alter test, and it looks like there is an average loss of 1.5% in the color integrity.

Looking at the data manually, it seems like no color bit is off by more than 5 (10 -> 15, or c9 -> ce).

My editor automatically highlights the CSS colors, so here's a crappy visual representation:

screen shot 2014-08-11 at 12 09 00 am