安卓Palette原理分析

简单使用

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直方图

HSL / HSI 色域空间

其中H代表Hue 色调
S代表Saturation 饱和度
L / I 代表Itensity 强度

Web色

在该模型中,可以用相应的16制进制值00、33、66、99、CC和FF来表达三原色RGB)中的每一种。这种基本的Web调色板将作为所有的Web浏览器和平台的标准,它包括了这些16进制值的组合结果。这就意味着,我们潜在的输出结果包括6种红色调、6种绿色调、6种蓝色调。666的结果就给出了216种特定的颜色,这些颜色就可以安全的应用于所有的Web中,而不需要担心颜色在不同应用程序之间的变化。

Median cut 中位切割

中位切割算法(Median cut)是Paul Heckbert于1979年提出来的算法。概念上很简单,却也是最知名、应用最为广泛的减色算法(Color quantization)。
假如有任意一张图片,想要降低影像中的颜色数目到256色。

  1. 将图片内的所有像素加入到同一个区域

  2. 对于所有的区域做以下的事
    计算此区域内所有像素的RGB三元素最大值与最小值的差
    选出相差最大的那个颜色(R或G或B)
    根据那个颜色去排序此区域内所有像素
    分割前一半与后一半的像素到二个不同的区域(这里就是"中位切割"名字的由来)

  3. 重复第二步直到你有256个区域

  4. 将每个区域内的像素平均起来,于是就得到了256色

开始分析

Palette框架,使用了建造者模式,传入Bitmap或者ArrayList<Swatch>生成建造者。

建造者内部有两个重要变量mTargetsmFilters

mTargets存放要取的颜色类别(各种亮度,主副色调)

mFilters存放过滤器,可以过滤不符合规则的rgb/hsl

通过调用建造者的generate,最终生成Palette对象

Builder的generate

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

量化实现类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。

Palette的generate

    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突然变得有意思起来。寻踪溯源一下

Target追踪

首先,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. 首先,先把三维饱和度和三维亮度,每个维度设置为0 0.5 1

三维权重,下标1,代表的是最小值,下标2,代表的是目标值,下标3,代表的是最大值
2. 再设置三维权重,每个维度为 0.24 0.52 0.24

三维权重,下标1,代表的是饱和度,下标2,代表的是亮度,下标3,代表的是数量

差异表现在static函数。
static函数中只干了一件事: 即根据不同的Target,设置他们各种维度为初始值

回到Palette的generate函数中

在遍历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色域的使用有了新了想法。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

有头发的琦玉

打点钱,我会再努力的

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值