Color Theory/Algorithms

From Wikibooks, open books for an open world
Jump to navigation Jump to search
  "color operations should be done ...to either model human perception or the physical behavior of light" Björn Ottosson : How software gets color wrong 


color conversion[edit | edit source]

// https://www.w3.org/TR/css-color-4/#color-conversion-code
// Sample code for color conversions
// Conversion can also be done using ICC profiles and a Color Management System
// For clarity, a library is used for matrix multiplication (multiply-matrices.js)

// standard white points, defined by 4-figure CIE x,y chromaticities
const D50 = [0.3457 / 0.3585, 1.00000, (1.0 - 0.3457 - 0.3585) / 0.3585];
const D65 = [0.3127 / 0.3290, 1.00000, (1.0 - 0.3127 - 0.3290) / 0.3290];


function lin_sRGB_to_XYZ(rgb) {
	// convert an array of linear-light sRGB values to CIE XYZ
	// using sRGB's own white, D65 (no chromatic adaptation)

	var M = [
		[ 506752 / 1228815,  87881 / 245763,   12673 /   70218 ],
		[  87098 /  409605, 175762 / 245763,   12673 /  175545 ],
		[   7918 /  409605,  87881 / 737289, 1001167 / 1053270 ],
	];
	return multiplyMatrices(M, rgb);
}

function XYZ_to_lin_sRGB(XYZ) {
	// convert XYZ to linear-light sRGB

	var M = [
		[   12831 /   3959,    -329 /    214, -1974 /   3959 ],
		[ -851781 / 878810, 1648619 / 878810, 36519 / 878810 ],
		[     705 /  12673,   -2585 /  12673,   705 /    667 ],
	];

	return multiplyMatrices(M, XYZ);
}

//  display-p3-related functions


function lin_P3(RGB) {
	// convert an array of display-p3 RGB values in the range 0.0 - 1.0
	// to linear light (un-companded) form.

	return lin_sRGB(RGB);	// same as sRGB
}

function gam_P3(RGB) {
	// convert an array of linear-light display-p3 RGB  in the range 0.0-1.0
	// to gamma corrected form

	return gam_sRGB(RGB);	// same as sRGB
}

function lin_P3_to_XYZ(rgb) {
	// convert an array of linear-light display-p3 values to CIE XYZ
	// using  D65 (no chromatic adaptation)
	// http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
	var M = [
		[ 608311 / 1250200, 189793 / 714400,  198249 / 1000160 ],
		[  35783 /  156275, 247089 / 357200,  198249 / 2500400 ],
		[      0 /       1,  32229 / 714400, 5220557 / 5000800 ],
	];

	return multiplyMatrices(M, rgb);
}

function XYZ_to_lin_P3(XYZ) {
	// convert XYZ to linear-light P3
	var M = [
		[ 446124 / 178915, -333277 / 357830, -72051 / 178915 ],
		[ -14852 /  17905,   63121 /  35810,    423 /  17905 ],
		[  11844 / 330415,  -50337 / 660830, 316169 / 330415 ],
	];

	return multiplyMatrices(M, XYZ);
}

// prophoto-rgb functions

function lin_ProPhoto(RGB) {
	// convert an array of prophoto-rgb values
	// where in-gamut colors are in the range [0.0 - 1.0]
	// to linear light (un-companded) form.
	// Transfer curve is gamma 1.8 with a small linear portion
	// Extended transfer function
	const Et2 = 16/512;
	return RGB.map(function (val) {
		let sign = val < 0? -1 : 1;
		let abs = Math.abs(val);

		if (abs <= Et2) {
			return val / 16;
		}

		return sign * Math.pow(abs, 1.8);
	});
}

function gam_ProPhoto(RGB) {
	// convert an array of linear-light prophoto-rgb  in the range 0.0-1.0
	// to gamma corrected form
	// Transfer curve is gamma 1.8 with a small linear portion
	// TODO for negative values, extend linear portion on reflection of axis, then add pow below that
	const Et = 1/512;
	return RGB.map(function (val) {
		let sign = val < 0? -1 : 1;
		let abs = Math.abs(val);

		if (abs >= Et) {
			return sign * Math.pow(abs, 1/1.8);
		}

		return 16 * val;
	});
}

function lin_ProPhoto_to_XYZ(rgb) {
	// convert an array of linear-light prophoto-rgb values to CIE XYZ
	// using  D50 (so no chromatic adaptation needed afterwards)
	// http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
	var M = [
		[ 0.7977604896723027,  0.13518583717574031,  0.0313493495815248     ],
		[ 0.2880711282292934,  0.7118432178101014,   0.00008565396060525902 ],
		[ 0.0,                 0.0,                  0.8251046025104601     ]
	];

	return multiplyMatrices(M, rgb);
}

function XYZ_to_lin_ProPhoto(XYZ) {
	// convert XYZ to linear-light prophoto-rgb
	var M = [
	  	[  1.3457989731028281,  -0.25558010007997534,  -0.05110628506753401 ],
	  	[ -0.5446224939028347,   1.5082327413132781,    0.02053603239147973 ],
	  	[  0.0,                  0.0,                   1.2119675456389454  ]
	];

	return multiplyMatrices(M, XYZ);
}

// a98-rgb functions

function lin_a98rgb(RGB) {
	// convert an array of a98-rgb values in the range 0.0 - 1.0
	// to linear light (un-companded) form.
	// negative values are also now accepted
	return RGB.map(function (val) {
		let sign = val < 0? -1 : 1;
		let abs = Math.abs(val);

	  	return sign * Math.pow(abs, 563/256);
	});
}

function gam_a98rgb(RGB) {
	// convert an array of linear-light a98-rgb  in the range 0.0-1.0
	// to gamma corrected form
	// negative values are also now accepted
	return RGB.map(function (val) {
		let sign = val < 0? -1 : 1;
		let abs = Math.abs(val);

		return sign * Math.pow(abs, 256/563);
	});
}

function lin_a98rgb_to_XYZ(rgb) {
	// convert an array of linear-light a98-rgb values to CIE XYZ
	// http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
	// has greater numerical precision than section 4.3.5.3 of
	// https://www.adobe.com/digitalimag/pdfs/AdobeRGB1998.pdf
	// but the values below were calculated from first principles
	// from the chromaticity coordinates of R G B W
	// see matrixmaker.html
	var M = [
		[ 573536 /  994567,  263643 / 1420810,  187206 /  994567 ],
		[ 591459 / 1989134, 6239551 / 9945670,  374412 / 4972835 ],
		[  53769 / 1989134,  351524 / 4972835, 4929758 / 4972835 ],
	];

	return multiplyMatrices(M, rgb);
}

function XYZ_to_lin_a98rgb(XYZ) {
	// convert XYZ to linear-light a98-rgb
	var M = [
		[ 1829569 /  896150, -506331 /  896150, -308931 /  896150 ],
		[ -851781 /  878810, 1648619 /  878810,   36519 /  878810 ],
		[   16779 / 1248040, -147721 / 1248040, 1266979 / 1248040 ],
	];

	return multiplyMatrices(M, XYZ);
}

//Rec. 2020-related functions

