前言
本节主要介绍图像分割和修复的方法和OpenCV中提供的算法,并提供代码例程。
一、
1.什么是图像分割?
将前景物体从背景中分离出来
2.图像分割方法
- 传统的图像分割方法
(1)分水岭法
(2)GrabCut法
(3)MeanShift
(4)背景扣除 - 深度学习的图像分割
二、传统图像分割方法
1.分水岭法
原理
分水岭分割方法,是一种基于拓扑理论的数学形态学的分割方法.分水岭的概念和形成可以通过模拟浸入过程来说明。在每一个局部极小值表面,刺穿一个小孔,然后把整个模型慢慢浸入水中,随着浸入的加深,每一个局部极小值的影响域慢慢向外扩展,在两个集水盆汇合处构筑大坝,即形成分水岭。
分水岭的计算过程是一个迭代标注过程。比较经典的计算方法是L. Vincent提出的。在该算法中,分水岭计算分两个步骤,一个是排序过程,一个是淹没过程。首先对每个像素的灰度级进行从低到高排序,然后在从低到高实现淹没过程中,对每一个局部极小值在h阶高度的影响域采用先进先出(FIFO)结构进行判断及标注。
分水岭算法对微弱边缘具有良好的响应,图像中的噪声、物体表面细微的灰度变化,都会产生过度分割的现象。但同时应当看出,分水岭算法对微弱边缘具有良好的响应,是得到封闭连续边缘的保证的。另外,分水岭算法所得到的封闭的集水盆,为分析图像的区域特征提供了可能。
当图像存在过多的极小区域而产生的许多小的集水盆(红色区域),但实际我们可能只需要那些大的集水盆(绿色区域)。因此分水岭算法对微弱边缘具有良好的响应,图像中的噪声、物体表面细微的灰度变化,都会产生过度分割的现象。但同时应当看出,分水岭算法对微弱边缘具有良好的响应,是得到封闭连续边缘的保证的。另外,分水岭算法所得到的封闭的集水盆,为分析图像的区域特征提供了可能。
步骤
- 标记背景
- 标记前景
- 标记未知区域
前景、背景、未知区域形成掩码后,当做参数传递给OpenCV的分水岭算法 - 进行分割
watershed() 分水岭算法API
声明:void watershed( InputArray image, InputOutputArray markers );
参数:
image:需要分割的图像
markers:前景、背景、未知区域设置不同的值用以区分它们
distanceTransform() 距离变换API,计算非零像素到零的距离
声明:void distanceTransform( InputArray src, OutputArray dst, OutputArray labels, int distanceType, int maskSize, int labelType = DIST_LABEL_CCOMP );
参数:
distanceType:计算距离的函数
DIST_L1:计算长宽绝对值的和
DIST_L2:勾股定理计算距离
maskSize:扫描时kernel的大小
L1用3、L2用5
connectedComponents() 求连通域API
声明:int connectedComponents(InputArray image, OutputArray labels, int connectivity, int ltype, int ccltype);
参数:
connectivity:计算连通域方法:4,8(默认)周围4或8个像素点
返回值:
返回一个int整型 nccomps,函数返回值为连通区域的总数N,范围为[0,N-1],其中0代表背景。
代码示例:
import cv2
import numpy as np
from matplotlib import pyplot as plt
# 获取背景
# 1.通过二值法得到黑白图片
# 2.通过形态学获取背景
img = cv2.imread('./picture/water_coins.jpeg')
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.int8)
open1 = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations = 2)
# 膨胀
bg = cv2.dilate(open1, kernel, iterations = 1)
# 获取前景物体
dist = cv2.distanceTransform(open1, cv2.DIST_L2, 5)
ret, fg = cv2.threshold(dist, 0.7*dist.max(), 255, cv2.THRESH_BINARY)
# plt.imshow(dist, cmap = 'gray')
# plt.show()
# exit()
# 获取未知区域
fg = np.uint8(fg)
unknow = cv2.subtract(bg, fg)
# 创建连通域
ret, marker = cv2.connectedComponents(fg)
marker = marker + 1
marker[unknow == 255] = 0
# 进行图像分割
result = cv2.watershed(img, marker)
img[result == -1] = [0, 0, 255]
cv2.imshow("bin", thresh)
cv2.imshow("bg", bg)
cv2.imshow("fg", fg)
cv2.imshow("unknow", unknow)
cv2.imshow("img", img)
cv2.waitKey(0)
结果展示:
2.GrabCut
通过交互的方式获得前景物体,例如:框选出物体的大致范围,然后进行图像分割,在各种图片处理软件中都有运用。
grabCut()
声明:void grabCut( InputArray img, InputOutputArray mask, Rect rect, InputOutputArray bgdModel, InputOutputArray fgdModel, int iterCount, int mode = GC_EVAL );
参数:
mask:分割后的掩码,可通过其将图像抠出
rect:选中区域
bgdModel和fgdModel:固定值
iterCount:grabCut迭代的次数
mode:模式
mask:
BGD:背景,0
FGD:前景,1
PR_BGD:可能是背景,2
PR_FGD:可能是前景,3
mode:
GC_INIT_WITH_RECT:在指定矩形区域搜索前后景
GC_INIT_WITH_MASK:第一次用rect,以后通过mask提取前后景
代码案例:
import cv2
import numpy as np
class App:
flag_rect = False
startX = 0
startY = 0
rect = (0, 0, 0, 0)
def onmouse(self, event, x, y, flags, param):
if event == cv2.EVENT_LBUTTONDOWN:
self.flag_rect = True
self.startX = x
self.startY = y
print("LBUTTONDOWN")
elif event == cv2.EVENT_LBUTTONUP:
self.flag_rect = False
cv2.rectangle(self.img, (self.startX, self.startY), (x, y), (0, 0, 255), 2)
self.rect = (min(self.startX, x), min(self.startY, y), abs(self.startX-x), abs(self.startY-y))
print("LBUTTONUP")
elif event == cv2.EVENT_MOUSEMOVE:
if self.flag_rect == True:
self.img = self.img2.copy()
cv2.rectangle(self.img, (self.startX, self.startY), (x, y), (255, 0, 0), 2)
print("MOUSEMOVE")
print("onmouse")
def run(self):
print("run")
cv2.namedWindow("input")
cv2.setMouseCallback("input", self.onmouse)
self.img = cv2.imread("./picture/lana.jpeg")
self.img2 = self.img.copy()
self.mask = np.zeros(self.img.shape[:2], dtype=np.uint8)
self.output = np.zeros(self.img.shape, np.uint8)
while(1):
cv2.imshow("input", self.img)
cv2.imshow("output", self.output)
key = cv2.waitKey(100)
if key == 27:
break
if key == ord('g'):
bgdmodel = np.zeros((1, 65), np.float64)
fgdmodel = np.zeros((1, 65), np.float64)
cv2.grabCut(self.img2, self.mask, self.rect, bgdmodel, fgdmodel, 1, cv2.GC_INIT_WITH_RECT)
mask2 = np.where((self.mask == 1) | (self.mask == 3), 255, 0).astype('uint8')
self.output = cv2.bitwise_and(self.img2, self.img2, mask = mask2)
App().run()
结果展示:
3.MeanShift
严格来说MeanShift并不是用来对图像分割的,而是在色彩层面的平滑滤波,它会中和色彩分布相近的颜色,平滑色彩细节,侵蚀掉面积较小的颜色区域。它以图像上任意一点P为圆心,半径为sp,色彩幅值为sr进行不断的迭代。
pyrMeanShiftFiltering()
声明:void pyrMeanShiftFiltering( InputArray src, OutputArray dst, double sp, double sr, int maxLevel = 1, TermCriteria termcrit=TermCriteria(TermCriteria::MAX_ITER+TermCriteria::EPS,5,1) );
参数:
sp:半径
sr:色彩幅值的变化范围
代码示例:
# 平滑色彩
mean_img = cv2.pyrMeanShiftFiltering(img, 20, 30)
# 提取边缘
canny_img = cv2.Canny(mean_img, 150, 300)
contours, _ = cv2.findContours(canny_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# 绘制边沿
cv2.drawContours(img, contours, -1, (0, 0, 255), 2)
三、视频背景抠除
视频实际上是一组连续的帧,帧与帧之间关系密切(GOD),在GOD中,背景几乎是不变的。
1.MOG去背景
混合高斯模型为基础的前景/背景分割算法
createBackgroundSubtractorMOG2()
声明:createBackgroundSubtractorMOG2(int history=500, double varThreshold=16, bool detectShadows=true);
参数:
history:读取的历史帧,单位毫秒
detectShadows:是否检测阴影
代码示例:
import cv2
import numpy as np
cap = cv2.VideoCapture('./video/vtest.avi')
mog = cv2.createBackgroundSubtractorMOG2()
while True:
ret, frame = cap.read()
fgmask = mog.apply(frame)
cv2.imshow('img', fgmask)
k = cv2.waitKey(30)
if k == 27:
break
cap.release()
cv2.destroyAllWindows()
结果展示:
2.GMG去背景
从上图可以看出MOG2去背景方法会产生大量的噪点,由此提出GMG方法。静态背景图像估计和每个像素的贝叶斯分割抗噪性更强
3.图像修复
inpaint()
声明:void inpaint( InputArray src, InputArray inpaintMask, OutputArray dst, double inpaintRadius, int flags );
参数:
inpaintMask:与原始图像尺寸一样,黑底白色残缺位置的一张图片
inpaintRadius:算法考虑的每个修复点的圆形邻域半径
flags:
INPAINT_NS
INPAINT_TELEA
使用inpaint进行图像修复需要一张黑色背景,白色图案的原图像的缺失部分,且必须是单通道的。可以通过图像分割的方法获得。
代码示例:
import cv2
import numpy as np
img = cv2.imread("./picture/inpaint.png")
mask = cv2.imread("./picture/inpaint_mask.png")
mask = cv2.cvtColor(mask, cv2.COLOR_BGR2GRAY)
dst = cv2.inpaint(img, mask, 5, cv2.INPAINT_TELEA)
cv2.imshow("img", img)
cv2.imshow("dst", dst)
cv2.waitKey()
结果展示: