### 1.背景

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

### 3.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颜色空间，这样就会使整个直方图数组以及颜色数组长度大大减小，又不会太影响计算结果。

``````    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){
distinctColorIndex++;
}
}

``````

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

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

``````

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

``````        _priorityArray = [[TRIPPaletteVBoxArray alloc]init];
_colorVBox = [[VBox alloc]initWithLowerIndex:0 upperIndex:distinctColorIndex colorArray:_distinctColors];
// 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

``````

``````- (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
// Then offer the box back
}else{
NSLog(@"All boxes split");
return;
}
}
}

``````

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

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

``````@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

``````

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

``````

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

``````