Source code for scenic.simulators.utils.colors

"""A basic color type.

This used for example to represent car colors in the abstract driving domain,
as well as in the interfaces to GTA and Webots.
"""

from collections import namedtuple
import colorsys
import random
import struct

from scenic.core.distributions import Distribution, Normal, Options, Range, toDistribution
from scenic.core.lazy_eval import valueInContext
from scenic.core.object_types import Mutator


[docs]class Color(namedtuple("Color", ["r", "g", "b"])): """A color as an RGB tuple.""" @classmethod def withBytes(cls, color): return cls._make(c / 255.0 for c in color) @staticmethod def realToByte(color): return tuple(int(round(255 * c)) for c in color) @staticmethod def encodeTo(color, stream): stream.write(struct.pack("<ddd", *color)) @staticmethod def decodeFrom(stream): return Color(*struct.unpack("<ddd", stream.read(24)))
[docs] @staticmethod def uniformColor(): """Return a uniformly random color.""" return toDistribution(Color(Range(0, 1), Range(0, 1), Range(0, 1)))
[docs] @staticmethod def defaultCarColor(): """Default color distribution for cars. The distribution starts with a base distribution over 9 discrete colors, then adds Gaussian HSL noise. The base distribution uses color popularity statistics from a `2012 DuPont survey`_. .. _2012 DuPont survey: https://web.archive.org/web/20121229065631/http://www2.dupont.com/Media_Center/en_US/color_popularity/Images_2012/DuPont2012ColorPopularity.pdf """ baseColors = { (248, 248, 248): 0.24, # white (50, 50, 50): 0.19, # black (188, 185, 183): 0.16, # silver (130, 130, 130): 0.15, # gray (194, 92, 85): 0.10, # red (75, 119, 157): 0.07, # blue (197, 166, 134): 0.05, # brown/beige (219, 191, 105): 0.02, # yellow/gold (68, 160, 135): 0.02, # green } converted = {Color.withBytes(color): prob for color, prob in baseColors.items()} baseColor = Options(converted) # TODO improve this? hueNoise = Normal(0, 0.1) satNoise = Normal(0, 0.1) lightNoise = Normal(0, 0.1) return NoisyColorDistribution(baseColor, hueNoise, satNoise, lightNoise)
[docs]class NoisyColorDistribution(Distribution): """A distribution given by HSL noise around a base color. Arguments: baseColor (RGB tuple): base color hueNoise (float): noise to add to base hue satNoise (float): noise to add to base saturation lightNoise (float): noise to add to base lightness """ def __init__(self, baseColor, hueNoise, satNoise, lightNoise): super().__init__(baseColor, hueNoise, satNoise, lightNoise, valueType=Color) self.baseColor = baseColor self.hueNoise = hueNoise self.satNoise = satNoise self.lightNoise = lightNoise @staticmethod def addNoiseTo(color, hueNoise, lightNoise, satNoise): try: hue, lightness, saturation = colorsys.rgb_to_hls(*color[:3]) except ZeroDivisionError: hue, lightness, saturation = 0.0, 1.0, 0.0 hue = max(0, min(1, hue + hueNoise)) lightness = max(0, min(1, lightness + lightNoise)) saturation = max(0, min(1, saturation + satNoise)) return colorsys.hls_to_rgb(hue, lightness, saturation) def sampleGiven(self, value): bc = value[self.baseColor] return Color( *self.addNoiseTo( bc, value[self.hueNoise], value[self.lightNoise], value[self.satNoise] ) ) def evaluateInner(self, context): self.baseColor = valueInContext(self.baseColor, context) self.hueNoise = valueInContext(self.hueNoise, context) self.satNoise = valueInContext(self.satNoise, context) self.lightNoise = valueInContext(self.lightNoise, context)
[docs]class ColorMutator(Mutator): """Mutator that adds Gaussian HSL noise to the ``color`` property.""" def appliedTo(self, obj): stddev = 0.05 * obj.mutationScale hueNoise = random.gauss(0, stddev) satNoise = random.gauss(0, stddev) lightNoise = random.gauss(0, stddev) color = NoisyColorDistribution.addNoiseTo( obj.color, hueNoise, lightNoise, satNoise ) return (obj._copyWith(color=color), True) # allow further mutation