JavaScript实现Android Palette颜色提取算法

作者 : 开心源码 本文共5358个字,预计阅读时间需要14分钟 发布时间: 2022-05-13 共256人阅读

本文同时发表在我的博客wangyi.blog

Android Palette Library 是一个从 Bitmap中 提取图像的主题颜色的工具库。我最近对 Palette 的实现感兴趣,阅读源码了解了它的原理后,我打算用 JavaScript 来实现同样的功能。

example

1. 获取图片的像素数据

通过 canvas 获取图片的像素信息 ImageData, ImageData 中包含图片的宽高和一个Uint8数组,该数组以 RGBA的形式存储像素数据。

let width = this.image.width;let height = this.image.height;let canvas = document.createElement('canvas');canvas.width = width;canvas.height = height;let ctx = canvas.getContext("2d");ctx.drawImage(this.image, 0, 0);let data = ctx.getImageData(0, 0, width, height).data;

2. 以柱状图的形式统计所有颜色出现的次数

柱状图是一个一维 int 数组,数组 index 对应颜色的 int 值,对应的取值表示该颜色的出现次数。RGB888包含的颜色大约有1600万(255x255x255)种颜色,这里将RGB888颜色空间转成RGB555颜色空间。RGB555包含32768(32x32x32)种颜色,可减少大量的计算量。

let colorCount = 1 << 15;let histogram = new Int16Array(colorCount);for (let i = 0; i < data.length; i += 4) {    let r = data[i] >> 3;    let g = data[i + 1] >> 3;    let b = data[i + 2] >> 3;    histogram[r << (10) | g << 5 | b]++;}

3. 挑选出现次数大于0的颜色

将出现次数大于0的颜色保存在一个数组中,统计不同颜色的数量 distinctColorCount。**shouldIgnoreColor ** 方法会忽略掉接近白色、黑色和红色的颜色。

let distinctColorCount = 0;for (let color = 0; color < colorCount; color++) {    if (histogram[color] > 0 && ColorCutQuantizer.shouldIgnoreColor(color)) {        histogram[color] = 0;    }    if (histogram[color] > 0) {        distinctColorCount++    }}let colors = new Int16Array(distinctColorCount);let index = 0;for (let color = 0; color < colorCount; color++) {    if (histogram[color] > 0) {        colors[index++] = color;    }}

假如 distinctColorCount 小于等于我们需要提取的采样个数 maxColors,那么我们的采样流程结束,直接生成颜色样本。

if (distinctColorCount <= maxColors) {    this.quantizedColors = new Array(distinctColorCount);    for (let i = 0; i < distinctColorCount; i++) {        let color = colors[i];        let r = (color >> 10) & 0x1f;        let g = (color >> 10) & 0x1f;        let b = color & 0x1f;        this.quantizedColors[i] = new Swatch(r, g, b, histogram[color])    }} else {    this.quantizedColors = ColorCutQuantizer.quantizePixels(histogram, colors, maxColors)}

4. 通过中位切分算法提取样本

