Palette.from(bmp).maximumColorCount(16).generate(new Palette.PaletteAsyncListener() {
@Override
public void onGenerated(@Nullable Palette palette) {
/*
getDominantSwatch: 获取点数(population)最多的Swatch
getVibrantSwatch(); 获取充满活力的色调
getDarkVibrantSwatch(); 获取充满活力的黑
getLightVibrantSwatch();获取充满活力的亮
getMutedSwatch(); 获取柔和的色调
getDarkMutedSwatch(); 获取柔和的黑
getLightMutedSwatch(); 获取柔和的亮
*/
}
});
首先补充一些观看本文需要的知识
swatch
样本
filter
过滤器
quantizer
量化
hist
直方图
其中H代表Hue 色调
S代表Saturation 饱和度
L / I 代表Itensity 强度
在该模型中,可以用相应的16制进制值00、33、66、99、CC和FF来表达三原色(RGB)中的每一种。这种基本的Web调色板将作为所有的Web浏览器和平台的标准,它包括了这些16进制值的组合结果。这就意味着,我们潜在的输出结果包括6种红色调、6种绿色调、6种蓝色调。666的结果就给出了216种特定的颜色,这些颜色就可以安全的应用于所有的Web中,而不需要担心颜色在不同应用程序之间的变化。
中位切割算法(Median cut)是Paul Heckbert于1979年提出来的算法。概念上很简单,却也是最知名、应用最为广泛的减色算法(Color quantization)。
假如有任意一张图片,想要降低影像中的颜色数目到256色。
将图片内的所有像素加入到同一个区域
对于所有的区域做以下的事
计算此区域内所有像素的RGB三元素最大值与最小值的差
选出相差最大的那个颜色(R或G或B)
根据那个颜色去排序此区域内所有像素
分割前一半与后一半的像素到二个不同的区域(这里就是"中位切割"名字的由来)
重复第二步直到你有256个区域
将每个区域内的像素平均起来,于是就得到了256色
Palette框架,使用了建造者模式,传入Bitmap或者ArrayList
建造者内部有两个重要变量mTargets
,mFilters
mTargets
存放要取的颜色类别(各种亮度,主副色调)
mFilters
存放过滤器,可以过滤不符合规则的rgb/hsl
通过调用建造者的generate,最终生成Palette对象
public Palette generate() {
List<Swatch> swatches;
// 首先根据判断mBitmap和mSwatches是否为空的结果
// 判断是传入的图片还是样本
if (mBitmap != null) {
// 对Bitmap降采样 默认112 * 112
final Bitmap bitmap = scaleBitmapDown(mBitmap);
// 采样的区域
final Rect region = mRegion;
// 量化所有颜色
final ColorCutQuantizer quantizer = new ColorCutQuantizer(
getPixelsFromBitmap(bitmap),
mMaxColors,//默认16哦
mFilters.isEmpty() ? null : mFilters.toArray(new Filter[mFilters.size()]));
swatches = quantizer.getQuantizedColors();
} else if (mSwatches != null) {
swatches = mSwatches;
} else {
throw new AssertionError();
}
final Palette p = new Palette(swatches, mTargets);
// 初始化
p.generate();
return p;
}
量化颜色时,传入了Bitmap的所有像素,Filter和划分的颜色数
首先看量化颜色类ColorCutQuantizer
private static final int QUANTIZE_WORD_WIDTH = 5;// 下文简称QWW
ColorCutQuantizer(final int[] pixels, final int maxColors, final Palette.Filter[] filters) {
mFilters = filters;
// 解释一下这个直方图数组的大小,RGB中,每个元素占8位,这样整张图片占用的空间巨大,所以谷歌采用了压缩方法,即抹去8位的后3位,使其每位占5字节
// 又因为,总共R,G,B三个元素,所以总颜色个数为2^5*3 即2*15
final int[] hist = mHistogram = new int[1 << (QUANTIZE_WORD_WIDTH * 3)];
for (int i = 0; i < pixels.length; i++) {
final int quantizedColor = quantizeFromRgb888(pixels[i]);
// 将原始数据变成临近颜色的数据
pixels[i] = quantizedColor;
// 更新直方图
hist[quantizedColor]++;
}
// 记录有区别的颜色数量
int distinctColorCount = 0;
for (int color = 0; color < hist.length; color++) {
// shouldIgnoreColor会将565颜色重新变为888颜色,再转为hsl,共同传给Filter
if (hist[color] > 0 && shouldIgnoreColor(color)) {
// 如果当前颜色被忽略,记得更新直方图
hist[color] = 0;
}
if (hist[color] > 0) {
// 更新一下不同色调的个数(翻译成色调感觉不太准确,不过更方便理解)
distinctColorCount++;
}
}
// 根据记录的有区别的颜色,构造一个数组
final int[] colors = mColors = new int[distinctColorCount];
int distinctColorIndex = 0;
for (int color = 0; color < hist.length; color++) {
if (hist[color] > 0) {
colors[distinctColorIndex++] = color;
}
}
//
至此,hist[i] 代表颜色i出现的个数
colors[i] 代表第i个有区别的颜色
hist的总大小,是2^15,即888颜色量化后的所有颜色个数
colors的总大小,仅仅是过滤后,感兴趣的颜色的个数
/
// 如果感兴趣的颜色少于 最多想要取到的颜色个数
if (distinctColorCount <= maxColors) {
mQuantizedColors = new ArrayList<>();
for (int color : colors) {
// 将颜色转换回去(这时已经损失原始颜色精度了),再把出现的个数记录下来
mQuantizedColors.add(new Palette.Swatch(approximateToRgb888(color), hist[color]));
}
} else {
// 再量化,通过取平均模板进行量化
mQuantizedColors = quantizePixels(maxColors);
}
}
quantizeFromRgb888函数的具体细节,就不多介绍了。简单来说就是类似将原始RGB变成WEB色。
quantizePixels采用的就是知识补充中提到的Median cut算法。
generate函数中,首先对图像进行了预处理,拿到了样本集合,再创建Palette对象,并调用了他的generate函数。
兜兜转转,终于完成了图像的预处理,进入了Palette。
private final Map<Target, Swatch> mSelectedSwatches;
void generate() {
// Google在这里玩了一手List遍历性能优化,代码风格和上文有些区别
// 看来Palette也是一个多人团队开发的框架。
for (int i = 0, count = mTargets.size(); i < count; i++) {
final Target target = mTargets.get(i);
target.normalizeWeights();
mSelectedSwatches.put(target, generateScoredTarget(target));
}
// 因为这里的mUsedColors只是记录Palette内部处理时使用的样本,所以在交给用户使用时,应该清空已节省内存
mUsedColors.clear();
}
mTargets突然变得有意思起来。寻踪溯源一下
首先,mTargets最开始是在Builder构造函数中被添加的,默认添加了6个Target对象
public Builder(@NonNull Bitmap bitmap) {
mTargets.add(Target.LIGHT_VIBRANT);
mTargets.add(Target.VIBRANT);
mTargets.add(Target.DARK_VIBRANT);
mTargets.add(Target.LIGHT_MUTED);
mTargets.add(Target.MUTED);
mTargets.add(Target.DARK_MUTED);
}
而Target.LIGHT_VIBRANT / VIBRANT等 ,是Target类的static对象
public final class Target{
// 只取两个当例子分析
public static final Target LIGHT_VIBRANT;
public static final Target VIBRANT;
...
static {
LIGHT_VIBRANT = new Target();
setDefaultLightLightnessValues(LIGHT_VIBRANT);
setDefaultVibrantSaturationValues(LIGHT_VIBRANT);
VIBRANT = new Target();
setDefaultNormalLightnessValues(VIBRANT);
setDefaultVibrantSaturationValues(VIBRANT);
...
}
Target() {
setTargetDefaultValues(mSaturationTargets);
setTargetDefaultValues(mLightnessTargets);
setDefaultWeights();
}
}
也就是说LIGHT_VIBRANT创建时,先setTargetDefaultValues,再setDefaultWeights,最后再调用setDefaultLightLightnessValues和setDefaultVibrantSaturationValues
static final int INDEX_MIN = 0;
static final int INDEX_TARGET = 1;
static final int INDEX_MAX = 2;
static final int INDEX_WEIGHT_SAT = 0;
static final int INDEX_WEIGHT_LUMA = 1;
static final int INDEX_WEIGHT_POP = 2;
private static void setTargetDefaultValues(final float[] values) {
values[INDEX_MIN] = 0f;
values[INDEX_TARGET] = 0.5f;
values[INDEX_MAX] = 1f;
}
private void setDefaultWeights() {
mWeights[INDEX_WEIGHT_SAT] = WEIGHT_SATURATION;
mWeights[INDEX_WEIGHT_LUMA] = WEIGHT_LUMA;
mWeights[INDEX_WEIGHT_POP] = WEIGHT_POPULATION;
}
private static void setDefaultLightLightnessValues(Target target) {
target.mLightnessTargets[INDEX_MIN] = MIN_LIGHT_LUMA;
target.mLightnessTargets[INDEX_TARGET] = TARGET_LIGHT_LUMA;
}
private static void setDefaultVibrantSaturationValues(Target target) {
target.mSaturationTargets[INDEX_MIN] = MIN_VIBRANT_SATURATION;
target.mSaturationTargets[INDEX_TARGET] = TARGET_VIBRANT_SATURATION;
}
不同Target既有共性,又有彼此的差异。
共性表现在构造函数。构造函数中,干了两件事:
三维权重,下标1,代表的是最小值,下标2,代表的是目标值,下标3,代表的是最大值
2. 再设置三维权重,每个维度为 0.24 0.52 0.24
三维权重,下标1,代表的是饱和度,下标2,代表的是亮度,下标3,代表的是数量
差异表现在static函数。
static函数中只干了一件事: 即根据不同的Target,设置他们各种维度为初始值
在遍历mTargets时,调用了Target的normalizeWeights函数。
void normalizeWeights() {
float sum = 0;
for (int i = 0, z = mWeights.length; i < z; i++) {
float weight = mWeights[i];
if (weight > 0) {
sum += weight;
}
}
if (sum != 0) {
for (int i = 0, z = mWeights.length; i < z; i++) {
if (mWeights[i] > 0) {
mWeights[i] /= sum;
}
}
}
}
normalizeWeights函数代码很简单,名字也很明显。实质上就是进行归一化处理,值均衡操作。
接下来调用了generateScoredTarget函数。分析来看,这个函数是关键,实现了根据Target取到Swatch
private Swatch generateScoredTarget(final Target target) {
final Swatch maxScoreSwatch = getMaxScoredSwatchForTarget(target);
if (maxScoreSwatch != null) {
mUsedColors.append(maxScoreSwatch.getRgb(), true);
}
return maxScoreSwatch;
}
private Swatch getMaxScoredSwatchForTarget(final Target target) {
float maxScore = 0;
Swatch maxScoreSwatch = null;
// 遍历所有的样本,拿到和当前Target最匹配的样本并返回
for (int i = 0, count = mSwatches.size(); i < count; i++) {
final Swatch swatch = mSwatches.get(i);
// shouldBeScoredForTarget是比较样本的hsl和Target的hsl,判断是否在范围内。同时,判断是否使用(mUsedColors),也在这个函数中实现
if (shouldBeScoredForTarget(swatch, target)) {
// generateScore是根据样本和Target的hsl差值,并乘以权重生成的
final float score = generateScore(swatch, target);
if (maxScoreSwatch == null || score > maxScore) {
maxScoreSwatch = swatch;
maxScore = score;
}
}
}
return maxScoreSwatch;
}
mUsedColors顾名思义,就是已经被 使用/取出 过的颜色
经过以上的处理,Palette内的mSelectedSwatches已经记录了不同Target对应的样本。
当我们通过函数getDominantSwatch等获取颜色时,内部实际上是一层封装。
public Swatch getDarkMutedSwatch() {
return getSwatchForTarget(Target.DARK_MUTED);
}
public Swatch getSwatchForTarget(final Target target) {
return mSelectedSwatches.get(target);
}
至此,Palette的分析之旅终于结束了。学习到了一些传统图像处理的方法。对hsl色域的使用有了新了想法。