图像分割与提取
图像分割与提取的概念
在图像处理的过程中, 经常需要从图像中将前景对象作为目标图像提取出来。例如无人驾驶技术, 我们关心的是周围的交通工具, 其他障碍物等, 而对于背景本身并不关注, 故而, 我们需要将这些东西从图片(视频)中提取出来, 而忽略那些只有背景的图像。
常见的图像分割方法
- 基于阈值的分割方法:全局阈值法、局部阈值法等
- 基于区域的分割方法:分水岭方法、区域生长法等
- 基于边缘的分割方法:Canny边缘检测、轮廓检测等
-
基于特定理论的分割方法:聚类、模糊集等(机器学习)
分水岭算法的概念
图像的灰度空间很像地球表面的整个地理结构,每个像素的灰度值代表高度。其中的灰度值较大的像素连成的线可以看做山脊,也就是分水岭。
当水平面上升到一定高度时,水就会溢出当前山谷,可以通过在分水岭上修大坝,从而避免两个山谷的水汇集,这样图像就被分成2个像素集,一个是被水淹没的山谷像素集,一个是分水岭线像素集。最终这些大坝形成的线就对整个图像进行了分区,实现对图像的分割。
分水岭算法的步骤
原图
二值化
import cv2
import numpy as np
import matplotlib.pyplot as plt
img = cv2.imread("coins.jpg")
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
ret,thresh = cv2.threshold(gray,0,255,cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU)
plt.imshow(thresh,cmap='gray'
plt.show()
形态学操作(移除噪声)
先使用开运算去除图像中的细小白色噪点,然后通过腐蚀运算移除边界像素,得到的图像中的白色区域肯定是真实前景,即靠近硬币中心的区域;膨胀运算使得一部分背景成为了物体的边界,得到的图像中的黑色区域肯定是真实背景,即远离硬币傅区域。剩下的区域(硬币的边界附近)还不能确定是前景函数背景,可通过膨胀图减去腐蚀图得到。
开运算
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3,3))
opening = cv2.morphologyEx(thresh, cv2.MORPH_OPEN,kernel,iterations=2)
plt.imshow(opening,cmap='gray')
plt.show()
膨胀-确认背景区域
sure_bg = cv2.dilate(opening,kernel,iterations=2)
plt.imshow(sure_bg,cmap='gray')
plt.show()
腐蚀-确定前景区域
sure_fg = cv2.erode(opening,kernel,iterations=2)
plt.imshow(sure_fg,cmap='gray')
plt.show()
减法-不确定区域
unknown = cv2.subtract(sure_bg,sure_fg)
plt.imshow(unknown,cmap='gray')
plt.show()
距离变换
剩下的区域不确定是硬币还是背景,这些区域通常在前景和背景接触的区域(或者两个不同硬币接触的区域),我们称之为边界。通过分水岭算法应该能找到确定的边界。
由于硬币之间彼此接触,我们使用另一个确定前景的方法,就是带阈值的距离变换。
下面左边的图为得到的距离转换图像,其实每个像素的值为其到最近的背景像素(灰度值为0)的距离,可以看到硬币的中心像素值最大(中心离背景像素最远)。对其进行二值处理就得到了分离的前景图(下面中间的图),白色区域肯定是硬币区域,而且还相互分离,下面右边的图为之前的膨胀图减去中间这个表示前景的图。
dist_transform=cv2.distanceTransform(opening,cv2.DIST_L2,5)
cv2.normalize(dist_transform,dist_transform,0,1.0,cv2.NORM_MINMAX)
plt.imshow(dist_transform,cmap='gray')
plt.show()
阈值变换-寻找前景区域
ret,sure_fg = cv2.threshold(dist_transform,0.5*dist_transform.max(),255,0)
plt.imshow(sure_fg,cmap='gray')
plt.show()
减法-寻找不确定区域
sure_fg = np.uint8(sure_fg)
unknow = cv2.subtract(sure_bg,sure_fg)
plt.imshow(unknow,camp='gray')
plt,show()
标记图像
现在我们可以确定哪些是硬币区域,哪些是背景区域。然后创建标记(marker,它是一个与原始图像大小相同的矩阵,int32数据类型),表示其中的每个区域,分水岭算法将标记为0的区域视为不确定区域,将标记为1的区域视为背景区域,将标记大于1的正整数表示我们想得到的前景。
我们可以使用cv2.connectedComponent()来实现这个功能,它是用0标记图像的背景,用大于0的整数标记其他对象,所以我们需要对其进行加一,用1来标记图像的背景。
ret,markers = cv2.connectedComponnents(sure_fg)
markers = markers+1
markers[unknown==255] = 0
显示makers
markers_copy = markers_copy()
markers_copy[markers==0] = 150
markers_copy[markers==1] = 0
markers_copy[markers>1] = 255
markers_copy = np.uint8(markers_copy)
plt.imshow(markers_copy,cmap='gray')
plt.show()
应用分水岭算法
markers = cv2.watershed(img,markers)
img[markers==-1] = [0,-,255]
img = cv2.cvtColor(img,cv2.COLOR_RGB2BGR)
plt.figure(1)
plt.imshow(markers,cmap='gray')
plt.figure(2)
plt.imshow(img,cmap='gray')
plt.show()
鼠标交互
opencv的鼠标交互操作主要通过两个函数实现:
第一个是
cv2.setMouseCallback(windowName, onMouse [, param])
第二个是setMouseCallback()的第二个参数,称为鼠标回调函数
onMouse(event, x, y, flags, param)
setMouseCallback()
cv2.setMouseCallback(windowName, onMouse [, param])
windowName:必需。类似于cv.imshow()函数,opencv具体操作哪个窗口以窗口名作为识别标识,这有点类似窗口句柄的概念。
onMouse:必需。鼠标回调函数。鼠标回调函数的定义是onMouse(event, x, y, flags, param),我们想要做什么鼠标操作,都是在这个函数内实现。
onMouse()
onMouse(event, x, y, flags,param)
event:由回调函数根据鼠标对图像的操作自动获得,内容包含左键点击,左键弹起,右键点击…等等等非常多的操作。
x,y:由回调函数自动获得,记录了鼠标当前位置的坐标,坐标mothod以图像左上角为原点(0, 0),x方向向右为正,y方向向下为正。
flags:记录了一些专门的操作,下面有说明。
param:从setMouseCallback()里传递过来的参数。该参数在setMouseCallback()处是可选参数,所以可以不设置。
event类型
cv2.EVENT_MOUSEMOVE
鼠标滑动
cv2.EVENT_LBUTTONDOWN
左键点击
cv2.EVENT_RBUTTONDOWN
右键点击
cv2.EVENT_MBUTTONDOWN
中键点击
cv2.EVENT_LBUTTONUP
左键放开
cv2.EVENT_RBUTTONUP
右键放开
cv2.EVENT_MBUTTONUP
中键放开
cv2.EVENT_LBUTTONDBLCLK
左键双击
cv2.EVENT_RBUTTONDBLCLK
右键双击
cv2.EVENT_MBUTTONDBLCLK
中键双击
cv2.EVENT_FLAG_LBUTTON
左键拖曳
cv2.EVENT_FLAG_RBUTTON
右键拖曳
cv2.EVENT_FLAG_MBUTTON
中键拖曳
cv2.EVENT_FLAG_CTRLKEY
按Ctrl不放事件
cv2.EVENT_FLAG_SHIFTKEY
按Shift不放事件
cv2.EVENT_FLAG_ALTKEY
按Alt不放事件
鼠标画圆
import cv2
def draw_circle(event,x,y,flags,param):
if event == cv2.EVENT_LBUTTONDOWN:
cv2.circle(img,center=(x,y),redius=5,
color=(255,0,0),thickness=-1)
elif event == cv2.EVENT_RBUTTONDOWN:
cv2.circle(img,center=(x,y),radius=5,
color=(0,255,0),thickness=1)
img = cv2.cv2.imread("peppa.jpg")
cv2.namedWindow(winname='drawing')
cv2.setMouseCallback('drawing',draw_circle)
while True:
cv2.imshow('drawing',img)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cv2.destroyAllWindows()
使用鼠标交互函数,实现图像数据标注软件demo
import cv2
drawing = False
start = (-1,-1)
def mouse_event(event,x,y,flags,param):
global start,drawing,made
if event == cv2.EVENT_LBUTTONDOWN:
drawing = True
start = (x,y)
elif event == cv2.EVENT_MOUSEMOVE:
if drawing:
cv2.circle(img,(x,y),5,(0,0,255),-1)
elif event == cv2.EVENT_LBUTTONUP:
drawing = False
cv2.circle(img,(x,y),5(0,0,255),-1)
cv2.imwrite("draw.png",img)
img = cv2.imread('peppa.jpg')
cv2.namedWindow(winname='drawing')
cv2.setMouseCallback('drawing',mouse_event)
while True:
cv2.imshow('drawing',img)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cv2.destroyAllWindows()
区域生长算法
选取某个种子点(一般实际交互时就是鼠标点击的位置),从图像种子点位置开始,将种子点相邻的符合某个阈值范围内的像素添加到生长区域中,接着判断下一个像素点,直到没有可以符合条件的像素为止,此时分割完毕。
区域生长算法的步骤
[
定义point类
import cv2
import numpy as np
class Point(object):
def __init__(self,x,y):
self.x = x
self.y = y
def getX(self):
return self.x
def getY(self):
return self.y
计算像素之间的偏差
def getGrayDiff(img,currentPoint,tmpPoint):
return abs(int(img[currentPoint.x,currentPoint.y]) - int(img[tmpPoint.y]))
设定四领域或八领域
def selectConnects(p):
if p == 8:
connects = [point(-1,-1),point(0,-1),point(1,-1),point(1,0),point(1,1),\
point(0,1),point(-1,1),point(-1,0)]
else:
connects = [point(0,-1),point(1,0),point(0,1),point(-1,0]
return connects
定义生长函数
def getGrayDiff(img,currentPoint,tmpPoint):
return abs(int(img[currentPoint.x,currentPoint.y]) - int(img[tmpPoint.x,tmpPoint]))
def selectConnects(p):
if p == 8:
connects = [Print(-1,-1),point(0,-1),point(1,-1),point(1,0),point(1,1),point(0,1),point(-1,1),point(-1,0)]
else:
connects = [Point(0,-1),point(1,0),Point(0,1),Point(-1,0)]
return connects
def regionGrow(img,seeds,thresh):
height,width = img.shape
seedMark = np.zeros(img.shape)
seedList = []
for seed in seeds:
seedList.append(seed)
label = 1
p=4
connects = selectConnects(p)
while(len(seedList)>0):
currentPoint = seedList.pop(0)
seedMark[currentPoint.x,currentPoint.y] = label
for i in range(p):
tmpX = currentPoint.x + connects[i].x
tmpY = currentPoint.y + connects[i].y
if tmpX < 0 or tmpY < 0 or tmpX >= height or tmpY >= width:
continue
grayDiff = getGrayDiff(img,currentPoint,Point(tmpX,tmpY))
if grayDiff < thresh and seedMark[tmpX,tmpY] == 0:
seedMark[tmpX,tmpY] = label
seedList.append(Point(tmpX,tmpY))
return seedMark
使用鼠标交互实现区域生长算法
import cv2
import numpy as np
class Point(object):
def __init__(self,x,y):
self.x=x
self.y=y
def getX(self):
return self.x
def getY(self):
return self.y
def getGrayDiff(img,currentPoint,tmpPoint):
return abs(int(img[currentPoint.x,currentPoint.y]) -int(img[tmpPoint.x,tmpPoint.y]))
def selectConnects():
connects = [Point(-1, -1), Point(0, -1), Point(1, -1), Point(1, 0), Point(1, 1),
Point(0, 1), Point(-1, 1), Point(-1, 0)]
return connects
def regionGrow(img,seeds,thresh,p = 1):
height, weight = img.shape
seedMark = np.zeros(img.shape)
seedList = []
for seed in seeds:
seedList.append(seed)
label = 1
connects = selectConnects()
while(len(seedList)>0):
currentPoint = seedList.pop(0)
seedMark[currentPoint.x,currentPoint.y] = label
for i in range(8):
tmpX = currentPoint.x + connects[i].x
tmpY = currentPoint.y + connects[i].y
if tmpX < 0 or tmpY < 0 or tmpX >= height or tmpY >= weight:
continue
grayDiff = getGrayDiff(img,currentPoint,Point(tmpX,tmpY))
if grayDiff < thresh and seedMark[tmpX,tmpY] == 0:
seedMark[tmpX,tmpY] = label
seedList.append(Point(tmpX,tmpY))
return seedMark
def mouse(event,x,y,flages,param):
if event==cv2.EVENT_LBUTTONDOWN:
seeds=[Point(x,y)]
binaryImg=regionGrow(img,seeds,3)
cv2.imshow("xxw1",binaryImg)
img=cv2.imread("peppa.jpg",0)
cv2.namedWindow('gray')
cv2.setMouseCallback("gray",mouse)
while True:
cv2.imshow("xxw",img)
if cv2.waitKey(1)==27:
break
cv2.destroyAllWindows()