function lin_2020(RGB) {
	// convert an array of rec2020 RGB values in the range 0.0 - 1.0
	// to linear light (un-companded) form.
	// ITU-R BT.2020-2 p.4

	const α = 1.09929682680944 ;
	const β = 0.018053968510807;

	return RGB.map(function (val) {
		let sign = val < 0? -1 : 1;
		let abs = Math.abs(val);

		if (abs < β * 4.5 ) {
			return val / 4.5;
		}

		return sign * (Math.pow((abs + α -1 ) / α, 1/0.45));
	});
}

function gam_2020(RGB) {
	// convert an array of linear-light rec2020 RGB  in the range 0.0-1.0
	// to gamma corrected form
	// ITU-R BT.2020-2 p.4

	const α = 1.09929682680944 ;
	const β = 0.018053968510807;


	return RGB.map(function (val) {
		let sign = val < 0? -1 : 1;
		let abs = Math.abs(val);

		if (abs > β ) {
			return sign * (α * Math.pow(abs, 0.45) - (α - 1));
		}

		return 4.5 * val;
	});
}

function lin_2020_to_XYZ(rgb) {
	// convert an array of linear-light rec2020 values to CIE XYZ
	// using  D65 (no chromatic adaptation)
	// http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
	var M = [
		[ 63426534 / 99577255,  20160776 / 139408157,  47086771 / 278816314 ],
		[ 26158966 / 99577255, 472592308 / 697040785,   8267143 / 139408157 ],
		[        0 /        1,  19567812 / 697040785, 295819943 / 278816314 ],
	];
	// 0 is actually calculated as  4.994106574466076e-17

	return multiplyMatrices(M, rgb);
}

function XYZ_to_lin_2020(XYZ) {
	// convert XYZ to linear-light rec2020
	var M = [
		[  30757411 / 17917100, -6372589 / 17917100, -4539589 / 17917100 ],
		[ -19765991 / 29648200, 47925759 / 29648200,   467509 / 29648200 ],
		[    792561 / 44930125, -1921689 / 44930125, 42328811 / 44930125 ],
	];

	return multiplyMatrices(M, XYZ);
}

// Chromatic adaptation

function D65_to_D50(XYZ) {
	// Bradford chromatic adaptation from D65 to D50
	// The matrix below is the result of three operations:
	// - convert from XYZ to retinal cone domain
	// - scale components from one reference white to another
	// - convert back to XYZ
	// http://www.brucelindbloom.com/index.html?Eqn_ChromAdapt.html
	var M =  [
		[  1.0479298208405488,    0.022946793341019088,  -0.05019222954313557 ],
		[  0.029627815688159344,  0.990434484573249,     -0.01707382502938514 ],
		[ -0.009243058152591178,  0.015055144896577895,   0.7518742899580008  ]
	];

	return multiplyMatrices(M, XYZ);
}

function D50_to_D65(XYZ) {
	// Bradford chromatic adaptation from D50 to D65
	var M = [
		[  0.9554734527042182,   -0.023098536874261423,  0.0632593086610217   ],
		[ -0.028369706963208136,  1.0099954580058226,    0.021041398966943008 ],
		[  0.012314001688319899, -0.020507696433477912,  1.3303659366080753   ]
	];

	return multiplyMatrices(M, XYZ);
}

// CIE Lab and LCH

function XYZ_to_Lab(XYZ) {
	// Assuming XYZ is relative to D50, convert to CIE Lab
	// from CIE standard, which now defines these as a rational fraction
	var ε = 216/24389;  // 6^3/29^3
	var κ = 24389/27;   // 29^3/3^3

	// compute xyz, which is XYZ scaled relative to reference white
	var xyz = XYZ.map((value, i) => value / D50[i]);

	// now compute f
	var f = xyz.map(value => value > ε ? Math.cbrt(value) : (κ * value + 16)/116);

	return [
		(116 * f[1]) - 16, 	 // L
		500 * (f[0] - f[1]), // a
		200 * (f[1] - f[2])  // b
	];
	// L in range [0,100]. For use in CSS, add a percent
}

function Lab_to_XYZ(Lab) {
	// Convert Lab to D50-adapted XYZ
	// http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
	var κ = 24389/27;   // 29^3/3^3
	var ε = 216/24389;  // 6^3/29^3
	var f = [];

	// compute f, starting with the luminance-related term
	f[1] = (Lab[0] + 16)/116;
	f[0] = Lab[1]/500 + f[1];
	f[2] = f[1] - Lab[2]/200;

	// compute xyz
	var xyz = [
		Math.pow(f[0],3) > ε ?   Math.pow(f[0],3)            : (116*f[0]-16)/κ,
		Lab[0] > κ * ε ?         Math.pow((Lab[0]+16)/116,3) : Lab[0]/κ,
		Math.pow(f[2],3)  > ε ?  Math.pow(f[2],3)            : (116*f[2]-16)/κ
	];

	// Compute XYZ by scaling xyz by reference white
	return xyz.map((value, i) => value * D50[i]);
}

function Lab_to_LCH(Lab) {
	// Convert to polar form
	var hue = Math.atan2(Lab[2], Lab[1]) * 180 / Math.PI;
	return [
		Lab[0], // L is still L
		Math.sqrt(Math.pow(Lab[1], 2) + Math.pow(Lab[2], 2)), // Chroma
		hue >= 0 ? hue : hue + 360 // Hue, in degrees [0 to 360)
	];
}

function LCH_to_Lab(LCH) {
	// Convert from polar form
	return [
		LCH[0], // L is still L
		LCH[1] * Math.cos(LCH[2] * Math.PI / 180), // a
		LCH[1] * Math.sin(LCH[2] * Math.PI / 180) // b
	];
}

// OKLab and OKLCH
// https://bottosson.github.io/posts/oklab/

// XYZ <-> LMS matrices recalculated for consistent reference white
// see https://github.com/w3c/csswg-drafts/issues/6642#issuecomment-943521484

function XYZ_to_OKLab(XYZ) {
	// Given XYZ relative to D65, convert to OKLab
	var XYZtoLMS = [
		[ 0.8190224432164319,    0.3619062562801221,   -0.12887378261216414  ],
		[ 0.0329836671980271,    0.9292868468965546,     0.03614466816999844 ],
		[ 0.048177199566046255,  0.26423952494422764,    0.6335478258136937  ]
	];
	var LMStoOKLab = [
		[  0.2104542553,   0.7936177850,  -0.0040720468 ],
		[  1.9779984951,  -2.4285922050,   0.4505937099 ],
		[  0.0259040371,   0.7827717662,  -0.8086757660 ]
	];

	var LMS = multiplyMatrices(XYZtoLMS, XYZ);
	return multiplyMatrices(LMStoOKLab, LMS.map(c => Math.cbrt(c)));
	// L in range [0,1]. For use in CSS, multiply by 100 and add a percent
}

