OpenCV中的圖像處理
顏色空間的轉換:
最常用的:BGR->Gray和BGR->HSV
我們要用到的函數是:cv2.cvtColor(input_image,flag),其中 flag就是轉換類型。
在 OpenCV 的 HSV 格式中,H(色彩/色度)的取值范圍是 [0,179], S(飽和度)的取值范圍 [0,255],V(亮度)的取值范圍 [0,255]。但是不 同的軟件使用的值可能不同。所以當你需要拿 OpenCV 的 HSV 值與其他軟 件的 HSV 值進行對比時,一定要記得歸一化。
物體跟蹤
現在我們知道怎樣將一幅圖像從 BGR 轉換到 HSV 了,我們可以利用這一點來提取帶有某個特定顏色的物體。在 HSV 顏色空間中要比在 BGR 空間中更容易表示一個特定顏色。在我們的程序中,我們要提取的是一個藍色的物
# 可以檢測到藍色的物品 import cv2 import numpy as np # 打開攝像頭 cap=cv2.VideoCapture(0) while(1): # 獲取每一幀 ret,frame=cap.read() # 轉換到 HSV hsv=cv2.cvtColor(frame,cv2.COLOR_BGR2HSV) # 設定藍色的閾值 lower_blue=np.array([110,50,50]) upper_blue=np.array([130,255,255]) # 根據閾值構建蒙版 mask=cv2.inRange(hsv,lower_blue,upper_blue) # 對原圖像和蒙版進行位運算 res=cv2.bitwise_and(frame,frame,mask=mask) # 顯示圖像 cv2.imshow('frame',frame) cv2.imshow('mask',mask) cv2.imshow('res',res) k=cv2.waitKey(5)&0xFF if k==27: break # 關閉窗口 cv2.destroyAllWindows()
圖像中仍然有一些噪音,我們會在后面的章節中介紹如何消減噪音。
這是物體跟蹤中最簡單的方法。當你學習了輪廓之后,你就會學到更多相關知識,那是你就可以找到物體的重心,並根據重心來跟蹤物體,僅僅在攝像頭前揮揮手就可以畫出同的圖形,或者其他更有趣的事。
src: 輸入圖像
lowerb: 像素值的下邊界,如果圖中的像素低於這個值,就變為0
upperb: 像素值的上邊界,如果圖中的像素高於這個值,就變為0,lowerb~upperb之間的值變為255
dst: 輸出的是二值化的圖像
怎樣找到要跟蹤對象的 HSV 值?
import cv2 import numpy as np img = cv2.imread("goods.jpg") img = img[:,:,[2,1,0]] green=np.uint8([[[0,255,0]]]) lower_blue=np.array([10,50,50]) upper_blue=np.array([130,255,255]) hsv = cv2.cvtColor(img,cv2.COLOR_RGB2HSV) mask=cv2.inRange(hsv,lower_blue,upper_blue) res=cv2.bitwise_and(img,img,mask=mask) constant= cv2.copyMakeBorder(img,10,10,10,10,cv2.BORDER_CONSTANT,value=[255,0,0]) import matplotlib.pyplot as plt import matplotlib as mpl mpl.rcParams["font.sans-serif"] = ["SimHei"] plt.subplot(221),plt.imshow(img,'gray'),plt.title('原圖') plt.subplot(222),plt.imshow(mask,'gray'),plt.title('HSV提取圖') plt.subplot(223),plt.imshow(res,'gray'),plt.title('進行與運算') plt.subplot(224),plt.imshow(constant,'gray'),plt.title('加邊框') plt.show()
幾何變換
擴展縮放
擴展縮放只是改變圖像的尺寸大小。OpenCV 提供的函數 cv2.resize()可以實現這個功能。圖像的尺寸可以自己手動設置,你也可以指定縮放因子。我們可以選擇使用不同的插值方法。在縮放時我們推薦使用 cv2.INTER_AREA, 在擴展時我們推薦使用 v2.INTER_CUBIC(慢) 和 v2.INTER_LINEAR。默認情況下所有改變圖像尺寸大小的操作使用的插值方法都是 cv2.INTER_LINEAR。你可以使用下面任意一種方法改變圖像的尺寸:
import cv2 img=cv2.imread('dmn.jpg') # 下面的 None 本應該是輸出圖像的尺寸,但是因為后邊我們設置了縮放因子 # 因此這里為 None res=cv2.resize(img,None,fx=2,fy=2,interpolation=cv2.INTER_CUBIC) #OR # 這里呢,我們直接設置輸出圖像的尺寸,所以不用設置縮放因子 height,width=img.shape[:2] res=cv2.resize(img,(2*width,2*height),interpolation=cv2.INTER_CUBIC) while(1): cv2.imshow('res',res) cv2.imshow('img',img) if cv2.waitKey(1) & 0xFF == 27: break cv2.destroyAllWindows()
平移
平移就是將對象換一個位置。如果你要沿(x,y)方向移動,移動的距離是(tx,ty),你可以以下面的方式構建移動矩陣:

