2020-04-05

微信小程序-获取图片主色调

写在前面


开发准备

  • 理一下思路
    1. 将网络图片绘制进canvas
    2. 通过canvas的getImageData获取图片的像素数据
    3. 分析目前主要用于获取图片主色调的算法,并实践
    4. 得到主要色调,再将rgb转换成hsb,并对b值,进行修改,制作渐变,左侧35,右侧15
    • 比较常见的应用就是用于提取图片的主色调用于上色配色,当然也可以用于图像分割
    • 主流算法
      • 两个大方向
      • 在颜色空间合理地选取采样点来构造颜色表,使得减色后的图像和原图尽可能地接近
        • 直接量化
          • 对每个颜色通道单独重新采样,将每个通道的色阶从256减少到某个指定的数字。这样得到一个新的小的多的颜色空间,而原图像中的每一个像素将被用在新的颜色空间中的最近邻取代
        • 统计量化
          • 核心:调整直方图使得累积分布曲线呈线性,从而使图像像素点的亮度值尽可能均匀地分布
          • 利用原图的直方图来引导采样点的选取,使得每个采样点可以大致覆盖相同数量的像素点
          • 对每个颜色通道建立直方图,然后根据这些直方图对各个颜色通道单独采样,在像素值分布多的区域进行密集采样,别的区域稀疏采样,再利用这些采样点来组合成最终的颜色表,原图中的每个像素点用颜色表中最接近的颜色替换掉
        • 颜色空间分割(Median-Cut)
          • 核心:在颜色空间建立一棵二叉树,通过不断地细化这棵树来近似得到一个颜色三维直方图,然后再根据这棵树来分配采样点
          • 基于图像颜色样本分布的自适应方法,不论图像中颜色样本的分布如何,总是可以生成一个和颜色样本分布匹配良好的颜色表:在颜色样本分布密集的区域内采样点分布也相对密集,其他区域则分配了较少的采样点。且相同数目的颜色样本总是用同样数量的采样点来代表,所以颜色样本分布密集的区域,采样点的数量自然就会多,反之则相应的比较少
          • 最重要、应用最广泛的减色算法之一
        • k均值聚类(k-Means clustering)
          • 核心:将像素按颜色的相似程度归类
      • 从一个初始的颜色表出发,通过不断修改颜色表来改善减色效果
        • 神经网络方法(ANN)
          • 颜色表通过神经元来编码,通过缓慢的调节神经元的颜色值以保证整个神经网络逐步收敛于最小误差状态(即通过神经网络产生的图像和原图之间误差最小)

            直接量化实践

            • 首先我们可以来看一看getImageData后的数据
    • getImageData数据
    • 可以简单看作是外层循环为rgba的打平了二维数组
      • 如果按满足需求做,只要一个主色调的话,可以把整个getImageData获得的数据,进行各个通道的平均值求值,再拼接,像这样
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        getUniqueColor(imageData) {
        let res_r = 0
        let res_g = 0
        let res_b = 0
        let res_a = 0
        for (let i = 0; i < imageData.length; i += 1) {
        if (i % 4 === 0) {
        res_r += imageData[i]
        } else if (i % 4 === 1) {
        res_g += imageData[i]
        } else if (i % 4 === 2) {
        res_b += imageData[i]
        } else if (i % 4 === 3) {
        res_a += imageData[i]
        }
        }
        res_r = Math.round(res_r / (imageData.length / 4))
        res_g = Math.round(res_g / (imageData.length / 4))
        res_b = Math.round(res_b / (imageData.length / 4))
        res_a = Math.round(res_a / (imageData.length / 4))
        console.log('res_r', res_r)
        console.log('res_g', res_g)
        console.log('res_b', res_b)
        console.log('res_a', res_a)
        },
    • 运行截图
      • 暴力法求主色调
      • 无相关点越多,误差越大👎
        • 我们可以“对每个颜色通道单独重新采样,将每个通道的色阶从256减少到某个指定的数字。这样得到一个新的小的多的颜色空间,而原图像中的每一个像素将被用在新的颜色空间中的最近邻取代”
          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          const COLOR_SIZE = 40 // 单位色块的大小(像素个数,默认40)。以单位色块的平均像素值为作为统计单位
          const LEVEL = 32 // 色深,颜色分区参数(0-255),总256,2^8,即8bit,4个通道(rgba),即默认色深4*8bit,32bit
          // 分区块,可以拓展性的求主要色板,用来做palette
          const mapData = that.getLevelData(imageData);
          const colors = that.getMostColor(mapData);
          if (!colors) {
          return
          } else {
          const color = that.getAverageColor(colors)
          console.log('color', color)
          }
    • 将getImageData数据分成特定大小的区块,分别算出各个区块的averageColor,再利用map特性,将averageColor作key,count各个averageColor个数
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      // 获取每段的颜色数据
      // 根据像素数据,按单位色块进行切割
      getLevelData(imageData) {
      const len = imageData.length;
      const mapData = {};
      for (let i = 0; i < len; i += COLOR_SIZE * 4) {
      const blockColor = this.getBlockColor(imageData, i); // 该区块平均rgba [{r,g,b,a}]数据
      // 获取各个区块的平均rgba数据,将各个通道的颜色进行LEVEL色深降级
      // 根据r_g_b_a 建立map索引
      const key = this.getColorLevel(blockColor);
      !mapData[key] && (mapData[key] = []);
      mapData[key].push(blockColor);
      }
      return mapData;
      },

      // 获取单位块的全部色值
      // 并根据全部色值,计算平均色值
      // 处理最后边界值,小于COLOR_SIZE
      getBlockColor(imageData, start) {
      let data = [],
      count = COLOR_SIZE,
      len = COLOR_SIZE * 4;
      imageData.length <= start + len && (count = Math.floor((imageData.length - start - 1) / 4));
      for (let i = 0; i < count; i += 4) {
      data.push({
      r: imageData[start + i + 0],
      g: imageData[start + i + 1],
      b: imageData[start + i + 2],
      a: imageData[start + i + 3]
      })
      }
      return this.getAverageColor(data);
      },

      // 取出各个通道的平均值,即为改色块的平均色值
      getAverageColor(colorArr) {
      const len = colorArr.length;
      let sr = 0, sg = 0, sb = 0, sa = 0;
      colorArr.map(function (item) {
      sr += item.r;
      sg += item.g;
      sb += item.b;
      sa += item.a;
      });
      return {
      r: Math.round(sr / len),
      g: Math.round(sg / len),
      b: Math.round(sb / len),
      a: Math.round(sa / len)
      }
      },

      getColorLevel(color) {
      return this.getLevel(color.r) + '_' + this.getLevel(color.g) + '_' + this.getLevel(color.b) + '_' + this.getLevel(color.a)
      },

      // 色深降级
      getLevel(value) {
      return Math.round(value / LEVEL)
      },
    • 把出现次数最多的averageColor区块,作为采样区块,再获取一遍averageColor,即是最终主色调
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      // 根据色块颜色,获取
      getMostColor(colorData) {
      let rst = null, len = 0;
      for (let key in colorData) {
      console.log('colorData[key].length', colorData[key].length)
      console.log('colorData[key].length', colorData[key])
      console.log('colorData[key].length', key)
      colorData[key].length > len && (
      rst = colorData[key],
      len = colorData[key].length
      )
      }
      return rst;
      },
    • 运行截图
      • 区块获取主色调
      • 颜色可信度提高了很多

        拓展实践-医学灰色影像

        • 核心:将三通道转成单通道即可
          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          14
          15
          16
          17
          18
          19
          20
          21
          22
          23
          24
          25
          26
          27
          28
          let greyImageData = that.greyTheImage(imageData)
          wx.canvasPutImageData({
          canvasId: 'myCanvas',
          x: 150,
          y: 0,
          width: 150,
          height: 100,
          data: greyImageData,
          success (res) {
          console.log('canvasPutImageData it worked!')
          }
          })

          greyTheImage(imageData) {
          // imageData有4个通道rgba
          for (let i = 0; i < imageData.length; i += 4) {
          let sum_rgb = 0
          // 但我们只需要rgb三通道,a-alpha通道无用
          for (let j = 0; j < 3; j += 1) {
          sum_rgb = sum_rgb + imageData[i + j]
          }
          let grey = Math.round(sum_rgb / 3)
          imageData[i] = grey
          imageData[i + 1] = grey
          imageData[i + 2] = grey
          }
          return imageData
          },
        • 运行截图
          医学灰色影像

