部分 III
核心操作
9 圖像的基礎操作
目標
• 獲取像素值並修改
• 獲取圖像的屬性(信息)
• 圖像的 ROI()
• 圖像通道的拆分及合並
幾乎所有這些操作與 Numpy 的關系都比與 OpenCV 的關系更加緊密,因此熟練 Numpy 可以幫助我們寫出性能更好的代碼。
(示例將會在 Python 終端中展示,因為他們大部分都只有一行代碼)
9.1 獲取並修改像素值
首先我們需要讀入一幅圖像:
import cv2
import numpy as np
img=cv2.imread('messi5.jpg')
你可以根據像素的行和列的坐標獲取他的像素值。對 BGR 圖像而言,返回值為 B,G,R 的值。對灰度圖像而言,會返回他的灰度值(亮度?intensity)
import cv2
import numpy as np
img=cv2.imread('messi5.jpg')
px=img[100,100]
print(px)
blue=img[100,100,0]
print(blue)
你可以以類似的方式修改像素值。
import cv2
import numpy as np
img=cv2.imread('messi5.jpg')
img[100,100]=[255,255,255]
print(img[100,100])
## [255 255 255]
警告:Numpy 是經過優化了的進行快速矩陣運算的軟件包。所以我們不推薦逐個獲取像素值並修改,這樣會很慢,能有矩陣運算就不要用循環。
注意:上面提到的方法被用來選取矩陣的一個區域,比如說前 5 行的后 3列。對於獲取每一個像素值,也許使用 Numpy 的 array.item() 和 array.itemset() 會更好。但是返回值是標量。如果你想獲得所有 B,G,R 的
值,你需要使用 array.item() 分割他們。
獲取像素值及修改的更好方法。
import cv2
import numpy as np
img=cv2.imread('messi5.jpg')
print(img.item(10,10,2))
img.itemset((10,10,2),100)
print(img.item(10,10,2))
# 59
# 100
9.2 獲取圖像屬性
圖像的屬性包括:行,列,通道,圖像數據類型,像素數目等img.shape 可以獲取圖像的形狀。他的返回值是一個包含行數,列數,通道數的元組。
import cv2
import numpy as np
img=cv2.imread('messi5.jpg')
print(img.shape)
##(342, 548, 3)
注意:如果圖像是灰度圖,返回值僅有行數和列數。所以通過檢查這個返回值就可以知道加載的是灰度圖還是彩色圖。
img.size 可以返回圖像的像素數目:
注意:在debug時 img.dtype 非常重要。因為在 OpenCV Python 代碼中經常出現數據類型的不一致。
9.3 圖像 ROI
有時你需要對一幅圖像的特定區域進行操作。例如我們要檢測一副圖像中眼睛的位置,我們首先應該在圖像中找到臉,再在臉的區域中找眼睛,而不是直接在一幅圖像中搜索。這樣會提高程序的准確性和性能。
ROI 也是使用 Numpy 索引來獲得的。現在我們選擇球的部分並把他拷貝到圖像的其他區域。
import cv2
import numpy as np
img=cv2.imread('messi5.jpg')
ball=img[280:340,330:390]
img[273:333,100:160]=ball
img=cv2.imshow('test', img)
cv2.waitKey(0)
看看結果吧:
9.4 拆分及合並圖像通道
有時我們需要對 BGR 三個通道分別進行操作。這是你就需要把 BGR 拆分成單個通道。有時你需要把獨立通道的圖片合並成一個 BGR 圖像。你可以這樣做:
import cv2 import numpy as np img=cv2.imread('/home/duan/workspace/opencv/images/roi.jpg') b,g,r=cv2.split(img) img=cv2.merge(b,g,r)
或者:
import cv2 import numpy as np img=cv2.imread('/home/duan/workspace/opencv/images/roi.jpg') b=img[:,:,0]
假如你想使所有像素的紅色通道值都為 0,你不必先拆分再賦值。你可以直接使用 Numpy 索引,這會更快。
import cv2 import numpy as np img=cv2.imread('/home/duan/workspace/opencv/images/roi.jpg') img[:,:,2]=0
警告:cv2.split() 是一個比較耗時的操作。只有真正需要時才用它,能用Numpy 索引就盡量用。
9.5 為圖像擴邊(填充)
如果你想在圖像周圍創建一個邊,就像相框一樣,你可以使用 cv2.copyMakeBorder()函數。這經常在卷積運算或 0 填充時被用到。這個函數包括如下參數:
• src 輸入圖像
• top, bottom, left, right 對應邊界的像素數目。
• borderType 要添加那種類型的邊界,類型如下:
– cv2.BORDER_CONSTANT 添加有顏色的常數值邊界,還需要下一個參數(value)。
– cv2.BORDER_REFLECT 邊界元素的鏡像。比如: fedcba|abcde-fgh|hgfedcb
– cv2.BORDER_REFLECT_101 or cv2.BORDER_DEFAULT跟上面一樣,但稍作改動。例如: gfedcb|abcdefgh|gfedcba
– cv2.BORDER_REPLICATE 重復最后一個元素。例如: aaaaaa|abcdefgh|hhhhhhh
– cv2.BORDER_WRAP 不知道怎么說了, 就像這樣: cdefgh|abcdefgh|abcdefg
• value 邊界顏色,如果邊界的類型是 cv2.BORDER_CONSTANT
為了更好的理解這幾種類型請看下面的演示程序。
import cv2 import numpy as np from matplotlib import pyplot as plt BLUE=[255,0,0] img1=cv2.imread('opencv_logo.png') replicate = cv2.copyMakeBorder(img1,10,10,10,10,cv2.BORDER_REPLICATE) reflect = cv2.copyMakeBorder(img1,10,10,10,10,cv2.BORDER_REFLECT) reflect101 = cv2.copyMakeBorder(img1,10,10,10,10,cv2.BORDER_REFLECT_101) wrap = cv2.copyMakeBorder(img1,10,10,10,10,cv2.BORDER_WRAP) constant= cv2.copyMakeBorder(img1,10,10,10,10,cv2.BORDER_CONSTANT,value=BLUE) plt.subplot(231),plt.imshow(img1,'gray'),plt.title('ORIGINAL') plt.subplot(232),plt.imshow(replicate,'gray'),plt.title('REPLICATE') plt.subplot(233),plt.imshow(reflect,'gray'),plt.title('REFLECT') plt.subplot(234),plt.imshow(reflect101,'gray'),plt.title('REFLECT_101') plt.subplot(235),plt.imshow(wrap,'gray'),plt.title('WRAP') plt.subplot(236),plt.imshow(constant,'gray'),plt.title('CONSTANT') plt.show()
結果如下(由於是使用 matplotlib 繪制,所以交換 R 和 B 的位置,OpenCV 中是按 BGR,matplotlib 中是按 RGB 排列):
10 圖像上的算術運算
目標
• 學習圖像上的算術運算,加法,減法,位運算等。
• 我們將要學習的函數與有:cv2.add(),cv2.addWeighted() 等。
10.1 圖像加法
你可以使用函數 cv2.add() 將兩幅圖像進行加法運算,當然也可以直接使用 numpy,res=img1+img。兩幅圖像的大小,類型必須一致,或者第二個圖像可以使一個簡單的標量值。
注意:OpenCV 中的加法與 Numpy 的加法是有所不同的。OpenCV 的加法是一種飽和操作,而 Numpy 的加法是一種模操作。
例如下面的兩個例子:
x = np.uint8([250])
y = np.uint8([10]) print cv2.add(x,y) # 250+10 = 260 => 255 [[255]] print x+y # 250+10 = 260 % 256 = 4 [4]
這種差別在你對兩幅圖像進行加法時會更加明顯。OpenCV 的結果會更好一點。所以我們盡量使用 OpenCV 中的函數。
10.2 圖像混合
這其實也是加法,但是不同的是兩幅圖像的權重不同,這就會給人一種混合或者透明的感覺。圖像混合的計算公式如下:
g (x) = (1 − α)f 0 (x) + αf 1 (x)
通過修改 α 的值(0 → 1),可以實現非常酷的混合。
現在我們把兩幅圖混合在一起。第一幅圖的權重是 0.7,第二幅圖的權重是 0.3。函數 cv2.addWeighted() 可以按下面的公式對圖片進行混合操作。
dst = α · img1 + β · img2 + γ
這里 γ 的取值為 0。
img1 = cv2.imread('ml.png')
img2 = cv2.imread('opencv_logo.jpg') dst = cv2.addWeighted(img1,0.7,img2,0.3,0) cv2.imshow('dst',dst) cv2.waitKey(0) cv2.destroyAllWindows()
dst = cv2.addWeighted(img1,0.7,img2,0.3,0)
error: C:\projects\opencv-python\opencv\modules\core\src\arithm.cpp:659:
error: (-209) The operation is neither 'array op array' (where arrays have
the same size and the same number of channels), nor 'array op scalar',
nor 'scalar op array' in function cv::arithm_op
下面就是結果:# 這個運行有問題
10.3 按位運算
這里包括的按位操作有:AND,OR,NOT,XOR 等。當我們提取圖像的一部分,選擇非矩形 ROI 時這些操作會很有用(下一章你就會明白)。下面的例子就是教給我們如何改變一幅圖的特定區域。我想把 OpenCV 的標志放到另一幅圖像上。如果我使用加法,顏色會改變,如果使用混合,會得到透明效果,但是我不想要透明。如果他是矩形我可以象上一章那樣使用 ROI。但是他不是矩形。但是我們可以通過下面的按位運算實現:
import cv2
import numpy as np
# Load two images
img1 = cv2.imread('messi5.jpg')
img2 = cv2.imread('opencv-logo-white.png')
# I want to put logo on top-left corner, So I create a ROI
rows,cols,channels = img2.shape
roi = img1[0:rows, 0:cols ]
# Now create a mask of logo and create its inverse mask also
img2gray = cv2.cvtColor(img2,cv2.COLOR_BGR2GRAY)
ret, mask = cv2.threshold(img2gray, 10, 255, cv2.THRESH_BINARY)
mask_inv = cv2.bitwise_not(mask)
# Now black-out the area of logo in ROI
img1_bg = cv2.bitwise_and(roi,roi,mask = mask_inv)
# Take only region of logo from logo image.
img2_fg = cv2.bitwise_and(img2,img2,mask = mask)
# Put logo in ROI and modify the main image
dst = cv2.add(img1_bg,img2_fg)
img1[0:rows, 0:cols ] = dst
cv2.imshow('res',img1)
cv2.waitKey(0)
cv2.destroyAllWindows()
結果如下。左面的圖像是我們創建的掩碼。右邊的是最終結果。為了幫助大家理解我把上面程序的中間結果也顯示了出來,特別是 img1_bg 和 img2_fg。
練習
1. 創建一個幻燈片用來演示一幅圖如何平滑的轉換成另一幅圖(使用函數cv2.addWeighted)
11 程序性能檢測及優化
目標
在圖像處理中你每秒鍾都要做大量的運算,所以你的程序不僅要能給出正確的結果,同時還必須要快。所以這節我們將要學習:
• 檢測程序的效率
• 一些能夠提高程序效率的技巧
• 你要學到的函數有:cv2.getTickCount,cv2.getTickFrequency等
除了 OpenCV,Python 也提供了一個叫 time 的的模塊,你可以用它來測量程序的運行時間。另外一個叫做 profile 的模塊會幫你得到一份關於你的程序的詳細報告,其中包含了代碼中每個函數運行需要的時間,以及每個函數被調用的次數。如果你正在使用 IPython 的話,所有這些特點都被以一種用戶友好的方式整合在一起了。我們會學習幾個重要的,要想學到更加詳細的知識就打開更多資源中的鏈接吧。
11.1 使用 OpenCV 檢測程序效率
cv2.getTickCount 函數返回從參考點到這個函數被執行的時鍾數。所以當你在一個函數執行前后都調用它的話,你就會得到這個函數的執行時間(時鍾數)。
cv2.getTickFrequency 返回時鍾頻率,或者說每秒鍾的時鍾數。所以你可以按照下面的方式得到一個函數運行了多少秒:
e1 = cv2.getTickCount()
# your code execution
e2 = cv2.getTickCount() time = (e2 - e1)/ cv2.getTickFrequency()
我們將會用下面的例子演示。下面的例子是用窗口大小不同(5,7,9)的核函數來做中值濾波:
img1 = cv2.imread('messi5.jpg')
e1 = cv2.getTickCount() for i in xrange(5,49,2): img1 = cv2.medianBlur(img1,i) e2 = cv2.getTickCount() t = (e2 - e1)/cv2.getTickFrequency() print t # Result I got is 0.521107655 seconds
注 意: 你 也 可 以 中 time 模 塊 實 現 上 面 的 功 能。 但 是 要 用 的 函 數 是time.time() 而不是 cv2.getTickCount。比較一下這兩個結果的差別吧。
11.2 OpenCV 中的默認優化
OpenCV 中的很多函數都被優化過(使用 SSE2,AVX 等)。也包含一些沒有被優化的代碼。如果我們的系統支持優化的話要盡量利用只一點。在編譯時優化是被默認開啟的。因此 OpenCV 運行的就是優化后的代碼,如果你把優化關閉的話就只能執行低效的代碼了。你可以使用函數 cv2.useOptimized()來查看優化是否被開啟了,使用函數 cv2.setUseOptimized() 來開啟優化。
讓我們來看一個簡單的例子吧。
# check if optimization is enabled
In [5]: cv2.useOptimized()
Out[5]: True In [6]: %timeit res = cv2.medianBlur(img,49) 10 loops, best of 3: 34.9 ms per loop # Disable it In [7]: cv2.setUseOptimized(False) In [8]: cv2.useOptimized() Out[8]: False In [9]: %timeit res = cv2.medianBlur(img,49) 10 loops, best of 3: 64.1 ms per loop
看見了嗎,優化后中值濾波的速度是原來的兩倍。如果你查看源代碼的話,你會發現中值濾波是被 SIMD 優化的。所以你可以在代碼的開始處開啟優化(你要記住優化是默認開啟的)。
11.3 在 IPython 中檢測程序效率
有時你需要比較兩個相似操作的效率,這時你可以使用 IPython 為你提供的魔法命令%time。他會讓代碼運行好幾次從而得到一個准確的(運行)時間。它也可以被用來測試單行代碼的。
例如,你知道下面這同一個數學運算用哪種行式的代碼會執行的更快嗎?
x = 5; y = x ∗ ∗2
x = 5; y = x ∗ x
x = np.uint([5]); y = x ∗ x
y = np.squre(x)
我們可以在 IPython 的 Shell 中使用魔法命令找到答案。
In [10]: x = 5
In [11]: %timeit y=x**2
10000000 loops, best of 3: 73 ns per loop In [12]: %timeit y=x*x 10000000 loops, best of 3: 58.3 ns per loop In [15]: z = np.uint8([5]) In [17]: %timeit y=z*z 1000000 loops, best of 3: 1.25 us per loop In [19]: %timeit y=np.square(z) 1000000 loops, best of 3: 1.16 us per loop
竟然是第一種寫法,它居然比 Nump 快了 20 倍。如果考慮到數組構建的話,能達到 100 倍的差。
注意:Python 的標量計算比 Nump 的標量計算要快。對於僅包含一兩個元素的操作 Python 標量比 Numpy 的數組要快。但是當數組稍微大一點時Numpy 就會勝出了。
我們來再看幾個例子。我們來比較一下 cv2.countNonZero() 和
np.count_nonzero()。
In [35]: %timeit z = cv2.countNonZero(img)
100000 loops, best of 3: 15.8 us per loop In [36]: %timeit z = np.count_nonzero(img) 1000 loops, best of 3: 370 us per loop
看見了吧,OpenCV 的函數是 Numpy 函數的 25 倍。
注意:一般情況下 OpenCV 的函數要比 Numpy 函數快。所以對於相同的操作最好使用 OpenCV 的函數。當然也有例外,尤其是當使用 Numpy 對視圖(而非復制)進行操作時。
11.4 更多 IPython 的魔法命令
還有幾個魔法命令可以用來檢測程序的效率,profiling,line profiling,內存使用等。他們都有完善的文檔。所以這里只提供了超鏈接。感興趣的可以自己學習一下。
11.5 效率優化技術
有些技術和編程方法可以讓我們最大的發揮 Python 和 Numpy 的威力。
我們這里僅僅提一下相關的,你可以通過超鏈接查找更多詳細信息。我們要說的最重要的一點是:首先用簡單的方式實現你的算法(結果正確最重要),當結果正確后,再使用上面的提到的方法找到程序的瓶頸來優化它。
1. 盡量避免使用循環,尤其雙層三層循環,它們天生就是非常慢的。
2. 算法中盡量使用向量操作,因為 Numpy 和 OpenCV 都對向量操作進行了優化。
3. 利用高速緩存一致性。
4. 沒有必要的話就不要復制數組。使用視圖來代替復制。數組復制是非常浪費資源的。
就算進行了上述優化,如果你的程序還是很慢,或者說大的訓話不可避免的話,你你應該嘗試使用其他的包,比如說 Cython,來加速你的程序。
更多資源
1. Python Optimization Techniques
2. Scipy Lecture Notes - Advanced Numpy
3. Timing and Profiling in IPython