function OKLab_to_XYZ(OKLab) {
	// Given OKLab, convert to XYZ relative to D65
	var LMStoXYZ =  [
		[  1.2268798733741557,  -0.5578149965554813,   0.28139105017721583 ],
		[ -0.04057576262431372,  1.1122868293970594,  -0.07171106666151701 ],
		[ -0.07637294974672142, -0.4214933239627914,   1.5869240244272418  ]
	];
	var OKLabtoLMS = [
        [ 0.99999999845051981432,  0.39633779217376785678,   0.21580375806075880339  ],
        [ 1.0000000088817607767,  -0.1055613423236563494,   -0.063854174771705903402 ],
        [ 1.0000000546724109177,  -0.089484182094965759684, -1.2914855378640917399   ]
    ];

	var LMSnl = multiplyMatrices(OKLabtoLMS, OKLab);
	return multiplyMatrices(LMStoXYZ, LMSnl.map(c => c ** 3));
}

function OKLab_to_OKLCH(OKLab) {
	var hue = Math.atan2(OKLab[2], OKLab[1]) * 180 / Math.PI;
	return [
		OKLab[0], // L is still L
		Math.sqrt(OKLab[1] ** 2 + OKLab[2] ** 2), // Chroma
		hue >= 0 ? hue : hue + 360 // Hue, in degrees [0 to 360)
	];
}

function OKLCH_to_OKLab(OKLCH) {
	return [
		OKLCH[0], // L is still L
		OKLCH[1] * Math.cos(OKLCH[2] * Math.PI / 180), // a
		OKLCH[1] * Math.sin(OKLCH[2] * Math.PI / 180)  // b
	];
}

// Premultiplied alpha conversions

function rectangular_premultiply(color, alpha) {
// given a color in a rectangular orthogonal colorspace
// and an alpha value
// return the premultiplied form
	return color.map((c) => c * alpha)
}

function rectangular_un_premultiply(color, alpha) {
// given a premultiplied color in a rectangular orthogonal colorspace
// and an alpha value
// return the actual color
	if (alpha === 0) {
		return color; // avoid divide by zero
	}
	return color.map((c) => c / alpha)
}

function polar_premultiply(color, alpha, hueIndex) {
	// given a color in a cylindicalpolar colorspace
	// and an alpha value
	// return the premultiplied form.
	// the index says which entry in the color array corresponds to hue angle
	// for example, in OKLCH it would be 2
	// while in HSL it would be 0
	return color.map((c, i) => c * (hueIndex === i? 1 : alpha))
}

function polar_un_premultiply(color, alpha, hueIndex) {
	// given a color in a cylindicalpolar colorspace
	// and an alpha value
	// return the actual color.
	// the hueIndex says which entry in the color array corresponds to hue angle
	// for example, in OKLCH it would be 2
	// while in HSL it would be 0
	if (alpha === 0) {
		return color; // avoid divide by zero
	}
	return color.map((c, i) => c / (hueIndex === i? 1 : alpha))
}

// Convenience functions can easily be defined, such as
function hsl_premultiply(color, alpha) {
	return polar_premultiply(color, alpha, 0);
}

convertion inside sRGB[edit | edit source]

js code from CSS[edit | edit source]

// https://www.w3.org/TR/css-color-4/#color-conversion-code
// Sample code for color conversions
// Conversion can also be done using ICC profiles and a Color Management System
// For clarity, a library is used for matrix multiplication (multiply-matrices.js)


// sRGB-related functions

function lin_sRGB(RGB) {
	// convert an array of sRGB values
	// where in-gamut values are in the range [0 - 1]
	// to linear light (un-companded) form.
	// en wiki: SRGB
	// Extended transfer function:
	// for negative values,  linear portion is extended on reflection of axis,
	// then reflected power function is used.
	return RGB.map(function (val) {
		let sign = val < 0? -1 : 1;
		let abs = Math.abs(val);

		if (abs < 0.04045) {
			return val / 12.92;
		}

		return sign * (Math.pow((abs + 0.055) / 1.055, 2.4));
	});
}

function gam_sRGB(RGB) {
	// convert an array of linear-light sRGB values in the range 0.0-1.0
	// to gamma corrected form
	// en wiki: SRGB
	// Extended transfer function:
	// For negative values, linear portion extends on reflection
	// of axis, then uses reflected pow below that
	return RGB.map(function (val) {
		let sign = val < 0? -1 : 1;
		let abs = Math.abs(val);

		if (abs > 0.0031308) {
			return sign * (1.055 * Math.pow(abs, 1/2.4) - 0.055);
		}

		return 12.92 * val;
	});
}

linearisation[edit | edit source]

Steps[2]

  • read value ( from sRGB)
  • linearize and widening your intensity (convert 8-bit integer to 16 bit integer of float)
  • process image
  • delinearize and convert to 8-bit
  • save as sRGB


When you're reading an sRGB image, and you want linear intensities, apply this formula to each intensity:

float s = read_channel();
float linear;
if (s <= 0.04045) linear = s / 12.92;
else linear = pow((s + 0.055) / 1.055, 2.4);


Going the other way, when you want to write an image as sRGB, apply this formula to each linear intensity:

float linear = do_processing();
float s;
if (linear <= 0.0031308) s = linear * 12.92;
else s = 1.055 * pow(linear, 1.0/2.4) - 0.055;



Stages:[3]

  • applying the inverse of the sRGB nonlinear transform function f_inv
  • doing computations
  • then switching back by f

For colors in sRGB in range 0.0 to 1.0 this can be done by applying these functions component wise (provided in C-like pseudocode):


float f_inv(float x)
{
   if (x >= 0.04045)
       return ((x + 0.055)/(1 + 0.055))^2.4
   else 
       return x / 12.92
}
float f(float x)
{
   if (x >= 0.0031308)
       return (1.055) * x^(1.0/2.4) - 0.055
   else
       return 12.92 * x
}

Convertion between HSL and sRGB[edit | edit source]

Converting an HSL color to sRGB in JavaScript.

/**
 * https://www.w3.org/TR/css-color-4/#hsl-to-rgb
 * @param {number} hue - Hue as degrees 0..360
 * @param {number} sat - Saturation as percentage 0..100
 * @param {number} light - Lightness as percentage 0..100
 * @return {number[]} Array of RGB components 0..1
 */
function hslToRgb(hue, sat, light) {
    hue = hue % 360;

    if (hue < 0) {
        hue += 360;
    }

    sat /= 100;
    light /= 100;

    function f(n) {
        let k = (n + hue/30) % 12;
        let a = sat * Math.min(light, 1 - light);
        return light - a * Math.max(-1, Math.min(k - 3, 9 - k, 1));
    }

    return [f(0), f(8), f(4)];
}

Converting sRGB Colors to HSL

/**
 * https://www.w3.org/TR/css-color-4/#hsl-to-rgb
 * @param {number} red - Red component 0..1
 * @param {number} green - Green component 0..1
 * @param {number} blue - Blue component 0..1
 * @return {number[]} Array of HSL values: Hue as degrees 0..360, Saturation and Lightness as percentages 0..100
 */
function rgbToHsl (red, green, blue) {
    let max = Math.max(red, green, blue);
    let min = Math.min(red, green, blue);
    let [hue, sat, light] = [NaN, 0, (min + max)/2];
    let d = max - min;

    if (d !== 0) {
        sat = (light === 0 || light === 1)
            ? 0
            : (max - light) / Math.min(light, 1 - light);

        switch (max) {
            case red:   hue = (green - blue) / d + (green < blue ? 6 : 0); break;
            case green: hue = (blue - red) / d + 2; break;
            case blue:  hue = (red - green) / d + 4;
        }

        hue = hue * 60;
    }

    return [hue, sat * 100, light * 100];
}

