Android Palette吸色原理及源码解析

一、Android Palette原理:

    需求来自于设计想搞一个吸色的背景,就想到了Palette。
    1、RGB和HSL

    一般的3D编程只需要使用RGB颜色空间就好了,但其实美术人员更多的是使用HSV(HSL),因为可以方便的调整饱和度和亮度。HSL 和 HSV(也叫做 HSB)是对RGB 色彩空间中点的两种有关系的表示,它们尝试描述比 RGB 更准确的感知颜色联系,并仍保持在计算上简单。HSL 表示 hue(色相)、saturation(饱和度)、lightness(亮度),HSV 表示 hue、saturation、value(色调) 而 HSB 表示 hue、saturation、brightness(明度)。HSL 和 HSV 二者都把颜色描述在圆柱体内的点,这个圆柱的中心轴取值为自底部的黑色到顶部的白色而在它们中间是的灰色,绕这个轴的角度对应于“色相”,到这个轴的距离对应于“饱和度”,而沿着这个轴的距离对应于“亮度”,“色调”或“明度”。

    对概念的了解具体可以移步:https://blog.csdn.net/jiangxinyu/article/details/8000999

    2、PS:
    Android中是自带ColorUtils类的,这个类可以提供方法让HSL格式的色值和RGB色值进行相互转化的。其内部有具体的计算公式。

二、工程引入:

    1、gradle
compile 'com.android.support:palette-v7:25.1.1'
    2、PS:

   这里有个问题要请教下大家,在使用的时候发现gradle在下载jar包后,如果缓存下已经找得到25版本的,此时再改为23版本的jar包,好像并不会重新下载,不太明白是gradle导致的,还是本地设置导致的。

三、使用:

Palette.from(bitmap).generate(new Palette.PaletteAsyncListener() {
            @Override
            public void onGenerated(Palette palette) {
                Palette.Swatch vibrant = palette.getVibrantSwatch();
                Palette.Swatch light = palette.getLightVibrantSwatch();
                Palette.Swatch dark = palette.getDarkVibrantSwatch();
                Palette.Swatch darkMute = palette.getDarkMutedSwatch();
                Palette.Swatch domin = palette.getDominantSwatch();
                setStatusBar(domin.toString(),
                        domin.getRgb());
            }
        });

    在使用的时候有几个点需要注意下

    1、Palette的使用必须有源bitmap

    2、旧版的Palette提供同步解析图片,但是在25版本时已经变为异步解析图片了。需要new一个Listener

    3、在提供的获取样本的方法中,主要分为3类,dominate、vibrant、mute三种。其中vibrant和mute又包含light-dark样本。所以总共的方法为7种。这里需要注意的是,除了dominate样本,其余6种都是可以为空的。

    4、获取dominate样本的方法在23中没有,25中有。

    5、性能:jar包大小23kb,图片解析速度在魅族Pro6上不到30ms。

四、源码解析

    源码太多,这里就不一一列举了,主要看下大致过程以及关键代码吧。

