实现图像智能配色
标题党了,说是智能配色,其实只是简单的色相变更而已。只不过在实际生产阶段,还会根据不同的场景自动计算色相值,所以就叫智能配色了 🥹。
本文需要特别感谢 ChatGPT 提供的技术支持(笑晕)。
最近在做模板配色相关的事情,简单来说就是一个模板可以快速更换成不同颜色主题的模板。例如下图所示
这是默认的模板样式:
选一个想要的配色:
这是选择了主题之后更新的样式:
截图所示的功能是京东玲珑编辑器中的智能配色功能。
因为业务上也需要实现相似的能力,所以就简单分析了一下它的实现原理。
关于 HSB 色彩模型
想要了解智能更换配色我们需要先了解一下 HSB 色彩模型。
HSB 指的是色相(Hue)、饱和度(Saturation)、明度(Brightness)。
而图片中的每一个像素都可以用 HSB 模型来表示。
所以我们可以通过调整 HSB 模型中的色相、饱和度、明度来改变像素点的颜色。
智能配色的原理
在清楚了 HSB 配色后,我们就可以来分析一下智能配色的原理了。
我们可以只调整 HSB 模型中的色相(Hue)值,来改变像素点的颜色。
我们可以通过 sharp (opens in a new tab) 这个库来实现 HSB 模型的转换。
由于 sharp 无法直接将图片的像素点转换为 hsb 模型,所以我们需要先将图片转换为 raw 格式,然后再转换为 hsb 模型。
先找地方抄两个 hsb 转换的函数:
function rgbToHsb(r, g, b) {
(r /= 255), (g /= 255), (b /= 255);
let max = Math.max(r, g, b),
min = Math.min(r, g, b);
let h,
s,
v = max;
let d = max - min;
s = max === 0 ? 0 : d / max;
if (max === min) {
h = 0; // achromatic
} else {
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;
}
h /= 6;
}
return [h * 360, s * 100, v * 100];
}
// HSB 到 RGB 的转换函数
function hsbToRgb(h, s, v) {
let r, g, b;
let i = Math.floor(h * 6);
let f = h * 6 - i;
let p = v * (1 - s);
let q = v * (1 - f * s);
let t = v * (1 - (1 - f) * s);
switch (i % 6) {
case 0:
(r = v), (g = t), (b = p);
break;
case 1:
(r = q), (g = v), (b = p);
break;
case 2:
(r = p), (g = v), (b = t);
break;
case 3:
(r = p), (g = q), (b = v);
break;
case 4:
(r = t), (g = p), (b = v);
break;
case 5:
(r = v), (g = p), (b = q);
break;
}
return [r * 255, g * 255, b * 255];
}
然后我们就可以通过 sharp 来实现智能配色了。
function changeHue(imageBuffer, newHue) {
return sharp(imageBuffer)
.toColourspace("srgb")
.raw()
.toBuffer({ resolveWithObject: true })
.then(({ data, info }) => {
for (let i = 0; i < data.length; i += info.channels) {
let [r, g, b] = [data[i], data[i + 1], data[i + 2]];
let [h, s, v] = rgbToHsb(r, g, b);
// 设置新的色相值
h = newHue;
// 转换回 RGB
[r, g, b] = hsbToRgb(h / 360, s / 100, v / 100);
[data[i], data[i + 1], data[i + 2]] = [r, g, b];
}
return sharp(data, {
raw: {
width: info.width,
height: info.height,
channels: info.channels,
},
}).toColourspace("srgb");
});
}
const hueArr = [360];
hueArr.forEach((hue) => {
sharp("input.png")
.toBuffer()
.then((buffer) => {
return changeHue(buffer, hue);
})
.then((data) => {
return data.toFile(`${hue}_output.png`);
})
.catch((err) => {
console.log(err);
});
});
看看效果:
可以看到,图片中的色相已经被变更了,awesome!
我们再找一张实际生产环境中图片看看效果:
原图:
效果图:
请忽略中间人物图的色相变更,因为在实际生产场景中,我们只会变更背景图&装饰图的色相。
更进一步,渐变色
仔细看玲珑的智能配色,其实还支持渐变色的替换,也就是说不是直接更换 Hue 值就可以来,还需要根据渐变色的配置入参。
const params = {
// 渐变色的色相值
hueArr: [360, 180, 90, 0],
// 角度
angle: 90,
}
所以我们就需要再实现一个 changeHueWithGradient
函数,来实现渐变色的替换。
但是这个函数转换听起来好像很复杂,因为需要进行一些数学换算,得到当前像素点的色相值。但是问题不大,因为我们有 ChatGPT。把我们的诉求告诉 ChatGPT 就好,他会帮我们实现:
先看最简单的两种色相渐变如何实现:
// 两种色相的渐变
function calculateGradientHue(startHue, endHue, x, y, width, height, angle) {
// 将角度转换为弧度
const radians = angle * (Math.PI / 180);
// 计算渐变的线性插值
const ratio =
(x * Math.cos(radians) + y * Math.sin(radians)) /
(width * Math.cos(radians) + height * Math.sin(radians));
// 计算并返回当前位置的色相
return startHue + ratio * (endHue - startHue);
}
但是如果能支持多种色相渐变,且支持定义每个色相在图片中的位置,那就更好了。
function calculateGradientHue(hues, positions, x, y, width, height, angle) {
// 将角度转换为弧度
const radians = angle * (Math.PI / 180);
// 计算渐变的线性插值
const ratio =
(x * Math.cos(radians) + y * Math.sin(radians)) /
(width * Math.cos(radians) + height * Math.sin(radians));
// 确定当前像素所处的渐变段
for (let i = 0; i < positions.length - 1; i++) {
if (ratio >= positions[i] && ratio <= positions[i + 1]) {
// 计算当前段内的相对位置
const segmentRatio =
(ratio - positions[i]) / (positions[i + 1] - positions[i]);
// 计算并返回当前位置的色相
return hues[i] + segmentRatio * (hues[i + 1] - hues[i]);
}
}
// 如果比例超出范围,则返回最后一个色相值
return hues[hues.length - 1];
}
完成!
再简单改一下 changeHue
的实现,让它能够处理渐变色:
function changeHue(imageBuffer, startHue, positions, angle) {
// ...
if (Array.isArray(startHue)) {
// 计算当前像素的色相
h = calculateGradientHue(
startHue,
positions,
x,
y,
info.width,
info.height,
angle,
);
} else {
// 设置新的色相值
h = startHue;
}
//...
}
sharp("input.png")
.toBuffer()
.then((buffer) => {
return changeHue(buffer, [50, 100, 200, 300], [0, 0.3, 0.5, 1], 90);
})
.then((data) => {
return data.toFile(`output.png`);
})
.catch((err) => {
console.log(err);
});
打完收工!
可惜的是 sharp 暂时只能在 node 环境下执行,因为底层依赖了 https://github.com/libvips/libvips (opens in a new tab) 这个库,所以在浏览器环境下是无法使用的。
至于怎么在游览器中实现智能配色,还得再想想。
效果展示
纯色
渐变色
使用 WebGL 实现
1.18 update
又研究了一把,发现可以使用 WebGL 着色器来实现来实现同样的效果。不过由于我对 WebGL 不是很熟悉,所以过程实在太过艰难。
我和 ChatGPT 对谈了几个小时才写出来,不过暂时只实现了纯色的效果,渐变色的效果还没实现,但是原理应该近似。
在这里不得不再感叹一下科技改版世界,如果不是有 ChatGPT 的帮助,我连第一版都要做很长时间的技术调研,到处查 API。 更别谈第二版的 WebGL 实现了,我连 WebGL 的基本概念都不知道,想把 WebGL 的代码直接实现出来不知道要等到何年何月。
我这里是直接通过 fabric.js 的 WebGLRenderer 来实现的,直接贴代码,原理什么的后面再慢慢补吧,先把代码贴出来,有兴趣的可以自己看看。
fabric.Image.filters.CustomHue = fabric.util.createClass(fabric.Image.filters.BaseFilter, {
type: 'CustomHue',
// 构造函数,用于初始化 hue 值
initialize: function(options) {
options = options || {};
this.hue = options.hue || 0;
},
getUniformLocations: function ( gl, program ) {
return {
hue: gl.getUniformLocation( program, 'hue' ),
};
},
sendUniformData: function(gl, uniformLocations) {
const hue = this.hue;
gl.uniform1f( uniformLocations.hue, hue );
},
fragmentSource: 'precision highp float;\n' +
'uniform sampler2D uTexture;\n' +
'uniform float hue;\n' +
'varying vec2 vTexCoord;\n' +
'vec3 rgbToHsv(vec3 c) {\n' +
'vec4 K = vec4(0.0, -1.0/3.0, 2.0/3.0, -1.0);\n' +
'vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));\n' +
'vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));\n' +
'float d = q.x - min(q.w, q.y);\n' +
'float e = 1.0e-10;\n' +
'return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);\n' +
'}\n' +
'vec3 hsvToRgb(vec3 c) {\n' +
'vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);\n' +
'vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);\n' +
'return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);\n' +
'}\n' +
'void main() {\n' +
'vec4 color = texture2D(uTexture, vTexCoord);\n' +
'vec3 hsv = rgbToHsv(color.rgb);\n' +
'hsv.x = hue / 360.0;\n' + // 将度转换为归一化的色相值
'vec3 rgb = hsvToRgb(hsv);\n' +
'gl_FragColor = vec4(rgb, color.a);\n' +
'}'
});
fabric.Image.filters.CustomHue.fromObject = fabric.Image.filters.BaseFilter.fromObject;
fabric.Image.fromURL('来个图片链接', function(img) {
const customHueFilter = new fabric.Image.filters.CustomHue({
hue: 180
});
img.filters.push(customHueFilter);
img.applyFilters();
},{
crossOrigin: 'anonymous'
});
今天算是正经第一次认识到 WebGL,还挺好用,后面找个时间好好学习一下,靠谱.jpg。