PK创意闹新春,我正在参加「春节创意投稿大赛」,详情请看:春节创意投稿大赛
前言
今天是大年初三,春节假期快过半了!大街上,放眼望去,门前的对联、高高挂起的灯笼、地上的鞭炮…… 它们都是中国红!写着代码低头一看,本命年的我也穿了一身红色的行头。似乎红色才是春节的主题色,那么你的春节是什么颜色的呢?
今天我们就来探究一下如何提取一张图片的主色调!先来看看最终实现的效果。
(感谢视觉中国提供的素材图)
众所周知,一张图片又若干像素块组成,每个像素由一组数据可以表示它的颜色。颜色空间有很多种,如我们熟悉的RGB以及CIELab、YUV等多种表示方法。在前端开发中,我们最常用到的就是使用RGB或RGBa来表示一种颜色。
一张图片的主色调,又或称作是主题色、平均颜色、调色盘,他们都可以作为图像的特征,特征可以用于图像的进一步分析。在设计行业,颜色的选择与搭配往往影响了视觉上最直观的感受。例如,某音乐平台播放音乐的时候,歌曲背景的颜色是由它的封面所决定的,这样的搭配更协调。
这里插入一则小广告,在我之前写过一个小项目中也用到了这个方案,感兴趣的朋友可以参考下这篇文章。 👉 基于Vue全家桶的在线音乐播放器(提供在线演示)
颜色提取算法
一些常见的算法
中位切分法
中位切分算法首先把所有像素映射到RGB空间,在这个三维的空间里反复切分出子空间,最后将切分空间的像素求均值作为提取结果。分割区块时都选择所有区块中最大(最长的边长最大,或体积最大,或像素最多)的区块,切割点应位于边方向上,使得分割后两个区块的像素各一半的位置。
-
像素映射到RGB空间
-
区块计算
-
中位切分
-
反复切分
-
计算区块的平均颜色
八叉树算法
八叉树算法的核心理念是用八叉树来划分颜色空间,然后合并叶节点来逐步聚拢颜色(量子化),八叉树的解释可参考《游戏场景管理的八叉树算法是怎样的?》
K-Means聚类算法
K均值聚类的思想十分简单,可分这几步:
-
选取初始的K个质心;
-
按照距离质心的远近对所有样本进行分类;
-
重新计算质心,判断是否退出条件:
- 两次质心的距离足够小视为满足退出条件;
- 不退出则重新回到步骤2;
一个常规的思路
上面的几种算法让人望而却步。
我看到这么个需求的时候,首先想到的是得到整个图像的所有像素的RGB值,然后根据RGB组合出现的次数进行一个排序,次数最多的就是这副图像的主色调!
const img = this.$refs.img
const that = this
img.onload = function(){
const w = this.width
const h = this.height
// 创建画布
const canvas = document.createElement('canvas')
canvas.width = w
canvas.height = h
// 绘制图片在画布上
const context = canvas.getContext('2d')
context.drawImage(this,0,0)
// 获取像素点rgb数据
let pxArr = context.getImageData(0,0,w,h).data
pxArr = [...pxArr]
// 对像素颜色进行统计
const colorList = {}
let i = 0
while(i < pxArr.length){
const r = pxArr[i]
const g = pxArr[i+1]
const b = pxArr[i+2]
i = i + 4
const key = [r,g,b].join(",")
key in colorList ? ++colorList[key] : (colorList[key] = 1)
}
// 对统计的数据进行排序
let arr = []
for(let key in colorList){
arr.push({
rgb: `rgb(${key})`,
num: colorList[key]
})
}
arr = arr.sort((a,b)=>b.num - a.num)
经过上述操作之后,我们得到了按照出现次数降序的RGB值数组,我们选取前十个作为我们的调色盘数据,得到的结果如下:
我们可以发现,正如肉眼所看到的,这张图片中颜色最多的确确实实是这种红色,与我们算法的输出结果是一致的。但是光凭肉眼很难区分它们有什么不同,且该算法的结果并不能表现出图像整体的特征。因此,这种方法仅仅能够得到图像中分布最密集的颜色集。
一款好用的插件
ColorThief 是一款用于从图像中提取 Dominant Color 和 Palette 的插件,它是使用JavaScript和Canvas开发的。它的使用方法也非常简单,下面将简单演示一下。
提取图像主色调 getColor()
getColor(sourceImage[, quality])
returns {r: num, g: num, b: num}
let colorThief = new ColorThief();
colorThief.getColor(sourceImage);
提取图像调色盘 getPalette()
getPalette(sourceImage[, colorCount, quality])
returns [ [num, num, num], [num, num, num], ... ]
// 这里我们提取8种颜色
let colorThief = new ColorThief();
colorThief.getPalette(sourceImage, 8);
据悉,这款插件的原理是基于中位切分法。
系统实现
我们这个项目最核心的部分 —— 颜色提取算法已经落实了,接下来就是开始页面的搭建了。
图片上传并显示
使用input标签选择本地的图片,使用FileReader读取选中的图片,解码为base64,最后再显示在页面中。
<input type="file" @change="getFile">
<img :src="imgSrc" class="img" ref="img">
getFile(e) {
let that = this
let files = e.target.files[0]
if(!e || !window.FileReader) return
let reader = new FileReader()
reader.readAsDataURL(files)
reader.onloadend = function(){
that.imgSrc = this.result
}
}
颜色去重
若源图像的颜色种类极少且小于我们人为设置的10,那么最终的结果就会出现具有相同RGB值的重复的颜色,如下图所示。
这里我们想到的是将
getPalette() 这个函数的返回结果做一个去重。但是这个数组中的元素同样是数组,使用 Set 很难做到对数组元素进行一个去重,既然无法判断数组元素,那么字符串总可以判断吧。我们先将数组元素转化为字符串,得到一个字符串数组,再进行去重,最后将去重后的字符串还原为数组就好了。
// 提取10种颜色
let colors = [...colorThief.getPalette(img,10)]
let colorsStrArr = []
// rgb去重
colors.forEach(item => {
colorsStrArr.push(item.join("-"))
})
colorsStrArr = [...new Set(colorsStrArr)]
colorsStrArr.forEach(item => {
this.colors.push(this.toRGB(item.split("-")))
})
这样调色盘中就没有重复的色卡了。
html保存为图片
一开始想到的是,将最终的生成结果直接转化为图片并保存到本地,但在网上冲浪的过程中,发现了这么一个插件:html2canvas,它可以将html页面转化为canvas,有了canvas就可以保存图片了!
它的使用的方法也非常非常简单!
html2canvas(document.querySelector('.card-wrap')).then(canvas => {
document.appendChild(canvas)
})
这样我们就得到了一张 canvas 画布,但是分辨率还是有点影响个人感觉。
有了 canvas 画布我们怎么保存为图片呢?
- 右键画布,就有一个另存为图片。
但我不想在页面上插入这个画布,这个时候就用到
a标签了。 - 动态设置
a标签的href属性为canvas.toDataURL("image/png"),再模拟点击。
<a href="#" ref="downloadA" style="opacity:0" download="我的春节颜色"></a>
downloadResult(){
html2canvas(document.querySelector('.card-wrap')).then(canvas => {
this.$refs.downloadA.href = canvas.toDataURL("image/png");
this.$refs.downloadA.click()
})
},