Google Palette算法详解以及OC化

1.背景

在发现百日大战时景项目中。有一个创新玩法,就是通过筛选图片主色调来显示如红色系,蓝色系照片。这就涉及到了图片主色调的提取。技术选型为客户端进行图片颜色提取,上传到服务端。但是由于项目时间限制,iOS和Android的图片色调提取算法不一样。Android采用的是Google官方推出的Palette算法,为了统一,在这一期我去研究了一下Palette算法,并将它OC化。最终将作为一个两端统一的技术方案,提供SDK到海纳平台上。

2.Google Palette算法简介

Palette算法是Android Lillipop中新增的特性。它可以从一张图中提取主要颜色,然后把提取的颜色融入的App的UI之中。现在在很多设计出彩的泛前端展示届非常普遍,如知乎等。大致效果如下:

可以看出来Android在Material Design上下了一番功夫。在很多Android官方的demo里,各种炫酷效果层出不穷。那我们就顺势站在巨人的肩膀上,将他人拿手之处,为我所用!

3.Palette算法分析

相比于很多传统的图片提取算法,Palette的特点是不单单是去筛选中出现颜色最多的。而是从使用角度出发,通过六种模式,如活力色彩,柔和色彩等,筛选出更符合人眼筛选视觉焦点的颜色。如夜晚中的霓虹灯,白色背景的产品照。同时,也可以自定义筛选模式,输入自己的筛选规则,得到目标颜色。下面将逐步分析一下每个步骤。

(1)压缩图片,遍历图片像素,引出颜色直方图的概念。并将不同的颜色存入新的颜色数组。

    unsigned int pixelCount;
    unsigned char *rawData = [self rawPixelDataFromImage:_image pixelCount:&pixelCount];
    if (!rawData){
        return;
    }
    NSInteger red,green,blue;
    for (int pixelIndex = 0 ; pixelIndex < pixelCount; pixelIndex++){

        red   = (NSInteger)rawData[pixelIndex*4+0];
        green = (NSInteger)rawData[pixelIndex*4+1];
        blue  = (NSInteger)rawData[pixelIndex*4+2];

        red = [TRIPPaletteColorUtils modifyWordWidthWithValue:red currentWidth:8 targetWidth:QUANTIZE_WORD_WIDTH];
        green = [TRIPPaletteColorUtils modifyWordWidthWithValue:green currentWidth:8 targetWidth:QUANTIZE_WORD_WIDTH];
        blue = [TRIPPaletteColorUtils modifyWordWidthWithValue:blue currentWidth:8 targetWidth:QUANTIZE_WORD_WIDTH];

        NSInteger quantizedColor = red << 2*QUANTIZE_WORD_WIDTH | green << QUANTIZE_WORD_WIDTH | blue;
        hist [quantizedColor] ++;
    }

Palette算法为了减少运算量,加快运算速度。一共做了两个事情,第一是将图片压缩。第二个是将RGB888颜色空间的颜色转变成RGB555颜色空间,这样就会使整个直方图数组以及颜色数组长度大大减小,又不会太影响计算结果。

颜色直方图的概念可以想象成一个颜色柱状分布图,某一柱越高,这柱代表的颜色在图片中也就越多。它本质上是一个int类型的一维数组。

    NSInteger distinctColorCount = 0;
    NSInteger length = sizeof(hist)/sizeof(hist[0]);
    for (NSInteger color = 0 ; color < length ;color++){
        if (hist[color] > 0 && [self shouldIgnoreColor:color]){
            hist[color] = 0;
        }
        if (hist[color] > 0){
            distinctColorCount ++;
        }
    }

    NSInteger distinctColorIndex = 0;
    _distinctColors = [[NSMutableArray alloc]init];
    for (NSInteger color = 0; color < length ;color++){
        if (hist[color] > 0){
            [_distinctColors addObject: [NSNumber numberWithInt:color]];
            distinctColorIndex++;
        }
    }

将不同的颜色存进distinctColors,留在后面进行判断。

(2)判断颜色种类是否大于设定的最大颜色数。

