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))
I have an application that defines a real world rectangle on top of an image/photograph, of course in 2D it may not be a rectangle because you are looking at it from an angle.
The problem is, say that the rectangle needs to have grid lines drawn on it, for example if it is 3x5 so I need to draw 2 lines from side 1 to side 3, and 4 lines from side 2 to side 4.
As of right now I am breaking up each line into equidistant parts, to get the start and end point of all the grid lines. However the more of an angle the rectangle is on, the more "incorrect" these lines become, as horizontal lines further from you should be closer together.
Does anyone know the name of the algorithm that I should be searching for?
Yes I know you can do this in 3D, however I am limited to 2D for this particular application.
Here's the solution.
The basic idea is you can find the perspective correct "center" of your rectangle by connecting the corners diagonally. The intersection of the two resulting lines is your perspective correct center. From there you subdivide your rectangle into four smaller rectangles, and you repeat the process. The number of times depends on how accurate you want it. You can subdivide to just below the size of a pixel for effectively perfect perspective.
Then in your subrectangles you just apply your standard uncorrected "textured" triangles, or rectangles or whatever.
You can perform this algorithm without going to the complex trouble of building a 'real' 3d world. it's also good for if you do have a real 3d world modeled, but your textriangles are not perspective corrected in hardware, or you need a performant way to get perspective correct planes without per pixel rendering trickery.
Image: Example of Bilinear & Perspective Transform (Note: The height of top & bottom horizontal grid lines is actually half of the rest lines height, on both drawings)
========================================
I know this is an old question, but I have a generic solution so I decided to publish it hopping it will be useful to the future readers.
The code bellow can draw an arbitrary perspective grid without the need of repetitive computations.
I begin actually with a similar problem: to draw a 2D perspective Grid and then transform the underline image to restore the perspective.
I started to read here:
http://www.imagemagick.org/Usage/distorts/#bilinear_forward
and then here (the Leptonica Library):
http://www.leptonica.com/affine.html
were I found this:
When you look at an object in a plane from some arbitrary direction at
a finite distance, you get an additional "keystone" distortion in the
image. This is a projective transform, which keeps straight lines
straight but does not preserve the angles between lines. This warping
cannot be described by a linear affine transformation, and in fact
differs by x- and y-dependent terms in the denominator.
The transformation is not linear, as many people already pointed out in this thread. It involves solving a linear system of 8 equations (once) to compute the 8 required coefficients and then you can use them to transform as many points as you want.
To avoid including all Leptonica library in my project, I took some pieces of code from it, I removed all special Leptonica data-types & macros, I fixed some memory leaks and I converted it to a C++ class (mostly for encapsulation reasons) which does just one thing:
It maps a (Qt) QPointF float (x,y) coordinate to the corresponding Perspective Coordinate.
If you want to adapt the code to another C++ library, the only thing to redefine/substitute is the QPointF coordinate class.
I hope some future readers would find it useful.
The code bellow is divided into 3 parts:
A. An example on how to use the genImageProjective C++ class to draw a 2D perspective Grid
B. genImageProjective.h file
C. genImageProjective.cpp file
//============================================================
// C++ Code Example on how to use the
// genImageProjective class to draw a perspective 2D Grid
//============================================================
#include "genImageProjective.h"
// Input: 4 Perspective-Tranformed points:
// perspPoints[0] = top-left
// perspPoints[1] = top-right
// perspPoints[2] = bottom-right
// perspPoints[3] = bottom-left
void drawGrid(QPointF *perspPoints)
{
(...)
// Setup a non-transformed area rectangle
// I use a simple square rectangle here because in this case we are not interested in the source-rectangle,
// (we want to just draw a grid on the perspPoints[] area)
// but you can use any arbitrary rectangle to perform a real mapping to the perspPoints[] area
QPointF topLeft = QPointF(0,0);
QPointF topRight = QPointF(1000,0);
QPointF bottomRight = QPointF(1000,1000);
QPointF bottomLeft = QPointF(0,1000);
float width = topRight.x() - topLeft.x();
float height = bottomLeft.y() - topLeft.y();
// Setup Projective trasform object
genImageProjective imageProjective;
imageProjective.sourceArea[0] = topLeft;
imageProjective.sourceArea[1] = topRight;
imageProjective.sourceArea[2] = bottomRight;
imageProjective.sourceArea[3] = bottomLeft;
imageProjective.destArea[0] = perspPoints[0];
imageProjective.destArea[1] = perspPoints[1];
imageProjective.destArea[2] = perspPoints[2];
imageProjective.destArea[3] = perspPoints[3];
// Compute projective transform coefficients
if (imageProjective.computeCoeefficients() != 0)
return; // This can actually fail if any 3 points of Source or Dest are colinear
// Initialize Grid parameters (without transform)
float gridFirstLine = 0.1f; // The normalized position of first Grid Line (0.0 to 1.0)
float gridStep = 0.1f; // The normalized Grd size (=distance between grid lines: 0.0 to 1.0)
// Draw Horizonal Grid lines
QPointF lineStart, lineEnd, tempPnt;
for (float pos = gridFirstLine; pos <= 1.0f; pos += gridStep)
{
// Compute Grid Line Start
tempPnt = QPointF(topLeft.x(), topLeft.y() + pos*width);
imageProjective.mapSourceToDestPoint(tempPnt, lineStart);
// Compute Grid Line End
tempPnt = QPointF(topRight.x(), topLeft.y() + pos*width);
imageProjective.mapSourceToDestPoint(tempPnt, lineEnd);
// Draw Horizontal Line (use your prefered method to draw the line)
(...)
}
// Draw Vertical Grid lines
for (float pos = gridFirstLine; pos <= 1.0f; pos += gridStep)
{
// Compute Grid Line Start
tempPnt = QPointF(topLeft.x() + pos*height, topLeft.y());
imageProjective.mapSourceToDestPoint(tempPnt, lineStart);
// Compute Grid Line End
tempPnt = QPointF(topLeft.x() + pos*height, bottomLeft.y());
imageProjective.mapSourceToDestPoint(tempPnt, lineEnd);
// Draw Vertical Line (use your prefered method to draw the line)
(...)
}
(...)
}
==========================================
//========================================
//C++ Header File: genImageProjective.h
//========================================
#ifndef GENIMAGE_H
#define GENIMAGE_H
#include <QPointF>
// Class to transform an Image Point using Perspective transformation
class genImageProjective
{
public:
genImageProjective();
int computeCoeefficients(void);
int mapSourceToDestPoint(QPointF& sourcePoint, QPointF& destPoint);
public:
QPointF sourceArea[4]; // Source Image area limits (Rectangular)
QPointF destArea[4]; // Destination Image area limits (Perspectivelly Transformed)
private:
static int gaussjordan(float **a, float *b, int n);
bool coefficientsComputed;
float vc[8]; // Vector of Transform Coefficients
};
#endif // GENIMAGE_H
//========================================
//========================================
//C++ CPP File: genImageProjective.cpp
//========================================
#include <math.h>
#include "genImageProjective.h"
// ----------------------------------------------------
// class genImageProjective
// ----------------------------------------------------
genImageProjective::genImageProjective()
{
sourceArea[0] = sourceArea[1] = sourceArea[2] = sourceArea[3] = QPointF(0,0);
destArea[0] = destArea[1] = destArea[2] = destArea[3] = QPointF(0,0);
coefficientsComputed = false;
}
// --------------------------------------------------------------
// Compute projective transform coeeeficients
// RetValue: 0: Success, !=0: Error
/*-------------------------------------------------------------*
* Projective coordinate transformation *
*-------------------------------------------------------------*/
/*!
* computeCoeefficients()
*
* Input: this->sourceArea[4]: (source 4 points; unprimed)
* this->destArea[4]: (transformed 4 points; primed)
* this->vc (computed vector of transform coefficients)
* Return: 0 if OK; <0 on error
*
* We have a set of 8 equations, describing the projective
* transformation that takes 4 points (sourceArea) into 4 other
* points (destArea). These equations are:
*
* x1' = (c[0]*x1 + c[1]*y1 + c[2]) / (c[6]*x1 + c[7]*y1 + 1)
* y1' = (c[3]*x1 + c[4]*y1 + c[5]) / (c[6]*x1 + c[7]*y1 + 1)
* x2' = (c[0]*x2 + c[1]*y2 + c[2]) / (c[6]*x2 + c[7]*y2 + 1)
* y2' = (c[3]*x2 + c[4]*y2 + c[5]) / (c[6]*x2 + c[7]*y2 + 1)
* x3' = (c[0]*x3 + c[1]*y3 + c[2]) / (c[6]*x3 + c[7]*y3 + 1)
* y3' = (c[3]*x3 + c[4]*y3 + c[5]) / (c[6]*x3 + c[7]*y3 + 1)
* x4' = (c[0]*x4 + c[1]*y4 + c[2]) / (c[6]*x4 + c[7]*y4 + 1)
* y4' = (c[3]*x4 + c[4]*y4 + c[5]) / (c[6]*x4 + c[7]*y4 + 1)
*
* Multiplying both sides of each eqn by the denominator, we get
*
* AC = B
*
* where B and C are column vectors
*
* B = [ x1' y1' x2' y2' x3' y3' x4' y4' ]
* C = [ c[0] c[1] c[2] c[3] c[4] c[5] c[6] c[7] ]
*
* and A is the 8x8 matrix
*
* x1 y1 1 0 0 0 -x1*x1' -y1*x1'
* 0 0 0 x1 y1 1 -x1*y1' -y1*y1'
* x2 y2 1 0 0 0 -x2*x2' -y2*x2'
* 0 0 0 x2 y2 1 -x2*y2' -y2*y2'
* x3 y3 1 0 0 0 -x3*x3' -y3*x3'
* 0 0 0 x3 y3 1 -x3*y3' -y3*y3'
* x4 y4 1 0 0 0 -x4*x4' -y4*x4'
* 0 0 0 x4 y4 1 -x4*y4' -y4*y4'
*
* These eight equations are solved here for the coefficients C.
*
* These eight coefficients can then be used to find the mapping
* (x,y) --> (x',y'):
*
* x' = (c[0]x + c[1]y + c[2]) / (c[6]x + c[7]y + 1)
* y' = (c[3]x + c[4]y + c[5]) / (c[6]x + c[7]y + 1)
*
*/
int genImageProjective::computeCoeefficients(void)
{
int retValue = 0;
int i;
float *a[8]; /* 8x8 matrix A */
float *b = this->vc; /* rhs vector of primed coords X'; coeffs returned in vc[] */
b[0] = destArea[0].x();
b[1] = destArea[0].y();
b[2] = destArea[1].x();
b[3] = destArea[1].y();
b[4] = destArea[2].x();
b[5] = destArea[2].y();
b[6] = destArea[3].x();
b[7] = destArea[3].y();
for (i = 0; i < 8; i++)
a[i] = NULL;
for (i = 0; i < 8; i++)
{
if ((a[i] = (float *)calloc(8, sizeof(float))) == NULL)
{
retValue = -100; // ERROR_INT("a[i] not made", procName, 1);
goto Terminate;
}
}
a[0][0] = sourceArea[0].x();
a[0][1] = sourceArea[0].y();
a[0][2] = 1.;
a[0][6] = -sourceArea[0].x() * b[0];
a[0][7] = -sourceArea[0].y() * b[0];
a[1][3] = sourceArea[0].x();
a[1][4] = sourceArea[0].y();
a[1][5] = 1;
a[1][6] = -sourceArea[0].x() * b[1];
a[1][7] = -sourceArea[0].y() * b[1];
a[2][0] = sourceArea[1].x();
a[2][1] = sourceArea[1].y();
a[2][2] = 1.;
a[2][6] = -sourceArea[1].x() * b[2];
a[2][7] = -sourceArea[1].y() * b[2];
a[3][3] = sourceArea[1].x();
a[3][4] = sourceArea[1].y();
a[3][5] = 1;
a[3][6] = -sourceArea[1].x() * b[3];
a[3][7] = -sourceArea[1].y() * b[3];
a[4][0] = sourceArea[2].x();
a[4][1] = sourceArea[2].y();
a[4][2] = 1.;
a[4][6] = -sourceArea[2].x() * b[4];
a[4][7] = -sourceArea[2].y() * b[4];
a[5][3] = sourceArea[2].x();
a[5][4] = sourceArea[2].y();
a[5][5] = 1;
a[5][6] = -sourceArea[2].x() * b[5];
a[5][7] = -sourceArea[2].y() * b[5];
a[6][0] = sourceArea[3].x();
a[6][1] = sourceArea[3].y();
a[6][2] = 1.;
a[6][6] = -sourceArea[3].x() * b[6];
a[6][7] = -sourceArea[3].y() * b[6];
a[7][3] = sourceArea[3].x();
a[7][4] = sourceArea[3].y();
a[7][5] = 1;
a[7][6] = -sourceArea[3].x() * b[7];
a[7][7] = -sourceArea[3].y() * b[7];
retValue = gaussjordan(a, b, 8);
Terminate:
// Clean up
for (i = 0; i < 8; i++)
{
if (a[i])
free(a[i]);
}
this->coefficientsComputed = (retValue == 0);
return retValue;
}
/*-------------------------------------------------------------*
* Gauss-jordan linear equation solver *
*-------------------------------------------------------------*/
/*
* gaussjordan()
*
* Input: a (n x n matrix)
* b (rhs column vector)
* n (dimension)
* Return: 0 if ok, 1 on error
*
* Note side effects:
* (1) the matrix a is transformed to its inverse
* (2) the vector b is transformed to the solution X to the
* linear equation AX = B
*
* Adapted from "Numerical Recipes in C, Second Edition", 1992
* pp. 36-41 (gauss-jordan elimination)
*/
#define SWAP(a,b) {temp = (a); (a) = (b); (b) = temp;}
int genImageProjective::gaussjordan(float **a, float *b, int n)
{
int retValue = 0;
int i, icol=0, irow=0, j, k, l, ll;
int *indexc = NULL, *indexr = NULL, *ipiv = NULL;
float big, dum, pivinv, temp;
if (!a)
{
retValue = -1; // ERROR_INT("a not defined", procName, 1);
goto Terminate;
}
if (!b)
{
retValue = -2; // ERROR_INT("b not defined", procName, 1);
goto Terminate;
}
if ((indexc = (int *)calloc(n, sizeof(int))) == NULL)
{
retValue = -3; // ERROR_INT("indexc not made", procName, 1);
goto Terminate;
}
if ((indexr = (int *)calloc(n, sizeof(int))) == NULL)
{
retValue = -4; // ERROR_INT("indexr not made", procName, 1);
goto Terminate;
}
if ((ipiv = (int *)calloc(n, sizeof(int))) == NULL)
{
retValue = -5; // ERROR_INT("ipiv not made", procName, 1);
goto Terminate;
}
for (i = 0; i < n; i++)
{
big = 0.0;
for (j = 0; j < n; j++)
{
if (ipiv[j] != 1)
{
for (k = 0; k < n; k++)
{
if (ipiv[k] == 0)
{
if (fabs(a[j][k]) >= big)
{
big = fabs(a[j][k]);
irow = j;
icol = k;
}
}
else if (ipiv[k] > 1)
{
retValue = -6; // ERROR_INT("singular matrix", procName, 1);
goto Terminate;
}
}
}
}
++(ipiv[icol]);
if (irow != icol)
{
for (l = 0; l < n; l++)
SWAP(a[irow][l], a[icol][l]);
SWAP(b[irow], b[icol]);
}
indexr[i] = irow;
indexc[i] = icol;
if (a[icol][icol] == 0.0)
{
retValue = -7; // ERROR_INT("singular matrix", procName, 1);
goto Terminate;
}
pivinv = 1.0 / a[icol][icol];
a[icol][icol] = 1.0;
for (l = 0; l < n; l++)
a[icol][l] *= pivinv;
b[icol] *= pivinv;
for (ll = 0; ll < n; ll++)
{
if (ll != icol)
{
dum = a[ll][icol];
a[ll][icol] = 0.0;
for (l = 0; l < n; l++)
a[ll][l] -= a[icol][l] * dum;
b[ll] -= b[icol] * dum;
}
}
}
for (l = n - 1; l >= 0; l--)
{
if (indexr[l] != indexc[l])
{
for (k = 0; k < n; k++)
SWAP(a[k][indexr[l]], a[k][indexc[l]]);
}
}
Terminate:
if (indexr)
free(indexr);
if (indexc)
free(indexc);
if (ipiv)
free(ipiv);
return retValue;
}
// --------------------------------------------------------------
// Map a source point to destination using projective transform
// --------------------------------------------------------------
// Params:
// sourcePoint: initial point
// destPoint: transformed point
// RetValue: 0: Success, !=0: Error
// --------------------------------------------------------------
// Notes:
// 1. You must call once computeCoeefficients() to compute
// the this->vc[] vector of 8 coefficients, before you call
// mapSourceToDestPoint().
// 2. If there was an error or the 8 coefficients were not computed,
// a -1 is returned and destPoint is just set to sourcePoint value.
// --------------------------------------------------------------
int genImageProjective::mapSourceToDestPoint(QPointF& sourcePoint, QPointF& destPoint)
{
if (coefficientsComputed)
{
float factor = 1.0f / (vc[6] * sourcePoint.x() + vc[7] * sourcePoint.y() + 1.);
destPoint.setX( factor * (vc[0] * sourcePoint.x() + vc[1] * sourcePoint.y() + vc[2]) );
destPoint.setY( factor * (vc[3] * sourcePoint.x() + vc[4] * sourcePoint.y() + vc[5]) );
return 0;
}
else // There was an error while computing coefficients
{
destPoint = sourcePoint; // just copy the source to destination...
return -1; // ...and return an error
}
}
//========================================
Using Breton's subdivision method (which is related to Mongo's extension method), will get you accurate arbitrary power-of-two divisions. To split into non-power-of-two divisions using those methods you will have to subdivide to sub-pixel spacing, which can be computationally expensive.
However, I believe you may be able to apply a variation of Haga's Theorem (which is used in origami to divide a side into Nths given a side divided into (N-1)ths) to the perspective-square subdivisions to produce arbitrary divisions from the closest power of 2 without having to continue subdividing.
The most elegant and fastest solution would be to find the homography matrix, which maps rectangle coordinates to photo coordinates.
With a decent matrix library it should not be a difficult task, as long as you know your math.
Keywords: Collineation, Homography, Direct Linear Transformation
However, the recursive algorithm above should work, but probably if your resources are limited, projective geometry is the only way to go.
I think the selected answer is not the best solution available. A better solution is to apply perspective (projective) transformation of a rectangle to simple grid as following Matlab script and image show. You can implement this algorithm with C++ and OpenCV as well.
function drawpersgrid
sz = [ 24, 16 ]; % [x y]
srcpt = [ 0 0; sz(1) 0; 0 sz(2); sz(1) sz(2)];
destpt = [ 20 50; 100 60; 0 150; 200 200;];
% make rectangular grid
[X,Y] = meshgrid(0:sz(1),0:sz(2));
% find projective transform matching corner points
tform = maketform('projective',srcpt,destpt);
% apply the projective transform to the grid
[X1,Y1] = tformfwd(tform,X,Y);
hold on;
%% find grid
for i=1:sz(2)
for j=1:sz(1)
x = [ X1(i,j);X1(i,j+1);X1(i+1,j+1);X1(i+1,j);X1(i,j)];
y = [ Y1(i,j);Y1(i,j+1);Y1(i+1,j+1);Y1(i+1,j);Y1(i,j)];
plot(x,y,'b');
end
end
hold off;
In the special case when you look perpendicular to sides 1 and 3, you can divide those sides in equal parts. Then draw a diagonal, and draw parallels to side 1 through each intersection of the diagonal and the dividing lines drawn earlier.
This a geometric solution I thought out. I do not know whether the 'algorithm' has a name.
Say you want to start by dividing the 'rectangle' into n pieces with vertical lines first.
The goal is to place points P1..Pn-1 on the top line which we can use to draw lines through them to the points where the left and right line meet or parallel to them when such point does not exist.
If the top and bottom line are parallel to each other just place thoose points to split the top line between the corners equidistantly.
Else place n points Q1..Qn on the left line so that theese and the top-left corner are equidistant and i < j => Qi is closer to the top-left cornern than Qj.
In order to map the Q-points to the top line find the intersection S of the line from Qn through the top-right corner and the parallel to the left line through the intersection of top and bottom line. Now connect S with Q1..Qn-1. The intersection of the new lines with the top line are the wanted P-points.
Do this analog for the horizontal lines.
Given a rotation around the y axis, especially if rotation surfaces are planar, the perspective is generated by vertical gradients. These get progressively closer in perspective. Instead of using diagonals to define four rectangles, which can work given powers of two... define two rectangles, left and right. They'll be higher than wide, eventually, if one continues to divide the surface into narrower vertical segments. This can accommodate surfaces that are not square. If a rotation is around the x axis, then horizontal gradients are needed.
What you need to do is represent it in 3D (world) and then project it down to 2D (screen).
This will require you to use a 4D transformation matrix which does the projection on a 4D homogeneous down to a 3D homogeneous vector, which you can then convert down to a 2D screen space vector.
I couldn't find it in Google either, but a good computer graphics books will have the details.
Keywords are projection matrix, projection transformation, affine transformation, homogeneous vector, world space, screen space, perspective transformation, 3D transformation
And by the way, this usually takes a few lectures to explain all of that. So good luck.
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;
}