简单实现 ZEEEN 的取色效果

ZEEEN 是 iOS 上一个非常漂亮的 Dribbble 第三方客户端,它的一个主要特性是,根据图片动态生成屏幕背景色,使得图片看起来仿佛与背景融合为一体:

ZEEEN

如果要在 Android 上实现类似的效果,一种可能的方案就是实用 Android support v7 中提供的 Palette ,但根据个人经验,Palette 的取色往往不太理想,有时甚至十分怪异,比较屎。

我更倾向于自己实现。算法可参考 StackOverflow 的这个链接,思路大致分为如下两步:

  1. 对给定图片进行四边取色,使用 YUV 对取到的颜色做归类计算,取归类后出现频率最高的颜色,作为背景色

  2. 对给定图片做全部取色,同样使用 YUV 对取到的颜色做归类计算,将归类后的颜色按照频率从高到低的排序,从中挑选出频率最高且满足撞色的第一个颜色,作为文字色

先上效果图:

Example

代码实现参考了 arcanis/colibrijs ;图片越大,计算所需的时间越长,可以使用 scale 参数适当缩小图片,加快处理速度(取色精度会有所下降);记住放在异步线程中使用:

import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.Matrix;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class Palette {
public static class Result {
private int mBackgroundColor;
private int mContentColor;

public Result(int backgroundColor, int contentColor) {
mBackgroundColor = backgroundColor;
mContentColor = contentColor;
}

public int getBackgroundColor() {
return mBackgroundColor;
}

public int getContentColor() {
return mContentColor;
}
}

public static Result extract(Bitmap bitmap, float scale) {
Matrix matrix = new Matrix();
matrix.postScale(scale, scale);
bitmap = Bitmap.createBitmap(bitmap, 0, 0,
bitmap.getWidth(), bitmap.getHeight(), matrix, false);
int width = bitmap.getWidth();
int height = bitmap.getHeight();

List<Integer> topBorderColors
= getColorsInRange(bitmap, 0, 0, width - 1, 1);
List<Integer> rightBorderColors
= getColorsInRange(bitmap, width - 1, 0, 1, height - 1);
List<Integer> leftBorderColors
= getColorsInRange(bitmap, 0, 1, 1, height - 1);
List<Integer> bottomBorderColors
= getColorsInRange(bitmap, 1, height - 1, width - 1, 1);
List<Integer> borderColors = new ArrayList<>();
borderColors.addAll(topBorderColors);
borderColors.addAll(rightBorderColors);
borderColors.addAll(leftBorderColors);
borderColors.addAll(bottomBorderColors);
int backgroundColor = getDominantColors(borderColors).get(0);

int contentColor = 0;
List<Integer> contentColors
= getColorsInRange(bitmap, 0, 0, width, height);
contentColors = getDominantColors(contentColors);
for (int color : contentColors) {
if (isDifferentColors(backgroundColor, color)) {
contentColor = color;
break;
}
}

if (contentColor == 0) {
if (isDifferentColors(backgroundColor, Color.WHITE)) {
return new Result(backgroundColor, Color.WHITE);
} else {
return new Result(backgroundColor, Color.BLACK);
}
}

return new Result(backgroundColor, contentColor);
}

private static List<Integer> getColorsInRange(Bitmap bitmap,
int x, int y, int width, int height) {
List<Integer> list = new ArrayList<>();

for (int i = x; i < x + width; i++) {
for (int j = y; j < y + height; j++) {
list.add(bitmap.getPixel(i, j));
}
}

return list;
}

private static List<Integer> getDominantColors(List<Integer> list) {
List<List<Integer>> buckets = getSimilarColors(list, 25.5F);
Collections.sort(buckets, new Comparator<List<Integer>>() {
@Override
public int compare(List<Integer> bucket1,
List<Integer> bucket2) {
return bucket2.size() - bucket1.size();
}
});

List<Integer> dominant = new ArrayList<>();
for (List<Integer> bucket : buckets) {
dominant.add(getMeanColor(bucket));
}

return dominant;
}

private static List<List<Integer>> getSimilarColors(
List<Integer> list, float compareDistance) {
List<List<Integer>> subsets = new ArrayList<>();

for (int color : list) {
List<Integer> closest;
int index;

for (index = 0; index < subsets.size(); index++) {
if (getColorsDistance(subsets.get(index).get(0), color)
< compareDistance) {
break;
}
}

if (index >= subsets.size()) {
closest = new ArrayList<>();
subsets.add(closest);
} else {
closest = subsets.get(index);
}

closest.add(color);
}

return subsets;
}

private static float getColorsDistance(int color1, int color2) {
float[] yue1 = rgb2Yuv(getColorRgb(color1));
float[] yue2 = rgb2Yuv(getColorRgb(color2));
return (float) Math.sqrt(Math.pow(yue1[0] - yue2[0], 2.0F)
+ Math.pow(yue1[1] - yue2[1], 2.0F)
+ Math.pow(yue1[2] - yue2[2], 2.0F));
}

private static float[] getColorRgb(int color) {
float[] rgb = new float[3];
rgb[0] = Color.red(color);
rgb[1] = Color.green(color);
rgb[2] = Color.blue(color);
return rgb;
}

private static float[] rgb2Yuv(float[] rgb) {
float[] yuv = new float[3];
yuv[0] = rgb[0] * 0.299F + rgb[1] * 0.587F + rgb[2] * 0.114F;
yuv[1] = rgb[0] * -0.169F + rgb[1] * -0.331F + rgb[2] * 0.500F
+ 128.0F;
yuv[2] = rgb[0] * 0.500F + rgb[1] * -0.419F + rgb[2] * -0.081F
+ 128.0F;
return yuv;
}

private static int getMeanColor(List<Integer> list) {
float[] mean = new float[3];

for (int color : list) {
float[] rgb = getColorRgb(color);
mean[0] += rgb[0];
mean[1] += rgb[1];
mean[2] += rgb[2];
}

mean[0] /= list.size();
mean[1] /= list.size();
mean[2] /= list.size();

return Color.rgb((int) mean[0], (int) mean[1], (int) mean[2]);
}

private static boolean isDifferentColors(int color1, int color2) {
float[] rgb1 = getColorRgb(color1);
float[] rgb2 = getColorRgb(color2);
float rgbDiff = Math.abs(rgb1[0] - rgb2[0])
+ Math.abs(rgb1[1] - rgb2[1])
+ Math.abs(rgb1[2] - rgb2[2]);
float brightnessDiff = Math.abs(getColorBrightness(color1)
- getColorBrightness(color2));
return rgbDiff >= 500.0F && brightnessDiff >= 125.0F;
}

private static float getColorBrightness(int color) {
float[] rgb = getColorRgb(color);
return rgb[0] * 0.299F + rgb[1] * 0.587F + rgb[2] * 0.114F;
}
}
返回