2009/02/05

用 Python 跑 Multi-threading 版 PNG 最佳化

我一直有收大圖的習慣,每次看到有人貼的高解析度的圖,雖然檔案很大,也幾乎沒辦法光以 22 吋螢幕就能一覽全圖而必須拖曳觀看或者以螢幕最適大小縮放觀看,但是這些圖幾乎都是直接以掃描器掃描下來的圖,沒有經過太多破壞 (頂多去紋理、去字、去摺痕、連接空白等等),十分不錯。前幾個月前偶然發現 PNG 檔可以經由再一次的最佳化運算來縮小檔案尺寸而不破壞原圖 (其他軟體如 pngnq, pngquant 是真的會針對原圖進行減色的最佳化方式,我使用的只針對壓縮流最佳化,應無此種問題),這對於硬碟中有許多張超大體積的 PNG 檔可真是福音,為它們減個肥日後也才能塞更多XD

首先我發現的是 OptiPNG 這套軟體,能夠將 PNG 檔的壓縮流給重新整理後最佳化,需要的是 CPU 的運算能力,非常耗時間。原本以為一套就夠了,誰知道找下去又發現還有 PNGOUT,這玩意兒我在 IrfanView 看圖軟體裡面有見過,在另存 png 時可以選擇啟用它以進行最佳化。這套軟體強在它可以進行非常多次的暴力極限運算,一次次的降低檔案大小。只要你給它時間,它就能給你不斷縮小的檔案大小。不過這耗費時間真是非常可觀,只能以龜速形容。更何況我相信這絕對會有個極限的,不過是逐步逼近,看著這一次的長久運算只省下了 1024 bytes 的空間,你確定還要繼續下去?

好吧,有這兩套應該就完美了吧,你錯了。經過我深入研究,其實還有許多發展空間,其中 RT 對這方面的研究似乎還滿透徹的,無意間在 MozTW.org 被我發現相關討論串,還在外國的某交流論壇發現 RT hack 過的 OptiPNG,可以進行更好的最佳化。似乎沒發現有其他人有這樣作,看起來 RT 這方面還挺行的。離題了,他在這篇討論串提到他都是使用多套軟體交互運算以取得最佳結果,其中許多套都套用 hack 過 zlib 函式庫或其他地方,加了更多更好的算法,比起原官方程式來說更能得出好結果。他也提供自己所編的檔案跟使用的批次檔,非常有價值。


我對他作了修正,以讓我能夠不僅處理相同目錄下的 PNG 檔,更可以指定單一 PNG 或是目錄來進行處理,這樣就非常方便了。

這樣還不夠。雖然已經是非常方便了,但是前面提到這些軟體所進行的運算是非常耗費 CPU 資源的一項工作,所以要是有許多超高解析 PNG 大檔,不算個幾小時可能不會全部完成。打開工作管理員你會吐血:這些最佳化程式沒有一個是原生支援多核心處理的!換言之以我的 Core2 Duo E6300 來說,常常都是跑在 50% 的地方,剩下的 50% 永遠吃不到,這是不支援多執行緒程式的一大弱點。雖然多執行緒可以很有效的加快速度,但是這些演算法是否有針對多核心的處理方式?搞的不好還會發生非同步化問題讓速度有減無增,資源都耗在結果的同步上了,這樣也不是好選擇。總之不管為什麼 WinRAR, 7-Zip 等軟體的壓縮演算法都支援多執行緒,而此 PNG 最佳化竟然沒有,有沒有其他方法呢?

當然有,如果沒有我還要繼續打嗎?不知道你有沒有用過 foobar2000 轉過音樂的經驗?LAME 這一套 MP3 編碼器同樣不具備原生支援多執行緒功能 (好像有其他非官方 hack 實作出來,不管他),但是當你的 CPU 支援,foobar2000 可以一次執行多個 LAME 實體分別轉換不同首音軌,以達成多核心運用的最佳方式。反正每一個 LAME 實體只要負責自己分配到的那首音軌就好,就沒有非同步化的問題,看起來就像多了好幾位工人分別處理不同事務一樣,不互相影響。

所以我的想法出來了,之前都是一個 Batch 批次檔在進行處理,我可不可以以類似的想法,同時自動產生多個處理行程?這樣我的 CPU 多核心就能發揮最大效用。昨天剛學會的 Python 似乎已經有原生 Multi-Threading 支援 (Java 也有,以前好像是 Green threads 現在不知改原生了沒;Ruby 則還是 Green threads),讓我們學會寫多執行緒程式來榨乾 CPU 效能吧!