最大颜色数在设计上可以设计为接收入参,满足不同使用者的需要,默认值为16。这个值不宜过大,因为如果过大的话,图片颜色会分的很散,图片颜色比较分散的时候,得出来的颜色可能会偏向某一小部分颜色,而不是从整体上来综合判断。而当图片筛选出来的颜色种类小于MaxColorNum的时候,整个流程会简单很多。

        for (NSInteger i = 0;i < distinctColorCount ; i++){
            NSInteger color = [_distinctColors[i] integerValue];
            NSInteger population = hist[color];

            NSInteger red = [TRIPPaletteColorUtils quantizedRed:color];
            NSInteger green = [TRIPPaletteColorUtils quantizedGreen:color];
            NSInteger blue = [TRIPPaletteColorUtils quantizedBlue:color];

            red = [TRIPPaletteColorUtils modifyWordWidthWithValue:red currentWidth:QUANTIZE_WORD_WIDTH targetWidth:8];
            green = [TRIPPaletteColorUtils modifyWordWidthWithValue:green currentWidth:QUANTIZE_WORD_WIDTH targetWidth:8];
            blue = [TRIPPaletteColorUtils modifyWordWidthWithValue:blue currentWidth:QUANTIZE_WORD_WIDTH targetWidth:8];

            color = red << 2 * 8 | green << 8 | blue;

            TRIPPaletteSwatch *swatch = [[TRIPPaletteSwatch alloc]initWithColorInt:color population:population];
            [swatchs addObject:swatch];
        }

这里引出了一个新的概念,叫Swatch(样本)。Swatch是最终被作为参考进行模式筛选的数据结构,它有两个最主要的属性,一个是Color,这个Color是最终要被展示出来的Color,所以需要的是RGB888空间的颜色。另外一个是Population,它来自于hist直方图。是作为之后进行模式筛选的时候一个重要的权重因素。但是如果颜色个数超出了最大颜色数,则需要进行第3步。

(3)通过VBox分裂的方式,找到代表平均颜色的Swatch。

        _priorityArray = [[TRIPPaletteVBoxArray alloc]init];
        _colorVBox = [[VBox alloc]initWithLowerIndex:0 upperIndex:distinctColorIndex colorArray:_distinctColors];
        [_priorityArray addVBox:_colorVBox];
        // split the VBox
        [self splitBoxes:_priorityArray];
        //Switch VBox to Swatch
        self.swatchArray = [self generateAverageColors:_priorityArray];

VBox是一个新的概念,它理解起来稍微抽象一点。可以这样来理解,我们拥有的颜色过多,但是我们只需要提取出例如16种颜色,需要需要用16个“筐”把颜色相近的颜色筐在一起,最终用每个筐的平均颜色来代表提取出来的16种主色调。它的属性如下:


@interface VBox :NSObject

@property (nonatomic,assign) NSInteger lowerIndex;

@property (nonatomic,assign) NSInteger upperIndex;

@property (nonatomic,strong) NSMutableArray *distinctColors;

@property (nonatomic,assign) NSInteger population;

@property (nonatomic,assign) NSInteger minRed;

@property (nonatomic,assign) NSInteger maxRed;

@property (nonatomic,assign) NSInteger minGreen;

@property (nonatomic,assign) NSInteger maxGreen;

@property (nonatomic,assign) NSInteger minBlue;

@property (nonatomic,assign) NSInteger maxBlue;

@end

其中lowerIndex和upperIndex指的是在所有的颜色数组distinctColors中,VBox所持有的颜色范围。Population代表的是这个颜色范围中,一共有多少个像素点。其它的则代表R,G,B值各自的最大最小值。
它决定了该VBox的Volume。范围越大,Volume越大,当分裂VBox的时候,总是分裂当前队列中VBox里Volume最大的那个。

- (void)splitBoxes:(TRIPPaletteVBoxArray*)queue{
    //queue is a priority queue.
    while (queue.count < maxColorNum) {
        VBox *vbox = [queue objectAtIndex:0];
        if (vbox != nil && [vbox canSplit]) {
            // First split the box, and offer the result
            [queue addVBox:[vbox splitBox]];
            // Then offer the box back
            [queue addVBox:vbox];
        }else{
            NSLog(@"All boxes split");
            return;
        }
    }
}

VBox的分裂规则是像素中点分裂,从lowerIndex递增到upperIndex,如果某一个点让整个像素个数累加起来大于了VBox像素个数的一半,则这个点就是splitPoint。而优先队列的排序规则是,队首永远是Volume最大的VBox,从大概率上来讲,这总是代表像素个数最多的VBox。当VBox个数大于最大颜色个数的时候,则return,获得优先队列中每个VBox的平均颜色。并生成平均颜色,之后将每个VBox转换成了一个一个的Swatch。

