OpenCV: 分水岭算法的图像分割及Grabcut算法交互式前景提取

OpenCV: 分水岭算法的图像分割及Grabcut算法交互式前景提取 蜡笔不信 2023-12-06 14:42:44 227

目标

  • 使用分水岭算法实现基于标记的图像分割
  • 函数:cv2.watershed()

理论

任何灰度图像都可以看作是一个地形表面,其中高 强度的像素表示山峰,低强度表示山谷。可以用不同颜色的水(标签)填充每个孤立的山谷(局部最小值)。随着水位的上升,根据附近的山峰(坡度),来自不同山谷的水明显会开始合并,颜色也不同。为了避免这种情况,要在水融合的地方建造屏障。继续填满水,建造障碍,直到所有的山峰都在水下。然后创建的屏障将返回分割结果。这就是Watershed(分水岭算法)背后的“思想”。

但是这种方法 会由于图像中的噪声或其他不规则性而产生过度分割的结果。因此OpenCV实现了一个基于标记的分水岭算法,可以指定哪些是要合并的山谷点,哪些不是。这是一个交互式的图像分割。所做的是给我们知道的对象赋予不同的标签。用一种颜色(或强度)标记我们确定为前景或对象的区域,用另一种颜色标记我们确定为背景或非对象的区域,最后用0标记我们不确定的区域。 这是我们的标记。然后应用分水岭算法。然后标记将使用我们给出的标签进行更新,对象的边界值将为-1。

代码

下面将看到一个有关如何使用距离变换和分水岭来分割相互接触的对象的示例。
考虑下面的硬币图像,硬币彼此接触。即使设置阈值,它们也会彼此接触。

先从寻找硬币的近似估计开始。因此,可以使用Otsu的二值化。

import cv2
import numpy
from matplotlib import 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)  # ret是阈值,thresh是结果
cv2.imshow('coins', thresh)
cv2.waitKey(0)
cv2.destroyAllWindows()


现在需要去除图像中的白点噪声,可以使用形态学膨胀。要去除对象中的任何小孔,可以使用形态学腐蚀。因此,现在可以确定,靠近对象中心的区域是前景,而离对象中心很远的区域是背景。不确定的唯一区域是硬币的边界区域。

因此,需要提取可确定为硬币的区域。腐蚀会去除边界像素。因此,无论剩余多少,都可以肯定它是硬币。如果物体彼此不接触,那将起作用。但是,由于它们彼此接触,因此另一个好选择是找到距离变换并应用适当的阈值。接下来,需要找到我们确定它们不是硬币的区域。为此,对其进行了膨胀,膨胀将对象边界增加到背景。这样,由于边界区域已删除,因此可以确保结果中背景中的任何区域实际上都是背景。

剩下的区域是不确定的区域,无论是硬币还是背景。分水岭算法应该找到它。这些区域通常位于前景和背景相遇(甚至两个不同的硬币相遇)的硬币边界附近,我们称之为边界。可以通过从sure_bg区域中减去sure_fg区域来获得。

import cv2
import numpy as np
from matplotlib import pyplot as plt

# noise removal
kernel = np.ones((3, 3), np.uint8)
opening = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=2)

# sure background area
sure_bg = cv2.dilate(opening, kernel, iterations=3)

# finding sure foreground area
dist_transform = cv2.distanceTransform(opening, cv2.DIST_L2, 5)
ret, sure_fg = cv2.threshold(dist_transform, 0.7*dist_transform.max(), 255, 0)

# finding unknow region
sure_fg = np.uint8(sure_fg)
unknow = cv2.subtract(sure_bg, sure_fg)

plt.subplot(121)
plt.imshow(dist_transform, cmap='gray')
plt.title('distance transform')
plt.xticks([])
plt.yticks([])

plt.subplot(122)
plt.imshow(thresh, cmap='gray')
plt.title('threshold')
plt.xticks([])
plt.yticks([])

