一 輪廓檢測
在計算機視覺中,輪廓檢測是另一個比較重要的任務,不單是用來檢測圖像或者視頻幀中物體的輪廓,而且還有其他操作與輪廓檢測相關。這些操作中,計算多邊形邊界,形狀逼近和計算機感 興趣區域。這是與圖像數據交互時的簡單操作,因為numpy中的矩陣中的矩形區域可以使用數組切片(slice)定義。在介紹物體檢測(包括人臉)和物體跟蹤的概念時會大量使用這種技術。
1、cv2.threshold(src,thresh,maxval,type[,dst])函數用於圖像閾值操作。
為了從一幅圖像中提取我們需要的部分,應該用圖像中的每一個像素點的灰度值與選取的閾值進行比較,並作出相應的判斷(閾值的選取依賴於具體的問題,物體在不同的圖像中可能會有不同的灰度值)。opencv提供了threshold()函數對圖像的閾值進行處理,threshold()共支持五中類型的閾值化方式,分別是二進制閾值化、反二進制閾值化、截斷閾值化、閾值化為0和反閾值化為0。返回閾值操作后的圖像。
- src: 輸入圖像,圖像必須為單通道8位或32位浮點型圖像
- thresh: 設定的閾值
- maxval: 使用cv2.THRESH_BINARY和cv2.THRESH_BINARY_INV類型的最大值
- type: 閾值化類型,可以通過ThresholdTypes查看,下面給出opencv中五種閾值化類型及其對應公式:
- dst: 輸出圖像,與輸入圖像尺寸和類型相同
2、 cv2.findContours(image,mode,method[,contours,hierarchy[,offset]])用於尋找尋找圖像輪廓。
opencv中提供findContours()函數來尋找圖像中物體的輪廓,並結合drawContours()函數將找到的輪廓繪制出。這個函數會修改輸入圖像,因此建議使用原始圖像的一份拷貝(比如說img.copy()作為輸入圖像)。函數返回三個值:返回修改后的圖像,圖像的輪廓以及它們的層次。
- image:輸入圖像,函數接受的參數是二值圖,即黑白的(不是灰度圖),我們同樣可以使用cv2.compare,cv2.inRange,cv2.threshold,cv2.adaptiveThreshold,cv2.Canny等函數來創建二值圖像,如果第二個參數為cv2.RETR_CCOMP或cv2.RETR_FLOODFILL,輸入圖像可以是32-bit整型圖像(cv2.CV_32SC1)
- mode輪廓檢索模式,如下
其中
- RETR_EXTERNAL:表示只檢測最外層輪廓,這對消除包含在其他輪廓中的輪廓很有用(比如在大多數情況下,不需要檢測一個目標包含在另一個與之相同的目標里面).對所有輪廓設置hierarchy[i][2]=hierarchy[i][3]=-1
- RETR_LIST:提取所有輪廓,檢測的輪廓不建立等級關系
- RETR_CCOMP:提取所有輪廓,並將輪廓組織成雙層結構(two-level hierarchy),頂層為連通域的外圍邊界,次層位內層邊界
- RETR_TREE:提取所有輪廓並重新建立網狀輪廓結構
- RETR_FLOODFILL:官網沒有介紹,應該是洪水填充法
- method:輪廓近似方法
- CHAIN_APPROX_NONE:獲取每個輪廓的每個像素,相鄰的兩個點的像素位置差不超過1。
- CHAIN_APPROX_SIMPLE:壓縮水平方向,垂直方向,對角線方向的元素,值保留該方向的重點坐標,如果一個矩形輪廓只需4個點來保存輪廓信息。
- CHAIN_APPROX_TC89_L1和CHAIN_APPROX_TC89_KCOS使用Teh-Chinl鏈逼近算法中的一種。
- contours:檢測到的輪廓(list),每個輪廓都是一個ndarray,每個ndarray是一個輪廓上點的集合。一個輪廓並不是存儲輪廓上所有的點,而是只存儲可以用直線描述輪廓的點,比如一個正方形,只需要四個頂點就能描述輪廓了。
- hierarchy:函數返回一個可選的hierarchy結果,這是一個ndarray,形狀為[1,輪廓個數,4],其中hierarchy[0]元素的個數和輪廓個數相同。每個輪廓contours[0][i]對應4個hierarchy元素hierarchy[0][i][0]~hierarchy[0][i][3],分別表示后一個輪廓,前一個輪廓,父輪廓,內嵌輪廓的索引,如果沒有對應項,則相應的hierarchy[0][i]設置為負數。
- offset:輪廓點可選偏移量,有默認值。
該函數返回繪制有輪廓的圖像。
- image:輸入/輸出圖像,指明在哪個圖像上繪制輪廓。並且該函數會修改源圖像image。
- contours:使用findContours檢測到的輪廓數據,傳入一個list。
- contourIdx:繪制輪廓的索引變量(表示繪制第幾個輪廓),如果為負值則繪制所有輸入輪廓。
- color:輪廓顏色。
- thickness:繪制輪廓所用線條粗細度,如果值為負值,則在輪廓內部繪制。
- lineTpye:線條類型,有默認值LINE_8,有如下可選類型
- hierarchy:可選層次結構信息
- maxLevel:用於繪制輪廓的最大等級。
- offset:可選輪廓便宜參數,用制定偏移量offset=(dx, dy)給出繪制輪廓的偏移量。
''' 輪廓檢測 ''' #加載圖像img img = cv2.imread('./image/img6.jpg',cv2.IMREAD_COLOR) cv2.imshow('img',img) #轉換為灰色gray_img gray_img = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY) cv2.imshow('gray_img',gray_img) #對圖像二值化處理 輸入圖像必須為單通道8位或32位浮點型 ret,thresh = cv2.threshold(gray_img,127,255,0) cv2.imshow('thresh',thresh) #尋找圖像輪廓 返回修改后的圖像 圖像的輪廓 以及它們的層次 image,contours,hierarchy = cv2.findContours(thresh,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE) cv2.imshow('image',image) print('contours[0]:',contours[0])
print('len(contours)',len(contours))
print('hierarchy,shape',hierarchy.shape)
print('hierarchy[0]:',hierarchy[0]) #在原圖img上繪制輪廓contours img = cv2.drawContours(img,contours,-1,(0,255,0),2) cv2.imshow('contours',img) cv2.waitKey() cv2.destroyAllWindows()
二 邊界框、最小矩形區域和最小閉圓的輪廓
找到一個正方形輪廓很簡單,要找到到不規則的,歪斜的以及旋轉的形狀,可以用Open CV的cv2.findContours()函數,它能得到最好的結果,下面來看一副圖:
現實的應用會對目標的邊界框,最小矩形面積,最小閉圓特別感興趣,將cv2.findContours()函數和少量的OpenCV的功能相結合就非常容易實現這些功能:
使用boundingRect()函數計算包圍輪廓的矩形框,使用minEnclosingCircle()函數計算包圍輪廓的最小圓包圍。
1、先計算一個簡單的邊界框(水平矩形):
x,y,w,h = cv2.boundingRect(c)
- cv2.boundingRect(img)函數。函數計算並返回點集最外面的矩形邊界(參數一般傳入一個輪廓,contours[0]),函數返回四個值,分別是x,y,w,h。x,y是矩陣左上點的左邊,w,h是矩陣的寬和高。
然后畫出這個矩形(在原圖img上繪制):這個操作非常簡單,它將輪廓信息轉換為(x,y)坐標,並加上矩形的高度和寬度。
cv2.rectangle(img,(x,y),(x+w,y+h),(0,255,0),2)
下面來將如何找到一個旋轉的矩陣和圓形輪廓。 首先加載圖片,然后在源圖像的灰度圖像上面執行一個二值化操作。這樣之后,可在這個灰度圖像上執行所有計算輪廓的操作,但在源圖像上可利用色彩信息來畫這些輪廓。
2、計算包含出包圍目標的最小矩形區域(旋轉矩形):
#找到最小區域
rect = cv2.minAreaRect(c)
#計算最小矩形的坐標
box = cv2.boxPoints(rect) #坐標轉換為整數 box = np.int0(box)
這里用到一個非常有趣的機制:Open CV沒有函數能直接從輪廓信息中計算出最小矩形頂點的坐標。所以需要計算最小矩形區域,然后計算這個矩形的頂點。注意計算出來的頂點左邊是浮點型,但是所得像素的坐標值是整數,所以需要做一個轉換。
函數 cv2.minAreaRect() 返回一個tuple:(最小外接矩形的中心(x,y),(寬度,高度),旋轉角度)。
但是要繪制這個矩形,我們需要矩形的4個頂點坐標box, 通過函數 cv2.cv.BoxPoints() 獲得,box:[ [x0,y0], [x1,y1], [x2,y2], [x3,y3] ]
最小外接矩形的4個頂點順序、中心坐標、寬度、高度、旋轉角度(是度數形式,不是弧度數)的對應關系如下:
注意:旋轉角度θ是水平軸(x軸)逆時針旋轉,與碰到的矩形的第一條邊的夾角。並且這個邊的邊長是width,另一條邊邊長是height。也就是說,在這里,width與height不是按照長短來定義的。
在opencv中,坐標系原點在左上角,相對於x軸,逆時針旋轉角度為負,順時針旋轉角度為正。在這里,θ∈(-90度,0]。
然后畫出這個矩形(在原圖img上繪制):
cv2.drawContours(img,[box],0,(255,0,0),3)
首先,該函數與所有繪圖函數一樣,它會修改源,其次該函數的第二個參數接收一個保存着輪廓的數組,從而可以在一次操作中繪制一系列的輪廓。因此如果只有一組點來表示多邊形輪廓,可以把這組點放到一個list中,就像前面例子里處理方框(box)那樣。這個函數第三個參數是繪制的輪廓數組的索引,-1表示繪制所有的輪廓,否則只繪制輪廓數組里指定的輪廓。
大多數繪圖函數把繪圖的顏色和線寬放在最后兩個參數里。
3、最后檢查的邊界輪廓為最小閉圓。
#計算閉圓中心店和和半徑
(x,y),radius = cv2.minEnclosingCircle(c)
#轉換為整型
center = (int(x),int(y)) radius = int(radius) #繪制閉圓(在原圖img上繪制) img = cv2.circle(img,center,radius,(0,255,0),2)
- cv2.minEnclosingCircle(points)函數 利用迭代算法,對給定的二維點集尋找計算可包圍點集的最小圓形。
points:輸入的二維點集,一般傳入一個輪廓 contours[0]
cv2.minEnclosingCircle()函數會返回一個元組,第一個元素為圓心的坐標組成的元素,第二個元素為圓的半徑值。把這些值轉換為整數后就能很容易地繪制出圓來。
完整代碼如下:
''' 邊框 最小矩形區域和最小閉圓的輪廓 ''' img = cv2.pyrDown(cv2.imread('./image/img16.jpg',cv2.IMREAD_UNCHANGED)) #轉換為灰色gray_img gray_img = cv2.cvtColor(img.copy(),cv2.COLOR_BGR2GRAY) #對圖像二值化處理 輸入圖像必須為單通道8位或32位浮點型 ret,thresh = cv2.threshold(gray_img,127,255,cv2.THRESH_BINARY) #尋找最外面的圖像輪廓 返回修改后的圖像 圖像的輪廓 以及它們的層次 image,contours,hierarchy = cv2.findContours(thresh,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE) print(type(contours)) print(type(contours[0])) print(len(contours)) #遍歷每一個輪廓 for c in contours: #找到邊界框的坐標 x,y,w,h = cv2.boundingRect(c) #在img圖像上 繪制矩形 線條顏色為green 線寬為2 cv2.rectangle(img,(x,y),(x+w,y+h),(0,255,0),2) #找到最小區域 rect = cv2.minAreaRect(c) #計算最小矩形的坐標 box = cv2.boxPoints(rect) #坐標轉換為整數 box = np.int0(box) #繪制輪廓 最小矩形 blue cv2.drawContours(img,[box],0,(255,0,0),3) #計算閉圓中心店和和半徑 (x,y),radius = cv2.minEnclosingCircle(c) #轉換為整型 center = (int(x),int(y)) radius = int(radius) #繪制閉圓 img = cv2.circle(img,center,radius,(0,255,0),2) cv2.drawContours(img,contours,-1,(0,0,255),2) cv2.imshow('contours',img)
運行后的結果:
三 凸輪廓與Douglas-Peucker算法
大多數處理輪廓的時候,圖的形狀(包括凸形狀)都是變化多樣的。凸形狀內部的任意兩點的連線都在該形狀內部。
cv2.approxPloyDP函數,它用來計算近似的多邊形框。該函數有三個參數:
- 第一個參數為輪廓
- 第二個參數為ε值,它表示源輪廓與近似多邊形的最大差值(這個值越小,近似多邊形與源輪廓越接近)
- 第三個參數為布爾標記,它表示這個多邊形是否閉合。
ε值對獲取有用的輪廓非常重要,所以需要理解它表示什么意思。ε是為所得到的近似多邊形周長與源輪廓周長之間的最大差值,這個值越小,近似多邊形與源輪廓就越相似。
為什么有了一個精確表示的輪廓卻還需要得到一個近似多邊形呢?這是因為一個多邊形由一組直線構成,能夠在一個區域里定義多邊形,以便於之后進行操作與處理,這在許多計算機視覺任務中非常重要。
在了解了ε值是什么之后,需要得到輪廓的周長信息來作為參考值。這可以通過cv2.arcLength函數來完成:
#arcLength獲取輪廓的周長 epsilon = 0.01*cv2.arcLength(cnt,True) #計算矩形的多邊形框 approx = cv2.approxPolyDP(cnt,epsilon,True)
可以通過OpenCV來有效地計算一個近似多邊形。為了計算凸形狀,需要利用cv2.convexHull來處理獲取的輪廓信息。
#從輪廓信息中計算得到凸形狀 hull = cv2.convexHull(cnt)
為了理解源輪廓、近似多邊形和凸包的不同之處,可以把他們放在一副圖片中進行觀察:
img = cv2.imread('./image/img18.jpg',cv2.IMREAD_COLOR) img = cv2.resize(img,None,fx=0.6,fy=0.6,interpolation=cv2.INTER_CUBIC) #創建一個空白圖像,用來繪制輪廓 canvas = np.zeros(img.shape,np.uint8) #轉換為灰色gray_img gray_img = cv2.cvtColor(img.copy(),cv2.COLOR_BGR2GRAY) #進行均值濾波,去除一些噪聲 kernel = np.ones((3,3),np.float32)/9 gray_img = cv2.filter2D(gray_img,-1,kernel) #cv2.imshow('gray_img',gray_img) #對圖像二值化處理 輸入圖像必須為單通道8位或32位浮點型 像素>125 設置為0(黑) 否則設置為255(白) ret,thresh = cv2.threshold(gray_img,125,255,cv2.THRESH_BINARY_INV) #cv2.imshow('thresh',thresh) #尋找圖像輪廓 返回修改后的圖像 圖像的輪廓 以及它們的層次 image,contours,hierarchy = cv2.findContours(thresh,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE) #獲取最大的一個輪廓 cnt = contours[0] max_area = cv2.contourArea(cnt) #對每一個輪廓進行遍歷 for cont in contours: if cv2.contourArea(cont) > max_area: cnt = cont max_area = cv2.contourArea(cont) print('max_area',max_area) '''計算最大輪廓的多邊形框''' #arcLength獲取輪廓的周長 epsilon = 0.01*cv2.arcLength(cnt,True) #計算矩形的多邊形框 approx = cv2.approxPolyDP(cnt,epsilon,True) #從輪廓信息中計算得到凸形狀 hull = cv2.convexHull(cnt) print('contours',len(contours),type(contours)) print('cnt.shape',cnt.shape,type(cnt)) print('approx.shape',approx.shape,type(approx)) print('hull.shape',hull.shape,type(hull)) #在源圖像中繪制所有輪廓 傳入的死一個list cv2.drawContours(img,contours,-1,(0,255,0),2) #GREEN 繪制所有的輪廓 cv2.drawContours(canvas,[cnt],-1,(0,255,0),2) #GREEN 繪制最大的輪廓 cv2.drawContours(canvas,[approx],-1,(0,0,255),2) #RED 繪制最大輪廓對應的多邊形框 cv2.drawContours(canvas,[hull],-1,(255,0,0),2) #BLUE 繪制最大輪廓對應的凸包 cv2.imshow('img',img) cv2.imshow('ALL',canvas) cv2.waitKey() cv2.destroyAllWindows()
如上圖所示,凸包是由藍色表示,然后里面是近似多邊形,使用紅色表示,在兩者之間的是源圖片中一個最大的輪廓,它主要由弧線構成。
四 直線和圓檢測
檢測邊緣和輪廓不僅重要,還經常用到,它們也是構成其他復雜操作的基礎。直線和形狀檢查與邊緣和輪廓檢測有密切的關系。
Hough變換是直線和形狀檢測背后的理論基礎,它由Richard Duda和Peter Hart發明,他們是對Paul Hough在20世紀60年代早期所做工作的擴展。
1、直線檢測
首先介紹直線檢測,這可通過HoughLines和HoughLinesP函數來完成,它們僅有的差別是:第一個函數使用標准的Hough變換,第二個函數使用概率Hough變換(因此名稱里有一個P)。
HoughLinesP函數之所以稱為概率版本的Hough變換是因為它只通過分析點的子集並估計這些點都屬於一條直線的概率,這是標准Hogh變換的優化版本。該函數的計算代價會少一些,執行會變得更快。
img = cv2.imread('./image/img19.jpg') #轉換為灰度圖片 gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY) #中值濾波 gray = cv2.medianBlur(gray,ksize=3) #邊緣檢測 edges = cv2.Canny(gray,50,100) minLineLength = 200 maxLineGap = 5 #直線檢測 lines = cv2.HoughLinesP(edges,1,np.pi/180,100, minLineLength,maxLineGap) print('len(lines)',len(lines),type(lines)) print('lines[0].shape',lines[0].shape) for i in range(len(lines)): for x1,y1,x2,y2 in lines[i]: cv2.line(img, (x1,y1), (x2,y2),(i*20,100+i*20,255),2) cv2. imshow("edges", edges) cv2. imshow("lines", img) cv2.waitKey() cv2.destroyAllWindows()
除了HoughLinesP函數調用是這段代碼的關鍵點以外,設置最小直線長度(更短的直線會被消除)和最大線段間隙也很重要,一條線段長度大於這個值會被視為兩條分開的線段。
注意:HoughLinesP函數會接收一個由Candy邊緣檢測濾波器處理過的單通道二值圖像。不一定需要Candy濾波器,但是一個經過去噪並且只有邊緣的圖像當中Hough變換的輸入會很不錯,因此使用Candy濾波器是一個普遍的慣例。
HoughLinesP函數參數如下:
- 需要處理的圖像,需要是灰度圖。
- 線段的幾何表示rho和theta,一般分別取1和np.pi/180。
- 閾值。低於該閾值的直線會被忽略。Hough變換可以理解為投票箱和投票數之間的關系,每一個投票箱代表一個直線,投票數達到閾值的直線會被保留,其他的會被刪除。
- 最小直線長度。
- 最大線段間隙。
該函數返回一個numpy.array類型,形狀為[num,1,4],每一行對應一條直線,每條直線形狀為(1,4),這4個數值表示起始點和終止點坐標。
2、圓檢測
OpenCV的HoughCircles函數可用來檢測圓,其主要是利用霍爾變換在圖像中尋找圓。我們知道,一個圓形的表達式為(x-x_center)2+(y-y_center)2=r2,一個圓環的確定需要三個參數,那么霍爾變換的累加器必須是三維的,但是這樣的計算效率很低,而opencv采用了霍夫梯度的方法,這里利用了邊界的梯度信息。
首先對圖像進行Candy邊緣檢測,對邊緣中的每一個非0點,通過sobel算子進行計算局部梯度。那么計算得到的梯度方向,實際上就是圓切線的法線。三條法線即可確定一個圓心,同理在累加器中對圓心通過的法線進行累加,就得到可圓環的判定。
cv2.HoughCircles(img,method,dp,minDist,circles,param1,param2,minRadius,maxRadius)函數的參數如下:
- img為輸入圖像,需要是灰度圖。
- method為檢測方法,常用cv2.HOUGH_GRADIENT
- dp為檢測內側圓心的累加器圖像的分辨率於輸入圖像之比的倒數,如dp=1,累加器和輸入圖像具有相同的分辨率,如果dp=2.累加器便有輸入圖像一半那么大的寬度和高度。
- minDist表示兩個圓之間圓心的最小距離。
- param1有默認值100,它是method設置的檢測方法對應的參數,對當前唯一的方法霍夫梯度法cv2.HOUGH_GRADIENT,它表示傳遞給Candy邊緣檢測算子的高閾值,而低閾值為高閾值的一半。
- param2有默認值100,它是method設置的檢測方法對應的參數,對當前唯一的方法霍夫梯度法cv2.HOUGH_GRADIENT,它表示在檢測階段圓心的累加器閾值,它越小,就越可以檢測到更多根本不存在的圓,而它越大的話,能通過檢測的圓就更接近完美的圓形了。
- minRadius有默認值0,圓半徑的最小值。
- maxRadius有默認值0,圓半徑的最大值。
下面是一個例子:
img = cv2.imread('./image/img20.jpg') #縮小 img = cv2.resize(img,None,fx=0.5,fy=0.5,interpolation=cv2.INTER_CUBIC) #轉換為灰度圖片 gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY) #中值濾波 gray = cv2.medianBlur(gray,ksize=3) #圓檢測 circles = cv2.HoughCircles(gray,cv2.HOUGH_GRADIENT,1,120,param1=100,param2=30,minRadius=0,maxRadius=0) print('circles',type(circles),circles.shape) #circles <class 'numpy.ndarray'> (1, 3, 3) circles = np.uint16(np.around(circles)) for i in circles[0,:]: #繪制圓 (i[0],i[1])為圓心,i[2]為半徑 cv2.circle(img,(i[0],i[1]),i[2],(0,255,0),2) #繪制圓心 cv2.circle(img,(i[0],i[1]),2,(255,0,0),3) cv2.imshow('circles',img) cv2.waitKey() cv2.destroyAllWindows()
3、檢測其他形狀
Hough變換能檢測的形狀僅限於圓,但是前面曾提到過檢測任何形狀的方法,特別是用approxPloyDP函數來檢測。該函數提供多邊形的近似,所以如果你的圖像有多邊形,再結合cv2.findContous函數和cv2.approxPloyDP函數,就可以相當准確的檢測出來。
參考文章:
[1]python-opencv2利用cv2.findContours()函數來查找檢測物體的輪廓
[2]Python下opencv使用筆記(十一)(詳解hough變換檢測直線與圓)