App基于手机壳颜色换肤?先尝试一下用 KMeans 来提取图像中的主色

酷酷的.jpg

背景

上周,某公司的产品经理提了一个需求:根据用户手机壳颜色来改变 App 主题颜色。可能是由于这天马行空的需求激怒了程序员,导致程序员和产品经理打了起来,最后双双被公司开除。

那如何实现这个功能呢?首先需要获取图像中的主色。

插一句题外话,作为程序员在桌面上还是要有一些必备的东西需要放的。

程序员桌面必备杯垫.JPG

KMeans 算法

k-平均算法(英文:k-means clustering)源于信号处理中的一种向量量化方法,现在则更多地作为一种聚类分析方法流行于数据挖掘领域。k-平均聚类的目的是:把 n 个点(可以是样本的一次观察或一个实例)划分到k个聚类中,使得每个点都属于离他最近的均值(此即聚类中心)对应的聚类,以之作为聚类的标准。这个问题将归结为一个把数据空间划分为Voronoi cells的问题。

KMeans 算法思想为:给定n个数据点{x1,x2,…,xn},找到K个聚类中心{a1,a2,…,aK},使得每个数据点与它最近的聚类中心的距离平方和最小,并将这个距离平方和称为目标函数,记为Wn,其数学表达式为:

KMeans.png

本文使用 KMeans 算法对图像颜色做聚类。

算法基本流程:
1、初始的 K 个聚类中心。
2、按照距离聚类中心的远近对所有样本进行分类。
3、重新计算聚类中心,判断是否退出条件:
两次聚类中心的距离足够小视为满足退出条件;
不退出则重新回到步骤2。

算法实现

    public List<Scalar> extract(ColorProcessor processor) {
        // initialization the pixel data
        int width = processor.getWidth();
        int height = processor.getHeight();
        byte[] R = processor.getRed();
        byte[] G = processor.getGreen();
        byte[] B = processor.getBlue();
        
        //Create random points to use a the cluster center
        Random random = new Random();
        int index = 0;
        for (int i = 0; i < numOfCluster; i++)
        {
            int randomNumber1 = random.nextInt(width);
            int randomNumber2 = random.nextInt(height);
            index = randomNumber2 * width + randomNumber1;
            ClusterCenter cc = new ClusterCenter(randomNumber1, randomNumber2, R[index]&0xff, G[index]&0xff, B[index]&0xff);
            cc.cIndex = i;
            clusterCenterList.add(cc); 
        }
        
        // create all cluster point
        for (int row = 0; row < height; ++row)
        {
            for (int col = 0; col < width; ++col)
            {
                index = row * width + col;
                pointList.add(new ClusterPoint(row, col, R[index]&0xff, G[index]&0xff, B[index]&0xff));

            }
        }
        
        // initialize the clusters for each point
        double[] clusterDisValues = new double[clusterCenterList.size()];
        for(int i=0; i<pointList.size(); i++)
        {
            for(int j=0; j<clusterCenterList.size(); j++)
            {
                clusterDisValues[j] = calculateEuclideanDistance(pointList.get(i), clusterCenterList.get(j));
            }
            pointList.get(i).clusterIndex = (getCloserCluster(clusterDisValues));
        }
        
        // calculate the old summary
        // assign the points to cluster center
        // calculate the new cluster center
        // computation the delta value
        // stop condition--
        double[][] oldClusterCenterColors = reCalculateClusterCenters();
        int times = 10;
        while(true)
        {
            stepClusters();
            double[][] newClusterCenterColors = reCalculateClusterCenters();
            if(isStop(oldClusterCenterColors, newClusterCenterColors))
            {               
                break;
            } 
            else
            {
                oldClusterCenterColors = newClusterCenterColors;
            }
            if(times > 10) {
                break;
            }
            times++;
        }
        
        //update the result image
        List<Scalar> colors = new ArrayList<Scalar>();
        for(ClusterCenter cc : clusterCenterList) {
            
            colors.add(cc.color);
        }
        return colors;
    }

    private boolean isStop(double[][] oldClusterCenterColors, double[][] newClusterCenterColors) {
        boolean stop = false;
        for (int i = 0; i < oldClusterCenterColors.length; i++) {
            if (oldClusterCenterColors[i][0] == newClusterCenterColors[i][0] &&
                    oldClusterCenterColors[i][1] == newClusterCenterColors[i][1] &&
                    oldClusterCenterColors[i][2] == newClusterCenterColors[i][2]) {
                stop = true;
                break;
            }
        }
        return stop;
    }

    /**
     * update the cluster index by distance value
     */
    private void stepClusters() 
    {
        // initialize the clusters for each point
        double[] clusterDisValues = new double[clusterCenterList.size()];
        for(int i=0; i<pointList.size(); i++)
        {
            for(int j=0; j<clusterCenterList.size(); j++)
            {
                clusterDisValues[j] = calculateEuclideanDistance(pointList.get(i), clusterCenterList.get(j));
            }
            pointList.get(i).clusterIndex = (getCloserCluster(clusterDisValues));
        }
        
    }

    /**
     * using cluster color of each point to update cluster center color
     * 
     * @return
     */
    private double[][] reCalculateClusterCenters() {
        
        // clear the points now
        for(int i=0; i<clusterCenterList.size(); i++)
        {
             clusterCenterList.get(i).numOfPoints = 0;
        }
        
        // recalculate the sum and total of points for each cluster
        double[] redSums = new double[numOfCluster];
        double[] greenSum = new double[numOfCluster];
        double[] blueSum = new double[numOfCluster];
        for(int i=0; i<pointList.size(); i++)
        {
            int cIndex = (int)pointList.get(i).clusterIndex;
            clusterCenterList.get(cIndex).numOfPoints++;
            int tr = pointList.get(i).pixelColor.red;
            int tg = pointList.get(i).pixelColor.green;
            int tb = pointList.get(i).pixelColor.blue;
            redSums[cIndex] += tr;
            greenSum[cIndex] += tg;
            blueSum[cIndex] += tb;
        }
        
        double[][] oldClusterCentersColors = new double[clusterCenterList.size()][3];
        for(int i=0; i<clusterCenterList.size(); i++)
        {
            double sum  = clusterCenterList.get(i).numOfPoints;
            int cIndex = clusterCenterList.get(i).cIndex;
            int red = (int)(greenSum[cIndex]/sum);
            int green = (int)(greenSum[cIndex]/sum);
            int blue = (int)(blueSum[cIndex]/sum);
            clusterCenterList.get(i).color = new Scalar(red, green, blue);
            oldClusterCentersColors[i][0] = red;
            oldClusterCentersColors[i][0] = green;
            oldClusterCentersColors[i][0] = blue;
        }
        
        return oldClusterCentersColors;
    }
    
    

    /**
     * 
     * @param clusterDisValues
     * @return
     */
    private double getCloserCluster(double[] clusterDisValues)
    {
        double min = clusterDisValues[0];
        int clusterIndex = 0;
        for(int i=0; i<clusterDisValues.length; i++)
        {
            if(min > clusterDisValues[i])
            {
                min = clusterDisValues[i];
                clusterIndex = i;
            }
        }
        return clusterIndex;
    }

    /**
     *
     * @param p
     * @param c
     * @return distance value
     */
    private double calculateEuclideanDistance(ClusterPoint p, ClusterCenter c) 
    {
        int pr = p.pixelColor.red;
        int pg = p.pixelColor.green;
        int pb = p.pixelColor.blue;
        int cr = c.color.red;
        int cg = c.color.green;
        int cb = c.color.blue;
        return Math.sqrt(Math.pow((pr - cr), 2.0) + Math.pow((pg - cg), 2.0) + Math.pow((pb - cb), 2.0));
    }

