Palette顾名思义就是一个调色板,它根据传入的bitmap,提取出几种主体颜色,这些颜色如果设置到背景或者字体上,就能使界面色彩元素丰富的同时也不失协调。
使用简介
它是一个建造者模式的实例化模式,用法很简单,总的说来分三步:
1.通过builder添加参数:
Palette p = Palette.from(BitMap)//将图片传入调色板中
.addFilter(Filter)//添加一个过滤器,根据HSL过滤(可以不设置,有默认值)
.maximumColorCount(20)//设置一个颜色种数的临界值,有效的颜色块数量小于这个值,直接用这些颜色作为最终统计到的样本。有效的颜色块后面解释(可以不设置,有默认值)
.resizeBitmapSize(480)//bitmap放入调色板时,会先压缩,提高计算速率,这里就是设置压缩后的尺寸。(可以不设置,有默认值)
.clearFilters()//清除颜色过滤器(可以不调用)
.generate();//第二步,创建Palette实例。
2.构建Palette实例。产生实例的方法generate有无参和有参数的两个重载方法。
- 无参的直接返回Palette,是同步方法。
- 有参的返回的是一个异步任务,通过传入的参数回调。因为统计图片颜色是比较耗时的,颜色种类越多,耗时越长。
3.通过Palette获取颜色。Palette取得的颜色并不是一种,有可能有多个样本。它提供了6种风格的颜色:
p.getDarkMutedColor(defaultColor);
p.getDarkVibrantColor(defaultColor);
p.getLightMutedColor(defaultColor);
p.getLightVibrantColor(defaultColor);
p.getMutedColor(defaultColor);
p.getVibrantColor(defaultColor);~~~~
柔和三种,生动三种。但并不一定每种都采集到了合适的颜色。
实现原理
使用方法讲完, 接下来是分析源码。首先,我们需要了解几个概念:
- RGB彩色空间:把颜色抽象到空间模型中,“Color Space”,彩色空间,又称作色域。RGB彩色空间是Color Space的一种,将R、G、B三色作为xyz轴,其颜色模型如下图。在这个彩色空间里可以表示所有RGB的颜色,当然,这并不代表自然界的所有颜色。另外,RGB颜色也根据精度不同,有RGB888、ARGB8888、RGB565、RGB555等这些格式。
- HSL:工业界的一种颜色标准,是通过对色相(H)、饱和度(S)、明度(L)三个颜色通道的变化以及它们相互之间的叠加来得到各式各样的颜色的,它和RGB是可以互相转换的。这里就不提供转化算法了。
- VBox:java对象,代表一个RGB彩色空间,或者说是子彩色空间,下图中的每个方块都可以是一个Vbox。Vbox的三边就是颜色的red、blue、green三个分量,所以其体积代表的含义就是该彩色空间的色域大小。
Palette的取色原理:通过builder构建palette的过程就是量化统计颜色的过程,这个过程是发生在builder的generate()方法里,首先压缩图片至指定(resizeBitmapSize(int size))或者默认大小,从而减少计算量,然后构建一个ColorCutQuantizer用于颜色采集,采集满足要求颜色的逻辑就是它来实现的。接下来把采集到的样本交给Palette里的Generator,通过Generator筛选出Muted、vibrant等6种风格颜色。
在这个过程中,最重要的当属ColorCutQuantizer的采集过程。这里用到了Median-cut(中位切分法算法)。
final class ColorCutQuantizer {
ColorCutQuantizer(final int[] pixels, final int maxColors, final Palette.Filter[] filters) {
mTimingLogger = LOG_TIMINGS ? new TimingLogger(LOG_TAG, "Creation") : null;
mFilters = filters;
//创建颜色直方图的数组,即横坐标。
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]++;
}
if (LOG_TIMINGS) {
mTimingLogger.addSplit("Histogram created");
}
// 根据filters定义的规则过滤掉不需要统计的颜色
int distinctColorCount = 0;
for (int color = 0; color < hist.length; color++) {
if (hist[color] > 0 && shouldIgnoreColor(color)) {
// If we should ignore the color, set the population to 0
hist[color] = 0;
}
if (hist[color] > 0) {
// If the color has population, increase the distinct color count
distinctColorCount++;
}
}
if (LOG_TIMINGS) {
mTimingLogger.addSplit("Filtered colors and distinct colors counted");
}
//只保留直方图中统计值大于0的颜色值
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;
}
}
if (LOG_TIMINGS) {
mTimingLogger.addSplit("Distinct colors copied into array");
}
if (distinctColorCount <= maxColors) {
// 图片颜色较少时,直接将这些颜色作为样本返回。
mQuantizedColors = new ArrayList<>();
for (int color : colors) {
mQuantizedColors.add(new Palette.Swatch(approximateToRgb888(color), hist[color]));
}
if (LOG_TIMINGS) {
mTimingLogger.addSplit("Too few colors present. Copied to Swatches");
mTimingLogger.dumpToLog();
}
} else {
// 如果图片有效颜色多余设定的颜色提取最大值,需要通过中位切分算法提取主要颜色
mQuantizedColors = quantizePixels(maxColors);
if (LOG_TIMINGS) {
mTimingLogger.addSplit("Quantized colors computed");
mTimingLogger.dumpToLog();
}
}
}
}
ColorCutQuantizer量化颜色的步骤如下:
- 创建一个颜色直方图来统计图片中的颜色,如下图:
图中横坐标代表的颜色为RGB555,rgb分量分别只有5bit,所以颜色总数为2的15次方,所以源码中的mHistogram就是代表的这个直方图的数组,数组的索引其实也代表的就是对应的颜色。将RGB888或ARGB8888转换为RGB555的目的是简化了需要统计的颜色数量,提高效率,但损失的精度对结果影响很小。 -
将图片中每个像素的颜色转换为RGB555的颜色值,并统计到对应直方图中。量化位数从8bit到5bit,取原8bit的高位,量化上做了压缩,当然损失了精度。
private static int quantizeFromRgb888(int color) { int r = modifyWordWidth(Color.red(color), 8, QUANTIZE_WORD_WIDTH); int g = modifyWordWidth(Color.green(color), 8, QUANTIZE_WORD_WIDTH); int b = modifyWordWidth(Color.blue(color), 8, QUANTIZE_WORD_WIDTH); return r << (QUANTIZE_WORD_WIDTH + QUANTIZE_WORD_WIDTH) | g << QUANTIZE_WORD_WIDTH | b; }
- 根据Palette中默认的filter或者通过addFilter()添加的filter过滤掉不需要统计的颜色。
- 只保留统计值>0的颜色,用新的直方图保存。
- 通过中位切分法进一步量化颜色直至颜色种数小于maxColors。如果图片颜色数量本身就少于maxColors,直接返回这些颜色对应的样本就行。
- 通过得到的样本和配置的配色方案,得到各种配色方案下的颜色。
中位切分法
下面先了解下中位切分算法,作为一种量化颜色的算法,对彩色空间Vbox进行切分。要知道切分原则前,我们先举例了解下Vbox,假设当前定义了4个彩色空间Vbox0、Vbox1、Vbox2、Vbox3,Vbox0为Vbox1、Vbox2、Vbox3的并集,且Vbox1、Vbox2、Vbox3互斥,且他们都有自己的颜色数量P。那么选Vbox0较长边作为方向,将Vbox1、Vbox2、Vbox3进行该方向上进行排序,并按顺序累加他们的P,当累加值大于等于Vbox0的P时,此时的索引位置就为切分点,就此进行切分。把Vbox0切成新的两个Vbox01、Vbox02,Vbox1、Vbox2、Vbox3分别包含于Vbox01、Vbox02之中,那么,这是就选Vbox01、Vbox02中p值较大的,继续上面的才分过程。这就是中位切分的过程。那么看看源码:
final class ColorCutQuantizer {
...
private List quantizePixels(int maxColors) {
// Create the priority queue which is sorted by volume descending. This means we always
// split the largest box in the queue
final PriorityQueue pq = new PriorityQueue<>(maxColors, VBOX_COMPARATOR_VOLUME);
// To start, offer a box which contains all of the colors
pq.offer(new Vbox(0, mColors.length - 1));
// Now go through the boxes, splitting them until we have reached maxColors or there are no
// more boxes to split
splitBoxes(pq, maxColors);
// Finally, return the average colors of the color boxes
return generateAverageColors(pq);
}
/**
* Iterate through the {@link java.util.Queue}, popping
* {@link ColorCutQuantizer.Vbox} objects from the queue
* and splitting them. Once split, the new box and the remaining box are offered back to the
* queue.
*
* @param queue {@link java.util.PriorityQueue} to poll for boxes
* @param maxSize Maximum amount of boxes to split
*/
@SuppressWarnings("NullAway") // mTimingLogger initialization and access guarded by LOG_TIMINGS.
private void splitBoxes(final PriorityQueue queue, final int maxSize) {
while (queue.size() < maxSize) {
final Vbox vbox = queue.poll();
if (vbox != null && vbox.canSplit()) {
// First split the box, and offer the result
queue.offer(vbox.splitBox());
if (LOG_TIMINGS) {
mTimingLogger.addSplit("Box split");
}
// Then offer the box back
queue.offer(vbox);
} else {
if (LOG_TIMINGS) {
mTimingLogger.addSplit("All boxes split");
}
// If we get here then there are no more boxes to split, so return
return;
}
}
}
private List generateAverageColors(Collection vboxes) {
ArrayList colors = new ArrayList<>(vboxes.size());
for (Vbox vbox : vboxes) {
Palette.Swatch swatch = vbox.getAverageColor();
if (!shouldIgnoreColor(swatch)) {
// As we're averaging a color box, we can still get colors which we do not want, so
// we check again here
colors.add(swatch);
}
}
return colors;
}
...
}
先构造一个PriorityQueue优先队列,并设定根据体积大小进行排序的规则,如下:
private static final Comparator VBOX_COMPARATOR_VOLUME = new Comparator() {
@Override
public int compare(Vbox lhs, Vbox rhs) {
return rhs.getVolume() - lhs.getVolume();
}
};
源码中,mColors中的每个颜色值和其对应的mHistogram中的数据组合起来都代表着一个概念上的Vbox,只是代码中并没有全部创建Vbox实例。然后根据每次都从队列中取出最大Vbox,进行切分,切完再把切下来的Vbox放进队列中,直到Vbox数量等于maxColors。
Palette的解析差不多就讲到这里。