Converting from linear sRGB to Oklab[edit | edit source]

A color in Oklab is represented with three coordinates Lab:[4][5]

  • L – perceived lightness ( a unitless number in the range [0,1] )
  • a – how green/red the color is
  • b – how blue/yellow the color is


It's corresponding polar form is called Oklch.

The standard coordinate can also be transformed into polar form ( Lch), with the coordinates:

  • L = lightness
  • c = chroma
  • h = hue

In reverse direction:

Converting from linear sRGB to Oklab in c++

// https://bottosson.github.io/misc/ok_color.h
struct Lab {float L; float a; float b;};
struct RGB {float r; float g; float b;};

Lab linear_srgb_to_oklab(RGB c) 
{
    float l = 0.4122214708f * c.r + 0.5363325363f * c.g + 0.0514459929f * c.b;
	float m = 0.2119034982f * c.r + 0.6806995451f * c.g + 0.1073969566f * c.b;
	float s = 0.0883024619f * c.r + 0.2817188376f * c.g + 0.6299787005f * c.b;

    float l_ = cbrtf(l);
    float m_ = cbrtf(m);
    float s_ = cbrtf(s);

    return {
        0.2104542553f*l_ + 0.7936177850f*m_ - 0.0040720468f*s_,
        1.9779984951f*l_ - 2.4285922050f*m_ + 0.4505937099f*s_,
        0.0259040371f*l_ + 0.7827717662f*m_ - 0.8086757660f*s_,
    };
}

RGB oklab_to_linear_srgb(Lab c) 
{
    float l_ = c.L + 0.3963377774f * c.a + 0.2158037573f * c.b;
    float m_ = c.L - 0.1055613458f * c.a - 0.0638541728f * c.b;
    float s_ = c.L - 0.0894841775f * c.a - 1.2914855480f * c.b;

    float l = l_*l_*l_;
    float m = m_*m_*m_;
    float s = s_*s_*s_;

    return {
		+4.0767416621f * l - 3.3077115913f * m + 0.2309699292f * s,
		-1.2684380046f * l + 2.6097574011f * m - 0.3413193965f * s,
		-0.0041960863f * l - 0.7034186147f * m + 1.7076147010f * s,
    };
}

Image conversion[edit | edit source]

C code[6]

#include "helpers.h"
#include <math.h>
// Convert image to grayscale
void grayscale(int height, int width, RGBTRIPLE image[height][width])
{
    // update to avg of blue green and red
    float avg = 0;
    for (int i = 0; i < height; i++)
    {
        for (int j = 0; j < width; j ++)
        {
            avg = (((float)image[i][j].rgbtBlue + image[i][j].rgbtGreen + image[i][j].rgbtRed) / 3);
            int roundi = round(avg);
            image[i][j].rgbtBlue = roundi;
            image[i][j].rgbtGreen = roundi;
            image[i][j].rgbtRed = roundi;

        }
    }
    return;
}

// Reflect image horizontally
void reflect(int height, int width, RGBTRIPLE image[height][width])
{
    // swaping 2 vals [end to start]
    for (int i = 0; i < height; i++)
    {
        //only till width / 2 because we dont want it to swap again
        for (int j = 0; j < (int)width / 2 ; j ++)
        {
            int tmpblue = image[i][j].rgbtBlue;
            image[i][j].rgbtBlue = image[i][width - j - 1].rgbtBlue;
            image[i][width - j - 1].rgbtBlue = tmpblue;

            int tmpgreen = image[i][j].rgbtGreen;
            image[i][j].rgbtGreen = image[i][width - j - 1].rgbtGreen;
            image[i][width - j - 1].rgbtGreen = tmpgreen;

            int tmpred = image[i][j].rgbtRed;
            image[i][j].rgbtRed = image[i][width - j - 1].rgbtRed;
            image[i][width - j - 1].rgbtRed = tmpred;

        }
    }
    return;
}

// Convert image to sepia
void sepia(int height, int width, RGBTRIPLE image[height][width])
{
    //sepia red blue green is all 0 for now
    float sepiaRed = 0;
    float sepiaBlue = 0;
    float sepiaGreen = 0;
    for (int i = 0; i < height; i++)
    {
        for (int j = 0; j < width; j ++)
        {
            // applying algo to all
            sepiaRed = (0.393 * (float)image[i][j].rgbtRed) + (0.769 * (float)image[i][j].rgbtGreen) + (0.189 * (float)image[i][j].rgbtBlue);
            sepiaGreen = (0.349 * (float)image[i][j].rgbtRed) + (0.686 * (float)image[i][j].rgbtGreen) + (0.168 * (float)image[i][j].rgbtBlue);
            sepiaBlue = (0.272 * (float)image[i][j].rgbtRed) + (0.534 * (float)image[i][j].rgbtGreen) + (0.131 * (float)image[i][j].rgbtBlue);

            int sred = round(sepiaRed);
            int sgreen = round(sepiaGreen);
            int sblue = round(sepiaBlue);

            // if more than 255 which is max of rgb cap it to 255
            if (sred > 255)
            {
                sred = 255;
            }
            if (sgreen > 255)
            {
                sgreen = 255;
            }
            if (sblue > 255)
            {
                sblue = 255;
            }
            image[i][j].rgbtBlue = sblue;
            image[i][j].rgbtGreen = sgreen;
            image[i][j].rgbtRed = sred;

        }
    }
    return;

}

// Blur image
void blur(int height, int width, RGBTRIPLE image[height][width])
{
    RGBTRIPLE tmp[height][width];

    for (int row = 0; row < height; row ++)
    {
        for (int col = 0; col < width; col ++)
        {
            int count = 0;

            int xaxis[] = {row - 1, row, row + 1};
            int yaxis[] = {col - 1, col, col + 1};

            float totalR = 0, totalG = 0, totalB = 0;

            for (int r = 0; r < 3; r++)
            {
                for (int c = 0; c < 3; c++)
                {
                    int curRow = xaxis[r];
                    int curCol = yaxis[c];

                    if (curRow >= 0 && curRow < height && curCol >= 0 && curCol < width)
                    {
                        RGBTRIPLE pixel = image[curRow][curCol];

                        totalR += pixel.rgbtRed;
                        totalG += pixel.rgbtGreen;
                        totalB += pixel.rgbtBlue;
                        count ++;

                    }
                }
            }

            tmp[row][col].rgbtRed = round(totalR / count);
            tmp[row][col].rgbtGreen = round(totalG / count);
            tmp[row][col].rgbtBlue = round(totalB / count);

        }
    }

    for (int i = 0; i < height; i++)
    {
        for (int j = 0; j < width; j ++)
        {
            image[i][j] = tmp[i][j];
        }
    }
    return;
}

