分水岭算法是在分割的过程中,它会把跟临近像素间的相似性作为重要的参考依据,从而将在空间位置上相近并且灰度值相近(求梯度)的像素点互相连接起来构成一个封闭的轮廓。分水岭算法常用的操作步骤:彩色图像灰度化,然后再求梯度图,最后在梯度图的基础上进行分水岭算法,求得分段图像的边缘线。
下面左边的灰度图,可以描述为右边的地形图,地形的高度是有灰度图的灰度值决定,灰度为0对应地形图的地面,灰度值最大的像素对应地形图的最高点。
对灰度图的地形图的解释,我们考虑三类点:
假设我们在盆地的最小值点,打一个洞,然后往盆地里面注水,并阻止两个盆地的水汇聚,我们会在两个盆地的水汇集的时刻,在交界的边缘上(即分水岭线),建一个大坝,来阻止两个盆地的水汇聚成一片水域。这样图像就被分成2个像素集,一个是注水盆地像素集,一个是分水岭线像素集。
在真实图像中,由于噪声点或者其它干扰因素的存在,使得分水岭算法常常出现过度分割的现象,这主要是因为图像中可能存在很多很小的局部极小点的存在,对这些局部盆地进行分割会导致过分割。为了解决过分割的问题,学者们提出了基于标记(mark)图像的分水岭算法,就是通过先验知识,来指导分水岭算法,以便获得更好的图像分割效果。通常的mark图像,都是在某个区域定义了一些灰度层级,在这个区域的洪水淹没过程中,水平面都是从定义的高度开始的,这样可以避免一些很小的噪声极小值区域的分割。
下面我们来学习一下OpenCV中提供的watershed函数:
cv2.watershed(image, markers) → None
参数: |
|
---|
该函数实现了[Meyer92]中描述的分水岭,基于非参数标记的分割算法的变体之一。
在将图像传递给函数之前,必须markers
使用positive(>0
)索引粗略勾画图像中的所需区域。因此,每个区域被表示为具有像素值1,2,3等的一个或多个连通分量。可以使用findContours()
和从二进制掩码中检索这些标记drawContours()
(参见watershed.cpp
演示)。标记是未来图像区域的“种子”。markers
其中与轮廓区域的关系未知且应由算法定义的所有其他像素应设置为0。在函数输出中,标记中的每个像素设置为“种子”组件的值,或者设置为区域之间的边界处的-1。
可以在OpenCV示例目录中找到该函数的可视演示和用法示例(请参阅watershed.cpp
演示)。
注意
任何两个相邻的连通分量不一定用分水岭边界(-1的像素)分开; 例如,它们可以在传递给函数的初始标记图像中相互接触。
和往常一样为了满足懒癌晚期的读者们,上面给出了opencv官方文档对于watershed函数的解释,不过个人更喜欢下面大神给出的解释,通俗易懂.
参数说明:
接下来执行分水岭会发生什么呢?算法会根据markers传入的轮廓作为种子(我们把注水点由盆地的最小值点转为图像的轮廓,这一点在程序中会有体现),对图像上其他的像素点根据分水岭算法规则进行判断,并对每个像素点的区域归属进行划定,直到处理完图像上所有像素点。而区域与区域之间的分界处的值被置为“-1”,以做区分。
简单概括一下就是说第二个入参markers必须包含了种子点信息。Opencv官方例程中使用鼠标划线标记,其实就是在定义种子,只不过需要手动操作,而使用findContours可以自动标记种子点。而分水岭方法完成之后并不会直接生成分割后的图像,还需要进一步的显示处理,如此看来,只有两个参数的watershed其实并不简单。
本质来讲,在程序中实现前三步已经完成了分水岭算法的使用以及显示,最后一步只是为了更好的观感,所以使用numpy生成随机数字,作为颜色填充,然后与原图像融合,实现彩色效果.
下面给出完整代码,可以直接copy运行.
# -*- coding:utf-8 -*-
import cv2
import numpy as np
'''
created on 11:10:10 2018-11-15
@author:ren_dong
分水岭算法 图像分割
分水岭算法实现图像自动分割的步骤:
1 图像灰度化、Canny边缘检测
2 查找轮廓,并且把轮廓信息按照不同的编号绘制到watershed的第二个参数markers上,相当于标记注水点。
3 watershed分水岭算法
4 绘制分割出来的区域,然后使用随机颜色填充,再跟源图像融合,以得到更好的显示效果。
cv2.watershed(image, markers) → None
'''
# 读入图片
img = cv2.imread('1.jpg')
cv2.imshow('origin',img)
# 转换为灰度图片
gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# canny边缘检测 函数返回一副二值图,其中包含检测出的边缘。
canny = cv2.Canny(gray_img, 80, 150)
cv2.imshow('Canny', canny)
# 寻找图像轮廓 返回修改后的图像 图像的轮廓 以及它们的层次
canny, contours, hierarchy = cv2.findContours(canny, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
# 32位有符号整数类型
marks = np.zeros(img.shape[:2], np.int32)
# findContours检测到的轮廓
imageContours = np.zeros(img.shape[:2], np.uint8)
# 轮廓颜色
compCount = 0
index = 0
# 绘制每一个轮廓
for index in range(len(contours)):
# 对marks进行标记,对不同区域的轮廓使用不同的亮度绘制,相当于设置注水点,有多少个轮廓,就有多少个注水点
# 图像上不同线条的灰度值是不同的,底部略暗,越往上灰度越高
marks = cv2.drawContours(marks, contours, index, (index, index, index), 1, 8, hierarchy)
# 绘制轮廓,亮度一样
# masks底部亮度暗,越往上亮度越高, imageContours 整体亮度一致
# imageContours = cv2.drawContours(imageContours, contours, index, (255, 255, 255), 1, 8, hierarchy)
# 查看 使用线性变换转换输入数组元素成8位无符号整型。
markerShows = cv2.convertScaleAbs(marks)
cv2.imshow('markerShows', markerShows)
# cv2.imshow('imageContours',imageContours)
# 使用分水岭算法
marks = cv2.watershed(img, marks)
afterWatershed = cv2.convertScaleAbs(marks)
cv2.imshow('afterWatershed', afterWatershed)
###分水岭算法之后,让水漫起来,并且把堤坝即分水岭绘制为绿色
img[marks == -1] = [ 0, 255, 0]
cv2.imshow('masks',img)
###到此 分水岭算法已经算是完成,下面是利用numpy生成随机颜色,进行填充空白图像,然后将其和原图像融合
# 生成随机颜色
colorTab = np.zeros((np.max(marks) + 1, 3))
# 生成0~255之间的随机数
for i in range(len(colorTab)):
aa = np.random.uniform(0, 255)
bb = np.random.uniform(0, 255)
cc = np.random.uniform(0, 255)
colorTab[i] = np.array([aa, bb, cc], np.uint8)
bgrImage = np.zeros(img.shape, np.uint8)
# 遍历marks每一个元素值,对每一个区域进行颜色填充
for i in range(marks.shape[0]):
for j in range(marks.shape[1]):
# index值一样的像素表示在一个区域
index = marks[i][j]
# 判断是不是区域与区域之间的分界,如果是边界(-1),则使用白色显示
if index == -1:
bgrImage[i][j] = np.array([255, 255, 255])
else:
bgrImage[i][j] = colorTab[index]
cv2.imshow('After ColorFill', bgrImage)
# 填充后与原始图像融合
result = cv2.addWeighted(img, 0.55, bgrImage, 0.45, 0)
cv2.imshow('addWeighted', result)
cv2.waitKey()
cv2.destroyAllWindows()
这次因为中间过程太多,所以选择进行一步步讲解,便于理解.
我们的原始图像如下,对它进行分水岭算法处理.(动漫党福利哦!)
然后进行代码分析
# canny边缘检测 函数返回一副二值图,其中包含检测出的边缘。
canny = cv2.Canny(gray_img, 80, 150)
cv2.imshow('Canny', canny)
# 寻找图像轮廓 返回修改后的图像 图像的轮廓 以及它们的层次
canny, contours, hierarchy = cv2.findContours(canny, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
# 32位有符号整数类型
marks = np.zeros(img.shape[:2], np.int32)
# findContours检测到的轮廓
imageContours = np.zeros(img.shape[:2], np.uint8)
# 轮廓颜色
compCount = 0
index = 0
# 绘制每一个轮廓
for index in range(len(contours)):
# 对marks进行标记,对不同区域的轮廓使用不同的亮度绘制,相当于设置注水点,有多少个轮廓,就有多少个注水点
# 图像上不同线条的灰度值是不同的,底部略暗,越往上灰度越高
marks = cv2.drawContours(marks, contours, index, (index, index, index), 1, 8, hierarchy)
# 绘制轮廓,亮度一样
# masks底部亮度暗,越往上亮度越高, imageContours 整体亮度一致
# imageContours = cv2.drawContours(imageContours, contours, index, (255, 255, 255), 1, 8, hierarchy)
# 查看 使用线性变换转换输入数组元素成8位无符号整型。
markerShows = cv2.convertScaleAbs(marks)
cv2.imshow('markerShows', markerShows)
# cv2.imshow('imageContours',imageContours)
下面进行解释,左图为canny边缘检测之后的效果,右图按照轮廓设定注水点,所以仔细观看会发现,中间的图亮度是不同的,底部略暗,但是越往上灰度越高,所以显得越亮.
# 使用分水岭算法
marks = cv2.watershed(img, marks)
afterWatershed = cv2.convertScaleAbs(marks)
cv2.imshow('afterWatershed', afterWatershed)
###分水岭算法之后,让水漫起来,并且把堤坝即分水岭绘制为绿色
img[marks == -1] = [ 0, 255, 0]
cv2.imshow('masks',img)
另外为了更好的显示轮廓作为区域边界的效果,我用绿色描绘了一下,最右边的图就是 [手贱]
OK,完成以上步骤,分水岭算法就算是完成了,当然人类总是贪婪的(追求进步,不要理解错了),所以根据大神博客的指点,我又进行了颜色填充,使得结果更加绚丽.
# 生成随机颜色
colorTab = np.zeros((np.max(marks) + 1, 3))
# 生成0~255之间的随机数
for i in range(len(colorTab)):
aa = np.random.uniform(0, 255)
bb = np.random.uniform(0, 255)
cc = np.random.uniform(0, 255)
colorTab[i] = np.array([aa, bb, cc], np.uint8)
bgrImage = np.zeros(img.shape, np.uint8)
# 遍历marks每一个元素值,对每一个区域进行颜色填充
for i in range(marks.shape[0]):
for j in range(marks.shape[1]):
# index值一样的像素表示在一个区域
index = marks[i][j]
# 判断是不是区域与区域之间的分界,如果是边界(-1),则使用白色显示
if index == -1:
bgrImage[i][j] = np.array([255, 255, 255])
else:
bgrImage[i][j] = colorTab[index]
cv2.imshow('After ColorFill', bgrImage)
# 填充后与原始图像融合
result = cv2.addWeighted(img, 0.55, bgrImage, 0.45, 0)
cv2.imshow('addWeighted', result)
cv2.waitKey()
cv2.destroyAllWindows()
下面是结果展示,左图是根据轮廓标记的masks生成随机颜色图,右图是图片融合的结果,融合比例可以自选哦(比如0.7:0.3)
OK, 按照本人今天的plan, 算是完成了今天的任务,晚上还有课......,话说人生真的是自己决定的,当初脑残选了一个格外严厉的导师,每天训斥不断,苦啊,可这又能怪谁呢,只能自己承受了(幸亏我体重大,可以承受的住,哈哈).
吐糟到此完毕,还是要感谢大神,
大神博客:https://www.cnblogs.com/zyly/p/9392881.html
opencv官方文档:https://docs.opencv.org/2.4/modules/imgproc/doc/miscellaneous_transformations.html?highlight=cv2.water#cv2.watershed
本人github地址:https://github.com/RenDong3/OpenCV_Notes