前端图片主题色提取

图片主题色在图片所占比例较大的页面中,能够配合图片起到很好视觉效果,给人一种和谐、一致的感觉。同时也可用在图像分类,搜索识别等方面。通常主题色的提取都是在后端完成的,前端将需要处理的图片以链接或id的形式提供给后端,后端通过运行相应的算法来提取出主题色后,再返回相应的结果。

这样可以满足大多数展示类的场景,但对于需要根据用户“定制”、“生成”的图片,这样的方式就有了一个上传图片---->后端计算---->返回结果的时间,等待时间也许就比较长了。由此,我尝试着利用 canvas在前端进行图片主题色的提取。

一、主题色算法

目前比较常用的主题色提取算法有:最小差值法、中位切分法、八叉树算法、聚类、色彩建模法等。其中聚类和色彩建模法需要对提取函数和样本、特征变量等进行调参和回归计算,用到 python的数值计算库 numpy和机器学习库 scikit-learn,用 python来实现相对比较简单,而目前这两种都没有成熟的js库,并且js本身也不擅长回归计算这种比较复杂的计算。我也就没有深入的研究,而主要将目光放在了前面的几个颜色量化算法上。

而最小差值法是在给定给定调色板的情况下找到与色差最小的颜色,使用的场景比较小,所以我主要看了中位切分法和八叉树算法,并进行了实践。

中位切分法

中位切分法通常是在图像处理中降低图像位元深度的算法,可用来将高位的图转换位低位的图,如将24bit的图转换为8bit的图。我们也可以用来提取图片的主题色,其原理是是将图像每个像素颜色看作是以R、G、B为坐标轴的一个三维空间中的点,由于三个颜色的取值范围为0~255,所以图像中的颜色都分布在这个颜色立方体内,如下图所示。

之后将RGB中最长的一边从颜色统计的中位数一切为二,使得到的两个长方体所包含的像素数量相同,如下图所示

重复这个过程直到切出长方体数量等于主题色数量为止,最后取每个长方体的中点即可。

在实际使用中如果只是按照中点进行切割,会出现有些长方体的体积很大但是像素数量很少的情况。解决的办法是在切割前对长方体进行优先级排序,排序的系数为体积 * 像素数。这样就可以基本解决此类问题了。

八叉树算法

八叉树算法也是在颜色量化中比较常见的,主要思路是将R、G、B通道的数值做二进制转换后逐行放下,可得到八列数字。如 #FF7880转换后为

R: 1111 1111
G: 0111 1000
B: 0000 0000

再将RGB通道逐列粘合,可以得到8个数字,即为该颜色在八叉树中的位置,如图。

在将所有颜色插入之后,再进行合并运算,直到得到所需要的颜色数量为止。

在实际操作中,由于需要对图像像素进行遍历后插入八叉树中,并且插入过程有较多的递归操作,所以比中位切分法要消耗更长的时间。

二、中位切分法实践

根据之前的介绍和网上的相关资料,此处贴上我自己理解实现的中位切分法代码,并且找了几张图片将结果与QQ音乐已有的魔法色相关算法进行比较,图一为中位切分法结果,图二为后台cgi返回结果

图一

图二

可以看到有一定的差异,但是差值相对都还比较小的,处理速度在pc上面还是比较快的,三张图分别在70ms,100ms,130ms左右。这里贴上代码,待后续批量处理进行对比之后再分析。

