Palette使用简介与实现原理
in Android with 0 comment
Palette使用简介与实现原理
in Android with 0 comment

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);

    柔和三种,生动三种。但并不一定每种都采集到了合适的颜色。

实现原理

使用方法讲完, 接下来是分析源码。首先,我们需要了解几个概念:

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量化颜色的步骤如下:

  1. 创建一个颜色直方图来统计图片中的颜色,如下图:
    android-palette-quantizer.webp
    图中横坐标代表的颜色为RGB555,rgb分量分别只有5bit,所以颜色总数为2的15次方,所以源码中的mHistogram就是代表的这个直方图的数组,数组的索引其实也代表的就是对应的颜色。将RGB888或ARGB8888转换为RGB555的目的是简化了需要统计的颜色数量,提高效率,但损失的精度对结果影响很小。
  2. 将图片中每个像素的颜色转换为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;
     }
  3. 根据Palette中默认的filter或者通过addFilter()添加的filter过滤掉不需要统计的颜色。
  4. 只保留统计值>0的颜色,用新的直方图保存。
  5. 通过中位切分法进一步量化颜色直至颜色种数小于maxColors。如果图片颜色数量本身就少于maxColors,直接返回这些颜色对应的样本就行。
  6. 通过得到的样本和配置的配色方案,得到各种配色方案下的颜色。

中位切分法

下面先了解下中位切分算法,作为一种量化颜色的算法,对彩色空间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<Palette.Swatch> 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<Vbox> 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<Vbox> 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<Palette.Swatch> generateAverageColors(Collection<Vbox> vboxes) {
        ArrayList<Palette.Swatch> 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> VBOX_COMPARATOR_VOLUME = new Comparator<Vbox>() {
    @Override
    public int compare(Vbox lhs, Vbox rhs) {
        return rhs.getVolume() - lhs.getVolume();
    }
};

源码中,mColors中的每个颜色值和其对应的mHistogram中的数据组合起来都代表着一个概念上的Vbox,只是代码中并没有全部创建Vbox实例。然后根据每次都从队列中取出最大Vbox,进行切分,切完再把切下来的Vbox放进队列中,直到Vbox数量等于maxColors。
Palette的解析差不多就讲到这里。

Responses