天天看點

深度估計與分割

深度攝像頭

深度攝像頭是極少的在捕獲圖像時能估計物體與攝像頭之間距離的裝置。深度攝像頭(例如微軟的Kniect)将傳統攝像頭和一個紅外傳感器相結合來幫助攝像頭差別相似物體并計算他們與攝像頭的距離。但是該類攝像頭比較昂貴。(深度估計的方法與普通攝像頭有差别)

深度估計與分割

普通攝像頭進行深度估計

使用普通攝像頭進行深度估計主要用到的方法是幾何學中的極幾何,它屬于立體視覺(stereo vision)幾何學。立體視覺幾何學是計算機視覺的一個分支,從同一物體的兩張不同圖像提取三維資訊

視差:從一定距離的兩個點上觀察同一目标所産生的方向差異(百度百科),使用視差角來衡量

視差與深度的關系可以檢視文章:https://blog.csdn.net/weixin_34128237/article/details/86938913

下面為使用OpenCV 如何使用極幾何計算視差圖,它是對圖像中檢測到的不同深度的基本表示。這樣就能提取出一張圖檔的前景部分而抛棄其他部分。

首先需要同一物體在不同視角下拍攝兩幅圖像,但是要注意的是這兩幅圖像是距物體相同距離拍攝的,否則計算将會失敗,視差圖也就沒有了意義

import numpy as np
import cv2
def update(val=0):
    stereo.setBlockSize(cv2.getTrackbarPos('window_size', 'disparity'))
    stereo.setUniquenessRatio(cv2.getTrackbarPos('uniquenessRatio', 'disparity'))
    stereo.setSpeckleWindowSize(cv2.getTrackbarPos('speckleWindowSize', 'disparity'))
    stereo.setSpeckleRange(cv2.getTrackbarPos('speckleRange', 'disparity'))
    stereo.setDisp12MaxDiff(cv2.getTrackbarPos('disp12MaxDiff', 'disparity'))

    print ('computing disparity...')
    disp = stereo.compute(imgL,imgR).astype(np.float32) / 16.0

    cv2.imshow('left', imgL)
    cv2.imshow('disparity', (disp - min_disp) / num_disp)


if __name__ == "__main__":
    window_size = 5
    min_disp = 16
    num_disp = 192 - min_disp
    blockSize = window_size
    uniquenessRatio = 1
    speckleRange = 3
    speckleWindowSize = 3
    disp12MaxDiff = 200
    P1 = 600
    P2 = 2400
    imgL = cv2.imread('lefteye.png')
    imgR = cv2.imread('righteye.png')
    cv2.namedWindow('disparity')
    # 最後一個參數為回調函數
    cv2.createTrackbar('speckleRange', 'disparity', speckleRange, 50, update)
    cv2.createTrackbar('window_size', 'disparity', window_size, 21, update)
    cv2.createTrackbar('speckleWindowSize', 'disparity', speckleWindowSize, 200, update)
    cv2.createTrackbar('uniquenessRatio', 'disparity', uniquenessRatio, 50, update)
    cv2.createTrackbar('disp12MaxDiff', 'disparity', disp12MaxDiff, 250, update)
    stereo = cv2.StereoSGBM_create(
        minDisparity=min_disp,
        numDisparities=num_disp,
        blockSize=window_size,
        uniquenessRatio=uniquenessRatio,
        speckleRange=speckleRange,
        speckleWindowSize=speckleWindowSize,
        disp12MaxDiff=disp12MaxDiff,
        P1=P1,
        P2=P2
    )
    update()
    cv2.waitKey()


           

上述示例處理過程:加載兩幅圖像,建立一個StereoSGBM執行個體(StereoSGBM是 semiglobal block matching 的縮寫,這是一種計算視差圖的算法),并建立幾個跟蹤條(cv2.createTrackbar)來調整算法參數,然後調用update函數。

update 函數将跟蹤條的值傳給 StereoSGBM 執行個體,然後調用 compute 方法來得到一個視差圖。這個過程相當簡單!

StereoSGBM 用到的幾個參數如下 :