格式化rgba输出

  • 像这样格式化rgba输出
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    // 对最终颜色的字符串格式化
    /**
    * result:{
    * hex:'#ffffff', 十六位值
    * hexa:'#ffffff00', 十六位值带alpha值
    * rgb:'rgb(0,0,0)', RGB值
    * rgba:'rgba(0,0,0,0)' RGB值带alpha值
    * }
    */

    colorStrFormat(color) {
    const rgba = 'rgba(' + color.r + ',' + color.g + ',' + color.b + ',' + (color.a / 255).toFixed(4).replace(/\.*0+$/, '') + ')';
    const rgb = 'rgb(' + color.r + ',' + color.g + ',' + color.b + ')';
    const hex = '#' + this.Num2Hex(color.r) + this.Num2Hex(color.g) + this.Num2Hex(color.b);
    const hexa = hex + this.Num2Hex(color.a);
    return {
    rgba: rgba,
    rgb: rgb,
    hex: hex,
    hexa: hexa
    }
    },

    Num2Hex(num) {
    const hex = num.toString(16) + '';
    if (hex.length < 2) {
    return '0' + hex;
    } else {
    return hex;
    }
    },

    参考文献

  • phg1024 JavaScript图像处理(6) - 减色算法(Color Reduction)
  • 获取图片主色调的插件
  • 其实也可以利用 CSS新特性去改变背景颜色,比如高斯模糊等来达到业务需求

    写在后面

  • 周六听了一天 John Lennon 歌曲【这也是我拖更的原因😶 羡慕他和 Yoko 那段伊甸园般的爱情,他对 Yoko 的痴爱,他和母亲的两次分别感同身受,Beatles在一起做音乐的欢乐,他对 Sean 的宠溺父爱,但他的人生却在一次枪杀后戛然而止,心痛 心痛 🌧
  • 祝大家多多发财