plt.show()

查看结果。在阈值图像中,得到了一些硬币区域,确定它们是硬币,并且现在已分离它们。(在某些情况下,可能只对前景分割感兴趣,而不对分离相互接触的对象感兴趣。在那种情况下,无需使用距离变换,只需侵蚀就足够了。侵蚀只是提取确定前景区域的另一种方法。)

现在可以确定哪些是硬币的区域,哪些是背景。因此,我们创建了标记(它的大小与原始图像的大小相同,但具有int32数据类型),并标记其中的区域。肯定知道的区域(无论是前景还是背景)都标有任何正整数,但是带有不同的整数,而不确定的区域则保留为零。为此,使用cv2.connectedComponents()。它用0标记图像的背景,然后其他对象用从1开始的整数标记。

但是,如果背景标记为0,则分水岭会将其视为未知区域。所以我们想用不同的整数来标记它。相反,将未知定义的未知区域标记为0。

# Marker labelling
ret, markers = cv2.connectedComponents(sure_fg)
# Add one to all labels so that sure background is not 0, but 1
markers = markers + 1
# Now, mark the region of unknown with zero
markers[unknow==255] = 0

plt.imshow(markers)
plt.xticks([])
plt.yticks([])
plt.show

参见JET colormap中显示的结果。深蓝色区域显示未知区域。当然,硬币的颜色不同。剩下,肯定为背景的区域显示在较浅的蓝色,跟未知区域相比。

现在标记已准备就绪。到了最后一步的时候了,使用分水岭算法。然后标记图像将被修改,边界区域将标记为-1。

void watershed( InputArray image, InputOutputArray markers ); 第一个参数 image,必须是一个8bit 3通道彩色图像矩阵序列,第一个参数没什么要说的。关键是第二个参数 markers,Opencv官方文档的说明如下: Before passing the image to the function, you have to roughly outline the desired regions in the image markers with positive (>0) indices. So, every region is represented as one or more connected components with the pixel values 1, 2, 3, and so on. Such markers can be retrieved from a binary mask using findContours() and drawContours(). The markers are “seeds” of the future image regions. All the other pixels in markers , whose relation to the outlined regions is not known and should be defined by the algorithm, should be set to 0’s. In the function output, each pixel in markers is set to a value of the “seed” components or to -1 at boundaries between the regions. 在执行分水岭函数watershed之前,必须对第二个参数markers进行处理,它应该包含不同区域的轮廓,每个轮廓有一个自己唯一的编号,轮廓的定位可以通过Opencv中findContours方法实现,这个是执行分水岭之前的要求。 接下来执行分水岭会发生什么呢?算法 会根据markers传入的轮廓作为种子(也就是所谓的注水点),对图像上其他的像素点根据分水岭算法规则进行判断,并对每个像素点的区域归属进行划定,直到处理完图像上所有像素点。而区域与区域之间的分界处的值被置为“-1”,以做区分。 简单概括一下就是说第二个入参markers必须包含了种子点信息。Opencv官方例程中使用鼠标划线标记,其实就是在定义种子,只不过需要手动操作,而使用findContours可以自动标记种子点。而分水岭方法完成之后并不会直接生成分割后的图像,还需要进一步的显示处理,如此看来,只有两个参数的watershed其实并不简单。void watershed( InputArray image, InputOutputArray markers ); 第一个参数 image,必须是一个8bit 3通道彩色图像矩阵序列,第一个参数没什么要说的。关键是第二个参数 markers,Opencv官方文档的说明如下: Before passing the image to the function, you have to roughly outline the desired regions in the image markers with positive (>0) indices. So, every region is represented as one or more connected components with the pixel values 1, 2, 3, and so on. Such markers can be retrieved from a binary mask using findContours() and drawContours(). The markers are “seeds” of the future image regions. All the other pixels in markers , whose relation to the outlined regions is not known and should be defined by the algorithm, should be set to 0’s. In the function output, each pixel in markers is set to a value of the “seed” components or to -1 at boundaries between the regions. 在执行分水岭函数watershed之前,必须对第二个参数markers进行处理,它应该包含不同区域的轮廓,每个轮廓有一个自己唯一的编号,轮廓的定位可以通过Opencv中findContours方法实现,这个是执行分水岭之前的要求。 接下来执行分水岭会发生什么呢?算法 会根据markers传入的轮廓作为种子(也就是所谓的注水点),对图像上其他的像素点根据分水岭算法规则进行判断,并对每个像素点的区域归属进行划定,直到处理完图像上所有像素点。而区域与区域之间的分界处的值被置为“-1”,以做区分。 简单概括一下就是说第二个入参markers必须包含了种子点信息。Opencv官方例程中使用鼠标划线标记,其实就是在定义种子,只不过需要手动操作,而使用findContours可以自动标记种子点。而分水岭方法完成之后并不会直接生成分割后的图像,还需要进一步的显示处理,如此看来,只有两个参数的watershed其实并不简单。