// Detect edges
void edges(int height, int width, RGBTRIPLE image[height][width])
{
    RGBTRIPLE tmp[height][width];

    //gx matrix
    int Gx[3][3] =
    {
        {-1, 0, 1},
        {-2, 0, 2},
        {-1, 0, 1}
    };

    //gy matrix
    int Gy[3][3] =
    {
        {-1, -2, -1},
        {0, 0, 0},
        {1, 2, 1}
    };

    for (int row = 0; row < height; row ++)
    {
        for (int col = 0; col < width; col ++)
        {
            int count = 0;

            //x axis
            int xaxis[] = {row - 1, row, row + 1};
            // y axis
            int yaxis[] = {col - 1, col, col + 1};

            // flaot vals for gx rgb
            float Gx_R = 0, Gx_G = 0, Gx_B = 0;

            // flaot vals for gy rgb
            float Gy_R = 0, Gy_G = 0, Gy_B = 0;

            for (int r = 0; r < 3; r++)
            {
                for (int c = 0; c < 3; c++)
                {
                    int curRow = xaxis[r];
                    int curCol = yaxis[c];

                    if (curRow >= 0 && curRow < height && curCol >= 0 && curCol < width)
                    {
                        RGBTRIPLE pixel = image[curRow][curCol];

                        // matrix for gx_rgb * the gx vals
                        Gx_R += Gx[r][c] * pixel.rgbtRed;
                        Gx_G += Gx[r][c] * pixel.rgbtGreen;
                        Gx_B += Gx[r][c] * pixel.rgbtBlue;

                        // matrix for gy_rgb * the gy vals
                        Gy_R += Gy[r][c] * pixel.rgbtRed;
                        Gy_G += Gy[r][c] * pixel.rgbtGreen;
                        Gy_B += Gy[r][c] * pixel.rgbtBlue;


                    }
                }
            }

            //sqrt of the vals of gx and gy rgb then roud it
            int final_red = round(sqrt((Gx_R * Gx_R) + (Gy_R * Gy_R)));
            int final_green = round(sqrt((Gx_G * Gx_G) + (Gy_G * Gy_G)));
            int final_blue = round(sqrt((Gx_B * Gx_B) + (Gy_B * Gy_B)));

            // if the value more than 255 then cap it to 255
            if (final_red > 255)
            {
                final_red = 255;
            }
            if (final_green > 255)
            {
                final_green = 255;
            }
            if (final_blue > 255)
            {
                final_blue = 255;
            }

            //update vals in the tmp
            tmp[row][col].rgbtRed = final_red;
            tmp[row][col].rgbtGreen = final_green;
            tmp[row][col].rgbtBlue = final_blue;


        }
    }

    // updating the vals into the new image output
    for (int i = 0; i < height; i++)
    {
        for (int j = 0; j < width; j ++)
        {
            image[i][j] = tmp[i][j];
        }
    }
    return;
}

Gray conversion[edit | edit source]

Python code[7]

# Gray Algorithms---------------------------------------------
def grayAverage(r,g,b):
    algorithm = (r + g + b) // 3
    return (algorithm)

def invertRGB(r,g,b):
        r = 255 - r
        g = 255 - g
        b = 255 - b
        return (r,g,b)

def lightness(r,g,b):
        algorithm = (max(r, g, b) + min(r, g, b)) // 2
        return (algorithm)

def luminosity(r,g,b):
        algorithm = int(((0.21 * r) + (0.71 * g) + (0.07 * b)))
        return (algorithm)

matrix multiplication[edit | edit source]

matrix multiplication. (This is more readable than inlining all the multiplies and adds). The matrices are in column-major order.

/**
 * https://www.w3.org/TR/css-color-4/multiply-matrices.js
 * Simple matrix (and vector) multiplication
 * Warning: No error handling for incompatible dimensions!
 * @author Lea Verou 2020 MIT License
 */
// A is m x n. B is n x p. product is m x p.
function multiplyMatrices(A, B) {
	let m = A.length;

	if (!Array.isArray(A[0])) {
		// A is vector, convert to [[a, b, c, ...]]
		A = [A];
	}

	if (!Array.isArray(B[0])) {
		// B is vector, convert to [[a], [b], [c], ...]]
		B = B.map(x => [x]);
	}

	let p = B[0].length;
	let B_cols = B[0].map((_, i) => B.map(x => x[i])); // transpose B
	let product = A.map(row => B_cols.map(col => {
		if (!Array.isArray(row)) {
			return col.reduce((a, c) => a + c * row, 0);
		}

		return row.reduce((a, c, i) => a + c * (col[i] || 0), 0);
	}));

	if (m === 1) {
		product = product[0]; // Avoid [[a, b, c, ...]]
	}

	if (p === 1) {
		return product.map(x => x[0]); // Avoid [[a], [b], [c], ...]]
	}

	return product;
}

Mix colors[edit | edit source]

Methods

  • simple interpolation of the sRGB values (gamma-corrected values). Lerp
  • interpolation on linear values makes the red-green gradient better, but at the expense of the back-white gradient.
  • Hue Interpolation ( in HSV, HSB)[8]. We could convert RGB to HSV and lerp each of the H, S, and V components. This is also pretty bad.
  • separating the light intensities from the color
  • Luminosity Interpolation: The best way is to lerp in perceptually linear colourspace ( LAB, OKLab, HCL...). The default color space for mixing (and gradients) in CSS is oklab


Goal: The intensity of the gradient must be constant

Code:

simple interpolation[edit | edit source]

Mix 2 RGB colors ( naive and wrong form)

//This is the wrong algorithm. Don't do this
Color ColorMixWrong(Color c1, Color c2, Single mix)
{
   //Mix [0..1]
   //  0   --> all c1
   //  0.5 --> equal mix of c1 and c2
   //  1   --> all c2
   Color result;

   result.r = c1.r*(1-mix) + c2.r*(mix);
   result.g = c1.g*(1-mix) + c2.g*(mix);
   result.b = c1.b*(1-mix) + c2.b*(mix);

   return result;
}

Simple interpolation of the sRGB values, particularly the red-green gradient is too dark in the middle.

Intuitively, lerping ( linear interpolations) draws a straight line between two points in the colourspace being used. However, a straight line in the colourspace usually does not result in a perceptually linear interpolation.


interpolation on linear values[edit | edit source]

RGB colors on computers are in the sRGB color space. And those numerical values have a gamma of approx 2.4 applied.

Without applying the inverse gamma, the mixed colors are darker than they're supposed to be.


In order to mix the colors correctly you must first undo this gamma adjustment. Correct steps are:

  • undo the gamma adjustment ( Invert sRGB gamma compression, InverseSrgbCompanding)
  • apply your r,g,b mixing algorithm above
  • reapply the gamma ( Reapply sRGB gamma compression, SrgbCompanding)


//This is the wrong algorithm. Don't do this
Color ColorMix(Color c1, Color c2, Single mix)
{
   //Mix [0..1]
   //  0   --> all c1
   //  0.5 --> equal mix of c1 and c2
   //  1   --> all c2

   //Invert sRGB gamma compression
   c1 = InverseSrgbCompanding(c1);
   c2 = InverseSrgbCompanding(c2);

   result.r = c1.r*(1-mix) + c2.r*(mix);
   result.g = c1.g*(1-mix) + c2.g*(mix);
   result.b = c1.b*(1-mix) + c2.b*(mix);

   //Reapply sRGB gamma compression
   result = SrgbCompanding(result);

   return result;
}

The gamma adjustment of sRGB isn't quite just 2.4. They actually have a linear section near black - so it's a piecewise function.

Helper functions:

Color InverseSrgbCompanding(Color c)
{
    //Convert color from 0..255 to 0..1
    Single r = c.r / 255;
    Single g = c.g / 255;
    Single b = c.b / 255;

    //Inverse Red, Green, and Blue
    if (r > 0.04045) r = Power((r+0.055)/1.055, 2.4) else r = r / 12.92;
    if (g > 0.04045) g = Power((g+0.055)/1.055, 2.4) else g = g / 12.92;
    if (b > 0.04045) b = Power((b+0.055)/1.055, 2.4) else b = b / 12.92;

    //return new color. Convert 0..1 back into 0..255
    Color result;
    result.r = r*255;
    result.g = g*255;
    result.b = b*255;

    return result;
}

And you re-apply the companding as:

Color SrgbCompanding(Color c)
{
    //Convert color from 0..255 to 0..1
    Single r = c.r / 255;
    Single g = c.g / 255;
    Single b = c.b / 255;

    //Apply companding to Red, Green, and Blue
    if (r > 0.0031308) r = 1.055*Power(r, 1/2.4)-0.055 else r = r * 12.92;
    if (g > 0.0031308) g = 1.055*Power(g, 1/2.4)-0.055 else g = g * 12.92;
    if (b > 0.0031308) b = 1.055*Power(b, 1/2.4)-0.055 else b = b * 12.92;

    //return new color. Convert 0..1 back into 0..255
    Color result;
    result.r = r*255;
    result.g = g*255;
    result.b = b*255;

    return result;
}

The color blending in linear RGB space is good when colors are equal RGB total value; but the linear blending scale does not seem linear - especially for the black-white case. Using interpolation on linear values rather than gamma-corrected values makes the red-green gradient better, but at the expense of the back-white gradient.

separating the light intensities from the color[edit | edit source]

Steps

  • calculate L (lightness) for an RGB color:
    • calculate only the Y (luminance) of CIE XYZ and use that to get L
    • That gives L as 0-1 for any RGB
  • Then to lerp RGB:
    • first interpolate linear RGB,
    • fix lightness by lerping the start/end L
    • scale the RGB by targetL / resultL
/* Copyright (c) 2022 Nathan Sweet
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package com.esotericsoftware.hsluv;

import static com.esotericsoftware.hsluv.Hsl.*;

/** Stores an RGB color. Interpolation is done without losing brightness.
 * @author Nathan Sweet */
public class Rgb {
	public float r, g, b;

	public Rgb () {
	}

	public Rgb (Rgb rgb) {
		set(rgb);
	}

	public Rgb (float r, float g, float b) {
		set(r, g, b);
	}

	public Rgb set (Rgb rgb) {
		this.r = r < 0 ? 0 : (r > 1 ? 1 : r);
		this.g = g < 0 ? 0 : (g > 1 ? 1 : g);
		this.b = b < 0 ? 0 : (b > 1 ? 1 : b);
		return this;
	}

	public Rgb set (float r, float g, float b) {
		this.r = r;
		this.g = g;
		this.b = b;
		return this;
	}

	public Rgb set (int rgb) {
		r = ((rgb & 0xff0000) >>> 16) / 255f;
		g = ((rgb & 0x00ff00) >>> 8) / 255f;
		b = ((rgb & 0x0000ff)) / 255f;
		return this;
	}
    
    // from https://github.com/EsotericSoftware/hsl/blob/main/src/com/esotericsoftware/hsluv/Hsl.java
    static float fromLinear (float value) {
		return value <= 0.0031308f ? value * 12.92f : (float)(Math.pow(value, 1 / 2.4f) * 1.055f - 0.055f);
	}

	static float toLinear (float value) {
		return value <= 0.04045f ? value / 12.92f : (float)Math.pow((value + 0.055f) / 1.055f, 2.4f);
	}


	public Rgb lerp (Rgb target, float a) {
		if (a == 0) return this;
		if (a == 1) return set(target);
		float r = toLinear(this.r), g = toLinear(this.g), b = toLinear(this.b);
		float r2 = toLinear(target.r), g2 = toLinear(target.g), b2 = toLinear(target.b);
		float L = rgbToL(r, g, b);
		L += (rgbToL(r2, g2, b2) - L) * a;
		r += (r2 - r) * a;
		g += (g2 - g) * a;
		b += (b2 - b) * a;
		float L2 = rgbToL(r, g, b);
		float scale = L2 < 0.00001f ? 1 : L / L2;
		this.r = fromLinear(r * scale);
		this.g = fromLinear(g * scale);
		this.b = fromLinear(b * scale);
		return this;
	}

	public int toInt () {
		return ((int)(255 * r) << 16) | ((int)(255 * g) << 8) | ((int)(255 * b));
	}

	public boolean equals (Object o) {
		if (o == null) return false;
		Rgb other = (Rgb)o;
		return (int)(255 * r) == (int)(255 * other.r) //
			&& (int)(255 * g) == (int)(255 * other.g) //
			&& (int)(255 * b) == (int)(255 * other.b);
	}

	public int hashCode () {
		int result = (int)(255 * r);
		result = 31 * result + (int)(255 * g);
		return 31 * result + (int)(255 * b);
	}

	public String toString () {
		String value = Integer.toHexString(toInt());
		while (value.length() < 6)
			value = "0" + value;
		return value;
	}

	static private float rgbToL (float r, float g, float b) {
		float Y = minv[1][0] * r + minv[1][1] * g + minv[1][2] * b;
		return Y <= epsilon ? Y * kappa : 1.16f * (float)Math.pow(Y, 1 / 3f) - 0.16f;
	}
}

HSV[edit | edit source]

/*
 * Created by C.J. Kimberlin (http://cjkimberlin.com)
 * 
 * The MIT License (MIT)
 * 
 * Copyright (c) 2015 
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 * 
 * 
 * 
 * ============= Description =============
 * 
 * An ColorHSV struct for interpreting a color in hue/saturation/value instead of red/green/blue.
 * NOTE! hue will be a value from 0 to 1 instead of 0 to 360.
 * 
 * ColorHSV hsvRed = new ColorHSV(1, 1, 1, 1); // RED
 * ColorHSV hsvGreen = new ColorHSV(0.333f, 1, 1, 1); // GREEN
 * 
 * 
 * Also supports implicit conversion between Color and Color32.
 * 
 * ColorHSV hsvBlue = Color.blue; // HSVA(0.667f, 1, 1, 1)
 * Color blue = hsvBlue; // RGBA(0, 0, 1, 1)
 * Color32 blue32 = hsvBlue; // RGBA(0, 0, 255, 255) 
 *
 * 
 * If functions are desired instead of implicit conversion then use the following.
 * 
 * Color yellowBefore = Color.yellow; // RBGA(1, .922f, 0.016f, 1)
 * ColorHSV hsvYellow = Color.yellowBefore.ToHSV(); // HSVA(0.153f, 0.984f, 1, 1)
 * Color yellowAfter = hsvYellow.ToRGB(); // RBGA(1, .922f, 0.016f, 1)
 * */

using UnityEngine;

public struct ColorHSV
{
    public float h;
    public float s;
    public float v;
    public float a;

    public ColorHSV(float h, float s, float v, float a)
    {
        this.h = h;
        this.s = s;
        this.v = v;
        this.a = a;
    }

