深度摄像头
深度摄像头是极少的在捕获图像时能估计物体与摄像头之间距离的设备。深度摄像头(例如微软的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()
实现,算法的实现步骤:
- 预定以一个含有(一个或多个)物体的矩形(必须包含想要分割的物体)
- 矩形外的区域都会被认为是背景部分
- 对于预定义的矩形部分,会通过背景部分区分其中的背景和前景区域。
- 用高斯混合模型(Gaussians Mixture Model, GMM)来对背景和前景建模,未定义部分标记为可能的前景或背景(可能是前景,可能是背景)
- 图像中的每一个像素点都被看作通过一条虚拟边与周围像素点相连接,每条边都有一个属于前景或背景的概率,这基于它与周围像素颜色的相似性(一般同一物体的像素点具有连续性和相似性)
- 每一个像素点会与前景或背景节点连接
- 在节点连接完成后会存在,若节点之间的边属于不同终端则将他们分离出来了
背景部分为黑色,前景部分为白色
下面是使用 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()
使用分水岭算法进行图像分割
算法叫做分水岭是因为有水的概念。把图像中低密度的区域(变化很少)想象成山谷,图像中高密度的区域(变化很多)想象成山峰。开始向山谷中注入水知道不同的山谷中的水开始汇聚。为了阻止不同山谷的水汇聚,可以设置一些栅栏,最后得到的栅栏就是图像分割。
算法过程:
- 使用大津算法将图像二值化
- 形态学开变换
morphologyEX
去除图像中的小白点 - 膨胀获取绝大部分为背景的图像
- 使用
distanceTransform
获取前景部分 - 重合部分利用背景和前景相减获取(一般为边界处)
- 使用
connectedComponents
获得标记(将背景标记为0,其他对象用从1开始的整数标记) - 将标记加一(防止背景部分被当作unknown),将unknown 部分置0
- 最后打开门,让水漫起来并把栅栏设置为红色
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 获取确定的前景部分,在使用阈值处理哪些为前景部分(估计)
ret,markers=cv2.connectedComponents(sure_fg)
这个函数主要将前景部分确定的为前景的节点连接起来,将背景标记为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
和分水岭算法进行图像分割