參數 描述
minDisparity 表示可能的最小視內插補點。它通常為零,但有時校正算法會易懂圖像,是以參數值也要相應的調整
numDisparity 這個參數表示最大的視差與最小的視內插補點之差。這個內插補點總是大于0。在上述執行個體中,這個值要能被16整除
windowSize 這個參數為一個比對塊的大小,它必須是大于等于1的奇數。通常在3-11之間
P1 這個參數是控制視差圖平滑度的第一個參數。具體看下面介紹
P2 這個參數是控制視差圖平滑度的第二個參數。這個值越大,視差圖越平滑。P1是鄰近像素間視內插補點變化為1的懲罰值,P2是鄰近像素間視內插補點變化大于1時的懲罰值。算法要求P2>P1。stereo_match.cpp 樣例中給出一些P1和P2的合理取值(比如,P1、P2分别是8*number_of_image_channelswindowSizewindowSize和32*number_of_image_channelswindowSizewindowSize)
disp12MaxDiff 這個參數表示在左右視差檢查中最大允許的偏差(整數像素機關)。設為非正值将不做檢查
preFilterCap 這個參數表示預過濾圖像像素的截斷值。算法首先計算每個像素在x方向上的衍生值
uniquenessRatio 這個參數表示由代價函數計算得到的最好(最小)結果值比第二好的值小多少(用百分比表示)才被認為是正确的。通常在5~15之間就可以了
speckleWindowSize 這個參數表示平滑視差區域的最大視窗尺寸,以考慮噪聲斑點或無效性。将它設為0就不會進行斑點過濾。否則應取 50 ~ 200 之間的某個值
speckleRange 該參數是指每個已連接配接部分的最大視差變化。如果進行斑點過濾,則該參數取正值,函數會自動乘以16.一般情況下該參數取1或2就足夠了

使用分水嶺和GrabCut算法進行物體分割

使用grabCut 進行前景檢測

視差圖對檢測圖檔前景部分很有用,StereoSGBM 是很好的一種方法,但是它主要是從二維圖檔中擷取三維資訊。真正的實作前景檢測和顯示還是需要

grabCut()

實作,算法的實作步驟:

  1. 預定以一個含有(一個或多個)物體的矩形(必須包含想要分割的物體)
  2. 矩形外的區域都會被認為是背景部分
  3. 對于預定義的矩形部分,會通過背景部分區分其中的背景和前景區域。
  4. 用高斯混合模型(Gaussians Mixture Model, GMM)來對背景和前景模組化,未定義部分标記為可能的前景或背景(可能是前景,可能是背景)
  5. 圖像中的每一個像素點都被看作通過一條虛拟邊與周圍像素點相連接配接,每條邊都有一個屬于前景或背景的機率,這基于它與周圍像素顔色的相似性(一般同一物體的像素點具有連續性和相似性)
  6. 每一個像素點會與前景或背景節點連接配接
  7. 在節點連接配接完成後會存在,若節點之間的邊屬于不同終端則将他們分離出來了
背景部分為黑色,前景部分為白色

下面是使用

cv2.grabCut()

進行圖像分割的示例

import  numpy as np
import  cv2
import  matplotlib.pyplot as plt

img=cv2.imread('hello.jpg',cv2.IMREAD_UNCHANGED)
# 建立與圖像大小相同的黑色掩模
mask=np.zeros(img.shape[:2],np.uint8)
# 定義兩個前景和背景模型
# 這兩個模型會用于算法内部使用的數組,大小為 (1,65)類型我為np.float64
bgdModel=np.zeros((1,65),np.float64)
fgdModel=np.zeros((1,65),np.float64)

#預定義包含想分割出來前景的矩形
rect=(300,20,600,600)
# 調用grabCut 方法
# 第6個參數為算法的疊代次數,可以把他設的更大,但是像素分類總會收斂在某個地方,在增大效果也相同
cv2.grabCut(img,mask,rect,bgdModel,fgdModel,8,cv2.GC_INIT_WITH_RECT)
# 當調用grabCut 算法過後掩模mask 會變成隻有0 ~ 3的值
# 分别代表 0背景 1前景 2可能是背景 3可能是前景
# 使用np.where方法将值為0和2的轉為0,值為1和3的将轉為1,然後儲存在mask2中
mask2=np.where((mask==2)|(mask==0),0,1).astype('uint8')
# 使用mask2将背景與前景部分區分開來
img=img*mask2[:,:,np.newaxis]
plt.subplot(121),plt.imshow(img)
plt.title('grabcut'),plt.xticks([]),plt.yticks([])
plt.subplot(122),plt.imshow(cv2.cvtColor((cv2.imread('hello.jpg')),cv2.COLOR_BGR2RGB))
plt.title('original'),plt.xticks([]),plt.yticks([])
plt.show()

           
深度估計與分割
使用分水嶺算法進行圖像分割

