首頁  >  文章  >  web前端  >  Photoshop 油畫效果濾鏡

Photoshop 油畫效果濾鏡

高洛峰
高洛峰原創
2017-02-18 13:33:162470瀏覽

  本濾鏡是我採用PS SDK 開發而成,而濾鏡的演算法具體是由誰提出的可能不詳,我是參考了FilterExplorer 的源碼(VC 6),本演算法的主要參考來源是該專案中的Filters.cpp, 作者是Jason Waltman (18, April, 2001) 。另國內另一個用C#語言編寫的軟體 PhotoSprite (Version 3.0 ,2006,由 聯駿 編寫)其中的油畫濾鏡的演算法應該也是引用自了前者(或其他同源程式碼)。在研究此濾鏡演算法時,我主要參考的是前者的 C++ 程式碼,本文對此演算法的概念性描述屬於我的理解和解讀。但該演算法的效率並不高,我將該演算法的效率大大提高,關於模板尺寸的時間複雜度從O ( n^2 ) 改進為線性複雜度O ( n ),關於像素數量的複雜度的常數係數大幅減小,對相同測試樣本(某個1920 * 1200 像素RGB 影像)相同參數的處理速度從大約35 秒降低到大約3 秒,處理速度大概提高到10 ~ 12倍左右(粗略估算)。

    本文主要是發布 Photoshop 油畫效果濾鏡(OilPaint)。演算法並非我提出,可以參考本文的參考資料。該濾鏡在用 C# 開發的國產軟體 PhotoSprite 中可以看到。 2010 年曾有人請求我幫忙開發該濾鏡,現在我花了大概幾天時間將其開發出來並免費提供。

 

    (1)對油畫濾鏡的演算法的概念性描述

 

    這是我透過閱讀 FilterExplorer 原始碼後所理解的。此濾鏡有兩個參數,一個是模板半徑(radius),則模板尺寸是(radius * 2 + 1)*(radius * 2 + 1)大小,也就是以當前像素為中心,向外擴展radius 個像素的矩形區域,作為一個搜尋範圍,我們暫時將它稱為「模板」(實際上該演算法並不是例如高斯模糊,自定濾鏡那種標準模板法,僅僅是處理過程類似,因此我才能實現稍後介紹的優化)。

    另一個參數是光滑度(smoothness),實際上他是灰度桶的個數。我們假設把像素的灰階/亮度( 0 ~ 255 )均勻的分成smoothness 個區間,則每個區間我們在此稱它為一個桶(bucket),這樣我們就有很多個桶,暫時稱之為桶陣列(buckets)。

    此演算法遍歷圖上的每個像素,針對當前位置(x, y) 像素,將模板範圍內的所有像素灰度化,即把圖像變成灰度圖像,然後把像素值進一步離散化,即根據像素的灰度落入的區間,把模板內的像素依序投入到對應的桶子中。然後從這些桶子中找到一個落入像素個數最多的桶,並對該桶中的所有像素求出顏色平均值,作為位置 (x, y) 的結果值。

    上面的演算法描述,用下面的示意圖來表示。中間的影像是從原圖灰度化+離散化(相當於 Photoshop 中的色調分離)的結果,小方框表示的是模板。下方表示的是桶陣列(8 個桶,即把0~255的灰階值離散化成 8 個區間段)。

 

    Photoshop 油画效果滤镜

 

    (2)對完成的生活程式碼的效率的改善濾中並不難,我大概花了1 ~ 2 天的業餘時間就基本調試成功了。但是在閱讀老外的源碼時,我明顯感覺到原有程式碼的效率不夠高。此演算法遍歷一次影像即可完成,對每個像素的處理是常數時間,因此針對像素數量(影像長度*影像寬度)是O(n)複雜度,但是原有程式碼的常係數較大,例如,每次計算像素結果時,都要重新計算模板範圍內像素的灰度,並把它投入桶中,實際上造成大量的重複性計算。

 

    2.1 為此,我的第一個改進是在PS 中把當前的整個圖像貼片進行灰度化並離散化(投入桶中),這樣在用模板遍歷貼片時,就不需要重複性的計算灰階並離散化了。這樣大概把演算法的運行速度提高了一倍左右(針對某個樣本,處理速度從20多秒提高到10秒左右)。

 

    2.2 但這樣對速度的增加仍不夠顯著。因此我進行另一項更重要的最佳化,即把針對模板尺寸的複雜度從平方降到線性複雜度。這個依據是,考慮模板在目前行間從左向右逐格移動,模板中部像素(相鄰兩個模板的交集)在結果中的統計資料是不變的。只有最左側一列移出模板,最右側一列進入模板,因此我們在遍歷圖像時就不必管模板中部像素,只需要處理模板的兩個邊緣即可。如下圖所示(半徑為2,模板尺寸是 5 * 5 像素):

 

    Photoshop 油画效果滤镜

 

    當到達貼片右側邊緣時,我們不是類似回車換行那樣重新復位到行首,而是把模板向下移動一行,進入下一條尾部,然後再向左平移,這樣一個模板的行進軌跡就成為一個蛇形迂迴步進的軌跡。當這樣改進以後,我們遍歷像素的時候就只需要處理模板的兩個邊緣像素。這樣,就把針對模板尺寸(參數中的半徑)從O(n^2)降低到O(n),從而使該演算法的運算速度大大提高,結合優化2.1 ,最終使演算法的運算速度大概提高了11 倍(該數值只是粗略估算,未經過大量樣本測試),優化後的演算法對大圖像的處理時間也是變得可以接受的。

 

    【注意】我能做到這樣的優化的原因是該濾鏡算法並不是標準的模板算法,它的本質是求模板範圍內的統計信息,即結果和像素的模板坐標無關。這就好像是我們想得到某個局部範圍的人口數,男女比例等資訊一樣。因此我們按以上方法進行優化。

 

    模板移動的軌跡是蛇形迂迴步進,例如:

 

    → → → → → → →

                            ↓

    ← ← ← ← ← ← ←

    ↓

    → → ...

 

    下面我將給出本濾鏡的核心演算法的程式碼,位於algorithm.cpp 中的全部程式碼:

 

    2.3 原有代碼對半徑的範圍限制是(1 ~ 5),由於我優化了程式碼,所以我把半徑的區間可以大大提高,我把半徑設定到100 時我發現半徑太大沒什麼意義,因為基本上已經看不出原圖是什麼了。

 

    【總結】改進後的程式碼的更具技巧性和挑戰性,包括大量底層的指針操作和不同矩形(輸入貼片,輸出貼片,模板)之間的坐標定位,代碼可讀性上可能略有降低,但只要理解上面的原理,程式碼依然是具有較好的可讀性的。此外,我還對該演算法的改進想到,把模板從矩形改為“圓形”,以及在遍歷圖像時,使模板半徑和桶數這兩個參數隨機抖動,但這些改進都會使2.2 中提到的最佳化失效,會使演算法的速度又降回較低的水平。

 

    (3)用多執行緒技術提高縮圖顯示效率,避免影響UI 執行緒的交互性

 

   教程  在參考的參數上顯示在略圖的參數之前參考圖中的第四篇文章,在此不再敘述。這裡我講解的是更新縮圖時對 UI 互動的改進,以及縮放平移技術。

    下圖是在 Photoshop 中呼叫此濾鏡時,彈出的參數設定對話框。使用者可以拖曳滑桿控制項(TrackBar,又稱 Slider),也可以直接在後面的文字方塊中輸入來改變參數。縮圖將實時性的更新已反應新參數。原來的濾鏡實作中,我把更新縮圖的處理放在和對話框UI的相同執行緒內。這樣做的話就會引入下面的問題,當用戶很快的拖動滑桿控制時,由於參數改變的速度很快,而UI 線程可能正忙於處理縮圖數據而短期被“阻塞”,使其不能立即回應後續的控制事件,即表現為滑桿控制的拖曳不夠流暢,有跳躍,頓挫,遲鈍感,對滑鼠拖曳的回饋不夠靈敏。

 

    

 

    為了改進該問題,不影響UI 執行緒完成執行緒在完成的處理縮圖時略圖對話方塊更新其視圖。當拖曳 Trackbar 時,UI 執行緒會以很快的頻率收到控制項通知,好像「浪湧」一樣,這要求後續到達的 UI 事件能讓正在執行的執行緒任務很快的中途中止並退出。

    我把濾鏡演算法提取出來作為一個共享的函數,這樣濾鏡的實際處理和更新縮圖可以共享這個函數。在 PS 實際呼叫濾鏡,以及更新縮圖期間,實際上都要求濾鏡演算法定期偵測「任務取消」事件。例如,當 PS 呼叫濾鏡時,如果使用者按下 ESC 鍵,就會立即放棄一個比較耗時的濾鏡操作。在更新縮圖時,如果發生 UI 事件的突波,同樣要求處理執行緒能夠迅速中止。

    在濾鏡的核心演算法函數中,我定期檢測「任務取消」事件,由於在PS 呼叫濾鏡時的測試取消和更新縮圖時測試取消的方法不同,因此在濾鏡演算法函數中,我增加了一個回呼函數參數(TestAbortProc)。這樣,在PS 呼叫濾鏡實際處理時,使用的是PS 內建的回調函數去偵測取消事件,在更新對話框的縮圖時,我用自己提供的一個回調函數來偵測取消事件(該函數偵測一個布爾變數來獲知是否有新的UI 事件等待處理)。

    縮圖的處理,我使用的是單線程。即每個新的UI 事件到來時,都要檢測縮圖處理線程是否正在運行,如果是,我置有新的UI 事件的標記,然後等待線程退出,等上一個線程退出後我才會啟動新的線程,這樣處理縮圖的線程永遠只有一個,而不會在UI 事件連續到來時開啟過多的線程,這樣做的好處是邏輯清晰,容易控制,不會讓我們陷入線程太多無法維護的麻煩之中。缺點是,儘管線程定期檢測取消事件,但線程中止還是需要少量時間的,這導致UI 線程依然可能存在微小的“停頓感”,但它是微不足道的,比起在UI 線程裡來更新縮圖已取得本質的提升。

    改進以後,可以用很快的速度拖曳參數對話框上的兩個滑竿,儘管這個濾鏡核心演算法的運算量較大,但我們能看到,參數對話框依然具有流暢的響應。

 

    (4)縮圖的縮放和平移功能

 

    其實不管縮放圖還是平移,更新縮放的資料本身並不難圖。難點主要在縮圖的平移,因為涉及滑鼠交互,這需要非常紮實的 windows 程式設計技巧和對 windows 程式底層機制的理解。 

    縮圖的拖曳有以下兩種方式:

 

    4.1 直接拖曳結果圖片。

    這又分為兩種方法。其一是比較完美的拖曳效果,但以浪費一定空間和時間為代價,編碼也具有一定挑戰性。即把縮圖的輸入資料外擴成 9 倍大小,並在記憶體中得到結果圖。顯示的時候僅僅顯示的是結果圖的中央部分。拖曳時,縮圖中不會出現空白部分。

    另一種方法是在拖曳時把當前結果圖進行快照(截圖),然後拖曳時,僅僅把截圖結果貼到螢幕對應位置上。這樣的效率較高,但缺點是拖曳時能看到縮圖旁邊有空白部分。這種方法常用語更新視圖的成本較大的情況,例如向量圖繪製等。我在這個濾鏡中實現的方法屬於這種方法。

 

    4.2 拖曳圖片為原始輸入圖片。

    即拖曳時,使用的圖片是原始數據,而不是結果圖片,這也是降低更新數據成本的一種折中方式。例如在 Photoshop 內建的濾鏡高斯模糊中,就是採用這種方法。當拖曳縮圖時,顯示的縮圖是原圖,僅在滑鼠釋放以後,才顯示成預覽效果。這樣做是比較經濟且有效的。因為我們要求原始資料的成本不高,但用濾鏡處理一次縮圖的成本較高。

    這裡額外提一點技術上的細節,要注意,由於鼠標可能移動到客戶區以外(成為負數),這時不能直接使用LOWORD ( lParam ) 和HIWORD ( lParam )  去獲得客戶區坐標(因為WORD是無符號數),使用前應該它們轉換成有符號數(short)。正確的方法是使用 windowsx.h 頭檔中的巨集:GET_X_LPARAM 和 GET_Y_LPARAM。

 

    (5)此濾鏡的下載連接(附件中含有我寫的PS 插件安裝工具,可簡化用戶安裝)

      插件 [該插件最近

    //我開發的PS 插件最新合集(含ICO,OilPaint,DrawTable 等)

            http://files.cnblogs.com/hoodlum1980/P〜3/P〜.  安裝後並重啟Photoshop 以後:

    在菜單:濾鏡- hoodlum1980 - OilPaint 中呼叫此濾鏡。

    在選單:幫助 - 關於增效工具 - OilPaint... 中可以看到關於對話框(和我開發的 ICO 文件格式插件的關於對話框的外觀幾乎一致)。

    在選單:幫助 - 系統資訊 中可以看到「OilPaint」一項是否已載入及其版本資訊。

 

    (6)一些不太重要的補充說明

 

    6.1 我採用的輸出貼片大小是128 *Photo 128   6.1 我採用的輸出貼片大小是128 *Photoshop 128*28 上的像素。的每一次步進表示完成了一個輸出貼片。輸入貼片通常大於等於輸出貼片,輸入貼片大小和濾鏡參數中的半徑是相關的(在四個方向上向外擴模板半徑的像素距離)。

    6.2 在濾鏡核心演算法中,為了提高對取消事件的靈敏度,在當前行中我每處理16個像素就檢測一次取消(行中像素索引 & 0x0F == 0x0F)。在每行處理結束後(列循環中)也偵測一次取消。但這個檢測頻率略顯過於頻繁了,過於頻繁可能會增加函數呼叫的成本開銷。

    6.3 採用相同影像,相同參數,我對我的濾鏡,FilterExplorer,PhotoSprite 三種軟體分別處理,然後再在 Photoshop 中比較。由於我的演算法是參考 FilterExplorer 的源碼,並在它的演算法上改進而來,因此我的演算法和 FilterExplorer 是等效的,只是效率更高,因此結果完全相同。但我的濾鏡以及FilterExplorer的結果,同 PhotoSprite 相比較整體效果非常接近,但結果有細微的不同。我查看了下 PhotoSprite 的程式碼發現,這是在圖像灰度化的演算法的差異造成的(當我把 PhotoSprite 的灰度化演算法調整為和 FilterExplorer 中相同後,處理結果就變成相同了)。

    在PhotoSprite 中,對像素灰度化所採用的方法是:
    gray = ( byte ) ( ( 19661 * R + 38666 * G + 7209 .  在 FilterExplorer / 我開發的濾鏡中,對像素灰階化所採用的方法是:

    gray = ( byte ) ( 0.3 * R + 0.59 * G + 0.11 * B ) ;

倠🀋〣中浮點〟〜〜〜〜〜〜〜〜〟〜〟〜〜〜〜〟〜〜〜〜〟〜〟〜〜〜〜〜〟〜〜〜、〟〜〜〟㟣㟜〜〟㟣㟜〜 㟜〜〟〜〜〜〜〟㟀㟜〜〼轉換成了整數乘法,效率可能會有一點點提高吧,但在此表現並不顯著。


 

    (7)參考資料

 

    7.1  FilterExplorer 原始碼。

    7.2  Photoshop 6.0 SDK Documents。

    7.3  FillRed 和 IcoFormat 插件原始碼,by hoodlum1980( myself )。

 

    (8)修正歷史

 

    8.01 [H] 固定記憶體的空間在「空間化」中,分配灰階圖大小的記憶體大小時,修復空間錯誤的記憶體在空間化時,分配錯誤。大小)的BUG。此 BUG 容易在以下條件下被觸發:文件尺寸太小,或半徑參數很小且光滑度參數很大,這些因素可能導致某個貼片過小。這時由於灰階桶空間的分配小於實際需求的大小,則可能後續程式碼發生記憶體越界,進而使 PS 進程意外終止。 2011-1-19  19:01。 

    8.02 [M] 新增縮圖縮放按鈕和功能,且優化了放大縮小按鈕處理時的程式碼,降低放大縮小時的閃爍感。 2011-1-19 19:06。

    8.03 [M] 新增縮圖滑鼠拖曳功能,且進一步調整了程式碼,使縮圖矩形固定下來,完全避免了縮放時的閃爍。 2011-1-19 23:50。

    8.04 [L] 新增功能,當滑鼠移到縮圖上和進行拖曳時,遊標變成伸開/抓住的手形。這是透過呼叫 PS 中的 Suite PEA UI Hooks suite 中的函數來實現的,即在縮圖上看到的是 PS 內部的遊標。 2011-1-20 1:23。

    8.05 [M] 修正在參數對話框上點選放大,縮小按鈕時,半徑參數折算的不正確的 BUG。此 BUG 使點擊放大縮小按鈕後,縮圖的顯示不準。 2011-1-20 1:55。

    8.06 [L] 把關於對話框上的網址連結調整為 SysLink 控件,這樣可以大大簡化關於對話框的視窗過程程式碼。 2011-1-20 18:12。

    8.07 [L] 更新插件安裝輔助工具,使它可以一次安裝多個插件。 2011-1-20 21:15。

    8.08 [L] 由於縮放縮圖時計算有誤差(原因不詳,可能是因為浮點運算誤差),可能會使靠近邊緣的右下角影像無法平移到縮圖視圖中,因此對平移範圍新增誤差緩衝。 2011-1-27。

    8.09 [L] 美化:把濾鏡參數對話框的放大縮小按鈕,改為使用 DirectUI 技術實現(新增一個類 CImgButton),其介面效果比用原始按鈕控制更好。 2011-2-14。

    8.10 [M] 性能:調整了插件在參數對話框時,調節半徑和光滑度參數的滑竿的交互性能。 2013-4-1。


更多Photoshop 油畫效果濾鏡 相關文章請關注PHP中文網!


陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn