1.背景
在發(fā)現(xiàn)百日大戰(zhàn)時景項目中。有一個創(chuàng)新玩法,就是通過篩選圖片主色調(diào)來顯示如紅色系,藍(lán)色系照片。這就涉及到了圖片主色調(diào)的提取。技術(shù)選型為客戶端進(jìn)行圖片顏色提取,上傳到服務(wù)端。但是由于項目時間限制,iOS和Android的圖片色調(diào)提取算法不一樣。Android采用的是Google官方推出的Palette算法,為了統(tǒng)一,在這一期我去研究了一下Palette算法,并將它OC化。
2.Google Palette算法簡介
Palette算法是Android Lillipop中新增的特性。它可以從一張圖中提取主要顏色,然后把提取的顏色融入的App的UI之中。現(xiàn)在在很多設(shè)計出彩的泛前端展示屆非常普遍,如知乎等。大致效果如下:

可以看出來Android在Material Design上下了一番功夫。在很多Android官方的demo里,各種炫酷效果層出不窮。那我們就順勢站在巨人的肩膀上,將他人拿手之處,為我所用!
3.Palette算法分析
相比于很多傳統(tǒng)的圖片提取算法,Palette的特點是不單單是去篩選中出現(xiàn)顏色最多的。而是從使用角度出發(fā),通過六種模式,如活力色彩,柔和色彩等,篩選出更符合人眼篩選視覺焦點的顏色。如夜晚中的霓虹燈,白色背景的產(chǎn)品照。同時,也可以自定義篩選模式,輸入自己的篩選規(guī)則,得到目標(biāo)顏色。下面將逐步分析一下每個步驟。
(1)壓縮圖片,遍歷圖片像素,引出顏色直方圖的概念。并將不同的顏色存入新的顏色數(shù)組。
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顏色空間的顏色轉(zhuǎn)變成RGB555顏色空間,這樣就會使整個直方圖數(shù)組以及顏色數(shù)組長度大大減小,又不會太影響計算結(jié)果。
顏色直方圖的概念可以想象成一個顏色柱狀分布圖,某一柱越高,這柱代表的顏色在圖片中也就越多。它本質(zhì)上是一個int類型的一維數(shù)組。
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++;
}
}
將不同的顏色存進(jìn)distinctColors,留在后面進(jìn)行判斷。
(2)判斷顏色種類是否大于設(shè)定的最大顏色數(shù)。
最大顏色數(shù)在設(shè)計上可以設(shè)計為接收入?yún)ⅲ瑵M足不同使用者的需要,默認(rèn)值為16。這個值不宜過大,因為如果過大的話,圖片顏色會分的很散,圖片顏色比較分散的時候,得出來的顏色可能會偏向某一小部分顏色,而不是從整體上來綜合判斷。而當(dāng)圖片篩選出來的顏色種類小于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是最終被作為參考進(jìn)行模式篩選的數(shù)據(jù)結(jié)構(gòu),它有兩個最主要的屬性,一個是Color,這個Color是最終要被展示出來的Color,所以需要的是RGB888空間的顏色。另外一個是Population,它來自于hist直方圖。是作為之后進(jìn)行模式篩選的時候一個重要的權(quán)重因素。但是如果顏色個數(shù)超出了最大顏色數(shù),則需要進(jìn)行第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種主色調(diào)。它的屬性如下:
@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
其中l(wèi)owerIndex和upperIndex指的是在所有的顏色數(shù)組distinctColors中,VBox所持有的顏色范圍。Population代表的是這個顏色范圍中,一共有多少個像素點。其它的則代表R,G,B值各自的最大最小值。
它決定了該VBox的Volume。范圍越大,Volume越大,當(dāng)分裂VBox的時候,總是分裂當(dāng)前隊列中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的分裂規(guī)則是像素中點分裂,從lowerIndex遞增到upperIndex,如果某一個點讓整個像素個數(shù)累加起來大于了VBox像素個數(shù)的一半,則這個點就是splitPoint。而優(yōu)先隊列的排序規(guī)則是,隊首永遠(yuǎn)是Volume最大的VBox,從大概率上來講,這總是代表像素個數(shù)最多的VBox。當(dāng)VBox個數(shù)大于最大顏色個數(shù)的時候,則return,獲得優(yōu)先隊列中每個VBox的平均顏色。并生成平均顏色,之后將每個VBox轉(zhuǎn)換成了一個一個的Swatch。
(4)找到某一種模式下得分最高的Swatch,也就是獲得了最終的色調(diào)提取值。
在Palette算法里,“模式”對應(yīng)的數(shù)據(jù)結(jié)構(gòu)是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主要保存了飽和度和明度以及權(quán)重的數(shù)組。數(shù)組里保存了最小值,最大值,和目標(biāo)值。這些參數(shù)都是后面用來給HSL顏色值評分用的。這些值是經(jīng)過Google的團(tuán)隊進(jìn)行調(diào)優(yōu)之后,篩選出來的值。可以說是整套算法中最有價值的參數(shù)。
- (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;
}
通過這些已經(jīng)經(jīng)過調(diào)優(yōu)的參數(shù),可以得出每一項的得分:飽和度得分,明度得分,像素Population得分,將三項得分加起來,可以得到該Target評估得分最高的Swatch,也就是我們最終要提取的對應(yīng)顏色值。分值具體的具體方法如下:
- (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.最后
該算法已經(jīng)運用在了飛豬發(fā)現(xiàn)廣場的時景項目中(Android版本)。下一期,iOS端也會切換成這種提取算法。并且將這套算法沉淀在基礎(chǔ)線,只需要使用UIImage+Palette的接口即可調(diào)用。考慮到它的使用場景,會盡快沉淀為SDK,屆時會更新github地址,有需求的同學(xué)保持關(guān)注哦~