OpenCV——分水岭算法

前言

图像分割是按照一定的原则,将一幅图像分为若干个互不相交的小局域的过程,它是图像处理中最为基础的研究领域之一。目前有很多图像分割方法,其中分水岭算法是一种基于区域的图像分割算法,分水岭算法因实现方便,已经在医疗图像,模式识别等领域得到了广泛的应用。

正文

原理

分水岭算法的原理网上讲的也都差不多。基本就是,你把图片想象成一大片山脉,像素的大小就是山的高度,那么就肯定有高有低了。类似于下图:
在这里插入图片描述
然后,我们开始朝里面灌水,注意,灌得是不同颜色的水,当两者凹地的水满起来并相互接触时,便形成一道巨大的屏障,这样,就可以把我们的图像一个一个的分割出来了,我的大体理解是这样的。
在这里插入图片描述

流程

在这里插入图片描述

代码

效果图
在这里插入图片描述

有些人比较急躁,那我这里先给出代码,修改图片位置即可用:

import cv2 as cv
import numpy as np

def waterDemo(image):
    print(image.shape)
    # blurred = cv.bilateralFilter(image, 0, 100, 15)
    # blurred = cv.GaussianBlur(src, (15, 15), 0)  # 高斯模糊
    blurred = cv.pyrMeanShiftFiltering(image,10,100)
    gray = cv.cvtColor(blurred,cv.COLOR_BGR2GRAY)
    ret,binary = cv.threshold(gray,0,255,cv.THRESH_BINARY|cv.THRESH_OTSU)
    cv.imshow("binary image",binary)

    # morphologyEx Operation
    kernel = cv.getStructuringElement(cv.MORPH_RECT,(3,3))
    mb = cv.morphologyEx(binary,cv.MORPH_OPEN,kernel,iterations=2)
    sure_bg = cv.dilate(mb,kernel,iterations=3)
    cv.imshow("dilater image",sure_bg)

    # distance transform
    dist = cv.distanceTransform(mb,1,5)
    # dist_output = cv.normalize(dist,0,1.0,cv.NORM_MINMAX)# 相当于做一下归一化的一个操作
    # cv.imshow("distance-t",dist_output*50)

    ret,surface = cv.threshold(dist,dist.max()*0.6,255,cv.THRESH_BINARY)
    # cv.imshow("surface-bin",surface)

    surface_fg = np.uint8(surface)
    unknown = cv.subtract(sure_bg,surface_fg)
    ret,markers = cv.connectedComponents(surface_fg)
    print(ret)

    #watershed transform
    markers = markers+1
    markers[unknown==255] = 0
    markers = cv.watershed(image,markers=markers)
    src[markers==-1] = [0,0,255]
    cv.imshow("result",src)


src = cv.imread("../images/circle.png")
cv.namedWindow("input image",cv.WINDOW_AUTOSIZE)
cv.imshow('input image', src)
waterDemo(src)
cv.waitKey(0)  # 等有键输入或者1000ms后自动将窗口消除,0表示只用键输入结束窗口

cv.destroyAllWindows()

详讲

  1. 首先,因为图片肯定有一些噪声,我们先将图片做一下均值迁移滤波。这个函数严格来说并不是图像的分割,而是图像在色彩层面的平滑滤波,它可以中和色彩分布相近的颜色,平滑色彩细节,侵蚀掉面积较小的颜色区域。至于为啥选这个,我也不懂啊!反正尝试了一下,就是这个滤波的效果会比较好。这里有张图可以参考一下:
    在这里插入图片描述
  2. 然后,就是灰度化,至于为何要灰度化,下面的这个答案写的很好:

噪声信号。对于图像处理的许多应用,颜色信息无助于我们识别重要的边缘或其他特征。有例外。如果在灰度图像中难以检测到色相的边缘(像素值的阶跃变化),或者如果我们需要识别已知色调的物体(绿色叶子前面的橙色水果),则颜色信息可能是有用。如果我们不需要颜色,那么我们可以认为它是噪音。起初,用灰度“思考”有点违反直觉,但你已经习惯了。
代码的复杂性。如果你想根据亮度和色度找到边缘,你就有更多的工作要做。如果额外的颜色信息对感兴趣的应用没有帮助,那么额外的工作(以及额外的调试,支持软件的额外痛苦等)很难证明是合理的。
对于学习图像处理,最好首先了解灰度处理,并理解它如何应用于多通道处理,而不是从全彩成像开始,并且缺少可以(并应该)从单通道处理中学习的所有重要见解。
难以可视化。在灰度图像中,分水岭算法相当容易概念化,因为我们可以将两个空间维度和一个亮度维度想象为具有丘陵,山谷,集水盆地,山脊等的3D图像。“峰值亮度”仅仅是山峰在我们的灰度图像的三维可视化。有许多算法可以用直观的“物理”解释来帮助我们思考问题。在RGB,HSI,Lab和其他色彩空间中,这种可视化更加困难,因为标准人类大脑无法容易地将其视觉化。当然,我们可以想到“峰值发红”,但是这个山峰在(x,y,h,s,i)空间中看起来像什么呢?哎哟。一种解决方法是将每个颜色变量视为强度图像,但这导致我们回到灰度图像处理。
颜色很复杂。人类以易于欺骗的方式感知颜色并识别颜色。如果您尝试区分彼此的颜色,那么您需要(a)遵循传统并控制照明,摄像机颜色校准和其他因素以确保最佳效果,或(b)解决或者(c)希望您可以重新开始灰阶工作,因为至少可以解决问题。
速度。使用现代计算机和并行编程,可以毫秒级地执行简单的逐像素处理百万像素图像。面部识别,OCR,内容感知调整大小,均值移动分割和其他任务可能需要比这更长的时间。无论处理图像需要多少处理时间,还是从中挤出一些有用的数据,大多数客户/用户都希望它更快。如果我们假设处理三通道彩色图像需要处理灰度图像的三倍 - 或者四倍长,因为我们可能会创建一个单独的亮度通道 - 那么这不是一个大的假设如果我们正在处理视频图像,每帧可以在不到1/30或1/25秒的时间内处理。但是,如果我们要分析数千个来自数据库的图像,那么通过调整图像大小,分析图像的一部分和/或消除我们不需要的颜色通道可以节省处理时间是非常好的。将处理时间缩短三到四倍可能意味着在重新开始工作前结束8小时过夜测试,并使计算机的处理器连续挂24小时之间的差异。

接下来就是简单的代码了:

gray = cv.cvtColor(blurred,cv.COLOR_BGR2GRAY)
  1. 然后二值化开始,这里有个解释:

很多处理方法都是为了后续处理和识别进行的。图像的二值化处理就是一个重要的前期图像处理。举个我做的工作,我在做二维码识别算法的探究时,首先对采集的图像进行平滑处理,就是降低噪点。然后对图像进行二值化处理,由于识别精度有要求,我是用了区域阀值,二值化需要一个阀值,区域阀值就是参考该像素点给定窗口大小内像素的均值,可以用卷积的方法去实现。使用二值化处理的原因是,因为我要识别二维码,而二维码是由“0”和“1”两种信息构成的,而我们采集的图像一般是256灰度分辨率的(灰度图像),为了方便识别,把图像中介于0到255之间的值划分为0或255是为了方便后续的识别。

所以,我这里的理由也是为了我后面可以较为容易的分割出边缘出来。
代码:

ret,binary = cv.threshold(gray,0,255,cv.THRESH_BINARY|cv.THRESH_OTSU)
  1. 接下来就是进行形态学操作,去除一些除中心位置的小白点。
kernel = cv.getStructuringElement(cv.MORPH_RECT, (3, 3))
opening = cv.morphologyEx(binary, cv.MORPH_OPEN, kernel=kernel, iterations=2)#去除了主体外小白点的
sure_bg = cv.dilate(opening, kernel, iterations=3)# 对该主体的边缘进行扩大

注意:开运算:先腐蚀后膨胀,开运算可以用来消除小的块,纤细点处分离物体,并在平滑较大的物体边界的同时不明显的改变其面积
闭运算:先膨胀后腐蚀,消除小型的黑洞,
在这里插入图片描述

  1. 接下来就开始距离变换了,使用的是cv.distanceTransform。Opencv中distanceTransform方法用于计算图像中每一个非零点距离离自己最近的零点的距离,distanceTransform的第二个Mat矩阵参数dst保存了每一个点与最近的零点的距离信息,图像上越亮的点,代表了离零点的距离越远。
    可以根据距离变换的这个性质,经过简单的运算,用于细化字符的轮廓和查找物体质心(中心)。
    当图像内的各个子图没有连接时,可以直接使用形态学的腐蚀操作确定前景对象,但是如果图像内的子图连接在一起时,就很难确定前景对象了。此时,借助于距离变换函数 cv2.distanceTransform()可以方便地将前景对象提取出来。
    注意:距离变换的结果不是另一幅二值图像,而是一幅灰度级图像,即距离图像,图像中每个像素的灰度值为该像素与距其最近的背景像素间的距离(就是原二值图像中像素值为1的像素点与最近的像素点为0的像素点之间的距离)
    由于掩码是一幅二值图像,所以经过距离变化后还需要将图像进行二值化。这样子的结果图像中像素点为1的区域就一定是硬币的位置,即前景图
    code
    dist_transform = cv.distanceTransform(opening, 1,5)
    ret, sure_fg = cv.threshold(dist_transform, 0.6*dist_transform.max(), 255, 0)