    public override string ToString()
    {
        return string.Format("HSVA: ({0:F3}, {1:F3}, {2:F3}, {3:F3})", h, s, v, a);
    }

    public static bool operator ==(ColorHSV lhs, ColorHSV rhs)
    {
        if (lhs.a != rhs.a)
        {
            return false;
        }

        if (lhs.v == 0 && rhs.v == 0)
        {
            return true;
        }

        if (lhs.s == 0 && rhs.s == 0)
        {
            return lhs.v == rhs.v;
        }

        return lhs.h == rhs.h &&
               lhs.s == rhs.s &&
               lhs.v == rhs.v;
    }

    public static implicit operator ColorHSV(Color c)
    {
        return c.ToHSV();
    }

    public static implicit operator Color(ColorHSV hsv)
    {
        return hsv.ToRGB();
    }

    public static implicit operator ColorHSV(Color32 c32)
    {
        return ((Color) c32).ToHSV();
    }

    public static implicit operator Color32(ColorHSV hsv)
    {
        return hsv.ToRGB();
    }

    public static bool operator !=(ColorHSV lhs, ColorHSV rhs)
    {
        return !(lhs == rhs);
    }

    public override bool Equals(object other)
    {
        if (other == null)
        {
            return false;
        }

        if (other is ColorHSV || other is Color || other is Color32)
        {
            return this == (ColorHSV) other;
        }

        return false;
    }

    public override int GetHashCode()
    {
        // This is maybe not a good implementation :)
        return ((Color) this).GetHashCode();
    }

    public Color ToRGB()
    {
        Vector3 rgb = HUEtoRGB(h);
        Vector3 vc = ((rgb - Vector3.one) * s + Vector3.one) * v;

        return new Color(vc.x, vc.y, vc.z, a);
    }

    private static Vector3 HUEtoRGB(float h)
    {
        float r = Mathf.Abs(h * 6 - 3) - 1;
        float g = 2 - Mathf.Abs(h * 6 - 2);
        float b = 2 - Mathf.Abs(h * 6 - 4);

        return new Vector3(Mathf.Clamp01(r), Mathf.Clamp01(g), Mathf.Clamp01(b));
    }
}

public static class ColorExtension
{
    private const float EPSILON = 1e-10f;

    public static ColorHSV ToHSV(this Color rgb)
    {
        Vector3 hcv = RGBtoHCV(rgb);
        float s = hcv.y / (hcv.z + EPSILON);

        return new ColorHSV(hcv.x, s, hcv.z, rgb.a);
    }

    private static Vector3 RGBtoHCV(Color rgb)
    {
        Vector4 p = rgb.g < rgb.b ? new Vector4(rgb.b, rgb.g, -1, 2f / 3f) : new Vector4(rgb.g, rgb.b, 0, -1f / 3f);
        Vector4 q = rgb.r < p.x ? new Vector4(p.x, p.y, p.w, rgb.r) : new Vector4(rgb.r, p.y, p.z, p.x);
        float c = q.x - Mathf.Min(q.w, q.y);
        float h = Mathf.Abs((q.w - q.y) / (6 * c + EPSILON) + q.z);

        return new Vector3(h, c, q.x);
    }
}
// https://www.alanzucconi.com/2016/01/06/colour-interpolation/
public static Color LerpHSV (ColorHSV a, ColorHSV b, float t)
{
    // Hue interpolation
    float h;
    float d = b.h - a.h;
    if (a.h > b.h)
    {
        // Swap (a.h, b.h)
        var h3 = b.h2;
        b.h = a.h;
        a.h = h3;

        d = -d;
        t = 1 - t;
    }

    if (d > 0.5) // 180deg
    {
        a.h = a.h + 1; // 360deg
        h = ( a.h + t * (b.h - a.h) ) % 1; // 360deg
    }
    if (d <= 0.5) // 180deg
    {
        h = a.h + t * d
    }

    // Interpolates the rest
    return new ColorHSV
    (
        h,            // H
        a.s + t * (b.s-a.s),    // S
        a.v + t * (b.v-a.v),    // V
        a.a + t * (b.a-a.a)    // A
    );
}

LAB[edit | edit source]

How to generate a smooth color gradient between two colors.[9]

The intensity of the gradient must be constant in a perceptual color space or it will look unnaturally dark or light at points in the gradient. You can see this easily in a gradient based on simple interpolation of the sRGB values, particularly the red-green gradient is too dark in the middle. Using interpolation on linear values rather than gamma-corrected values makes the red-green gradient better, but at the expense of the back-white gradient. By separating the light intensities from the color you can get the best of both worlds.

Often when a perceptual color space is required, the Lab color space will be proposed. I think sometimes it goes too far, because it tries to accommodate the perception that blue is darker than an equivalent intensity of other colors such as yellow. This is true, but we are used to seeing this effect in our natural environment and in a gradient you end up with an overcompensation.

A power-law function of 0.43 was experimentally determined by researchers to be the best fit for relating gray light intensity to perceived brightness.

Algorithm in pseudocode:

Algorithm MarkMix
   Input:
      color1: Color, (rgb)   The first color to mix
      color2: Color, (rgb)   The second color to mix
      mix:    Number, (0..1) The mix ratio. 0 ==> pure Color1, 1 ==> pure Color2
   Output:
      color:  Color, (rgb)   The mixed color
   
   //Convert each color component from 0..255 to 0..1
   r1, g1, b1 ← Normalize(color1)
   r2, g2, b2 ← Normalize(color1)

   //Apply inverse sRGB companding to convert each channel into linear light
   r1, g1, b1 ← sRGBInverseCompanding(r1, g1, b1)       
   r2, g2, b2 ← sRGBInverseCompanding(r2, g2, b2)

   //Linearly interpolate r, g, b values using mix (0..1)
   r ← LinearInterpolation(r1, r2, mix)
   g ← LinearInterpolation(g1, g2, mix)
   b ← LinearInterpolation(b1, b2, mix)

   //Compute a measure of brightness of the two colors using empirically determined gamma
   gamma ← 0.43
   brightness1 ← Pow(r1+g1+b1, gamma)
   brightness2 ← Pow(r2+g2+b2, gamma)

   //Interpolate a new brightness value, and convert back to linear light
   brightness ← LinearInterpolation(brightness1, brightness2, mix)
   intensity ← Pow(brightness, 1/gamma)

   //Apply adjustment factor to each rgb value based
   if ((r+g+b) != 0) then
      factor ← (intensity / (r+g+b))
      r ← r * factor
      g ← g * factor
      b ← b * factor
   end if

   //Apply sRGB companding to convert from linear to perceptual light
   r, g, b ← sRGBCompanding(r, g, b)

   //Convert color components from 0..1 to 0..255
   Result ← MakeColor(r, g, b)
End Algorithm MarkMix

Python code by Mark Ransom

''' https://stackoverflow.com/questions/22607043/color-gradient-algorithm'''
def all_channels(func):
    def wrapper(channel, *args, **kwargs):
        try:
            return func(channel, *args, **kwargs)
        except TypeError:
            return tuple(func(c, *args, **kwargs) for c in channel)
    return wrapper