在 Google 搜尋「Python threading」可以找到前人的成果,我看了快樂過活@資料無限大,能力無限大: [Python]Threading這篇網誌的介紹跟引用,了解了基本的觀念。簡單來說 Python 採用 Queue 佇列跟 Threading 的配合來解決同步問題,你只要在一開始將要處理的東西推進佇列裡面,接下來隨意開幾個執行緒,這些工人們就會自己去抓來處理,無須你煩心囉。我直接先把我的成果貼上來:

這個簡單的 Python 程式使用了 threading, Queue 來達成多執行緒架構,然後我為了其他的需求而引了其他函式庫。os 用來呼叫外部程式用的,sys 用來抓命令列參數,glob 則是方便我簡單的下萬用字元就能搜尋符合的 PNG 檔案。

首先我建立了一個叫做 Processlet 的函式 (#8-14),這個就是我的執行緒要執行的內容 (工人),他會不斷的執行我指定的要求 (有個 while 迴圈包住了)。然後是 main 程式進入點 (#15-32),我在這邊使用 for 迴圈產生了兩個執行緒 (因為我的 CPU 是雙核心),這邊使用委任的方式直接呼叫 threading.Thread 包住我的 Processlet 函式,產生一個執行緒。網路上有些人是用繼承的方式繼承一個執行緒類別再撰寫內容,但我的程式簡單,使用此種委任方式也很直覺。

#21-26 是處理沒有輸入參數的情況,顯示的預設說明訊息。此訊息清楚的說明我的程式是這樣呼叫的:mt.py [資料夾路徑],比方說 mt.py "B:\"。當我取得了目錄位置後我使用 glob 來下查詢,得到此目錄下的所有 png 檔案絕對位置 (#27-30)。這下子終於可以把它們推到佇列,準備進行我們的工作囉。

#31 要做的事只有一個,把佇列關起來。同時告訴執行緒工作開始了!這跟搭乘遊樂區的雲霄飛車設施一樣,人都就座了服務員就會關門,不讓其他人進入搗亂,同時代表刺激的旅程要開始了。

讓我們再回到 #10-13,Processlet 的核心。當佇列關閉同時代表可以開始的同時,剛才產生的數個執行緒就會爭先恐後的開始工作,誰也不會等誰,因此工作的分配使用佇列來管理是十分妥善的。這邊主要要做的是取出處理檔案名稱、執行外部程式和告知此工作完成三個,剛好一人一行。從 Queue 中 get 出檔案位置後,我利用 os 提供的 system 函式來呼叫剛才寫好的 bat 檔案,為何不寫在 py 檔案的原因是我已經有寫好的 bat 了,一來套用方便,二來可以分開維護 (py 負責多執行緒,bat 負責最佳化 PNG;跟 foobar2000 負責多執行緒,LAME 負責轉檔相同)。等漫長的 BAT 執行完成後,要告訴執行緒我們已經完成一項工作,以便再取得新的工作內容,有 task_done() 這個函式可以用。


以上就是全部的架構,當我執行 mt.py 同時在命令列參數指定目錄時,Python 就會產生兩個執行緒,以外部批次檔去處理我的 PNG 檔案了。這時候再打開工作管理員 (或者功能更強的 Process Explorer) 你就可以看到 CPU 被吃滿的情況了 (Process Explorer 更能看到 python.exe 有兩個 cmd.exe 子行程,呼叫一堆 exe 在處理檔案)。速度是變快了,但是這樣就不能邊轉換邊上網了,很頓啊 XDDD

3 則留言:

  1. 還是得回應一下,zlib hack其實只是把 encode 的 gzip-1.2.4 hack port到zlib中這樣(gzip跟zlib是同宗)…別把我說得這麼神 XD

    回覆刪除
  2. 離題一下,說實在的現在硬碟這麼大,一套CG用多用幾套壓也只能多省個幾mb,卻要多花幾個小時,實在不值。所以最後我反而用比預設值還少的去跑,來節省時間。又或者把bmp用7z包起來,然後看jpg。
    話說回來 Optipng 還有一些可以進步的(針對CG來講)地方。通常一套CG會有幾種最佳化的參數。你作 Optipng 的時候會發現可能最後算出來的只用了三種。要是能有程式先把前幾啪算好的參數來讓後面選測,那會省不少時間。不過這不適合放在 Optipng 裡面,可能 script 會比較適合(但是也很麻煩)。

    回覆刪除
  3. 不過現在的處理器強的跟什麼一樣,我的研究室的電腦使用Core2 Duo E8400@3.0GHz,
    同樣的檔案跑沒幾分鐘就完成了,比E6300快非常多。
    現在又已經有四核心和更高時脈的Core i7,我想時間已經可以縮到合理忍受範圍內。

    至於我對PNG最佳化的演算法沒有研究,所以都沒有提到太多,
    單純把RT的成果當工具運用罷了。

    回覆刪除