圖片顏色提取算法Google Palette分析及OC化

1.背景

在發(fā)現(xiàn)百日大戰(zhàn)時(shí)景項(xiàng)目中烫止。有一個(gè)創(chuàng)新玩法,就是通過篩選圖片主色調(diào)來(lái)顯示如紅色系戳稽,藍(lán)色系照片馆蠕。這就涉及到了圖片主色調(diào)的提取。技術(shù)選型為客戶端進(jìn)行圖片顏色提取惊奇,上傳到服務(wù)端互躬。但是由于項(xiàng)目時(shí)間限制,iOS和Android的圖片色調(diào)提取算法不一樣颂郎。Android采用的是Google官方推出的Palette算法吼渡,為了統(tǒng)一,在這一期我去研究了一下Palette算法乓序,并將它OC化寺酪。

2.Google Palette算法簡(jiǎn)介

Palette算法是Android Lillipop中新增的特性。它可以從一張圖中提取主要顏色竭缝,然后把提取的顏色融入的App的UI之中》课現(xiàn)在在很多設(shè)計(jì)出彩的泛前端展示屆非常普遍,如知乎等抬纸。大致效果如下:

可以看出來(lái)Android在Material Design上下了一番功夫咙俩。在很多Android官方的demo里,各種炫酷效果層出不窮湿故。那我們就順勢(shì)站在巨人的肩膀上阿趁,將他人拿手之處,為我所用坛猪!

3.Palette算法分析

相比于很多傳統(tǒng)的圖片提取算法脖阵,Palette的特點(diǎn)是不單單是去篩選中出現(xiàn)顏色最多的。而是從使用角度出發(fā)墅茉,通過六種模式命黔,如活力色彩,柔和色彩等就斤,篩選出更符合人眼篩選視覺焦點(diǎn)的顏色悍募。如夜晚中的霓虹燈,白色背景的產(chǎn)品照洋机。同時(shí)坠宴,也可以自定義篩選模式,輸入自己的篩選規(guī)則绷旗,得到目標(biāo)顏色喜鼓。下面將逐步分析一下每個(gè)步驟副砍。

(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算法為了減少運(yùn)算量顿锰,加快運(yùn)算速度谨垃。一共做了兩個(gè)事情,第一是將圖片壓縮硼控。第二個(gè)是將RGB888顏色空間的顏色轉(zhuǎn)變成RGB555顏色空間,這樣就會(huì)使整個(gè)直方圖數(shù)組以及顏色數(shù)組長(zhǎng)度大大減小胳赌,又不會(huì)太影響計(jì)算結(jié)果牢撼。

顏色直方圖的概念可以想象成一個(gè)顏色柱狀分布圖,某一柱越高疑苫,這柱代表的顏色在圖片中也就越多熏版。它本質(zhì)上是一個(gè)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è)計(jì)上可以設(shè)計(jì)為接收入?yún)⑼ξ穑瑵M足不同使用者的需要曲横,默認(rèn)值為16。這個(gè)值不宜過大不瓶,因?yàn)槿绻^大的話禾嫉,圖片顏色會(huì)分的很散,圖片顏色比較分散的時(shí)候蚊丐,得出來(lái)的顏色可能會(huì)偏向某一小部分顏色熙参,而不是從整體上來(lái)綜合判斷。而當(dāng)圖片篩選出來(lái)的顏色種類小于MaxColorNum的時(shí)候麦备,整個(gè)流程會(huì)簡(jiǎn)單很多孽椰。

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