(function () {

    /**
     * 颜色盒子类
     *
     * @param {Array} colorRange    [[rMin, rMax],[gMin, gMax], [bMin, bMax]] 颜色范围
     * @param {any} total   像素总数, imageData / 4
     * @param {any} data    像素数据集合
     */
    function ColorBox(colorRange, total, data) {
        this.colorRange = colorRange;
        this.total = total;
        this.data = data;
        this.volume = (colorRange[0][1] - colorRange[0][0]) * (colorRange[1][1] - colorRange[1][0]) * (colorRange[2][1] - colorRange[2][0]);
        this.rank = this.total * (this.volume);
    }

    ColorBox.prototype.getColor = function () {
        var total = this.total;
        var data = this.data;

        var redCount = 0,
            greenCount = 0,
            blueCount = 0;

        for (var i = 0; i < total; i++) {
            redCount += data[i * 4];
            greenCount += data[i * 4 + 1];
            blueCount += data[i * 4 + 2];
        }

        return [parseInt(redCount / total), parseInt(greenCount / total), parseInt(blueCount / total)];
    }

    // 获取切割边
    function getCutSide(colorRange) {   // r:0,g:1,b:2
        var arr = [];
        for (var i = 0; i < 3; i++) {
            arr.push(colorRange[i][1] - colorRange[i][0]);
        }
        return arr.indexOf(Math.max(arr[0], arr[1], arr[2]));
    }

    // 切割颜色范围
    function cutRange(colorRange, colorSide, cutValue) {
        var arr1 = [];
        var arr2 = [];
        colorRange.forEach(function (item) {
            arr1.push(item.slice());
            arr2.push(item.slice());
        })
        arr1[colorSide][1] = cutValue;
        arr2[colorSide][0] = cutValue;
        return [arr1, arr2];
    }

    // 找到出现次数为中位数的颜色
    function getMedianColor(colorCountMap, total) {
        var arr = [];
        for (var key in colorCountMap) {
            arr.push({
                color: parseInt(key),
                count: colorCountMap[key]
            })
        }

        var sortArr = __quickSort(arr);
        var medianCount = 0;
        var medianColor = 0;
        var medianIndex = Math.floor(sortArr.length / 2)

        for (var i = 0; i <= medianIndex; i++) {
            medianCount += sortArr[i].count;
        }

        return {
            color: parseInt(sortArr[medianIndex].color),
            count: medianCount
        }

        // 另一种切割颜色判断方法,根据数量和差值的乘积进行判断,自己试验后发现效果不如中位数方法,但是少了排序,性能应该有所提高
        // var count = 0;
        // var colorMin = arr[0].color;
        // var colorMax = arr[arr.length - 1].color
        // for (var i = 0; i < arr.length; i++) {
        //     count += arr[i].count;

        //     var item = arr[i];

        //     if (count * (item.color - colorMin) > (total - count) * (colorMax - item.color)) {
        //         return {
        //             color: item.color,
        //             count: count
        //         }
        //     }
        // }

        return {
            color: colorMax,
            count: count
        }



        function __quickSort(arr) {
            if (arr.length <= 1) {
                return arr;
            }
            var pivotIndex = Math.floor(arr.length / 2),
                pivot = arr.splice(pivotIndex, 1)[0];

            var left = [],
                right = [];
            for (var i = 0; i < arr.length; i++) {
                if (arr[i].count <= pivot.count) {
                    left.push(arr[i]);
                }
                else {
                    right.push(arr[i]);
                }
            }
            return __quickSort(left).concat([pivot], __quickSort(right));
        }
    }

    // 切割颜色盒子
    function cutBox(colorBox) {
        var colorRange = colorBox.colorRange,
            cutSide = getCutSide(colorRange),
            colorCountMap = {},
            total = colorBox.total,
            data = colorBox.data;

        // 统计出各个值的数量
        for (var i = 0; i < total; i++) {
            var color = data[i * 4 + cutSide];

            if (colorCountMap[color]) {
                colorCountMap[color] += 1;
            }
            else {
                colorCountMap[color] = 1;
            }
        }
        var medianColor = getMedianColor(colorCountMap, total);
        var cutValue = medianColor.color;
        var cutCount = medianColor.count;
        var newRange = cutRange(colorRange, cutSide, cutValue);
        var box1 = new ColorBox(newRange[0], cutCount, data.slice(0, cutCount * 4)),
            box2 = new ColorBox(newRange[1], total - cutCount, data.slice(cutCount * 4))
        return [box1, box2];
    }

    // 队列切割
    function queueCut(queue, num) {

        while (queue.length < num) {

            queue.sort(function (a, b) {
                return a.rank - b.rank
            });
            var colorBox = queue.pop();
            var result = cutBox(colorBox);
            queue = queue.concat(result);
        }

        return queue.slice(0, 8)
    }

    function themeColor(img, callback) {

        var canvas = document.createElement('canvas'),
            ctx = canvas.getContext('2d'),
            width = 0,
            height = 0,
            imageData = null,
            length = 0,
            blockSize = 1,
            cubeArr = [];

        width = canvas.width = img.width;
        height = canvas.height = img.height;

        ctx.drawImage(img, 0, 0, width, height);

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

        var total = imageData.length / 4;

        var rMin = 255,
            rMax = 0,
            gMin = 255,
            gMax = 0,
            bMin = 255,
            bMax = 0;

        // 获取范围
        for (var i = 0; i < total; i++) {
            var red = imageData[i * 4],
                green = imageData[i * 4 + 1],
                blue = imageData[i * 4 + 2];

            if (red < rMin) {
                rMin = red;
            }

            if (red > rMax) {
                rMax = red;
            }

            if (green < gMin) {
                gMin = green;
            }

            if (green > gMax) {
                gMax = green;
            }

            if (blue < bMin) {
                bMin = blue;
            }

            if (blue > bMax) {
                bMax = blue;
            }
        }

        var colorRange = [[rMin, rMax], [gMin, gMax], [bMin, bMax]];
        var colorBox = new ColorBox(colorRange, total, imageData);

        var colorBoxArr = queueCut([colorBox], 8);

        var colorArr = [];
        for (var j = 0; j < colorBoxArr.length; j++) {
            colorBoxArr[j].total && colorArr.push(colorBoxArr[j].getColor())
        }

        callback(colorArr);
    }

    window.themeColor = themeColor

})()

三、八叉树算法实践

也许是我算法实现的问题,使用八叉树算法得到的最终结果并不理想,所消耗的时间相对于中位切分法也长了不少,平均时间分别为160ms,250ms,400ms还是主要看八叉树算法吧...同样贴上代码

(function () {

    var OctreeNode = function () {
        this.isLeaf = false;
        this.pixelCount = 0;
        this.red = 0;
        this.green = 0;
        this.blue = 0;
        this.children = [null, null, null, null, null, null, null, null];
        this.next = null;
    }

    var root = null,
        leafNum = 0,
        colorMap = null,
        reducible = null;

    function createNode(index, level) {
        var node = new OctreeNode();
        if (level === 7) {
            node.isLeaf = true;
            leafNum++;
        } else {
            // 将其丢到第 level 层的 reducible 链表中
            node.next = reducible[level];
            reducible[level] = node;
        }

        return node;
    }

    function addColor(node, color, level) {
        if (node.isLeaf) {
            node.pixelCount += 1;
            node.red += color.r;
            node.green += color.g;
            node.bllue += color.b;
        }
        else {
            var str = "";
            var r = color.r.toString(2);
            var g = color.g.toString(2);
            var b = color.b.toString(2);
            while (r.length < 8) r = '0' + r;
            while (g.length < 8) g = '0' + g;
            while (b.length < 8) b = '0' + b;

            str += r[level];
            str += g[level];
            str += b[level];

            var index = parseInt(str, 2);

            if (null === node.children[index]) {
                node.children[index] = createNode(index, level + 1);
            }

            if (undefined === node.children[index]) {
                console.log(index, level, color.r.toString(2));
            }

            addColor(node.children[index], color, level + 1);
        }
    }

    function reduceTree() {

        // 找到最深层次的并且有可合并节点的链表
        var level = 6;
        while (null == reducible[level]) {
            level -= 1;
        }

        // 取出链表头并将其从链表中移除
        var node = reducible[level];
        reducible[level] = node.next;

        // 合并子节点
        var r = 0;
        var g = 0;
        var b = 0;
        var count = 0;
        for (var i = 0; i < 8; i++) {
            if (null === node.children[i]) continue;
            r += node.children[i].red;
            g += node.children[i].green;
            b += node.children[i].blue;
            count += node.children[i].pixelCount;
            leafNum--;
        }

        // 赋值
        node.isLeaf = true;
        node.red = r;
        node.green = g;
        node.blue = b;
        node.pixelCount = count;
        leafNum++;
    }

    function buidOctree(imageData, maxColors) {
        var total = imageData.length / 4;
        for (var i = 0; i < total; i++) {
            // 添加颜色
            addColor(root, {
                r: imageData[i * 4],
                g: imageData[i * 4 + 1],
                b: imageData[i * 4 + 2]
            }, 0);

            // 合并叶子节点
            while (leafNum > maxColors) reduceTree();
        }
    }

    function colorsStats(node, object) {
        if (node.isLeaf) {
            var r = parseInt(node.red / node.pixelCount);
            var g = parseInt(node.green / node.pixelCount);
            var b = parseInt(node.blue / node.pixelCount);

            var color = r + ',' + g + ',' + b;
            if (object[color]) object[color] += node.pixelCount;
            else object[color] = node.pixelCount;
            return;
        }

        for (var i = 0; i < 8; i++) {
            if (null !== node.children[i]) {
                colorsStats(node.children[i], object);
            }
        }
    }

    window.themeColor = function (img, callback) {
        var canvas = document.createElement('canvas'),
            ctx = canvas.getContext('2d'),
            width = 0,
            height = 0,
            imageData = null,
            length = 0,
            blockSize = 1;

        width = canvas.width = img.width;
        height = canvas.height = img.height;

        ctx.drawImage(img, 0, 0, width, height);

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

        root = new OctreeNode();
        colorMap = {};
        reducible = {};
        leafNum = 0;

        buidOctree(imageData, 8)

        colorsStats(root, colorMap)

        var arr = [];
        for (var key in colorMap) {
            arr.push(key);
        }
        arr.sort(function (a, b) {
            return colorMap[a] - colorMap[b];
        })
        arr.forEach(function (item, index) {
            arr[index] = item.split(',')
        })
        callback(arr)
    }
})()

