简介 :
在图像处理的过程中, 经常需要从图像中将前景对象作为目标图像提取出来。例如无人驾驶技术, 我们关心的是周围的交通工具, 其他障碍物等, 而对于背景本身并不关注, 故而, 我们需要将这些东西从图片(视频)中提取出来, 而忽略那些只有背景的图像。
该算法将图像形象的比喻为地理学上的地形平面(等高线地形图), 从而实现图像的分割
对于一幅灰度图像, 可以将其看作地理学上的地形表面, 灰度值高的区域可以呗看成山峰, 相对应的, 灰度值低的区域被看作山谷。如下图(左图为原始图像, 右图为对应的地形表面) :
此时, 我们向山谷中注水, 当水位逐渐升高, 若不加阻拦, 不同山谷中的水会汇聚到一起, 为了防止不同闪过中的水交汇, 我们需要在水流可能进行交汇的地方修建堤坝, 这个过程会将图像分割称为两个互不相同的集合 : 集水盆地与分水岭线, 也即对与原始图像的分割, 这就是分水岭算法。如下图 :
但由于噪声等因素的影响, 基础的分水岭算法经常会存在过度分割的情况, 这样的国度分割会将图像分成一个个独立的小块, 让分割失去了意义。如下图(左图为电泳现象的图像, 右图为过度分割的结果的图像) :
这问题真的是…肉眼可见的严重╮(╯-╰)╭
所以, 为了改善图像分割的效果, 提出了基于掩模的改进型分水岭算法。
该算法允许用户将他认为是同一区域的部分标注出来, 这样, 分水岭算法在进行处理时就会将标注部分处理为同一个分割区域, 类似于PowerPoint中的 ‘删除背景功能’ 如下图(红色为标注处) :
使用该算法对电泳图像进行改进有 :
可以看出效果较上次有了明显的改进
在 OpenCV-Python中可以使用函数 cv2.watershed() 实现分水岭算法, 但在具体的实施过程中还需要借助形态学函数. 距离变换函数 cv2.distanceTransform() 、 cv2.connectedComponemts() 来完成图像分割, 下面来对用到的函数进行简单介绍
开运算是线=先腐蚀后膨胀的操作, 开运算能够去除图像中的噪声
在用分水岭算法处理图像之前, 需要先用开运算去除图像内的噪声, 以避免噪声对应图像分割造成的干扰。开运算效果图如下 :
相关函数介绍 : opencv形态学操作函数morphologyEx
通过形态学操作和减法可以实现获取图像的边界, 效果图如下 :
实例(使用形态学操作获取一幅图像的边缘信息) :
程序 :
# 使用形态学操作获取一幅图像的边缘信息
from cv2 import cv2 as cv
import numpy as np
o = cv.imread(r"D:\anaconda\vscode-python\pic\opencv.jpg",-1)
k = np.ones((5,5),np.uint8)
e = cv.erode(o,k)
# erode()函数可以对输入图像用特定结构元素进行腐蚀操作,
# 该结构元素确定腐蚀操作过程中的邻域的形状,
# 各点像素值将被替换为对应邻域上的最小值。
b = cv.subtract(o,e)
# 图像相减操作
cv.imshow("o",o)
cv.imshow("e",e)
cv.imshow("b",b)
cv.waitKey()
cv.destroyAllWindows()
效果 :
上面的OpenCV比较简单, 如果换成较为复杂的图像…
就成了
这样, emmm…有点恐怖…(请自动忽略水印)
通过以上分析可知, 形态学操作与减法运算能过获取图像边界信息, 但是一旦图像较为复杂, 或图像内的前景对象存在连接的情况, 形态学操作就无法准确的获取各个子图的边界了
当图像内各个子图没有连接时, 可以直接使用形态学腐蚀操作确定前景对象, 但如果图像中的子图连接在一起时就很难确定前景图像了, 此时就需要借助距离变换函数 cv2.distanceTransform() 将前景对象提取出来
cv2.distanceTransform() 计算二值图像内任意点到最近的背景点的距离。一般情况下, 该函数计算的是图像内非零值像素点到最近的零值像素点之间的距离, 即计算二值图像中所有像素点距其最近的零值像素点之间的距离(如果本身像素值为零值则距离也为零)
cv2.distanceTransform() 函数的语法格式如下 :
dst = cv2.distanceTransform(src,
distanceType,
maskSize,
dstType)
参数 :
(1). src : 8位单通道二值图像
(2). distanceType : 为距离类型参数, 具体含义见下表
(3). maskSize : 为掩模的尺寸, 其所有可能值如下表。需要注意的是当 distanceType = cv2.DIST_L1 或 cv2.DIST_C 时, maskSize 强制为3(3或5或更大实际没什么区别了)
(4). dstType : 为目标图像的类型, 默认值为CV_32F(32位浮点型)
返回值 :
(1). dst : 计算得到的目标图像, 可能是8位或32位浮点数, 尺寸与src相同
实例(使用该函数起算一幅图像的确定前景, 并观察效果) :
程序 :
from cv2 import cv2 as cv
import numpy as np
img = cv.imread(r"D:\anaconda\vscode-python\pic\timgg.jpg")
gray = cv.cvtColor(img,cv.COLOR_BGR2GRAY)
img = cv.cvtColor(img,cv.COLOR_BGR2RGB)
ishow = img.copy()
# 阈值处理
ret,thresh = cv.threshold(gray,0,255,cv.THRESH_BINARY_INV+cv.THRESH_OTSU)
kernel = np.ones((3,3),np.uint8)
# 形态学开运算
opening = cv.morphologyEx(thresh,cv.MORPH_OPEN,kernel,iterations=2)
# 计算并提取前景图像
dist_transform = cv.distanceTransform(opening,cv.DIST_L2,5)
# 对距离图像进行阈值化处理
ret,fore = cv.threshold(dist_transform,0.7*dist_transform.max(),255,0)
cv.imshow("ishow",ishow)
cv.imshow("dis_transform",dist_transform)
cv.imshow("fore",fore)
cv.waitKey()
cv.destroyAllWindows()
效果(素材来源于网络) :
其中, 左图为原始图像, 中间的图为距离变换函数 cv2.distanceTransform() 计算得到的距离图像, 右图为对距离图像进行阈值化处理后的结果。
由图可见, 右图可以比较准确的显示出左图内的 ‘确定前景’ (这里的确定前景通常是指前景对象的中心), 之所以认为这些点为确定前景, 是因为它们距离背景点的距离足够远, 都是距离大于足够大的固定阈值(0.7*dist_transform.max()) 的点。
使用形态学的膨胀操作能够将图像内的前景膨胀放大, 当图形内的前景被放大后, 其背景就会被相应的压缩, 故而此时得到的背景信息一定是小于实际背景的, 不包含前景的确定背景
距离变换函数 cv2.distanceTransform() 能够获取图像的中心, 得到确定前景
图像中除却确定前景确定背景外, 剩下的区域就是位置区域。这部分区域正是分水岭算法需要进一步明确的区域, 针对一幅图像, 有 :
未知区域 = 原始图像 - 确定前景 - 确定背景
= (原始图像 - 确定背景) - 确定前景
上式中的 (原始图像 - 确定背景) 可由对图像进行形态学膨胀得到
实例(标注一幅图像的确定前景, 确定背景及未知区域) :
程序 :
from cv2 import cv2 as cv
import numpy as np
img = cv.imread(r"D:\anaconda\vscode-python\pic\timgg.jpg")
gray = cv.cvtColor(img,cv.COLOR_BGR2GRAY)
img = cv.cvtColor(img,cv.COLOR_BGR2RGB)
ishow = img.copy()
# 阈值处理
ret,thresh = cv.threshold(gray,0,255,cv.THRESH_BINARY_INV+cv.THRESH_OTSU)
kernel = np.ones((3,3),np.uint8)
# 形态学开运算
opening = cv.morphologyEx(thresh,cv.MORPH_OPEN,kernel,iterations=2)
# 膨胀得到bg,其背景图像为确定背景,前景图像为原始图像-确定背景
bg = cv.dilate(opening,kernel,iterations=3)
# 计算并提取前景图像
dist_transform = cv.distanceTransform(opening,cv.DIST_L2,5)
# 对距离图像进行阈值化处理
ret,fore = cv.threshold(dist_transform,0.7*dist_transform.max(),255,0)
# 两图像做差以得到未知区域
fore = np.uint8(fore)
un = cv.subtract(bg,fore)
cv.imshow("ishow",ishow)
cv.imshow("bg",bg)
cv.imshow("fore",fore)
cv.imshow("un",un)
cv.waitKey()
cv.destroyAllWindows()
效果 :
bg 为对原始图像ishow进行膨胀后得到的,其背景图像为确定背景,前景图像为原始图像-确定背景
fore 为确定的前景图像
un 为未知区域图像, 是由bg - force 得到的
在明确了前景后可以对确定的前景图像进行标注了, 在OpenCV中可以使用函数 cv2.connectedComponents() 进行标注, 该函数会将背景标注为0, 将其他的对象使用从1开始的正整数进行标注, 其语法格式如下 :
retval,labels = cv2.connectedComponents(img)
参数 :
(1). img 为8位单通道的待标注图像
返回值 :
(1). retval 为返回的标注数量
(2). labels 为标注的结果图像
实例(使用函数cv2.connectedComponents()标注一幅图像, 并观察标注效果)
from cv2 import cv2 as cv
import numpy as np
img = cv.imread(r"D:\anaconda\vscode-python\pic\timgg.jpg")
gray = cv.cvtColor(img,cv.COLOR_BGR2GRAY)
img = cv.cvtColor(img,cv.COLOR_BGR2RGB)
ishow = img.copy()
# 阈值处理
ret,thresh = cv.threshold(gray,0,255,cv.THRESH_BINARY_INV+cv.THRESH_OTSU)
kernel = np.ones((3,3),np.uint8)
# 形态学开运算
opening = cv.morphologyEx(thresh,cv.MORPH_OPEN,kernel,iterations=2)
# 膨胀得到bg,其背景图像为确定背景,前景图像为原始图像-确定背景
bg = cv.dilate(opening,kernel,iterations=3)
# 计算并提取前景图像
dist_transform = cv.distanceTransform(opening,cv.DIST_L2,5)
# 对距离图像进行阈值化处理
ret,fore = cv.threshold(dist_transform,0.7*dist_transform.max(),255,0)
# 两图像做差以得到未知区域
fore = np.uint8(fore)
rect,markers = cv.connectedComponents(fore)
markers = np.array(markers,dtype=np.uint8)
cv.imshow("ishow",ishow)
cv.imshow("fore",fore)
cv.imshow("markers",markers)
cv.waitKey()
cv.destroyAllWindows()
效果 :
最后一幅图有点浅…
左图为原始图像,
中间的为经过距离变换得到的前景图像的中心点图像fore,
右图为对fore进行标注后的结果图像
这样处理后的结果 :
数值0代表背景区域
从数值1开始的值代表不同的前景区域
但在分水岭算法中, 标注值为0的代表的为未知区域, 所以,我们需要对函数cv2.connectedComponents() 标注结果进行调整 : 将标注结果都加上1, 再将未知区域置为0
关键代码为 :
ret,markers = cv2.connectedComponents(fore)
markers = markers + 1
markers[未知区域] = 0
完整代码 :
from cv2 import cv2 as cv
import numpy as np
img = cv.imread(r"D:\anaconda\vscode-python\pic\timgg.jpg")
gray = cv.cvtColor(img,cv.COLOR_BGR2GRAY)
img = cv.cvtColor(img,cv.COLOR_BGR2RGB)
ishow = img.copy()
# 阈值处理
ret,thresh = cv.threshold(gray,0,255,cv.THRESH_BINARY_INV+cv.THRESH_OTSU)
kernel = np.ones((3,3),np.uint8)
# 形态学开运算
opening = cv.morphologyEx(thresh,cv.MORPH_OPEN,kernel,iterations=2)
# 膨胀得到bg,其背景图像为确定背景,前景图像为原始图像-确定背景
bg = cv.dilate(opening,kernel,iterations=3)
# 计算并提取前景图像
dist_transform = cv.distanceTransform(opening,cv.DIST_L2,5)
# 对距离图像进行阈值化处理
ret,fore = cv.threshold(dist_transform,0.7*dist_transform.max(),255,0)
# 两图像做差以得到未知区域
fore = np.uint8(fore)
rect,markers = cv.connectedComponents(fore)
markers = np.array(markers,dtype=np.uint8)
fore1 = fore.copy()
unknow = cv.subtract(bg,fore1)
ret1,markers2 = cv.connectedComponents(fore1)
markers2 = markers2 + 1
markers2[unknow == 255] = 0
markers2 = np.array(markers2,dtype=np.uint8)
cv.imshow("markers",markers)
cv.imshow("markers2",markers2)
cv.waitKey()
cv.destroyAllWindows()
完成前面的处理后就可以使用分水岭算法对预处理结果进行分割了, 在OpenCV中采用 cv2.watershed() 函数实现分水岭算法, 其语法格式如下 :
markers = cv2.watershed(image,markers)
参数 :
(1). image : 输入图像, 其格式为8位3通道图像, 在对函数使用cv2.watershed()之前, 必须先用正数大致勾画出图像中的期望分割区域。每一个分割区域会被标注为1, 2, 3 等。对于尚未确定的区域, 需要将其标注为0, 我们可以将标注区域理解为进行分水岭算法分割的种子区域。
(2). maekers : 为32位单通道的标注结果, 它应该与image具有相同大小, 。在markers中, 每个像素要么被设置为初期的 ‘种子值’ 要么被设置为 ‘-1’ 表示边界。 markers可以省略
返回值 :
(1). markers : emmmm 这个和前面的呢个参数一样, 或者说可以看作是在原图像上进行操作
(1). 通过形态学运算对原始图像o进行去噪
(2). 通过腐蚀操作获取确定背景b
(3). 利用距离变换函数cv2.distanceTransform() 对原始图像进行运算, 并对其进行阈值处理, 得到确定前景f
(4). 计算未知区域un = o - b 0 f
(5). 利用函数 cv2.connectedComponents() 的标注结果对o进行标注
(6). 对函数 cv2.connectedComponents() 的标注结果进行i修正
(7). 使用分水岭函数完成对图像的分割
from cv2 import cv2 as cv
import numpy as np
img = cv.imread(r"D:\anaconda\vscode-python\pic\timgg.jpg")
gray = cv.cvtColor(img,cv.COLOR_BGR2GRAY)
img = cv.cvtColor(img,cv.COLOR_BGR2RGB)
ishow = img.copy()
# 阈值处理
ret,thresh = cv.threshold(gray,0,255,cv.THRESH_BINARY_INV+cv.THRESH_OTSU)
kernel = np.ones((3,3),np.uint8)
# 形态学开运算
opening = cv.morphologyEx(thresh,cv.MORPH_OPEN,kernel,iterations=2)
# 膨胀得到bg,其背景图像为确定背景,前景图像为原始图像-确定背景
bg = cv.dilate(opening,kernel,iterations=3)
# 计算并提取前景图像
dist_transform = cv.distanceTransform(opening,cv.DIST_L2,5)
# 对距离图像进行阈值化处理
ret,fg = cv.threshold(dist_transform,0.7*dist_transform.max(),255,0)
# 两图像做差以得到未知区域
fg = np.uint8(fg)
unknow = cv.subtract(bg,fg)
ret1,markers = cv.connectedComponents(fg)
markers = markers + 1
markers[unknow == 255] = 0
markers = cv.watershed(img,markers)
markers = np.array(markers,dtype=np.uint8)
img[markers == -1] = [0,255,0]
cv.imshow("ishow",ishow)
cv.imshow("markers",markers)
cv.waitKey()
cv.destroyAllWindows()
本文参考自 : 李立宗 《OpenCV 轻松入门 : 面向 Python》