跳转至内容

色彩理论/算法

来自维基教科书,开放的书籍,开放的世界
  "color operations should be done ...to either model human perception or the physical behavior of light" Björn Ottosson : How software gets color wrong 


颜色转换

[编辑 | 编辑源代码]
// 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);
}

sRGB 内部转换

[编辑 | 编辑源代码]

来自 CSS 的 js 代码

[编辑 | 编辑源代码]
// 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;
	});
}

线性化

[编辑 | 编辑源代码]

步骤[2]

  • 读取值(来自 sRGB)
  • 线性化并扩展强度(将 8 位整数转换为 16 位浮点数整数)
  • 处理图像
  • 去线性化并转换为 8 位
  • 另存为 sRGB


当你读取 sRGB 图像并想要线性强度时,将此公式应用于每个强度

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


反过来,当你想要将图像另存为 sRGB 时,将此公式应用于每个线性强度

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;



阶段:[3]

  • 应用 sRGB 非线性转换函数 f_inv 的逆
  • 进行计算
  • 然后通过 f 切换回来

对于 sRGB 中范围为 0.0 到 1.0 的颜色,可以通过对这些函数分量进行操作来实现(以 C 语言类伪代码提供)


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
}

HSL 与 sRGB 之间的转换

[编辑 | 编辑源代码]

在 JavaScript 中将 HSL 颜色转换为 sRGB。

/**
 * 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)];
}

将 sRGB 颜色转换为 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];
}

从线性 sRGB 转换为 Oklab

[编辑 | 编辑源代码]

Oklab 中的颜色用三个坐标 Lab 表示:[4][5]

  • L - 感知亮度(范围为 [0,1] 的无量纲数)
  • a - 颜色是绿色/红色程度
  • b - 颜色是蓝色/黄色程度


其对应的极坐标形式称为 Oklch。

标准坐标也可以转换为极坐标形式(Lch),坐标为

  • L = 亮度
  • c = 色彩
  • h = 色相

反向

在 c++ 中从线性 sRGB 转换为 Oklab

// 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,
    };
}

图像转换

[编辑 | 编辑源代码]

C 代码[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;
}

灰度转换

[编辑 | 编辑源代码]

Python 代码[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)

矩阵乘法

[编辑 | 编辑源代码]

矩阵乘法。(这比内联所有乘法和加法更易读)。矩阵采用列优先顺序。

/**
 * 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;
}

混合颜色

[编辑 | 编辑源代码]

方法

  • sRGB 值的简单插值(伽马校正值)。Lerp
  • 线性值插值使红绿渐变更好,但代价是黑白渐变变差。
  • 色相插值(在 HSV、HSB 中)[8]。我们可以将 RGB 转换为 HSV 并对 H、S 和 V 每个分量进行 Lerp。这也很糟糕。
  • 分离光强度与颜色
  • 亮度插值:最好的方法是在感知线性色彩空间(LAB、OKLab、HCL...)中进行线性插值。CSS 中混合(和渐变)的默认色彩空间是 oklab


目标:渐变的强度必须保持恒定

代码

简单插值

[编辑 | 编辑源代码]

混合 2 个 RGB 颜色(幼稚且错误的形式)

//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;
}

对 sRGB 值进行简单的插值,特别是红绿渐变在中间太暗了。

直观地说,线性插值(线性插值)在所使用的色彩空间中,在两个点之间画一条直线。但是,色彩空间中的直线通常不会产生感知线性插值。


对线性值的插值

[编辑 | 编辑源代码]

计算机上的 RGB 颜色处于 sRGB 色彩空间。这些数值应用了大约 2.4 的伽马。

如果不应用逆伽马,混合的颜色将比预期更暗。


为了正确地混合颜色,你必须先撤销这种伽马调整。正确的步骤是

  • 撤销伽马调整(反转 sRGB 伽马压缩,InverseSrgbCompanding)
  • 应用你的 r、g、b 混合算法(如上)
  • 重新应用伽马(重新应用 sRGB 伽马压缩,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;
}

sRGB 的伽马调整并不完全是 2.4。实际上,它们在黑色附近有一个线性部分 - 所以它是一个分段函数。

辅助函数

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;
}

然后你重新应用压缩为

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;
}

当颜色具有相等的 RGB 总值时,线性 RGB 空间中的颜色混合很好;但线性混合比例似乎并不线性 - 特别是对于黑白情况。在对线性值而不是伽马校正值进行插值时,红绿渐变效果更好,但以牺牲黑白渐变为代价。

分离光强度与颜色

[编辑 | 编辑源代码]

步骤

  • 为 RGB 颜色计算 L(亮度)
    • 仅计算 CIE XYZ 的 Y(亮度),并使用它来获取 L
    • 这将 L 设置为任何 RGB 的 0-1
  • 然后对 RGB 进行线性插值
    • 首先插值线性 RGB
    • 通过对开始/结束 L 进行线性插值来修正亮度
    • 将 RGB 按目标 L / 结果 L 缩放
/* 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;
	}
}
/*
 * 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
    );
}

如何在两种颜色之间生成平滑的彩色渐变。[9]

渐变的强度必须在感知色彩空间中保持恒定,否则它在渐变中的某些点看起来会不自然地暗或亮。在基于 sRGB 值简单插值的渐变中,你可以很容易地看到这一点,特别是红绿渐变在中间太暗了。在对线性值而不是伽马校正值进行插值时,红绿渐变效果更好,但以牺牲黑白渐变为代价。通过将光强度与颜色分离,你可以同时获得两全其美的效果。

当需要感知色彩空间时,通常会提出 Lab 色彩空间。我认为它有时太过分了,因为它试图适应蓝色比黄色等其他颜色相同强度更暗的感知。这是真的,但我们习惯于在我们自然环境中看到这种效果,在渐变中,你会得到过度补偿。

研究人员通过实验确定了 0.43 的幂律函数最适合将灰度光强度与感知亮度联系起来。

伪代码中的算法

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

Mark Ransom 编写的 Python 代码

''' 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


CSS 中混合(和渐变)的默认色彩空间是 oklab

感知均匀渐变

[编辑 | 编辑源代码]

如何通过编程方式使颜色变亮 ?

[编辑 | 编辑源代码]

如何将 hsl 颜色转换为名称 ?

[编辑 | 编辑源代码]

例子

  • hsl(120,100%,50%) = 高饱和的绿色
  • hsl(120,65%,25%) = 饱和度较高、较深的绿色。


# 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))

样条渐变

[编辑 | 编辑源代码]
  • Matt DesLauriers 编写的 js 中的样条渐变


// 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);

参考文献

[编辑 | 编辑源代码]
  1. Nayuki 编写的 srgb-transform-library
  2. stackoverflow 问题 : 在使用线性与非线性颜色时,实际差异是什么
  3. Björn Ottosson:软件如何错误地处理颜色
  4. oklab:用于图像处理的感知色彩空间
  5. Tycho Tatitscheff 编写的 OKLAB
  6. github 0xStarlight (Bhaskar Pal) : Filter-Program
  7. Doug-Luce Grayscale rev9.py 代码
  8. alan zucconi:颜色插值
  9. stackoverflow 问题:颜色渐变算法
  10. stackoverflow 问题:通过编程方式使颜色变亮
华夏公益教科书