def getStartingColors(self, hueFilters=[], lightnessRange=[25,85], onlyUseRGB=True): """Randomly select a starting color from a subset of CIE Lab space. This function returns a set of highly preferable colors within a subspace of the typical 8,325-color CIE Lab space that fall within the range of any hue filters. Rather than the normal every-5 interval, the subspace specifies an every-15 interval along L, a, and b axis starting at the origin. Args: hueFilters (np.array): an n by 2 nd.array specifying lower and upper hue filter bounds that fall within [0,360) degrees. lightnessRange (list): a two-element list that sets the lightness range for filtering for color space before sampling. onlyUseRGB (bool): whether color space should be restricted to RGB. Returns: startingColors (np.array): an n x 3 array of n highly preferable CIE Lab D65 starting colors. """ hueFilters = np.array(hueFilters) lIntervals = CIE_LAB_STARTING_SUBSPACE_INTERVALS["L"] aIntervals = CIE_LAB_STARTING_SUBSPACE_INTERVALS["a"] bIntervals = CIE_LAB_STARTING_SUBSPACE_INTERVALS["b"] isInterval = np.zeros((self.colorSpaces.shape[0], 3)) isInterval[:,0] = np.in1d(self.colorSpaces[:,0], lIntervals) isInterval[:,1] = np.in1d(self.colorSpaces[:,1], aIntervals) isInterval[:,2] = np.in1d(self.colorSpaces[:,2], bIntervals) isIntervalMask = np.all(isInterval, axis=1) startColors = self.colorSpaces[isIntervalMask] isRGB = np.logical_and(startColors[:,[6,7,8]] >= 0, startColors[:,[6,7,8]] <= 255) isRGB = np.all(isRGB, axis=1) if lightnessRange[0] <= 10: minLightness = 0 else: minLightness = lightnessRange[0] + 0.01 if lightnessRange[1] <= 15: maxLightness = 15 else: maxLightness = lightnessRange[1] inLightness = np.logical_not(np.logical_or(startColors[:,0] < minLightness, startColors[:,0] > maxLightness)) startColors = startColors[np.logical_and(isRGB, inLightness)] if hueFilters.size > 0: hueFilters = convert.convertHueRanges(hueFilters) okHue = [np.logical_and(startColors[:,3] >= low, startColors[:,3] <= high) for low,high in hueFilters] okHue = np.any(np.array(okHue), axis=0) startColors = startColors[okHue] # With the remaining subspace, enumerate all unique color pairs. # For efficiency, unique pairs are calculated via one of the triangles # of the cartesian product of all remaining colors. labs = startColors[:,:3] color_col_products = [cartesian((labs[:,i],labs[:,i])) for i in xrange(labs.shape[1])] productSize = (color_col_products[0].shape[0],2*len(color_col_products)) color_product = np.zeros(productSize) for i, d in enumerate(color_col_products): color_product[:,i] = d[:,0] color_product[:,i+len(color_col_products)] = d[:,0] idxs = np.transpose(np.array(np.triu_indices(len(labs),1))) colorPairs = np.ascontiguousarray(labs[idxs,].reshape((-1, 6))) colorPairPreferenceScores = npc.score(colorPairs)[:,2] # Penalize preference scores for colors that are ``ugly''. labs1 = np.ascontiguousarray(colorPairs[:,:3]) labs2 = np.ascontiguousarray(colorPairs[:,3:6]) penalties = np.minimum(npc.scorePenalty(labs1)[:,0], npc.scorePenalty(labs2)[:,0]) colorPairPreferenceScores = colorPairPreferenceScores * penalties maxPref = np.max(colorPairPreferenceScores) stdPref = np.std(colorPairPreferenceScores) prefThreshold = maxPref - 0.75*stdPref colorPairs = colorPairs[colorPairPreferenceScores > prefThreshold,] # Extract the unique colors from color combination list # http://stackoverflow.com/questions/16970982 def getUnique(a): a = colorPairs[:,:3] b = np.ascontiguousarray(a).view(np.dtype((np.void, a.dtype.itemsize * a.shape[1]))) _, idx = np.unique(b, return_index=True) return a[idx] uniq1 = getUnique(colorPairs[:,:3]) uniq2 = getUnique(colorPairs[:,3:]) startingColors = getUnique( np.vstack(( uniq1, uniq2 )) ) return startingColors
def getStartingColors(self, hueFilters=[], lightnessRange=[25, 85], onlyUseRGB=True): """Randomly select a starting color from a subset of CIE Lab space. This function returns a set of highly preferable colors within a subspace of the typical 8,325-color CIE Lab space that fall within the range of any hue filters. Rather than the normal every-5 interval, the subspace specifies an every-15 interval along L, a, and b axis starting at the origin. Args: hueFilters (np.array): an n by 2 nd.array specifying lower and upper hue filter bounds that fall within [0,360) degrees. lightnessRange (list): a two-element list that sets the lightness range for filtering for color space before sampling. onlyUseRGB (bool): whether color space should be restricted to RGB. Returns: startingColors (np.array): an n x 3 array of n highly preferable CIE Lab D65 starting colors. """ hueFilters = np.array(hueFilters) lIntervals = CIE_LAB_STARTING_SUBSPACE_INTERVALS["L"] aIntervals = CIE_LAB_STARTING_SUBSPACE_INTERVALS["a"] bIntervals = CIE_LAB_STARTING_SUBSPACE_INTERVALS["b"] isInterval = np.zeros((self.colorSpaces.shape[0], 3)) isInterval[:, 0] = np.in1d(self.colorSpaces[:, 0], lIntervals) isInterval[:, 1] = np.in1d(self.colorSpaces[:, 1], aIntervals) isInterval[:, 2] = np.in1d(self.colorSpaces[:, 2], bIntervals) isIntervalMask = np.all(isInterval, axis=1) startColors = self.colorSpaces[isIntervalMask] isRGB = np.logical_and(startColors[:, [6, 7, 8]] >= 0, startColors[:, [6, 7, 8]] <= 255) isRGB = np.all(isRGB, axis=1) if lightnessRange[0] <= 10: minLightness = 0 else: minLightness = lightnessRange[0] + 0.01 if lightnessRange[1] <= 15: maxLightness = 15 else: maxLightness = lightnessRange[1] inLightness = np.logical_not( np.logical_or(startColors[:, 0] < minLightness, startColors[:, 0] > maxLightness)) startColors = startColors[np.logical_and(isRGB, inLightness)] if hueFilters.size > 0: hueFilters = convert.convertHueRanges(hueFilters) okHue = [ np.logical_and(startColors[:, 3] >= low, startColors[:, 3] <= high) for low, high in hueFilters ] okHue = np.any(np.array(okHue), axis=0) startColors = startColors[okHue] # With the remaining subspace, enumerate all unique color pairs. # For efficiency, unique pairs are calculated via one of the triangles # of the cartesian product of all remaining colors. labs = startColors[:, :3] color_col_products = [ cartesian((labs[:, i], labs[:, i])) for i in xrange(labs.shape[1]) ] productSize = (color_col_products[0].shape[0], 2 * len(color_col_products)) color_product = np.zeros(productSize) for i, d in enumerate(color_col_products): color_product[:, i] = d[:, 0] color_product[:, i + len(color_col_products)] = d[:, 0] idxs = np.transpose(np.array(np.triu_indices(len(labs), 1))) colorPairs = np.ascontiguousarray(labs[idxs, ].reshape((-1, 6))) colorPairPreferenceScores = npc.score(colorPairs)[:, 2] # Penalize preference scores for colors that are ``ugly''. labs1 = np.ascontiguousarray(colorPairs[:, :3]) labs2 = np.ascontiguousarray(colorPairs[:, 3:6]) penalties = np.minimum( npc.scorePenalty(labs1)[:, 0], npc.scorePenalty(labs2)[:, 0]) colorPairPreferenceScores = colorPairPreferenceScores * penalties maxPref = np.max(colorPairPreferenceScores) stdPref = np.std(colorPairPreferenceScores) prefThreshold = maxPref - 0.75 * stdPref colorPairs = colorPairs[colorPairPreferenceScores > prefThreshold, ] # Extract the unique colors from color combination list # http://stackoverflow.com/questions/16970982 def getUnique(a): a = colorPairs[:, :3] b = np.ascontiguousarray(a).view( np.dtype((np.void, a.dtype.itemsize * a.shape[1]))) _, idx = np.unique(b, return_index=True) return a[idx] uniq1 = getUnique(colorPairs[:, :3]) uniq2 = getUnique(colorPairs[:, 3:]) startingColors = getUnique(np.vstack((uniq1, uniq2))) return startingColors
def make(self, palSize, hueFilters=[], lightnessRange=[25,85], onlyUseRGB=True, noticeableDifferenceAngle=1.0/3.0, startPalette=[], weights={"ciede2000":1,"nameDifference":1,"nameUniqueness":0, "pairPreference":1}): """Make a palette with palSize colors by sampling using weights. Args: palSize (int): the number of colors to sample for the palette. hueFilters (list): a two-dimensional list, such that each element of hue filters is a two-element list that contains the lower and upper hue angle boundary for each hue angle region to include when sampling colors. lightnessRange (list): a two-element list that sets the lightness range for filtering for color space before sampling. onlyUseRGB (bool): whether color space should be restricted to RGB. noticeableDifferenceAngle (float): the visual angle that should be used when calculating CIE Lab noticeable difference intervals using Stone, Szafir, and Setlur's engineering color difference model http://www.danielleszafir.com/2014CIC_48_Stone_v3.pdf. startPalette (list): a two-dimensional list, such that each element of startPalette is a 3-element list that specifies a valid CIE Lab D65 color. Any dimension that are not a multiple of 5 will be rounded accordingly. weights (dict): user-defined weights ([0,1]) for the four palette scores such that the total weight always sums to 1. The weight names are `ciede2000`, `nameDifference`, `nameUniqueness`, and `pairPreference`. Returns: palette (np.ndarray): an array of CIE Lab D65 colors. """ assert isinstance(palSize, ( int, long )) and palSize > 0 assert "ciede2000" in weights and "nameDifference" in weights and\ "nameUniqueness" in weights and "pairPreference" in weights assert np.sum([weights[w] >= 0.0 and weights[w] <= 1.0 for w in weights]) == 4 hueFilters = np.array(hueFilters) hueFilters = convert.convertHueRanges(hueFilters) startPalette = list(startPalette) ndL, ndA, ndB = [d*3 for d in jnd.cieLabJND(noticeableDifferenceAngle)] if len(startPalette) > 0: palette = startPalette else: possibleStartColors = self.getStartingColors(hueFilters=hueFilters, lightnessRange=lightnessRange, onlyUseRGB=onlyUseRGB) startColorIdx = np.random.choice(possibleStartColors.shape[0]) palette = [possibleStartColors[startColorIdx]] if len(palette) >= palSize: return palette colorSpaces = self.colorSpaces nus = self.nameUniquenesses if hueFilters.size > 0: okHue = [np.logical_and(colorSpaces[:,3] >= low, colorSpaces[:,3] <= high) for low,high in hueFilters] okHue = np.any(np.array(okHue), axis=0) colorSpaces = colorSpaces[okHue] nus = nus[okHue] isRGB = np.logical_and(colorSpaces[:,[6,7,8]] >= 0, colorSpaces[:,[6,7,8]] <= 255) isRGB = np.all(isRGB, axis=1) minLightness = lightnessRange[0] + 0.01 maxLightness = lightnessRange[1] inLightness = np.logical_not(np.logical_or(colorSpaces[:,0] < minLightness, colorSpaces[:,0] > maxLightness)) rgbAndLightnessMask = np.logical_and(isRGB, inLightness) colorSpaces = colorSpaces[rgbAndLightnessMask] nus = nus[rgbAndLightnessMask] # filter ``ugly'' colors # TODO push this to the scorePenalty C function as a 0 weighting isUgly = np.zeros((colorSpaces.shape[0], 4)) isUgly[:,0] = colorSpaces[:,3] >= 85 isUgly[:,1] = colorSpaces[:,3] <= 114 isUgly[:,2] = colorSpaces[:,0] <= 75 isUgly[:,3] = colorSpaces[:,0] >= 35 isUglyMask = np.logical_not(np.all(isUgly, axis=1)) colorSpaces = colorSpaces[isUglyMask] nus = nus[isUglyMask] # remove any colors that aren't noticeably different from start palette for color in palette: diffs = np.absolute(colorSpaces[:,0:3] - color) isND = np.logical_or(diffs[:,0] >= ndL, np.logical_or(diffs[:,1] >= ndA, diffs[:,2] >= ndB)) colorSpaces = colorSpaces[isND] nus = nus[isND] if colorSpaces.shape[0] == 0: print 'Ran out of candidates.' return np.array(palette) lab = colorSpaces[:,0:3] lch = colorSpaces[:,[0,4,3]] rgb = colorSpaces[:,6:] candidates = lab scorePenalty = npc.scorePenalty(np.ascontiguousarray(candidates))[:,0] # apply the name uniqueness weight to all NU values nus *= weights["nameUniqueness"] nus = nus.reshape( (nus.shape[0], 1) ) # reshape for join startPalSize = len(palette) # TODO memoize loop s.t. previous results don't need to be recomputed for pi in xrange(palSize - startPalSize): # create combinations of candidates + palette colors # Each tile stores a candidate color paired with a palette color tiled = np.tile(candidates, (pi+startPalSize, 1)) tiled = np.hstack(( tiled, np.zeros(tiled.shape[0]*3).reshape(tiled.shape[0], 3) )) for i, p in enumerate(palette): tiled[:,3:] = p # stack the tiles to into the score ufunc format (2 Lab colors/row) scores = npc.score( np.ascontiguousarray(tiled) )[:,0:3] # reshape the 2 Lab colors/row so that each row is instead a # potential candidate and the columns are its scores to all of the # already-picked palette colors scores = np.hstack(np.split(scores,pi+startPalSize)) # Take the minimum value for each candidate score des = scores[:,0::3].min(axis=1) nds = scores[:,1::3].min(axis=1) pps = scores[:,2::3].min(axis=1) # Normalize CIEDE2000 and Pair Preference to [0,1] to match the Name # Difference and Name Uniqueness scores # The following distance bounds are precomputed from the 8325 colors in the dataset maxDistance = 122.48163103 minDistance = 1.02043527056 des = (des - minDistance) / (maxDistance - minDistance) maxPreference = 107.909 minPreference = -101.423 pps = (pps - minPreference) / (maxPreference - minPreference) # apply weights to the palette scores des *= weights["ciede2000"] nds *= weights["nameDifference"] pps *= weights["pairPreference"] # reshape scores for stacking des = des.reshape( (des.shape[0], 1) ) nds = nds.reshape( (nds.shape[0], 1) ) pps = pps.reshape( (pps.shape[0], 1) ) scores = np.hstack( (des, nds, pps, nus) ) scores = np.sum(scores, axis=1) # apply the ugly-color penalty function scores *= scorePenalty # sample a color above the score threshold limit threshold = np.max(scores) - 0.75*np.std(scores) choices = candidates[ scores > threshold, :] # If the thresholding yielded no candidates, don't perform it # This typically happens with low (i.e., 1) color candidate sets if choices.shape[0] == 0: choices = candidates choice = choices[np.random.choice(choices.shape[0])] palette = palette + [choice] # Prune choice and not noticeably different colors from sample space diffs = np.absolute(candidates-choice) isND = np.logical_or(diffs[:,0] >= ndL, np.logical_or(diffs[:,1] >= ndA, diffs[:,2] >= ndB)) candidates = candidates[isND] nus = nus[isND] scorePenalty = scorePenalty[isND] if candidates.shape[0] == 0: print 'Ran out when picking color #'+str(pi) break return np.array(palette)
def make( self, palSize, hueFilters=[], lightnessRange=[25, 85], onlyUseRGB=True, noticeableDifferenceAngle=1.0 / 3.0, startPalette=[], weights={ "ciede2000": 1, "nameDifference": 1, "nameUniqueness": 0, "pairPreference": 1 }): """Make a palette with palSize colors by sampling using weights. Args: palSize (int): the number of colors to sample for the palette. hueFilters (list): a two-dimensional list, such that each element of hue filters is a two-element list that contains the lower and upper hue angle boundary for each hue angle region to include when sampling colors. lightnessRange (list): a two-element list that sets the lightness range for filtering for color space before sampling. onlyUseRGB (bool): whether color space should be restricted to RGB. noticeableDifferenceAngle (float): the visual angle that should be used when calculating CIE Lab noticeable difference intervals using Stone, Szafir, and Setlur's engineering color difference model http://www.danielleszafir.com/2014CIC_48_Stone_v3.pdf. startPalette (list): a two-dimensional list, such that each element of startPalette is a 3-element list that specifies a valid CIE Lab D65 color. Any dimension that are not a multiple of 5 will be rounded accordingly. weights (dict): user-defined weights ([0,1]) for the four palette scores such that the total weight always sums to 1. The weight names are `ciede2000`, `nameDifference`, `nameUniqueness`, and `pairPreference`. Returns: palette (np.ndarray): an array of CIE Lab D65 colors. """ assert isinstance(palSize, (int, long)) and palSize > 0 assert "ciede2000" in weights and "nameDifference" in weights and\ "nameUniqueness" in weights and "pairPreference" in weights assert np.sum( [weights[w] >= 0.0 and weights[w] <= 1.0 for w in weights]) == 4 hueFilters = np.array(hueFilters) hueFilters = convert.convertHueRanges(hueFilters) startPalette = list(startPalette) ndL, ndA, ndB = [ d * 3 for d in jnd.cieLabJND(noticeableDifferenceAngle) ] if len(startPalette) > 0: palette = startPalette else: possibleStartColors = self.getStartingColors( hueFilters=hueFilters, lightnessRange=lightnessRange, onlyUseRGB=onlyUseRGB) startColorIdx = np.random.choice(possibleStartColors.shape[0]) palette = [possibleStartColors[startColorIdx]] if len(palette) >= palSize: return palette colorSpaces = self.colorSpaces nus = self.nameUniquenesses if hueFilters.size > 0: okHue = [ np.logical_and(colorSpaces[:, 3] >= low, colorSpaces[:, 3] <= high) for low, high in hueFilters ] okHue = np.any(np.array(okHue), axis=0) colorSpaces = colorSpaces[okHue] nus = nus[okHue] isRGB = np.logical_and(colorSpaces[:, [6, 7, 8]] >= 0, colorSpaces[:, [6, 7, 8]] <= 255) isRGB = np.all(isRGB, axis=1) minLightness = lightnessRange[0] + 0.01 maxLightness = lightnessRange[1] inLightness = np.logical_not( np.logical_or(colorSpaces[:, 0] < minLightness, colorSpaces[:, 0] > maxLightness)) rgbAndLightnessMask = np.logical_and(isRGB, inLightness) colorSpaces = colorSpaces[rgbAndLightnessMask] nus = nus[rgbAndLightnessMask] # filter ``ugly'' colors # TODO push this to the scorePenalty C function as a 0 weighting isUgly = np.zeros((colorSpaces.shape[0], 4)) isUgly[:, 0] = colorSpaces[:, 3] >= 85 isUgly[:, 1] = colorSpaces[:, 3] <= 114 isUgly[:, 2] = colorSpaces[:, 0] <= 75 isUgly[:, 3] = colorSpaces[:, 0] >= 35 isUglyMask = np.logical_not(np.all(isUgly, axis=1)) colorSpaces = colorSpaces[isUglyMask] nus = nus[isUglyMask] # remove any colors that aren't noticeably different from start palette for color in palette: diffs = np.absolute(colorSpaces[:, 0:3] - color) isND = np.logical_or( diffs[:, 0] >= ndL, np.logical_or(diffs[:, 1] >= ndA, diffs[:, 2] >= ndB)) colorSpaces = colorSpaces[isND] nus = nus[isND] if colorSpaces.shape[0] == 0: print 'Ran out of candidates.' return np.array(palette) lab = colorSpaces[:, 0:3] lch = colorSpaces[:, [0, 4, 3]] rgb = colorSpaces[:, 6:] candidates = lab scorePenalty = npc.scorePenalty(np.ascontiguousarray(candidates))[:, 0] # apply the name uniqueness weight to all NU values nus *= weights["nameUniqueness"] nus = nus.reshape((nus.shape[0], 1)) # reshape for join startPalSize = len(palette) # TODO memoize loop s.t. previous results don't need to be recomputed for pi in xrange(palSize - startPalSize): # create combinations of candidates + palette colors # Each tile stores a candidate color paired with a palette color tiled = np.tile(candidates, (pi + startPalSize, 1)) tiled = np.hstack( (tiled, np.zeros(tiled.shape[0] * 3).reshape(tiled.shape[0], 3))) for i, p in enumerate(palette): tiled[:, 3:] = p # stack the tiles to into the score ufunc format (2 Lab colors/row) scores = npc.score(np.ascontiguousarray(tiled))[:, 0:3] # reshape the 2 Lab colors/row so that each row is instead a # potential candidate and the columns are its scores to all of the # already-picked palette colors scores = np.hstack(np.split(scores, pi + startPalSize)) # Take the minimum value for each candidate score des = scores[:, 0::3].min(axis=1) nds = scores[:, 1::3].min(axis=1) pps = scores[:, 2::3].min(axis=1) # Normalize CIEDE2000 and Pair Preference to [0,1] to match the Name # Difference and Name Uniqueness scores # The following distance bounds are precomputed from the 8325 colors in the dataset maxDistance = 122.48163103 minDistance = 1.02043527056 des = (des - minDistance) / (maxDistance - minDistance) maxPreference = 107.909 minPreference = -101.423 pps = (pps - minPreference) / (maxPreference - minPreference) # apply weights to the palette scores des *= weights["ciede2000"] nds *= weights["nameDifference"] pps *= weights["pairPreference"] # reshape scores for stacking des = des.reshape((des.shape[0], 1)) nds = nds.reshape((nds.shape[0], 1)) pps = pps.reshape((pps.shape[0], 1)) scores = np.hstack((des, nds, pps, nus)) scores = np.sum(scores, axis=1) # apply the ugly-color penalty function scores *= scorePenalty # sample a color above the score threshold limit threshold = np.max(scores) - 0.75 * np.std(scores) choices = candidates[scores > threshold, :] # If the thresholding yielded no candidates, don't perform it # This typically happens with low (i.e., 1) color candidate sets if choices.shape[0] == 0: choices = candidates choice = choices[np.random.choice(choices.shape[0])] palette = palette + [choice] # Prune choice and not noticeably different colors from sample space diffs = np.absolute(candidates - choice) isND = np.logical_or( diffs[:, 0] >= ndL, np.logical_or(diffs[:, 1] >= ndA, diffs[:, 2] >= ndB)) candidates = candidates[isND] nus = nus[isND] scorePenalty = scorePenalty[isND] if candidates.shape[0] == 0: print 'Ran out when picking color #' + str(pi) break return np.array(palette)