Tru*_*Tru 67 opencv image-processing computer-vision image-segmentation watershed
我正在用OpenCV为Android写作.我正在使用标记控制的分水岭分割类似于下面的图像,而无需用户手动标记图像.我打算使用区域最大值作为标记.
minMaxLoc()会给我价值,但我怎么能把它限制在我感兴趣的blob?我可以利用findContours()cvBlob blob 的结果来限制ROI并对每个blob应用最大值吗?

mmg*_*mgp 110
首先:该函数minMaxLoc仅查找给定输入的全局最小值和全局最大值,因此它对于确定区域最小值和/或区域最大值几乎没用.但是你的想法是正确的,基于区域最小/最大值提取标记以执行基于标记的分水岭变换是完全没问题的.让我试着澄清什么是Watershed Transform以及如何正确使用OpenCV中的实现.
处理分水岭的一些相当数量的论文与后面的内容相似(我可能会错过一些细节,如果你不确定的话:问).考虑一下您知道的某个区域的表面,它包含山谷和山峰(以及其他与我们无关的细节).假设在这个表面下方,你只有水,有色水.现在,在你的表面的每个山谷中打洞,然后水开始填满所有区域.在某些时候,不同颜色的水域会相遇,当发生这种情况时,你会建造一个大坝,使它们不会相互接触.最后,您将拥有一系列水坝,这是将所有不同颜色的水分开的分水岭.
现在,如果你在那个表面上制作太多的洞,你最终会得到太多的区域:过分割.如果你做得太少,你会得到一个不足的细分.因此,几乎任何建议使用分水岭的纸张实际上都提供了避免这些问题的技术,以应对纸张正在处理的应用.
我写了所有这些(对于知道Watershed Transform是什么的人来说,这可能太天真了),因为它直接反映了你应该如何使用分水岭实现(当前接受的答案以完全错误的方式进行).现在让我们使用Python绑定开始OpenCV示例.
问题中呈现的图像由许多对象组成,这些对象过于接近并且在某些情况下重叠.此处分水岭的用处是正确分离这些对象,而不是将它们分组为单个组件.因此,每个对象至少需要一个标记,背景需要很好的标记.作为示例,首先通过Otsu对输入图像进行二值化并执行用于移除小对象的形态开口.该步骤的结果如左图所示.现在用二进制图像考虑对它应用距离变换,得到右边的结果.

利用距离变换结果,我们可以考虑一些阈值,使得我们仅考虑距离背景最远的区域(左下图).这样做,我们可以通过在较早的阈值之后标记不同的区域来获得每个对象的标记.现在,我们还可以考虑上面左图的扩张版本的边界来构成我们的标记.完整标记显示在右下方(某些标记太暗而无法看到,但左图中的每个白色区域都显示在右图中).

我们这里的标记很有意义.每个都colored water == one marker将开始填充该区域,并且分水岭变换将构建水坝以阻止不同的"颜色"合并.如果我们进行变换,我们会得到左边的图像.通过将它们与原始图像组合来仅考虑水坝,我们得到了正确的结果.

