颜色空间的互相转换
前言
在上一篇中,我们介绍了常见颜色空间的一些定义及表示,在这一章中,我们将大致了解各个颜色空间的互相转换
颜色转换算法
由于有些颜色空间可能并不能直接转换,或着过于繁杂,本文主要介绍由RGB向其它空间的转换,涉及到的代码也采用Ts
进行演示讲解
在文章的最后面,会给出封装的转换算法(TS版),如对文章内容不感兴趣,可直接拖到文末查看获取方法
HEX
将一个RGB颜色转换为HEX模式,其实就是将十进制值转换为十六进制,没什么好说的,直接看代码理解即可
/**
* RGB 转为 HEX
* @param {number} r [0-255]红色通道值
* @param {number} g [0-255]绿色通道值
* @param {number} b [0-255]蓝色通道值
* @param {boolean} ad 是否带 # ,默认带有
* @return {HEX} 返回转换的 hex 值
*/
const rgbToHex = function (
r: number,
g: number,
b: number,
ad: boolean = true
): HEX {
if (ad) return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
else return `${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
};
主要就是按顺序将RGB各分量值,通过左移运算转换为一个对应十六进制的十进制数,最后将其转换为十六进制字符串即可
CMYK
RGB只有三个通道(取值范围0-255
),而CMYK有四个通道(取值范围0-100
),故从RGB到CMYK的转换主要两个过程:
-
RGB转CMY
- \(C = (255 - R) / 255\)
- \(M = (255 - G) / 255\)
- \(Y = (255 - B) / 255\)
-
CMY转CMYK
- \(K = \min (C, M, Y)\)
- \(C = (C - K) / (1 - K)\)
- \(M = (M - K) / (1 - K)\)
- \(Y = (Y - K) / (1 - K)\)
具体的代码为:
/**
* RGB 转为 CMYK
* @param {number} r [0-255]红色通道值
* @param {number} g [0-255]绿色通道值
* @param {number} b [0-255]蓝色通道值
* @return {CMYK} 返回转换的 cmyk 值
*/
const rgbToCmyk = function (
r: number,
g: number,
b: number
): CMYK {
let c = (255 - r) / 255;
let m = (255 - g) / 255;
let y = (255 - b) / 255;
let k = Math.min(c, m, y);
if (k === 1) {
// 此时为纯黑色,其它分量均为零
c = m = y = 0;
} else {
let kk = 1 - k;
c = (c - k) / kk;
m = (m - k) / kk;
y = (y - k) / kk;
}
return {
c: toFixed(c * 100, 0),
m: toFixed(m * 100, 0),
y: toFixed(y * 100, 0),
k: toFixed(k * 100, 0)
};
};
// toFixed为一个保留指定小数位的函数
该算法只是比较粗糙的转换,由于两个颜色空间色域并不一致,故转换过程中可能会存在一定程度的颜色偏差和失真
HSV
在开始转换之前,先分析一下各分量的值域,首先是RGB,值域为[0-255]
;接着是HSV,H值域为[0-360]
,S和V的值域是[0-100]
转换的第一步就是统一分量值域
- 先将RGB的值域转换为
[0-1]
,由此计算出来的S和V值域也为[0-1]
(H不变),后续根据需要转变即可 - 然后,根据一下公式步骤计算
对应的转换代码为:
/**
* RGB 转为 HSV
* @param {number} r [0-255]红色通道值
* @param {number} g [0-255]绿色通道值
* @param {number} b [0-255]蓝色通道值
* @return {HSV} 返回转换的 hsv 值
*/
const rgbToHsv = function (
r: number,
g: number,
b: number
): HSV {
r = r / 255; // [0, 1]
g = g / 255; // [0, 1]
b = b / 255; // [0, 1]
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const d = max - min;
const v = max;
const s = max === 0 ? 0 : d / max;
let h = 0;
if (d !== 0) {
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / d + 2;
break;
case b:
h = (r - g) / d + 4;
break;
default:
break;
}
h = h / 6;
}
// 返回时再规整值域
return {
h: toFixed(h * 360),
s: toFixed(s * 100),
v: toFixed(v * 100)
};
};
HSL
在转换前,同样需先确定好各分量的计算值域
HSL与HSV比较类似,其中的H均表示色调(色相),值域为[0-360]
;接着规定S和L的值域为[0-1]
,故RGB同样需转换为[0-1]
的值
接着根据计算公式计算:
具体的代码如下:
/**
* RGB 转为 HSL
* @param {number} r [0-255]红色通道值
* @param {number} g [0-255]绿色通道值
* @param {number} b [0-255]蓝色通道值
* @return {HSL} 返回转换的 hsl 值
*/
const rgbToHsl = function (
r: number,
g: number,
b: number
): HSL {
r = r / 255; // [0, 1]
g = g / 255; // [0, 1]
b = b / 255; // [0, 1]
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const d = max - min;
const l = (max + min) / 2;
const s = d === 0 ? 0 : l > 0.5 ? d / (2 - 2 * l) : d / (2 * l);
let h = 0;
if (d !== 0) {
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / d + 2;
break;
case b:
h = (r - g) / d + 4;
break;
default:
break;
}
h = h / 6;
}
return {
h: toFixed(h * 360),
s: toFixed(s * 100),
l: toFixed(l * 100)
};
};
LAB
LAB是一种色域极广的颜色模式,相较于RGB,其采用更加科学的颜色表示方法,它是基于人眼对颜色的感知来定义的,其主要有三个分量:L表示亮度,A表示绿色到红色的色差,B表示蓝色到黄色的色差
由于两个颜色空间的定义原理,RGB无法直接转换为LAB,需用XYZ作为一个中间层,即RGB转XYZ,再转LAB
XYZ也是一个颜色空间,其全称为CIE 1931 XYZ色彩空间(也叫做CIE 1931色彩空间),由国际照明委员会(CIE)于1931年创立。
XYZ是为了解决更精确地定义色彩而提出来的, 其三个分量中, XY代表的是色度, 而Y既可以代表亮度也可以代表色度,单位为
nit
。在日常生活中,我们无法用RGB来精确定义颜色, 因为,不同的设备显示的RGB其实都是不一样的,不同的设备, 显示同一个RGB, 在人眼看出来可能是千差万别的, XYZ就是为了解决这样的问题,使不同设备颜色显示更精确
具体的转换步骤为:
- RGB归一化,即值域转变为
[0-1]
- 将RGB值进行逆伽马校正
- 具体就是将各分量值传入逆伽马校正的函数内求结果,可根据实际情况使用不同标准的逆伽马函数或者设备的校准曲线
- 接着将校正后的RGB转换为XYZ空间值
- 接着将XYZ进行归一计算,使其归一到参考白点,常用的是
D65
白点(0.950456, 1, 1.088754)
- 然后将XYZ值再进行非线性变换
- 最后计算转换为LAB值
大致的数学公式如下:
大致的代码如下:
/**
* RGB 转为 XYZ
* @param {number} r [0-255]红色通道值
* @param {number} g [0-255]绿色通道值
* @param {number} b [0-255]蓝色通道值
* @return {XYZ} 返回转换的 xyz 值
*/
const rgbToXyz = function (
r: number,
g: number,
b: number
): XYZ {
// 归一化
r = r / 255;
g = g / 255;
b = b / 255;
r = gamma(r);
g = gamma(g);
b = gamma(b);
let x = 0.4124564 * r + 0.3575761 * g + 0.1804375 * b;
let y = 0.2126729 * r + 0.7151522 * g + 0.072175 * b;
let z = 0.0193339 * r + 0.119192 * g + 0.9503041 * b;
return {
x,
y,
z
};
};
const XN = 0.950456;
const YN = 1;
const ZN = 1.088754;
/**
* XYZ 转 LAB
* @param x
* @param y
* @param z
*/
const xyzToLab = function (
x: number,
y: number,
z: number
): LAB {
// 归一化
x = x / XN;
y = y / YN;
z = z / ZN;
x = f(x);
y = f(y);
z = f(z);
let l = 116 * y - 16;
let a = 500 * (x - y);
let b = 200 * (y - z);
return {
l,
a,
b
};
};
/**
* RGB 转为 LAB
* @param {number} r [0-255]红色通道值
* @param {number} g [0-255]绿色通道值
* @param {number} b [0-255]蓝色通道值
* @return {LAB} 返回转换的 Lab 值
*/
const rgbToLab = function (
r: number,
g: number,
b: number
): LAB {
let xyz = rgbToXyz(r, g, b);
let lab = xyzToLab(xyz.x, xyz.y, xyz.z);
return {
l: toFixed(lab.l),
a: toFixed(lab.a),
b: toFixed(lab.b)
};
};
/**
* XYZ 转 LAB 的非线性变换
* @param {number} t x, y, z的值
*/
function f(t: number): number {
// Math.pow(29 / 6, 2) / 3 = 7.787037037037035
// 16 / 116 = 0.13793103448275862
if (t > 0.008856) {
return Math.pow(t, 1 / 3);
} else {
return 7.787037037037035 * t + 0.13793103448275862;
}
}
/**
* gamma变换
* @param t - r, g, b值
*/
function gamma(t: number) {
return
t > 0.04045 ?
Math.pow((t + 0.055) / 1.055, 2.4)
:
t / 12.92;
}
上方给出的转换方法并不是绝对的,不同转换方法会有不同标准,具体根据自己需要选择
由于篇幅有限,这里只给出了RGB转换向其它的空间的,至于其它空间转换向RGB的并没给出,但具体的代码均封装为一个包(
TS
版),具体可按下列方法获取封装的转换算法获取方法:在公众号:代码杂谈内回复颜色转换算法