需求
在独立开发者训练营开发lostelk照片分享应用时产生了两个想法:
- 提取图片主色作为背景色
- 通过颜色名称搜索图像
在当今的网站和应用程序开发中,用户体验是至关重要的。其中,颜色在创造视觉吸引力和传达品牌氛围方面扮演着至关重要的角色。而提取图片的主色作为背景色,能够为用户提供一种和谐、一致的视觉体验,为应用程序带来更具吸引力的外观。另外,结合颜色名称搜索图像的功能,可以为用户提供一种直观的搜索体验,使他们能够通过感知和色彩直观地搜索图像。
技术栈
客户端:浏览器环境下的任意JavaScript技术栈,本文案例Vue3.0
服务端:Node.js
需求分析
- 上传照片时首先提取图像颜色
- 获取所提取颜色的中英文名称
- 在服务端将所提取的图像颜色和对应的中英文名称 存入数据库中,以便提供图像搜索服务(Node与数据库相关内容本文不赘述,有兴趣可以参加王皓老师的独立开发者训练营)
提取图像颜色
方案一: 造轮子
以下代码仅实现了基础功能,没有做更深入的优化,供有兴趣的同学研究一下。没有兴趣可以跳至方案二 或直接使用本文的最终方案------color-extraction。
- 首先创建一个canvas容器
- 将图片绘制到容器中
- 使用getImageData方法获取rgba, 查看getImageData
- 通过中位数切分算法切割并提取颜色
- 筛选掉相似的颜色
color.vue
ts
<template>
<div>
<canvas style="display: none" id="canvas"></canvas>
<div
id="extract-color-id"
class="extract-color"
style="display: flex;padding: 0 20px; justify-content:end;"
></div>
</div>
</template>
<script lang="ts">
import { defineComponent, onMounted } from 'vue';
import themeColor from '../../components/colorExtraction';
export default defineComponent({
setup() {
/**
* 设置颜色方法
*/
const SetColor = (colorArr: number[][]) => {
// 初始化删除多余子节点
const extractColor = document.querySelector(
'#extract-color-id'
) as HTMLElement;
while (extractColor.firstChild) {
extractColor.removeChild(extractColor.firstChild);
}
// 创建子节点
for (let index = 0; index < colorArr.length; index++) {
const bgc =
'(' +
colorArr[index][0] +
',' +
colorArr[index][1] +
',' +
colorArr[index][2] +
')';
const colorBlock = document.createElement('div') as HTMLElement;
colorBlock.id = `color-block-id${index}`;
colorBlock.style.cssText =
'height: 50px;width: 50px;margin-right: 10px;border-radius: 50%;';
colorBlock.style.backgroundColor = `rgb${bgc}`;
extractColor.appendChild(colorBlock);
}
};
onMounted(() => {
const img = new Image();
img.src = `图片的地址`;
img.crossOrigin = 'anonymous';
img.onload = () => {
SetColor([]);
themeColor(50, img, 20, SetColor);
};
});
}
});
</script>
colorExtraction.ts
ts
/**
* 颜色盒子类
*
* @param {Array} colorRange [[rMin, rMax],[gMin, gMax], [bMin, bMax]] 颜色范围
* @param {any} total 像素总数, imageData / 4
* @param {any} data 像素数据集合
*/
class ColorBox {
colorRange: unknown[];
total: number;
data: Uint8ClampedArray;
volume: number;
rank: number;
constructor(colorRange: any[], total: number, data: Uint8ClampedArray) {
this.colorRange = colorRange;
this.total = total;
this.data = data;
this.volume = (colorRange[0][1] - colorRange[0][0]) * (colorRange[1][1] - colorRange[1][0]) * (colorRange[2][1] - colorRange[2][0]);
this.rank = total * this.volume;
}
getColor() {
const total = this.total;
const data = this.data;
let redCount = 0,
greenCount = 0,
blueCount = 0;
for (let i = 0; i < total; i++) {
redCount += data[i * 4];
greenCount += data[i * 4 + 1];
blueCount += data[i * 4 + 2];
}
return [redCount / total, greenCount / total, blueCount / total];
}
}
// 获取切割边
const getCutSide = (colorRange: number[][]) => { // r:0,g:1,b:2
const arr = [];
for (let i = 0; i < 3; i++) {
arr.push(colorRange[i][1] - colorRange[i][0]);
}
return arr.indexOf(Math.max(arr[0], arr[1], arr[2]));
}
// 切割颜色范围
const cutRange = (colorRange: number[][], colorSide: number, cutValue: any) => {
const arr1: number[][] = [];
const arr2: number[][] = [];
colorRange.forEach(function (item) {
arr1.push(item.slice());
arr2.push(item.slice());
})
arr1[colorSide][1] = cutValue;
arr2[colorSide][0] = cutValue;
return [arr1, arr2];
}
// 找到出现次数为中位数的颜色
const __quickSort = (arr: any[]): any => {
if (arr.length <= 1) {
return arr;
}
const pivotIndex = Math.floor(arr.length / 2);
const pivot = arr.splice(pivotIndex, 1)[0];
const left = [];
const right = [];
for (let i = 0; i < arr.length; i++) {
if (arr[i].count <= pivot.count) {
left.push(arr[i]);
}
else {
right.push(arr[i]);
}
}
return __quickSort(left).concat([pivot], __quickSort(right));
}
const getMedianColor = (colorCountMap: Record<string, number>, total: number) => {
const arr = [];
for (const key in colorCountMap) {
arr.push({
color: parseInt(key),
count: colorCountMap[key]
})
}
const sortArr = __quickSort(arr);
let medianCount = 0;
const medianIndex = Math.floor(sortArr.length / 2)
for (let i = 0; i <= medianIndex; i++) {
medianCount += sortArr[i].count;
}
return {
color: parseInt(sortArr[medianIndex].color),
count: medianCount
}
}
// 切割颜色盒子
const cutBox = (colorBox: { colorRange: number[][]; total: number; data: Uint8ClampedArray }) => {
const colorRange = colorBox.colorRange;
const cutSide = getCutSide(colorRange);
const colorCountMap: Record<string, number> = {};
const total = colorBox.total;
const data = colorBox.data;
// 统计出各个值的数量
for (let i = 0; i < total; i++) {
const color = data[i * 4 + cutSide];
if (colorCountMap[color]) {
colorCountMap[color] += 1;
}
else {
colorCountMap[color] = 1;
}
}
const medianColor = getMedianColor(colorCountMap, total);
const cutValue = medianColor.color;
const cutCount = medianColor.count;
const newRange = cutRange(colorRange, cutSide, cutValue);
const box1 = new ColorBox(newRange[0], cutCount, data.slice(0, cutCount * 4));
const box2 = new ColorBox(newRange[1], total - cutCount, data.slice(cutCount * 4));
return [box1, box2];
}
// 队列切割
const queueCut = (queue: any[], num: number) => {
while (queue.length < num) {
queue.sort((a: { rank: number }, b: { rank: number }) => {
return a.rank - b.rank
});
const colorBox = queue.pop();
const result = cutBox(colorBox);
queue = queue.concat(result);
}
return queue.slice(0, num)
}
// 颜色去重
const colorFilter = (colorArr: number[][], difference: number) => {
for (let i = 0; i < colorArr.length; i++) {
for (let j = i + 1; j < colorArr.length; j++) {
if (Math.abs(colorArr[i][0] - colorArr[j][0]) < difference && Math.abs(colorArr[i][1] - colorArr[j][1]) < difference && Math.abs(colorArr[i][2] - colorArr[j][2]) < difference) {
colorArr.splice(j, 1)
j--
}
}
}
return colorArr
}
/**
* 提取颜色
* @param colorNumber 提取最大颜色数量
* @param img 需要提取的图片
* @param difference 图片颜色筛选精准度
* @param callback 回调函数
*/
const themeColor = (colorNumber: number, img: CanvasImageSource, difference: number, callback: (arg0: number[][]) => void) => {
const canvas = document.createElement('canvas') as HTMLCanvasElement;
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
let width = 0
let height = 0
let imageData = null
canvas.width = img.width as number;
width = canvas.width as number
canvas.height = img.height as number
height = canvas.height
ctx.drawImage(img, 0, 0, width, height);
imageData = ctx.getImageData(0, 0, width, height).data;
const total = imageData.length / 4;
let rMin = 255,
rMax = 0,
gMin = 255,
gMax = 0,
bMin = 255,
bMax = 0;
// 获取范围
for (let i = 0; i < total; i++) {
const red = imageData[i * 4];
const green = imageData[i * 4 + 1];
const blue = imageData[i * 4 + 2];
if (red < rMin) {
rMin = red;
}
if (red > rMax) {
rMax = red;
}
if (green < gMin) {
gMin = green;
}
if (green > gMax) {
gMax = green;
}
if (blue < bMin) {
bMin = blue;
}
if (blue > bMax) {
bMax = blue;
}
}
const colorRange = [[rMin, rMax], [gMin, gMax], [bMin, bMax]];
const colorBox = new ColorBox(colorRange, total, imageData);
const colorBoxArr = queueCut([colorBox], colorNumber);
let colorArr = [];
for (let j = 0; j < colorBoxArr.length; j++) {
colorBoxArr[j].total && colorArr.push(colorBoxArr[j].getColor())
}
colorArr = colorFilter(colorArr, difference)
callback(colorArr);
}
export default themeColor
方案二:colorthief
colorthief - 基于JavaScript和canvas来用于提取图片的主色或调色板的工具
具体使用方法不在这里赘述,详情见colorthief文档
获取所提取颜色的中英文名称
- 定义一个基准色数组
- 传入图片主色,基于欧几里德距离的最近邻搜索算法从给定的基准色中获取最近似的色值
ts
// 计算两个颜色之间的欧几里德距离
function getColorDistance(color1: number[], color2: number[]): number {
const rDiff = color1[0] - color2[0];
const gDiff = color1[1] - color2[1];
const bDiff = color1[2] - color2[2];
return Math.sqrt(rDiff * rDiff + gDiff * gDiff + bDiff * bDiff);
}
// 在颜色对象数组中找到最接近的颜色
function findClosestColor(targetColor: number[], colorArray: { key: string[], value: number[] }[]): { key: string[], value: number[] } {
let closestColor = { key: [], value: [] };
let minDistance = Infinity;
for (let i = 0; i < colorArray.length; i++) {
const currentColor = colorArray[i];
const currentDistance = getColorDistance(targetColor, currentColor.value);
if (currentDistance < minDistance) {
minDistance = currentDistance;
closestColor = { key: currentColor.key, value: currentColor.value };
}
}
return closestColor;
}
// 示例颜色对象数组
const colors: { key: string[], value: number[] }[] = [
{ key: ["red", "红色"], value: [255, 0, 0] },
{ key: ["green", "绿色"], value: [0, 255, 0] },
{ key: ["blue", "蓝色"], value: [0, 0, 255] },
{ key: ["white", "白色"], value: [255, 255, 255] },
{ key: ["black", "黑色"], value: [0, 0, 0] }
];
// 目标颜色
const targetColor: number[] = [200, 50, 100]; // 示例的目标颜色
// 寻找最接近的颜色
const closestColor = findClosestColor(targetColor, colors);
console.log('Closest color:', closestColor.key, closestColor.value); // 输出最接近的颜色的键和值
color-extraction
color-extraction - 是一个强大的工具,它能够在浏览器和 Node.js 环境中帮助您提取图像的主色或调色板,并提供对应的中英文颜色名称。它的使用简单高效,而且能够提供准确且可靠的结果。另外,这个工具建立在
colorthief
的基础上,因此在提取颜色方面具有很强的稳定性;并且在中英文颜色提取方面,它展现了对颜色定义的极其全面的支持,能够准确地识别并提取各种中英文颜色名称,从而为开发人员提供了更加全面和丰富的颜色选项。
Node 中使用
1.安装和导入
sh
$ npm i color-extraction
js
const colorExtraction = require('color-extraction');
2.使用
mainColor()和 paletteColor()方法在 Node 中使用时都会返回一个 Promise。
js
const img = resolve(process.cwd(), 'lostElk.png');
colorExtraction
.mainColor('img', 10)
.then((color) => {
console.log(color);
})
.catch((err) => {
console.log(err);
});
colorExtraction
.paletteColor('img', { colorCount: 5, quality: 10 })
.then((color) => {
console.log(color);
})
.catch((err) => {
console.log(err);
});
在浏览器中使用
1.安装
-
作为 npm 的依赖项安装。
sh$ npm i color-extraction
-
从 CDN 加载。
js<script src="https://unpkg.com/color-extraction"></script>
2. 导入
-
作为一个全局变量。
全局变量命名为 coloreextraction,而不是 color-extraction
js<script src="https://unpkg.com/color-extraction"></script>
-
作为 ES6 模块。
jsimport colorExtraction from 'color-extraction';
3. 使用
mainColor()和 paletteColor()方法在浏览器中使用时都会返回一个 Promise。
js
// Get image
const img = document.getElementById('img');
// Make sure image is finished loading
if (img.complete) {
colorExtraction.mainColor(img, 10).then((colorName) => {
console.log(colorName);
});
} else {
img.addEventListener('load', function () {
colorExtraction.mainColor(img, 10).then((colorName) => {
console.log(colorName);
});
});
}
具体使用方法请参照:color-extraction中文文档
案例参考:lostelk.cn