import sys
import cv2
import numpy
from scipy.ndimage import label
def segment_on_dt(a, img):
border = cv2.dilate(img, None, iterations=5)
border = border - cv2.erode(border, None)
dt = cv2.distanceTransform(img, 2, 3)
dt = ((dt - dt.min()) / (dt.max() - dt.min()) * 255).astype(numpy.uint8)
_, dt = cv2.threshold(dt, 180, 255, cv2.THRESH_BINARY)
lbl, ncc = label(dt)
lbl = lbl * (255 / (ncc + 1))
# Completing the markers now.
lbl[border == 255] = 255
lbl = lbl.astype(numpy.int32)
cv2.watershed(a, lbl)
lbl[lbl == -1] = 0
lbl = lbl.astype(numpy.uint8)
return 255 - lbl
img = cv2.imread(sys.argv[1])
# Pre-processing.
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, img_bin = cv2.threshold(img_gray, 0, 255,
cv2.THRESH_OTSU)
img_bin = cv2.morphologyEx(img_bin, cv2.MORPH_OPEN,
numpy.ones((3, 3), dtype=int))
result = segment_on_dt(img, img_bin)
cv2.imwrite(sys.argv[2], result)
result[result != 255] = 0
result = cv2.dilate(result, None)
img[result == 255] = (0, 0, 255)
cv2.imwrite(sys.argv[3], img)
Run Code Online (Sandbox Code Playgroud)
Abi*_*n K 41
我想解释一下如何在这里使用分水岭的简单代码.我正在使用OpenCV-Python,但我希望你不会有任何理解的困难.
在此代码中,我将使用分水岭作为前景 - 背景提取的工具.(这个例子是OpenCV cookbook中C++代码的python版本).这是一个了解分水岭的简单案例.除此之外,您还可以使用分水岭计算此图像中的对象数量.这将是此代码的略微高级版本.
1 - 首先我们加载图像,将其转换为灰度,并使用合适的值对其进行阈值处理.我采用了大津的二值化,因此它会找到最佳阈值.
import cv2
import numpy as np
img = cv2.imread('sofwatershed.jpg')
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
ret,thresh = cv2.threshold(gray,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU)
Run Code Online (Sandbox Code Playgroud)
以下是我得到的结果:

(即使结果很好,因为前景和背景图像之间的对比度很大)
2 - 现在我们必须创建标记.标记是与原始图像大小相同的图像,即32SC1(32位有符号单通道).
现在原始图像中会有一些区域,您可以确定该区域属于前景.在标记图像中标记255个这样的区域.现在你肯定是背景的区域标有128.你不确定的区域标有0.我们接下来要做的.
A - 前景区域: - 我们已经有一个阈值图像,药丸是白色的.我们对它们进行了一点侵蚀,以便我们确定剩下的区域属于前景.
fg = cv2.erode(thresh,None,iterations = 2)
Run Code Online (Sandbox Code Playgroud)
fg:

B - 背景区域: - 这里我们扩展阈值图像,以减少背景区域.但我们确信剩下的黑色区域是100%的背景.我们将它设置为128.
bgt = cv2.dilate(thresh,None,iterations = 3)
ret,bg = cv2.threshold(bgt,1,128,1)
Run Code Online (Sandbox Code Playgroud)
现在我们得到如下bg:

C - 现在我们添加fg和bg:
marker = cv2.add(fg,bg)
Run Code Online (Sandbox Code Playgroud)
以下是我们得到的:

现在我们可以从上面的图像清楚地了解到,白色区域是100%前景,灰色区域是100%背景,黑色区域我们不确定.
然后我们将其转换为32SC1:
marker32 = np.int32(marker)
Run Code Online (Sandbox Code Playgroud)
3 - 最后我们应用分水岭并将结果转换回uint8图像:
cv2.watershed(img,marker32)
m = cv2.convertScaleAbs(marker32)
Run Code Online (Sandbox Code Playgroud)
米:

4 - 我们正确地对其进行阈值以获取掩码并bitwise_and使用输入图像执行:
ret,thresh = cv2.threshold(m,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU)
res = cv2.bitwise_and(img,img,mask = thresh)
Run Code Online (Sandbox Code Playgroud)
res:

希望能帮助到你!!!
方舟
前言
我之所以如此,是因为我发现OpenCV文档(和C ++示例)中的分水岭教程以及上面的mmgp答案都相当令人困惑。我多次重新审视分水岭的方法,最终放弃了挫败感。我终于意识到,我至少需要尝试一下这种方法,并在实际中看到它。这是我整理完所有教程后得出的结论。
除了成为计算机视觉新手之外,我的大部分麻烦可能与我使用OpenCVSharp库而不是Python的要求有关。C#没有像在NumPy中发现的那样内置高功率数组运算符(尽管我意识到这已经通过IronPython进行了移植),因此我在理解和实现C#中的这些操作上费了不少力气。另外,为了记录在案,我真的很鄙视大多数这些函数调用的细微差别和不一致之处。OpenCVSharp是我使用过的最脆弱的库之一。但是,嘿,这是一个港口,所以我期待什么?最重要的是,它是免费的。
事不宜迟,让我们谈谈我对分水岭的OpenCVSharp实施,并希望阐明总体上分水岭实施的一些棘手要点。
应用
首先,确保分水岭是您想要的,并了解其用途。我正在使用染色的细胞板,就像这样:
我花了好一会儿才弄清楚我不能只打一个分水岭的电话来区分田间的每个单元。相反,我首先必须隔离田野的一部分,然后在那小部分上进行分水岭。我通过多个过滤器隔离了感兴趣的区域(ROI),在此我将对其进行简要说明:
一旦我们清理了上述阈值操作产生的轮廓,就该寻找分水岭的候选对象了。就我而言,我只是简单地遍历大于特定区域的所有轮廓。
码
假设我们已将上述轮廓与上述字段隔离开来作为我们的投资回报率:
让我们看一下如何编写分水岭。
我们将从空白垫开始,仅绘制定义投资回报率的轮廓:
var isolatedContour = new Mat(source.Size(), MatType.CV_8UC1, new Scalar(0, 0, 0));
Cv2.DrawContours(isolatedContour, new List<List<Point>> { contour }, -1, new Scalar(255, 255, 255), -1);
Run Code Online (Sandbox Code Playgroud)
为了使分水岭工作正常,它将需要一些有关ROI的“提示”。如果您是像我这样的完整初学者,建议您查看CMM分水岭页面以快速入门。可以说,我们将通过在右侧创建形状来创建关于ROI的提示:
要创建此“提示”形状的白色部分(或“背景”),我们将Dilate像这样隔离形状:
var kernel = Cv2.GetStructuringElement(MorphShapes.Ellipse, new Size(2, 2));
var background = new Mat();
Cv2.Dilate(isolatedContour, background, kernel, iterations: 8);
Run Code Online (Sandbox Code Playgroud)
要在中间(或“前景”)中创建黑色部分,我们将使用距离转换和阈值,这使我们从左侧的形状转到右侧的形状:
这需要一些步骤,您可能需要尝试一下阈值的下限才能获得适合您的结果:
var foreground = new Mat(source.Size(), MatType.CV_8UC1);
Cv2.DistanceTransform(isolatedContour, foreground, DistanceTypes.L2, DistanceMaskSize.Mask5);
Cv2.Normalize(foreground, foreground, 0, 1, NormTypes.MinMax); //Remember to normalize!
foreground.ConvertTo(foreground, MatType.CV_8UC1, 255, 0);
Cv2.Threshold(foreground, foreground, 150, 255, ThresholdTypes.Binary);
Run Code Online (Sandbox Code Playgroud)
然后,我们减去这两个垫子以获得“提示”形状的最终结果:
var unknown = new Mat(); //this variable is also named "border" in some examples
Cv2.Subtract(background, foreground, unknown);
Run Code Online (Sandbox Code Playgroud)
同样,如果我们Cv2.ImShow 未知,它将看起来像这样:
真好!这对我来说很容易。然而,下一部分让我很困惑。让我们看一下将“提示”变成Watershed函数可以使用的东西。为此,我们需要使用ConnectedComponents,这基本上是根据像素索引进行分组的大像素矩阵。例如,如果我们有一个垫子,字母为“ HI”,则ConnectedComponents可能返回此矩阵:
0 0 0 0 0 0 0 0 0
0 1 0 1 0 2 2 2 0
0 1 0 1 0 0 2 0 0
0 1 1 1 0 0 2 0 0
0 1 0 1 0 0 2 0 0
0 1 0 1 0 2 2 2 0
0 0 0 0 0 0 0 0 0
Run Code Online (Sandbox Code Playgroud)
因此,0是背景,1是字母“ H”,而2是字母“ I”。(如果您到此为止并希望可视化矩阵,我建议您查看此说明性答案。)现在,这是我们将如何利用ConnectedComponents它为分水岭创建标记(或标签)的方法:
0 0 0 0 0 0 0 0 0
0 1 0 1 0 2 2 2 0
0 1 0 1 0 0 2 0 0
0 1 1 1 0 0 2 0 0
0 1 0 1 0 0 2 0 0
0 1 0 1 0 2 2 2 0
0 0 0 0 0 0 0 0 0
Run Code Online (Sandbox Code Playgroud)
请注意,分水岭功能要求边界区域用0标记。因此,我们在标签/标记数组中将所有边界像素设置为0。
此时,我们应该都设置为call Watershed。但是,在我的特定应用程序中,仅在此调用期间可视化整个源图像的一小部分很有用。这对您来说可能是可选的,但是我首先只是通过扩展它来掩盖一小部分源:
var labels = new Mat(); //also called "markers" in some examples
Cv2.ConnectedComponents(foreground, labels);
labels = labels + 1;
//this is a much more verbose port of numpy's: labels[unknown==255] = 0
for (int x = 0; x < labels.Width; x++)
{
for (int y = 0; y < labels.Height; y++)
{
//You may be able to just send "int" in rather than "char" here:
var labelPixel = (int)labels.At<char>(y, x); //note: x and y are inexplicably
var borderPixel = (int)unknown.At<char>(y, x); //and infuriatingly reversed
if (borderPixel == 255)
labels.Set(y, x, 0);
}
}
Run Code Online (Sandbox Code Playgroud)
然后进行魔术调用:
var mask = new Mat();
Cv2.Dilate(isolatedContour, mask, new Mat(), iterations: 20);
var sourceCrop = new Mat(source.Size(), source.Type(), new Scalar(0, 0, 0));
source.CopyTo(sourceCrop, mask);
Run Code Online (Sandbox Code Playgroud)
结果
上面的Watershed调用将labels 在适当位置进行修改。您必须回想起有关产生的矩阵ConnectedComponents。此处的区别是,如果流域在流域之间发现任何水坝,它们将在该矩阵中标记为“ -1”。像ConnectedComponents结果一样,将以类似的数字递增方式标记不同的分水岭。出于我的目的,我想将它们存储到单独的轮廓中,因此创建了此循环以将它们拆分:
Cv2.Watershed(sourceCrop, labels);
Run Code Online (Sandbox Code Playgroud)
然后,我想用随机颜色打印这些轮廓,因此创建了以下垫子:
var watershedContours = new List<Tuple<int, List<Point>>>();
for (int x = 0; x < labels.Width; x++)
{
for (int y = 0; y < labels.Height; y++)
{
var labelPixel = labels.At<Int32>(y, x); //note: x, y switched
var connected = watershedContours.Where(t => t.Item1 == labelPixel).FirstOrDefault();
if (connected == null)
{
connected = new Tuple<int, List<Point>>(labelPixel, new List<Point>());
watershedContours.Add(connected);
}
connected.Item2.Add(new Point(x, y));
if (labelPixel == -1)
sourceCrop.Set(y, x, new Vec3b(0, 255, 255));
}
}
Run Code Online (Sandbox Code Playgroud)
显示时产生以下内容:
如果我们在源图像上绘制之前用-1标记的水坝,则会得到以下信息:
编辑:
我忘了要注意:使用完后,请确保清洁垫子。它们将保留在内存中,并且OpenCVSharp可能会出现一些难以理解的错误消息。我确实应该在using上面使用,但是mat.Release()也是一种选择。
同样,mmgp的答案包括以下这行代码:dt = ((dt - dt.min()) / (dt.max() - dt.min()) * 255).astype(numpy.uint8),这是应用于距离变换结果的直方图拉伸步骤。由于多种原因,我省略了此步骤(主要是因为我认为我看到的直方图并不狭窄,无法开始),但是您的里程可能会有所不同。