markers = cv2.watershed(img, markers) 
img[markers == -1] = [255,0,0]
plt.subplot(121)
plt.imshow(markers)
plt.title('marker image after segmentation')
plt.xticks([])
plt.yticks([])

plt.subplot(122)
plt.imshow(img)
plt.title('result')
plt.xticks([])
plt.yticks([])
plt.show()

可以从结果中看到,对某些硬币,它们接触的区域被正确地分割,而对于某些硬币,却没有被正确地分割。

import cv2
import numpy

img = cv2.imread("coins.jpg")
cv2.imshow("img", img)

# 1.图像二值化
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)

# 2.噪声去除
kernel = numpy.ones((3, 3), dtype=numpy.uint8)
open = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=2)

# 3.确定背景区域
sure_bg = cv2.dilate(open, kernel, iterations=3)

# 4.寻找前景区域
dist_transform = cv2.distanceTransform(open, 1, 5)
ret, sure_fg = cv2.threshold(dist_transform, 0.5 * dist_transform.max(), 255, cv2.THRESH_BINARY)

# 5.找到未知区域
sure_fg = numpy.uint8(sure_fg)
unknow = cv2.subtract(sure_bg, sure_fg)

# 6.类别标记
ret, markers = cv2.connectedComponents(sure_fg)
# 为所有的标记加1,保证背景是0而不是1
markers = markers + 1
# 现在让所有的未知区域为0
markers[unknow == 255] = 0

# 7.分水岭算法
markers = cv2.watershed(img, markers)
img[markers == -1] = (0, 0, 255)

cv2.imshow("gray", gray)
cv2.imshow("thresh", thresh)
cv2.imshow("open", open)
cv2.imshow("sure_bg", sure_bg)
cv2.imshow("sure_fg", sure_fg)
cv2.imshow("unknow", unknow)
cv2.imshow("img_watershed", img)
cv2.waitKey(0)
cv2.destroyWindow()

目标2

  • 看到 GrabCut算法来提取图像中的前景
  • 为此创建一个交互式应用程序

理论

GrabCut 算法由英国剑桥微软研究院的 Carsten Rother、Vladimir Kolmogorov 和 Andrew Blake 设计。在他们的论文“GrabCut”中:使用迭代图切割的交互式前景提取。需要一种算法以最少的用户交互进行前景提取,结果是 GrabCut。

从用户的角度来看它是如何工作的?最初用户在前景区域周围绘制一个矩形(前景区域应该完全在矩形内)。然后算法对其进行迭代分割以获得最 佳结果。完毕。但是在某些情况下,分割不会很好。例如,它可能将某些前景区域标记为背景,反之亦然。在这种情况下,用户需要进行精细的修饰。只需在存在一些错误结果的图像上进行一些描边即可。Strokes 基本上是说 “嘿,这个区域应该是前景,你把它标记为背景,在下一次迭代中纠正它” 或者它的相反背景。然后在下一次迭代中,将获得更好的结果。

