zoukankan      html  css  js  c++  java
  • 基于聚类和神经网络的图像颜色提取和评分方案

    概述

    图像颜色提取的前端React方案,基于canvas,使用K均值聚类算法提取主要颜色(颜色量子化),用离线训练的神经网络进行评分,附带可视化方案和相关动画。

    详细

    本文github地址:colorful-color

    体验 demo


    一、目录结构

    snap 1497925990.jpg

    本项目是基于React编写的,build目录下有编译好的版本,编译源码前执行以下指令以安装依赖(需要安装好了nodenpm):

    npm install

    二、组件介绍

    components目录下包含6个组件:

    1. CanvasBubbleChart.js:一个基于canvas,用于颜色可视化的组件,使用方式:

    <CanvasBubbleChart colors={this.state.colorsInfo}></CanvasBubbleChart>

    colors 是输入的颜色信息,是一个数组,数组中的对象格式如下:

    {
        fre: "表示颜色的权重,也就是出现次数",
        h: "HSL中的色相",
        s: "HSL中的饱和度"
        l: "HSL中的明度"
    }

    2. ColorBar.js:显示三个颜色,使用方式如下:

    <ColorBar label="main color" ></ColorBar>

    组件包含的如下state:

    this.state = {
        colorStart: '',
        colorMiddle: '',
        colorEnd: ''
    };

    3. ColorCard.js:以卡片形式显示一系列颜色,使用方式如下:

    <ColorCard colors={}></ColorCard>

    colors 属性是数组,包含要显示的颜色:

    colors = ["red", "blue", "#333"];

    4. ImageInfo.js:显示提取结果的相关信息:

     <ImageInfo processInfo={this.state.processInfo}></ImageInfo>

    colors 属性是数组,包含要显示的颜色:

    processInfo = {
            colors: 0,
            censusTime: 0,
            kmeansIteration:0,
            kmeansTime:0,
            top5Count: 0,
            showQr:false
    },

    5. ImageShowcase.js:用于显示用户选取的图像,最终的调色盘也会绘制在此组件的canvas中:

     <ImageShowcase setScoreLayer={this.setScoreLayer} censusColors={this.censusColors} resetApp={this.resetApp}></ImageShowcase>

    setScoreLayer 函数用于唤起评分结果页,censusColors 函数是颜色提取和评分的入口函数,resetApp 函数用于重置整个app,组件包含的状态如下:

    this.state = {
          bgC: "",
          K: 6,
          isMounted: false,
          clusterColors:[],
          showSave:false
    };

    6. ScoreLayer.js:用于显示用户选取的图像,最终的调色盘也会绘制在此组件的canvas中:

     <ScoreLayer loopColors={this.state.loopColors} score={this.state.score} setScoreLayer={this.setScoreLayer}  showScoreLayer = {this.state.showScoreLayer}></ScoreLayer>

    loopColors 属性用于唤起评分结果页,score 表示得分,showScoreLayer 用于控制全屏显示组件与否。


    三、关键函数

    3.1 读取图像

    读取图像的逻辑在 ImageShowcase 类中,入口是 readFile 函数,核心在于对尺寸比例的控制,因为要使得移动端显示清晰,所以确定尺寸要考虑 window.devicePixelRatio ,代码如下:

    let pixelRatio = this.pixelRatio;
    let c_w = canvas.width;
    let c_h = canvas.height;
    let img_w = img.width > (c_w-_w)/pixelRatio ? (c_w-_w)/pixelRatio : img.width;
    let img_h = img.height > (c_h-_h)/pixelRatio ? (c_h-_h)/pixelRatio : img.height;
    let scale = (img_w / img.width < img_h / img.height) ? (img_w / img.width) : (img_h / img.height);
    img_w = img.width * scale;
    img_h = img.height * scale;
    canvas.style.width = img_w + _w/pixelRatio + "px";
    canvas.style.height = img_h + _h/pixelRatio + "px";
    canvas.width = (img_w*pixelRatio + _w);
    canvas.height = (img_h*pixelRatio + _h);
    ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, img_w*pixelRatio, img_h*pixelRatio);

    3.2 render

    Demo的根组件是 ColorCensus.js ,里面包含了一个叫做 ColorCensus 的类,所谓关键函数就是 ColorCensus 类的各个成员函数。首先是render函数,也就是Demo的JSX部分咯,看代码:

     render() {
        let mcProps =  this.colorToProps(this.state.mainColor);
        let acProps = this.colorToProps(this.state.averageColor);
        let footWrapClass = this.state.showQr ? 'show-qr' : '';
        footWrapClass += " foot-wrap";
        return (
          <div className="App">
             // 顶部的组件,用于显示图像和提取的主要颜色
            <ImageShowcase setScoreLayer={this.setScoreLayer} censusColors={this.censusColors} resetApp={this.resetApp}></ImageShowcase>
            // 评分结果组件
            <ScoreLayer loopColors={this.state.loopColors} score={this.state.score} setScoreLayer={this.setScoreLayer}  showScoreLayer = {this.state.showScoreLayer}></ScoreLayer>
            // 显示主要颜色与平均颜色
            <ColorBar label="main color" {...mcProps}></ColorBar>
            <ColorBar label="average color" {...acProps}></ColorBar>
            // 显示聚类后的主要颜色
            <ColorCard colors={this.state.clusterColors}></ColorCard>
            // 显示提取的信息
            <ImageInfo processInfo={this.state.processInfo}></ImageInfo>
             // 颜色可视化--color bubble
            <CanvasBubbleChart colors={this.state.colorsInfo}></CanvasBubbleChart>
            <div className={footWrapClass}>
              // 页面底部内容,不重要所以忽略
          </div>
        );
      }

    3.3 censusColors

    颜色提取与评分的入口函数,首先来看形参:

      /**
       * [censusColors description]
       * @param  {Object}  ctx          context of canvas
       * @param  {number}  K            K for K-Means
       * @param  {number}  c_w          width of canvas
       * @param  {number}  c_h          height of canvas
       * @param  {Boolean} isHorizontal direction of image
       * @param  {function}  callBack     callback when census done
       */
      censusColors(ctx, K, c_w, c_h, isHorizontal, callBack) {}

    颜色提取的第一步是统计图片的颜色信息,所以需要获取canvas中的图像数据,把颜色转换到 HSL 空间,降采样后编码得到不同颜色的 key ,看代码:

    // 从canvas中获取图像数据
    let w = c_w;
    let h = c_h;
    let imageDate;
    let pixelRatio = window.devicePixelRatio || 1;
    if(isHorizontal){
        imageDate = ctx.getImageData(0, 0, w, h-100*pixelRatio);
    }else{
        imageDate = ctx.getImageData(0, 0, w-100*pixelRatio, h);
    }
    
    // 遍历图像以统计颜色信息
    let rows = imageDate.height;
    let cols = imageDate.width;
    let keys = [];
    let colors_info = [];
    let pixel_step = (rows * cols < 600 * 600) ? 1 : 2;  // 降采样步长
    
    // 遍历
    for (let row = 1; row < rows - 1;) {
        for (let col = 1; col < cols - 1;) {
            r = imageDate.data[row * cols * 4 + col * 4];
            g = imageDate.data[row * cols * 4 + col * 4 + 1];
            b = imageDate.data[row * cols * 4 + col * 4 + 2];
            hsl = rgbToHsl(r,g,b);
            // 过滤太亮或太暗的颜色
            if("太亮或太暗的条件"){
                continue;
            }
            // 编码得到key
            h_key = Math.floor(hsl[0] / 10) * 10000;
            s_key = Math.floor(hsl[1] / 5) * 100;
            l_key = Math.floor(hsl[2] / 5);
            key = h_key + s_key + l_key;
            let index = keys.indexOf(key);
            if (index < 0) {
                  // 没找到该颜色,将key加入到keys序列中
            } else {
                  // 找到了key,更新颜色的fre(出现次数)值
            }
            col += pixel_step;
        }
        row += pixel_step;
    }

    接下来是对颜色信息进行排序和过滤:

    // 按照出现次数从高到低排序
    colors_info.sort(function(pre, next) {
        return next.fre - pre.fre;
    });
    // 过滤掉孤立的颜色
    colors_info = colors_info.filter((color) => {
        // isolated color
        let flag = (color.fre < 5 - pixel_step) && (len > 400);
        return !flag;
    });

    之后便是进行聚类和评分了,后续会展开讲。

    3.4 chooseSeedColors

    因为K均值聚类对初始种子点比较敏感,为了尽快收敛和聚类的精确性,专门写了一个函数用于筛选种子点,核心代码如下:

    // 从出现次数最高的颜色开始遍历
    for (let i = 0; i < len; i++) {
          // 比较和已有种子点的差异
          for (; j < l; j++) {
            let h_diff = Math.abs(init_seed[j].h - color.h);
            let s_diff = Math.abs(init_seed[j].s - color.s);
            let l_diff = Math.abs(init_seed[j].l - color.l);
            if (h_diff + s_diff + l_diff < 45) {
              // 差异太小则跳过该颜色
              break;
            }
          }
          // 差异比较大则加入种子点数组
          if (j === l) {
            init_seed.push({
              h:color.h,
              s:color.s,
              l:color.l,
              category: color.category,
              fre: color.fre
            });
          }
          // 如果已经满足指定选取的种子点数量,则停止遍历
          if (init_seed.length >= num) {
            break;
          }
        }

    3.5 K均值聚类

    kMC函数是K均值聚类的实现,形参:

      /**
       * [kMC description]
       * @param  {Array} colors   colors of image
       * @param  {Array} seeds    init seeds
       * @param  {number} max_step max iteration of KMeans
       * @return {Array}          results of KMeans
       */
      kMC(colors, seeds, max_step) {}

    核心流程:

    kMC(colors, seeds, max_step) {
        let iteration_count = 0;
        // 迭代
        while (iteration_count++ < max_step) {
          // filter seeds
          seeds = seeds.filter((seed) => {
            return seed;
          });
    
          // divide colors into different categories with duff's device
          let len = colors.length;
          let count = (len / 8) ^ 0;
          let start = len % 8;
          while (start--) {
            this.classifyColor(colors[start], seeds);
          }
          while (count--) {
            this.classifyColor(colors[--len], seeds);
            this.classifyColor(colors[--len], seeds);
            this.classifyColor(colors[--len], seeds);
            this.classifyColor(colors[--len], seeds);
            this.classifyColor(colors[--len], seeds);
            this.classifyColor(colors[--len], seeds);
            this.classifyColor(colors[--len], seeds);
            this.classifyColor(colors[--len], seeds);
          }
    
          // compute center of category
          len = colors.length;
          let hsl_count = [];
          let category;
          while (len--) {
            category = colors[len].category;
            if (!hsl_count[category]) {
              hsl_count[category] = {};
              hsl_count[category].h = 0;
              hsl_count[category].s = 0;
              hsl_count[category].l = 0;
              hsl_count[category].fre_count = colors[len].fre;
            } else {
              hsl_count[category].fre_count += colors[len].fre;
            }
          }
          len = colors.length;
          while (len--) {
            category = colors[len].category;
            hsl_count[category].h += colors[len].h*colors[len].fre/hsl_count[category].fre_count;
            hsl_count[category].s += colors[len].s*colors[len].fre/hsl_count[category].fre_count;
            hsl_count[category].l += colors[len].l*colors[len].fre/hsl_count[category].fre_count;
          }
          // 判断是否满足退出条件
          let flag = hsl_count.every((ele, index) => {
            return Math.abs(ele.h - seeds[index].h)<0.5 && Math.abs(ele.s - seeds[index].s)<0.5 && Math.abs(ele.l - seeds[index].l)<0.5;
          });
          // 新的聚类中心
          seeds = hsl_count.map((ele, index) => {
            return {
              h: ele.h,
              s: ele.s,
              l: ele.l,
              category: index,
              fre: ele.fre_count
            };
          });
          if (flag) {
            break;
          }
        }
        return [seeds,iteration_count];
      }

    其中的 classifyColor 函数用于对颜色分类,比较好懂,具体实现参见源码。

    3.6 评分

    评分部分的实现在 imageScore 函数中,提取的评价指标比较好运算,所以这里只讲一下相关指标的含义:

    let info = {
          colorCount: (Math.log10(colorInfo.length)), // 总的颜色数量
          average:0,  // 平均出现次数
          variance: 0,  // 标准差
          top50Count: 0,  // 前50种颜色占比(出现次数)
          top50Average: 0,  // 前50种颜色的平均出现次数
          top50Variance: 0,  // 前50种颜色出现次数的标准差
          top20Count: 0,  // 同上
          top20Average: 0,
          top20Variance: 0,
          top10Count: 0,  // 同上
          top10Average: 0,
          top10Variance: 0,
          top5Count: 0,  // 同上
          top5Average: 0,
          top5Variance: 0
    };

    需要注意的是:

    1. 有些指标的数量级太大需要使用 Math.log10() 减少量级;

    2. 所有指标需要归一化到0~1之间,为了保留尽可能多的信息采用除最大值的归一化方法

    神经网络评分:

    this.net.run(info)

    神经网络采用的是 brain ,离线网络存储在util文件夹下的 trainData.js 中,训练数据也在其中,为了提高精确性我用了两个网络评分,最后加权值算了个综合的结果。训练的图片选自图虫网热门图片,因为很难找到相关的带评分的图像数据集,且评分往往是综合的(包含内容,构图,人物等)考量结果,很难从评分中分离出只和颜色相关的部分,所以我按照自己的主观想法对图片进行了打分,训练结果会强烈的偏向我个人的喜好。你也可以按照自己的喜好重新训练,训练方式参见brain的说明文档。


    四、SVG和动画

    svg动画在 ScoreLayer.js 中,如下:

    <svg className="circle" viewBox="0,0,120,120">
        <defs>
            <linearGradient key="basic0" id="basic0" x1="0" y1="70%" x2="100%" y2="30%">
                <stop offset="0%" style={{stopColor:this.props.loopColors.bc0[0],stopOpacity:1}}></stop>
                <stop offset="100%" style={{stopColor:this.props.loopColors.bc0[1],stopOpacity:1}}></stop>
            </linearGradient>
            <linearGradient key="basic1" id="basic1" x1="0" y1="0%" x2="100%" y2="100%">
                <stop offset="0%" style={{stopColor:this.props.loopColors.bc1[0],stopOpacity:1}} />
                <stop offset="100%" style={{stopColor:this.props.loopColors.bc1[1],stopOpacity:1}}/>
            </linearGradient>
            <linearGradient key="basic2" id="basic2" x1="0" y1="0%" x2="100%" y2="100%">
                <stop offset="0%" style={{stopColor:this.props.loopColors.bc2[0],stopOpacity:1}} />
                <stop offset="100%" style={{stopColor:this.props.loopColors.bc2[1],stopOpacity:1}} />
            </linearGradient>
            <linearGradient key="basic3" id="basic3" x1="0" y1="0%" x2="100%" y2="100%">
                <stop offset="0%" style={{stopColor:this.props.loopColors.bc3[0],stopOpacity:0.9}} />
                <stop offset="100%" style={{stopColor:this.props.loopColors.bc3[1],stopOpacity:0.9}} />
            </linearGradient>
        </defs>
        <g>
            <path className="c1" d="m 40 25.35 q 20 -25.35 40 0 q 31.96 4.65 20 34.64 q 11.96 30 -20 34.64 q -20 25.35 -40 0 q -31.96 -4.65 -20 -34.64 q -11.96 -30 20 -34.64"/>
            <animateTransform attributeName="transform" begin="0s" dur="17s" type="rotate" values="0 60 60;-180 59 59;-360 60 60" repeatCount="indefinite"/>
        </g>
        <g>
            <path className="c2" d="m 40 25.35 q 20 -25.35 40 0 q 31.96 4.65 20 34.64 q 11.96 30 -20 34.64 q -20 25.35 -40 0 q -31.96 -4.65 -20 -34.64 q -11.96 -30 20 -34.64"/>
            <animateTransform attributeName="transform" begin="0s" dur="13s" type="rotate" values="0 60 60;180 61 59;360 60 60" repeatCount="indefinite"/>
        </g>
        <g>
            <path className="c3" d="m 40 25.35 q 20 -25.35 40 0 q 31.96 4.65 20 34.64 q 11.96 30 -20 34.64 q -20 25.35 -40 0 q -31.96 -4.65 -20 -34.64 q -11.96 -30 20 -34.64"/>
            <animateTransform attributeName="transform" begin="0s" dur="11s" type="rotate" values="0 60 60;-180 59 61;-360 60 60" repeatCount="indefinite"/>
        </g>
        <g>
            <circle cx="60" cy="60" r="45" className="c0">
                <animate attributeName="r" values="45;40;45" dur="11s" repeatCount="indefinite" />
            </circle>
        </g>
        <text className="scores" x="50%" y="50%" textAnchor="middle" dominantBaseline="middle">{this.props.score ? this.props.score.toFixed(1) : 'score...'}</text>
    </svg>

    SVG部分可参考我的codepen,canvas动画本身也不简单,代码也都是我自己实现的,详细的可参考这个codepen

    五、运行结果

    操作:

    我已经把编译好的代码打包到 colorful-color 目录下的 index.html 中了,直接在浏览器中就可以打开运行。如果你修改了源代码,需要你重新编译(npm run build)。

    先选取图片,然后点击紫色button执行demo。

    snap 1497955572.jpg

    结果:

    snap 1497951224.jpg

    有任何问题可以到github提issue。

    注:本文著作权归作者,由demo大师发表,拒绝转载,转载需要作者授权

  • 相关阅读:
    [iOS]为什么不要在init初始化方法里调用self.view
    [iOS]ARC和MRC下混编
    CollectionView的基础代码
    关于ios项目中加入webp格式的图片
    【音频】远程链接音频播放(AVPlayer)
    iOS微信支付回调和iOS9系统左上角返回的冲突解决
    【转载】iOS开发经验总结
    【转载】iOS超全开源框架、项目和学习资料汇总(4)数据库、缓存处理、图像浏览、摄像照相视频音频篇
    【转载】3分钟实现iOS语言本地化/国际化(图文详解)
    微信小程序推荐网站
  • 原文地址:https://www.cnblogs.com/demodashi/p/8463999.html
Copyright © 2011-2022 走看看