OpenCV中的图像处理 —— 霍夫线 / 圈变换 + 图像分割(分水岭算法) + 交互式前景提取(GrabCut算法)
OpenCV中的圖像處理 —— 霍夫線 / 圈變換 + 圖像分割(分水嶺算法) + 交互式前景提取(GrabCut算法)
🌎上一節我們介紹了OpenCV中傅里葉變換和模板匹配,這一部分我們來聊一聊霍夫線/圈變換的原理和應用、使用分水嶺算法實現圖像分割和使用GrabCut算法實現交互式前景提取
🏠哈嘍大家好,這里是ErrorError!,一枚某高校大二本科在讀的♂同學,希望未來在機器視覺領域能夠有所成就,很榮幸能夠在CSDN結識眾多志同道合和在各方面都有所造詣的小伙伴,我們一起加油吧~💖
🚀上節內容:OpenCV中的圖像處理 —— 傅里葉變換+模板匹配
目錄🌻🌷
- OpenCV中的圖像處理 —— 霍夫線 / 圈變換 + 圖像分割(分水嶺算法) + 交互式前景提取(GrabCut算法)
- 1. 霍夫線變換
- 1.1 HoughLines工作原理
- 1.2 OpenCV中的霍夫曼變換
- 1.3 概率霍夫線變換
- 2. 霍夫圈變換
- 3. 圖像分割與分水嶺算法
- 3.1 分水嶺算法
- 3.2 圖像分割的實現
- 4. 使用GrabCut算法實現交互式前景提取
- 4.1 GrabCut算法
- 4.2 使用OpenCV進行GrabCut算法
1. 霍夫線變換
1.1 HoughLines工作原理
經過上一節中”模板匹配”的了解,是不是發現我們有點兒目標檢測的雛形了呢?這一部分說的霍夫線變換也是一個不斷深入的關鍵點。如果可以如果可以用數學形式表示形狀,則霍夫變換是一種檢測任何形狀的流行技術,即使形狀有些破損或變形,也可以檢測出形狀,我們將看到它如何作用于一條線
🚀霍夫線眼中的線:通常一條線可以表示為y=mx+cy=mx+c或以參數形式表示為ρ=xcosθ+ysinθ,其中ρ是從原點到該線的垂直距離,而θ是由該垂直線和水平軸形成的角度以逆時針方向測量(該方向隨我們如何表示坐標系而變化),因此,如果線在原點下方通過,則它將具有正的ρ且角度小于180,如果線在原點上方,則將角度取為小于180,而不是大于180的角度,ρ取負值,任何垂直線將具有0度,水平線將具有90度
🚀霍夫線怎么處理線:任何一條線都可以用(ρ,θ)這兩個術語表示。因此,首先創建2D數組或累加器(以保存兩個參數的值),并將其初始設置為0。讓行表示ρ,列表示θ,陣列的大小取決于所需的精度。假設我們希望角度的精度為1度,則需要180列。對于ρ,最大距離可能是圖像的對角線長度。因此,以一個像素精度為準,行數可以是圖像的對角線長度
🚀放在實際圖像中:假設有一個100*100的圖像,中間有一條水平線。取直線的第一點,且我們知道它的坐標(x,y)值。現在在線性方程式中,將值θ= 0,1,2,… 180放進去,然后檢查得到ρ。對于每對(ρ,θ),在累加器中對應的(ρ,θ)單元格將值增加1。
現在,對行的第二個點執行與上述相同的操作,遞增(ρ,θ)對應的單元格中的值,這一次操作使單元格(50,90)=2。實際上,我們正在對(ρ,θ)值進行投票。我們對線路上的每個點都繼續執行此過程。在每個點上,單元格(50,90)都會增加或投票,而其他單元格可能會或可能不會投票。這樣一來,最后,單元格(50,90)的投票數將最高。因此,如果我們在累加器中搜索最大票數,則將獲得(50,90)值,該值表示該圖像中的一條線與原點的距離為50,角度為90度
1.2 OpenCV中的霍夫曼變換
OpenCV把上述所有的霍夫曼變換過程都封裝在了函數cv.HoughLines()里,它返回的是一個math:(rho,theta)值的數組,ρ以像素為單位,θ以弧度為單位。這個函數包括4個參數,第一個即二進制原圖,因此在使用霍夫曼變換之前我們會先使用閾值或Canny邊緣檢測,第二、第三個參數是ρ和θ的精度,第四個參數是閾值,它意味著行的最低投票,票數取決于線上的點數,因此這個閾值也表示檢測到的最小線長
import cv2 as cv import numpy as npimg = cv.imread(cv.samples.findFile(r'E:\image\test19.png')) gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY) edges = cv.Canny(gray, 50, 150, apertureSize=3) lines = cv.HoughLines(edges, 1, np.pi / 180, 100) for line in lines:rho, theta = line[0]a = np.cos(theta)b = np.sin(theta)x0 = a * rhoy0 = b * rhox1 = int(x0 + 1000 * (-b))y1 = int(y0 + 1000 * (a))x2 = int(x0 - 1000 * (-b))y2 = int(y0 - 1000 * (a))cv.line(img, (x1, y1), (x2, y2), (0, 0, 255), 2) cv.imshow('houghlines.jpg', img) cv.waitKey(0)1.3 概率霍夫線變換
在霍夫線變換中即使對于帶有兩個參數的行,也需要大量計算,概率霍夫變換是我們看到的霍夫變換的優化,它沒有考慮所有要點。取而代之的是,它僅采用隨機的點子集,足以進行線檢測,只是我們必須降低閾值
OpenCV的實現基于Matas,J.和Galambos,C.和Kittler, J.V.使用漸進概率霍夫變換對行進行的穩健檢測[145]。使用的函數是cv.HoughLinesP()。它有兩個新的屬性:1. - minLineLength - 最小行長,小于此長度的線段將被拒絕;2. - maxLineGap - 線段之間允許將它們視為一條線的最大間隙
import cv2 as cv import numpy as npimg = cv.imread(cv.samples.findFile(r'E:\image\test19.png')) gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY) edges = cv.Canny(gray, 50, 150, apertureSize=3) lines = cv.HoughLinesP(edges, 1, np.pi / 180, 100, minLineLength=100, maxLineGap=10) for line in lines:x1, y1, x2, y2 = line[0]cv.line(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv.imshow('houghlines.jpg', img) cv.waitKey(0)2. 霍夫圈變換
上面說完了霍夫線變換,現在挨到霍夫圈變換了,也就是說霍夫線變換面向的是圖像中的線,而圈變換面向的就是圓咯
圓在數學上表示為(x?xcenter)^ 2+(y?ycenter)^2= r^2,其中(xcenter,ycenter)(xcenter,ycenter)是圓的中心,rr是圓的半徑,從等式中,我們可以看到我們有3個參數,因此我們需要3D累加器進行霍夫變換,這將非常低效。因此,OpenCV使用更加技巧性的方法,即使用邊緣的梯度信息的Hough梯度方法,我們在這里使用的函數是cv.HoughCircles()
這個函數參數有點兒多,我們有必要說說,它的原型是cv2.HoughCircles(image, method, dp, minDist, circles, param1, param2, minRadius, maxRadius),第一個參數為原圖像(灰度圖),第二個參數是檢測方法,第三個參數為檢測內側圓心的累加器圖像的分辨率于輸入圖像之比的倒數,如dp=1,累加器和輸入圖像具有相同的分辨率,如果dp=2,累計器便有輸入圖像一半那么大的寬度和高度,第四個參數表示兩個圓之間圓心的最小距離
param1與param2有默認值100,它們是method設置的檢測方法的對應的參數,對當前唯一的方法霍夫梯度法cv2.HOUGH_GRADIENT,param1表示傳遞給canny邊緣檢測算子的高閾值,而低閾值為高閾值的一半,param2表示在檢測階段圓心的累加器閾值,它越小,就越可以檢測到更多根本不存在的圓,而它越大的話,能通過檢測的圓就更加接近完美的圓形了
minRadius和maxRadius有默認值0,分別表示圓半徑的最小值和最大值
import numpy as np import cv2 as cvimg = cv.imread(r'E:\image\test20.png', 0) img = cv.medianBlur(img, 5) cimg = cv.cvtColor(img, cv.COLOR_GRAY2BGR) circles = cv.HoughCircles(img, cv.HOUGH_GRADIENT, 1, 100, param1=100, param2=30, minRadius=100, maxRadius=200) circles = np.uint16(np.around(circles)) for i in circles[0, :]:# 繪制外圓cv.circle(cimg, (i[0], i[1]), i[2], (0, 255, 0), 2)# 繪制圓心cv.circle(cimg, (i[0], i[1]), 2, (0, 0, 255), 3) cv.imshow('detected circles', cimg) cv.waitKey(0)3. 圖像分割與分水嶺算法
3.1 分水嶺算法
🚀算法思想:任何灰度圖像都可以看作是一個地形表面,其中高強度表示山峰,低強度表示山谷,我們用不同顏色的**水(標簽)**填充每個孤立的山谷(局部最小值)。隨著水位的上升,根據附近的山峰(坡度),來自不同山谷的水明顯會開始合并,顏色也不同。為了避免這種情況,我們要在水融合的地方建造屏障。繼續填滿水,建造障礙,直到所有的山峰都在水下。然后我們創建的屏障將返回你的分割結果
但是這種方法會由于圖像中的噪聲或其他不規則性而產生過度分割的結果。因此OpenCV實現了一個基于標記的分水嶺算法,我們可以指定哪些是要合并的山谷點,哪些不是。這是一個交互式的圖像分割。我們所做的是給我們知道的對象賦予不同的標簽。用一種顏色(或強度)標記我們確定為前景或對象的區域,用另一種顏色標記我們確定為背景或非對象的區域,最后用0標記我們不確定的區域。這是我們的標記。然后應用分水嶺算法。然后我們的標記將使用我們給出的標簽進行更新,對象的邊界值將為-1
3.2 圖像分割的實現
有一張布滿硬幣的白紙,部分硬幣之間相互接觸,我們將這張圖作為源圖像,我們先從尋找硬幣的近似估計開始,因此我們要使用閾值化(Otsu的二值化)
import numpy as np import cv2 as cv from matplotlib import pyplot as plt img = cv.imread(r'E:\image\test21.png') gray = cv.cvtColor(img,cv.COLOR_BGR2GRAY) ret, thresh = cv.threshold(gray,0,255,cv.THRESH_BINARY_INV+cv.THRESH_OTSU)然后由于分水嶺算法對噪聲非常敏感,所有我們要去除圖像中的所有白點噪聲,為此我們可以使用形態學擴張,如果要去除硬幣對象中的小孔,我們可以使用形態學侵蝕,在進行完這些操作后,我們可以十分確信靠近對象中心的區域是前景,離對象中心很遠的就是背景,現在我們唯一不確定的就是硬幣的邊界區域
接下來我們需要提取我們可確認為硬幣的區域(因為侵蝕會去除邊界像素),如果硬幣之間不接觸那么我們之前的操作完全沒問題,但是事實是他們接觸了,因此我們更好的選擇是找到距離變換并應用適當的閾值。此時我們需要確定一定不是硬幣的區域,形態學擴張可以滿足我們的需求
剩下的區域是我們不知道的區域,無論是硬幣還是背景。分水嶺算法應該找到它。這些區域通常位于前景和背景相遇(甚至兩個不同的硬幣相遇)的硬幣邊界附近。可以通過從sure_bg區域中減去sure_fg區域來獲得
# 噪聲去除 kernel = np.ones((3,3),np.uint8) opening = cv.morphologyEx(thresh,cv.MORPH_OPEN,kernel, iterations = 2) # 確定背景區域 sure_bg = cv.dilate(opening,kernel,iterations=3) # 尋找前景區域 dist_transform = cv.distanceTransform(opening,cv.DIST_L2,5) ret, sure_fg = cv.threshold(dist_transform,0.7*dist_transform.max(),255,0) # 找到未知區域 sure_fg = np.uint8(sure_fg) unknown = cv.subtract(sure_bg,sure_fg)??代碼解析:第3行的cv.morphologyEx()函數是高級形態學轉換函數,其功能取決于第二個參數的選取,具體內容請移步 OpenCV-Python——第13章:圖像的形態學操作(腐蝕,膨脹,開運算,閉運算…)
第5行cv.dilate()函數即形態學膨脹功能函數
現在我們已經得到了硬幣區域并且將它們分割了,在某些情況下,我們只對前景分割感興趣,而對是否接觸或分離接觸并不感興趣,所以這個時候我們不用使用距離變換,只需要侵蝕就可以滿足我們的需求了(侵蝕是一種提取確定前景區域的重要方法)
現在我們可以創建標記了,使用cv.connectedComponents()就很不錯,它用0標記圖像的背景,然后其他對象用從1開始的整數標記,標記完成后使用分水嶺方法cv.watershed()完成圖像分割
# 類別標記 ret, markers = cv.connectedComponents(sure_fg) # 為所有的標記加1,保證背景是0而不是1 markers = markers+1 # 現在讓所有的未知區域為0 markers[unknown==255] = 0 markers = cv.watershed(img,markers) img[markers == -1] = [255,0,0]標記完成后使用分水嶺方法cv.watershed()完成圖像分割
plt.subplot(241), plt.imshow(cv2.cvtColor(src, cv2.COLOR_BGR2RGB)), plt.title('Original'), plt.axis('off') plt.subplot(242), plt.imshow(thresh, cmap='gray'), plt.title('Threshold'), plt.axis('off') plt.subplot(243), plt.imshow(sure_bg, cmap='gray'), plt.title('Dilate'), plt.axis('off') plt.subplot(244), plt.imshow(dist_transform, cmap='gray'), plt.title('Dist Transform'), plt.axis('off') plt.subplot(245), plt.imshow(sure_fg, cmap='gray'), plt.title('Threshold'), plt.axis('off') plt.subplot(246), plt.imshow(unknown, cmap='gray'), plt.title('Unknow'), plt.axis('off') plt.subplot(247), plt.imshow(np.abs(markers), cmap='jet'), plt.title('Markers'), plt.axis('off') plt.subplot(248), plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)), plt.title('Result'), plt.axis('off') plt.show()代碼資源參考自:OpenCV-Python——第22章:分水嶺算法實現圖像分割
4. 使用GrabCut算法實現交互式前景提取
4.1 GrabCut算法
🚀起源:GrabCut算法由英國微軟研究院的Carsten Rother,Vladimir Kolmogorov和Andrew Blake設計,在他們的論文“GrabCut”中:使用迭代圖割的交互式前景提取
🚀算法步驟:最初用戶在前景區域周圍繪制一個矩形(前景區域應完全位于矩形內部),然后算法會對其進行迭代分割,以獲得最佳結果。做完了但在某些情況下,分割可能不會很好,例如,可能已將某些前景區域標記為背景,反之亦然。在這種情況下,需要用戶進行精修。只需在圖像錯誤分割區域上畫些筆畫,筆畫會對算法說: “嘿,該區域應該是前景,你將其標記為背景,在下一次迭代中對其進行校正”或與背景相反,然后在下一次迭代中,我們將獲得更好的結果
🚀算法原理:用戶輸入矩形后,此矩形外部的所有內容都將作為背景(這是在矩形應包含所有對象之前提到的原因),而矩形內的所有內容都是未知的。任何指定前景和背景的用戶輸入都被視為硬標簽,這意味著它們在此過程中不會更改。計算機根據我們提供的數據進行初始標記,它標記前景和背景像素(或對其進行硬標記)
- 現在使用**高斯混合模型(GMM)**對前景和背景進行建模。 根據我們提供的數據,GMM可以學習并創建新的像素分布。也就是說,未知像素根據顏色統計上與其他硬標記像素的關系而被標記為可能的前景或可能的背景(就像聚類一樣)
- 根據此像素分布構建圖形,圖中的節點為像素。添加了另外兩個節點,即“源”節點和“接收器”節點。每個前景像素都連接到源節點,每個背景像素都連接到接收器節點
- 通過像素是前景/背景的概率來定義將像素連接到源節點/末端節點的邊緣的權重。像素之間的權重由邊緣信息或像素相似度定義。如果像素顏色差異很大,則它們之間的邊緣將變低
- 然后使用mincut算法對圖進行分割。它將圖切成具有最小成本函數的兩個分離的源節點和宿節點。成本函數是被切割邊緣的所有權重的總和。剪切后,連接到“源”節點的所有像素都變為前景,而連接到“接收器”節點的像素都變為背景
- 繼續該過程,直到分類收斂為止
4.2 使用OpenCV進行GrabCut算法
OpenCV提供了函數cv.grabCut(),這個函數的參數同樣有些多,我們接著攤開聊聊,其中包括7個參數,第一個參數**- img -即源圖像,第二個參數- mask -是掩碼圖像,在其中我們會指定哪些區域為背景,第三個參數- rect -是它的矩形坐標,格式為(x, y, w, h),其中包括前景對象,第四第五個參數 - bdgModel, fgdModel - 是算法內部使用的數組,我們只需要創建兩個大小為(1,65)的np.float64類型零數組,第六個參數- iterCount -是算法應運行的迭代次數,第七個參數- model -**應該是cv.GC_INIT_WITH_RECT或cv.GC_INIT_WITH_MASK或兩者結合,決定我們要繪制矩形還是最終的修飾筆觸,廢話不多說上實例
import numpy as np import cv2 as cv from matplotlib import pyplot as pltimg = cv.imread(r'E:\image\test22.png') mask = np.zeros(img.shape[:2], np.uint8) bgdModel = np.zeros((1, 65), np.float64) fgdModel = np.zeros((1, 65), np.float64) rect = (50, 50, 450, 290) cv.grabCut(img, mask, rect, bgdModel, fgdModel, 5, cv.GC_INIT_WITH_RECT) mask2 = np.where((mask == 2) | (mask == 0), 0, 1).astype('uint8') img = img * mask2[:, :, np.newaxis] plt.imshow(img), plt.colorbar(), plt.show()有時候我們分割手部骨骼,發現少了一根手指頭,而且個別手指還沒顯示完全,我們這時就需要用畫筆精修了,在paint應用程序中打開輸入圖像,并在圖像中添加了另一層,使用畫筆中的畫筆工具,在新圖層上用白色標記了錯過的前景,而用白色標記了不需要的背景(例如logo,地面等),然后用灰色填充剩余的背景,然后將該mask圖像加載到OpenCV中,編輯我們在新添加的mask圖像中具有相應值的原始mask圖像
# newmask是我手動標記過的mask圖像 newmask = cv.imread('newmask.png',0) # 標記為白色(確保前景)的地方,更改mask = 1 # 標記為黑色(確保背景)的地方,更改mask = 0 mask[newmask == 0] = 0 mask[newmask == 255] = 1 mask, bgdModel, fgdModel = cv.grabCut(img,mask,None,bgdModel,fgdModel,5,cv.GC_INIT_WITH_MASK) mask = np.where((mask==2)|(mask==0),0,1).astype('uint8') img = img*mask[:,:,np.newaxis] plt.imshow(img),plt.colorbar(),plt.show()(注:文章內容參考OpenCV4.1中文官方文檔)
如果文章對您有所幫助,記得一鍵三連支持一下哦👍+??+📝
總結
以上是生活随笔為你收集整理的OpenCV中的图像处理 —— 霍夫线 / 圈变换 + 图像分割(分水岭算法) + 交互式前景提取(GrabCut算法)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 相对论的应用
- 下一篇: Elasticsearch:Elasti