Google Palette 2 -运行原理解析

导语

在上一篇,我介绍了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 的浮点表示

解析代码–跟着代码往下走

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

这里的逻辑大体就是,压缩>裁剪>颜色统计>生成推荐

  1. 压缩方法 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);
        }
  1. 计算出需要提取颜色的区域, 这里可以发现一个额外设置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());
                }
  1. 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;
            }
        }

  1. 创建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);
        }
    }
  1. 现在跳回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 值

  1. 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 的权重 ,和整体浮动的权重

  1. 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值,并且返回使用

问题解答

  1. Palette 是如何分析出图片颜色
    答:通过将图片拆分像素点,并且转为HSL 颜色系进行匹配分类和统计得出相应的颜色

  2. Palette 是如何给出各种颜色模式的,是根据什么规则去返回的
    答:未能找出结果,要继续下一个帖子分析

  3. Palette 为什么会返回空的Swatch ,什么情况下会返回
    答:未能找出结果,但是根据以上的代码 必然是generateScoredTarget 方法没有匹配到相应的结果

  4. 看看Palette 还有什么玩法(功能)
    在使用时可以再from() 后面调用以下方法有以下方法给你调用
    resizeBitmapSize() 设置图片最大的像素值,如果你的图片特别大,可以用这个降低分析图片的消耗
    resizeBitmapArea() 设置图片尺寸比例,如果和上一个方法同时使用,则最后一个生效
    clearFilters() 删除所有的筛选器,上面说到有一个默认的筛选器会将黑白 红(不确定)颜色去除,如果你想不被去除,则可以使用这个方法
    addFilter() 添加一个筛选器,这个筛选器可以自定义,如果添加了多个,则多个一起协助生效
    setRegion() 上面说到的mRegion 就是通过这个方法进行设置,可以设置需要匹配的区域,如果设置的区域超过图片的大小,这里会抛出异常
    clearRegion() 删除已经设置的区域,但是默认就是没有的,所以没什么用
    addTarget() 添加一个自定义的Target
    cleanTarget() 删除所有的Target ,因为有默认的预设值,所以这个方法也可以清除掉

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值