華章程式員書庫
Java圖像處理:基于OpenCV與JVM
Java Image Processing Recipes: With OpenCV and JVM

[法] 尼古拉斯·莫德奇克(Nicolas Modrzyk) 著
魏 蘭 潘婉瓊 譯
第1章
基于JavaVM的OpenCV
幾年前,在去上海的旅途中,一位好友送給我一本很厚的書,是介紹OpenCV的。書中包含了海量的圖像處理方法、實時視訊分析例子和引人入勝的深度解析,于是我迫不及待地配置好環境來測試書中的程式。
衆所周知,OpenCV是開源計算機視覺(Open Source Computer Vision)的英文簡寫。作為一個開源庫,OpenCV提供可直接使用的進階圖像處理算法,既包括簡單易用的進階圖像操作,也包括形狀識别以及實時視訊監測和分析功能。
OpenCV中最核心的内容是多元矩陣對象,叫作Mat。通過本書的學習,Mat将成為我們最熟悉的朋友。在許多攻略中,輸入的對象是Mat,處理的内容是Mat,輸出的結果也是Mat。
雖然Mat即将成為我們的好朋友,但是作為一個C++對象,它并不是很好相處。你必須重新編譯、安裝和小心地配置任何使用Mat的新環境。
但是Mat可以被打包。
Mat雖然在本地運作,但它可以被神不知鬼不覺地加載到Java虛拟機中運作。
第1章将通過介紹Java虛拟機中的多種語言讓你開始上手使用OpenCV,當然包括Java語言,也包括通俗易懂的Scala語言和谷歌最愛的Kotlin語言。
為了使用同樣的方法來運作不同的語言,你會首先(重新)認識一種Java編譯工具,叫作Leiningen,之後利用它來運作簡單的OpenCV函數。
第1章是第2章的入門基礎。第2章的内容是相似的基于JVM的Clojure語言,可以為富有創造性的OpenCV代碼帶來即時的視覺回報。
1.1 初識Leiningen
問題定義
有一句名言是“一次編寫,随處運作”,也就是說,在不同的機器上,可以用同樣簡單便捷的方法來編譯和運作Java程式。當然,你總是可以使用最原始的javac指令來編譯Java代碼,然後使用單純的Java在指令行中運作編譯過的代碼,但現在已經是21世紀了,我們應該尋找更有效的方法。
無論使用何種程式設計語言,手動配置工作環境都是一項大工程。而且當你完成配置之後,很難與他人分享勝利果實。
使用編譯工具,可以用簡單的方法定義項目所需的依賴,同時也可幫助其他使用者更快地上手。
接下來,我們介紹一個簡單易用的編譯工具。
解決方法
Leiningen 是(主要)面向JavaVM的編譯工具。它與一些知名的工具有些相似,例如Ant、Maven和Gradle。
當Leiningen指令行安裝完成之後,就可以基于模闆來建立JavaVM項目,并且毫無顧慮地運作程式了。
本攻略将介紹如何快速安裝Leiningen,以及如何使用它運作你的第一個Java程式。
工作原理
首先,把Leiningen安裝在你需要的地方,然後用它來建立一個空的Java項目。
注意: 安裝Leiningen之前,需要在你的電腦上安裝Java 8。由于Java 9通過破壞現有方法來解決舊的問題,我們目前還是選擇使用Java 8。
安裝Leiningen
Leiningen的網站首頁是
https://leiningen.org/。
在首頁上方,可以找到手動安裝Leiningen的四個簡單步驟。
在MacOS和Unix環境中:
1.下載下傳lein腳本。
https://raw.githubusercontent.com/technomancy/leiningen/stable/bin/lein2.把它放在你的$PATH變量中,以便shell可以找到它(例如~/bin)。
3.設定腳本為可運作(chmod a+x ~/bin/lein)。
4.在終端運作lein,然後它會下載下傳一個安裝包。
在Windows環境中:
1.下載下傳lein.bat批處理腳本。
https://raw.githubusercontent.com/technomancy/leiningen/stable/bin/lein.bat2.用管理者權限把它放在你的C:/Windows/System32檔案夾下。
3.打開指令提示符運作lein,然後它會下載下傳一個安裝包。
在Unix環境中,你可以使用包管理器。在MacOS中,Brew有Leiningen的包。
在Windows中,也有一個很好的安裝器在
https://djpowell.github.io/leiningen-win-installer/如果你是一個Chocolatey粉絲,Windows也有Chocolatey的包:
https://chocolatey.org/packages/Lein如果你在終端或指令提示符中安裝成功,那麼你就可以看到已安裝工具的版本号。在第一次運作的時候,Leiningen會下載下傳它的内部依賴(internal dependecy),但是之後的運作通常會非常快。
用Leiningen建立新的包含OpenCV的Java項目
Leiningen通常使用一個文本檔案,叫作project.clj,裡面有一個簡單的圖,存儲着中繼資料(metadata)、依賴(dependencies)、插件(plug-ins)和配置(settings)。
當你通過調用lein指令來運作項目時,lein會去project.clj檔案中查找該項目的相關資訊。
Leiningen自帶可直接使用的項目模闆,但是為了更好地了解,我們來一步步地學習第一個例子。
對于一個Leiningen Java項目,你需要兩個檔案:
■一個用來描述項目的project.clj檔案
■一個包含Java代碼的.java檔案,例如Hello.java
第一個項目的目錄結構看起來是這個樣子的:
一個目錄,兩個檔案。
為了簡化問題,第一個Java例子十分簡單。
接下來,讓我們來看一看project.clj檔案中的細節:
這其實是Clojure代碼,不過我們把它看作一種領域專用語言(Domain Specific Language,DSL),即使用特定術語描述項目的語言。
為了友善,每種術語在表1-1中有解釋。
接下來,請建立對應的檔案夾和檔案目錄,複制并粘貼每個檔案對應的内容。
完成之後,就可以運作你的第一個Leiningen指令:
這個指令會根據你的環境,在你的終端或控制台中生成以下内容:
太棒啦!我們的旅程開始啦!但是,等一等,剛才到底發生了什麼呢?
其中包含了一點魔法。Leiningen的run指令會讓Leiningen運作一個編譯過的Java類main函數。這個被運作的類定義在項目的中繼資料中,你應該記得,叫作Hello。
在運作Java類之前,我們需要先編譯它。預設情況下,Leiningen會在運作run之前進行編譯,這也解釋了“Compiling...”是從哪裡來的。
之後,你可能會注意到,在你的項目中建立了一個叫作target的檔案夾,裡面包含了一個叫作classes的檔案夾和一個Hello.class檔案。
target和classes檔案夾是編譯過的Java位元組碼(bytecode)的預設存儲位址,這個target檔案夾之後會被加入Java運作時的類路徑(classpath)。
緊跟着是“lein run”觸發的執行階段,Hello類的主函數中的代碼塊被執行,并輸出資訊。
你可能會問:“如果我有多個Java檔案,并且想運作非主函數之外的其他函數呢?”
這是一個非常合理的問題,在第1章中編寫和運作不同代碼時,你會經常使用這個技巧。
假如你在同樣的Java檔案夾中寫了第二個Java類,叫作Hello2.Java,其中包含更新的旅途内容。
為了運作Hello2.java的主函數,在調用lein run時,需要加上-m選項,這裡m代表着主函數,之後跟着需要運作的Java類的名稱。
這個指令輸出以下内容:
太好啦!根據這些指導,你可以勇往直前,運作你的第一個OpenCV Java程式了。
1.2 編寫你的第一個OpenCV Java程式
在通過Leiningen設定的Java項目中,直接使用OpenCV庫。
在利用OpenCV運作Java代碼之前,有一個令人頭疼的問題(自己編譯OpenCV封裝時),希望這一步能夠越簡單越好。
解決方法
1.1節介紹了Leiningen的基本環境配置。這一節介紹如何添加對OpenCV C++庫和Java封裝的依賴。
工作原理
第一個OpenCV例子将使用Leiningen項目模闆來配置,其中project.clj檔案和檔案夾已經建立完畢。Leiningen項目模闆不需要單獨下載下傳,可以通過在建立新項目時使用Leiningen的內建指令new來調用。
為了在你的機器上建立該項目,在指令行中運作lein指令。
無論是在Windows還是Mac中,這個指令都會輸出:
以上指令做了兩件事情:
1.建立了一個新的項目檔案夾,叫作hellocv。
2.根據名為jvm-opencv的模闆,在相關的檔案夾中建立了目錄和檔案。
指令運作完之後,相對簡單的項目檔案就自動生成了。
這個看起來好像不是很令人印象深刻,但是這些檔案和上一個攻略中的兩個檔案基本一樣,都是一個項目描述檔案和一個Java檔案。
project.clj檔案的内容與之前有所不同:
也許你會馬上注意到有三行從未見過的内容。
首先是repositories區域,這是一個新的區域,用于查找依賴。這裡填寫的内容是作者存儲OpenCV建構檔案的公用位址。
OpenCV的核心依賴以及本地依賴都已被編譯好,并且上傳至該公共區域供你使用。
這兩個依賴分别是:
■opencv
■opencv-native
你也許會問,為什麼需要兩個依賴呢?
opencv-native是OpenCV針對不同平台的C++代碼,例如MacOS、Windows或Linux的平台相關依賴。opencv是平台無關的Java封裝,用來調用不同平台的C++代碼。
當你編譯OpenCV時,這也是OpenCV代碼傳送的方式。
為了友善起見,打包好的opencv-native中包含針對Windows、Linux和MacOS的所有源代碼。
HelloCv.java檔案中的Java代碼位于Java檔案夾中,是一個類似于helloworld的簡單例子,會直接加載OpenCV的源代碼庫。内容如下所示:
這段代碼做了什麼呢?
①它告訴Java運作時利用loadLibrary來加載native opencv庫。這是使用OpenCV的必要步驟,每次運作你的應用時都要調用一次。
②通過Java對象,建立了一個Mat對象。Mat本質上是一個圖像存儲器,像矩陣一樣,這裡我們設定它的尺寸為3×3:高度為3個像素,寬度為3個像素。每個像素的類型是8UC1,這個奇怪的名字代表着包含8個位的無符号(8U)單通道(C1)整數。
③最終輸出Mat(矩陣)對象中的内容。
和之前一樣,這個項目是可以直接被運作的,無論你使用什麼平台,lein run指令都可以完成任務。
該指令輸出以下内容:
這裡的1和0代表着建立的矩陣對象的實際内容。
1.3 自動編譯和運作代碼
雖然lein指令非常通用,你可能還是想在背景啟動你的程式,并且在更新代碼的時候讓你的代碼自動運作。
Leiningen配有自動插件。啟用後,該插件會監視檔案模式的變化并觸發指令。讓我們來試試吧!
當你用jvm-opencv模闆建立項目時(請參閱1.2節),你會注意到project.clj檔案的内容略長于本書中顯示的内容。它實際上看起來更像這樣:
多出來的兩行被高亮顯示出來。一行是項目中繼資料在:plugins部分增加了lein-auto的插件。
另一行(即:auto部分)定義要監視變化的檔案模式,這裡所有以Java結尾的檔案的變化都會激活自動重新整理的子指令。
回到指令行,現在我們将在通常的run指令前添加auto指令,你需要編寫下面這樣的指令:
第一次運作它時,它将提供與之前相同的輸出,但是會添加一些額外的行:
不錯,請注意,Leiningen指令尚未完成運作,它實際上是在監聽檔案的變化。
從現在開始,你可以随意修改HelloCv的Java代碼中Mat對象的大小。将以下行
替換為
更新的代碼表示Mat對象現在是5×5矩陣,每個像素仍然由一個位元組的整數表示。
然後檢視Leiningen指令所在的終端或控制台,你會看到以下正在更新的輸出:
注意這次列印出的Mat對象是由5行5列組成的。
1.4 使用更好的文本編輯器
到目前為止,你可能一直在使用你自己的文本編輯器輸入代碼,但是想要一個更好一些的OpenCV工作環境。
解決方法
雖然這未必是最好的方案,可能有其他的環境讓你覺得效率更高,但我發現通過很簡單的設定,Github上的Atom編輯器就非常高效。這款編輯器在敲代碼時非常好用。
享受使用Atom工作的主要原因之一是圖檔加載非常快,是以在做與圖像有關的項目時,更新的圖像可以非常快地自動反映到你的螢幕上。據我所知,這是唯一支援圖像顯示的文本編輯器。讓我們看看它是如何工作的!
安裝基本的Atom編輯器很簡單,你隻需到下面的網站下載下傳安裝程式即可:
https://atom.io/Atom不僅是一個很好的編輯器,而且你可以很容易地安裝很多新的插件,以使它更符合你的工作風格。
對于OpenCV,我們想添加三個插件:
■一個通用的內建開發環境(Integrated Development Environment,IDE)插件
■一個Java語言插件,它将使用下面的插件
■用于編輯器内終端的插件
這三個插件如圖1-1~1-3所示。
在底部打開的終端會讓你輸入相同的"lein auto run"指令,是以你不需要額外的指令提示符或者另外的終端視窗來執行Leiningen的自動運作函數。這樣你将能讓所有的代碼都在一個視窗中編寫。
理想情況下,Atom布局看起來如圖1-4或圖1-5所示。
請注意,現在針對Java語言的自動補全功能也已經通過Atom的Java插件得到支援了,是以當你輸入函數名的時候會看到一個下拉菜單列出可用的函數,如圖1-6所示。
最後,對圖像進行的更新,雖然不能被實時地顯示出來,但在儲存檔案時可以看到。如果你在背景打開檔案,會看到檔案在每次儲存都會被重新整理,儲存是通過OpenCV的imwrite函數完成的。
是以,由于有leiningen auto run在背景一直運作,儲存檔案時,compilation/run 循環會被觸發并更新圖像。
圖1-7顯示了即使沒有儲存檔案外的使用者行為,螢幕上的圖像是如何在視覺上更新的。
在本章後續部分,你會看到現在作為參考的内容,即使用submat函數更改Mat對象中部分區域的顔色,這裡先把代碼片段展示出來。
現在你可以開始享受使用OpenCV的所有功能了。我們來使用吧。
1.5 學習OpenCV矩陣對象基礎知識
Mat(矩陣)對象是OpenCV架構的核心,掌握它你可以更加得心應手地使用OpenCV。
讓我們通過幾個核心示例來看看如何建立矩陣對象并檢視它們的内容。
此攻略需要你完成與前幾節相同的配置。
要建立一個每個“點”隻有一個通道的簡單矩陣,通常用到Mat類中以下三個靜态函數中的一個:zeros,eye,ones。
通過表1-2可以更清楚地看到這三個函數的用途。
如果你之前使用過OpenCV(如果還沒有,請相信我),你會記得CV_8UC1是OpenCV對8位無符号字的稱呼,每個像素一個通道,是以最終有3×3即9個值。
正如你所料,它的“堂兄”CV_8UC3給每個像素配置設定了三個通道,是以1×1的Mat對象就具有三個值。在處理RGB圖像時你将經常使用三通道的Mat。它也是加載圖像時的預設格式。
第一個例子簡單地顯示了加載每個像素為單通道的Mat對象的三種方法,以及加載每個像素包含三個通道的Mat對象的一種方法。
最後一個Mat對象mat4每個像素包含三個通道。如果嘗試列印該對象的資訊,你将看到一個包含三個0的數組。
CV_8UC1和CV_8UC3是兩種常見的像素格式,在CvType類中還定義了許多其他的像素格式。
當進行矩陣之間的計算時,可能還需要每個通道為浮點數的矩陣。以下是實作方式:
輸出矩陣:
在許多情況下,你可能并不會從頭建立矩陣,而是從檔案中加載圖像。
1.6 從檔案加載圖像
加載圖像檔案,并把它轉換為Mat對象以進行數字操作。
OpenCV有一個名為imread的簡單函數,用以從檔案中讀取圖像。它通常隻需要圖像在本地檔案系統上的檔案路徑,但同時這個函數還帶有一個預設的類型參數。讓我們看看如何使用不同形式的imread。
工作原理
imread函數位于Imgcodecs類的同名包中。
它的标準用法是簡單地給出檔案的路徑。假設你已從Google搜尋下載下傳了貓咪圖像并将它存儲在images/kittenjpg路徑下(如圖1-8所示),如下代碼給出了如何加載這個圖像:
如果OpenCV可以找到并正确加載貓咪圖像,則輸出以下消息到控制台中:
需要注意的是,如果找不到該檔案,OpenCV也不會抛出任何異常或者報告任何錯誤資訊,而是顯示加載的Mat對象為空,是以沒有行和列:
你可以根據自己的編碼方式,嘗試封裝檢查Mat大小的代碼,以確定可以找到圖像并正确解碼。
這個函數也可以加載灰階圖像(如圖1-9所示),這是通過傳遞另外一個參數控制的。
這個參數取自同一個Imgcodecs類。
在這裡,我們使用IMREAD_GRAYSCALE将圖像強制轉換為灰階圖像并加載到Mat對象中。
除了使用IMREAD_GRAYSCALE外,還可以向imread函數傳遞其他選項來得到特定的處理通道和圖像深度,其中最有用的如表1-3所示。
圖1-10顯示了使用REDUCED_COLOR_8加載得到的圖像。
你可能已經注意到,使用imread加載圖像時不需要提供圖像的格式。OpenCV會根據檔案的擴充名以及檔案中的二進制資訊自動完成相應的圖像解碼工作。
1.7 儲存圖像到檔案
使用OpenCV儲存圖像。
OpenCV有一個同imread函數相對應的用來寫入檔案的函數,函數名是imwrite,也在Imgcodecs類中定義。通常情況下,該函數僅使用本地檔案系統裡指向圖像存儲位置的檔案路徑作為參數,但它也可以使用一些參數來修改圖像存儲的方式。
imwrite函數同imread函數工作原理相似,不同之處是它除了路徑,還需要一個Mat對象來存儲圖像。
第一個代碼片段簡單地實作将以彩色形式加載的貓咪圖像存儲到檔案中。
圖1-11展示了輸出的.jpg圖檔的内容。
現在,當儲存Mat對象時,你也可以僅通過使用一個不同的擴充名來改變存儲格式。例如,想要儲存為便攜式網絡圖形(Portable Network Graphic,PNG)格式,僅需調用imwrite函數時,使用一個不同的擴充名即可。
不需要進行圖像編碼和令人發狂的位元組操作,你輸出的檔案确實是PNG格式。
可以向imwrite函數傳遞參數,最常見的參數是壓縮參數。
例如,按照官方文檔:
■對于JPEG,可以使用CV_IMWRITE_JPEG_QUALITY參數,參數值範圍為0~100(值越大圖像品質越高)。預設值是95。
■對于PNG,可以使用0~9作為壓縮程度的參數值,值越大表示圖像越小且壓縮時間越長。預設值是3。
可以通過使用另一個叫作MatOfInt的OpenCV對象來實作使用壓縮參數壓縮輸出檔案,MatOfInt是一個整型矩陣,或者是一個更簡單的形式,即數組。
上段代碼實作PNG圖檔壓縮。同時,通過檢視檔案大小,實際上你可以發現這個PNG檔案大小至少減少了10%。
1.8 利用子矩陣修剪圖像
隻儲存圖像指定的子區域。
這篇簡短的攻略的主要目标是介紹submat函數。submat的傳回值是一個矩陣對象,内容是原圖的子矩陣或子區域。
讀入一張貓咪圖檔,通過submat來截取我們想要的那部分内容。這個例子使用的貓咪圖檔如圖1-12所示。
當然,可以使用任何一張你喜歡的貓咪圖檔。現在,讓我們使用imread來讀取這個檔案。
根據觀察可知,println輸出了矩陣對象本身的一些資訊。它的大部分資訊與記憶體有關,是以你可以直接通路記憶體,同時它也顯示了這個矩陣對象是否是一個子矩陣。在這個例子中,由于這個矩陣對象是原始圖檔,是以它的isSubmat值是false。
如圖1-13所示,Atom編輯器中的自動補全功能會向你提示不同版本的submat函數。
現在我們使用submat函數的第一種形式,輸入參數是每一行和每一列的起始和終止值。
輸出的對象顯示新建立的矩陣對象确實是一個子矩陣。
你可以像處理普通矩陣對象那樣來處理這個建立的子矩陣,例如可以嘗試儲存它。
由于邊界值是根據原始貓咪圖檔精心挑選的,我們可以得到圖1-14中的漂亮結果。
有一件很好的事情是,當你對子矩陣進行了操作之後,原始矩陣也會受到同樣的影響。例如,你對子矩陣中貓咪的臉進行了模糊處理,并且儲存了整個矩陣(不是子矩陣),那麼就隻有貓咪的臉會變得模糊。具體操作如下所示:
blur是org.opencv.imgproc.Imgproc類中的一個核心函數,它的輸入參數是size對象,用來指明每個像素模糊區域大小,size越大,模糊的效果也越強。
模糊的結果如圖1-15所示,當你仔細看的時候會發現,隻有貓咪的臉部被模糊了,這也是我們之前儲存的子矩陣的位置。
你之前也見到過submat函數的其他定義,還有兩種方法可以獲得子矩陣。
一種是采用兩個Range參數,第一個代表行(y或高度)的範圍,第二個代表列(x或寬度)的範圍,都是使用Range類來建立的。
另一種方法是使用矩形,首先給出左上角的坐标,然後是矩形的大小。
後一種方法最常用,因為它最自然。同時,當在圖檔中檢測物體時,你可以用該物體的包圍框,它的類型是Rect對象。
值得注意的是,修改子矩陣會破壞原矩陣的效果。如果你想把子矩陣改成藍色:
submat3_2.png和submat3_3.png都會變成如圖1-16所示的藍色貓咪臉。
同時原矩陣也會被變成如圖1-17所示的樣子!
這裡想表達的觀點是,無論在何時何地使用submat函數,一定要小心謹慎,通常情況下,它是一個強有力的圖像處理工具。
1.9 從子矩陣生成矩陣
讓我們來學習如何手動地通過多個子矩陣生成一個完整的矩陣。
setTo和copyTo是OpenCV中兩個非常重要的函數。setTo可以将一個矩陣中的所有像素設定為指定的顔色,而copyTo可以将一個已有的矩陣複制到另一個矩陣之中。當使用setTo或者copyTo時,你經常需要與子矩陣打交道,即隻對矩陣中的一部分進行處理。
為了使用setTo,我們會用到OpenCV的Scalar對象來定義顔色,這裡會使用RGB顔色空間的一組值來建立。讓我們來看一下具體是怎麼工作的。
第一個例子使用setTo将多個子矩陣合成一個矩陣,每個子矩陣有不同的顔色。
從彩色子矩陣生成矩陣
首先我們通過RGB值來定義顔色。之前提到過,顔色是通過Scalar對象建立出來的,包含三個整數值,每個值的範圍是0~255。
第一個顔色值代表藍色的深度,第二個值代表綠色的深度,最後一個值代表紅色的深度。為了得到紅色、綠色或者藍色,可以把對應的顔色值設為最高值,即255,其他值設為0。
下面的例子介紹了如何得到紅色、綠色和藍色。
為了定義藍綠色、品紅色和黃色,我們把這些顔色當作RGB的補充色。是以把其他通道設定為最大值255,主通道設定為0。
藍綠色是紅色的補充色,是以紅色值通道被設為0,而另外兩個通道為255:
品紅是綠色的補充色,黃色是藍色的補充色,它們的值如下所示:
我們把顔色都設定好了,現在使用這些對象來建立一個包含所有顔色的矩陣。接下來的setColors方法把輸入的矩陣中的一行填充為主顔色RGB或補充色CMY。
我們來看一下如何使用setTo将子矩陣設定為給定的Scalar顔色。
接下來,我們建立一個包含三個顔色通道的矩陣,并且填充它的第一行和第二行。
結果是一個包含兩行的矩陣,如圖1-18所示,每一行都包含不同顔色的子矩陣。
從圖檔子矩陣生成矩陣
顔色很棒,但是你也許更希望能處理圖像。第二個例子介紹如何使用圖像填充子矩陣。
首先建立一個大小為200×200的矩陣和兩個子矩陣:一個是主矩陣的上部,一個是主矩陣的下部。
然後加載一個圖檔以建立另一個小矩陣,并把它的大小調整為上部(或下部)的子矩陣大小。這裡會引入Imgproc類中的resize函數。
當然,你可以任意選擇其他的圖像。這裡,假設加載的圖像如圖1-19所示。
這個貓咪矩陣被複制到上部子矩陣和下部子矩陣。
請注意,之前設定大小的步驟很關鍵。複制能夠成功,是因為小矩陣和子矩陣的大小是完全相同的,是以複制的時候沒有出現任何問題。
生成的matofpictures.jpg檔案包含兩隻貓咪,如圖1-20所示。
如果你忘了調整小矩陣的大小,那麼複制會徹底失敗,結果可能會是如圖1-21所示的樣子。
1.10 高亮顯示圖像中的物體
一張圖檔中包含一組物體、動物或者形狀,也許是因為你想得到圖像中物體的個數,想把它們高亮顯示出來。
OpenCV提供了一個非常有名的函數叫作Canny,它可以高亮顯示圖像中的線條。本章的後幾節會詳細介紹Canny的用法。我們先使用Java來實作一些簡單的操作。
OpenCV的Canny函數可以檢測灰階矩陣中的輪廓。我們需要做的隻是把輸入的矩陣轉換為灰階圖像,剩下的工作将由Canny完成。
通過Core類中的cvtColor函數,OpenCV可以很容易地改變顔色空間。
假設你有一張工具圖檔,如圖1-22所示。
和往常一樣,我們把圖檔加載到矩陣中。
接下來,使用cvtColor函數來進行顔色轉換,它的輸入包含源矩陣、目标矩陣和目标顔色空間。顔色空間的常量可以在Imgproc類中找到,它們的名字以COLOR_為字首。
使用顔色常量COLOR_RGB2GRAY,可以把矩陣變成黑白兩色。
這個黑白圖像可以被直接送入Canny中。Canny函數包含以下參數:
■源矩陣
■目标矩陣
■低門檻值,使用150.0
■高門檻值,通常是低門檻值的2倍或3倍
■光圈,3~7之間的一個奇數,我們使用3。光圈值越大,被檢測到的輪廓越多
■L2梯度,暫時設定為true
對每一個像素,Canny使用一個卷積矩陣包含一個核心像素和它的鄰居像素,得到一個梯度值。如果梯度值大于高門檻值,那麼它就被檢測為邊界。如果梯度值在高門檻值和低門檻值之間,并且有個高門檻值和它連接配接,那麼它也會被保留。
接下來,我們來調用Canny函數。
輸出的圖檔如圖1-23所示。
為了保護眼睛、節省列印機油墨和樹木資源,有些時候把矩陣中的白色變成黑色、黑色變成白色會讓物體更容易辨認。反色操作可以通過Core類中的bitwise_not函數實作。
當然,也可以把Canny函數用在更多的貓咪圖檔中。圖1-25~1-27展示了同樣的Canny函數用在貓咪圖檔中的效果。
1.11 使用Canny結果作為掩膜
Canny的邊緣檢測非常棒,它的輸出還可以被作為掩膜(mask),用于生成一個精美的藝術化圖檔。
讓我們來嘗試把Canny的結果畫在另一張圖檔上。
當進行複制操作時,可以使用一個叫作掩膜的參數。掩膜是一個單通道的矩陣,值隻包含0和1。
當使用掩膜進行複制時,如果掩膜中的像素值是0的話,源矩陣中的像素就不會被複制,如果值是1的話,源像素就會被複制到目标矩陣中。
在1.10節攻略中,根據bitwise_not函數輸出的結果,我們得到了一個新的矩陣對象。
如果你決定把kittens輸出的話(也許不是一個好主意,因為檔案很大),你會看到一堆0和1,這就是掩膜的制作方法。
現在有了掩膜,我們來建立一個叫作target的白色矩陣,作為copy函數的目标參數。
然後為copy函數加載一個源矩陣,你應該記得,我們需要确定它的大小和copy函數的目标矩陣(也就是target矩陣)的大小一緻。
讓我們來調整背景對象的大小。
這樣我們就準備好進行複制操作了。
輸出的矩陣如圖1-28所示。
接下來你可以回答這個問題:為什麼貓咪是白色的?
正确答案其實是,底層的矩陣在初始化時是純白色的,參照new Mat(..., WHITE)聲明。當掩膜阻礙了一個像素的複制,也就是說掩膜中這個像素對應的值是0時,矩陣原來的顔色就會顯示出來,這裡是白色,這也是圖1-28中的貓咪是白色的原因。你當然可以嘗試一個黑色背景的源矩陣,或者是自己選擇一個圖檔。
在接下來的章節,我們将看到更多的例子。
1.12 使用輪廓進行邊緣檢測
在Canny操作的結果中,希望找到一組可繪制的輪廓,并把它們繪制在矩陣中。
OpenCV中有兩個函數常與Canny函數一同使用:findContours和drawContours。
findContours讀入一個矩陣,并在這個矩陣中查找邊緣,或者說定義形狀的邊界。因為原圖像可能包含許多顔色和亮度的噪聲,你通常需要一個經過預處理的圖檔,即一個由Canny處理過的黑白矩陣。
drawContours讀入findContours的結果,也就是一組輪廓對象,并允許你用具體的特征來繪制這些輪廓,例如繪制線條的粗細和顔色。
如同在解決方法中提到的,OpenCV的findContours函數輸入一個預處理過的圖檔,包含以下參數:
1.預處理過的矩陣
2.用于接收輪廓對象的空隊列(MatOfPoint)
3.一個分層矩陣,你目前可以忽略它,并把它設定為空矩陣
4.輪廓追蹤模式,例如是否建立輪廓之間的關系或傳回所有内容
5.存儲輪廓的近似類型,例如是繪制所有的點還是隻繪制一些關鍵點
第一步,我們把預處理圖檔和追蹤輪廓一起放在自定義的find_contours函數中。
該函數傳回一組檢測到的輪廓,每個輪廓包含一組像素點,用OpenCV的話說,就是一個MatOfPoint對象。
接下來,我們定義一個draw_contours函數,讀入源矩陣來找出第一步中得到的每個輪廓的大小,輸入還包括我們希望用來繪制邊緣的線條粗度。
在OpenCV中繪制輪廓,通常需要一個for循環,并把要繪制的輪廓索引給drawContours函數。
太棒啦,該攻略最核心的部分已經完成,現在你可以運作它了。可以和之前一樣使用貓咪的照片來作為基準輸入圖像。
draw-contours的結果如圖1-2所示。
接下來換一種粗度來繪制輪廓,例如,當粗度是3時,結果會有些許不同,如圖1-30所示,線條更細一些。
從現在開始,我們可以使用結果矩陣作為掩膜進行背景複制。
下面的代碼取自1-11節。該函數讀入一個掩膜,并且用這個掩膜進行複制。
圖1-31顯示了掩膜複制的結果,其中輪廓繪制時的粗度為3。
值得注意的是,第3章将介紹更酷的使用掩膜和背景的方法,用于生成藝術圖檔,這一節攻略暫時告一段落。
1.13 處理視訊流
你希望使用OpenCV來對視訊流進行實時的圖像處理。
Java版本的OpenCV提供了一個videoio包,以及一個特定的VideoCapture對象,它提供了多種方法來直接從連接配接的視訊裝置中讀取矩陣對象。
首先,你會看到如何從視訊裝置中擷取一個特定大小的矩陣對象,然後将矩陣存入檔案中。
通過使用幀(frame),你将看到如何将之前學習到的預處理代碼應用在實時擷取到的圖像中。
拍攝靜止圖檔
首先介紹do_still_captures函數。它的輸入參數是一組需要抓取的幀、每幀間隔的時間以及從哪個camera_id讀入圖像。
camera_id是連接配接到你機器的捕獲裝置索引。通常你會使用0,但是如果你還有其他外接裝置的話,就要選擇對應的camera_id。
首先建立一個 VideoCapture對象,camera_id作為參數。
然後建立一個空的矩陣對象,把它傳入camera.read()函數來讀取資料。
這裡的矩陣對象是你熟悉的标準OpenCV矩陣Mat,于是你也可以應用那些之前學過的變換。
到目前為止,我們先把每一幀存儲好,用時間戳作為檔案名。
完成後,你可以通過VideoCapture對象中的release函數來把相機設定回待機模式。
看看以下代碼是怎麼實作的。
調用建立的函數隻需填入所需參數,接下來從ID為0的裝置中讀取10張圖檔,每間隔1秒拍攝1次。
如圖1-32所示,這10張圖檔被建立在該項目的video檔案夾中。确實,時間過得飛快,現在已經是深夜了。
實時處理
好吧,壞消息是OpenCV的Java封裝不包含将矩陣轉為BufferedImage的明确方法,BufferedImage是Java的graphic包中處理圖像的對象。
這裡不介紹太多細節,假設你需要一個MatToBufferedImage函數來實時處理Java幀,通過把矩陣對象轉換為BufferedImage,即可将它渲染為标準的Java GUI對象。
讓我們快速地寫一個函數,将矩陣轉換為标準的Java BufferedImage。
當你有了這段代碼之後,事情就變得簡單了起來。但你仍然需要另外一段代碼:一個自定義的panel,它繼承了Java的Panel類JPanel。
這個自定義的panel,稱為MatPanel,包含一個需要繪制的矩陣對象。MatPanel繼承Java的JPanel類的方法是,在paint() 函數中直接調用你剛剛見過的函數:MatToBufferedImage。
好了,标準OpenCV包中缺少的代碼已經被實作了,你可以直接建立JFrame來接收矩陣對象。
本攻略的最後一步是使用一段與do_still_captures函數類似的代碼,但并不在幾幀之後停下,你将會寫一個無限循環來處理視訊流。
圖1-33展示了一個日本房間在淩晨1點鐘的實時景象,通過JFrame實時渲染。
顯然,目标是實時處理矩陣對象,對于你來說一個很好的練習是試着生成圖1-34所示的螢幕截圖效果。
答案如下所示,你也應該猜到了,這段代碼隻是将Canny函數應用在視訊讀取的矩陣對象中。
1.14 用Scala寫OpenCV代碼
既然你已經可以使用Java寫一些OpenCV代碼了,并且開始享受它,但此刻你想要使用Scala來減少樣闆代碼。
到目前為止,你使用的目前OpenCV設定可以很容易運作任何為JavaVM編譯的類。是以,如果你能夠編譯Scala類,并且正好有Leiningen插件,那麼剩下的工作就十分相似了。
那意味着通過到目前為止已經使用的Leiningen設定,你僅需要更新project.clj檔案中的項目中繼資料,該檔案存放于幾個地方來確定運作正常。
該工作需要兩步。第一步,添加Scala編譯器和庫;第二步,更新目錄,使Scala代碼檔案可以被找到。
基本設定
project.clj檔案需要在如下重點陳述的幾個地方被更新。
**■項目名稱,當然那是可選的。
■主類,你可以使用同樣的名稱,但如果那樣做,確定使用lein clean指令删除舊的Java代碼。
■接下來添加lein-zinc插件,這是一個集多能于一體的Leiningen插件。
■lein-zinc插件需要在lein執行編譯前觸發,是以我們需要在項目中繼資料中的prep-tasks鍵中添加一步。prep-tasks鍵負責定義在相似指令執行前需執行的任務。
■最後,将Scala庫依賴加入到依賴鍵中。**
更新的project.clj檔案如下。
你為Scala建立的新項目檔案結構應該看上去如圖1-35所示。
就像你看到的,同Java設定相比沒有太大改變,但是需確定你的源檔案現在是在scala檔案夾中。
為了確定所有的檔案都在正确的位置且設定正确,讓我們再一次嘗試一個簡單的OpenCV例子,但這一次使用Scala。
你将像在前面Java示例中做的一樣,加載OpenCV本地庫。如果你在scala對象定義中的任何地方都會調用loadLibrary,它将被JVM當作靜态調用,并且在加載使用Scala最新寫的SimpleOpenCV類時加載庫。
其餘的代碼更像是Java代碼的直譯。
當編譯上述代碼時,Scala源代碼會在目标檔案夾中生成一些Java位元組碼,就像Java代碼生成的方式一樣。
是以,你可以像在Java中做的一樣來運作Scala代碼,或者通過指令行運作:
在螢幕上,控制台輸出預期的OpenCV的3x3矩陣。
圖1-36展示了Scala更新設定元素的全景圖。
模糊
第一個Scala示例的确顯得有點太簡單了,那麼現在讓我們在Scala中試試OpenCV的模糊效果。
就像你看到的,模糊效果在一行中被連續調用多次,可以在同一個矩陣對象上增加模糊效果。
圖1-37中這隻無聊貓咪被模糊成了圖1-38中的模糊無聊貓咪。
你一定已經在本地機器上嘗試了,并且發現Scala設定中兩件十分友好的事情。
編譯時間縮短了一些,并且實際上可以更快地看到你的OpenCV代碼執行。Scala編譯器似乎通過增量代碼變化确定需要的編譯步驟。
此外,盡管靜态導入在Java中已存在,但在Scala中它似乎內建得更加自然。
Canny效果
在更多地減少樣闆代碼的嘗試中,Scala使導入類和方法變得更簡單。
Scala攻略中第三個示例将展示在改變加載的OpenCV矩陣的顔色空間後,如何使用Canny變換。
下面的代碼十分整潔,唯一不足的部分是OpenCV的vconcat函數需要java.util.Array并且無法使用本地Scala對象作為參數,是以你将需要使用名為Arrays.asList的Java函數來替代。
代碼中使用了Canny參數以在這個簡單的藝術空間中輸出一些結果,但這一次并沒有很有效地找出邊緣。圖1-39和圖1-40展示了在加載的貓咪圖像上使用Canny效果處理前/處理後的結果。
為Java編寫的畫輪廓示例也被引入到Scala中并且提供了源碼,位于本書提供的案例源碼庫中。現在,這個示例留給讀者作為一個簡單的練習題。
<p style="text-align:center"></p>
1.15 用Kotlin寫OpenCV代碼
使用Scala寫OpenCV變換程式令人興奮,但現在谷歌正在力推Kotlin語言,你将會非常喜歡使用Kotlin寫OpenCV代碼。
當然,Leiningen中有Kotlin插件。就像Scala設定一樣,你需要再一次在project.clj檔案中更新中繼資料。
你最需要做的是添加Kotlin插件以及通路Kotlin源檔案的路徑。
在project.clj檔案中需要更新的地方同那些在Scala設定需要更新的地方十分相似,并且已在接下來的代碼片段中被高亮标出。
因為Kotlin類是通過插件顯式地編譯到JavaVM位元組碼中,你可以參考那些到目前為止你已經完成編譯的類。
顯而易見,第一個測試是檢驗你是否可以加載一個矩陣對象并且列印它的0和1值。
下面十分簡短的Kotlin代碼片段實作了上述功能。
在你執行通常的Leiningen運作指令之前,需要将First.kt檔案放到Kotlin檔案夾中。
這個指令輸出同樣是必要的,展示了正确建立的OpenCV對象并且将它列印到控制台中。
這是一個簡單的示例。讓我們使用Kotlin和OpenCV來完成更加複雜一點的事情吧。
顔色映射
下面這個新示例展示出如何使用Imgproc類中的applyColorMap函數完成不同顔色映射之間的變換,這個示例完全用Kotlin代碼實作。
就像你掌握的那樣,Kotlin的構造函數調用不需要使用明确的new關鍵字,而且就像在Scala中一樣,可以使用靜态引入方法。
現在你可以從圖1-41中的原始輸入圖像開始看這段代碼的運作效果。
你會看到程式建立了三個檔案,如圖1-42、圖1-43和圖1-44所展示的三個輸出檔案所示。
在Kotlin中,合适的類型轉換看上去有一些挑戰,但是代碼同樣是非常緊湊的,就像在Scala中移除了一些樣闆代碼。
使用者接口
你想要使用Kotlin的一個主要原因是它那不可思議的tornadofx庫,這個庫使在GUI架構JavaFX下的JVM中編寫簡單的使用者接口變得更容易。
這樣的小應用對于給使用者創造調整OpenCV參數的機會并且僞實時地看到結果,是非常有用的。
Kotlin設定
tornadofx庫可以被添加到存在于依賴部分的project.clj檔案中,如下面提取出的片段所示。
由于本攻略的目的是培養你創造性的想法,是以我們不再深入學習如何編寫Kotlin程式以及使用tornadofx庫編寫Kotlin程式。但是你将很快學習到一些如何将這些方法內建到OpenCV中的Kotlin示例。
下面的第一個示例将向你介紹如何引導你的Kotlin代碼顯示一幀中的一副圖像。
仿制使用者接口
一個簡單的tornadofx應用基本遵循了一個結構,即給定的啟動器(Launcher)→應用→視圖,如圖1-45中的流程圖所示。
有了這張圖的概念,我們需要建立三個類。
**■HelloWorld0:UI應用的主視圖
■MyApp0:用來發送給JavaFX啟動器的JavaFX應用對象
■World0:主類,隻會被建立一次,是以使用對象代替類來定義它,以此啟動基于JVM的應用**
一個tornadofx中的視圖由一個根面闆(Root Panel)組成,你可以按照自己的意願定制JavaFX小部件作為根面闆。
**■下面的代碼建立一個單一視圖,該視圖由嵌入在imageview小部件中的圖像組成。
■imageview中圖像的尺寸由定義小部件的子產品設定。
■視圖初始化由init{......}子產品完成,而且由于根對象無法再一次初始化,是以使用神奇的with函數完成。**
這段代碼的其餘部分是标準的tornadofx/javafx樣闆模闆,以此正确啟動基于JavaFX的應用。
如同到目前為止你所完成的那樣,通過如下指令使用Leiningen自動模式運作上述代碼。
你的螢幕上将會出現圖形化的一幀(圖1-46)。
實際上,這段代碼和這一幀有一些不同。在根子產品中,通過在合适的地方插入下面的代碼片段設定了一個标題。你會找到這是在哪裡插入的。
回報按鈕的使用者接口
接下來的示例基于前述示例并且增加了一個按鈕,當按下按鈕,内部計數器會增加,而且計數器的值會實時顯示在螢幕上。
回報值可以通過SimpleIntegerProperty建立,或者通過javafx.beans包中的Simple-
XXXProperty建立。
該回報值可以綁定到小部件上,在接下來的示例中,将會綁定到一個标簽上,是以标簽值與屬性值相同。
按鈕是你可以用來定義一個處理句柄的UI小部件。句柄代碼存在于子產品内部或者一個不同的Kotlin函數中。
根據上述目标和解釋,讓我們開始介紹下面的代碼片段。
運作計數器應用的結果如圖1-47所示。
在點選這個漂亮的按鈕幾次之後,你會得到如圖1-48所示内容。
模糊應用
這些應用很酷,但這看上去像是建立GUI的課程,而且同OpenCV沒有太大關系。
确實如此。
是以,最後一個Kotlin應用基于上述兩個示例,介紹如何建立一個模糊應用,其中模糊程度由回報屬性設定。
你需要在Java環境下的圖像對象和OpenCV環境下的Mat對象來回轉換。下面的示例介紹一種快速轉換的方法,通過使用OpenCV的imencode函數實作,該函數将Mat對象編碼為位元組而無須将它們存儲到檔案中。
這個模糊應用使用了SimpleObjectProperty類型的變量,該變量随着它的圖像化視圖更新而變化。
較長的導入清單有些煩人,但你可能不必為自己自定義的應用加入更多的導入。
通常情況下,Leiningen在檔案改變後為你自動完成全部Kotlin編譯工作,模糊應用效果如圖1-49所示。
當你點選增加(increment)按鈕後,貓咪圖像變得越來越模糊;當你點選減小(decrement)按鈕後,它變得越來越清晰。
在本書的代碼樣本中有更多的tornadofx示例,是以無須猶豫,找出它們來練習。你可能會通過OpenCV方法獲得更多的UI。例如一個圖像拖拽面闆,圖像可以根據你的意願被模糊處理。那聽起來不再是無法實作的,是嗎?
第1章寫滿了攻略,從在基于JavaVM的OpenCV中建立一個小項目開始,逐漸學習更加複雜的圖像操作示例,最開始使用Java,最終熟練使用JavaVM運作環境,以使用Scala代碼以及含有令人印象深刻的tornadofx庫的Kotlin代碼。
介紹origami庫的大門已經打開,該庫是為OpenCV設計的Clojure封裝。該環境帶給你更加簡潔的代碼并且更具有互動性,以此來嘗試新事物并且變得更加有創造性。是時候興奮起來了。
**我對未來有興奮的感覺,而我不知道那看上去會是什麼樣。但是無論如何,未來将會是我創造的樣子。
——Amanda Lindhout**