使用 JavaScript 对图像进行量化并提取主要颜色

news2024/11/15 10:41:10

前言

前段时间在 Halo 的 应用市场 中遇到希望主题和插件的封面图背景色为封面图主色的问题,于是乎需要根据封面图提取主色就想到使用 K-Means 算法来提取。

在图像处理中,图像是由像素点构成的,每个像素点都有一个颜色值,颜色值通常由 RGB 三个分量组成。因此,我们可以将图像看作是一个由颜色值构成的点云,每个点代表一个像素点。

为了更好地理解,我们可以将图像的颜色值可视化为一个 Scatter 3D 图。在 Scatter 3D 图中,每个点的坐标由 RGB 三个分量组成,点的颜色与其坐标对应的颜色值相同。

图像的颜色值量化

以下面的图片为例

它的色值分布为如下的图像
k-means-figure_1.png
从上述 RGB 3D Scatter Plot 图如果将相似的颜色值归为一类可以看出图像大概有三种主色调蓝色、绿色和粉色:
k-means-figure_2.jpg
如果我们从三簇中各选一个中心,如以 A、B、C三点表示 A(50, 150, 200)B(240, 150, 200)C(50, 100, 50) 并将每个数据点分配到最近的中心所在的簇中这个过程称之为聚类而这个中心称之为聚类中心,这样就可以得到 K 个以聚类中心为坐标的主色值。而 K-Means 算法是一种常用的聚类算法,它的基本思想就是将数据集分成 K 个簇,每个簇的中心点称为聚类中心,将每个数据点分配到最近的聚类中心所在的簇中。

K-Means 算法的实现过程如下:

  1. 初始化聚类中心:随机选择 K 个点作为聚类中心。

  2. 分配数据点到最近的聚类中心所在的簇中:对于每个数据点,计算它与每个聚类中心的距离,将它分配到距离最近的聚类中心所在的簇中。

  3. 更新聚类中心:对于每个簇,计算它的所有数据点的平均值,将这个平均值作为新的聚类中心。

  4. 重复步骤 2 和步骤 3,直到聚类中心不再改变或达到最大迭代次数。

在图像处理中,我们可以将每个像素点的颜色值看作是一个三维向量,使用欧几里得距离计算两个颜色值之间的距离。对于每个像素点,我们将它分配到距离最近的聚类中心所在的簇中,然后将它的颜色值替换为所在簇的聚类中心的颜色值,如 A1(10, 140, 170) 以距离它最近的距离中心 A 的坐标表示即 A1 = A(50, 150, 200)。这样,我们就可以将图像中的颜色值进行量化,将相似的颜色值归为一类。

最后,我们可以根据聚类中心的颜色值,计算每个颜色值在图像中出现的次数,并按出现次数从大到小排序,取前几个颜色作为主要颜色。

