Android开发日记(二十)—— Android 上的调色板 Palette

Android 上有一个比较短小精悍的库 —— Palette,整个库只有PaletteTargetColorCutQuantizer三个文件, 作用是从图像中提取突出的颜色提供UI使用。

如何使用

导入依赖包

1
implementation 'com.android.support:palette-v7:27.0.2'

简单使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//需要传入要提取的图片 bitmap
val builder = Palette.from(bitmap)
builder.generate(Palette.PaletteAsyncListener { palette ->
/**
* palette.getVibrantSwatch(); //获取到充满活力的色调
* palette.getDarkVibrantSwatch(); //获取充满活力的黑
* palette.getLightVibrantSwatch(); //获取充满活力的亮
* palette.getMutedSwatch(); //获取柔和的色调
* palette.getDarkMutedSwatch(); //获取柔和的黑
* palette.getLightMutedSwatch(); //获取柔和的亮
*/

//获取color值
val swatch: Palette.Swatch? = palette.lightVibrantSwatch
val color = swatch.rgb
}
})

如上代码所示,提取图片上的颜色需要传入 bitmap,Palette 默认给了6种提取色调的种类。

源码解析

Palette有两种初始化方法,也就是同步和异步方法,使用 Builder构造器时默认使用的是异步方法。每种方法下都可以提供调色板大小参数来设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 最好在线程中使用
// 默认调色板大小.
private static final int DEFAULT_CALCULATE_NUMBER_COLORS = 16;
Palette p = Palette.generate(bitmap);
//设置调色板大小numcolor
Palette p = Palette.generate(bitmap, numcolor);

// 内部使用AsyncTask
Palette.generateAsync(bitmap, new Palette.PaletteAsyncListener() {
@Override
public void onGenerated(Palette palette) {
// palette为生成的调色板
}
});
// 设置调色板大小
Palette.generateAsync(bitmap, numcolor, new Palette.PaletteAsyncListener() {
@Override
public void onGenerated(Palette palette) {
// palette为生成的调色板
}
})

当我们传入 Bitmap 后会首先对 Bitmap 做处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// First we'll scale down the bitmap if needed
final Bitmap bitmap = scaleBitmapDown(mBitmap);

/**
* Scale the bitmap down as needed.
*/

private Bitmap scaleBitmapDown(final Bitmap bitmap) {
double scaleRatio = -1;

if (mResizeArea > 0) {
final int bitmapArea = bitmap.getWidth() * bitmap.getHeight();
if (bitmapArea > mResizeArea) {
scaleRatio = Math.sqrt(mResizeArea / (double) bitmapArea);
}
} else if (mResizeMaxDimension > 0) {
final int maxDimension = Math.max(bitmap.getWidth(), bitmap.getHeight());
if (maxDimension > mResizeMaxDimension) {
scaleRatio = mResizeMaxDimension / (double) maxDimension;
}
}

if (scaleRatio <= 0) {
// Scaling has been disabled or not needed so just return the Bitmap
return bitmap;
}

return Bitmap.createScaledBitmap(bitmap,
(int) Math.ceil(bitmap.getWidth() * scaleRatio),
(int) Math.ceil(bitmap.getHeight() * scaleRatio),
false);
}

这样做为了防止 Bitmap 太大而导致计算消耗大量资源,默认的ResizeArea大小是112*112。

处理完 Bitmap 后通过 getPixelsFromBitmap(bitmap) 方法获得图像上的像素数组,然后通过 ColorCutQuantizer 来处理图像上色值。
获取色值后,就可以将这些色值填入 Palette 中以便使用。

颜色量化算法

ColorCutQuantizer 是一个基于 中位切分法(Median cut) 的颜色量化器,主要作用就是从彩色图像中提取其中的主题颜色。

中位切分算法的原理很简单直接,将图像颜色看作是色彩空间中的长方体(VBox),从初始整个图像作为一个长方体开始,将RGB中最长的一边从颜色统计的中位数一切为二,使得到的两个长方体所包含的像素数量相同,重复上述步骤,直到最终切分得到长方体的数量等于主题颜色数量为止。
VBox.png

其中RGB最长的一边意思是,比如说在所有像素中Red颜色的分布范围是(10-50),Green的分布范围是(5-100),Blue的分布范围是(0-200)。那么此时就应该以Blue为基准,分成左右两堆,一堆的像素Blue值比中值小,另一堆像素Blue值比中值大。

但是有时候某些条件下VBOX里面的像素数量很少,比如实现过滤白色和黑色时候,这时候就不能以类似二分法来切割VBOX,需要使用优先级队列进行排序,刚开始时这一队列以VBox仅以VBox所包含的像素数作为优先级考量,当切分次数变多之后,将体积*包含像素数作为优先级。

除此之外,算法中最重要的部分是统计色彩分布直方图。我们需要将三维空间中的任意一点对应到一维坐标中的整数,这样才能以最快地速度定位这一颜色。如果采用全部的24位信息,那么我们用于保存直方图的数组长度至少要是224=16777216,既然是要提取颜色主题(或是颜色量化),我们可以将颜色由RGB各8位压缩至5位,这样数组长度只有215=32768:

量化压缩,举例:
24bit RGB888 -> 16bit RGB565 的转换

24bit RGB888
R7 R6 R5 R4 R3 R2 R1 R0 G7 G6 G5 G4 G3 G2 G1 G0 B7 B6 B5 B4 B3 B2 B1 B0

16bit RGB656
R7 R6 R5 R4 R3 G7 G6 G5 G4 G3 G2 B7 B6 B5 B4 B3

说明:在24bit上以8位为一组数据,在16bit上以5或者6为一组数据,
量化位数从8bit到5bit或6bit,取原8bit的高位,量化上做了压缩,却损失了精度。

量化补偿,举例:16bit RGB565 -> 24bit RGB888 的转换

16bit RGB656 R4 R3 R2 R1 R0 G5 G4 G3 G2 G1 G0 B4 B3 B2 B1 B0

24ibt RGB888 R4 R3 R2 R1 R0 0 0 0 G5 G4 G3 G2 G1 G0 0 0 B4 B3 B2 B1 B0 0 0 0

24ibt RGB888 R4 R3 R2 R1 R0 R2 R1 R0 G5 G4 G3 G2 G1 G0 G1 G0 B4 B3 B2 B1 B0 B2 B1 B0

说明:第二行的 24bit RGB888 数据为转换后,未进行补偿的数据,在精度上会有损失
第三行的 24bit RGB888 数据为经过量化补偿的数据,对低位做了量化补偿


参考资料:
wiki Median_cut
RGB565 与 RGB888的相互转换