@all_channels
def to_sRGB_f(x):
    ''' Returns a sRGB value in the range [0,1]
        for linear input in [0,1].
    '''
    return 12.92*x if x <= 0.0031308 else (1.055 * (x ** (1/2.4))) - 0.055

@all_channels
def to_sRGB(x):
    ''' Returns a sRGB value in the range [0,255]
        for linear input in [0,1]
    '''
    return int(255.9999 * to_sRGB_f(x))

@all_channels
def from_sRGB(x):
    ''' Returns a linear value in the range [0,1]
        for sRGB input in [0,255].
    '''
    x /= 255.0
    if x <= 0.04045:
        y = x / 12.92
    else:
        y = ((x + 0.055) / 1.055) ** 2.4
    return y

def all_channels2(func):
    def wrapper(channel1, channel2, *args, **kwargs):
        try:
            return func(channel1, channel2, *args, **kwargs)
        except TypeError:
            return tuple(func(c1, c2, *args, **kwargs) for c1,c2 in zip(channel1, channel2))
    return wrapper

@all_channels2
def lerp(color1, color2, frac):
    return color1 * (1 - frac) + color2 * frac



def perceptual_steps(color1, color2, steps):
    gamma = .43
    color1_lin = from_sRGB(color1)
    bright1 = sum(color1_lin)**gamma
    color2_lin = from_sRGB(color2)
    bright2 = sum(color2_lin)**gamma
    for step in range(steps):
        intensity = lerp(bright1, bright2, step, steps) ** (1/gamma)
        color = lerp(color1_lin, color2_lin, step, steps)
        if sum(color) != 0:
            color = [c * intensity / sum(color) for c in color]
        color = to_sRGB(color)
        yield color


The default color space for mixing (and gradients) in CSS is oklab

perceptually uniform gradient[edit | edit source]

How to Programmatically Lighten a Color ?[edit | edit source]

How to convert hsl color to name ?[edit | edit source]

Examples:

  • hsl(120,100%,50%) = A highly saturated green
  • hsl(120,65%,25%) = A rather saturated, dark green.


# https://github.com/LeaVerou/human-colours/blob/master/py/en_gb.py

# hue
def hueName(h):
	if h <  15: hue = 'red'
	if h == 15: hue = 'reddish'
	if h >  15: hue = 'orange'
	if h >  45: hue = 'yellow'
	if h >  70: hue = 'lime'
	if h >  79: hue = 'green'
	if h > 163: hue = 'cyan'
	if h > 193: hue = 'blue'
	if h > 240: hue = 'indigo'
	if h > 260: hue = 'violet'
	if h > 270: hue = 'purple'
	if h > 291: hue = 'magenta'
	if h > 327: hue = 'rose'
	if h > 344: hue = 'red'
	return hue

# saturation
def saturationName(s):
	if s < 10: sat = 'almost grey'
	if s >  9: sat = 'very unsaturated'
	if s > 30: sat = 'unsaturated'
	if s > 60: sat = 'rather saturated'
	if s > 80: sat = 'highly saturated'
	return sat

# lightness
def lightnessName(l):
	if l < 10: light = 'almost black'
	if l >  9: light = 'very dark'
	if l > 22: light = 'dark'
	if l > 30: light = 'normal'
	if l > 60: light = 'light'
	if l > 80: light = 'very light'
	if l > 94: light = 'almost white'
	return light
 
 # https://github.com/LeaVerou/human-colours/blob/master/py/en_gb_example.py
    
     # generate human color names
    t = 'a %s, %s, %s rectangle\non a %s, %s %s background' % \
        (hueName(h1*360), saturationName(s1*100), lightnessName(l1*100), \
        hueName(h2*360), saturationName(s2*100), lightnessName(l2*100))

spline gradient[edit | edit source]

  • spline gradient in js by Matt DesLauriers


// https://gist.github.com/mattdesl/2a7b2013492cbcbafc797d3f9164e92c
global.THREE = require("three");

const canvasSketch = require('canvas-sketch');
const Random = require('canvas-sketch-util/random');

const gradientHeight = 512;
const settings = {
  dimensions: [ 2048, gradientHeight * 2 ]
};

const sketch = (props) => {
  return ({ context, width, height }) => {
    context.fillStyle = 'white';
    context.fillRect(0, 0, width, height);

    // your stepped color data, to be filled in
    const colorsAsHexList = [ /* '#ff0000' */ ];
    const colorsAsLabList = [ /* [ 100, 0, 0 ] */ ];

    const grd = context.createLinearGradient(0, 0, width, 0);
    colorsAsHexList.forEach((hex, i, list) => {
      const t = i / (list.length - 1);
      grd.addColorStop(t, hex);
    });
    context.fillStyle = grd;
    context.fillRect(0, 0, width, gradientHeight);

    img = context.createImageData(width, gradientHeight);
    // draw curve
    const labPositions = colorsAsLabList.map(lab => {
      return new THREE.Vector3().fromArray(lab);
    });
    const curve = new THREE.CatmullRomCurve3(labPositions);
    curve.curveType = 'catmullrom';

    // can play with tension to make sharper or softer gradients
    curve.tension = 0.5;

    // uncomment to make a seamless gradient that wraps around
    // curve.closed = true;

    const samples = getCurveDivisions(curve, img.width, false)
      .map(p => p.toArray())
    for (let y = 0; y < img.height; y++) {
      for (let x = 0; x < img.width; x++) {
        const i = x + y * img.width;
        let Lab = labJitter(samples[x], 0.5);
        // lab2rgb not implemented here
        const [ R, G, B ] = YourColorConverter.lab2rgb(Lab);
        img.data[i * 4 + 0] = R;
        img.data[i * 4 + 1] = G;
        img.data[i * 4 + 2] = B;
        img.data[i * 4 + 3] = 255;
      }
    }
    context.putImageData(img, 0, gradientHeight);
  };

  // Entirely optional, but as we are doing this per-pixel we may as well
  // add subtle noise to reduce banding.
  function labJitter (lab, s = 1) {
    // fudged jittering in L*a*b* space
    const lw = 100 / 200;
    const K = 2.624;
    const [ x, y, z ] = Random.insideSphere(Random.gaussian(0, s * K))
    const [ L, a, b ] = lab;
    return [ L + x * lw, a + y, b + z ];
  }

  function getCurveDivisions (curve, n, spaced = false) {
    const points = [];
    for (let i = 0; i < n; i++) {
      const t = curve.closed ? (i / n) : (i / (n - 1));
      let p = spaced ? curve.getPointAt(t) : curve.getPoint(t);
      points.push(p);
    }
    return points;
  }
};

canvasSketch(sketch, settings);

See also[edit | edit source]

References[edit | edit source]

  1. srgb-transform-library by Nayuki
  2. stackoverflow question : what-are-the-practical-differences-when-working-with-colors-in-a-linear-vs-a-no
  3. Björn Ottosson: How software gets color wrong
  4. oklab: A perceptual color space for image processing
  5. OKLAB by Tycho Tatitscheff
  6. github 0xStarlight ( Bhaskar Pal) : Filter-Program
  7. Doug-Luce Grayscale rev9.py code
  8. alan zucconi: colour-interpolation
  9. stackoverflow question: color-gradient-algorithm
  10. stackoverflow questions: programmatically-lighten-a-color