这个图片的效果是这样的:
在这里插入图片描述

  1. 接下来,我们为了剪辑出那部分轮廓,因为我们要绘图,画圈啊,所以,先将之前做的背景图做一个uint8的数据类型转换,然后再和前景色相减。代码如下:
sure_fg = np.uint8(sure_fg)
unknown = cv.subtract(sure_bg,sure_fg)#将第四点扩大的边缘和这个前景色进行相减
  1. 接下来,用到的这个函数叫连通组件标记。概念是:连接组件标记算法(connected component labeling algorithm)是图像分析中最常用的算法之一,算法的实质是扫描二值图像的每个像素点,对于像素值相同的而且相互连通分为相同的组(group),最终得到图像中所有的像素连通组件。
ret, markers1 =cv.connectedComponents(sure_fg)

markers1就是输出的标记图像。相当于是一个分类用的一个数组,将每部分都打上一个标签。

  1. 接下来就是用分水岭算法对markers1那些值比较特殊的地方进行操作了。
    第一行关于这里+1的操作的理由是我们使用了connectedComponents这个函数,它是用0标记图像的背景,用大于0的整数标记其他对象。所以我们需要对其进行加一,用1来标记图像的背景。
    第二行应该就是之前对其中不确定区域进行操作,在markers中将这些灰度值为255的对应像素置为0,因为我们希望markers是用0标记图像的不确定区域。unknow区域是这样的:
    在这里插入图片描述

接下来,了解到应该是,分水岭算法会对makers都是0的区域进行操作。然后,形成边界,这里的边界的值为-1.接下来,就对-1这个边界进行绘制了,进行像素操作即可。

# watershed transform
    markers = markers1 + 1
    markers[unknown==255] = 0
    markers3 = cv.watershed(image, markers=markers)
    image[markers3 == -1] =[0, 0, 255]

最终的结果就是这样的图像了。

在这里插入图片描述
效果还可以。注意任何两个相邻连接的组件不一定被分水岭边界(-1的像素)分开;例如在传递给 watershed 函数的初始标记图像中的物体相互接触。

函数

cv.pyrMeanShiftFiltering

void pyrMeanShiftFiltering( InputArray src, OutputArray dst,
                                         double sp, double sr, int maxLevel=1,
                                         TermCriteria termcrit=TermCriteria(
                                            TermCriteria::MAX_ITER+TermCriteria::EPS,5,1) );

第一个参数src,输入图像,8位,三通道的彩色图像,并不要求必须是RGB格式,HSV、YUV等Opencv中的彩色图像格式均可;

第二个参数dst,输出图像,跟输入src有同样的大小和数据格式;

第三个参数sp,定义的漂移物理空间半径大小;

第四个参数sr,定义的漂移色彩空间半径大小;

第五个参数maxLevel,定义金字塔的最大层数;

第六个参数termcrit,定义的漂移迭代终止条件,可以设置为迭代次数满足终止,迭代目标与中心点偏差满足终止,或者两者的结合;

cv2.connectedComponents(image, connectivity, ltype)

retval, labels =cv2.connectedComponents(image, connectivity, ltype)1

  1. image, // 输入二值图像,黑色背景
  2. connectivity = 8, // 连通域,默认是8连通
  3. ltype = CV_32, // 输出的labels类型,默认是CV_32S 输出
  4. retval, //num_labels - labels, // 输出的标记图像,背景index=0

参考

  1. OpenCV—图像分割中的分水岭算法原理与应用
  2. 对图象进行二值化处理的意义?
  3. opencv形态学中的开闭操作的用途
  4. Opencv距离变换distanceTransform应用——细化字符轮廓&&查找物体质心
  5. OpenCV图像处理-连通组件
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值