色彩理论/算法
"color operations should be done ...to either model human perception or the physical behavior of light" Björn Ottosson : How software gets color wrong
- sRGB 到线性转换[1]
- AMPAS 学院色彩编码系统开发人员资源
// 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);
}
// 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 }
在 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];
}
- 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
目标:渐变的强度必须保持恒定
代码
- d3js:插值颜色 在 js 中
- R colorRamp 函数
混合 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
- Roman Frołow 编写的 color-gradient-algorithm
- Matt DesLauriers 编写的 perceptually-smooth-multi-color-linear-gradients
- color-gradient-algorithm-in-lab-color-space
- Olivier Vicario 编写的 pereptual-color
- SO 问答[10]
- Lea Verou:human-colours:一组函数,你可以使用它们将 HSL 颜色表示法转换为人类可读的文本。
- VASILIS VAN GEMERT 尝试将正确的颜色名称映射到 hsl-hue 值
- 尝试将正确的饱和度名称映射到 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);
- 计算机图形技术 : 2D 算法
- pure-color = Wicky Nilliams 编写的 JavaScript 中用于颜色转换和解析的纯函数
- Bruce Lindbloom 编写的有用的颜色方程
- Bruce Lindbloom 编写的从数据集获取最佳伽马
- JS 中 cpetry 编写的 ColorConverter
- 简单 RGB
- stackoverflow:标记为 rgb 的问题
- ↑ Nayuki 编写的 srgb-transform-library
- ↑ stackoverflow 问题 : 在使用线性与非线性颜色时,实际差异是什么
- ↑ Björn Ottosson:软件如何错误地处理颜色
- ↑ oklab:用于图像处理的感知色彩空间
- ↑ Tycho Tatitscheff 编写的 OKLAB
- ↑ github 0xStarlight (Bhaskar Pal) : Filter-Program
- ↑ Doug-Luce Grayscale rev9.py 代码
- ↑ alan zucconi:颜色插值
- ↑ stackoverflow 问题:颜色渐变算法
- ↑ stackoverflow 问题:通过编程方式使颜色变亮