在正式接觸NumPy程式庫之前,我曾經用過另一套OpenCV程式庫,在那當下需求的出發點在於玩OpenSCAD時,想要取得圖片載入後的像素資料做進一步處理,以便建立有趣的模型,像是將圖片方格化,或切為Voronoi拼接磚,雖然OpenSCAD內建的surface模組可以載入圖片,依灰階度來建立厚度,然而,並無法取得像素資料,為此,我打算透過OpenCV來轉換出像素資料,再匯入OpenSCAD。

隨意找幾篇OpenCV讀取圖片的文件,經過閱讀並做了簡單實驗後,我知道讀入的圖片會是陣列,包含了每一格像素資料,於是,我單純地透過迴圈及索引,取出資料另存,其實這並不算是錯,畢竟也是使用Python的好處。作為一門動態定型語言,我只要知道讀入圖片後傳回的物件,具有陣列的行為,就可以使用索引來操作,就算效能不好,然而,如此的作法確實解決了我的需求。

前陣子玩NumPy時找到了方向,能以正確的典範切入NumPy,經過一路學習,知道了OpenCV是基於NumPy。後來,我回頭看看先前寫的幾個圖片取像素資料程式,就覺得:無論是在流程的撰寫或者是效能上,都有點可笑了。

圖片就是NumPy陣列

因為是基於NumPy,所以OpenCV如果透過IMREAD_COLOR讀取圖片,會讀取圖片的RGB資訊,然而忽略透明度,傳回的是具有三維的NumPy陣列行為之物件,由於圖片使用的是電腦繪圖座標,因此,y往下為正,x往右為正,NumPy陣列的軸0用來指定圖片像素的y索引,軸1用來指定圖片像素的x索引;若是讀取了圖片的RGB,軸2依序是像素的BGR欄位資訊;如果讀入的是250 x 250的圖片,陣列形狀就是會(250,250,3)。

因為具有NumPy陣列的行為,只要能取出部分的陣列內容,就可以達到裁剪圖片的效果。自行使用迴圈雖然可以達到目的,然而,若能善用NumPy陣列索引取法,就有機會得到效能優勢。例如,若指定範圍為左上起點top、left,右下終點right、bottom,圖片為img,那麼,img[top:bottom,left:right]就能得到裁剪後的圖片。

如果要旋轉圖片,我們可以透過NumPy的rot90函式,如果要翻轉圖片呢?若是以IMREAD_GRAYSCALE讀入圖片,會得到具有二維的NumPy陣列行為之物件,每個元素代表著灰階值,透過NumPy的.T可以得到轉置矩陣,實際上,也就是翻轉後的圖片資料了。

當然,OpenCV本身有transpose、rotate等處理圖片的API,但在API無法達成等情境下,我們若能善用NumPy的一些特性,也是方便且有彈性的作法。

結合Matplotlib

在使用NumPy時,我們通常會搭配Matplotlib來進行資料的視覺化,而OpenCV內建了顯示圖片的功能,但是,有時還是會想結合Matplotlib的功能來顯示,或是依圖片資訊繪圖(例如稍後會談到的圖片三角分割),只不過,若直接將OpenCV以IMREAD_COLOR讀入圖片後的資料陣列,透過Matplotlib的imshow 顯示,會出現色彩顯示不正確的問題。

方才談到,OpenCV的陣列,軸2的順序會是像素的BGR欄位,也就是RGB的相反。事實上,現代開發者多習慣RGB,因此常踏到這個坑。之所以會使用BGR,是來自歷史性的原因,早期一些相機、軟硬體使用的就是BGR,而OpenCV就這麼用到現在,一些基於OpenCV而建構的程式庫或框架,也常見使用BGR而不是RGB。

若要透過OpenCV的方案來解決問題,可以運用split函式將BGR拆分出來,接著再透過merge函式以RGB順序合併;另一個方式是透過cvtColor函式,來指定COLOR_BGR2RGB;如果對NumPy陣列索引夠熟悉,也可以透過範圍索引的方式來分離BGR,例如,運用img[:,:,0]、img[:,:,1]、img[:,:,2]就可以個別取得B、G、R,之後,我們再透過merge函式以RGB合併。

不過,既然可以透過範圍索引的方式來分離BGR,在最後一個範圍索引處,直接透過-1來反轉,不就好了?也因此,img[:,:,::-1]這個神奇的語法,就可以直接將BGR轉為RGB,而且善用了NumPy的索引效能。

如果用OpenCV以灰階方式讀入圖片,因為元素只有灰階值,雖然沒有BGR轉RGB的問題,但是,因為在不指定Matplotlib的imshow之cmap(Color map)時,會使用rcParams["image.cmap"]作為預設,顯示時就不會是灰階圖,此時記得要自行指定cmap為'gray',才會是正確的顯示結果。

來個圖片三角分割器

當我這次再次接觸OpenCV,由於已經具備了NumPy與Matplotlib的基礎,在使用OpenCV時就感覺很踏實了,面對需求時,也就知道該怎麼適當地銜接一些技術元件來完成任務。

例如,我曾經試著在OpenSCAD中,實作圖片三角分割器(Image triangulator),這種做法可以將圖片處理為三角形拼接,看來就很像是低面數(Low-Poly)的3D藝術圖像。

基本上,圖片三角分割器的原理是,先尋找圖像邊緣,取得邊緣的像素座標,進行Delaunay三角分割,對每個三角形取內心座標,用內心座標取得圖像對應位置的顏色,再用該顏色塗滿三角形。雖然我在OpenSCAD中實作出來了,然而OpenSCAD畢竟不是專門用來處理圖像的軟體,運算過於耗時而難以接受。

這時我想起,Matplotlib的plot_trisurf函式在繪製三角曲面的當下,實際上,就是利用了Delaunay三角分割,內部使用的是mtri.Triangulation,若只是想畫出三角分割,將Triangulation實例丟給triplot,就可以了;如果我想取得分割後的三角形索引,可以透過該實例的get_masked_triangles方法;若是要取得圖像對應位置的顏色,那純粹就只是NumPy索引的應用。

於是,問題只剩下尋找影像邊緣,而這就是OpenCV的專長了,我們只要以灰階方式讀入圖片,再透過GaussianBlur函式調整模糊參數、Canny函式調整邊緣檢測參數,之後,就可以取得想要的邊緣偵測結果,而這會是個黑白影像,白色就是偵測出來的邊緣。

也就是說,結果就是個元素為0或255組成的二維陣列。若想得到白色像素的座標,可以透過NumPy的where函式,例如where(img == 255)取得255元素的索引,這些索引就是座標;將以上資訊結合起來,最後要繪製三角形時,我們可以透過Matplotlib來進行多邊形繪製,也就是透過PolyCollection的facecolor,來指定三角形的座標及面的顏色。

按部就班地認識技術堆疊

第一次接觸OpenCV時,我看相關文件有種迷失感,這應該是對NumPy、Matplotlib與OpenCV之間的關係不夠熟悉,將各自的元素都混在一起了。相較之下,在已經認識技術堆疊的情況下,我這次就能清楚地分辨出哪些元素是來自於哪個技術了。

如果只是解決一些無關痛癢的小需求,隨便翻閱文件,胡亂模仿刻出來的程式,或許可以解決需求,然而,面對這類有著技術堆疊關係的方案,如果我們能夠釐清路線(Roadmap),從底層按部就班地認識,真的才是能深入而靈活應用它們的不二法門。

專欄作者

熱門新聞

Advertisement