在 Android 中使用该算法来提取主色:

demo1.png
demo2.png

完整的算法实现可以在:https://github.com/imageprocessor/cv4j/blob/master/cv4j/src/main/java/com/cv4j/core/pixels/PrincipalColorExtractor.java 找到,它是一个典型的 KMeans 算法。

我们的算法中,K默认值是5,当然也可以自己指定。

以上算法目前在 demo 上耗时蛮久,不过可以有优化空间。例如,可以使用 RxJava 在 computation 线程中做复杂的计算操作然后切换回ui线程。亦或者可以使用类似 Kotlin 的 Coroutines 来做复杂的计算操作然后切换回ui线程。

总结

提取图像中的主色,还有其他算法例如八叉树等,在 Android 中也可以使用 Palette 的 API来实现。

cv4jgloomyfish和我一起开发的图像处理库,纯java实现,我们已经分离了一个Android版本和一个Java版本。

如果您想看该系列先前的文章可以访问下面的文集:
https://www.jianshu.com/nb/10401400

最后提醒一句,作为程序员,还是要多健身。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,302评论 5 470
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,232评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,337评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,977评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,920评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,194评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,638评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,319评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,455评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,379评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,426评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,106评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,696评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,786评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,996评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,467评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,043评论 2 341

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,537评论 18 399
  • 在开始正文之前,先和大家聊点闲话:这是我关注简书以来写的第一篇笔记,当然我坚信这只是一个开始,以后会坚持...
    牧阳十二阅读 1,179评论 0 4
  • 文丨顾惜 昨天听一朋友说,她下班回家看到阳台上小盆子里有条锦鲤,一问才知道,原来是婆婆在外面带回来,说是给宝贝孙子...
    石之画趣阅读 1,545评论 6 25
  • 昨夜下了很大的雨,扰的一夜不安,也就索性披了件薄衫便起身。 没有开灯,就在一片黑暗里摸索。 从窗外透进微弱的红色光...
    苏小拾阅读 211评论 0 0