安裝OpenCV以及運行示例很有趣,但是在這一階段,我們希望以
自己的方式來嘗試一下。本章將介紹OpenCV的I/O功能,還將討論項目
的概念,並開始對該項目進行面向對象設計,並在後續章節中繼續對
該項目進行充實。
首先,我們來看一下I/O的功能和設計模式,我們將以制作三明治
的方式構建項目——由外而內。面包切片和塗抹,或者端點和黏合,
都是添加餡料和算法之前的工作。之所以選擇這種方式是因為計算機
視覺通常是外向的——它專注於計算機之外的真實世界——我們希望
通過一個共同接口將所有的後續算法工作都應用於真實世界。
本章將介紹以下主題:
·從圖像文件、視頻文件、攝像頭設備或內存中的原始數據字節
讀取圖像。
·將圖像寫入圖像文件或視頻文件。
·在NumPy數組中處理圖像數據。
·在窗口中顯示圖像。
·處理鍵盤和鼠標輸入。
·實現基於面向對象設計的應用程序。
2.1 技術需求
本章使用了Python、OpenCV以及NumPy。安裝說明請參閱第1章。
本章的完整代碼可以在本書GitHub庫(網址為
https://github.com/PacktPublishing/Learning-OpenCV-4-
Computer-Vision-with-Python-Third-Edition)的chapter02文件中
找到。
2.2 基本I/O腳本
大多數計算機視覺(Computer Vision,CV)應用程序需要獲取圖
像作為輸入。大多數計算機視覺應用程序還會生成圖像作為輸出。交
互式計算機視覺應用程序可能需要把攝像頭作為輸入源,還需要將窗
口作為輸出目標。但是其他可能的源和目標包括圖像文件、視頻文件
以及原始字節。例如,如果把過程式圖形合成到應用程序中,那麼原
始字節可能通過網絡連接進行傳輸,也可能由算法生成。我們來看看
每一種可能性。
2.2.1 讀取/寫入圖像文件
OpenCV提供了imread函數來從文件加載圖像,也提供了imwrite函
數來將圖像寫入文件。這些函數支持靜態圖像(非視頻)的各種文件
格式。支持的格式各不相同——在OpenCV的自定義構建中可以添加或
刪除某些格式——但是,通常BMP、PNG、JPEG和TIFF都是所支持的格
式。
我們來研究一下在OpenCV和NumPy中圖像表示的解剖結構。一幅圖
像就是一個多維數組,有列像素和行像素,每個像素都有一個值。對
於不同類型的圖像數據,像素值可以使用不同的格式。例如,通過簡
單地創建一個二維NumPy數組,可以從頭開始創建一幅3×3的黑色正方
形圖像:
如果將這幅圖像打印到控制台,獲得的結果如下所示:
這裡,每個像素都用一個8位整數表示,這意味著每個像素的值都
在0~255的范圍內,其中0表示黑色,255表示白色,中間的值表示灰
色。這是一幅灰度圖像。
現在,我們使用cv2.cvtColor函數把這幅圖像轉換成藍–綠–紅
(Blue-Green-Red,BGR)格式:
我們來看圖像是如何變化的:
如你所見,現在每個像素都用一個三元數組表示,每個整數分別
表示三個顏色通道(B、G和R)中的一個。HSV之類的其他常見顏色模
型的表示方法也類似,只是取值范圍不同。例如,HSV顏色模型的色調
值的范圍是0~180。
有關顏色模型的更多內容,請參閱第3章,尤其是3.2節。
通過查看shape屬性,你可以查看圖像的結構,shape屬性返回
行、列和通道數(如果有多個通道的話)。
考慮如下示例:
上述代碼將打印(5,3),表示我們有一幅5行3列的灰度圖像。如
果將該圖像轉換成BGR格式,shape將是(5,3,3),表示每個像素有3
個通道。
圖像可以從一種文件格式加載並保存為另一種格式。例如,把一
幅圖像從PNG轉換為JPEG:
OpenCV的Python模塊命名為cv2,盡管我們使用的是OpenCV
4.x而非OpenCV 2.x。以前,OpenCV有兩個Python模塊:cv2和cv。cv封
裝了用C實現的OpenCV的一個舊版本。目前,OpenCV只有cv2 Python
模塊,該模塊封裝了用C++實現的OpenCV當前版本。
默認情況下,imread返回BGR格式的圖像,即使該文件使用的是灰
度格式。BGR表示與紅–綠–藍(Red-Green-Blue,RGB)相同的顏色
模型,只是字節順序相反。
我們還可以指定imread的模式,所支持的選項包括:
·cv2.IMREAD_COLOR:該模式是默認選項,提供3通道的BGR圖
像,每個通道一個8位值(0~255)。
·cv2.IMREAD_GRAYSCALE:該模式提供8位灰度圖像。
·cv2.IMREAD_ANYCOLOR:該模式提供每個通道8位的BGR圖
像或者8位灰度圖像,具體取決於文件中的元數據。
·cv2.IMREAD_UNCHANGED:該模式讀取所有的圖像數據,包
括作為第4通道的α或透明度通道(如果有的話)。
·cv2.IMREAD_ANYDEPTH:該模式加載原始位深度的灰度圖
像。例如,如果文件以這種格式表示一幅圖像,那麼它提供每個通道
16位的一幅灰度圖像。
·cv2.IMREAD_ANYDEPTH|cv2.IMREAD_COLOR:該組合模式
加載原始位深度的BGR彩色圖像。
·cv2.IMREAD_REDUCED_GRAYSCALE_2:該模式加載的灰度
圖像的分辨率是原始分辨率的1/2。例如,如果文件包括一幅640×480
的圖像,那麼它加載的是一幅320×240的圖像。
·cv2.IMREAD_REDUCED_COLOR_2:該模式加載每個通道8位
的BGR彩色圖像,分辨率是原始圖像的1/2。
·cv2.IMREAD_REDUCED_GRAYSCALE_4:該模式加載灰度圖
像,分辨率是原始圖像的1/4。
·cv2.IMREAD_REDUCED_COLOR_4:該模式加載每個通道8位
的彩色圖像,分辨率是原始圖像的1/4。
·cv2.IMREAD_REDUCED_GRAYSCALE_8:該模式加載灰度圖
像,分辨率是原始圖像的1/8。
·cv2.IMREAD_REDUCED_COLOR_8:該模式加載每個通道8位
的彩色圖像,分辨率為原始圖像的1/8。
舉個例子,我們將一個PNG文件加載為灰度圖像(在此過程中會丟
失所有顏色信息),再將其保存為一個灰度PNG圖像:
除非是絕對路徑,否則圖像的路徑都是相對於工作目錄(Python
腳本的運行路徑)的,因此在前面的例子中,MyPic.png必須在工作目
錄中,否則將找不到該圖像。如果你希望避免對工作目錄的假設,可
以使用絕對路徑,比如Windows上的
C:\Users\Joe\Pictures\MyPic.png、Mac上
的/Users/Joe/Pictures/MyPic.png,或者Linux上
的/home/joe/pictures/MyPic.png。
imwrite()函數要求圖像為BGR格式或者灰度格式,每個通道具有
輸出格式可以支持的特定位數。例如,BMP文件格式要求每個通道8
位,而PNG允許每個通道8位或16位。
2.2.2 在圖像和原始字節之間進行轉換
從概念上講,一個字節就是0~255范圍內的一個整數。目前,在
實時圖形應用程序中,像素通常由每個通道一個字節來表示,但是也
可以使用其他表示方式。
OpenCV圖像是numpy.array類型的二維或者三維數組。8位灰度圖
像是包含字節值的一個二維數組。24位的BGR圖像是一個三維數組,也
包含字節值。我們可以通過使用類似於image[0,0]或者image[0,0,0]
的表達式來訪問這些值。第一個索引是像素的y坐標或者行,0表示頂
部。第二個索引是像素的x坐標或者列,0表示最左邊。第三個索引
(如果有的話)表示一個顏色通道。可以用下面的笛卡兒坐標系可視
化數組的三維空間(見圖2-1)。
例如,在左上角為白色像素的8位灰度圖像中,image[0,0]是
255。在左上角為藍色像素的24位(每個通道8位)BGR圖像中,
image[0,0]是[255,0,0]。
假設圖像的每個通道有8位,我們可以將其強制轉換為標准的
Python bytearray對象(一維的):
相反,假設bytearray以一種合適的順序包含字節,我們對其進行
強制轉換後再將其變維,可以得到一幅numpy.array類型的圖像:
舉個更完整的例子,我們將包含隨機字節的bytearray轉換為灰度
圖像和BGR圖像:
此處,我們使用Python的標准os.urandom函數生成隨機的原始
字節,然後再將其轉換成NumPy數組。請注意,也可以使用像
numpy.random.randint(0,256,120000).reshape(300,400)這樣的語句直接(而
且更有效)生成隨機NumPy數組。我們使用os.urandom的唯一原因是:
這有助於展示原始字節的轉換。
運行這個腳本之後,在腳本目錄中應該有一對隨機生成的圖像:
RandomGray.png和RandomColor.png。
圖2-2是RandomGray.png的一個例子(你得到的結果很可能會有所
不同,因為這是隨機生成的)。
類似地,圖2-3是RandomColor.png的一個例子。
既然我們已經對數據如何形成圖像有了一個更好的理解,那麼就
可以開始對其執行基本操作了。
2.2.3 基於numpy.array訪問圖像數據
我們已經知道在OpenCV中加載圖像最簡單(也是最常見)的方法
是使用imread函數。我們還知道這將返回一幅圖像,它實際上是一個
數組(是二維還是三維取決於傳遞給imread的參數)。
numpy.array類對數組操作進行極大的優化,它允許某些類型的批
量操作,而這些操作在普通Python列表中是不可用的。這些類型的
numpy.array都是OpenCV中特定於數組類型的操作,對於圖像操作來說
很方便。但是,我們還是從一個基本的例子開始,逐步探討圖像操
作。假設你想操作BGR圖像的(0,0)坐標處的像素,並將其轉換成白
色像素:
如果將修改後的圖像保存到文件後再查看該圖像,你會在圖像的
左上角看到一個白點。當然,這種修改並不是很有用,但是它顯示了
某種修改的可能性。現在,我們利用numpy.array的功能在數組上執行
變換的速度比普通的Python列表要快得多。
假設你想更改某一特定像素的藍色值,例如(150,120)坐標處的
像素。numpy.array類型提供了一個方便的方法item,它有三個參數:
x(或者left)位置、y(或者top)位置以及數組中(x,y)位置的索
引(請記住,在BGR圖像中,某個特定位置處的數據是一個三元數組,
包含按照B、G和R順序排列的值),並返回索引位置的值。另一個方法
itemset可以將某一特定像素的特定通道的值設置為指定的值。
itemset有兩個參數:三元組(x、y和索引)以及新值。
在下面的例子中,我們將(150,120)處的藍色通道值從其當前值
更改為255:
對於修改數組中的單個元素,itemset方法比我們在本節第一個例
子中看到的索引語法要快一些。
同樣,修改數組的一個元素本身並沒有太大意義,但是它確實打
開了一個充滿可能性的世界。然而,就性能而言,這只適合於感興趣
的小區域。當需要操作整個圖像或者感興趣的大區域時,建議使用
OpenCV的函數或者NumPy的數組切片。NumPy的數組切片允許指定索引
的范圍。我們來考慮使用數組切片來操作顏色通道的一個例子。將一
幅圖像的所有G(綠色)值都設置為0非常簡單,如下面的代碼所示:
這段代碼執行了一個相當重要的操作,而且很容易理解。相關的
代碼行是最後一行,它指示程序從所有行和列中獲取所有像素,並把
綠色值(在三元BGR數組的一個索引處)設置為0。如果顯示此圖像,
你會注意到綠色完全消失了。
通過使用NumPy的數組切片訪問原始像素,我們可以做一些有趣的
事情,其中之一是定義感興趣區域(Region Of Interest,ROI)。一
旦定義了感興趣區域,就可以執行一系列的操作了。例如,可以把這
個區域綁定到一個變量,定義第二個區域,將第一個區域的值賦給第
二個區域(從而將圖像的一部分復制到圖像的另一個位置):
確保兩個區域在大小上一致很重要。如果大小不一致,NumPy會
(立刻)控訴這兩個形狀不匹配。
最後,我們可以訪問numpy.array的屬性,如下列代碼所示:
這三個屬性的定義如下:
·shape:描述數組形狀的一個元組。對於圖像,它(依次)包括
高度、寬度、通道數(如果是彩色圖像的話)。shape元組的長度是確
定圖像是灰度的還是彩色的一種有用方法。對於灰度圖像,
len(shape)==2,對於彩色圖像,len(shape)==3。
·size:數組中的元素數。對於灰度圖像,這和像素數是一樣的。
對於BGR圖像,它是像素數的3倍,因為每個像素都由3個元素(B、G
和R)表示。
·dtype:數組元素的數據類型。對於每個通道8位的圖像,數據類
型是numpy.uint8。
總之,強烈建議你在使用OpenCV時,了解NumPy的一般情況以及
numpy.array的特殊情況。這個類是Python中使用OpenCV進行所有圖像
處理的基礎。
2.2.4 讀取/寫入視頻文件
OpenCV提供了VideoCapture和VideoWriter類,支持各種視頻文件
格式。支持的格式取決於操作系統和OpenCV的構建配置,但是通常情
況下,假設支持AVI格式是安全的。通過它的read方法,VideoCapture
對象可以依次查詢新的幀,直到到達視頻文件的末尾。每一幀都是一
幅BGR格式的圖像。
相反,圖像可以傳遞給VideoWriter類的write方法,該方法將圖
像添加到VideoWriter的文件中。我們來看一個例子,從一個AVI文件
讀取幀,再用YUV編碼將其寫入另一個文件:
VideoWriter類的構造函數的參數值得特別注意。必須指定一個視
頻文件的名稱。具有此名稱的所有之前存在的文件都將被覆蓋。還必
須指定一個視頻編解碼器。可用的編解碼器因系統而異。支持的選項
可能包括以下內容:
·0:這個選項表示未壓縮的原始視頻文件。文件擴展名應該
是.avi。
·cv2.VideoWriter_fourcc('I','4','2','0'):這個選項表示未壓縮的YUV編
碼,4:2:0色度抽樣。這種編碼是廣泛兼容的,但是會產生大的文件。文
件擴展名應該是.avi。
·cv2.VideoWriter_fourcc('P','I','M','1'):這個選項是MPEG-1。文件擴
展名應該是.avi。
·cv2.VideoWriter_fourcc('X','V','I','D'):這個選項是一種相對較舊的
MPEG-4編碼。如果想限制生成的視頻大小,這是一個不錯的選項。文
件擴展名應該是.avi。
·cv2.VideoWriter_fourcc('M','P','4','V'):這個選項是另一種相對較舊
的MPEG-4編碼。如果想限制生成的視頻大小,這是一個不錯的選項。
文件擴展名應該是.mp4。
·cv2.VideoWriter_fourcc('X','2','6','4'):這個選項是一種相對較新的
MPEG-4編碼。如果想限制生成的視頻大小,這可能是最佳的選項。文
件擴展名應該是.mp4。
·cv2.VideoWriter_fourcc('T','H','E','O'):這個選項是Ogg Vorbis。文
件擴展名應該是.ogv。
·cv2.VideoWriter_fourcc('F','L','V','1'):這個選項表示Flash視頻。文
件擴展名應該是.flv。
幀率和幀大小也必須指定。因為我們是從另一個視頻復制的,所
以這些屬性可以從VideoCapture類的get方法讀取。
2.2.5 捕捉攝像頭幀
攝像頭幀流也可以用VideoCapture對象來表示。但是,對於攝像
頭,我們通過傳遞攝像頭設備索引(而不是視頻文件名稱)來構造
VideoCapture對象。我們來考慮下面這個例子,它從攝像頭抓取10秒
的視頻,並將其寫入AVI文件。代碼與2.2.4節的示例(從視頻文件獲
取的,而不是從攝像頭中獲取的)類似,更改的內容標記為粗體:
對於某些系統上的一些攝像頭,
cameraCapture.get(cv2.CAP_PROP_FRAME_WIDTH)和
cameraCapture.get(cv2.CAP_PROP_FRAME_HEIGHT)可能會返回不准確
的結果。為了更加確定圖像的實際大小,可以先抓取一幀,再用像
h,w=frame.shape[:2]這樣的代碼來獲得圖像的高度和寬度。有時,你可
能會遇到攝像頭在開始產生大小穩定的好幀之前,產生一些大小不穩
定的壞幀的情況。如果你關心的是如何防范這種情況,在開始捕捉會
話時你可能想要讀取並忽略一些幀。
可是,在大多數情況下,VideoCapture的get方法不會返回攝像頭
幀率的准確值,通常會返回0。
http://docs.opencv.org/modules/highgui/doc/reading_and_writin
g_images_and_video.html上的官方文檔警告如下:
當查詢VideoCapture實例使用的後端不支持的屬性時,返回值為0。
注意:
讀/寫屬性涉及許多層。沿著這條鏈可能會發生一些意想不到的結
果[sic]。
VideoCapture->API Backend->Operating System->DeviceDriver-
>Device Hardware
返回值可能與設備實際使用的值不同,也可能使用設備相關規則
(例如,步長或者百分比)對其進行編碼。有效的行為取決於[sic]設備
驅動程序和API後端。
要為攝像頭創建合適的VideoWriter類,我們必須對幀率做一個假
設(就像前面代碼中所做的那樣),或者使用計時器測量幀率。後一
種方法更好,我們將在本章後面對其進行介紹。
當然,攝像頭數量及其順序取決於系統。可是,OpenCV不提供任
何查詢攝像頭數量或者攝像頭屬性的方法。如果用無效的索引構造
VideoCapture類,VideoCapture類將不會產生任何幀,它的read方法
將返回(False,None)。要避免試圖從未正確打開的VideoCapture對象
檢索幀,你可能想先調用VideoCapture.isOpened方法,返回一個布爾
值。
當我們需要同步一組攝像頭或者多攝像頭相機(如立體攝像機)
時,read方法是不合適的。我們可以改用grab和retrieve方法。對於
一組(兩台)攝像機,可以使用類似於下面的代碼:
2.2.6 在窗口中顯示圖像
OpenCV中一個最基本的操作是在窗口中顯示圖像。這可以通過
imshow函數實現。如果你有任何其他GUI框架背景,那麼可能認為調用
imshow來顯示圖像就足夠了。可是,在OpenCV中,只有當調用另一個
函數waitKey時,才會繪制(或者重新繪制)窗口。後一個函數抽取窗
口事件隊列(允許處理各種事件,比如繪圖),並且它返回用戶在指
定的超時時間內輸入的任何鍵的鍵碼。在某種程度上,這個基本設計
簡化了開發使用視頻或網絡攝像頭輸入的演示程序的任務,至少開發
人員可以手動控制新幀的獲取和顯示。
下面是一個非常簡單的示例腳本,用於從文件中讀取圖像,並對
其進行顯示:
imshow函數有兩個參數:顯示圖像的窗口名稱以及圖像自己的名
稱。我們將在2.2.7節中對waitKey進行更詳細的介紹。
恰如其名,destroyAllWindows函數會注銷由OpenCV創建的所有窗
口。
2.2.7 在窗口中顯示攝像頭幀
OpenCV允許使用namedWindow、imshow和destroyWindow函數來創
建、重新繪制和注銷指定的窗口。此外,任何窗口都可以通過waitKey
函數捕獲鍵盤輸入,通過setMouseCallback函數捕獲鼠標輸入。我們
來看一個例子,展示從實時攝像頭獲取的幀:
waitKey的參數是等待鍵盤輸入的毫秒數,默認情況下為0,這是
一個特殊的值,表示無窮大。返回值可以是-1(表示未按下任何
鍵),也可以是ASCII鍵碼(如27表示Esc)。有關ASCII鍵碼的列表,
請參閱http://www.asciitable.com/。另外,請注意Python提供了一
個標准函數ord,可以將字符轉換成ASCII鍵碼。例如,ord('a')返回
97。
同樣,請注意,OpenCV的窗口函數和waitKey是相互依賴的。
OpenCV窗口只在調用waitKey時更新。相反,waitKey只在OpenCV窗口
有焦點時才捕捉輸入。
傳遞給setMouseCallback的鼠標回調應有5個參數,如代碼示例所
示。把回調的param參數設置為setMouseCallback的第3個可選參數,
默認情況下為0。回調的事件參數是以下操作之一:
·cv2.EVENT_MOUSEMOVE:這個事件指的是鼠標移動。
·cv2.EVENT_LBUTTONDOWN:這個事件指的是按下左鍵時,
左鍵向下。
·cv2.EVENT_RBUTTONDOWN:這個事件指的是按下右鍵時,
右鍵向下。
·cv2.EVENT_MBUTTONDOWN:這個事件指的是按下中間鍵
時,中間鍵向下。
·cv2.EVENT_LBUTTONUP:這個事件指的是釋放左鍵時,左鍵
回到原位。
·cv2.EVENT_RBUTTONUP:這個事件指的是釋放右鍵時,右鍵
回到原位。
·cv2.EVENT_MBUTTONUP:這個事件指的是釋放中間鍵時,中
間鍵回到原位。
·cv2.EVENT_LBUTTONDBLCLK:這個事件指的是雙擊左鍵。
·cv2.EVENT_RBUTTONDBLCLK:這個事件指的是雙擊右鍵。
·cv2.EVENT_MBUTTONDBLCLK:這個事件指的是雙擊中間
鍵。
鼠標回調的flag參數可能是以下事件的一些按位組合:
·cv2.EVENT_FLAG_LBUTTON:這個事件指的是按下左鍵。
·cv2.EVENT_FLAG_RBUTTON:這個事件指的是按下右鍵。
·cv2.EVENT_FLAG_MBUTTON:這個事件指的是按下中間鍵。
·cv2.EVENT_FLAG_CTRLKEY:這個事件指的是按下Ctrl鍵。
·cv2.EVENT_FLAG_SHIFTKEY:這個事件指的是按下Shift鍵。
·cv2.EVENT_FLAG_ALTKEY:這個事件指的是按下Alt鍵。
可是,OpenCV不提供任何手動處理窗口事件的方法。例如,單擊
窗口關閉按鈕不能停止應用程序。因為OpenCV的事件處理和GUI功能有
限,許多開發人員更喜歡將其與其他應用程序框架集成。在本章的2.4
節中,我們將設計一個抽象層來幫助OpenCV與應用程序框架集成。
2.3 項目Cameo(人臉跟蹤和圖像處理)
通常,通過一種烹饪書式的方法研究OpenCV,這種方法涵蓋了很
多算法,但是沒有涉及高級應用程序開發。在某種程度上,這種方法
是可以理解的,因為OpenCV的潛在應用非常多樣化。OpenCV廣泛應用
於各種各樣的應用,如照片/視頻編輯器、運動控制游戲、機器人的人
工智能,或者我們記錄參與者眼球運動的心理學實驗等。在這些不同
的用例中,我們能真正研究一組有用的抽象嗎?
本書的作者相信我們可以,而且越早開始抽象,學習效果越好。
我們將圍繞單個應用程序構建許多OpenCV示例,但是在每個步驟中,
我們將設計一個可擴展且可重用的應用程序組件。
我們將開發一個交互式應用程序,對攝像頭輸入進行實時的人臉
跟蹤和圖像處理。這種類型的應用程序涵蓋了OpenCV的各種功能,而
且創建一個高效且有效的實現對我們來說是一種挑戰。
具體來說,我們的應用程序將實時合並人臉。給定2個攝像頭輸入
流(或者預錄制的視頻輸入),應用程序將把一個流的人臉疊加到另
一個流的人臉上。將濾鏡和畸變應用到這個混合的場景中,將會給人
一種統一的感覺。用戶應該有進入另一個環境和角色參與現場表演的
體驗。這種類型的用戶體驗在像迪士尼樂園這樣的游樂園中很受歡
迎。
在這樣的應用程序中,用戶會立刻注意到缺陷,如低幀率或者跟
蹤不准確等。為了達到最好的效果,我們將嘗試使用傳統成像和深度
成像的幾種方法。
我們將應用程序命名為“Cameo”。Cameo(在珠寶中)是一個人
的小肖像,或者(在電影中)是由名人扮演的、非常短暫的一個角
色。
2.4 Cameo:面向對象的設計
可以用純過程式風格編寫Python應用程序。通常,這是通過小型
應用程序(例如前面討論過的基本I/O腳本)實現的。但是,從現在開
始,我們將經常使用面向對象的風格,因為面向對象促進了模塊化和
可擴展性。
從對OpenCV的I/O功能的概述中,我們知道不管源圖像或者目標圖
像是什麼,所有圖像都是相似的。不管獲取的圖像流是什麼,或者將
其作為輸出發送到哪裡,我們都可以對這個流的每一幀應用相同的特
定於應用程序的邏輯。在使用多個I/O流的應用程序(例如Cameo)
中,I/O代碼和應用程序代碼的分離變得特別方便。
我們將創建的類命名為CaptureManager和WindowManager,作為
I/O流的高級接口。應用程序代碼可以使用CaptureManager讀取新幀,
也可以將每一幀分派給一個或多個輸出,包括靜態圖像文件、視頻文
件和窗口(通過WindowManager類)。WindowManager類允許應用程序
代碼以面向對象風格處理窗口和事件。
CaptureManager和WindowManager都是可擴展的。我們可以實現不
依賴OpenCV的I/O。
2.4.1 基於managers.CaptureManager提取視頻流
正如我們所看到的,OpenCV可以獲取、顯示和記錄來自視頻文件
或來自攝像頭的圖像流,但是在每種情況下都會有一些特殊考慮的事
項。CaptureManager類提取了一些差異並提供了一個更高級的接口,
將圖像從獲取流分發到一個或多個輸出——靜態圖像文件、視頻文
件,或者窗口。
CaptureManager對象是由VideoCapture對象初始化的,並擁有
enterFrame和exitFrame方法,通常應該在應用程序主循環的每次迭代
中調用這兩個方法。在調用enterFrame和exitFrame之間,應用程序可
以(任意次)設置一個channel屬性並獲得一個frame屬性。channel屬
性初始為0,只有多攝像頭相機使用其他值。frame屬性是在調用
enterFrame時,對應於當前通道狀態的一幅圖像。
CaptureManager類還擁有可以在任何時候調用的writeImage、
startWriting Video和stopWritingVideo方法。實際的文件寫入被推
遲到exitFrame。同樣,在執行exitFrame方法期間,可以在窗口中顯
示frame,這取決於應用程序代碼將WindowManager類作為
CaptureManager構造函數的參數提供,還是通過設置
previewWindowManager屬性提供。
如果應用程序代碼操作frame,那麼將在記錄文件和窗口中體現這
些操作。CaptureManager類有一個構造函數參數和一個名為
shouldMirrorPreview的屬性,如果想要在窗口中鏡像(水平翻轉)
frame,但不記錄在文件中,那麼此屬性應該為True。通常,在面對攝
像頭時,用戶更喜歡鏡像實時攝像頭回傳信號。
回想一下,VideoWriter對象需要一個幀率,但是OpenCV沒有提供
任何可靠的方法來為攝像頭獲取准確的幀率。CaptureManager類通過
使用幀計數器和Python的標准time.time函數來解決此限制,如有必要
還會估計幀率。這種方法並非萬無一失。取決於幀率的波動和依賴於
系統的time.time實現,估計的准確率在某些情況下可能仍然很糟糕。
但是,如果部署到未知的硬件,這也比只假設用戶攝像頭有某個特定
的幀率要好。
我們創建一個名為managers.py的文件,該文件將包含
CaptureManager實現。這個實現非常很長,所以我們將分成幾個部分
介紹:
(1)首先,添加導入和構造函數,如下所示:
2)接下來,為CaptureManager的屬性添加下面的getter和
setter方法:
請注意,大多數member變量是非公共的,由變量名稱中下劃線前
綴所示,如self._enteredFrame。這些非公共變量與當前幀的狀態和
任何文件的寫入操作相關。如前所述,應用程序代碼只需要配置一些
內容,這些內容是作為構造函數參數和可設置的公共屬性(攝像頭通
道、窗口管理器以及鏡像攝像頭預覽的選項)實現的。
本書假設讀者對Python有一定的了解,但是如果你對這些@注釋
(例如@property)感到困惑,請參考有關decorator的Python文檔,
decorator是Python語言的內置特性,允許函數被另一個函數封裝,通
常用來在應用程序的幾個地方應用用戶定義的行為。具體來說,可以
在
https://docs.python.org/3/reference/compound_stmts.html#gramm
ar-token-decorator查看相關文檔。
Python沒有強制使用非公共的成員變量的概念,但是在開發人
員想要將變量視為非公共的情況下,通常會看到單下劃線前綴(_)或
者雙下劃線前綴(__)。單下劃線前綴只是一種約定,表示應該將變
量視為受保護的(僅在類及其子類中訪問)。雙下劃線前綴實際上會
導致Python解釋器重命名變量,這樣MyClass.__myVariable就變成了
MyClass._MyClass__myVariable。這被稱為名稱重整(非常恰當)。按照
慣例,應該將這樣的變量視為私有的(只能在類內訪問,不能在子類
中訪問)。相同的前綴,具有相同的意義,可以應用於方法和變量。
(3)將enterFrame方法添加到managers.py:
請注意,enterFrame的實現只(同步地)抓取一幀,而來自通道
的實際檢索被推遲到frame變量的後續讀取。
(4)接下來,把exitFrame方法添加到managers.py:
exitFrame的實現從當前通道獲取圖像,估計幀率,通過窗口管理
器(如果有的話)顯示圖像,並完成將圖像寫入文件的所有掛起請
求。
(5)其他幾種方法也適用於文件的寫入。將下列名為
writeImage、startWriting Video和stopWritingVideo的公共方法的
實現添加到managers.py:
上述方法只更新了文件寫入操作的參數,實際的寫入操作被推遲
到exitFrame的下一次調用。
(6)在本節的前面,我們看到exitFrame調用了一個名為
_writeVideoFrame的輔助方法。把下面的_writeVideoFrame實現添加
到managers.py:
上述方法創建或添加視頻文件的方式應該與之前的腳本相似(請
參考2.2.4節)。但是,在幀率未知的情況下,我們在捕獲會話開始
時,跳過一些幀,這樣就有時間構建幀率的估計。
我們對CaptureManager的實現就結束了。盡管CaptureManager的
實現依賴於VideoCapture,我們可以完成不使用OpenCV作為輸入的其
他實現。例如,我們可以創建用套接字連接實例化的子類,將其字節
流解析為圖像流。另外,我們還可以使用第三方攝像頭庫創建子類,
並提供與OpenCV不同的硬件支持。但是,對於Cameo,當前的實現就足
夠了。
2.4.2 基於managers.WindowManager提取窗口和鍵盤
正如我們所見,OpenCV提供了一些函數用於創建、撤銷窗口,顯
示圖像以及處理事件。這些函數不是窗口類的方法,因而要求將窗口
名稱作為參數傳遞。因為這個接口不是面向對象的,所以與OpenCV的
一般風格不一致。而且,它不太可能與我們最終想要使用的(而不是
OpenCV的)其他窗口或者事件處理接口兼容。
為了面向對象和適應性,我們將這個功能抽象成具有
createWindow、destroy Window、show和processEvents方法的
WindowManager類。作為一個屬性,WindowManager有一個名為
keypressCallback的函數,在響應按鍵時可以從processEvents調用
(如果不是None的話)。keypressCallback對象必須是一個接受單個
參數(尤其是ASCII鍵碼)的函數。
我們將WindowManager的實現添加到managers.py。該實現首先定
義下列類聲明和__init__方法:
該實現接著使用下面的方法來管理窗口及其事件的生命周期:
當前的實現只支持鍵盤事件,對於Cameo足夠了。但是,我們也可
以修改Window Manager來支持鼠標事件。例如,類接口可以擴展為包
含mouseCallback屬性(和可選的構造函數參數),但是其他方面保持
不變。使用OpenCV之外的事件框架,我們可以通過添加回調屬性以同
樣的方式支持其他事件類型。
2.4.3 基於cameo.Cameo應用所有內容
我們的應用程序由帶有兩個方法(run和onKeypress)的Cameo類
表示。在初始化時,Cameo對象創建了一個WindowManager對象(將
onKeypress作為一個回調),以及一個使用攝像頭(具體來說,是一
個cv2.VideoCapture對象)和同一WindowManager對象的
CaptureManager對象。在調用run時,應用程序執行一個主循環,並在
這個主循環中處理幀和事件。
作為事件處理的結果,可能會調用onKeypress。空格鍵會產生一
個屏幕截圖,選項卡(Tab)鍵會使屏幕播放(視頻錄制)開始/停
止,Esc鍵會使應用程序退出。
在與managers.py相同的目錄中,創建一個名為cameo.py的文件,
並在此實現Cameo類:
(1)首先,實現下面的import語句和__init__方法:
(2)接下來,添加以下run()方法的實現:
(3)下面是為完成Cameo類實現的onKeypress()方法:
(4)最後,添加一個__main__塊來實例化並運行Cameo,如下所
示:
在運行應用程序時,請注意實時攝像頭回傳信號是鏡像的,而屏
幕截圖和屏幕播放則不是鏡像的。這是預期的行為,因為在初始化
CaptureManager類時,我們將True傳給了shouldMirrorPreview。
圖2-4是Cameo的一個屏幕截圖,顯示了一個窗口(標題為Cameo)
和來自攝像頭的當前幀。
到目前為止,除了為預覽而對幀進行鏡像之外,我們沒有對幀執
行任何操作。我們將在第3章開始添加更有趣的效果。
2.5 本章小結
現在,我們應該擁有了一個顯示攝像頭回傳信號、監聽鍵盤輸入
並(在命令下)記錄屏幕截圖或屏幕播放的應用程序。我們打算通過
在每一幀的開始和結束之間插入一些圖像濾波代碼(見第3章)來擴展
應用程序。此外,除了OpenCV所支持的那些功能外,我們還准備集成
其他攝像頭驅動程序或應用程序框架。
我們還掌握了把圖像作為NumPy數組進行操作的知識。這將為下一
個主題——圖像濾波器——奠定完美的基礎。