(4)找到某一种模式下得分最高的Swatch,也就是获得了最终的色调提取值。

在Palette算法里,“模式”对应的数据结构是target。它对颜色的识别和筛选不是使用的RGB色彩空间,而采用的是HSL颜色模型。它的主要属性如下:

@interface TRIPPaletteTarget()

@property (nonatomic,strong) NSMutableArray *saturationTargets;

@property (nonatomic,strong) NSMutableArray *lightnessTargets;

@property (nonatomic,strong) NSMutableArray* weights;

@property (nonatomic,assign) BOOL isExclusive; // default to true

@property (nonatomic,assign) PaletteTargetMode mode;

@end

Target主要保存了饱和度和明度以及权重的数组。数组里保存了最小值,最大值,和目标值。这些参数都是后面用来给HSL颜色值评分用的。这些值是经过Google的团队进行调优之后,筛选出来的值。可以说是整套算法中最有价值的参数。

- (TRIPPaletteSwatch)getMaxScoredSwatchForTarget:(TRIPPaletteTarget)target{
    CGFloat maxScore = 0;
    TRIPPaletteSwatch *maxScoreSwatch = nil;
    for (NSInteger i = 0 ; i<_swatchArray.count; i++){
        TRIPPaletteSwatch *swatch = [_swatchArray objectAtIndex:i];
        if ([self shouldBeScoredForTarget:swatch target:target]){
            CGFloat score = [self generateScoreForTarget:target swatch:swatch];
            if (maxScore == 0 || score > maxScore){
                maxScoreSwatch = swatch;
                maxScore = score;
            }
        }
    }
    return maxScoreSwatch;
}

通过这些已经经过调优的参数,可以得出每一项的得分:饱和度得分,明度得分,像素Population得分,将三项得分加起来,可以得到该Target评估得分最高的Swatch,也就是我们最终要提取的对应颜色值。分值具体的具体方法如下:

- (CGFloat)generateScoreForTarget:(TRIPPaletteTarget)target swatch:(TRIPPaletteSwatch)swatch{
    NSArray *hsl = [swatch getHsl];

    float saturationScore = 0;
    float luminanceScore = 0;
    float populationScore = 0;

    if ([target getSaturationWeight] > 0) {
        saturationScore = [target getSaturationWeight]
        * (1.0f - fabsf([hsl[1] floatValue] - [target getTargetSaturation]));
    }
    if ([target getLumaWeight] > 0) {
        luminanceScore = [target getLumaWeight]
        * (1.0f - fabsf([hsl[2] floatValue] - [target getTargetLuma]));
    }
    if ([target getPopulationWeight] > 0) {
        populationScore = [target getPopulationWeight]
        * ([swatch getPopulation] / (float) _maxPopulation);
    }

    return saturationScore + luminanceScore + populationScore;
}

(5)Palette算法OC化效果展示。

图上红框部分即是筛选出来的主题色。

4.最后

该算法已经运用在了飞猪发现广场的时景项目中(Android版本)。下一期,iOS端也会切换成这种提取算法。并且将这套算法沉淀在基础线,只需要使用UIImage+Palette的接口即可调用。考虑到它的使用场景,会尽快沉淀为SDK,供集团内其它App使用。

时间: 2024-08-31 04:41:47

Google Palette算法详解以及OC化的相关文章

机器学习(二)--- 分类算法详解

感觉狼厂有些把机器学习和数据挖掘神话了,机器学习.数据挖掘的能力其实是有边界的.机器学习.数据挖掘永远是给大公司的业务锦上添花的东西,它可以帮助公司赚更多的钱,却不能帮助公司在与其他公司的竞争中取得领先优势,所以小公司招聘数据挖掘/机器学习不是为了装逼就是在自寻死路.可是相比Java和C++语言开发来说,机器学习/数据挖掘确实是新一些老人占的坑少一些,而且可以经常接触一些新的东西.还是赶紧再次抓住机会集中的再总结一下吧,不能再拖拖拉拉了.  其实数据挖掘的主要任务是分类.聚类.关联分析.预测.时

Floyd求最短路径算法详解

倘若我们要在计算机上建立一个交通咨询系统则可以采用图的结构来表示实际的交通网络.其实现最基本的功能,求出任意两点间的最短路径, 求最短路径的经典方法有很多种,最常用的便是迪杰斯特拉算法和佛洛依德(Floyd)算法,这篇文章就着重介绍Floyd算法. 求两点之间的最短路径无外乎有两种情况,一种就是从一点直接到另一点,另一种就是从一点经过n个节点后再到另一个节点,比如说要从A到B,则有两种情况就是A直接到B,或者是从A经过N个节点后再到B,所以,我们假设Dis(AB)为节点A到节点B的最短路径的距离

javascript快速排序算法详解_javascript技巧

"快速排序"的思想很简单,整个排序过程只需要三步: (1)在数据集之中,找一个基准点 (2)建立两个数组,分别存储左边和右边的数组 (3)利用递归进行下次比较 看一个demo:http://jsdo.it/norahiko/oxIy/fullscreen(网页打开可能较慢,慢慢等待吧) <script type="text/javascript"> function quickSort(arr){ if(arr.length<=1){ return

Javascript实现快速排序(Quicksort)的算法详解_javascript技巧

目前,最常见的排序算法大概有七八种,其中"快速排序"(Quicksort)使用得最广泛,速度也较快.它是图灵奖得主C. A. R. Hoare(1934--)于1960时提出来的. "快速排序"的思想很简单,整个排序过程只需要三步: (1)在数据集之中,选择一个元素作为"基准"(pivot). (2)所有小于"基准"的元素,都移到"基准"的左边:所有大于"基准"的元素,都移到"

php 二分查找法算法详解

一.概念:二分查找又称折半查找,优点是比较次数少,查找速度快,平均性能好:其缺点是要求待查表为有序表,且插入删除困难.因此,折半查找方法适用于不经常变动而查找频繁的有序列表.首先,假设表中元素是按升序排列,将表中间位置记录的关键字与查找关键字比较,如果两者相等,则查找成功:否则利用中间位置记录将表分成前.后两个子表,如果中间位置记录的关键字大于查找关键字,则进一步查找前一子表,否则进一步查找后一子表.重复以上过程,直到找到满足条件的记录,使查找成功,或直到子表不存在为止,此时查找不成功. 二.代

C语言 奇偶排序算法详解及实例代码_C 语言

C语言奇偶排序算法 奇偶排序,或奇偶换位排序,或砖排序,是一种相对简单的排序算法,最初发明用于有本地互连的并行计算.这是与冒泡排序特点类似的一种比较排序.该算法中,通过比较数组中相邻的(奇-偶)位置数字对,如果该奇偶对是错误的顺序(第一个大于第二个),则交换.下一步重复该操作,但针对所有的(偶-奇)位置数字对.如此交替进行下去. 使用奇偶排序法对一列随机数字进行排序的过程 处理器数组的排序 在并行计算排序中,每个处理器对应处理一个值,并仅有与左右邻居的本地互连.所有处理器可同时与邻居进行比较.交

Google V8编程详解附录

Google V8编程详工具函数 头文件:utils.h #ifndef UTILS_H_ #define UTILS_H_ #include "v8.h" #include <iostream> using namespace v8; using namespace std; v8::Handle<v8::String> ReadJS(const char* name); void printValue(Handle<Value> result);

算法详解之回溯法具体实现_C 语言

理论辅助: 回溯算法也叫试探法,它是一种系统地搜索问题的解的方法.回溯算法的基本思想是:从一条路往前走,能进则进,不能进则退回来,换一条路再试.用回溯算法解决问题的一般步骤为: 1.定义一个解空间,它包含问题的解. 2.利用适于搜索的方法组织解空间. 3.利用深度优先法搜索解空间. 4.利用限界函数避免移动到不可能产生解的子空间. 问题的解空间通常是在搜索问题的解的过程中动态产生的,这是回溯算法的一个重要特性. 还是那个基调,不喜欢纯理论的东西,喜欢使用例子来讲诉理论,在算法系列总结:动态规划(

机器学习之从极大似然估计到最大熵原理以及EM算法详解

一.极大似然估计 极大似然估计是建立在极大似然原理的基础上的一个统计方法,极大似然原理的直观想法是,一个随机试验如有若干个可能的结果A,B,C,- ,若在一次试验中,结果A出现了,那么可以认为实验条件对A的出现有利,也即出现的概率P(A)较大.极大似然原理的直观想法我们用下面例子说明.设甲箱中有99个白球,1个黑球:乙箱中有1个白球.99个黑球.现随机取出一箱,再从抽取的一箱中随机取出一球,结果是黑球,这一黑球从乙箱抽取的概率比从甲箱抽取的概率大得多,这时我们自然更多地相信这个黑球是取自乙箱的.