2 years ago
#64243

Jacob Birkett
Inconsistent results with floating point arithmetic (color conversion formulae)
I am trying to parse a bunch of color values from various files, perform lightness changes, and write them back into the files. I have to put this in a PKGBUILD
for the Arch User Repository and would love to avoid any dependencies.
I have written the simple class below, and it works sometimes. The test function at the bottom of the snippet makes an iterable of Color
instances from a decent number of test values, converts them to HSL, and then back to RGB. It then compares each with the original, to check if they match.
Sometimes the script exits successfully, but more often times not. I randomly see huge discrepancies, such as RGB #c7c7c7
-> HSL -> RGB #be3841
. This doesn't make sense to me, how can it be this far off? There has to be an issue with my math, but I have checked that against Wikipedia so many times that I have come to the conclusion that it is simply a rounding error due to the precision of a 64-bit float, which Python uses by default on my platform.
I simply do not understand the randomness. I have tried using gmpy2
with 128-bit precision to no avail, it fails just as often. And if I use Decimal
, the interpreter complains that I am accessing local variables before assignment which means that the comparison operators in the algorithms are not working as I would expect.
If you quickly run the script a few times, eventually there won't be an error. https://www.online-python.com/ujtET97ZxQ
class Color:
def __init__(self, rgba):
rgba = rgba.lstrip("#")
if len(rgba) > 8:
raise Exception("Expected up to 8 hexadecimal characters")
self._red = int(rgba[0:2], 16)
self._green = int(rgba[2:4], 16)
self._blue = int(rgba[4:6], 16)
if len(rgba) == 8:
self.alpha = int(rgba[6:8], 16)
else:
self.alpha = None
self._rgb_changed = True
self._hsl_changed = False
def __repr__(self):
return self.hex()
def __hash__(self):
return hash(self.hex())
def __eq__(self, other):
return self.__class__ is other.__class__ and self.__hash__() == other.__hash__()
def hex(self):
return "#{:02x}{:02x}{:02x}".format(self.red, self.green, self.blue) + ("{:02x}".format(self.alpha) if self.alpha else "")
def hsl(self):
if self._hsl_changed:
raise Exception(
"Cannot convert from RGB when the last modified value was of HSL")
rp = self._red / 255
gp = self._green / 255
bp = self._blue / 255
cmax = max(rp, gp, bp)
cmin = min(rp, gp, bp)
delta = cmax - cmin
if delta == 0:
self._hue = 0
elif cmax == rp:
self._hue = 60 * ((gp - bp) / delta % 6)
elif cmax == gp:
self._hue = 60 * ((bp - rp) / delta + 2)
elif cmax == bp:
self._hue = 60 * ((rp - gp) / delta + 4)
self._lightness = (cmax + cmin) / 2
if delta == 0:
self._saturation = 0
else:
self._saturation = delta / (1 - abs(2 * self._lightness - 1))
self._rgb_changed = False
return (self._hue, self._saturation, self._lightness)
def rgb(self):
if self._rgb_changed:
raise Exception(
"Cannot convert from HSL when the last modified value was of RGB")
c = (1 - abs(2 * self._lightness - 1)) * self._saturation
x = c * (1 - abs((self._hue / 60) % 2 - 1))
m = self._lightness - c / 2
if self._hue >= 0 and self._hue < 60:
(rp, gp, bp) = (c, x, 0)
elif self._hue >= 60 and self._hue < 120:
(rp, gp, bp) = (x, c, 0)
elif self._hue >= 120 and self._hue < 180:
(rp, gp, bp) = (0, c, x)
elif self._hue >= 180 and self._hue < 240:
(rp, gp, bp) = (0, x, c)
elif self._hue >= 240 and self._hue < 300:
(rp, gp, bp) = (x, 0, c)
elif self._hue >= 300 and self._hue < 360:
(rp, gp, bp) = (c, 0, x)
self._red = round((rp + m) * 255)
self._green = round((gp + m) * 255)
self._blue = round((bp + m) * 255)
self._hsl_changed = False
return (self._red, self._green, self._blue)
@property
def red(self):
if self._hsl_changed:
self.rgb()
return self._red
@property
def green(self):
if self._hsl_changed:
self.rgb()
return self._green
@property
def blue(self):
if self._hsl_changed:
self.rgb()
return self._blue
@red.setter
def red(self, red):
self._red = red
self._rgb_changed = True
@green.setter
def green(self, green):
self._green = green
self._rgb_changed = True
@blue.setter
def blue(self, blue):
self._blue = blue
self._rgb_changed = True
@property
def hue(self):
if self._rgb_changed:
self.hsl()
return self._hue
@property
def saturation(self):
if self._rgb_changed:
self.hsl()
return self._saturation
@property
def lightness(self):
if self._rgb_changed:
self.hsl()
return self._lightness
@hue.setter
def hue(self, hue):
self._hue = hue
self._hsl_changed = True
@saturation.setter
def saturation(self, saturation):
self._saturation = saturation
self._hsl_changed = True
@lightness.setter
def lightness(self, lightness):
self._lightness = lightness
self._hsl_changed = True
def __test(colors):
from copy import deepcopy
colors = set(Color(color) for color in colors)
colors_mutated = deepcopy(colors)
for color in colors_mutated:
color.hsl()
color.rgb()
for (color_a, color_b) in zip(colors, colors_mutated):
if color_a != color_b:
raise AssertionError(
"Colors do not match! ({}, {})".format(color_a, color_b))
print("{} == {}".format(color_a, color_b))
if __name__ == "__main__":
colors = set("#353b48, #666666, #444852, #fcfcfc, #434343, #90939b, #353537, #2b303b, #b6b8c0, #241f31, #303440, #000000, #9398a2, #dfdfdf, #f0f1f2, #cfcfcf, #d3d8e2, #505666, #808080, #8a939f, #282b36, #afb8c6, #383838, #4dadd4, #353a48, #838383, #202229, #7a7f8a, #7a7f8b, #2e3340, #70788d, #66a1dc, #17191f, #d7d7d7, #545860, #39404d, #161a26, #be3841, #3c4049, #2f3a42, #f0f2f5, #4e4eff, #262934, #1d1f26, #404552, #353945, #383c45, #8f939d, #f7ef45, #a4aab7, #b2cdf1, #444a58, #bac3cf, #ff00ff, #f46067, #5c6070, #c7cacf, #525762, #ff0b00, #323644, #f75a61, #464646, #ecedf0, #171717, #e01b24, #1b1b1b, #797d87, #15171c, #8c919d, #4d4f52, #5b627b, #728495, #454c5c, #4080fb, #e2e2e2, #d1d3da, #c0e3ff, #3580e4, #b7c0d3, #232428, #2d323f, #6e6e6e, #dcdcdc, #b9bcc2, #cc575d, #a1a1a1, #52555e, #353a47, #7c818c, #979dac, #2f343f, #dde3e9, #828282, #c5dcf7, #001aff, #722563, #afb8c5, #222529, #8abfdd, #666a74, #f68086, #edf5fb, #4b5162, #a9acb2, #786613, #c7c7c7, #eeeff1, #2b2e37, #f70505, #292c36, #3e434f, #5c616c, #f57900, #2d303b, #f5f6f7, #5f697f, #2e3436, #808791, #f08437, #cbd2e3, #e5a50a, #eeeeee, #252932, #e7e8eb, #3e4350, #ff1111, #ef2929, #fc4138, #fcfdfd, #7a7a7a, #21242b, #bebebe, #ffffff, #252a35, #5252ff, #767b87, #535353, #3e3e3e, #aa5555, #5f6578, #c4c7cc, #383c4a, #102b68, #21252b, #f3af0b, #cfd6e6, #d7787d, #ff7a80, #fdfdfd, #398dd3, #a51d2d, #73d216, #f8f8f9, #262932, #2f343b, #2b2e39, #2d3036, #f04a50, #006098, #3f4453, #ad4242, #1b1c21, #b9bfce, #ff1616, #e5e5e5, #ed686f, #eaebed, #fbfcfc, #398cd3, #262933, #5294e2, #0000ff, #d7d8dd, #2b2f3b, #f13039, #999999, #1f1f1f, #50dbb5, #525252, #ff2121, #f27835, #91949c, #adafb5, #3b3c3e, #d3d4d8, #525d76, #434652, #cacaca, #2d323d, #f9fafb, #617c95, #ededed, #1a1a1a, #d8354a, #90949e, #313541, #a8a8a8, #dbdfe3, #cecece, #0f0f0f, #1d242a, #b8babf, #0f1116, #eef4fc, #e2e7ef, #d3dae3".split(", "))
__test(colors)
python
python-3.x
math
floating-point
precision
0 Answers
Your Answer