public Palette generate() {
            final TimingLogger logger = LOG_TIMINGS
                    ? new TimingLogger(LOG_TAG, "Generation")
                    : null;

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

                if (logger != null) {
                    logger.addSplit("Processed Bitmap");
                }

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

                if (logger != null) {
                    logger.addSplit("Color quantization completed");
                }
            } else {
                // Else we're using the provided swatches
                swatches = mSwatches;
            }

            // Now create a Palette instance
            final Palette p = new Palette(swatches, mTargets);
            // And make it generate itself
            p.generate();

            if (logger != null) {
                logger.addSplit("Created Palette");
                logger.dumpToLog();
            }

            return p;
        }

    1、图片裁剪    
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);
        }
    2、提取色值、构建直方图

    ColorCutQuantizer这个类是具体执行操作的。ColorCutQuantizer是基于中位切分法(Median cut) 的颜色量化器,主要作用就是从彩色图像中提取其中的主题颜色。中位切分算法的原理很简单直接,将图像颜色看作是色彩空间中的长方体(VBox),从初始整个图像作为一个长方体开始,将RGB中最长的一边从颜色统计的中位数一切为二,使得到的两个长方体所包含的像素数量相同,重复上述步骤,直到最终切分得到长方体的数量等于主题颜色数量为止。

    Android Palette吸色原理及源码解析_第1张图片

    其中RGB最长的一边意思是,比如说在所有像素中Red颜色的分布范围是(10-50),Green的分布范围是(5-100),Blue的分布范围是(0-200)。那么此时就应该以Blue为基准,分成左右两堆,一堆的像素Blue值比中值小,另一堆像素Blue值比中值大。
    但是有时候某些条件下VBOX里面的像素数量很少,比如实现过滤白色和黑色时候,这时候就不能以类似二分法来切割VBOX,需要使用优先级队列进行排序,刚开始时这一队列以VBox仅以VBox所包含的像素数作为优先级考量,当切分次数变多之后,将体积*包含像素数作为优先级。

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]);
            // Now update the pixel value to the quantized value
            pixels[i] = quantizedColor;
            // And update the histogram
            hist[quantizedColor]++;
        }

        if (LOG_TIMINGS) {
            mTimingLogger.addSplit("Histogram created");
        }

        // Now let's count the number of distinct colors
        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");
        }

        // Now lets go through create an array consisting of only distinct colors
        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) {
            // The image has fewer colors than the maximum requested, so just return the colors
            mQuantizedColors = new ArrayList<>();
            for (int color : colors) {
                mQuantizedColors.add(new Swatch(approximateToRgb888(color), hist[color]));
            }

            if (LOG_TIMINGS) {
                mTimingLogger.addSplit("Too few colors present. Copied to Swatches");
                mTimingLogger.dumpToLog();
            }
        } else {
            // We need use quantization to reduce the number of colors
            mQuantizedColors = quantizePixels(maxColors);

            if (LOG_TIMINGS) {
                mTimingLogger.addSplit("Quantized colors computed");
                mTimingLogger.dumpToLog();
            }
        }
    }

    从代码中可以看出,Palette构建一个一维数组,每一位代表一种色值,每一位的值代表该色值在图片中出现的次数。

    然后将压缩后的图片转为像素,从像素中提取不同的色值。并存放在一维数组内。

    在解析图片完成后,会优化该数组,提出从没出现过的色值。

    将清洗后的数组转存        

    3、循环拆分获取色值

    这里用到了一个ColorCutQuantizer内部类VBox,对VBox中的颜色进行,切分后得到的大概是这样的

Android Palette吸色原理及源码解析_第2张图片

    图片取自 https://blog.csdn.net/shanglianlm/article/details/50051269

    4、根据设定好的样本进行取样

    target样本会去已经计算好的VBox中匹配,如果匹配不到就为null。最初提到的7中方式,除了dominate以外,6种样本都是Target的实例,各自内部都有一套筛选机制。例如

   LIGHT_VIBRANT = new Target();
        setDefaultLightLightnessValues(LIGHT_VIBRANT);
        setDefaultVibrantSaturationValues(LIGHT_VIBRANT);

   VIBRANT = new Target();
        setDefaultNormalLightnessValues(VIBRANT);
        setDefaultVibrantSaturationValues(VIBRANT);

    dominate的颜色获取:        

private Palette.Swatch findDominantSwatch() {
        int maxPop = -2147483648;
        Palette.Swatch maxSwatch = null;
        int i = 0;

        for(int count = this.mSwatches.size(); i < count; ++i) {
            Palette.Swatch swatch = (Palette.Swatch)this.mSwatches.get(i);
            if (swatch.getPopulation() > maxPop) {
                maxSwatch = swatch;
                maxPop = swatch.getPopulation();
            }
        }

        return maxSwatch;
    }

你可能感兴趣的:(Android,面试题,应用开发)