許多從天然場景中拍攝的圖像,其色彩分佈上會給人一種和諧、一致的感受;反過來,在許多界面設計應用中,咱們也但願選擇的顏色能夠達到這樣的效果,但對通常人來講卻並不那麼容易,這屬於色彩心理學的範疇(固然不是指某些僞神棍所謂的那種)。從彩色圖像中提取其中的主題顏色,不只能夠用於色彩設計(參考網站:Design Seeds),也可用於圖像分類、搜索、識別等,本文分別總結並實現圖像主題顏色提取的幾種算法,包括顏色量化法(Color Quantization)、聚類(Clustering)和顏色建模的方法(顏色建模法僅做總結),源碼可見:GitHub: ImageColorTheme。html
彩色圖像通常採用RGB色彩模式,每一個像素由RGB三個顏色份量組成。隨着硬件的不斷升級,彩色圖像的存儲由最初的8位、16位變成如今的24位、32真彩色。所謂全綵是指每一個像素由8位(28=0~255)表示,紅綠藍三原色組合共有1677萬(256∗256∗256)萬種顏色,若是將RGB看做是三維空間中的三個座標,能夠獲得下面這樣一張色彩空間圖:node
固然,一張圖像不可能包含全部顏色,咱們將一張彩色圖像所包含的像素投射到色彩空間中,能夠更直觀地感覺圖像中顏色的分佈:python
所以顏色量化問題能夠用全部矢量量化(vector quantization, VQ)算法解決。這裏採用開源圖像處理庫 Leptonica 中用到的兩種算法:中位切分法、八叉樹算法。git
1.1. 中位切分法(Median cut)github
GitHub: color-theif 項目採用了 Leptonica 中的用到的(調整)中位切分法,Js 代碼比 C 要易讀得多。中位切分算法的原理很簡單直接,將圖像顏色看做是色彩空間中的長方體(VBox),從初始整個圖像做爲一個長方體開始,將RGB中最長的一邊從顏色統計的中位數一切爲二,使獲得的兩個長方體所包含的像素數量相同,重複上述步驟,直到最終切分獲得長方體的數量等於主題顏色數量爲止。算法
Leptonica 做者在報告 Median-Cut Color Quantization 中總結了這一算法存在的一些問題,其中主要問題是有可能存在某些條件下 VBox 體積很大但只包含少許像素。解決的方法是,每次進行切分時,並非對上一次切分獲得的全部VBox進行切分,而是經過一個優先級隊列進行排序,剛開始時這一隊列以VBox僅以VBox所包含的像素數做爲優先級考量,當切分次數變多以後,將體積*包含像素數做爲優先級。數組
Python 3 中內置了PriorityQueue
:xcode
1 from queue import PriorityQueue as PQueue 2 3 class VBox(object): 4 def __init__(self, r1, r2, g1, g2, b1, b2, histo): 5 self.vol = calV() 6 self.npixs = calN() 7 self.priority = self.npixs * -1 # PQueue 是按優先級自小到大排序 8 9 boxQueue.put((vbox0.priority, vbox0)) 10 11 vbox.priority *= vbox.vol 12 boxQueue.put((vbox0.priority, vbox0))
除此以外,算法中最重要的部分是統計色彩分佈直方圖。咱們須要將三維空間中的任意一點對應到一維座標中的整數,這樣才能以最快地速度定位這一顏色。若是採用所有的24位信息,那麼咱們用於保存直方圖的數組長度至少要是224=16777216,既然是要提取顏色主題(或是顏色量化),咱們能夠將顏色由RGB各8位壓縮至5位,這樣數組長度只有215=32768:app
def getColorIndex(self, r, g, b): return (r << (2 * self.SIGBITS)) + (g << self.SIGBITS) + b def getPixHisto(self): pixHisto = np.zeros(1 << (3 * self.SIGBITS)) for y in range(self.h): for x in range(self.w): r = self.pixData[y, x, 0] >> self.rshift g = self.pixData[y, x, 1] >> self.rshift b = self.pixData[y, x, 2] >> self.rshift pixHisto[self.getColorIndex(r, g, b)] += 1 return pixHisto
分別對4張圖片進行切分、提取:機器學習
def testMMCQ(pixDatas, maxColor): start = time.process_time() themes = list(map(lambda d: MMCQ(d, maxColor).quantize(), pixDatas)) print("MMCQ Time cost: {0}".format(time.process_time() - start)) return themes imgs = map(lambda i: 'imgs/photo%s.jpg' % i, range(1,5)) pixDatas = list(map(getPixData, imgs)) maxColor = 7 themes = [testMMCQ(pixDatas, maxColor)] imgPalette(pixDatas, themes, ["MMCQ Palette"])
1.2. 八叉樹算法(Octree)
八叉樹算法的原理能夠參考這篇文章:圖片主題色提取算法小結。做者也提供了 Js 實現的代碼,雖然與 Leptonica 中 C 實現的方法差異很大,但原理上是一致的。
創建八叉樹的原理實際上跟上面提到的統計直方圖有些類似,將顏色成分轉換成二進制以後,較低位(八叉樹中位置較深層)數值將被壓縮進較高位(八叉樹中較淺層)。八叉樹算法應用到主題色提取可能存在的問題是,每次削減掉的葉子數不肯定,可是新增長的只有一個,這就致使咱們須要的主題色數量並不必定恰好獲得知足,例如設定的主題色數量爲7,可能上一次葉子時總數還有10個,到了下一次只剩5個了。相似的問題在後面手動實現的KMeans算法中也有出現,爲了保證能夠獲得足夠的主題色,不得不強行提升算法中的顏色數量,而後取圖像中包含數量較多的做爲主題色:
def getColors(self, node): if node.isLeaf: [r, g, b] = list(map(lambda n: int(n[0] / n[1]), zip([node.r, node.g, node.b], [node.n]*3))) self.theme.append([r,g,b, node.n]) else: for i in range(8): if node.children[i] is not None: self.getColors(node.children[i]) self.theme = sorted(self.theme, key=lambda c: -1*c[1]) return list(map(lambda l: l[:-1],self.theme[:self.maxColor]))
對比上面兩種算法的結果:
def testOQ(pixDatas, maxColor): start = time.process_time() themes = list(map(lambda d: OQ(d, maxColor).quantize(), pixDatas)) print("OQ Time cost: {0}".format(time.process_time() - start)) return themes themes = [testMMCQ(pixDatas, maxColor), testOQ(pixDatas, maxColor)] imgPalette(pixDatas, themes, ["MMCQ Palette", "OQ Palette"])
可見八叉樹算法可能更適合用於提取調色板,並且兩種算法運行時間差別也很明顯:
#MMCQ Time cost: 8.238793 #OQ Time cost: 55.173573
除了OQ中採用較多遞歸之外,未對原圖進行抽樣處理也是其中緣由之一。
聚類是一種無監督式機器學習算法,咱們這裏採用K均值算法。雖說是「機器學習」聽起來時髦些,但算法本質上比上面兩種更加簡單粗暴。
KMeans算法的原理更加簡潔:「物以類聚」。咱們目的是將一堆零散的數據(如上面圖2)歸爲k個類別,使得每一個類別中的每一個數據樣本,距離該類別的中心(質心,centroid)距離最小,數學公式爲:
上文提到八叉樹算法可能出現結果與主題色數量不一致的狀況,在KMeans算法中,初始的k個類別的質心的選擇也可能致使相似的問題。當採用隨機選擇的方法時,有可能出如今迭代過程當中,選擇的中心點距離全部其它數據太遠而最終致使被孤立。這裏分別採用手動實現和scikit-learn的方法實現,根據scikit-learn 提供的API,完成主題色的提取大概只須要幾行代碼:
from sklearn.cluster import KMeans as KM import numpy as np #@pixData image pixels stored in numpy.ndarray #@maxColor theme color number h, w, d = pixData.shape data = np.reshape((h*w, d)) km = KM(n_clusters=maxColor) km.fit(data) theme = np.array(km.cluster_centers_, dtype=np.uint8) imgs = map(lambda i: 'imgs/photo%s.jpg' % i, range(1,5)) pixDatas = list(map(getPixData, imgs)) maxColor = 7 themes = [testKmeans(pixDatas, maxColor), testKmeans(pixDatas, maxColor, useSklearn=False)] imgPalette(pixDatas, themes, ["KMeans Palette", "KMeans DIY"])
測試比較手動實現和scikit-learn的結果以下:
好吧我認可很慘,耗時方面也是慘不忍睹。
從上面幾種算法結果來看,MMCQ和 KMeans在時間和結果上都還算不錯,但仍有改進的空間。若是從人類的角度出發,兩種算法的策略或者說在解決主題色提取這一問題時採納的特徵(feature)都接近於顏色密度,即相近的顏色湊在一塊兒數量越多,越容易被提取爲主題顏色。
最後要提到的算法來自斯坦福可視化組13年的一篇研究:Modeling how people extract color themes from images,實際上比較像一篇心理學研究的套路:建模-找人類被試進行行爲實驗-調參擬合。文章提取了圖像中的79個特徵變量並進行多元迴歸,同時找到普通人類被試和藝術系學生對圖像的主題顏色進行選擇,結果證實特徵+迴歸可以更好地擬合人類選擇的結果。
79個特徵的多元迴歸模型,不知道會不會出現過分擬合?另外雖然比前面算法多了不少特徵,但仍舊多物理特徵。對人類觀察者來講,咱們看到的並不是一堆無心義的色塊,雖然有研究代表顏色信息並不是場景識別的必要線索,但反過來場景圖像中的語義信息卻頗有可能影響顏色對觀察者的意義,這大概就是心理學研究與計算機科學方向上的差別。
參考:http://www.cad.zju.edu.cn/home/vagblog/?p=1211
以上算法若要應用還需更多優化,例如先抽樣再處理,計算密集的地方用C/C++或並行等。另外須要一個對Python每一個函數執行時間進行記錄的工具,分析運行時間長的部分。
原文:http://blog.rainy.im/2015/11/25/extract-color-themes-from-images/