导语
在上一篇,我介绍了Palette 的基本信息和使用,这一篇帖子里,我主要目的是分析他的原理,以及发掘他更多的功能。
在使用中可以发现,Palette 可以分析出图片中使用了什么颜色,并且给出几种不同的模式用于使用,但是有时候发现并不准确,甚至根本不知道为什么会给出这样的颜色
问题
1.Palette 是如何分析出图片颜色
2.Palette 是如何给出各种颜色模式的,是根据什么规则去返回的
3.Palette 为什么会返回空的Swatch ,什么情况下会返回
4.看看Palette 还有什么玩法(功能)
基础知识 了解的可以跳过这段
–RGB颜色,我们Android 中使用的颜色值#FFFFFF 实际上就是RGB 值,拆分每两位十六进制分别是红 绿 蓝三原色
–HSL颜色,Palette 重点使用的概念,一种颜色的表示方式,由三个值组成,百度:HSL说法是一种将RGB色彩模型中的点在圆柱坐标系中的表示法,具体HSL 和RGB 色系如何转换,这里不去解释,Android 的ColorUtils 中有工具进行转换
------H 色相 可以理解为颜色 Android中使用 0~360之间的浮点值表示
------S 饱和度 可以理解为颜色的纯度 Android 使用0~1 的浮点表示
------L 亮度 Android 中使用0~1 的浮点表示
解析代码–跟着代码往下走
- 我们忽略异步的方式。只看同步的解析代码,有必要自己可以去看一下源码,这里我精简了一些log打印,并且加入一些注释
public Palette generate() {
List<Swatch> swatches;
if (mBitmap != null) {
// We have a Bitmap so we need to use quantization to reduce the number of colors
//这里将图片尺寸进行压缩
final Bitmap bitmap = scaleBitmapDown(mBitmap);
//mRegion 是一个设置值,用户可以指定分析颜色的区域
final Rect region = mRegion;
if (bitmap != mBitmap && region != null) {
// If we have a scaled bitmap and a selected region, we need to scale down the
// region to match the new scale
final double scale = bitmap.getWidth() / (double) mBitmap.getWidth();
region.left = (int) Math.floor(region.left * scale);
region.top = (int) Math.floor(region.top * scale);
region.right = Math.min((int) Math.ceil(region.right * scale),
bitmap.getWidth());
region.bottom = Math.min((int) Math.ceil(region.bottom * scale),
bitmap.getHeight());
}
//在这里进行代图片的解析,将图片拆成像素点并且统计颜色
final ColorCutQuantizer quantizer = new ColorCutQuantizer(
getPixelsFromBitmap(bitmap),
mMaxColors,
mFilters.isEmpty() ? null : mFilters.toArray(new Filter[mFilters.size()]));
// If created a new bitmap, recycle it
if (bitmap != mBitmap) {
bitmap.recycle();
}
// 获取到统计完成以后的队列
swatches = quantizer.getQuantizedColors();
} else if (mSwatches != null) {
// Else we're using the provided swatches
swatches = mSwatches;
} else {
// The constructors enforce either a bitmap or swatches are present.
throw new AssertionError();
}
final Palette p = new Palette(swatches, mTargets);
// 在这里面创建所有模式下对应的颜色值
p.generate()
return p;
}
这里的逻辑大体就是,压缩>裁剪>颜色统计>生成推荐
- 压缩方法 scaleBitmapDown ,这里的压缩其实用到的就是Bitmap 中的图片压缩
这里可以看到两个额外的设置参数去限定了压缩的 比例 mResizeArea 或最大像素值mResizeMaxDimension 这两个值只会生效一个,优先ResizeArea,最终依然是使用BitMap 的createScaledBitmap 方法进行压缩
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);
}
- 计算出需要提取颜色的区域, 这里可以发现一个额外设置mRegion
final Rect region = mRegion;
if (bitmap != mBitmap && region != null) {
// If we have a scaled bitmap and a selected region, we need to scale down the
// region to match the new scale
final double scale = bitmap.getWidth() / (double) mBitmap.getWidth();
region.left = (int) Math.floor(region.left * scale);
region.top = (int) Math.floor(region.top * scale);
region.right = Math.min((int) Math.ceil(region.right * scale),
bitmap.getWidth());
region.bottom = Math.min((int) Math.ceil(region.bottom * scale),
bitmap.getHeight());
}
- getPixelsFromBitmap 获取像素队列,如果这样上一步有设置区域,则会在区域内提取
private int[] getPixelsFromBitmap(Bitmap bitmap) {
final int bitmapWidth = bitmap.getWidth();
final int bitmapHeight = bitmap.getHeight();
final int[] pixels = new int[bitmapWidth * bitmapHeight];
bitmap.getPixels(pixels, 0, bitmapWidth, 0, 0, bitmapWidth, bitmapHeight);
if (mRegion == null) {
// If we don't have a region, return all of the pixels
return pixels;
} else {
// If we do have a region, lets create a subset array containing only the region's
// pixels
final int regionWidth = mRegion.width();
final int regionHeight = mRegion.height();
// pixels contains all of the pixels, so we need to iterate through each row and
// copy the regions pixels into a new smaller array
final int[] subsetPixels = new int[regionWidth * regionHeight];
for (int row = 0; row < regionHeight; row++) {
System.arraycopy(pixels, ((row + mRegion.top) * bitmapWidth) + mRegion.left,
subsetPixels, row * regionWidth, regionWidth);
}
return subsetPixels;
}
}
- 创建ColorCutQuantizer 对象对像素进行筛选整理
我们能看到ColorCutQuantizer 的构造函数是这样的,同样删除了一些log 和加入注释
ColorCutQuantizer(final int[] pixels, final int maxColors, final Palette.Filter[] filters) {
mFilters = filters;
//这里遍历了一遍产生两个数组,hist 存放的是每一个颜色值出现的个数(position 代表颜色RGB,值代表个数)
//并且在这里会检查一遍颜色是否是RGB888 如果不是则转换
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]);
// Now update the pixel value to the quantized value
pixels[i] = quantizedColor;
// And update the histogram
hist[quantizedColor]++;
}
//计算出有多少种颜色,同时在shouldIgnoreColor 中通过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++;
}
}
// 将所有颜色组成一个数组
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;
}
}
//创建Swatch 对象的队列,这个队列保存了颜色值和他对应出现个数
if (distinctColorCount <= maxColors) {
// The image has fewer colors than the maximum requested, so just return the colors
mQuantizedColors = new ArrayList<>();
for (int color : colors) {
mQuantizedColors.add(new Palette.Swatch(approximateToRgb888(color), hist[color]));
}
} else {
// We need use quantization to reduce the number of colors
mQuantizedColors = quantizePixels(maxColors);
}
}
- 现在跳回Palette 对象中的generate方法,我们看到ColorCutQuantizer 最终的作用就是生成了
mQuantizedColors
final Palette p = new Palette(swatches, mTargets);
p.generate();
在这里生成了Palette 对象并且调用generate 然后返回出去。
值得注意的是,Palette 的实例化方法里
Palette(List<Swatch> swatches, List<Target> targets) {
mSwatches = swatches;
mTargets = targets;
mUsedColors = new SparseBooleanArray();
mSelectedSwatches = new ArrayMap<>();
mDominantSwatch = findDominantSwatch();
}
我们看到,除了赋值以外,有个方法findDominantSwatch 遍历获取到颜色最多的一个Swatch 值
- Target 对象
以上我一直存在但是没有解释的一个对象Target ,这个对象其实是整个Palette 解析像素的规则的一个方法
而Target 是在使用时,Palette.from() 的时候就已经创建好了
public Builder(@NonNull Bitmap bitmap) {
if (bitmap == null || bitmap.isRecycled()) {
throw new IllegalArgumentException("Bitmap is not valid");
}
mFilters.add(DEFAULT_FILTER);
mBitmap = bitmap;
mSwatches = null;
// Add the default targets
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);
}
同时创建了6个可供选择的模式,并且在这里可以注意到,他也创建了一个默认的筛选器,这个筛选器里面,过滤了一些接近白色和黑色的值,并且过滤了一个红色区域的值(这里我没有颜色的基础,所以不知道为什么Palette 要这么做)
一个Target 对象中,关键的属性是这些
final float[] mSaturationTargets = new float[3];
final float[] mLightnessTargets = new float[3];
final float[] mWeights = new float[3];
boolean mIsExclusive = true; // default to true
查看代码,可以发现 mSaturationTargets 和 mLightnessTargets 由三等级组成 分别是MIN,TARGER,MAX
mWeights 比较特殊,三个值分别代表着S 的权重,L 的权重 ,和整体浮动的权重
- Palette 的generate 方法
void generate() {
// We need to make sure that the scored targets are generated first. This is so that
// inherited targets have something to inherit from
for (int i = 0, count = mTargets.size(); i < count; i++) {
final Target target = mTargets.get(i);
target.normalizeWeights();
mSelectedSwatches.put(target, generateScoredTarget(target));
}
// We now clear out the used colors
mUsedColors.clear();
}
能看到两个方法 先是normalizeWeights() 方法,这个方法中,将权重进行重新计算,最大的一个设为1,其他按照同比例放大或缩小
另外一个就是generateScoredTarget了 这个方法是用于计算出最匹配target 规则的Swatch,具体target 的计算规则相对复杂,我下一个帖子再进行深入分析
本篇,我们需要了解到,Palette 在这里将所有的target 计算完成以后,生成了一个map 存在mSelectedSwatches 中,在我们使用的时候通过target 找到相应的Value值,并且返回使用
问题解答
-
Palette 是如何分析出图片颜色
答:通过将图片拆分像素点,并且转为HSL 颜色系进行匹配分类和统计得出相应的颜色 -
Palette 是如何给出各种颜色模式的,是根据什么规则去返回的
答:未能找出结果,要继续下一个帖子分析 -
Palette 为什么会返回空的Swatch ,什么情况下会返回
答:未能找出结果,但是根据以上的代码 必然是generateScoredTarget 方法没有匹配到相应的结果 -
看看Palette 还有什么玩法(功能)
在使用时可以再from() 后面调用以下方法有以下方法给你调用
resizeBitmapSize() 设置图片最大的像素值,如果你的图片特别大,可以用这个降低分析图片的消耗
resizeBitmapArea() 设置图片尺寸比例,如果和上一个方法同时使用,则最后一个生效
clearFilters() 删除所有的筛选器,上面说到有一个默认的筛选器会将黑白 红(不确定)颜色去除,如果你想不被去除,则可以使用这个方法
addFilter() 添加一个筛选器,这个筛选器可以自定义,如果添加了多个,则多个一起协助生效
setRegion() 上面说到的mRegion 就是通过这个方法进行设置,可以设置需要匹配的区域,如果设置的区域超过图片的大小,这里会抛出异常
clearRegion() 删除已经设置的区域,但是默认就是没有的,所以没什么用
addTarget() 添加一个自定义的Target
cleanTarget() 删除所有的Target ,因为有默认的预设值,所以这个方法也可以清除掉