四、结果对比

在批量跑了10000张图片之后,得到了下面的结果

平均耗时对比(js-cgi)

可以看到在不考虑图片加载时间的情况下,用中位切分法提取的耗时相对较短,而图片加载的耗时可以说是难以逾越的障碍了(整整拖慢了450ms),不过目前的代码还有不错的优化空间,比如间隔采样,绘制到canvas时减小图片尺寸,优化切割点查找等,就需要后续进行更深一点的探索了。

颜色偏差

所以看来准确性还是可以的,约76%的颜色与cgi提取结果相近,在大于100的中抽查后发现有部分图片两者提取到的主题色各有特点,或者平分秋色,比如

五、小结

总结来看,通过canvas的中位切分法与cgi提取的结果相似程度还是比较高的,也有许多图片有很大差异,需要在后续的实践中不断优化。同时,图片加载时间也是一个难以逾越的障碍,不过目前的代码还有不错的优化空间,比如间隔采样,绘制到canvas时减小图片尺寸,优化切割点查找等,就需要后续进行更深一点的探索了。

参考文章

http://acm.nudt.edu.cn/~twcourse/ColorQuantization.html

https://xcoder.in/2014/09/17/theme-color-extract/

http://blog.rainy.im/2015/11/24/extract-color-themes-from-images/

https://xinyo.org/archives/66115

https://xinyo.org/archives/66352

https://github.com/lokesh/color-thief/

http://y.qq.com/m/demo/2018/magic_color.html

原文发布于微信公众号 - QQ音乐前端团队(QQMusicFE)

原文发表时间:2018-05-19

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏人工智能

Apache Spark中的决策树

原文地址:https://dzone.com/articles/decision-trees-in-apache-spark

89280
来自专栏点滴积累

geotrellis使用(十六)使用缓冲区分析的方式解决投影变换中边缘数据值计算的问题

Geotrellis系列文章链接地址http://www.cnblogs.com/shoufengwei/p/5619419.html 目录 前言 问题探索 ...

36340
来自专栏Python爬虫实战

图片转字符画

字符画是一系列字符的组合,我们可以把字符看作是比较大块的像素,一个字符能表现一种颜色(暂且这么理解吧),字符的种类越多,可以表现的颜色也越多,图片也会更有层次感...

25520
来自专栏新智元

【前沿】TensorFlow Pytorch Keras代码实现深度学习大神Hinton NIPS2017 Capsule论文

【新智元导读】10月26日,深度学习元老Hinton的NIPS2017 Capsule论文《Dynamic Routing Between Capsules》终...

44270
来自专栏专知

【前沿】TensorFlow Pytorch Keras代码实现深度学习大神Hinton NIPS2017 Capsule论文

【导读】10月26日,深度学习元老Hinton的NIPS2017 Capsule论文《Dynamic Routing Between Capsules》终于在a...

43660
来自专栏从流域到海域

Decision Trees in Apache Spark (Apache Spark中的决策树)

Decision Trees in Apache Spark 原文作者:Akash Sethi 原文地址:https://dzone.com/article...

21860
来自专栏腾讯Bugly的专栏

机器学习入门之HelloWorld(Tensorflow)

1 环境搭建 (Windows) 安装虚拟环境 Anaconda,方便python包管理和环境隔离。 Anaconda3 4.2 http://mirrors...

50080
来自专栏AI深度学习求索

CAM实践:基于pytorch的使用方法

注意:如果为了快一点,不使用网络的图片以及文件的话,记得更改图片地址和已下载文件地址哦

57150
来自专栏逍遥剑客的游戏开发

边缘高亮效果(三)

16720
来自专栏杨熹的专栏

一文学会用 Tensorflow 搭建神经网络

---- cs224d-Day 6: 快速入门 Tensorflow 本文是学习这个视频课程系列的笔记,课程链接是 youtube 上的, 讲的很好,浅显易懂...

51840

扫码关注云+社区

领取腾讯云代金券