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

这篇博客探讨了如何利用KMeans算法提取图像的主要颜色,以满足将App主题色与手机壳颜色匹配的需求。虽然这是一个有趣但颇具挑战性的任务,文章通过介绍KMeans算法的基本原理和在Android中的实现,提供了实现这一功能的思路。还提到了可以使用RxJava或Kotlin的Coroutines优化计算性能,并推荐了cv4j图像处理库。最后,作者建议程序员们关注健康,保持健身习惯。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

640?wx_fmt=jpeg


背景

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

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

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

640?wx_fmt=jpeg


KMeans 算法

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

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

640?wx_fmt=png

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

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

算法实现


 
  1.    public List<Scalar> extract(ColorProcessor processor) {

  2.        // initialization the pixel data

  3.        int width = processor.getWidth();

  4.        int height = processor.getHeight();

  5.        byte[] R = processor.getRed();

  6.        byte[] G = processor.getGreen();

  7.        byte[] B = processor.getBlue();

  8.        //Create random points to use a the cluster center

  9.        Random random = new Random();

  10.        int index = 0;

  11.        for (int i = 0; i < numOfCluster; i++)

  12.        {

  13.            int randomNumber1 = random.nextInt(width);

  14.            int randomNumber2 = random.nextInt(height);

  15.            index = randomNumber2 * width + randomNumber1;

  16.            ClusterCenter cc = new ClusterCenter(randomNumber1, randomNumber2, R[index]&0xff, G[index]&0xff, B[index]&0xff);

  17.            cc.cIndex = i;

  18.            clusterCenterList.add(cc);

  19.        }

  20.        // create all cluster point

  21.        for (int row = 0; row < height; ++row)

  22.        {

  23.            for (int col = 0; col < width; ++col)

  24.            {

  25.                index = row * width + col;

  26.                pointList.add(new ClusterPoint(row, col, R[index]&0xff, G[index]&0xff, B[index]&0xff));

  27.            }

  28.        }

  29.        // initialize the clusters for each point

  30.        double[] clusterDisValues = new double[clusterCenterList.size()];

  31.        for(int i=0; i<pointList.size(); i++)

  32.        {

  33.            for(int j=0; j<clusterCenterList.size(); j++)

  34.            {

  35.                clusterDisValues[j] = calculateEuclideanDistance(pointList.get(i), clusterCenterList.get(j));

  36.            }

  37.            pointList.get(i).clusterIndex = (getCloserCluster(clusterDisValues));

  38.        }

  39.        // calculate the old summary

  40.        // assign the points to cluster center

  41.        // calculate the new cluster center

  42.        // computation the delta value

  43.        // stop condition--

  44.        double[][] oldClusterCenterColors = reCalculateClusterCenters();

  45.        int times = 10;

  46.        while(true)

  47.        {

  48.            stepClusters();

  49.            double[][] newClusterCenterColors = reCalculateClusterCenters();

  50.            if(isStop(oldClusterCenterColors, newClusterCenterColors))

  51.            {              

  52.                break;

  53.            }

  54.            else

  55.            {

  56.                oldClusterCenterColors = newClusterCenterColors;

  57.            }

  58.            if(times > 10) {

  59.                break;

  60.            }

  61.            times++;

  62.        }

  63.        //update the result image

  64.        List<Scalar> colors = new ArrayList<Scalar>();

  65.        for(ClusterCenter cc : clusterCenterList) {

  66.            colors.add(cc.color);

  67.        }

  68.        return colors;

  69.    }

  70.    private boolean isStop(double[][] oldClusterCenterColors, double[][] newClusterCenterColors) {

  71.        boolean stop = false;

  72.        for (int i = 0; i < oldClusterCenterColors.length; i++) {

  73.            if (oldClusterCenterColors[i][0] == newClusterCenterColors[i][0] &&

  74.                    oldClusterCenterColors[i][1] == newClusterCenterColors[i][1] &&

  75.                    oldClusterCenterColors[i][2] == newClusterCenterColors[i][2]) {

  76.                stop = true;

  77.                break;

  78.            }

  79.        }

  80.        return stop;

  81.    }

  82.    /**

  83.     * update the cluster index by distance value

  84.     */

  85.    private void stepClusters()

  86.    {

  87.        // initialize the clusters for each point

  88.        double[] clusterDisValues = new double[clusterCenterList.size()];

  89.        for(int i=0; i<pointList.size(); i++)

  90.        {

  91.            for(int j=0; j<clusterCenterList.size(); j++)

  92.            {

  93.                clusterDisValues[j] = calculateEuclideanDistance(pointList.get(i), clusterCenterList.get(j));

  94.            }

  95.            pointList.get(i).clusterIndex = (getCloserCluster(clusterDisValues));

  96.        }

  97.    }

  98.    /**

  99.     * using cluster color of each point to update cluster center color

  100.     *

  101.     * @return

  102.     */

  103.    private double[][] reCalculateClusterCenters() {

  104.        // clear the points now

  105.        for(int i=0; i<clusterCenterList.size(); i++)

  106.        {

  107.             clusterCenterList.get(i).numOfPoints = 0;

  108.        }

  109.        // recalculate the sum and total of points for each cluster

  110.        double[] redSums = new double[numOfCluster];

  111.        double[] greenSum = new double[numOfCluster];

  112.        double[] blueSum = new double[numOfCluster];

  113.        for(int i=0; i<pointList.size(); i++)

  114.        {

  115.            int cIndex = (int)pointList.get(i).clusterIndex;

  116.            clusterCenterList.get(cIndex).numOfPoints++;

  117.            int tr = pointList.get(i).pixelColor.red;

  118.            int tg = pointList.get(i).pixelColor.green;

  119.            int tb = pointList.get(i).pixelColor.blue;

  120.            redSums[cIndex] += tr;

  121.            greenSum[cIndex] += tg;

  122.            blueSum[cIndex] += tb;

  123.        }

  124.        double[][] oldClusterCentersColors = new double[clusterCenterList.size()][3];

  125.        for(int i=0; i<clusterCenterList.size(); i++)

  126.        {

  127.            double sum  = clusterCenterList.get(i).numOfPoints;

  128.            int cIndex = clusterCenterList.get(i).cIndex;

  129.            int red = (int)(greenSum[cIndex]/sum);

  130.            int green = (int)(greenSum[cIndex]/sum);

  131.            int blue = (int)(blueSum[cIndex]/sum);

  132.            clusterCenterList.get(i).color = new Scalar(red, green, blue);

  133.            oldClusterCentersColors[i][0] = red;

  134.            oldClusterCentersColors[i][0] = green;

  135.            oldClusterCentersColors[i][0] = blue;

  136.        }

  137.        return oldClusterCentersColors;

  138.    }

  139.    /**

  140.     *

  141.     * @param clusterDisValues

  142.     * @return

  143.     */

  144.    private double getCloserCluster(double[] clusterDisValues)

  145.    {

  146.        double min = clusterDisValues[0];

  147.        int clusterIndex = 0;

  148.        for(int i=0; i<clusterDisValues.length; i++)

  149.        {

  150.            if(min > clusterDisValues[i])

  151.            {

  152.                min = clusterDisValues[i];

  153.                clusterIndex = i;

  154.            }

  155.        }

  156.        return clusterIndex;

  157.    }

  158.    /**

  159.     *

  160.     * @param p

  161.     * @param c

  162.     * @return distance value

  163.     */

  164.    private double calculateEuclideanDistance(ClusterPoint p, ClusterCenter c)

  165.    {

  166.        int pr = p.pixelColor.red;

  167.        int pg = p.pixelColor.green;

  168.        int pb = p.pixelColor.blue;

  169.        int cr = c.color.red;

  170.        int cg = c.color.green;

  171.        int cb = c.color.blue;

  172.        return Math.sqrt(Math.pow((pr - cr), 2.0) + Math.pow((pg - cg), 2.0) + Math.pow((pb - cb), 2.0));

  173.    }

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

640?wx_fmt=png

640?wx_fmt=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来实现。

cv4j(https://github.com/imageprocessor/cv4j)  是gloomyfish(http://blog.youkuaiyun.com/jia20003)和我一起开发的图像处理库,纯java实现,我们已经分离了一个Android版本和一个Java版本。

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

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


关注【Java与Android技术栈】

新增了关键词回复,赶紧来调戏本公众号吧~


更多精彩内容请关注扫码

640?wx_fmt=jpeg


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值