這里引出了一個(gè)新的概念,叫Swatch(樣本)凛篙。Swatch是最終被作為參考進(jìn)行模式篩選的數(shù)據(jù)結(jié)構(gòu)黍匾,它有兩個(gè)最主要的屬性,一個(gè)是Color鞋诗,這個(gè)Color是最終要被展示出來(lái)的Color膀捷,所以需要的是RGB888空間的顏色。另外一個(gè)是Population削彬,它來(lái)自于hist直方圖全庸。是作為之后進(jìn)行模式篩選的時(shí)候一個(gè)重要的權(quán)重因素秀仲。但是如果顏色個(gè)數(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是一個(gè)新的概念覆劈,它理解起來(lái)稍微抽象一點(diǎn)保礼。可以這樣來(lái)理解责语,我們擁有的顏色過多炮障,但是我們只需要提取出例如16種顏色,需要需要用16個(gè)“筐”把顏色相近的顏色筐在一起坤候,最終用每個(gè)筐的平均顏色來(lái)代表提取出來(lái)的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代表的是這個(gè)顏色范圍中智末,一共有多少個(gè)像素點(diǎn)。其它的則代表R,G,B值各自的最大最小值徒河。
它決定了該VBox的Volume系馆。范圍越大,Volume越大顽照,當(dāng)分裂VBox的時(shí)候由蘑,總是分裂當(dāng)前隊(duì)列中VBox里Volume最大的那個(gè)。

- (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ī)則是像素中點(diǎn)分裂棒厘,從lowerIndex遞增到upperIndex纵穿,如果某一個(gè)點(diǎn)讓整個(gè)像素個(gè)數(shù)累加起來(lái)大于了VBox像素個(gè)數(shù)的一半,則這個(gè)點(diǎn)就是splitPoint奢人。而優(yōu)先隊(duì)列的排序規(guī)則是谓媒,隊(duì)首永遠(yuǎn)是Volume最大的VBox,從大概率上來(lái)講何乎,這總是代表像素個(gè)數(shù)最多的VBox句惯。當(dāng)VBox個(gè)數(shù)大于最大顏色個(gè)數(shù)的時(shí)候,則return支救,獲得優(yōu)先隊(duì)列中每個(gè)VBox的平均顏色抢野。并生成平均顏色,之后將每個(gè)VBox轉(zhuǎn)換成了一個(gè)一個(gè)的Swatch各墨。

(4)找到某一種模式下得分最高的Swatch指孤,也就是獲得了最終的色調(diào)提取值。

在Palette算法里,“模式”對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu)是target恃轩。它對(duì)顏色的識(shí)別和篩選不是使用的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ù)都是后面用來(lái)給HSL顏色值評(píng)分用的鸣峭。這些值是經(jīng)過Google的團(tuán)隊(duì)進(jìn)行調(diào)優(yōu)之后,篩選出來(lái)的值酥艳√埽可以說(shuō)是整套算法中最有價(jià)值的參數(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ù)充石,可以得出每一項(xiàng)的得分:飽和度得分更扁,明度得分,像素Population得分赫冬,將三項(xiàng)得分加起來(lái),可以得到該Target評(píng)估得分最高的Swatch溃列,也就是我們最終要提取的對(duì)應(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化效果展示。

圖上紅框部分即是篩選出來(lái)的主題色听隐。

4.最后

該算法已經(jīng)運(yùn)用在了飛豬發(fā)現(xiàn)廣場(chǎng)的時(shí)景項(xiàng)目中(Android版本)补鼻。下一期,iOS端也會(huì)切換成這種提取算法雅任。并且將這套算法沉淀在基礎(chǔ)線风范,只需要使用UIImage+Palette的接口即可調(diào)用』γ矗考慮到它的使用場(chǎng)景硼婿,會(huì)盡快沉淀為SDK,屆時(shí)會(huì)更新github地址禽车,有需求的同學(xué)保持關(guān)注哦~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末寇漫,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子殉摔,更是在濱河造成了極大的恐慌州胳,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件逸月,死亡現(xiàn)場(chǎng)離奇詭異栓撞,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)碗硬,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門瓤湘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)瓢颅,“玉大人,你說(shuō)我怎么就攤上這事岭粤∠鳎” “怎么了?”我有些...
    開封第一講書人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵剃浇,是天一觀的道長(zhǎng)巾兆。 經(jīng)常有香客問我,道長(zhǎng)虎囚,這世上最難降的妖魔是什么角塑? 我笑而不...
    開封第一講書人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮淘讥,結(jié)果婚禮上圃伶,老公的妹妹穿的比我還像新娘。我一直安慰自己蒲列,他們只是感情好窒朋,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蝗岖,像睡著了一般侥猩。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上抵赢,一...
    開封第一講書人閱讀 51,631評(píng)論 1 305
  • 那天欺劳,我揣著相機(jī)與錄音,去河邊找鬼铅鲤。 笑死划提,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的邢享。 我是一名探鬼主播鹏往,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼驼仪!你這毒婦竟也來(lái)了掸犬?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤绪爸,失蹤者是張志新(化名)和其女友劉穎湾碎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體奠货,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡介褥,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片柔滔。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡溢陪,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出睛廊,到底是詐尸還是另有隱情形真,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布超全,位于F島的核電站咆霜,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏嘶朱。R本人自食惡果不足惜蛾坯,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望疏遏。 院中可真熱鬧脉课,春花似錦、人聲如沸财异。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)戳寸。三九已至视事,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間庆揩,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工跌穗, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留订晌,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓蚌吸,卻偏偏與公主長(zhǎng)得像锈拨,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子羹唠,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容