你可以使用 Numpy 數組構建這個矩陣(數據類型是 np.float32),然后把它傳給函數 cv2.warpAffine()。看看下面這個例子吧,它被移動了(100,50)個像素。
import cv2 img = cv2.imread('dmn.jpg') img = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) rows, cols = img.shape import numpy as np # 注意這里的數據類型必須是float32 100是x移動的大小,200是y移動的大小 M= np.array([[1,0,100],[0,1,200]],dtype=np.float32) # 這里M是乘於的矩陣,后面的元組是畫板的大小 dst = cv2.warpAffine(img, M, (cols+100 , rows+100 )) while (1): cv2.imshow('img', dst) if cv2.waitKey(1) & 0xFF == 27: break cv2.destroyAllWindows()
旋轉
對一個圖像旋轉角度 θ, 需要使用到下面形式的旋轉矩陣。

但是 OpenCV 允許你在任意地方進行旋轉,但是旋轉矩陣的形式應該修 改為

其中: α = scale · cos θ β = scale · sin θ 為了構建這個旋轉矩陣,OpenCV 提供了一個函數:cv2.getRotationMatrix2D。 下面的例子是在不縮放的情況下將圖像旋轉 90 度。
import cv2 img = cv2.imread('dmn.jpg') img = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) rows, cols = img.shape # 這里的第一個參數為旋轉中心,第二個為旋轉角度,第三個為旋轉后的縮放因子 # 可以通過設置旋轉中心,縮放因子,以及窗口大小來防止旋轉后超出邊界的問題 M = cv2.getRotationMatrix2D((cols / 2, rows / 2), 90, 0.6) # 第三個參數是輸出圖像的尺寸中心 dst = cv2.warpAffine(img, M, (cols + 10, rows + 10)) while (1): cv2.imshow('img', dst) if cv2.waitKey(1) & 0xFF == 27: break cv2.destroyAllWindows()
仿射變換
在仿射變換中,原圖中所有的平行線在結果圖像中同樣平行。為了創建這個矩陣我們需要從原圖像中找到三個點以及他們在輸出圖像中的位置。然后cv2.getAffineTransform 會創建一個 2x3 的矩陣,最后這個矩陣會被傳給函數 cv2.warpAffine。
import cv2 import numpy as np from matplotlib import pyplot as plt img=cv2.imread('dmn.jpg') img = img[:,:,[2,1,0]] rows,cols,ch=img.shape pts1=np.float32([[50,50],[200,50],[50,200]]) pts2=np.float32([[10,100],[200,50],[100,250]]) M=cv2.getAffineTransform(pts1,pts2) dst=cv2.warpAffine(img,M,(cols,rows)) plt.subplot(121),plt.imshow(img) plt.subplot(122),plt.imshow(dst) plt.show()
透視變換
對於視角變換,我們需要一個 3x3 變換矩陣。在變換前后直線還是直線。要構建這個變換矩陣,你需要在輸入圖像上找 4 個點,以及他們在輸出圖像上對應的位置。這四個點中的任意三個都不能共線。這個變換矩陣可以有函數 cv2.getPerspectiveTransform() 構建。然后把這個矩陣傳給函數 cv2.warpPerspective。
import cv2 import numpy as np from matplotlib import pyplot as plt img=cv2.imread('img/fnn.jpg') rows,cols,ch=img.shape pts1 = np.float32([[56,65],[368,52],[28,387],[389,390]]) pts2 = np.float32([[0,0],[300,0],[0,300],[300,300]]) M=cv2.getPerspectiveTransform(pts1,pts2) dst=cv2.warpPerspective(img,M,(300,300)) plt.subplot(121),plt.imshow(img) plt.subplot(122),plt.imshow(dst) plt.show()
圖像閾值
簡單閾值
與名字一樣,這種方法非常簡單。但像素值高於閾值時,我們給這個像素賦予一個新值(可能是白色),否則我們給它賦予另外一種顏色(也許是黑色)。這個函數就是 cv2.threshhold()。這個函數的第一個參數就是原圖像,原圖像應該是灰度圖。第二個參數就是用來對像素值進行分類的閾值。第三個參數就是當像素值高於(有時是小於)閾值時應該被賦予的新的像素值。OpenCV提供了多種不同的閾值方法,這是有第四個參數來決定的。這些方法包括:
import cv2 from matplotlib import pyplot as plt img=cv2.imread('../img/fnn.jpg',0) ret,thresh1=cv2.threshold(img,127,255,cv2.THRESH_BINARY) ret,thresh2=cv2.threshold(img,127,255,cv2.THRESH_BINARY_INV) ret,thresh3=cv2.threshold(img,127,255,cv2.THRESH_TRUNC) ret,thresh4=cv2.threshold(img,127,255,cv2.THRESH_TOZERO) ret,thresh5=cv2.threshold(img,127,255,cv2.THRESH_TOZERO_INV) titles = ['Original Image','BINARY','BINARY_INV','TRUNC','TOZERO','TOZERO_INV'] images = [img, thresh1, thresh2, thresh3, thresh4, thresh5] for i in range(6): plt.subplot(2,3,i+1),plt.imshow(images[i],'gray') plt.title(titles[i]) plt.xticks([]),plt.yticks([]) plt.show()
自適應閾值
在前面的部分我們使用是全局閾值,整幅圖像采用同一個數作為閾值。當時這種方法並不適應與所有情況,尤其是當同一幅圖像上的不同部分的具有不同亮度時。這種情況下我們需要采用自適應閾值。此時的閾值是根據圖像上的每一個小區域計算與其對應的閾值。因此在同一幅圖像上的不同區域采用的是不同的閾值,從而使我們能在亮度不同的情況下得到更好的結果。這種方法需要我們指定三個參數,返回值只有一個。
Adaptive Method- 指定計算閾值的方法。 – cv2.ADPTIVE_THRESH_MEAN_C:閾值取自相鄰區域的平均值 – cv2.ADPTIVE_THRESH_GAUSSIAN_C:閾值取值相鄰區域的加權和,權重為一個高斯窗口。 • Block Size - 鄰域大小(用來計算閾值的區域大小)。 • C - 這就是是一個常數,閾值就等於的平均值或者加權平均值減去這個常數。
import cv2 import numpy as np from matplotlib import pyplot as plt img = cv2.imread('../img/fnn.jpg',0) # 中值濾波 img = cv2.medianBlur(img,5) ret,th1 = cv2.threshold(img,127,255,cv2.THRESH_BINARY) #11 為 Block size, 2 為 C 值 th2 = cv2.adaptiveThreshold(img,255,cv2.ADAPTIVE_THRESH_MEAN_C,cv2.THRESH_BINARY,11,2) th3 = cv2.adaptiveThreshold(img,255,cv2.ADAPTIVE_THRESH_GAUSSIAN_C,cv2.THRESH_BINARY,11,2) titles = ['Original Image', 'Global Thresholding (v = 127)','Adaptive Mean Thresholding', 'Adaptive Gaussian Thresholding'] images = [img, th1, th2, th3] for i in range(4): plt.subplot(2,2,i+1),plt.imshow(images[i],'gray') plt.title(titles[i]) plt.xticks([]),plt.yticks([]) plt.show()
Otsu’s 二值化
在使用全局閾值時,我們就是隨便給了一個數來做閾值,那我們怎么知道我們選取的這個數的好壞呢?答案就是不停的嘗試。如果是一副雙峰圖像(簡單來說雙峰圖像是指圖像直方圖中存在兩個峰)呢?我們豈不是應該在兩個峰之間的峰谷選一個值作為閾值?這就是 Otsu 二值化要做的。簡單來說就是對一副雙峰圖像自動根據其直方圖計算出一個閾值。(對於非雙峰圖像,這種方法得到的結果可能會不理想)。
這里用到到的函數還是 cv2.threshold(),但是需要多傳入一個參數(flag):cv2.THRESH_OTSU。這時要把閾值設為 0。然后算法會找到最優閾值,這個最優閾值就是返回值 retVal。如果不使用 Otsu 二值化,返回的retVal 值與設定的閾值相等。下面的例子中,輸入圖像是一副帶有噪聲的圖像。第一種方法,我們設127 為全局閾值。第二種方法,我們直接使用 Otsu 二值化。第三種方法,我們首先使用一個 5x5 的高斯核除去噪音,然后再使用 Otsu 二值化。看看噪音去除對結果的影響有多大吧。
import cv2 import numpy as np from matplotlib import pyplot as plt img = cv2.imread('../img/fnn.jpg',0) # global thresholding ret1,th1 = cv2.threshold(img,127,255,cv2.THRESH_BINARY) # Otsu's thresholding ret2,th2 = cv2.threshold(img,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU) #(5,5)為高斯核的大小,0 為標准差 blur = cv2.GaussianBlur(img,(5,5),0) # 閾值一定要設為 0! ret3,th3 = cv2.threshold(blur,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU) # plot all the images and their histograms images = [img, 0, th1, img, 0, th2, blur, 0, th3] titles = ['Original Noisy Image','Histogram','Global Thresholding (v=127)', 'Original Noisy Image','Histogram',"Otsu's Thresholding", 'Gaussian filtered Image','Histogram',"Otsu's Thresholding"] # 這里使用了 pyplot 中畫直方圖的方法,plt.hist, 要注意的是它的參數是一維數組 # 所以這里使用了(numpy)ravel 方法,將多維數組轉換成一維,也可以使用 flatten 方法 for i in range(3): plt.subplot(3,3,i*3+1),plt.imshow(images[i*3],'gray') plt.title(titles[i*3]), plt.xticks([]), plt.yticks([]) plt.subplot(3,3,i*3+2),plt.hist(images[i*3].ravel(),256) plt.title(titles[i*3+1]), plt.xticks([]), plt.yticks([]) plt.subplot(3,3,i*3+3),plt.imshow(images[i*3+2],'gray') plt.title(titles[i*3+2]), plt.xticks([]), plt.yticks([]) plt.show()
在這一部分我們會演示怎樣使用 Python 來實現 Otsu 二值化算法,從而告訴大家它是如何工作的。如果你不感興趣的話可以跳過這一節。因為是雙峰圖,Otsu 算法就是要找到一個閾值(t), 使得同一類加權方差最小,需要滿足下列關系式:

其中:
其實就是在兩個峰之間找到一個閾值 t,將這兩個峰分開,並且使每一個峰內的方差最小。實現這個算法的 Python 代碼如下:
import cv2 import numpy as np import warnings warnings.filterwarnings("ignore") img = cv2.imread('../img/fnn.jpg',0) blur = cv2.GaussianBlur(img,(5,5),0) # find normalized_histogram, and its cumulative distribution function # 計算歸一化直方圖 #CalcHist(image, accumulate=0, mask=NULL) hist = cv2.calcHist([blur],[0],None,[256],[0,256]) hist_norm = hist.ravel()/hist.max() Q = hist_norm.cumsum() bins = np.arange(256) fn_min = np.inf thresh = -1 for i in range(1,256): p1,p2 = np.hsplit(hist_norm,[i]) # probabilities q1,q2 = Q[i],Q[255]-Q[i] # cum sum of classes b1,b2 = np.hsplit(bins,[i]) # weights # finding means and variances m1,m2 = np.sum(p1*b1)/q1, np.sum(p2*b2)/q2 v1,v2 = np.sum(((b1-m1)**2)*p1)/q1,np.sum(((b2-m2)**2)*p2)/q2 # calculates the minimization function fn = v1*q1 + v2*q2 if fn < fn_min: fn_min = fn thresh = i # find otsu's threshold value with OpenCV function ret, otsu = cv2.threshold(blur,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU) print(thresh,ret) # 結果:140 139.0