见下图。第一位球员和足球被封闭在一个蓝色矩形中。然后用 白色笔触(表示前景)和黑色笔触(表示背景) 进行一些最终修饰,最终得到了一个不错的结果。

这背后会发生什么?

  • 用户输入矩形。此矩形之外的所有内容都将被视为确定的背景(这就是之前提到的矩形应包含所有对象的原因)。矩形内的一切都是未知的。类似地,任何指定前景和背景的用户输入都被视为硬标签,这意味着它们不会在过程中发生变化。
  • 计算机根据所提供的数据进行初始标记。标记前景和背景像素(或硬标记)
  • 现在使用高斯混合模型(GMM)对前景和背景进行建模
  • 根据提供的数据,GMM 学习并创建新的像素分布。即,未知像素被标记为可能的前景或可能的背景,这取决于它与其他硬标记像素在颜色统计方面的关系(就像聚类一样)。
  • 图形是根据此像素分布构建的。图中的节点是像素。添加了另外两个节点,Source node和Sink node。每个前景像素都连接到 Source 节点,每个背景像素都连接到 Sink 节点。
  • 将像素连接到源节点/结束节点的边的权重由像素为前景/背景的概率定义。像素之间的权重由边缘信息或像素相似度定义。如果像素颜色存在较大差异,则它们之间的边缘将获得较低的权重。
  • 然后使用mincut算法对图进行分割。它以最小的代价函数将图切割成两个分离的源节点和汇节点。成本函数是被切割的边的所有权重的总和。剪切后,所有连接到源节点的像素成为前景,连接到接收节点的像素成为背景。
  • 该过程一直持续到分类收敛。 示意图如下所示

    演示

    现在使用 OpenCV 实现grabcut算法。OpenCV 有函数cv2.grabCut() 。我们将首先看到它的参数:

    mask, bgdModel, fgdModel = cv2.grabCut(img, mask, rect, bgdModel, fgdModel, iterCount[, mode] )
    img - 输入图像
    mask - 遮罩图像,指定哪些区域是背景、前景或可能的背景/前景等。由以下标志完成,cv2.GC_BGD,cv2.GC_FGD,cv2.GC_PR_BGD,cv2.GC_PR_FGD,或简单地通过0,1,2,3
    rect - 它是包含格式为 (x,y,w,h) 的前景对象的矩形的坐标
    bdgModel , fgdModel - 这些是算法内部使用的数组。只需创建两个大小为 (1,65) 的 np.float64 类型零数组
    iterCount - 算法应该运行的迭代次数
    mode 应该是cv2.GC_INIT_WITH_RECT或cv2.GC_INIT_WITH_MASK或组合,这决定了是绘制矩形还是最终的修饰笔触。

首先,看看矩形模式( rectangular mode)。加载图像,然后创建一个类似的蒙版图像。创建fgdModelbgdModel。并给出矩形参数。这一切都是直截了当的。让算法迭代运行 5 次。模式应该是cv2.GC_INIT_WITH_RECT,这是因为使用的是矩形。然后运行grabcut。它修改蒙版图像。在新的蒙版图像中,像素将被标记为四个标志,表示上面指定的背景/前景。所以修改掩码,使得所有 0 像素和 2 像素都置为 0(即背景),所有 1 像素和 3 像素均置为 1(即前景像素)。现在最终的mask准备好了。只需将其与输入图像相乘即可得到分割图像。

import cv2
import numpy as np
from matplotlib import pyplot as plt

img = cv2.imread('messi2.jpg')
mask = np.zeros(img.shape[:2], np.uint8)
cv2.imwrite('dd.jpg', mask)

bgdModel = np.zeros((1, 65), np.float64)
fgdModel = np.zeros((1, 65), np.float64)

