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

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

背景



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


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


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


image.png

程序员桌面必备杯垫.JPG


KMeans 算法



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


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


image.png

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 中使用该算法来提取主色:

image.png

demo1.png


image.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版本。

相关文章
|
19天前
|
小程序 JavaScript API
微信小程序开发之:保存图片到手机,使用uni-app 开发小程序;还有微信原生保存图片到手机
这篇文章介绍了如何在uni-app和微信小程序中实现将图片保存到用户手机相册的功能。
277 0
微信小程序开发之:保存图片到手机,使用uni-app 开发小程序;还有微信原生保存图片到手机
|
2月前
|
移动开发 Android开发 数据安全/隐私保护
移动应用与系统的技术演进:从开发到操作系统的全景解析随着智能手机和平板电脑的普及,移动应用(App)已成为人们日常生活中不可或缺的一部分。无论是社交、娱乐、购物还是办公,移动应用都扮演着重要的角色。而支撑这些应用运行的,正是功能强大且复杂的移动操作系统。本文将深入探讨移动应用的开发过程及其背后的操作系统机制,揭示这一领域的技术演进。
本文旨在提供关于移动应用与系统技术的全面概述,涵盖移动应用的开发生命周期、主要移动操作系统的特点以及它们之间的竞争关系。我们将探讨如何高效地开发移动应用,并分析iOS和Android两大主流操作系统的技术优势与局限。同时,本文还将讨论跨平台解决方案的兴起及其对移动开发领域的影响。通过这篇技术性文章,读者将获得对移动应用开发及操作系统深层理解的钥匙。
|
4月前
|
JavaScript Java 测试技术
基于SpringBoot+Vue+uniapp的多功能智能手机阅读APP的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue+uniapp的多功能智能手机阅读APP的详细设计和实现(源码+lw+部署文档+讲解等)
|
4月前
|
存储 移动开发 Android开发
使用kotlin Jetpack Compose框架开发安卓app, webview中h5如何访问手机存储上传文件
在Kotlin和Jetpack Compose中,集成WebView以支持HTML5页面访问手机存储及上传音频文件涉及关键步骤:1) 添加`READ_EXTERNAL_STORAGE`和`WRITE_EXTERNAL_STORAGE`权限,考虑Android 11的分区存储;2) 配置WebView允许JavaScript和文件访问,启用`javaScriptEnabled`、`allowFileAccess`等设置;3) HTML5页面使用`<input type="file">`让用户选择文件,利用File API;
|
4月前
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp的多功能智能手机阅读APP附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp的多功能智能手机阅读APP附带文章源码部署视频讲解等
70 1
|
4月前
|
机器学习/深度学习 人工智能 视频直播
AI直播手机APP震撼发布!3大场景直播,60秒一键开播!
🎉 青否数字人AI直播APP发布!🚀 在抖音等平台60秒一键开播,简化直播流程。💡 3种AI直播模式,融合6大AIGC技术,助力新手轻松直播带货且避免违规。💪 AI主播、声音克隆,实时话术改写,智能互动与讲品同步,提升转化。📊 实景与视频直播结合,适应多种场景。🌐 独立部署,自定义版权,1年免费升级,专业售后支持。🚀 (直播: zhibo175) #青否数字人 #AI直播
AI直播手机APP震撼发布!3大场景直播,60秒一键开播!
|
3月前
|
Java Android开发 UED
安卓scheme_url调端:如果手机上多个app都注册了 http或者https 的 intent。 调端的时候,调起哪个app呢?
当多个Android应用注册了相同的URL Scheme(如http或https)时,系统会在尝试打开这类链接时展示一个选择对话框,让用户挑选偏好应用。若用户选择“始终”使用某个应用,则后续相同链接将直接由该应用处理,无需再次选择。本文以App A与App B为例,展示了如何在`AndroidManifest.xml`中配置对http与https的支持,并提供了从其他应用发起调用的示例代码。此外,还讨论了如何在系统设置中管理这些默认应用选择,以及建议开发者为避免冲突应注册更独特的Scheme。
|
5月前
|
人工智能 搜索推荐 机器人
随着AI控制你的智能手机,App时代的结束可能已经指日可待
随着AI控制你的智能手机,App时代的结束可能已经指日可待
|
14天前
|
JSON 小程序 JavaScript
uni-app开发微信小程序的报错[渲染层错误]排查及解决
uni-app开发微信小程序的报错[渲染层错误]排查及解决
214 7
|
14天前
|
小程序 JavaScript 前端开发
uni-app开发微信小程序:四大解决方案,轻松应对主包与vendor.js过大打包难题
uni-app开发微信小程序:四大解决方案,轻松应对主包与vendor.js过大打包难题
284 1