前言
我之所以如此,是因为我发现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> { contour }, -1, new Scalar(255, 255, 255), -1);
为了使分水岭工作正常,它将需要一些有关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);
要在中间(或“前景”)中创建黑色部分,我们将使用距离转换和阈值,这使我们从左侧的形状转到右侧的形状:
这需要一些步骤,您可能需要尝试一下阈值的下限才能获得适合您的结果:
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);
然后,我们减去这两个垫子以获得“提示”形状的最终结果:
var unknown = new Mat(); //this variable is also named "border" in some examples
Cv2.Subtract(background, foreground, unknown);
同样,如果我们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
因此,0是背景,1是字母“ H”,而2是字母“ I”。(如果您到此为止并希望可视化矩阵,我建议您查看此说明性答案。)现在,这是我们将如何利用ConnectedComponents它为分水岭创建标记(或标签)的方法:
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(y, x); //note: x and y are inexplicably
var borderPixel = (int)unknown.At(y, x); //and infuriatingly reversed
if (borderPixel == 255)
labels.Set(y, x, 0);
}
}
请注意,分水岭功能要求边界区域用0标记。因此,我们在标签/标记数组中将所有边界像素设置为0。
此时,我们应该都设置为call Watershed。但是,在我的特定应用程序中,仅在此调用期间可视化整个源图像的一小部分很有用。这对您来说可能是可选的,但是我首先只是通过扩展它来掩盖一小部分源:
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);
然后进行魔术调用:
Cv2.Watershed(sourceCrop, labels);
结果
上面的Watershed调用将labels 在适当位置进行修改。您必须回想起有关产生的矩阵ConnectedComponents。此处的区别是,如果流域在流域之间发现任何水坝,它们将在该矩阵中标记为“ -1”。像ConnectedComponents结果一样,将以类似的数字递增方式标记不同的分水岭。出于我的目的,我想将它们存储到单独的轮廓中,因此创建了此循环以将它们拆分:
var watershedContours = new List>>();
for (int x = 0; x < labels.Width; x++)
{
for (int y = 0; y < labels.Height; y++)
{
var labelPixel = labels.At(y, x); //note: x, y switched
var connected = watershedContours.Where(t => t.Item1 == labelPixel).FirstOrDefault();
if (connected == null)
{
connected = new Tuple>(labelPixel, new List());
watershedContours.Add(connected);
}
connected.Item2.Add(new Point(x, y));
if (labelPixel == -1)
sourceCrop.Set(y, x, new Vec3b(0, 255, 255));
}
}
然后,我想用随机颜色打印这些轮廓,因此创建了以下垫子:
var watershed = new Mat(source.Size(), MatType.CV_8UC3, new Scalar(0, 0, 0));
foreach (var component in watershedContours)
{
if (component.Item2.Count < (labels.Width * labels.Height) / 4 && component.Item1 >= 0)
{
var color = GetRandomColor();
foreach (var point in component.Item2)
watershed.Set(point.Y, point.X, color);
}
}
显示时产生以下内容:
如果我们在源图像上绘制之前用-1标记的水坝,则会得到以下信息:
编辑:
我忘了要注意:使用完后,请确保清洁垫子。它们将保留在内存中,并且OpenCVSharp可能会出现一些难以理解的错误消息。我确实应该在using上面使用,但是mat.Release()也是一种选择。
同样,mmgp的答案包括以下这行代码:dt = ((dt - dt.min()) / (dt.max() - dt.min()) * 255).astype(numpy.uint8),这是应用于距离变换结果的直方图拉伸步骤。我出于很多原因而省略了此步骤(主要是因为我认为我所看到的直方图并不狭窄,无法开始),但是您的里程可能会有所不同。