<script>
  const img = new Image();
  img.src = "https://guqing-blog.oss-cn-hangzhou.aliyuncs.com/image.jpg";
  img.setAttribute("crossOrigin", "");

  img.onload = function () {
    const canvas = document.createElement("canvas");
    const ctx = canvas.getContext("2d");
    canvas.width = img.width;
    canvas.height = img.height;
    ctx.drawImage(img, 0, 0);

    const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
    const data = imageData.data;

    const k = 3; // 聚类数
    const centers = quantize(data, k);
    console.log(centers)

    for (const color of centers) {
      const div = document.createElement("div");
      div.style.width = "50px";
      div.style.height = "50px";
      div.style.backgroundColor = color;
      document.body.appendChild(div);
    }
  };

  function quantize(data, k) {
    // 将颜色值转换为三维向量
    const vectors = [];
    for (let i = 0; i < data.length; i += 4) {
      vectors.push([data[i], data[i + 1], data[i + 2]]);
    }

    // 随机选择 K 个聚类中心
    const centers = [];
    for (let i = 0; i < k; i++) {
      centers.push(vectors[Math.floor(Math.random() * vectors.length)]);
    }

    // 迭代更新聚类中心
    let iterations = 0;
    while (iterations < 100) {
      // 分配数据点到最近的聚类中心所在的簇中
      const clusters = new Array(k).fill().map(() => []);
      for (let i = 0; i < vectors.length; i++) {
        let minDist = Infinity;
        let minIndex = 0;
        for (let j = 0; j < centers.length; j++) {
          const dist = distance(vectors[i], centers[j]);
          if (dist < minDist) {
            minDist = dist;
            minIndex = j;
          }
        }
        clusters[minIndex].push(vectors[i]);
      }

      // 更新聚类中心
      let converged = true;
      for (let i = 0; i < centers.length; i++) {
        const cluster = clusters[i];
        if (cluster.length > 0) {
          const newCenter = cluster
            .reduce((acc, cur) => [
              acc[0] + cur[0],
              acc[1] + cur[1],
              acc[2] + cur[2],
            ])
            .map((val) => val / cluster.length);
          if (!equal(centers[i], newCenter)) {
            centers[i] = newCenter;
            converged = false;
          }
        }
      }

      if (converged) {
        break;
      }

      iterations++;
    }

    // 将每个像素点的颜色值替换为所在簇的聚类中心的颜色值
    for (let i = 0; i < data.length; i += 4) {
      const vector = [data[i], data[i + 1], data[i + 2]];
      let minDist = Infinity;
      let minIndex = 0;
      for (let j = 0; j < centers.length; j++) {
        const dist = distance(vector, centers[j]);
        if (dist < minDist) {
          minDist = dist;
          minIndex = j;
        }
      }
      const center = centers[minIndex];
      data[i] = center[0];
      data[i + 1] = center[1];
      data[i + 2] = center[2];
    }

    // 计算每个颜色值在图像中出现的次数,并按出现次数从大到小排序
    const counts = {};
    for (let i = 0; i < data.length; i += 4) {
      const color = `rgb(${data[i]}, ${data[i + 1]}, ${data[i + 2]})`;
      counts[color] = counts[color] ? counts[color] + 1 : 1;
    }
    const sortedColors = Object.keys(counts).sort(
      (a, b) => counts[b] - counts[a]
    );

    // 取前 k 个颜色作为主要颜色
    return sortedColors.slice(0, k);
  }

  function distance(a, b) {
    return Math.sqrt(
      (a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2 + (a[2] - b[2]) ** 2
    );
  }

  function equal(a, b) {
    return a[0] === b[0] && a[1] === b[1] && a[2] === b[2];
  }
</script>

自动选取 K 值

在实际应用中,我们可能不知道应该选择多少个聚类中心,即 K 值。一种常用的方法是使用 Gap 统计量法,它的基本思想是比较聚类结果与随机数据集的聚类结果之间的差异,选择使差异最大的 K 值。

Gap 统计量法的实现过程如下:

  1. 对原始数据集进行 K-Means 聚类,得到聚类结果。

  2. 生成 B 个随机数据集,对每个随机数据集进行 K-Means 聚类,得到聚类结果。

  3. 计算聚类结果与随机数据集聚类结果之间的差异,使用 Gap 统计量表示。

  4. 选择使 Gap 统计量最大的 K 值。

下面是使用 JavaScript 实现 Gap 统计量法的示例代码:

function gap(data, maxK) {
  const gaps = [];
  for (let k = 1; k <= maxK; k++) {
    const quantized = quantize(data, k);
    const gap = logWk(quantized) - logWk(randomData(data.length));
    gaps.push(gap);
  }
  const maxGap = Math.max(...gaps);
  return gaps.findIndex((gap) => gap === maxGap) + 1;
}

function logWk(quantized) {
  const counts = {};
  for (let i = 0; i < quantized.length; i++) {
    counts[quantized[i]] = counts[quantized[i]] ? counts[quantized[i]] + 1 : 1;
  }
  const n = quantized.length;
  const k = Object.keys(counts).length;
  const wk = Object.values(counts).reduce((acc, cur) => acc + cur * Math.log(cur / n), 0);
  return Math.log(n) + wk / n;
}

function randomData(n) {
  const data = new Uint8ClampedArray(n * 4);
  for (let i = 0; i < data.length; i++) {
    data[i] = Math.floor(Math.random() * 256);
  }
  return data;
}

使用:

const k = gap(data, 10)
// const k = 3; // 聚类数
const centers = quantize(data, k);

好吧,挺麻烦的,最终直接将封面图再作为背景图添加 backdrop-filter 来实现了 🤐。

附录

Python 绘制图片 Scatter 3D:

import matplotlib.pyplot as plt
import numpy as np

from mpl_toolkits.mplot3d import Axes3D

def visualize_rgb(image_path):
    image = plt.imread(image_path)
    height, width, _ = image.shape

    # Reshape the image array to a 2D array of pixels
    pixels = np.reshape(image, (height * width, 3))

    # Extract RGB values
    red = pixels[:, 0]
    green = pixels[:, 1]
    blue = pixels[:, 2]

    # Create 3D scatter plot
    fig = plt.figure()
    ax = fig.add_subplot(111, projection='3d')
    ax.scatter(red, green, blue, c=pixels/255, alpha=0.3)

    ax.set_xlabel('Red')
    ax.set_ylabel('Green')
    ax.set_zlabel('Blue')
    ax.set_title('RGB 3D Scatter Plot')

    plt.show()

# 调用函数并传入图像路径
visualize_rgb('image.jpg')

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1965689.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Gstreamer结合腾讯云进行rtmp直播

直播效果&#xff1a; 一、注册腾讯云直播账户&#xff0c;生成rtmp推流地址 腾讯云直播地址&#xff1a; https://console.cloud.tencent.com/live 首先需要注册登录。然后电机生成直播地址&#xff1a; 输入自己的流名字&#xff0c;比如test 复制这个RTMP地址。 这时候&am…

【数据结构与算法】循环队列

循环队列 一.循环队列的引入二.循环队列的原理三.循环队列判断是否为满或空1.是否为空2.是否为满 四.循环队列入队五.循环队列出队六.循环队列的遍历七.循环队列获取长度八.总结 一.循环队列的引入 还记得我们顺序队列的删除元素嘛,我们有两种方式,一种是将数组要删除元素后面…

系统数据库

Mysql数据库安装完成后&#xff0c;自带了一下四个数据库&#xff0c;具体作用如下&#xff1a; 常用工具&#xff1a;

<数据集>工程机械识别数据集<目标检测>

数据集格式&#xff1a;VOCYOLO格式 图片数量&#xff1a;6338张 标注数量(xml文件个数)&#xff1a;6338 标注数量(txt文件个数)&#xff1a;6338 标注类别数&#xff1a;7 标注类别名称&#xff1a;[Excavator, Loader, Dumb_truck, Mobile_crane, Roller, Bull_dozer, …

功能实现——采用 Hutool 工具发送邮件

目录 1.需求分析2.准备工作&#xff1a;开通邮箱的 SMTP 服务3.项目环境搭建4.代码实现mail.htmlEmployee.javaMailController.javaMailService.javaMailServiceImpl.java 5.测试 1.需求分析 采用 Hutool 工具来实现发送邮件的功能&#xff0c;具体来说&#xff1a;为新员工发…

准备笔试第21天,牛客.十字爆破牛客.比木名居的桃子牛客.chinka蜜柑01背包

目录 牛客.十字爆破 牛客.比木名居的桃子 牛客.chinka蜜柑 01背包 牛客.十字爆破 就是上下左右加上&#xff0c;但是会遇到的问题就是&#xff0c;这块并不容易去获取得分&#xff0c;如果可能要四重循环&#xff0c;遍历这一行&#xff0c;这一列&#xff0c;然后把他们存在…

解决Ubuntu报错:sudo: /etc/sudoers is world writable

1. 情况描述 /etc/sudoer这个文件的权限由440变成了777&#xff0c;由于账户下有多个子账户&#xff0c;导致子账户的sudo权限不能使用。报错如下&#xff1a; 2.解决办法 执行下面的语句就ok了 pkexec chmod 0440 /etc/sudoers 参考链接 3.总结 不要随便改系统文件的权…

模型组合、注意力机制在单步、多步、单变量、多变量预测中的应用

往期精彩内容&#xff1a; 时序预测&#xff1a;LSTM、ARIMA、Holt-Winters、SARIMA模型的分析与比较-CSDN博客 VMD CEEMDAN 二次分解&#xff0c;Transformer-BiGRU预测模型-CSDN博客 独家原创 | 基于TCN-SENet BiGRU-GlobalAttention并行预测模型-CSDN博客 独家原创 | B…

a newer version of WinPcap,ensp安装时候winpcap软件报错

ensp安装时候winpcap软件报错 a newer version of WinPcap… 找到C盘路径下的文件Packet.dll C:\Windows\SysWOW64 修改为&#xff1a;Packet.dll.1&#xff08;后缀名随便改一下&#xff09; 再次安装&#xff0c;成功

在线PS快速抠出透明背景(纯色背景+复杂背景抠图操作)

电脑硬盘快爆了&#xff0c;没必要安装个PS了&#xff0c;网上找了几个在线的PS网站&#xff0c;还别说&#xff0c;一般的PS操作都可以满足 我们使用PS通常用的较多的是抠背景操作吧&#xff0c;接下来演示几个在在线PS网站上进行抠背景操作 一、在线PS网站 Photopea&#x…

程序员转型人工智能:从“996”困境到拥抱光明未来

前言 在这个充满挑战与机遇的时代&#xff0c;各行各业的辛酸各有不同&#xff0c;而程序员群体无疑有着自己的独特体验。他们学习着普通人难以理解的计算机语言&#xff0c;工作在“996”的高压环境中&#xff0c;还未及中年就可能面临“聪明绝顶”的尴尬。面对行业的快速更新…

树与二叉树【中】

目录 二. 二叉树2.1 二叉树的性质2.2 二叉树的存储结构2.2.1 二叉树的顺序存储&#xff08;只适合存储完全二叉树&#xff09;2.2.2 二叉树的链式存储 2.3 二叉树的遍历2.3.1 先序遍历2.3.2 中序遍历2.3.3 后序遍历2.3.4 二叉树的层序遍历2.3.5 由遍历序列构造二叉树2.3.5.1 前…

【HarmonyOS】HarmonyOS NEXT学习日记:八、组件通信

【HarmonyOS】HarmonyOS NEXT学习日记&#xff1a;八、组件通信 通过前面的学习我们基本上掌握了如何封装组件&#xff0c;但是实际使用过程中组件之间的状态需要互相之间关联通讯&#xff0c;涉及到父子组件&#xff0c;后代组件之间的相互通信。 State装饰器&#xff1a;组…

Loader QML Type

文章目录 Loader QML Type描述属性&#xff08;Properties&#xff09;active : boolasynchronous : bool&#xff08;异步&#xff09;item : objectprogress : realsource : urlsourceComponent : Componentstatus : enumeration 信号&#xff08;Signal Documentation&#…

四,搭建环境:表述层

四&#xff0c;搭建环境&#xff1a;表述层 文章目录 四&#xff0c;搭建环境&#xff1a;表述层设定 Web 工程web.xml 的配置编写配置 ContextLoaderListener配置 DispatcherServlet配置 CharacterEncodingFilter配置 HiddenHttpMethodFilter 配置 Spring MVC配置视图解析相关…

【JKI SMO】框架讲解(九)

本节内容将演示如何向SMO框架添加启动画面。 1.打开LabVIEW新建一个空白项目&#xff0c;并保存。 2.找到工具&#xff0c;打开SMO Editor。 3.新建一个SMO&#xff0c;选择SMO.UI.Splash。 4. 打开LabVIEW项目&#xff0c;可以看到项目里多了一个SystemSplash类。 打开Process…

c++11-lambda表达式,包装器function,bind

lambda表达式 lambda表达式在很多语言都是有的&#xff0c;c当然是有的&#xff0c;但是像C语言就没有这个。和很多语言相同c的lambda表达式都是为了简化代码&#xff0c;当我们需要传函数的时候我们就可以用lambda表达式写一个匿名函数。 书写格式&#xff1a; [capture-li…

【Log4j2】代码执行漏洞复现!

执行以下命令 启动命令 systemctl start dockercd vulhub/log4j/CVE-2021-44228docker-compose up -d # 访问网址 http://192.168.3.42&#xff1a;xxxx/solr/#/ 启动靶场环境并在浏览器访问!!! 先在自己搭建的DNSLOG平台上获取一个域名来监控我们注入的效果. 可以发现 /solr…

人工智能:所有144本SCI期刊都在这里(20本Top,4本On Hold)

本周投稿推荐 SCI&EI • 4区“水刊”&#xff0c;纯正刊&#xff08;来稿即录&#xff09; • CCF-B类&#xff0c;IEEE一区-Top&#xff08;3天初审&#xff09; EI • 各领域沾边均可&#xff08;2天录用&#xff09; 知网&#xff08;CNKI&#xff09;、谷歌学术 …

CS61C | lecture5

CS61C | lecture5 浮点数的表示 用一个小数点作为边界分隔整数部分和小数部分。 10.101 0 2 1 2 1 1 2 − 1 1 2 − 3 2.62 5 10 10.1010_{2}1\times2^11\times2^{-1}1\times2^{-3}2.625_{10} 10.10102​12112−112−32.62510​ Scientific Notation(Binary) 单精度…