🎨人人都是艺术家,你的春节调色盘里都有什么颜色,快来瞅瞅吧!

1,759 阅读6分钟

PK创意闹新春,我正在参加「春节创意投稿大赛」,详情请看:春节创意投稿大赛

前言

今天是大年初三,春节假期快过半了!大街上,放眼望去,门前的对联、高高挂起的灯笼、地上的鞭炮…… 它们都是中国红!写着代码低头一看,本命年的我也穿了一身红色的行头。似乎红色才是春节的主题色,那么你的春节是什么颜色的呢?

今天我们就来探究一下如何提取一张图片的主色调!先来看看最终实现的效果。

QQ截图20220203185435.png (感谢视觉中国提供的素材图)

众所周知,一张图片又若干像素块组成,每个像素由一组数据可以表示它的颜色。颜色空间有很多种,如我们熟悉的RGB以及CIELabYUV等多种表示方法。在前端开发中,我们最常用到的就是使用RGBRGBa来表示一种颜色。

一张图片的主色调,又或称作是主题色、平均颜色、调色盘,他们都可以作为图像的特征,特征可以用于图像的进一步分析。在设计行业,颜色的选择与搭配往往影响了视觉上最直观的感受。例如,某音乐平台播放音乐的时候,歌曲背景的颜色是由它的封面所决定的,这样的搭配更协调。

这里插入一则小广告,在我之前写过一个小项目中也用到了这个方案,感兴趣的朋友可以参考下这篇文章。 👉 基于Vue全家桶的在线音乐播放器(提供在线演示)

颜色提取算法

一些常见的算法

中位切分法

中位切分算法首先把所有像素映射到RGB空间,在这个三维的空间里反复切分出子空间,最后将切分空间的像素求均值作为提取结果。分割区块时都选择所有区块中最大(最长的边长最大,或体积最大,或像素最多)的区块,切割点应位于边方向上,使得分割后两个区块的像素各一半的位置。

  1. 像素映射到RGB空间 image.png

  2. 区块计算 image.png

  3. 中位切分 image.png

  4. 反复切分 image.png

  5. 计算区块的平均颜色 image.png

八叉树算法

八叉树算法的核心理念是用八叉树来划分颜色空间,然后合并叶节点来逐步聚拢颜色(量子化),八叉树的解释可参考《游戏场景管理的八叉树算法是怎样的?》

K-Means聚类算法

K均值聚类的思想十分简单,可分这几步:

  1. 选取初始的K个质心;

  2. 按照距离质心的远近对所有样本进行分类;

  3. 重新计算质心,判断是否退出条件:

    • 两次质心的距离足够小视为满足退出条件;
    • 不退出则重新回到步骤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值数组,我们选取前十个作为我们的调色盘数据,得到的结果如下:

image.png

我们可以发现,正如肉眼所看到的,这张图片中颜色最多的确确实实是这种红色,与我们算法的输出结果是一致的。但是光凭肉眼很难区分它们有什么不同,且该算法的结果并不能表现出图像整体的特征。因此,这种方法仅仅能够得到图像中分布最密集的颜色集。

一款好用的插件

ColorThief 是一款用于从图像中提取 Dominant ColorPalette 的插件,它是使用JavaScriptCanvas开发的。它的使用方法也非常简单,下面将简单演示一下。

提取图像主色调 getColor()

getColor(sourceImage[, quality])
returns {r: num, g: num, b: num}
let colorThief = new ColorThief();
colorThief.getColor(sourceImage);

image.png

提取图像调色盘 getPalette()

getPalette(sourceImage[, colorCount, quality])
returns [ [num, num, num], [num, num, num], ... ]
// 这里我们提取8种颜色
let colorThief = new ColorThief();
colorThief.getPalette(sourceImage, 8);

image.png

据悉,这款插件的原理是基于中位切分法

项目地址:github.com/lokesh/colo…

系统实现

我们这个项目最核心的部分 —— 颜色提取算法已经落实了,接下来就是开始页面的搭建了。

图片上传并显示

使用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值的重复的颜色,如下图所示。 image.png 这里我们想到的是将 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("-")))
})

image.png 这样调色盘中就没有重复的色卡了。

html保存为图片

一开始想到的是,将最终的生成结果直接转化为图片并保存到本地,但在网上冲浪的过程中,发现了这么一个插件:html2canvas,它可以将html页面转化为canvas,有了canvas就可以保存图片了!

项目地址:github.com/niklasvh/ht…

它的使用的方法也非常非常简单!

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()
    })
},

在线体验😀

体验地址

参考资料

  1. 图像颜色提取 - 方泉水很甜
  2. JavaScript提取图片主题色 - Wang_Yi
  3. 主题色提取 - Gesangs
  4. Color Thief