rect = (50, 50, 450, 290)
cv2.grabCut(img, mask, rect, bgdModel, fgdModel, 5, cv2.GC_INIT_WITH_RECT)

mask2 = np.where((mask==2)|(mask==0), 0, 1).astype('uint8')
img = img * mask2[:,:, np.newaxis]

plt.imshow(img)
plt.colorbar()
plt.show()

查看以下结果:

哎呀,梅西的头发不见了。谁喜欢没有头发的梅西?我们需要把它弄回来。因此,将使用 1 像素(当然是前景) 进行精细修饰。同时,一些地面出现了我们不想要的图片,还有一些标志, 也需要移除它们。在那里,提供了一些 0 像素的修饰(当然是背景)。因此,正如现在所说的那样,修改了之前案例中的结果掩码。

实际做的是,在绘画应用程序中打开输入图像并为图像添加了另一个图层。在油漆中使用画笔工具,在这个新图层上用白色标记错过的前景(头发、鞋子、球等)和用黑色标记不需要的背景(如标志、地面等)。然后用灰色填充剩余的背景。 然后在 OpenCV 中加载该蒙版图像,编辑我们获得的原始蒙版图像,并在新添加的蒙版图像中使用相应的值。检查下面的代码:

# newmask is the mask image by manually labelled
newmask = cv2.imread('messi-new-mask.jpg', 0)
# wherever it is marked white (sure foreground), change mask=1
# wherever it is marked black (sure background), change mask=0
mask[newmask == 0] = 0
mask[newmask == 255] = 1
mask, bgdModel, fgdModel = cv2.grabCut(img,mask,None,bgdModel,fgdModel,5,cv2.GC_INIT_WITH_MASK)
mask = np.where((mask==2)|(mask==0),0,1).astype('uint8')
img = img*mask[:,:,np.newaxis]
plt.subplot(121)
plt.imshow(mask)

plt.subplot(122)
plt.imshow(img)
plt.colorbar()
plt.show()

在这里,可以直接进入掩码模式,而不是在 rect 模式下初始化。只需用 2 像素或 3 像素(可能的背景/前景)标记蒙版图像中的矩形区域。然后像我们在第二个示例中所做的那样用 1 像素标记我们的 sure_foreground。然后直接应用带有mask模式的grabCut函数。

声明:本文内容由易百纳平台入驻作者撰写,文章观点仅代表作者本人,不代表易百纳立场。如有内容侵权或者其他问题,请联系本站进行删除。
红包 点赞 收藏 评论 打赏
评论
0个
内容存在敏感词
手气红包
    易百纳技术社区暂无数据
相关专栏
置顶时间设置
结束时间
删除原因
  • 广告/SPAM
  • 恶意灌水
  • 违规内容
  • 文不对题
  • 重复发帖
打赏作者
易百纳技术社区
蜡笔不信
您的支持将鼓励我继续创作!
打赏金额:
¥1易百纳技术社区
¥5易百纳技术社区
¥10易百纳技术社区
¥50易百纳技术社区
¥100易百纳技术社区
支付方式:
微信支付
支付宝支付
易百纳技术社区微信支付
易百纳技术社区
打赏成功!

感谢您的打赏,如若您也想被打赏,可前往 发表专栏 哦~

举报反馈

举报类型

  • 内容涉黄/赌/毒
  • 内容侵权/抄袭
  • 政治相关
  • 涉嫌广告
  • 侮辱谩骂
  • 其他

详细说明

审核成功

发布时间设置
发布时间:
是否关联周任务-专栏模块

审核失败

失败原因
备注
拼手气红包 红包规则
祝福语
恭喜发财,大吉大利!
红包金额
红包最小金额不能低于5元
红包数量
红包数量范围10~50个
余额支付
当前余额:
可前往问答、专栏板块获取收益 去获取
取 消 确 定

小包子的红包

恭喜发财,大吉大利

已领取20/40,共1.6元 红包规则

    易百纳技术社区