假如我们拥有的颜色数量比需要的样本数量多,利用中位切割算法将颜色数量裁剪到需要的采样数量。

  1. 将所有的颜色放入一个长方体(Vbox
    Vbox

我们对 Vbox 进行初始化,得到该 Vbox 对应的R、G、B的最大和最小值,以及表示的该颜色范围内所有像素的数量的 population

fitBox() {    this.minRed = this.minGreen = this.minBlue = Number.MAX_VALUE;    this.maxRed = this.maxGreen = this.maxBlue = 0;    this.population = 0;    for (let i = this.lowerIndex; i <= this.upperIndex; i++) {        let color = this.colors[i];        this.population += this.histogram[color];        let r = quantizedRed(color);        let g = quantizedGreen(color);        let b = quantizedBlue(color);        if (r > this.maxRed) {            this.maxRed = r        }        if (r < this.minRed) {            this.minRed = r        }        if (g > this.maxGreen) {            this.maxGreen = g        }        if (g < this.minGreen) {            this.minGreen = g        }        if (b > this.maxBlue) {            this.maxBlue = b        }        if (b < this.minBlue) {            this.minBlue = b        }    }};
  1. 将这个 Vbox 放入一个优先级队列(PriorityQueue)中。JavaScript 中没有 PriorityQueue 这样的数据结构,我在 Github 上找到了对应的简单实现 TinyQueue。该队列根据 Vbox 的体积排序:
// 获取Vbox的体积 — 三边长的乘积getVolume() {    return (this.maxRed - this.minRed + 1) * (this.maxGreen - this.minGreen + 1) * (this.maxBlue - this.minBlue + 1);};...let queue = new TinyQueue();queue.compare = function (a, b) {    return b.getVolume() - a.getVolume();};
  1. 将RGB中最长的一边从颜色统计的中位数一切为二,使得到的两个长方体所包含的像素数量相同。中位切割最重要的是找到切割的点,下面是我们找到 Vbox 切割点的方法:
findSplitPoint() {    // 获取Vbox最长的边    let longestDimension = this.getLongestColorDimension();    // 我们需要根据最长的边对该Vbox中的颜色进行排序,因为当前是颜色RGB空间    // 假如最长的边是Green则需要把颜色修改为GRB,假如最长边是Blue修改为RGR    Vbox.modifySignificantOctet(this.colors, longestDimension, this.lowerIndex, this.upperIndex);    // 对Vbox内的颜色排序    Vbox.sortRange(this.colors, this.lowerIndex, this.upperIndex);    Vbox.modifySignificantOctet(this.colors, longestDimension, this.lowerIndex, this.upperIndex);    let midPoint = this.population / 2;    let count = 0;    for (let i = this.lowerIndex; i <= this.upperIndex; i++) {        count += this.histogram[this.colors[i]];        if (count >= midPoint) {            return Math.min(this.upperIndex - 1, i)        }    }    return this.lowerIndex};

将分割出的2个的 Vbox 放入队列中,而后我们再从队列中获取体积最大的一个 Vbox 继续分割,直到 Vbox数量达到我们需要的样本数量。

5. 根据Vbox生成样本Swatch

getAverageColor方法计算Vbox中的所有颜色的平均值,而后生成一个 Swatch。

getAverageColor() {    let redSum = 0, greenSum = 0, blueSum = 0, totalPopulation = 0;    for (let i = this.lowerIndex; i <= this.upperIndex; i++) {        let color = this.colors[i];        let colorPopulation = this.histogram[color];        totalPopulation += colorPopulation;        redSum += colorPopulation * quantizedRed(color);        greenSum += colorPopulation * quantizedGreen(color);        blueSum += colorPopulation * quantizedBlue(color);    }    let redMean = Math.round(redSum / totalPopulation);    let greenMean = Math.round(greenSum / totalPopulation);    let blueMean = Math.round(blueSum / totalPopulation);    return new Swatch(redMean, greenMean, blueMean, totalPopulation);};

6. 根据Target对Swatch打分,取得最终的主题颜色值列表

Target 定义了我们对颜色饱和度和亮度的最低值、目标值和计算评分的权重要求,默认定义了6种 Target:

  • Vibrant (有活力的)
  • Vibrant dark(有活力的 暗色)
  • Vibrant light(有活力的 亮色)
  • Muted (柔和的)
  • Muted dark(柔和的 暗色)
  • Muted light(柔和的 亮色)

我们得到的 Swatch 是RGB的颜色值,需要通过转换RGB(RGB转HSL算法)得到对应的HSL颜色值而后打分,HSL即色相(Hue)、饱和度(Saturation)、亮度(Lightness)。

在计算分数之前需要判断该Swatch能否满足评分的要求 — 饱和度和亮度在 Target 的要求范围之内,并且该Swatch 没有被其余Target使用。因而该 Targe 可能t获取不到对应的 Swatch。

shouldBeScoredForTarget(swatch, target) {    let hsl = swatch.getHsl();    let s = hsl[1];    let l = hsl[2];    return s >= target.getMinimumSaturation() && s <= target.getMaximumSaturation()        && l >= target.getMinimumLightness() && l <= target.getMaximumLightness()        && !this.usedColors.get(swatch.rgb);};

我们将饱和度分数、亮度分数、像素 Population 分数三项分数加起来,得到该 Target 评分最高的 Swatch。

generateScore(swatch, target) {    let saturationScore = 0;    let luminanceScore = 0;    let populationScore = 0;    let maxPopulation = this.dominantSwatch.population;    let hsl = swatch.getHsl();    if (target.getSaturationWeight() > 0) {        saturationScore = target.getSaturationWeight() * (1 - Math.abs(hsl[1] - target.getTargetSaturation()));    }    if (target.getLightnessWeight() > 0) {        luminanceScore = target.getLightnessWeight() * (1 - Math.abs(hsl[2] - target.getTargetLightness()));    }    if (target.getPopulationWeight() > 0) {        populationScore = target.getPopulationWeight() * (swatch.population / maxPopulation);    }    return saturationScore + luminanceScore + populationScore;};

一律代码上传到Github wangyiwy/palette-js

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » JavaScript实现Android Palette颜色提取算法

发表回复