Related
I wrote the two methods below to automatically select N distinct colors. It works by defining a piecewise linear function on the RGB cube. The benefit of this is you can also get a progressive scale if that's what you want, but when N gets large the colors can start to look similar. I can also imagine evenly subdividing the RGB cube into a lattice and then drawing points. Does anyone know any other methods? I'm ruling out defining a list and then just cycling through it. I should also say I don't generally care if they clash or don't look nice, they just have to be visually distinct.
public static List<Color> pick(int num) {
List<Color> colors = new ArrayList<Color>();
if (num < 2)
return colors;
float dx = 1.0f / (float) (num - 1);
for (int i = 0; i < num; i++) {
colors.add(get(i * dx));
}
return colors;
}
public static Color get(float x) {
float r = 0.0f;
float g = 0.0f;
float b = 1.0f;
if (x >= 0.0f && x < 0.2f) {
x = x / 0.2f;
r = 0.0f;
g = x;
b = 1.0f;
} else if (x >= 0.2f && x < 0.4f) {
x = (x - 0.2f) / 0.2f;
r = 0.0f;
g = 1.0f;
b = 1.0f - x;
} else if (x >= 0.4f && x < 0.6f) {
x = (x - 0.4f) / 0.2f;
r = x;
g = 1.0f;
b = 0.0f;
} else if (x >= 0.6f && x < 0.8f) {
x = (x - 0.6f) / 0.2f;
r = 1.0f;
g = 1.0f - x;
b = 0.0f;
} else if (x >= 0.8f && x <= 1.0f) {
x = (x - 0.8f) / 0.2f;
r = 1.0f;
g = 0.0f;
b = x;
}
return new Color(r, g, b);
}
This questions appears in quite a few SO discussions:
Algorithm For Generating Unique Colors
Generate unique colours
Generate distinctly different RGB colors in graphs
How to generate n different colors for any natural number n?
Different solutions are proposed, but none are optimal. Luckily, science comes to the rescue
Arbitrary N
Colour displays for categorical images (free download)
A WEB SERVICE TO PERSONALISE MAP COLOURING (free download, a webservice solution should be available by next month)
An Algorithm for the Selection of High-Contrast Color Sets (the authors offer a free C++ implementation)
High-contrast sets of colors (The first algorithm for the problem)
The last 2 will be free via most university libraries / proxies.
N is finite and relatively small
In this case, one could go for a list solution. A very interesting article in the subject is freely available:
A Colour Alphabet and the Limits of Colour Coding
There are several color lists to consider:
Boynton's list of 11 colors that are almost never confused (available in the first paper of the previous section)
Kelly's 22 colors of maximum contrast (available in the paper above)
I also ran into this Palette by an MIT student.
Lastly, The following links may be useful in converting between different color systems / coordinates (some colors in the articles are not specified in RGB, for instance):
http://chem8.org/uch/space-55036-do-blog-id-5333.html
https://metacpan.org/pod/Color::Library::Dictionary::NBS_ISCC
Color Theory: How to convert Munsell HVC to RGB/HSB/HSL
For Kelly's and Boynton's list, I've already made the conversion to RGB (with the exception of white and black, which should be obvious). Some C# code:
public static ReadOnlyCollection<Color> KellysMaxContrastSet
{
get { return _kellysMaxContrastSet.AsReadOnly(); }
}
private static readonly List<Color> _kellysMaxContrastSet = new List<Color>
{
UIntToColor(0xFFFFB300), //Vivid Yellow
UIntToColor(0xFF803E75), //Strong Purple
UIntToColor(0xFFFF6800), //Vivid Orange
UIntToColor(0xFFA6BDD7), //Very Light Blue
UIntToColor(0xFFC10020), //Vivid Red
UIntToColor(0xFFCEA262), //Grayish Yellow
UIntToColor(0xFF817066), //Medium Gray
//The following will not be good for people with defective color vision
UIntToColor(0xFF007D34), //Vivid Green
UIntToColor(0xFFF6768E), //Strong Purplish Pink
UIntToColor(0xFF00538A), //Strong Blue
UIntToColor(0xFFFF7A5C), //Strong Yellowish Pink
UIntToColor(0xFF53377A), //Strong Violet
UIntToColor(0xFFFF8E00), //Vivid Orange Yellow
UIntToColor(0xFFB32851), //Strong Purplish Red
UIntToColor(0xFFF4C800), //Vivid Greenish Yellow
UIntToColor(0xFF7F180D), //Strong Reddish Brown
UIntToColor(0xFF93AA00), //Vivid Yellowish Green
UIntToColor(0xFF593315), //Deep Yellowish Brown
UIntToColor(0xFFF13A13), //Vivid Reddish Orange
UIntToColor(0xFF232C16), //Dark Olive Green
};
public static ReadOnlyCollection<Color> BoyntonOptimized
{
get { return _boyntonOptimized.AsReadOnly(); }
}
private static readonly List<Color> _boyntonOptimized = new List<Color>
{
Color.FromArgb(0, 0, 255), //Blue
Color.FromArgb(255, 0, 0), //Red
Color.FromArgb(0, 255, 0), //Green
Color.FromArgb(255, 255, 0), //Yellow
Color.FromArgb(255, 0, 255), //Magenta
Color.FromArgb(255, 128, 128), //Pink
Color.FromArgb(128, 128, 128), //Gray
Color.FromArgb(128, 0, 0), //Brown
Color.FromArgb(255, 128, 0), //Orange
};
static public Color UIntToColor(uint color)
{
var a = (byte)(color >> 24);
var r = (byte)(color >> 16);
var g = (byte)(color >> 8);
var b = (byte)(color >> 0);
return Color.FromArgb(a, r, g, b);
}
And here are the RGB values in hex and 8-bit-per-channel representations:
kelly_colors_hex = [
0xFFB300, # Vivid Yellow
0x803E75, # Strong Purple
0xFF6800, # Vivid Orange
0xA6BDD7, # Very Light Blue
0xC10020, # Vivid Red
0xCEA262, # Grayish Yellow
0x817066, # Medium Gray
# The following don't work well for people with defective color vision
0x007D34, # Vivid Green
0xF6768E, # Strong Purplish Pink
0x00538A, # Strong Blue
0xFF7A5C, # Strong Yellowish Pink
0x53377A, # Strong Violet
0xFF8E00, # Vivid Orange Yellow
0xB32851, # Strong Purplish Red
0xF4C800, # Vivid Greenish Yellow
0x7F180D, # Strong Reddish Brown
0x93AA00, # Vivid Yellowish Green
0x593315, # Deep Yellowish Brown
0xF13A13, # Vivid Reddish Orange
0x232C16, # Dark Olive Green
]
kelly_colors = dict(vivid_yellow=(255, 179, 0),
strong_purple=(128, 62, 117),
vivid_orange=(255, 104, 0),
very_light_blue=(166, 189, 215),
vivid_red=(193, 0, 32),
grayish_yellow=(206, 162, 98),
medium_gray=(129, 112, 102),
# these aren't good for people with defective color vision:
vivid_green=(0, 125, 52),
strong_purplish_pink=(246, 118, 142),
strong_blue=(0, 83, 138),
strong_yellowish_pink=(255, 122, 92),
strong_violet=(83, 55, 122),
vivid_orange_yellow=(255, 142, 0),
strong_purplish_red=(179, 40, 81),
vivid_greenish_yellow=(244, 200, 0),
strong_reddish_brown=(127, 24, 13),
vivid_yellowish_green=(147, 170, 0),
deep_yellowish_brown=(89, 51, 21),
vivid_reddish_orange=(241, 58, 19),
dark_olive_green=(35, 44, 22))
For all you Java developers, here are the JavaFX colors:
// Don't forget to import javafx.scene.paint.Color;
private static final Color[] KELLY_COLORS = {
Color.web("0xFFB300"), // Vivid Yellow
Color.web("0x803E75"), // Strong Purple
Color.web("0xFF6800"), // Vivid Orange
Color.web("0xA6BDD7"), // Very Light Blue
Color.web("0xC10020"), // Vivid Red
Color.web("0xCEA262"), // Grayish Yellow
Color.web("0x817066"), // Medium Gray
Color.web("0x007D34"), // Vivid Green
Color.web("0xF6768E"), // Strong Purplish Pink
Color.web("0x00538A"), // Strong Blue
Color.web("0xFF7A5C"), // Strong Yellowish Pink
Color.web("0x53377A"), // Strong Violet
Color.web("0xFF8E00"), // Vivid Orange Yellow
Color.web("0xB32851"), // Strong Purplish Red
Color.web("0xF4C800"), // Vivid Greenish Yellow
Color.web("0x7F180D"), // Strong Reddish Brown
Color.web("0x93AA00"), // Vivid Yellowish Green
Color.web("0x593315"), // Deep Yellowish Brown
Color.web("0xF13A13"), // Vivid Reddish Orange
Color.web("0x232C16"), // Dark Olive Green
};
the following is the unsorted kelly colors according to the order above.
the following is the sorted kelly colors according to hues (note that some yellows are not very contrasting)
You can use the HSL color model to create your colors.
If all you want is differing hues (likely), and slight variations on lightness or saturation, you can distribute the hues like so:
// assumes hue [0, 360), saturation [0, 100), lightness [0, 100)
for(i = 0; i < 360; i += 360 / num_colors) {
HSLColor c;
c.hue = i;
c.saturation = 90 + randf() * 10;
c.lightness = 50 + randf() * 10;
addColor(c);
}
Like Uri Cohen's answer, but is a generator instead. Will start by using colors far apart. Deterministic.
Sample, left colors first:
#!/usr/bin/env python3
from typing import Iterable, Tuple
import colorsys
import itertools
from fractions import Fraction
from pprint import pprint
def zenos_dichotomy() -> Iterable[Fraction]:
"""
http://en.wikipedia.org/wiki/1/2_%2B_1/4_%2B_1/8_%2B_1/16_%2B_%C2%B7_%C2%B7_%C2%B7
"""
for k in itertools.count():
yield Fraction(1,2**k)
def fracs() -> Iterable[Fraction]:
"""
[Fraction(0, 1), Fraction(1, 2), Fraction(1, 4), Fraction(3, 4), Fraction(1, 8), Fraction(3, 8), Fraction(5, 8), Fraction(7, 8), Fraction(1, 16), Fraction(3, 16), ...]
[0.0, 0.5, 0.25, 0.75, 0.125, 0.375, 0.625, 0.875, 0.0625, 0.1875, ...]
"""
yield Fraction(0)
for k in zenos_dichotomy():
i = k.denominator # [1,2,4,8,16,...]
for j in range(1,i,2):
yield Fraction(j,i)
# can be used for the v in hsv to map linear values 0..1 to something that looks equidistant
# bias = lambda x: (math.sqrt(x/3)/Fraction(2,3)+Fraction(1,3))/Fraction(6,5)
HSVTuple = Tuple[Fraction, Fraction, Fraction]
RGBTuple = Tuple[float, float, float]
def hue_to_tones(h: Fraction) -> Iterable[HSVTuple]:
for s in [Fraction(6,10)]: # optionally use range
for v in [Fraction(8,10),Fraction(5,10)]: # could use range too
yield (h, s, v) # use bias for v here if you use range
def hsv_to_rgb(x: HSVTuple) -> RGBTuple:
return colorsys.hsv_to_rgb(*map(float, x))
flatten = itertools.chain.from_iterable
def hsvs() -> Iterable[HSVTuple]:
return flatten(map(hue_to_tones, fracs()))
def rgbs() -> Iterable[RGBTuple]:
return map(hsv_to_rgb, hsvs())
def rgb_to_css(x: RGBTuple) -> str:
uint8tuple = map(lambda y: int(y*255), x)
return "rgb({},{},{})".format(*uint8tuple)
def css_colors() -> Iterable[str]:
return map(rgb_to_css, rgbs())
if __name__ == "__main__":
# sample 100 colors in css format
sample_colors = list(itertools.islice(css_colors(), 100))
pprint(sample_colors)
For the sake of generations to come I add here the accepted answer in Python.
import numpy as np
import colorsys
def _get_colors(num_colors):
colors=[]
for i in np.arange(0., 360., 360. / num_colors):
hue = i/360.
lightness = (50 + np.random.rand() * 10)/100.
saturation = (90 + np.random.rand() * 10)/100.
colors.append(colorsys.hls_to_rgb(hue, lightness, saturation))
return colors
Here's an idea. Imagine an HSV cylinder
Define the upper and lower limits you want for the Brightness and Saturation. This defines a square cross section ring within the space.
Now, scatter N points randomly within this space.
Then apply an iterative repulsion algorithm on them, either for a fixed number of iterations, or until the points stabilise.
Now you should have N points representing N colours that are about as different as possible within the colour space you're interested in.
Hugo
Everyone seems to have missed the existence of the very useful YUV color space which was designed to represent perceived color differences in the human visual system. Distances in YUV represent differences in human perception. I needed this functionality for MagicCube4D which implements 4-dimensional Rubik's cubes and an unlimited numbers of other 4D twisty puzzles having arbitrary numbers of faces.
My solution starts by selecting random points in YUV and then iteratively breaking up the closest two points, and only converting to RGB when returning the result. The method is O(n^3) but that doesn't matter for small numbers or ones that can be cached. It can certainly be made more efficient but the results appear to be excellent.
The function allows for optional specification of brightness thresholds so as not to produce colors in which no component is brighter or darker than given amounts. IE you may not want values close to black or white. This is useful when the resulting colors will be used as base colors that are later shaded via lighting, layering, transparency, etc. and must still appear different from their base colors.
import java.awt.Color;
import java.util.Random;
/**
* Contains a method to generate N visually distinct colors and helper methods.
*
* #author Melinda Green
*/
public class ColorUtils {
private ColorUtils() {} // To disallow instantiation.
private final static float
U_OFF = .436f,
V_OFF = .615f;
private static final long RAND_SEED = 0;
private static Random rand = new Random(RAND_SEED);
/*
* Returns an array of ncolors RGB triplets such that each is as unique from the rest as possible
* and each color has at least one component greater than minComponent and one less than maxComponent.
* Use min == 1 and max == 0 to include the full RGB color range.
*
* Warning: O N^2 algorithm blows up fast for more than 100 colors.
*/
public static Color[] generateVisuallyDistinctColors(int ncolors, float minComponent, float maxComponent) {
rand.setSeed(RAND_SEED); // So that we get consistent results for each combination of inputs
float[][] yuv = new float[ncolors][3];
// initialize array with random colors
for(int got = 0; got < ncolors;) {
System.arraycopy(randYUVinRGBRange(minComponent, maxComponent), 0, yuv[got++], 0, 3);
}
// continually break up the worst-fit color pair until we get tired of searching
for(int c = 0; c < ncolors * 1000; c++) {
float worst = 8888;
int worstID = 0;
for(int i = 1; i < yuv.length; i++) {
for(int j = 0; j < i; j++) {
float dist = sqrdist(yuv[i], yuv[j]);
if(dist < worst) {
worst = dist;
worstID = i;
}
}
}
float[] best = randYUVBetterThan(worst, minComponent, maxComponent, yuv);
if(best == null)
break;
else
yuv[worstID] = best;
}
Color[] rgbs = new Color[yuv.length];
for(int i = 0; i < yuv.length; i++) {
float[] rgb = new float[3];
yuv2rgb(yuv[i][0], yuv[i][1], yuv[i][2], rgb);
rgbs[i] = new Color(rgb[0], rgb[1], rgb[2]);
//System.out.println(rgb[i][0] + "\t" + rgb[i][1] + "\t" + rgb[i][2]);
}
return rgbs;
}
public static void hsv2rgb(float h, float s, float v, float[] rgb) {
// H is given on [0->6] or -1. S and V are given on [0->1].
// RGB are each returned on [0->1].
float m, n, f;
int i;
float[] hsv = new float[3];
hsv[0] = h;
hsv[1] = s;
hsv[2] = v;
System.out.println("H: " + h + " S: " + s + " V:" + v);
if(hsv[0] == -1) {
rgb[0] = rgb[1] = rgb[2] = hsv[2];
return;
}
i = (int) (Math.floor(hsv[0]));
f = hsv[0] - i;
if(i % 2 == 0)
f = 1 - f; // if i is even
m = hsv[2] * (1 - hsv[1]);
n = hsv[2] * (1 - hsv[1] * f);
switch(i) {
case 6:
case 0:
rgb[0] = hsv[2];
rgb[1] = n;
rgb[2] = m;
break;
case 1:
rgb[0] = n;
rgb[1] = hsv[2];
rgb[2] = m;
break;
case 2:
rgb[0] = m;
rgb[1] = hsv[2];
rgb[2] = n;
break;
case 3:
rgb[0] = m;
rgb[1] = n;
rgb[2] = hsv[2];
break;
case 4:
rgb[0] = n;
rgb[1] = m;
rgb[2] = hsv[2];
break;
case 5:
rgb[0] = hsv[2];
rgb[1] = m;
rgb[2] = n;
break;
}
}
// From http://en.wikipedia.org/wiki/YUV#Mathematical_derivations_and_formulas
public static void yuv2rgb(float y, float u, float v, float[] rgb) {
rgb[0] = 1 * y + 0 * u + 1.13983f * v;
rgb[1] = 1 * y + -.39465f * u + -.58060f * v;
rgb[2] = 1 * y + 2.03211f * u + 0 * v;
}
public static void rgb2yuv(float r, float g, float b, float[] yuv) {
yuv[0] = .299f * r + .587f * g + .114f * b;
yuv[1] = -.14713f * r + -.28886f * g + .436f * b;
yuv[2] = .615f * r + -.51499f * g + -.10001f * b;
}
private static float[] randYUVinRGBRange(float minComponent, float maxComponent) {
while(true) {
float y = rand.nextFloat(); // * YFRAC + 1-YFRAC);
float u = rand.nextFloat() * 2 * U_OFF - U_OFF;
float v = rand.nextFloat() * 2 * V_OFF - V_OFF;
float[] rgb = new float[3];
yuv2rgb(y, u, v, rgb);
float r = rgb[0], g = rgb[1], b = rgb[2];
if(0 <= r && r <= 1 &&
0 <= g && g <= 1 &&
0 <= b && b <= 1 &&
(r > minComponent || g > minComponent || b > minComponent) && // don't want all dark components
(r < maxComponent || g < maxComponent || b < maxComponent)) // don't want all light components
return new float[]{y, u, v};
}
}
private static float sqrdist(float[] a, float[] b) {
float sum = 0;
for(int i = 0; i < a.length; i++) {
float diff = a[i] - b[i];
sum += diff * diff;
}
return sum;
}
private static double worstFit(Color[] colors) {
float worst = 8888;
float[] a = new float[3], b = new float[3];
for(int i = 1; i < colors.length; i++) {
colors[i].getColorComponents(a);
for(int j = 0; j < i; j++) {
colors[j].getColorComponents(b);
float dist = sqrdist(a, b);
if(dist < worst) {
worst = dist;
}
}
}
return Math.sqrt(worst);
}
private static float[] randYUVBetterThan(float bestDistSqrd, float minComponent, float maxComponent, float[][] in) {
for(int attempt = 1; attempt < 100 * in.length; attempt++) {
float[] candidate = randYUVinRGBRange(minComponent, maxComponent);
boolean good = true;
for(int i = 0; i < in.length; i++)
if(sqrdist(candidate, in[i]) < bestDistSqrd)
good = false;
if(good)
return candidate;
}
return null; // after a bunch of passes, couldn't find a candidate that beat the best.
}
/**
* Simple example program.
*/
public static void main(String[] args) {
final int ncolors = 10;
Color[] colors = generateVisuallyDistinctColors(ncolors, .8f, .3f);
for(int i = 0; i < colors.length; i++) {
System.out.println(colors[i].toString());
}
System.out.println("Worst fit color = " + worstFit(colors));
}
}
HSL color model may be well suited for "sorting" colors, but if you are looking for visually distinct colors you definitively need Lab color model instead.
CIELAB was designed to be perceptually uniform with respect to human color vision, meaning that the same amount of numerical change in these values corresponds to about the same amount of visually perceived change.
Once you know that, finding the optimal subset of N colors from a wide range of colors is still a (NP) hard problem, kind of similar to the Travelling salesman problem and all the solutions using k-mean algorithms or something won't really help.
That said, if N is not too big and if you start with a limited set of colors, you will easily find a very good subset of distincts colors according to a Lab distance with a simple random function.
I've coded such a tool for my own usage (you can find it here: https://mokole.com/palette.html), here is what I got for N=7:
It's all javascript so feel free to take a look on the source of the page and adapt it for your own needs.
A lot of very nice answers up there, but it might be useful to mention the python package distinctify in case someone is looking for a quick python solution. It is a lightweight package available from pypi that is very straightforward to use:
from distinctipy import distinctipy
colors = distinctipy.get_colors(12)
print(colors)
# display the colours
distinctipy.color_swatch(colors)
It returns a list of rgb tuples
[(0, 1, 0), (1, 0, 1), (0, 0.5, 1), (1, 0.5, 0), (0.5, 0.75, 0.5), (0.4552518132842178, 0.12660764790179446, 0.5467915225460569), (1, 0, 0), (0.12076092516775849, 0.9942188027771208, 0.9239958090462229), (0.254747094970068, 0.4768020779917903, 0.02444859177890535), (0.7854526395841417, 0.48630704929211144, 0.9902480906347156), (0, 0, 1), (1, 1, 0)]
Also it has some additional nice functionalities such as generating colors that are distinct from an existing list of colors.
Here's a solution to managed your "distinct" issue, which is entirely overblown:
Create a unit sphere and drop points on it with repelling charges. Run a particle system until they no longer move (or the delta is "small enough"). At this point, each of the points are as far away from each other as possible. Convert (x, y, z) to rgb.
I mention it because for certain classes of problems, this type of solution can work better than brute force.
I originally saw this approach here for tesselating a sphere.
Again, the most obvious solutions of traversing HSL space or RGB space will probably work just fine.
We just need a range of RGB triplet pairs with the maximum amount of distance between these triplets.
We can define a simple linear ramp, and then resize that ramp to get the desired number of colors.
In python:
from skimage.transform import resize
import numpy as np
def distinguishable_colors(n, shuffle = True,
sinusoidal = False,
oscillate_tone = False):
ramp = ([1, 0, 0],[1,1,0],[0,1,0],[0,0,1], [1,0,1]) if n>3 else ([1,0,0], [0,1,0],[0,0,1])
coltrio = np.vstack(ramp)
colmap = np.round(resize(coltrio, [n,3], preserve_range=True,
order = 1 if n>3 else 3
, mode = 'wrap'),3)
if sinusoidal: colmap = np.sin(colmap*np.pi/2)
colmap = [colmap[x,] for x in range(colmap.shape[0])]
if oscillate_tone:
oscillate = [0,1]*round(len(colmap)/2+.5)
oscillate = [np.array([osc,osc,osc]) for osc in oscillate]
colmap = [.8*colmap[x] + .2*oscillate[x] for x in range(len(colmap))]
#Whether to shuffle the output colors
if shuffle:
random.seed(1)
random.shuffle(colmap)
return colmap
I would try to fix saturation and lumination to maximum and focus on hue only. As I see it, H can go from 0 to 255 and then wraps around. Now if you wanted two contrasting colours you would take the opposite sides of this ring, i.e. 0 and 128. If you wanted 4 colours, you would take some separated by 1/4 of the 256 length of the circle, i.e. 0, 64,128,192. And of course, as others suggested when you need N colours, you could just separate them by 256/N.
What I would add to this idea is to use a reversed representation of a binary number to form this sequence. Look at this:
0 = 00000000 after reversal is 00000000 = 0
1 = 00000001 after reversal is 10000000 = 128
2 = 00000010 after reversal is 01000000 = 64
3 = 00000011 after reversal is 11000000 = 192
...
this way if you need N different colours you could just take first N numbers, reverse them, and you get as much distant points as possible (for N being power of two) while at the same time preserving that each prefix of the sequence differs a lot.
This was an important goal in my use case, as I had a chart where colors were sorted by area covered by this colour. I wanted the largest areas of the chart to have large contrast, and I was ok with some small areas to have colours similar to those from top 10, as it was obvious for the reader which one is which one by just observing the area.
This is trivial in MATLAB (there is an hsv command):
cmap = hsv(number_of_colors)
I have written a package for R called qualpalr that is designed specifically for this purpose. I recommend you look at the vignette to find out how it works, but I will try to summarize the main points.
qualpalr takes a specification of colors in the HSL color space (which was described previously in this thread), projects it to the DIN99d color space (which is perceptually uniform) and find the n that maximize the minimum distance between any oif them.
# Create a palette of 4 colors of hues from 0 to 360, saturations between
# 0.1 and 0.5, and lightness from 0.6 to 0.85
pal <- qualpal(n = 4, list(h = c(0, 360), s = c(0.1, 0.5), l = c(0.6, 0.85)))
# Look at the colors in hex format
pal$hex
#> [1] "#6F75CE" "#CC6B76" "#CAC16A" "#76D0D0"
# Create a palette using one of the predefined color subspaces
pal2 <- qualpal(n = 4, colorspace = "pretty")
# Distance matrix of the DIN99d color differences
pal2$de_DIN99d
#> #69A3CC #6ECC6E #CA6BC4
#> 6ECC6E 22
#> CA6BC4 21 30
#> CD976B 24 21 21
plot(pal2)
I think this simple recursive algorithm complementes the accepted answer, in order to generate distinct hue values. I made it for hsv, but can be used for other color spaces too.
It generates hues in cycles, as separate as possible to each other in each cycle.
/**
* 1st cycle: 0, 120, 240
* 2nd cycle (+60): 60, 180, 300
* 3th cycle (+30): 30, 150, 270, 90, 210, 330
* 4th cycle (+15): 15, 135, 255, 75, 195, 315, 45, 165, 285, 105, 225, 345
*/
public static float recursiveHue(int n) {
// if 3: alternates red, green, blue variations
float firstCycle = 3;
// First cycle
if (n < firstCycle) {
return n * 360f / firstCycle;
}
// Each cycle has as much values as all previous cycles summed (powers of 2)
else {
// floor of log base 2
int numCycles = (int)Math.floor(Math.log(n / firstCycle) / Math.log(2));
// divDown stores the larger power of 2 that is still lower than n
int divDown = (int)(firstCycle * Math.pow(2, numCycles));
// same hues than previous cycle, but summing an offset (half than previous cycle)
return recursiveHue(n % divDown) + 180f / divDown;
}
}
I was unable to find this kind of algorithm here. I hope it helps, it's my first post here.
Pretty neat with seaborn for Python users:
>>> import seaborn as sns
>>> sns.color_palette(n_colors=4)
it returns list of RGB tuples:
[(0.12156862745098039, 0.4666666666666667, 0.7058823529411765),
(1.0, 0.4980392156862745, 0.054901960784313725),
(0.17254901960784313, 0.6274509803921569, 0.17254901960784313),
(0.8392156862745098, 0.15294117647058825, 0.1568627450980392)]
Janus's answer but easier to read. I've also adjusted the colorscheme slightly and marked where you can modify for yourself
I've made this a snippet to be directly pasted into a jupyter notebook.
import colorsys
import itertools
from fractions import Fraction
from IPython.display import HTML as html_print
def infinite_hues():
yield Fraction(0)
for k in itertools.count():
i = 2**k # zenos_dichotomy
for j in range(1,i,2):
yield Fraction(j,i)
def hue_to_hsvs(h: Fraction):
# tweak values to adjust scheme
for s in [Fraction(6,10)]:
for v in [Fraction(6,10), Fraction(9,10)]:
yield (h, s, v)
def rgb_to_css(rgb) -> str:
uint8tuple = map(lambda y: int(y*255), rgb)
return "rgb({},{},{})".format(*uint8tuple)
def css_to_html(css):
return f"<text style=background-color:{css}> </text>"
def show_colors(n=33):
hues = infinite_hues()
hsvs = itertools.chain.from_iterable(hue_to_hsvs(hue) for hue in hues)
rgbs = (colorsys.hsv_to_rgb(*hsv) for hsv in hsvs)
csss = (rgb_to_css(rgb) for rgb in rgbs)
htmls = (css_to_html(css) for css in csss)
myhtmls = itertools.islice(htmls, n)
display(html_print("".join(myhtmls)))
show_colors()
If N is big enough, you're going to get some similar-looking colors. There's only so many of them in the world.
Why not just evenly distribute them through the spectrum, like so:
IEnumerable<Color> CreateUniqueColors(int nColors)
{
int subdivision = (int)Math.Floor(Math.Pow(nColors, 1/3d));
for(int r = 0; r < 255; r += subdivision)
for(int g = 0; g < 255; g += subdivision)
for(int b = 0; b < 255; b += subdivision)
yield return Color.FromArgb(r, g, b);
}
If you want to mix up the sequence so that similar colors aren't next to each other, you could maybe shuffle the resulting list.
Am I underthinking this?
This OpenCV function uses the HSV color model to generate n evenly distributed colors around the 0<=H<=360º with maximum S=1.0 and V=1.0. The function outputs the BGR colors in bgr_mat:
void distributed_colors (int n, cv::Mat_<cv::Vec3f> & bgr_mat) {
cv::Mat_<cv::Vec3f> hsv_mat(n,CV_32F,cv::Vec3f(0.0,1.0,1.0));
double step = 360.0/n;
double h= 0.0;
cv::Vec3f value;
for (int i=0;i<n;i++,h+=step) {
value = hsv_mat.at<cv::Vec3f>(i);
hsv_mat.at<cv::Vec3f>(i)[0] = h;
}
cv::cvtColor(hsv_mat, bgr_mat, CV_HSV2BGR);
bgr_mat *= 255;
}
This generates the same colors as Janus Troelsen's solution. But instead of generators, it is using start/stop semantics. It's also fully vectorized.
import numpy as np
import numpy.typing as npt
import matplotlib.colors
def distinct_colors(start: int=0, stop: int=20) -> npt.NDArray[np.float64]:
"""Returns an array of distinct RGB colors, from an infinite sequence of colors
"""
if stop <= start: # empty interval; return empty array
return np.array([], dtype=np.float64)
sat_values = [6/10] # other tones could be added
val_values = [8/10, 5/10] # other tones could be added
colors_per_hue_value = len(sat_values) * len(val_values)
# Get the start and stop indices within the hue value stream that are needed
# to achieve the requested range
hstart = start // colors_per_hue_value
hstop = (stop+colors_per_hue_value-1) // colors_per_hue_value
# Zero will cause a singularity in the caluculation, so we will add the zero
# afterwards
prepend_zero = hstart==0
# Sequence (if hstart=1): 1,2,...,hstop-1
i = np.arange(1 if prepend_zero else hstart, hstop)
# The following yields (if hstart is 1): 1/2, 1/4, 3/4, 1/8, 3/8, 5/8, 7/8,
# 1/16, 3/16, ...
hue_values = (2*i+1) / np.power(2,np.floor(np.log2(i*2))) - 1
if prepend_zero:
hue_values = np.concatenate(([0], hue_values))
# Make all combinations of h, s and v values, as if done by a nested loop
# in that order
hsv = np.array(np.meshgrid(hue_values, sat_values, val_values, indexing='ij')
).reshape((3,-1)).transpose()
# Select the requested range (only the necessary values were computed but we
# need to adjust the indices since start & stop are not necessarily multiples
# of colors_per_hue_value)
hsv = hsv[start % colors_per_hue_value :
start % colors_per_hue_value + stop - start]
# Use the matplotlib vectorized function to convert hsv to rgb
return matplotlib.colors.hsv_to_rgb(hsv)
Samples:
from matplotlib.colors import ListedColormap
ListedColormap(distinct_colors(stop=20))
ListedColormap(distinct_colors(start=30, stop=50))
Given a system (a website for instance) that lets a user customize the background color for some section but not the font color (to keep number of options to a minimum), is there a way to programmatically determine if a "light" or "dark" font color is necessary?
I'm sure there is some algorithm, but I don't know enough about colors, luminosity, etc to figure it out on my own.
I encountered similar problem. I had to find a good method of selecting contrastive font color to display text labels on colorscales/heatmaps. It had to be universal method and generated color had to be "good looking", which means that simple generating complementary color was not good solution - sometimes it generated strange, very intensive colors that were hard to watch and read.
After long hours of testing and trying to solve this problem, I found out that the best solution is to select white font for "dark" colors, and black font for "bright" colors.
Here's an example of function I am using in C#:
Color ContrastColor(Color color)
{
int d = 0;
// Counting the perceptive luminance - human eye favors green color...
double luminance = (0.299 * color.R + 0.587 * color.G + 0.114 * color.B)/255;
if (luminance > 0.5)
d = 0; // bright colors - black font
else
d = 255; // dark colors - white font
return Color.FromArgb(d, d, d);
}
This was tested for many various colorscales (rainbow, grayscale, heat, ice, and many others) and is the only "universal" method I found out.
Edit
Changed the formula of counting a to "perceptive luminance" - it really looks better! Already implemented it in my software, looks great.
Edit 2
#WebSeed provided a great working example of this algorithm: http://codepen.io/WebSeed/full/pvgqEq/
Based on Gacek's answer but directly returning color constants (additional modifications see below):
public Color ContrastColor(Color iColor)
{
// Calculate the perceptive luminance (aka luma) - human eye favors green color...
double luma = ((0.299 * iColor.R) + (0.587 * iColor.G) + (0.114 * iColor.B)) / 255;
// Return black for bright colors, white for dark colors
return luma > 0.5 ? Color.Black : Color.White;
}
Note: I removed the inversion of the luma value to make bright colors have a higher value, what seems more natural to me and is also the 'default' calculation method.
(Edit: This has since been adopted in the original answer, too)
I used the same constants as Gacek from here since they worked great for me.
You can also implement this as an Extension Method using the following signature:
public static Color ContrastColor(this Color iColor)
You can then easily call it via
foregroundColor = backgroundColor.ContrastColor().
Thank you #Gacek. Here's a version for Android:
#ColorInt
public static int getContrastColor(#ColorInt int color) {
// Counting the perceptive luminance - human eye favors green color...
double a = 1 - (0.299 * Color.red(color) + 0.587 * Color.green(color) + 0.114 * Color.blue(color)) / 255;
int d;
if (a < 0.5) {
d = 0; // bright colors - black font
} else {
d = 255; // dark colors - white font
}
return Color.rgb(d, d, d);
}
And an improved (shorter) version:
#ColorInt
public static int getContrastColor(#ColorInt int color) {
// Counting the perceptive luminance - human eye favors green color...
double a = 1 - (0.299 * Color.red(color) + 0.587 * Color.green(color) + 0.114 * Color.blue(color)) / 255;
return a < 0.5 ? Color.BLACK : Color.WHITE;
}
My Swift implementation of Gacek's answer:
func contrastColor(color: UIColor) -> UIColor {
var d = CGFloat(0)
var r = CGFloat(0)
var g = CGFloat(0)
var b = CGFloat(0)
var a = CGFloat(0)
color.getRed(&r, green: &g, blue: &b, alpha: &a)
// Counting the perceptive luminance - human eye favors green color...
let luminance = 1 - ((0.299 * r) + (0.587 * g) + (0.114 * b))
if luminance < 0.5 {
d = CGFloat(0) // bright colors - black font
} else {
d = CGFloat(1) // dark colors - white font
}
return UIColor( red: d, green: d, blue: d, alpha: a)
}
Javascript [ES2015]
const hexToLuma = (colour) => {
const hex = colour.replace(/#/, '');
const r = parseInt(hex.substr(0, 2), 16);
const g = parseInt(hex.substr(2, 2), 16);
const b = parseInt(hex.substr(4, 2), 16);
return [
0.299 * r,
0.587 * g,
0.114 * b
].reduce((a, b) => a + b) / 255;
};
Ugly Python if you don't feel like writing it :)
'''
Input a string without hash sign of RGB hex digits to compute
complementary contrasting color such as for fonts
'''
def contrasting_text_color(hex_str):
(r, g, b) = (hex_str[:2], hex_str[2:4], hex_str[4:])
return '000' if 1 - (int(r, 16) * 0.299 + int(g, 16) * 0.587 + int(b, 16) * 0.114) / 255 < 0.5 else 'fff'
Thanks for this post.
For whoever might be interested, here's an example of that function in Delphi:
function GetContrastColor(ABGColor: TColor): TColor;
var
ADouble: Double;
R, G, B: Byte;
begin
if ABGColor <= 0 then
begin
Result := clWhite;
Exit; // *** EXIT RIGHT HERE ***
end;
if ABGColor = clWhite then
begin
Result := clBlack;
Exit; // *** EXIT RIGHT HERE ***
end;
// Get RGB from Color
R := GetRValue(ABGColor);
G := GetGValue(ABGColor);
B := GetBValue(ABGColor);
// Counting the perceptive luminance - human eye favors green color...
ADouble := 1 - (0.299 * R + 0.587 * G + 0.114 * B) / 255;
if (ADouble < 0.5) then
Result := clBlack // bright colors - black font
else
Result := clWhite; // dark colors - white font
end;
This is such a helpful answer. Thanks for it!
I'd like to share an SCSS version:
#function is-color-light( $color ) {
// Get the components of the specified color
$red: red( $color );
$green: green( $color );
$blue: blue( $color );
// Compute the perceptive luminance, keeping
// in mind that the human eye favors green.
$l: 1 - ( 0.299 * $red + 0.587 * $green + 0.114 * $blue ) / 255;
#return ( $l < 0.5 );
}
Now figuring out how to use the algorithm to auto-create hover colors for menu links. Light headers get a darker hover, and vice-versa.
Short Answer:
Calculate the luminance (Y) of the given color, and flip the text either black or white based on a pre-determined middle contrast figure. For a typical sRGB display, flip to white when Y < 0.4 (i.e. 40%)
Longer Answer
Not surprisingly, nearly every answer here presents some misunderstanding, and/or is quoting incorrect coefficients. The only answer that is actually close is that of Seirios, though it relies on WCAG 2 contrast which is known to be incorrect itself.
If I say "not surprisingly", it is due in part to the massive amount of misinformation on the internet on this particular subject. The fact this field is still a subject of active research and unsettled science adds to the fun. I come to this conclusion as the result of the last few years of research into a new contrast prediction method for readability.
The field of visual perception is dense and abstract, as well as developing, so it is common for misunderstandings to exist. For instance, HSV and HSL are not even close to perceptually accurate. For that you need a perceptually uniform model such as CIELAB or CIELUV or CIECAM02 etc.
Some misunderstandings have even made their way into standards, such as the contrast part of WCAG 2 (1.4.3), which has been demonstrated as incorrect over much of its range.
First Fix:
The coefficients shown in many answers here are (.299, .587, .114) and are wrong, as they pertain to a long obsolete system known as NTSC YIQ, the analog broadcast system in North America some decades ago. While they may still be used in some YCC encoding specs for backwards compatibility, they should not be used in an sRGB context.
The coefficients for sRGB and Rec.709 (HDTV) are:
Red: 0.2126
Green: 0.7152
Blue: 0.0722
Other color spaces like Rec2020 or AdobeRGB use different coefficients, and it is important to use the correct coefficients for a given color space.
The coefficients can not be applied directly to 8 bit sRGB encoded image or color data. The encoded data must first be linearized, then the coefficients applied to find the luminance (light value) of the given pixel or color.
For sRGB there is a piecewise transform, but as we are only interested in the perceived lightness contrast to find the point to "flip" the text from black to white, we can take a shortcut via the simple gamma method.
Andy's Shortcut to Luminance & Lightness
Divide each sRGB color by 255.0, then raise to the power of 2.2, then multiply by the coefficients and sum them to find estimated luminance.
let Ys = Math.pow(sR/255.0,2.2) * 0.2126 +
Math.pow(sG/255.0,2.2) * 0.7152 +
Math.pow(sB/255.0,2.2) * 0.0722; // Andy's Easy Luminance for sRGB. For Rec709 HDTV change the 2.2 to 2.4
Here, Y is the relative luminance from an sRGB monitor, on a 0.0 to 1.0 scale. This is not relative to perception though, and we need further transforms to fit our human visual perception of the relative lightness, and also of the perceived contrast.
The 40% Flip
But before we get there, if you are only looking for a basic point to flip the text from black to white or vice versa, the cheat is to use the Y we just derived, and make the flip point about Y = 0.40;. so for colors higher than 0.4 Y, make the text black #000 and for colors darker than 0.4 Y, make the text white #fff.
let textColor = (Ys < 0.4) ? "#fff" : "#000"; // Low budget down and dirty text flipper.
Why 40% and not 50%? Our human perception of lightness/darkness and of contrast is not linear. For a self illuminated display, it so happens that 0.4 Y is about middle contrast under most typical conditions.
Yes it varies, and yes this is an over simplification. But if you are flipping text black or white, the simple answer is a useful one.
Perceptual Bonus Round
Predicting the perception of a given color and lightness is still a subject of active research, and not entirely settled science. The L* (Lstar) of CIELAB or LUV has been used to predict perceptual lightness, and even to predict perceived contrast. However, L* works well for surface colors in a very defined/controlled environment, and does not work as well for self illuminated displays.
While this varies depending on not only the display type and calibration, but also your environment and the overall page content, if you take the Y from above, and raise it by around ^0.685 to ^0.75, you'll find that 0.5 is typically the middle point to flip the text from white to black.
let textColor = (Math.pow(Ys,0.75) < 0.5) ? "#fff" : "#000"; // perceptually based text flipper.
Using the exponent 0.685 will make the text color swap on a darker color, and using 0.8 will make the text swap on a lighter color.
Spatial Frequency Double Bonus Round
It is useful to note that contrast is NOT just the distance between two colors. Spatial frequency, in other words font weight and size, are also CRITICAL factors that cannot be ignored.
That said, you may find that when colors are in the midrange, that you'd want to increase the size and or weight of the font.
let textSize = "16px";
let textWeight = "normal";
let Ls = Math.pow(Ys,0.7);
if (Ls > 0.33 && Ls < 0.66) {
textSize = "18px";
textWeight = "bold";
} // scale up fonts for the lower contrast mid luminances.
Hue R U
It's outside the scope of this post to delve deeply, but above we are ignoring hue and chroma. Hue and chroma do have an effect, such as Helmholtz Kohlrausch, and the simpler luminance calculations above do not always predict intensity due to saturated hues.
To predict these more subtle aspects of perception, a complete appearance model is needed. R. Hunt, M. Fairshild, E. Burns are a few authors worth looking into if you want to plummet down the rabbit hole of human visual perception...
For this narrow purpose, we could re-weight the coefficients slightly, knowing that green makes up the majority of of luminance, and pure blue and pure red should always be the darkest of two colors. What tends to happen using the standard coefficients, is middle colors with a lot of blue or red may flip to black at a lower than ideal luminance, and colors with a high green component may do the opposite.
That said, I find this is best addressed by increasing font size and weight in the middle colors.
Putting it all together
So we'll assume you'll send this function a hex string, and it will return a style string that can be sent to a particular HTML element.
Check out the CODEPEN, inspired by the one Seirios did:
CodePen: Fancy Font Flipping
One of the things the Codepen code does is increase the text size for the lower contrast midrange. Here's a sample:
And if you want to play around with some of these concepts, see the SAPC development site at https://www.myndex.com/SAPC/ clicking on "research mode" provides interactive experiments to demonstrate these concepts.
Terms of enlightenment
Luminance: Y (relative) or L (absolute cd/m2) a spectrally weighted but otherwise linear measure of light. Not to be confused with "Luminosity".
Luminosity: light over time, useful in astronomy.
Lightness: L* (Lstar) perceptual lightness as defined by the CIE. Some models have a related lightness J*.
I had the same problem but i had to develop it in PHP. I used #Garek's solution and i also used this answer:
Convert hex color to RGB values in PHP to convert HEX color code to RGB.
So i'm sharing it.
I wanted to use this function with given Background HEX color, but not always starting from '#'.
//So it can be used like this way:
$color = calculateColor('#804040');
echo $color;
//or even this way:
$color = calculateColor('D79C44');
echo '<br/>'.$color;
function calculateColor($bgColor){
//ensure that the color code will not have # in the beginning
$bgColor = str_replace('#','',$bgColor);
//now just add it
$hex = '#'.$bgColor;
list($r, $g, $b) = sscanf($hex, "#%02x%02x%02x");
$color = 1 - ( 0.299 * $r + 0.587 * $g + 0.114 * $b)/255;
if ($color < 0.5)
$color = '#000000'; // bright colors - black font
else
$color = '#ffffff'; // dark colors - white font
return $color;
}
Flutter implementation
Color contrastColor(Color color) {
if (color == Colors.transparent || color.alpha < 50) {
return Colors.black;
}
double luminance = (0.299 * color.red + 0.587 * color.green + 0.114 * color.blue) / 255;
return luminance > 0.5 ? Colors.black : Colors.white;
}
Based on Gacek's answer, and after analyzing #WebSeed's example with the WAVE browser extension, I've come up with the following version that chooses black or white text based on contrast ratio (as defined in W3C's Web Content Accessibility Guidelines (WCAG) 2.1), instead of luminance.
This is the code (in javascript):
// As defined in WCAG 2.1
var relativeLuminance = function (R8bit, G8bit, B8bit) {
var RsRGB = R8bit / 255.0;
var GsRGB = G8bit / 255.0;
var BsRGB = B8bit / 255.0;
var R = (RsRGB <= 0.03928) ? RsRGB / 12.92 : Math.pow((RsRGB + 0.055) / 1.055, 2.4);
var G = (GsRGB <= 0.03928) ? GsRGB / 12.92 : Math.pow((GsRGB + 0.055) / 1.055, 2.4);
var B = (BsRGB <= 0.03928) ? BsRGB / 12.92 : Math.pow((BsRGB + 0.055) / 1.055, 2.4);
return 0.2126 * R + 0.7152 * G + 0.0722 * B;
};
var blackContrast = function(r, g, b) {
var L = relativeLuminance(r, g, b);
return (L + 0.05) / 0.05;
};
var whiteContrast = function(r, g, b) {
var L = relativeLuminance(r, g, b);
return 1.05 / (L + 0.05);
};
// If both options satisfy AAA criterion (at least 7:1 contrast), use preference
// else, use higher contrast (white breaks tie)
var chooseFGcolor = function(r, g, b, prefer = 'white') {
var Cb = blackContrast(r, g, b);
var Cw = whiteContrast(r, g, b);
if(Cb >= 7.0 && Cw >= 7.0) return prefer;
else return (Cb > Cw) ? 'black' : 'white';
};
A working example may be found in my fork of #WebSeed's codepen, which produces zero low contrast errors in WAVE.
As Kotlin / Android extension:
fun Int.getContrastColor(): Int {
// Counting the perceptive luminance - human eye favors green color...
val a = 1 - (0.299 * Color.red(this) + 0.587 * Color.green(this) + 0.114 * Color.blue(this)) / 255
return if (a < 0.5) Color.BLACK else Color.WHITE
}
An implementation for objective-c
+ (UIColor*) getContrastColor:(UIColor*) color {
CGFloat red, green, blue, alpha;
[color getRed:&red green:&green blue:&blue alpha:&alpha];
double a = ( 0.299 * red + 0.587 * green + 0.114 * blue);
return (a > 0.5) ? [[UIColor alloc]initWithRed:0 green:0 blue:0 alpha:1] : [[UIColor alloc]initWithRed:255 green:255 blue:255 alpha:1];
}
iOS Swift 3.0 (UIColor extension):
func isLight() -> Bool
{
if let components = self.cgColor.components, let firstComponentValue = components[0], let secondComponentValue = components[1], let thirdComponentValue = components[2] {
let firstComponent = (firstComponentValue * 299)
let secondComponent = (secondComponentValue * 587)
let thirdComponent = (thirdComponentValue * 114)
let brightness = (firstComponent + secondComponent + thirdComponent) / 1000
if brightness < 0.5
{
return false
}else{
return true
}
}
print("Unable to grab components and determine brightness")
return nil
}
Swift 4 Example:
extension UIColor {
var isLight: Bool {
let components = cgColor.components
let firstComponent = ((components?[0]) ?? 0) * 299
let secondComponent = ((components?[1]) ?? 0) * 587
let thirdComponent = ((components?[2]) ?? 0) * 114
let brightness = (firstComponent + secondComponent + thirdComponent) / 1000
return !(brightness < 0.6)
}
}
UPDATE - Found that 0.6 was a better test bed for the query
Note there is an algorithm for this in the google closure library that references a w3c recommendation: http://www.w3.org/TR/AERT#color-contrast. However, in this API you provide a list of suggested colors as a starting point.
/**
* Find the "best" (highest-contrast) of the suggested colors for the prime
* color. Uses W3C formula for judging readability and visual accessibility:
* http://www.w3.org/TR/AERT#color-contrast
* #param {goog.color.Rgb} prime Color represented as a rgb array.
* #param {Array<goog.color.Rgb>} suggestions Array of colors,
* each representing a rgb array.
* #return {!goog.color.Rgb} Highest-contrast color represented by an array.
*/
goog.color.highContrast = function(prime, suggestions) {
var suggestionsWithDiff = [];
for (var i = 0; i < suggestions.length; i++) {
suggestionsWithDiff.push({
color: suggestions[i],
diff: goog.color.yiqBrightnessDiff_(suggestions[i], prime) +
goog.color.colorDiff_(suggestions[i], prime)
});
}
suggestionsWithDiff.sort(function(a, b) { return b.diff - a.diff; });
return suggestionsWithDiff[0].color;
};
/**
* Calculate brightness of a color according to YIQ formula (brightness is Y).
* More info on YIQ here: http://en.wikipedia.org/wiki/YIQ. Helper method for
* goog.color.highContrast()
* #param {goog.color.Rgb} rgb Color represented by a rgb array.
* #return {number} brightness (Y).
* #private
*/
goog.color.yiqBrightness_ = function(rgb) {
return Math.round((rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 1000);
};
/**
* Calculate difference in brightness of two colors. Helper method for
* goog.color.highContrast()
* #param {goog.color.Rgb} rgb1 Color represented by a rgb array.
* #param {goog.color.Rgb} rgb2 Color represented by a rgb array.
* #return {number} Brightness difference.
* #private
*/
goog.color.yiqBrightnessDiff_ = function(rgb1, rgb2) {
return Math.abs(
goog.color.yiqBrightness_(rgb1) - goog.color.yiqBrightness_(rgb2));
};
/**
* Calculate color difference between two colors. Helper method for
* goog.color.highContrast()
* #param {goog.color.Rgb} rgb1 Color represented by a rgb array.
* #param {goog.color.Rgb} rgb2 Color represented by a rgb array.
* #return {number} Color difference.
* #private
*/
goog.color.colorDiff_ = function(rgb1, rgb2) {
return Math.abs(rgb1[0] - rgb2[0]) + Math.abs(rgb1[1] - rgb2[1]) +
Math.abs(rgb1[2] - rgb2[2]);
};
base R version of #Gacek's answer to get luminance (you can apply your own threshold easily)
# vectorized
luminance = function(col) c(c(.299, .587, .114) %*% col2rgb(col)/255)
Usage:
luminance(c('black', 'white', '#236FAB', 'darkred', '#01F11F'))
# [1] 0.0000000 1.0000000 0.3730039 0.1629843 0.5698039
If you're manipulating color spaces for visual effect it's generally easier to work in HSL (Hue, Saturation and Lightness) than RGB. Moving colours in RGB to give naturally pleasing effects tends to be quite conceptually difficult, whereas converting into HSL, manipulating there, then converting back out again is more intuitive in concept and invariably gives better looking results.
Wikipedia has a good introduction to HSL and the closely related HSV. And there's free code around the net to do the conversion (for example here is a javascript implementation)
What precise transformation you use is a matter of taste, but personally I'd have thought reversing the Hue and Lightness components would be certain to generate a good high contrast colour as a first approximation, but you can easily go for more subtle effects.
You can have any hue text on any hue background and ensure that it is legible. I do it all the time. There's a formula for this in Javascript on Readable Text in Colour – STW*
As it says on that link, the formula is a variation on the inverse-gamma adjustment calculation, though a bit more manageable IMHO.
The menus on the right-hand side of that link and its associated pages use randomly-generated colours for text and background, always legible. So yes, clearly it can be done, no problem.
An Android variation that captures the alpha as well.
(thanks #thomas-vos)
/**
* Returns a colour best suited to contrast with the input colour.
*
* #param colour
* #return
*/
#ColorInt
public static int contrastingColour(#ColorInt int colour) {
// XXX https://stackoverflow.com/questions/1855884/determine-font-color-based-on-background-color
// Counting the perceptive luminance - human eye favors green color...
double a = 1 - (0.299 * Color.red(colour) + 0.587 * Color.green(colour) + 0.114 * Color.blue(colour)) / 255;
int alpha = Color.alpha(colour);
int d = 0; // bright colours - black font;
if (a >= 0.5) {
d = 255; // dark colours - white font
}
return Color.argb(alpha, d, d, d);
}
I would have commented on the answer by #MichaelChirico but I don't have enough reputation. So, here's an example in R with returning the colours:
get_text_colour <- function(
background_colour,
light_text_colour = 'white',
dark_text_colour = 'black',
threshold = 0.5
) {
background_luminance <- c(
c( .299, .587, .114 ) %*% col2rgb( background_colour ) / 255
)
return(
ifelse(
background_luminance < threshold,
light_text_colour,
dark_text_colour
)
)
}
> get_text_colour( background_colour = 'blue' )
[1] "white"
> get_text_colour( background_colour = c( 'blue', 'yellow', 'pink' ) )
[1] "white" "black" "black"
> get_text_colour( background_colour = c('black', 'white', '#236FAB', 'darkred', '#01F11F') )
[1] "white" "black" "white" "white" "black"
I'm looking for an algorithm to do additive color mixing for RGB values.
Is it as simple as adding the RGB values together to a max of 256?
(r1, g1, b1) + (r2, g2, b2) =
(min(r1+r2, 256), min(g1+g2, 256), min(b1+b2, 256))
To blend using alpha channels, you can use these formulas:
r = new Color();
r.A = 1 - (1 - fg.A) * (1 - bg.A);
if (r.A < 1.0e-6) return r; // Fully transparent -- R,G,B not important
r.R = fg.R * fg.A / r.A + bg.R * bg.A * (1 - fg.A) / r.A;
r.G = fg.G * fg.A / r.A + bg.G * bg.A * (1 - fg.A) / r.A;
r.B = fg.B * fg.A / r.A + bg.B * bg.A * (1 - fg.A) / r.A;
fg is the paint color. bg is the background. r is the resulting color. 1.0e-6 is just a really small number, to compensate for rounding errors.
NOTE: All variables used here are in the range [0.0, 1.0]. You have to divide or multiply by 255 if you want to use values in the range [0, 255].
For example, 50% red on top of 50% green:
// background, 50% green
var bg = new Color { R = 0.00, G = 1.00, B = 0.00, A = 0.50 };
// paint, 50% red
var fg = new Color { R = 1.00, G = 0.00, B = 0.00, A = 0.50 };
// The result
var r = new Color();
r.A = 1 - (1 - fg.A) * (1 - bg.A); // 0.75
r.R = fg.R * fg.A / r.A + bg.R * bg.A * (1 - fg.A) / r.A; // 0.67
r.G = fg.G * fg.A / r.A + bg.G * bg.A * (1 - fg.A) / r.A; // 0.33
r.B = fg.B * fg.A / r.A + bg.B * bg.A * (1 - fg.A) / r.A; // 0.00
Resulting color is: (0.67, 0.33, 0.00, 0.75), or 75% brown (or dark orange).
You could also reverse these formulas:
var bg = new Color();
if (1 - fg.A <= 1.0e-6) return null; // No result -- 'fg' is fully opaque
if (r.A - fg.A < -1.0e-6) return null; // No result -- 'fg' can't make the result more transparent
if (r.A - fg.A < 1.0e-6) return bg; // Fully transparent -- R,G,B not important
bg.A = 1 - (1 - r.A) / (1 - fg.A);
bg.R = (r.R * r.A - fg.R * fg.A) / (bg.A * (1 - fg.A));
bg.G = (r.G * r.A - fg.G * fg.A) / (bg.A * (1 - fg.A));
bg.B = (r.B * r.A - fg.B * fg.A) / (bg.A * (1 - fg.A));
or
var fg = new Color();
if (1 - bg.A <= 1.0e-6) return null; // No result -- 'bg' is fully opaque
if (r.A - bg.A < -1.0e-6) return null; // No result -- 'bg' can't make the result more transparent
if (r.A - bg.A < 1.0e-6) return bg; // Fully transparent -- R,G,B not important
fg.A = 1 - (1 - r.A) / (1 - bg.A);
fg.R = (r.R * r.A - bg.R * bg.A * (1 - fg.A)) / fg.A;
fg.G = (r.G * r.A - bg.G * bg.A * (1 - fg.A)) / fg.A;
fg.B = (r.B * r.A - bg.B * bg.A * (1 - fg.A)) / fg.A;
The formulas will calculate that background or paint color would have to be to produce the given resulting color.
If your background is opaque, the result would also be opaque. The foreground color could then take a range of values with different alpha values. For each channel (red, green and blue), you have to check which range of alphas results in valid values (0 - 1).
It depends on what you want, and it can help to see what the results are of different methods.
If you want
Red + Black = Red
Red + Green = Yellow
Red + Green + Blue = White
Red + White = White
Black + White = White
then adding with a clamp works (e.g. min(r1 + r2, 255)) This is more like the light model you've referred to.
If you want
Red + Black = Dark Red
Red + Green = Dark Yellow
Red + Green + Blue = Dark Gray
Red + White = Pink
Black + White = Gray
then you'll need to average the values (e.g. (r1 + r2) / 2) This works better for lightening/darkening colors and creating gradients.
Fun fact: Computer RGB values are derived from the square root of photon flux. So as a general function, your math should take that into account. The general function for this for a given channel is:
blendColorValue(a, b, t)
return sqrt((1 - t) * a^2 + t * b^2)
Where a and b are the colors to blend, and t is a number from 0-1 representing the point in the blend you want between a and b.
The alpha channel is different; it doesn't represent photon intensity, just the percent of background that should show through; so when blending alpha values, the linear average is enough:
blendAlphaValue(a, b, t)
return (1-t)*a + t*b;
So, to handle blending two colors, using those two functions, the following pseudocode should do you good:
blendColors(c1, c2, t)
ret
[r, g, b].each n ->
ret[n] = blendColorValue(c1[n], c2[n], t)
ret.alpha = blendAlphaValue(c1.alpha, c2.alpha, t)
return ret
Incidentally, I long for a programming language and keyboard that both permits representing math that (or more) cleanly (the combining overline unicode character doesn't work for superscripts, symbols, and a vast array of other characters) and interpreting it correctly. sqrt((1-t)*pow(a, 2) + t * pow(b, 2)) just doesn't read as clean.
Few points:
I think you want to use min instead of max
I think you want to use 255 instead of 256
This will give:
(r1, g1, b1) + (r2, g2, b2) = (min(r1+r2, 255), min(g1+g2, 255), min(b1+b2, 255))
However, The "natural" way of mixing colors is to use the average, and then you don't need the min:
(r1, g1, b1) + (r2, g2, b2) = ((r1+r2)/2, (g1+g2)/2, (b1+b2)/2)
Javascript function to blend rgba colors
c1,c2 and result - JSON's like
c1={r:0.5,g:1,b:0,a:0.33}
var rgbaSum = function(c1, c2){
var a = c1.a + c2.a*(1-c1.a);
return {
r: (c1.r * c1.a + c2.r * c2.a * (1 - c1.a)) / a,
g: (c1.g * c1.a + c2.g * c2.a * (1 - c1.a)) / a,
b: (c1.b * c1.a + c2.b * c2.a * (1 - c1.a)) / a,
a: a
}
}
PYTHON COLOUR MIXING THROUGH ADDITION IN CMYK SPACE
One possible way to do this is to first convert the colours to CMYK format, add them there and then reconvert to RGB.
Here is an example code in Python:
rgb_scale = 255
cmyk_scale = 100
def rgb_to_cmyk(self,r,g,b):
if (r == 0) and (g == 0) and (b == 0):
# black
return 0, 0, 0, cmyk_scale
# rgb [0,255] -> cmy [0,1]
c = 1 - r / float(rgb_scale)
m = 1 - g / float(rgb_scale)
y = 1 - b / float(rgb_scale)
# extract out k [0,1]
min_cmy = min(c, m, y)
c = (c - min_cmy)
m = (m - min_cmy)
y = (y - min_cmy)
k = min_cmy
# rescale to the range [0,cmyk_scale]
return c*cmyk_scale, m*cmyk_scale, y*cmyk_scale, k*cmyk_scale
def cmyk_to_rgb(self,c,m,y,k):
"""
"""
r = rgb_scale*(1.0-(c+k)/float(cmyk_scale))
g = rgb_scale*(1.0-(m+k)/float(cmyk_scale))
b = rgb_scale*(1.0-(y+k)/float(cmyk_scale))
return r,g,b
def ink_add_for_rgb(self,list_of_colours):
"""input: list of rgb, opacity (r,g,b,o) colours to be added, o acts as weights.
output (r,g,b)
"""
C = 0
M = 0
Y = 0
K = 0
for (r,g,b,o) in list_of_colours:
c,m,y,k = rgb_to_cmyk(r, g, b)
C+= o*c
M+=o*m
Y+=o*y
K+=o*k
return cmyk_to_rgb(C, M, Y, K)
The result to your question would then be (assuming a half-half mixture of your two colours:
r_mix, g_mix, b_mix = ink_add_for_rgb([(r1,g1,b1,0.5),(r2,g2,b2,0.5)])
where the 0.5's are there to say that we mix 50% of the first colour with 50% of the second colour.
Find here the mixing methods suggested by Fordi and Markus Jarderot in one python function that gradually mixes or blends between two colors A and B.
The "mix" mode is useful to interpolate between two colors. The "blend" mode (with t=0) is useful to compute the resulting color if one translucent color is painted on top of another (possibly translucent) color. gamma correction leads to nicer results because it takes into consideration the fact that physical light intensity and perceived brightness (by humans) are related non-linearly.
import numpy as np
def mix_colors_rgba(color_a, color_b, mode="mix", t=None, gamma=2.2):
"""
Mix two colors color_a and color_b.
Arguments:
color_a: Real-valued 4-tuple. Foreground color in "blend" mode.
color_b: Real-valued 4-tuple. Background color in "blend" mode.
mode: "mix": Interpolate between two colors.
"blend": Blend two translucent colors.
t: Mixing threshold.
gamma: Parameter to control the gamma correction.
Returns:
rgba: A 4-tuple with the result color.
To reproduce Markus Jarderot's solution:
mix_colors_rgba(a, b, mode="blend", t=0, gamma=1.)
To reproduce Fordi's solution:
mix_colors_rgba(a, b, mode="mix", t=t, gamma=2.)
To compute the RGB color of a translucent color on white background:
mix_colors_rgba(a, [1,1,1,1], mode="blend", t=0, gamma=None)
"""
assert(mode in ("mix", "blend"))
assert(gamma is None or gamma>0)
t = t if t is not None else (0.5 if mode=="mix" else 0.)
t = max(0,min(t,1))
color_a = np.asarray(color_a)
color_b = np.asarray(color_b)
if mode=="mix" and gamma in (1., None):
r, g, b, a = (1-t)*color_a + t*color_b
elif mode=="mix" and gamma > 0:
r,g,b,_ = np.power((1-t)*color_a**gamma + t*color_b**gamma, 1/gamma)
a = (1-t)*color_a[-1] + t*color_b[-1]
elif mode=="blend":
alpha_a = color_a[-1]*(1-t)
a = 1 - (1-alpha_a) * (1-color_b[-1])
s = color_b[-1]*(1-alpha_a)/a
if gamma in (1., None):
r, g, b, _ = (1-s)*color_a + s*color_b
elif gamma > 0:
r, g, b, _ = np.power((1-s)*color_a**gamma + s*color_b**gamma,
1/gamma)
return tuple(np.clip([r,g,b,a], 0, 1))
See below how this can be used. In "mix" mode the left and right colors match exactly color_a and color_b. In "blend" mode, the left color at t=0 is the color that results if color_a is blended over color_b (and a white background). In the example, color_a then is made increasingly translucent until one arrives at color_b.
Note that blending and mixing are equivalent if the alpha values are 1.0.
For completeness, here the code to reproduce the above plot.
import matplotlib.pyplot as plt
import matplotlib as mpl
def plot(pal, ax, title):
n = len(pal)
ax.imshow(np.tile(np.arange(n), [int(n*0.20),1]),
cmap=mpl.colors.ListedColormap(list(pal)),
interpolation="nearest", aspect="auto")
ax.set_xticks([])
ax.set_yticks([])
ax.set_xticklabels([])
ax.set_yticklabels([])
ax.set_title(title)
_, (ax1, ax2, ax3, ax4) = plt.subplots(nrows=4,ncols=1)
n = 101
ts = np.linspace(0,1,n)
color_a = [1.0,0.0,0.0,0.7] # transparent red
color_b = [0.0,0.0,1.0,0.8] # transparent blue
plot([mix_colors_rgba(color_a, color_b, t=t, mode="mix", gamma=None)
for t in ts], ax=ax1, title="Linear mixing")
plot([mix_colors_rgba(color_a, color_b, t=t, mode="mix", gamma=2.2)
for t in ts], ax=ax2, title="Non-linear mixing (gamma=2.2)")
plot([mix_colors_rgba(color_a, color_b, t=t, mode="blend", gamma=None)
for t in ts], ax=ax3, title="Linear blending")
plot([mix_colors_rgba(color_a, color_b, t=t, mode="blend", gamma=2.2)
for t in ts], ax=ax4, title="Non-linear blending (gamma=2.2)")
plt.tight_layout()
plt.show()
Formulas:
Linear mixing (gamma=1):
r,g,b,a: (1-t)*x + t*y
Non-linear mixing (gama≠1):
r,g,b: pow((1-t)*x**gamma + t*y**gamma, 1/gamma)
a: (1-t)*x + t*y
Blending (gamma=1):
a: 1-(1-(1-t)*x)*(1-y)
s: alpha_b*(1-alpha_a)*a
r,g,b: (1-s)*x + s*y
Blending (gamma≠1):
a: 1-(1-(1-t)*x)*(1-y)
s: alpha_b*(1-alpha_a)/a
r,g,b: pow((1-s)*x**gamma + s*y**gamma, 1/gamma)
And finally, here a useful read about gamma correction.
Yes, it is as simple as that. Another option is to find the average (for creating gradients).
It really just depends on the effect you want to achieve.
However, when Alpha gets added, it gets complicated. There are a number of different methods to blend using an alpha.
An example of simple alpha blending:
http://en.wikipedia.org/wiki/Alpha_compositing#Alpha_blending
When I came here I didn't find the "additive color mixing" algorithm I was actually looking for, which is also available in Photoshop and is described as "Screen" on Wikipedia. (Aka "brighten" or "invert multiply".) It produces a result similar to two light sources being combined.
With Screen blend mode the values of the pixels in the two layers are inverted, multiplied, and then inverted again. This yields the opposite effect to multiply. The result is a brighter picture.
Here it is:
// (rgb values are 0-255)
function screen(color1, color2) {
var r = Math.round((1 - (1 - color1.R / 255) * (1 - color2.R / 255)) * 255);
var g = Math.round((1 - (1 - color1.G / 255) * (1 - color2.G / 255)) * 255);
var b = Math.round((1 - (1 - color1.B / 255) * (1 - color2.B / 255)) * 255);
return new Color(r, g, b);
}
Have written/used something like #Markus Jarderot's sRGB blending answer (which is not gamma corrected since that is the default legacy) using C++
//same as Markus Jarderot's answer
float red, green, blue;
alpha = (1.0 - (1.0 - back.alpha)*(1.0 - front.alpha));
red = (front.red * front.alpha / alpha + back.red * back.alpha * (1.0 - front.alpha));
green = (front.green * front.alpha / alpha + back.green * back.alpha * (1.0 - front.alpha));
blue = (front.blue * front.alpha / alpha + back.blue * back.alpha * (1.0 - front.alpha));
//faster but equal output
alpha = (1.0 - (1.0 - back.alpha)*(1.0 - front.alpha));
red = (back.red * (1.0 - front.alpha) + front.red * front.alpha);
green = (back.green * (1.0 - front.alpha) + front.green * front.alpha);
blue = (back.blue * (1.0 - front.alpha) + front.blue * front.alpha);
//even faster but only works when all values are in range 0 to 255
int red, green, blue;
alpha = (255 - (255 - back.alpha)*(255 - front.alpha));
red = (back.red * (255 - front.alpha) + front.red * front.alpha) / 255;
green = (back.green * (255 - front.alpha) + front.green * front.alpha) / 255;
blue = (back.blue * (255 - front.alpha) + front.blue * front.alpha) / 255;
more info: what-every-coder-should-know-about-gamma
I was working on a similar problem and ended up here, but had to write my own implementation in the end. I wanted to basically "overlay" the new foreground color over the existing background color. (And without using an arbitrary midpoint like t. I believe my implementation is still "additive.") This also seems to blend very cleanly in all of my test-cases.
Here, new_argb just converts the int into a struct with 4 unsigned char so I can reduce the amount of bit-shifts.
int blend_argb(int foreground, int background)
{
t_argb fg;
t_argb bg;
t_argb blend;
double ratio;
fg = new_argb(foreground);
bg = new_argb(background);
// If background is transparent,
// use foreground color as-is and vice versa.
if (bg.a == 255)
return (foreground);
if (fg.a == 255)
return (background);
// If the background is fully opaque,
// ignore the foreground alpha. (Or the color will be darker.)
// Otherwise alpha is additive.
blend.a = ((bg.a == 0) ? 0 : (bg.a + fg.a));
// When foreground alpha == 0, totally covers background color.
ratio = fg.a / 255.0;
blend.r = (fg.r * (1 - ratio)) + (bg.r * ratio);
blend.g = (fg.g * (1 - ratio)) + (bg.g * ratio);
blend.b = (fg.b * (1 - ratio)) + (bg.b * ratio);
return (blend.a << 24 | blend.r << 16 | blend.g << 8 | blend.b);
}
For context, in my environment I'm writing color ints into a 1D pixel array, which is initialized with 0-bytes and increasing the alpha will make the pixel tend towards black. (0 0 0 0 would be opaque black and 255 255 255 255 would be transparent white... aka black.)
Here's a highly optimized, standalone c++ class, public domain, with floating point and two differently optimized 8-bit blending mechanisms in both function and macro formats, as well as a technical discussion of both the problem at hand and how to, and the importance of, optimization of this issue:
https://github.com/fyngyrz/colorblending
Thank you Markus Jarderot, Andras Zoltan and hkurabko; here is the Python code for blending a list of RGB images.
Using Markus Jarderot's code we can generate RGBA color, then i use Andras Zoltan and hkurabko's method to trans RGBA to RGB.
Thank you!
import numpy as np
def Blend2Color(C1,C2):
c1,c1a=C1
c2,c2a=C2
A = 1 - (1 - c1a) * (1 - c2a);
if (A < 1.0e-6):
return (0,0,0) #Fully transparent -- R,G,B not important
Result=(np.array(c1)*c1a+np.array(c2)*c2a*(1-c1a))/A
return Result,A
def RGBA2RGB(RGBA,BackGround=(1,1,1)):# whilt background
A=RGBA[-1]
RGB=np.add(np.multiply(np.array(RGBA[:-1]),A),
np.multiply(np.array(BackGround),1-A))
return RGB
def BlendRGBList(Clist,AlphaList=None,NFloat=2,ReturnRGB=True,
RGB_BackGround=(1,1,1)):
N=len(Clist)
if AlphaList==None:
ClistUse=Clist.copy()
else:
if len(AlphaList)==N:
AlphaListUse=np.multiply(AlphaList,10**NFloat).astype(int)
ClistUse=np.repeat(np.array(Clist), AlphaListUse, axis=0)
else:
raise('len of AlphaList must equal to len of Clist!')
while N!=1:
temp=ClistUse.copy()
ClistUse=[]
for C in temp[:-1]:
c1,a1=C
c2,a2=temp[-1]
ClistUse.append(Blend2Color(C1=(c1,a1*(1-1/N)),C2=(c2,a2*1/N)))
N=len(ClistUse)
Result=np.append(ClistUse[0][0],ClistUse[0][1])
if ReturnRGB:
Result=RGBA2RGB(Result,BackGround=RGB_BackGround)
return Result
Test
BlendRGBList([[(1,0,0),1],[(0,1,0),1]],ReturnRGB=True)
#array([0.75, 0.5 , 0.25])
BlendRGBList([[(1,0,0),1],[(0,1,0),1]],ReturnRGB=False)
#array([0.66666667, 0.33333333, 0. , 0.75 ])
I'm currently writing a program to generate really enormous (65536x65536 pixels and above) Mandelbrot images, and I'd like to devise a spectrum and coloring scheme that does them justice. The wikipedia featured mandelbrot image seems like an excellent example, especially how the palette remains varied at all zoom levels of the sequence. I'm not sure if it's rotating the palette or doing some other trick to achieve this, though.
I'm familiar with the smooth coloring algorithm for the mandelbrot set, so I can avoid banding, but I still need a way to assign colors to output values from this algorithm.
The images I'm generating are pyramidal (eg, a series of images, each of which has half the dimensions of the previous one), so I can use a rotating palette of some sort, as long as the change in the palette between subsequent zoom levels isn't too obvious.
This is the smooth color algorithm:
Lets say you start with the complex number z0 and iterate n times until it escapes. Let the end point be zn.
A smooth value would be
nsmooth := n + 1 - Math.log(Math.log(zn.abs()))/Math.log(2)
This only works for mandelbrot, if you want to compute a smooth function for julia sets, then use
Complex z = new Complex(x,y);
double smoothcolor = Math.exp(-z.abs());
for(i=0;i<max_iter && z.abs() < 30;i++) {
z = f(z);
smoothcolor += Math.exp(-z.abs());
}
Then smoothcolor is in the interval (0,max_iter).
Divide smoothcolor with max_iter to get a value between 0 and 1.
To get a smooth color from the value:
This can be called, for example (in Java):
Color.HSBtoRGB(0.95f + 10 * smoothcolor ,0.6f,1.0f);
since the first value in HSB color parameters is used to define the color from the color circle.
Use the smooth coloring algorithm to calculate all of the values within the viewport, then map your palette from the lowest to highest value. Thus, as you zoom in and the higher values are no longer visible, the palette will scale down as well. With the same constants for n and B you will end up with a range of 0.0 to 1.0 for a fully zoomed out set, but at deeper zooms the dynamic range will shrink, to say 0.0 to 0.1 at 200% zoom, 0.0 to 0.0001 at 20000% zoom, etc.
Here is a typical inner loop for a naive Mandelbrot generator. To get a smooth colour you want to pass in the real and complex "lengths" and the iteration you bailed out at. I've included the Mandelbrot code so you can see which vars to use to calculate the colour.
for (ix = 0; ix < panelMain.Width; ix++)
{
cx = cxMin + (double )ix * pixelWidth;
// init this go
zx = 0.0;
zy = 0.0;
zx2 = 0.0;
zy2 = 0.0;
for (i = 0; i < iterationMax && ((zx2 + zy2) < er2); i++)
{
zy = zx * zy * 2.0 + cy;
zx = zx2 - zy2 + cx;
zx2 = zx * zx;
zy2 = zy * zy;
}
if (i == iterationMax)
{
// interior, part of set, black
// set colour to black
g.FillRectangle(sbBlack, ix, iy, 1, 1);
}
else
{
// outside, set colour proportional to time/distance it took to converge
// set colour not black
SolidBrush sbNeato = new SolidBrush(MapColor(i, zx2, zy2));
g.FillRectangle(sbNeato, ix, iy, 1, 1);
}
and MapColor below: (see this link to get the ColorFromHSV function)
private Color MapColor(int i, double r, double c)
{
double di=(double )i;
double zn;
double hue;
zn = Math.Sqrt(r + c);
hue = di + 1.0 - Math.Log(Math.Log(Math.Abs(zn))) / Math.Log(2.0); // 2 is escape radius
hue = 0.95 + 20.0 * hue; // adjust to make it prettier
// the hsv function expects values from 0 to 360
while (hue > 360.0)
hue -= 360.0;
while (hue < 0.0)
hue += 360.0;
return ColorFromHSV(hue, 0.8, 1.0);
}
MapColour is "smoothing" the bailout values from 0 to 1 which then can be used to map a colour without horrible banding. Playing with MapColour and/or the hsv function lets you alter what colours are used.
Seems simple to do by trial and error. Assume you can define HSV1 and HSV2 (hue, saturation, value) of the endpoint colors you wish to use (black and white; blue and yellow; dark red and light green; etc.), and assume you have an algorithm to assign a value P between 0.0 and 1.0 to each of your pixels. Then that pixel's color becomes
(H2 - H1) * P + H1 = HP
(S2 - S1) * P + S1 = SP
(V2 - V1) * P + V1 = VP
With that done, just observe the results and see how you like them. If the algorithm to assign P is continuous, then the gradient should be smooth as well.
My eventual solution was to create a nice looking (and fairly large) palette and store it as a constant array in the source, then interpolate between indexes in it using the smooth coloring algorithm. The palette wraps (and is designed to be continuous), but this doesn't appear to matter much.
What's going on with the color mapping in that image is that it's using a 'log transfer function' on the index (according to documentation). How exactly it's doing it I still haven't figured out yet. The program that produced it uses a palette of 400 colors, so index ranges [0,399), wrapping around if needed. I've managed to get pretty close to matching it's behavior. I use an index range of [0,1) and map it like so:
double value = Math.log(0.021 * (iteration + delta + 60)) + 0.72;
value = value - Math.floor(value);
It's kind of odd that I have to use these special constants in there to get my results to match, since I doubt they do any of that. But whatever works in the end, right?
here you can find a version with javascript
usage :
var rgbcol = [] ;
var rgbcol = MapColor ( Iteration , Zy2,Zx2 ) ;
point ( ctx , iX, iY ,rgbcol[0],rgbcol[1],rgbcol[2] );
function
/*
* The Mandelbrot Set, in HTML5 canvas and javascript.
* https://github.com/cslarsen/mandelbrot-js
*
* Copyright (C) 2012 Christian Stigen Larsen
*/
/*
* Convert hue-saturation-value/luminosity to RGB.
*
* Input ranges:
* H = [0, 360] (integer degrees)
* S = [0.0, 1.0] (float)
* V = [0.0, 1.0] (float)
*/
function hsv_to_rgb(h, s, v)
{
if ( v > 1.0 ) v = 1.0;
var hp = h/60.0;
var c = v * s;
var x = c*(1 - Math.abs((hp % 2) - 1));
var rgb = [0,0,0];
if ( 0<=hp && hp<1 ) rgb = [c, x, 0];
if ( 1<=hp && hp<2 ) rgb = [x, c, 0];
if ( 2<=hp && hp<3 ) rgb = [0, c, x];
if ( 3<=hp && hp<4 ) rgb = [0, x, c];
if ( 4<=hp && hp<5 ) rgb = [x, 0, c];
if ( 5<=hp && hp<6 ) rgb = [c, 0, x];
var m = v - c;
rgb[0] += m;
rgb[1] += m;
rgb[2] += m;
rgb[0] *= 255;
rgb[1] *= 255;
rgb[2] *= 255;
rgb[0] = parseInt ( rgb[0] );
rgb[1] = parseInt ( rgb[1] );
rgb[2] = parseInt ( rgb[2] );
return rgb;
}
// http://stackoverflow.com/questions/369438/smooth-spectrum-for-mandelbrot-set-rendering
// alex russel : http://stackoverflow.com/users/2146829/alex-russell
function MapColor(i,r,c)
{
var di= i;
var zn;
var hue;
zn = Math.sqrt(r + c);
hue = di + 1.0 - Math.log(Math.log(Math.abs(zn))) / Math.log(2.0); // 2 is escape radius
hue = 0.95 + 20.0 * hue; // adjust to make it prettier
// the hsv function expects values from 0 to 360
while (hue > 360.0)
hue -= 360.0;
while (hue < 0.0)
hue += 360.0;
return hsv_to_rgb(hue, 0.8, 1.0);
}
Motivation
I'd like to find a way to take an arbitrary color and lighten it a few shades, so that I can programatically create a nice gradient from the one color to a lighter version. The gradient will be used as a background in a UI.
Possibility 1
Obviously I can just split out the RGB values and increase them individually by a certain amount. Is this actually what I want?
Possibility 2
My second thought was to convert the RGB to HSV/HSB/HSL (Hue, Saturation, Value/Brightness/Lightness), increase the brightness a bit, decrease the saturation a bit, and then convert it back to RGB. Will this have the desired effect in general?
As Wedge said, you want to multiply to make things brighter, but that only works until one of the colors becomes saturated (i.e. hits 255 or greater). At that point, you can just clamp the values to 255, but you'll be subtly changing the hue as you get lighter. To keep the hue, you want to maintain the ratio of (middle-lowest)/(highest-lowest).
Here are two functions in Python. The first implements the naive approach which just clamps the RGB values to 255 if they go over. The second redistributes the excess values to keep the hue intact.
def clamp_rgb(r, g, b):
return min(255, int(r)), min(255, int(g)), min(255, int(b))
def redistribute_rgb(r, g, b):
threshold = 255.999
m = max(r, g, b)
if m <= threshold:
return int(r), int(g), int(b)
total = r + g + b
if total >= 3 * threshold:
return int(threshold), int(threshold), int(threshold)
x = (3 * threshold - total) / (3 * m - total)
gray = threshold - x * m
return int(gray + x * r), int(gray + x * g), int(gray + x * b)
I created a gradient starting with the RGB value (224,128,0) and multiplying it by 1.0, 1.1, 1.2, etc. up to 2.0. The upper half is the result using clamp_rgb and the bottom half is the result with redistribute_rgb. I think it's easy to see that redistributing the overflows gives a much better result, without having to leave the RGB color space.
For comparison, here's the same gradient in the HLS and HSV color spaces, as implemented by Python's colorsys module. Only the L component was modified, and clamping was performed on the resulting RGB values. The results are similar, but require color space conversions for every pixel.
I would go for the second option. Generally speaking the RGB space is not really good for doing color manipulation (creating transition from one color to an other, lightening / darkening a color, etc). Below are two sites I've found with a quick search to convert from/to RGB to/from HSL:
from the "Fundamentals of Computer Graphics"
some sourcecode in C# - should be easy to adapt to other programming languages.
In C#:
public static Color Lighten(Color inColor, double inAmount)
{
return Color.FromArgb(
inColor.A,
(int) Math.Min(255, inColor.R + 255 * inAmount),
(int) Math.Min(255, inColor.G + 255 * inAmount),
(int) Math.Min(255, inColor.B + 255 * inAmount) );
}
I've used this all over the place.
ControlPaint class in System.Windows.Forms namespace has static methods Light and Dark:
public static Color Dark(Color baseColor, float percOfDarkDark);
These methods use private implementation of HLSColor. I wish this struct was public and in System.Drawing.
Alternatively, you can use GetHue, GetSaturation, GetBrightness on Color struct to get HSB components. Unfortunately, I didn't find the reverse conversion.
Convert it to RGB and linearly interpolate between the original color and the target color (often white). So, if you want 16 shades between two colors, you do:
for(i = 0; i < 16; i++)
{
colors[i].R = start.R + (i * (end.R - start.R)) / 15;
colors[i].G = start.G + (i * (end.G - start.G)) / 15;
colors[i].B = start.B + (i * (end.B - start.B)) / 15;
}
In order to get a lighter or a darker version of a given color you should modify its brightness. You can do this easily even without converting your color to HSL or HSB color. For example to make a color lighter you can use the following code:
float correctionFactor = 0.5f;
float red = (255 - color.R) * correctionFactor + color.R;
float green = (255 - color.G) * correctionFactor + color.G;
float blue = (255 - color.B) * correctionFactor + color.B;
Color lighterColor = Color.FromArgb(color.A, (int)red, (int)green, (int)blue);
If you need more details, read the full story on my blog.
Converting to HS(LVB), increasing the brightness and then converting back to RGB is the only way to reliably lighten the colour without effecting the hue and saturation values (ie to only lighten the colour without changing it in any other way).
A very similar question, with useful answers, was asked previously:
How do I determine darker or lighter color variant of a given color?
Short answer: multiply the RGB values by a constant if you just need "good enough", translate to HSV if you require accuracy.
I used Andrew's answer and Mark's answer to make this (as of 1/2013 no range input for ff).
function calcLightness(l, r, g, b) {
var tmp_r = r;
var tmp_g = g;
var tmp_b = b;
tmp_r = (255 - r) * l + r;
tmp_g = (255 - g) * l + g;
tmp_b = (255 - b) * l + b;
if (tmp_r > 255 || tmp_g > 255 || tmp_b > 255)
return { r: r, g: g, b: b };
else
return { r:parseInt(tmp_r), g:parseInt(tmp_g), b:parseInt(tmp_b) }
}
I've done this both ways -- you get much better results with Possibility 2.
Any simple algorithm you construct for Possibility 1 will probably work well only for a limited range of starting saturations.
You would want to look into Poss 1 if (1) you can restrict the colors and brightnesses used, and (2) you are performing the calculation a lot in a rendering.
Generating the background for a UI won't need very many shading calculations, so I suggest Poss 2.
-Al.
IF you want to produce a gradient fade-out, I would suggest the following optimization: Rather than doing RGB->HSB->RGB for each individual color you should only calculate the target color. Once you know the target RGB, you can simply calculate the intermediate values in RGB space without having to convert back and forth. Whether you calculate a linear transition of use some sort of curve is up to you.
Method 1: Convert RGB to HSL, adjust HSL, convert back to RGB.
Method 2: Lerp the RGB colour values - http://en.wikipedia.org/wiki/Lerp_(computing)
See my answer to this similar question for a C# implementation of method 2.
Pretend that you alpha blended to white:
oneMinus = 1.0 - amount
r = amount + oneMinus * r
g = amount + oneMinus * g
b = amount + oneMinus * b
where amount is from 0 to 1, with 0 returning the original color and 1 returning white.
You might want to blend with whatever the background color is if you are lightening to display something disabled:
oneMinus = 1.0 - amount
r = amount * dest_r + oneMinus * r
g = amount * dest_g + oneMinus * g
b = amount * dest_b + oneMinus * b
where (dest_r, dest_g, dest_b) is the color being blended to and amount is from 0 to 1, with zero returning (r, g, b) and 1 returning (dest.r, dest.g, dest.b)
I didn't find this question until after it became a related question to my original question.
However, using insight from these great answers. I pieced together a nice two-liner function for this:
Programmatically Lighten or Darken a hex color (or rgb, and blend colors)
Its a version of method 1. But with over saturation taken into account. Like Keith said in his answer above; use Lerp to seemly solve the same problem Mark mentioned, but without redistribution. The results of shadeColor2 should be much closer to doing it the right way with HSL, but without the overhead.
A bit late to the party, but if you use javascript or nodejs, you can use tinycolor library, and manipulate the color the way you want:
tinycolor("red").lighten().desaturate().toHexString() // "#f53d3d"
I would have tried number #1 first, but #2 sounds pretty good. Try doing it yourself and see if you're satisfied with the results, it sounds like it'll take you maybe 10 minutes to whip up a test.
Technically, I don't think either is correct, but I believe you want a variant of option #2. The problem being that taken RGB 990000 and "lightening" it would really just add onto the Red channel (Value, Brightness, Lightness) until you got to FF. After that (solid red), it would be taking down the saturation to go all the way to solid white.
The conversions get annoying, especially since you can't go direct to and from RGB and Lab, but I think you really want to separate the chrominance and luminence values, and just modify the luminence to really achieve what you want.
Here's an example of lightening an RGB colour in Python:
def lighten(hex, amount):
""" Lighten an RGB color by an amount (between 0 and 1),
e.g. lighten('#4290e5', .5) = #C1FFFF
"""
hex = hex.replace('#','')
red = min(255, int(hex[0:2], 16) + 255 * amount)
green = min(255, int(hex[2:4], 16) + 255 * amount)
blue = min(255, int(hex[4:6], 16) + 255 * amount)
return "#%X%X%X" % (int(red), int(green), int(blue))
This is based on Mark Ransom's answer.
Where the clampRGB function tries to maintain the hue, it however miscalculates the scaling to keep the same luminance. This is because the calculation directly uses sRGB values which are not linear.
Here's a Java version that does the same as clampRGB (although with values ranging from 0 to 1) that maintains luminance as well:
private static Color convertToDesiredLuminance(Color input, double desiredLuminance) {
if(desiredLuminance > 1.0) {
return Color.WHITE;
}
if(desiredLuminance < 0.0) {
return Color.BLACK;
}
double ratio = desiredLuminance / luminance(input);
double r = Double.isInfinite(ratio) ? desiredLuminance : toLinear(input.getRed()) * ratio;
double g = Double.isInfinite(ratio) ? desiredLuminance : toLinear(input.getGreen()) * ratio;
double b = Double.isInfinite(ratio) ? desiredLuminance : toLinear(input.getBlue()) * ratio;
if(r > 1.0 || g > 1.0 || b > 1.0) { // anything outside range?
double br = Math.min(r, 1.0); // base values
double bg = Math.min(g, 1.0);
double bb = Math.min(b, 1.0);
double rr = 1.0 - br; // ratios between RGB components to maintain
double rg = 1.0 - bg;
double rb = 1.0 - bb;
double x = (desiredLuminance - luminance(br, bg, bb)) / luminance(rr, rg, rb);
r = 0.0001 * Math.round(10000.0 * (br + rr * x));
g = 0.0001 * Math.round(10000.0 * (bg + rg * x));
b = 0.0001 * Math.round(10000.0 * (bb + rb * x));
}
return Color.color(toGamma(r), toGamma(g), toGamma(b));
}
And supporting functions:
private static double toLinear(double v) { // inverse is #toGamma
return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
}
private static double toGamma(double v) { // inverse is #toLinear
return v <= 0.0031308 ? v * 12.92 : 1.055 * Math.pow(v, 1.0 / 2.4) - 0.055;
}
private static double luminance(Color c) {
return luminance(toLinear(c.getRed()), toLinear(c.getGreen()), toLinear(c.getBlue()));
}
private static double luminance(double r, double g, double b) {
return r * 0.2126 + g * 0.7152 + b * 0.0722;
}