App基於手機殼顏色換膚?先嚐試一下用 KMeans 來提取圖像中的主色

酷酷的.jpg

背景

上週,某公司的產品經理提了一個需求:根據用戶手機殼顏色來改變 App 主題顏色。多是因爲這天馬行空的需求激怒了程序員,致使程序員和產品經理打了起來,最後雙雙被公司開除。java

那如何實現這個功能呢?首先須要獲取圖像中的主色。git

插一句題外話,做爲程序員在桌面上仍是要有一些必備的東西須要放的。程序員

程序員桌面必備杯墊.JPG

KMeans 算法

k-平均算法(英文:k-means clustering)源於信號處理中的一種向量量化方法,如今則更多地做爲一種聚類分析方法流行於數據挖掘領域。k-平均聚類的目的是:把 n 個點(能夠是樣本的一次觀察或一個實例)劃分到k個聚類中,使得每一個點都屬於離他最近的均值(此即聚類中心)對應的聚類,以之做爲聚類的標準。這個問題將歸結爲一個把數據空間劃分爲Voronoi cells的問題。github

KMeans 算法思想爲:給定n個數據點{x1,x2,…,xn},找到K個聚類中心{a1,a2,…,aK},使得每一個數據點與它最近的聚類中心的距離平方和最小,並將這個距離平方和稱爲目標函數,記爲Wn,其數學表達式爲:算法

KMeans.png

本文使用 KMeans 算法對圖像顏色作聚類。dom

算法基本流程: 一、初始的 K 個聚類中心。 二、按照距離聚類中心的遠近對全部樣本進行分類。 三、從新計算聚類中心,判斷是否退出條件: 兩次聚類中心的距離足夠小視爲知足退出條件; 不退出則從新回到步驟2。ide

算法實現

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

完整的算法實現能夠在:github.com/imageproces… 找到,它是一個典型的 KMeans 算法。優化

咱們的算法中,K默認值是5,固然也能夠本身指定。ui

以上算法目前在 demo 上耗時蠻久,不過能夠有優化空間。例如,可使用 RxJava 在 computation 線程中作複雜的計算操做而後切換回ui線程。亦或者可使用相似 Kotlin 的 Coroutines 來作複雜的計算操做而後切換回ui線程。

總結

提取圖像中的主色,還有其餘算法例如八叉樹等,在 Android 中也可使用 Palette 的 API來實現。

cv4j 是gloomyfish和我一塊兒開發的圖像處理庫,純java實現,咱們已經分離了一個Android版本和一個Java版本。

若是您想看該系列先前的文章能夠訪問下面的文集: www.jianshu.com/nb/10401400

最後提醒一句,做爲程序員,仍是要多健身。


Java與Android技術棧:每週更新推送原創技術文章,歡迎掃描下方的公衆號二維碼並關注,期待與您的共同成長和進步。

相關文章
相關標籤/搜索