算法叫做分水嶺是因為有水的概念。把圖像中低密度的區域(變化很少)想象成山谷,圖像中高密度的區域(變化很多)想象成山峰。開始向山谷中注入水知道不同的山谷中的水開始彙聚。為了阻止不同山谷的水彙聚,可以設定一些栅欄,最後得到的栅欄就是圖像分割。

算法過程:

  1. 使用大津算法将圖像二值化
  2. 形态學開變換

    morphologyEX

    去除圖像中的小白點
  3. 膨脹擷取絕大部分為背景的圖像
  4. 使用

    distanceTransform

    擷取前景部分
  5. 重合部分利用背景和前景相減擷取(一般為邊界處)
  6. 使用

    connectedComponents

    獲得标記(将背景标記為0,其他對象用從1開始的整數标記)
  7. 将标記加一(防止背景部分被當作unknown),将unknown 部分置0
  8. 最後打開門,讓水漫起來并把栅欄設定為紅色
import numpy as np
import cv2
from matplotlib import pyplot as plt

img = cv2.imread('water_coins.jpg')
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(gray,0,255,cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU)

# 形态學變換去除噪聲資料
# 開運算:去除小白點噪聲
# 閉運算:去除對象中的小孔
kernel = np.ones((3,3),np.uint8)
opening = cv2.morphologyEx(thresh,cv2.MORPH_OPEN,kernel, iterations = 2)

# 通過對morphologyEX 之後的圖像進行膨脹操作,可以得到大部分都是背景的圖像
sure_bg = cv2.dilate(opening,kernel,iterations=3)
# 通過distanceTransform 來擷取确定的前景區域(圖像中最可能是前景的部分)
# 通過距離運算(離背景越遠的點越可能是前景)
# 距離變換後為灰階級圖像,即距離圖像,圖像種每個像素的灰階值為該像素與其最近的背景像素之間的距離
dist_transform = cv2.distanceTransform(opening,cv2.DIST_L2,5)
# 通過門檻值來去頂哪些區域是前景
ret, sure_fg = cv2.threshold(dist_transform,0.7*dist_transform.max(),255,0)


# 尋找未知區域
sure_fg = np.uint8(sure_fg)
# 前景與背景存在重合部分,通過背景與前景相減
unknown = cv2.subtract(sure_bg,sure_fg)


# 用0标記背景區域,其他對象用從1開始的整數标記
ret, markers = cv2.connectedComponents(sure_fg)
# 如果不标記背景區域會被認為是unknown
markers = markers+1
# 标記未知區域為0
markers[unknown==255] = 0
markers = cv2.watershed(img,markers)
img[markers==-1]=[0,255,255]


plt.subplot(121)
plt.imshow(img)
plt.title('waterImg')


plt.subplot(122)
plt.imshow(cv2.imread('water_coins.jpg',cv2.IMREAD_UNCHANGED))
plt.title('original')

plt.show()
cv2.waitKey()
cv2.destroyAllWindows()

           

不确定部分(unknown):

深度估計與分割

從上圖就可以很明顯的看出未知部分和前景部分圓孔中的黑色為确定的前景部分,調用

watershed

進行”漫水“操作,水會沿着unknown部分慢慢散開,當兩個山谷水位相碰時則會停止,最終結果如下:

深度估計與分割

主要部分解析:

dist_transform=cv2.distanceTranceform(opening,cv2.DIST_L2,5)
ret,sure_fg=cv2.threshold(dist_transform,0.7*dis_transform.max(),255,0)
           

使用 distanceTranceform 擷取确定的前景部分,在使用門檻值處理哪些為前景部分(估計)

這個函數主要将前景部分确定的為前景的節點連接配接起來,将背景标記為0,其他部分(确定前景部分)置為大于1的整數

markers=markers+1
markers[unknown==255]=0
markers=cv.watershed(img,markers)
img[markers==-1]=[255,0,0]
           

将标記加1再把未知部分置為0(防止背景被當作unknown),如下圖藍色部分為未知部分(unknown),裡邊的小點為确定的前景部分(sure_fg),不确定部分使用漫水的方法慢慢的流入山谷,如果碰到栅欄就結束(标記為-1),将栅欄畫出來

深度估計與分割

總結

stereoSGBM:掌握從二維輸入(一段視訊或者一幅圖像)中得到三維資訊,使用極幾何進行視差圖的計算

使用

grabCut

和分水嶺算法進行圖像分割

繼續閱讀