OpenCV中的圖像處理 —— 霍夫線 / 圈變換 + 圖像分割(分水嶺算法) + 交互式前景提?。℅rabCut算法)
??上一節(jié)我們介紹了OpenCV中傅里葉變換和模板匹配,這一部分我們來(lái)聊一聊霍夫線/圈變換的原理和應(yīng)用、使用分水嶺算法實(shí)現(xiàn)圖像分割和使用GrabCut算法實(shí)現(xiàn)交互式前景提取
??哈嘍大家好,這里是ErrorError!,一枚某高校大二本科在讀的♂同學(xué),希望未來(lái)在機(jī)器視覺領(lǐng)域能夠有所成就,很榮幸能夠在CSDN結(jié)識(shí)眾多志同道合和在各方面都有所造詣的小伙伴,我們一起加油吧~??
??上節(jié)內(nèi)容:OpenCV中的圖像處理 —— 傅里葉變換+模板匹配
1. 霍夫線變換
1.1 HoughLines工作原理
經(jīng)過(guò)上一節(jié)中”模板匹配”的了解,是不是發(fā)現(xiàn)我們有點(diǎn)兒目標(biāo)檢測(cè)的雛形了呢?這一部分說(shuō)的霍夫線變換也是一個(gè)不斷深入的關(guān)鍵點(diǎn)。如果可以如果可以用數(shù)學(xué)形式表示形狀,則霍夫變換是一種檢測(cè)任何形狀的流行技術(shù),即使形狀有些破損或變形,也可以檢測(cè)出形狀,我們將看到它如何作用于一條線
??霍夫線眼中的線:通常一條線可以表示為y=mx+cy=mx+c或以參數(shù)形式表示為ρ=xcosθ+ysinθ,其中ρ是從原點(diǎn)到該線的垂直距離,而θ是由該垂直線和水平軸形成的角度以逆時(shí)針方向測(cè)量(該方向隨我們?nèi)绾伪硎咀鴺?biāo)系而變化),因此,如果線在原點(diǎn)下方通過(guò),則它將具有正的ρ且角度小于180,如果線在原點(diǎn)上方,則將角度取為小于180,而不是大于180的角度,ρ取負(fù)值,任何垂直線將具有0度,水平線將具有90度
??霍夫線怎么處理線:任何一條線都可以用(ρ,θ)這兩個(gè)術(shù)語(yǔ)表示。因此,首先創(chuàng)建2D數(shù)組或累加器(以保存兩個(gè)參數(shù)的值),并將其初始設(shè)置為0。讓行表示ρ,列表示θ,陣列的大小取決于所需的精度。假設(shè)我們希望角度的精度為1度,則需要180列。對(duì)于ρ,最大距離可能是圖像的對(duì)角線長(zhǎng)度。因此,以一個(gè)像素精度為準(zhǔn),行數(shù)可以是圖像的對(duì)角線長(zhǎng)度
??放在實(shí)際圖像中:假設(shè)有一個(gè)100*100的圖像,中間有一條水平線。取直線的第一點(diǎn),且我們知道它的坐標(biāo)(x,y)
值?,F(xiàn)在在線性方程式中,將值θ= 0,1,2,… 180放進(jìn)去,然后檢查得到ρ。對(duì)于每對(duì)(ρ,θ),在累加器中對(duì)應(yīng)的(ρ,θ)單元格將值增加1。
現(xiàn)在,對(duì)行的第二個(gè)點(diǎn)執(zhí)行與上述相同的操作,遞增(ρ,θ)對(duì)應(yīng)的單元格中的值,這一次操作使單元格(50,90)=2
。實(shí)際上,我們正在對(duì)(ρ,θ)值進(jìn)行投票。我們對(duì)線路上的每個(gè)點(diǎn)都繼續(xù)執(zhí)行此過(guò)程。在每個(gè)點(diǎn)上,單元格(50,90)都會(huì)增加或投票,而其他單元格可能會(huì)或可能不會(huì)投票。這樣一來(lái),最后,單元格(50,90)的投票數(shù)將最高。因此,如果我們?cè)诶奂悠髦兴阉髯畲笃睌?shù),則將獲得(50,90)值,該值表示該圖像中的一條線與原點(diǎn)的距離為50,角度為90度
1.2 OpenCV中的霍夫曼變換
OpenCV把上述所有的霍夫曼變換過(guò)程都封裝在了函數(shù)cv.HoughLines()里,它返回的是一個(gè)math:(rho,theta)值的數(shù)組,ρ以像素為單位,θ以弧度為單位。這個(gè)函數(shù)包括4個(gè)參數(shù),第一個(gè)即二進(jìn)制原圖,因此在使用霍夫曼變換之前我們會(huì)先使用閾值或Canny邊緣檢測(cè),第二、第三個(gè)參數(shù)是ρ和θ的精度,第四個(gè)參數(shù)是閾值,它意味著行的最低投票,票數(shù)取決于線上的點(diǎn)數(shù),因此這個(gè)閾值也表示檢測(cè)到的最小線長(zhǎng)
import cv2 as cv
import numpy as np
img = 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 * rho
y0 = b * rho
x1 = 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 概率霍夫線變換
在霍夫線變換中即使對(duì)于帶有兩個(gè)參數(shù)的行,也需要大量計(jì)算,概率霍夫變換是我們看到的霍夫變換的優(yōu)化,它沒有考慮所有要點(diǎn)。取而代之的是,它僅采用隨機(jī)的點(diǎn)子集,足以進(jìn)行線檢測(cè),只是我們必須降低閾值
OpenCV的實(shí)現(xiàn)基于Matas,J.和Galambos,C.和Kittler, J.V.使用漸進(jìn)概率霍夫變換對(duì)行進(jìn)行的穩(wěn)健檢測(cè)[145]。使用的函數(shù)是cv.HoughLinesP()。它有兩個(gè)新的屬性:1. - minLineLength - 最小行長(zhǎng),小于此長(zhǎng)度的線段將被拒絕;2. - maxLineGap - 線段之間允許將它們視為一條線的最大間隙
import cv2 as cv
import numpy as np
img = 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. 霍夫圈變換
上面說(shuō)完了霍夫線變換,現(xiàn)在挨到霍夫圈變換了,也就是說(shuō)霍夫線變換面向的是圖像中的線,而圈變換面向的就是圓咯
圓在數(shù)學(xué)上表示為(x?xcenter)^ 2+(y?ycenter)^2= r^2,其中(xcenter,ycenter)(xcenter,ycenter)是圓的中心,rr是圓的半徑,從等式中,我們可以看到我們有3個(gè)參數(shù),因此我們需要3D累加器進(jìn)行霍夫變換,這將非常低效。因此,OpenCV使用更加技巧性的方法,即使用邊緣的梯度信息的Hough梯度方法,我們?cè)谶@里使用的函數(shù)是cv.HoughCircles()
這個(gè)函數(shù)參數(shù)有點(diǎn)兒多,我們有必要說(shuō)說(shuō),它的原型是cv2.HoughCircles(image, method, dp, minDist, circles, param1, param2, minRadius, maxRadius),第一個(gè)參數(shù)為原圖像(灰度圖),第二個(gè)參數(shù)是檢測(cè)方法,第三個(gè)參數(shù)為檢測(cè)內(nèi)側(cè)圓心的累加器圖像的分辨率于輸入圖像之比的倒數(shù),如dp=1,累加器和輸入圖像具有相同的分辨率,如果dp=2,累計(jì)器便有輸入圖像一半那么大的寬度和高度,第四個(gè)參數(shù)表示兩個(gè)圓之間圓心的最小距離
param1與param2有默認(rèn)值100,它們是method設(shè)置的檢測(cè)方法的對(duì)應(yīng)的參數(shù),對(duì)當(dāng)前唯一的方法霍夫梯度法cv2.HOUGH_GRADIENT,param1表示傳遞給canny邊緣檢測(cè)算子的高閾值,而低閾值為高閾值的一半,param2表示在檢測(cè)階段圓心的累加器閾值,它越小,就越可以檢測(cè)到更多根本不存在的圓,而它越大的話,能通過(guò)檢測(cè)的圓就更加接近完美的圓形了
minRadius和maxRadius有默認(rèn)值0,分別表示圓半徑的最小值和最大值
import numpy as np
import cv2 as cv
img = 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 分水嶺算法
??算法思想:任何灰度圖像都可以看作是一個(gè)地形表面,其中高強(qiáng)度表示山峰,低強(qiáng)度表示山谷,我們用不同顏色的**水(標(biāo)簽)**填充每個(gè)孤立的山谷(局部最小值)。隨著水位的上升,根據(jù)附近的山峰(坡度),來(lái)自不同山谷的水明顯會(huì)開始合并,顏色也不同。為了避免這種情況,我們要在水融合的地方建造屏障。繼續(xù)填滿水,建造障礙,直到所有的山峰都在水下。然后我們創(chuàng)建的屏障將返回你的分割結(jié)果
但是這種方法會(huì)由于圖像中的噪聲或其他不規(guī)則性而產(chǎn)生過(guò)度分割的結(jié)果。因此OpenCV實(shí)現(xiàn)了一個(gè)基于標(biāo)記的分水嶺算法,我們可以指定哪些是要合并的山谷點(diǎn),哪些不是。這是一個(gè)交互式的圖像分割。我們所做的是給我們知道的對(duì)象賦予不同的標(biāo)簽。用一種顏色(或強(qiáng)度)標(biāo)記我們確定為前景或?qū)ο蟮膮^(qū)域,用另一種顏色標(biāo)記我們確定為背景或非對(duì)象的區(qū)域,最后用0
標(biāo)記我們不確定的區(qū)域。這是我們的標(biāo)記。然后應(yīng)用分水嶺算法。然后我們的標(biāo)記將使用我們給出的標(biāo)簽進(jìn)行更新,對(duì)象的邊界值將為-1
3.2 圖像分割的實(shí)現(xiàn)
有一張布滿硬幣的白紙,部分硬幣之間相互接觸,我們將這張圖作為源圖像,我們先從尋找硬幣的近似估計(jì)開始,因此我們要使用閾值化(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)
然后由于分水嶺算法對(duì)噪聲非常敏感,所有我們要去除圖像中的所有白點(diǎn)噪聲,為此我們可以使用形態(tài)學(xué)擴(kuò)張,如果要去除硬幣對(duì)象中的小孔,我們可以使用形態(tài)學(xué)侵蝕,在進(jìn)行完這些操作后,我們可以十分確信靠近對(duì)象中心的區(qū)域是前景,離對(duì)象中心很遠(yuǎn)的就是背景,現(xiàn)在我們唯一不確定的就是硬幣的邊界區(qū)域
接下來(lái)我們需要提取我們可確認(rèn)為硬幣的區(qū)域(因?yàn)榍治g會(huì)去除邊界像素),如果硬幣之間不接觸那么我們之前的操作完全沒問(wèn)題,但是事實(shí)是他們接觸了,因此我們更好的選擇是找到距離變換并應(yīng)用適當(dāng)?shù)拈撝?。此時(shí)我們需要確定一定不是硬幣的區(qū)域,形態(tài)學(xué)擴(kuò)張可以滿足我們的需求
剩下的區(qū)域是我們不知道的區(qū)域,無(wú)論是硬幣還是背景。分水嶺算法應(yīng)該找到它。這些區(qū)域通常位于前景和背景相遇(甚至兩個(gè)不同的硬幣相遇)的硬幣邊界附近??梢酝ㄟ^(guò)從sure_bg
區(qū)域中減去sure_fg
區(qū)域來(lái)獲得
# 噪聲去除
kernel = np.ones((3,3),np.uint8)
opening = cv.morphologyEx(thresh,cv.MORPH_OPEN,kernel, iterations = 2)
# 確定背景區(qū)域
sure_bg = cv.dilate(opening,kernel,iterations=3)
# 尋找前景區(qū)域
dist_transform = cv.distanceTransform(opening,cv.DIST_L2,5)
ret, sure_fg = cv.threshold(dist_transform,0.7*dist_transform.max(),255,0)
# 找到未知區(qū)域
sure_fg = np.uint8(sure_fg)
unknown = cv.subtract(sure_bg,sure_fg)
??代碼解析:第3行的cv.morphologyEx()函數(shù)是高級(jí)形態(tài)學(xué)轉(zhuǎn)換函數(shù),其功能取決于第二個(gè)參數(shù)的選取,具體內(nèi)容請(qǐng)移步 OpenCV-Python——第13章:圖像的形態(tài)學(xué)操作(腐蝕,膨脹,開運(yùn)算,閉運(yùn)算…)
第5行cv.dilate()函數(shù)即形態(tài)學(xué)膨脹功能函數(shù)
現(xiàn)在我們已經(jīng)得到了硬幣區(qū)域并且將它們分割了,在某些情況下,我們只對(duì)前景分割感興趣,而對(duì)是否接觸或分離接觸并不感興趣,所以這個(gè)時(shí)候我們不用使用距離變換,只需要侵蝕就可以滿足我們的需求了(侵蝕是一種提取確定前景區(qū)域的重要方法)
現(xiàn)在我們可以創(chuàng)建標(biāo)記了,使用cv.connectedComponents()就很不錯(cuò),它用0標(biāo)記圖像的背景,然后其他對(duì)象用從1開始的整數(shù)標(biāo)記,標(biāo)記完成后使用分水嶺方法cv.watershed()完成圖像分割
# 類別標(biāo)記
ret, markers = cv.connectedComponents(sure_fg)
# 為所有的標(biāo)記加1,保證背景是0而不是1
markers = markers+1
# 現(xiàn)在讓所有的未知區(qū)域?yàn)?
markers[unknown==255] = 0
markers = cv.watershed(img,markers)
img[markers == -1] = [255,0,0]
標(biāo)記完成后使用分水嶺方法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章:分水嶺算法實(shí)現(xiàn)圖像分割
4. 使用GrabCut算法實(shí)現(xiàn)交互式前景提取
4.1 GrabCut算法
??起源:GrabCut算法由英國(guó)微軟研究院的Carsten Rother,Vladimir Kolmogorov和Andrew Blake設(shè)計(jì),在他們的論文“GrabCut”中:使用迭代圖割的交互式前景提取
??算法步驟:最初用戶在前景區(qū)域周圍繪制一個(gè)矩形(前景區(qū)域應(yīng)完全位于矩形內(nèi)部),然后算法會(huì)對(duì)其進(jìn)行迭代分割,以獲得最佳結(jié)果。做完了但在某些情況下,分割可能不會(huì)很好,例如,可能已將某些前景區(qū)域標(biāo)記為背景,反之亦然。在這種情況下,需要用戶進(jìn)行精修。只需在圖像錯(cuò)誤分割區(qū)域上畫些筆畫,筆畫會(huì)對(duì)算法說(shuō): “嘿,該區(qū)域應(yīng)該是前景,你將其標(biāo)記為背景,在下一次迭代中對(duì)其進(jìn)行校正”或與背景相反,然后在下一次迭代中,我們將獲得更好的結(jié)果
??算法原理:用戶輸入矩形后,此矩形外部的所有內(nèi)容都將作為背景(這是在矩形應(yīng)包含所有對(duì)象之前提到的原因),而矩形內(nèi)的所有內(nèi)容都是未知的。任何指定前景和背景的用戶輸入都被視為硬標(biāo)簽,這意味著它們?cè)诖诉^(guò)程中不會(huì)更改。計(jì)算機(jī)根據(jù)我們提供的數(shù)據(jù)進(jìn)行初始標(biāo)記,它標(biāo)記前景和背景像素(或?qū)ζ溥M(jìn)行硬標(biāo)記)
- 現(xiàn)在使用**高斯混合模型(GMM)**對(duì)前景和背景進(jìn)行建模。 根據(jù)我們提供的數(shù)據(jù),GMM可以學(xué)習(xí)并創(chuàng)建新的像素分布。也就是說(shuō),未知像素根據(jù)顏色統(tǒng)計(jì)上與其他硬標(biāo)記像素的關(guān)系而被標(biāo)記為可能的前景或可能的背景(就像聚類一樣)
- 根據(jù)此像素分布構(gòu)建圖形,圖中的節(jié)點(diǎn)為像素。添加了另外兩個(gè)節(jié)點(diǎn),即“源”節(jié)點(diǎn)和“接收器”節(jié)點(diǎn)。每個(gè)前景像素都連接到源節(jié)點(diǎn),每個(gè)背景像素都連接到接收器節(jié)點(diǎn)
- 通過(guò)像素是前景/背景的概率來(lái)定義將像素連接到源節(jié)點(diǎn)/末端節(jié)點(diǎn)的邊緣的權(quán)重。像素之間的權(quán)重由邊緣信息或像素相似度定義。如果像素顏色差異很大,則它們之間的邊緣將變低
- 然后使用mincut算法對(duì)圖進(jìn)行分割。它將圖切成具有最小成本函數(shù)的兩個(gè)分離的源節(jié)點(diǎn)和宿節(jié)點(diǎn)。成本函數(shù)是被切割邊緣的所有權(quán)重的總和。剪切后,連接到“源”節(jié)點(diǎn)的所有像素都變?yōu)榍熬?,而連接到“接收器”節(jié)點(diǎn)的像素都變?yōu)楸尘?/li>
- 繼續(xù)該過(guò)程,直到分類收斂為止
4.2 使用OpenCV進(jìn)行GrabCut算法
OpenCV提供了函數(shù)cv.grabCut(),這個(gè)函數(shù)的參數(shù)同樣有些多,我們接著攤開聊聊,其中包括7個(gè)參數(shù),第一個(gè)參數(shù)**- img -即源圖像,第二個(gè)參數(shù)- mask -是掩碼圖像,在其中我們會(huì)指定哪些區(qū)域?yàn)楸尘埃谌齻€(gè)參數(shù)- rect -是它的矩形坐標(biāo),格式為(x, y, w, h),其中包括前景對(duì)象,第四第五個(gè)參數(shù) - bdgModel, fgdModel - 是算法內(nèi)部使用的數(shù)組,我們只需要?jiǎng)?chuàng)建兩個(gè)大小為(1,65)的np.float64類型零數(shù)組,第六個(gè)參數(shù)- iterCount -是算法應(yīng)運(yùn)行的迭代次數(shù),第七個(gè)參數(shù)- model -**應(yīng)該是cv.GC_INIT_WITH_RECT或cv.GC_INIT_WITH_MASK或兩者結(jié)合,決定我們要繪制矩形還是最終的修飾筆觸,廢話不多說(shuō)上實(shí)例
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
img = 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()
有時(shí)候我們分割手部骨骼,發(fā)現(xiàn)少了一根手指頭,而且個(gè)別手指還沒顯示完全,我們這時(shí)就需要用畫筆精修了,在paint應(yīng)用程序中打開輸入圖像,并在圖像中添加了另一層,使用畫筆中的畫筆工具,在新圖層上用白色標(biāo)記了錯(cuò)過(guò)的前景,而用白色標(biāo)記了不需要的背景(例如logo,地面等),然后用灰色填充剩余的背景,然后將該mask圖像加載到OpenCV中,編輯我們?cè)谛绿砑拥膍ask圖像中具有相應(yīng)值的原始mask圖像文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-402451.html
# newmask是我手動(dòng)標(biāo)記過(guò)的mask圖像
newmask = cv.imread('newmask.png',0)
# 標(biāo)記為白色(確保前景)的地方,更改mask = 1
# 標(biāo)記為黑色(確保背景)的地方,更改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()
(注:文章內(nèi)容參考OpenCV4.1中文官方文檔)
如果文章對(duì)您有所幫助,記得一鍵三連支持一下哦??+??+??文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-402451.html
到了這里,關(guān)于OpenCV中的圖像處理 —— 霍夫線 / 圈變換 + 圖像分割(分水嶺算法) + 交互式前景提?。℅rabCut算法)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!