为了账号安全,请及时绑定邮箱和手机立即绑定

如何在OpenCV中定义分水岭的标记?

/ 猿问

如何在OpenCV中定义分水岭的标记?

123456qqq 2019-10-23 16:59:34

我正在使用OpenCV为Android编写。我正在使用标记控制的分水岭来分割类似于下面的图像,而无需用户手动标记图像。我打算使用区域最大值作为标记。

minMaxLoc()会给我带来价值,但是如何将其限制在我感兴趣的斑点上?我可以利用来自findContours()或cvBlob Blob 的结果来限制ROI并将最大值应用于每个Blob吗?

http://img1.sycdn.imooc.com/5db016880001c63c02560192.jpg

查看完整描述

3 回答

?
精慕HU

首先:该函数minMaxLoc仅查找给定输入的全局最小值和全局最大值,因此对于确定区域最小值和/或区域最大值而言,它几乎没有用。但是您的想法是正确的,完全基于区域最小值/最大值提取标记以执行基于标记的分水岭变换。让我尝试阐明什么是分水岭变换,以及如何正确使用OpenCV中存在的实现。

一些处理分水岭的论文对其描述与随后的描述类似(如果不确定,我可能会漏掉一些细节:询问)。考虑一下您所知道的某个区域的表面,其中包含山谷和山峰(在这里,与我们无关的其他细节)。假设在此表面之下仅是水,有色水。现在,在表面的每个谷上打孔,然后水开始充满整个区域。在某个时候,会遇到不同颜色的水,当这种情况发生时,您要建造一个大坝,使它们彼此不接触。最后,您将获得水坝的集合,这是分隔所有不同颜色的水的分水岭。

现在,如果您在该表面上钻孔太多,则最终会出现太多区域:过度分割。如果您做得太少,您将获得细分市场。因此,几乎所有建议使用分水岭的论文实际上都提出了避免这些问题的技术,以解决论文正在处理的应用程序。

我写了所有这些内容(对于任何了解分水岭变换的人来说都太幼稚了),因为它直接反映了您应该如何使用分水岭实现(当前公认的答案是完全错误的方式)。现在让我们使用Python绑定从OpenCV示例开始。

问题中显示的图像由许多物体组成,这些物体太近且在某些情况下重叠。分水岭在这里的用途是正确地将这些对象分开,而不是将它们分组为单个组件。因此,每个对象至少需要一个标记,背景至少需要一个良好的标记。例如,首先通过Otsu对输入图像进行二值化,然后执行形态学打开操作以去除小物体。该步骤的结果如下左图所示。现在,对于二进制图像,请考虑对其应用距离变换,结果如右图所示。

//img1.sycdn.imooc.com/5db01699000144de02560192.jpg//img1.sycdn.imooc.com/5db0169a0001973d02560192.jpg根据距离变换结果,我们可以考虑一些阈值,以便仅考虑距背景最远的区域(下图为左图)。这样做,通过在较早的阈值之后标记不同的区域,我们可以获得每个对象的标记。现在,我们还可以考虑将左侧图片的放大版本的边框组成标记。完整的标记显示在右侧的下方(某些标记太暗而看不见,但左侧图像中的每个白色区域均在右侧图像中表示)。

//img1.sycdn.imooc.com/5db016a600015f2102560192.jpg//img2.sycdn.imooc.com/5db016a7000164ab02560192.jpg

我们在这里使用的这个标记很有意义。每个colored water == one marker将开始填充该区域,并且分水岭转换将建造水坝,以阻止不同的“颜色”合并。如果进行变换,则图像在左侧。通过将水坝与原始图像进行组合而仅考虑水坝,我们得到的结果是正确的。

//img2.sycdn.imooc.com/5db016b00001632602560192.jpg//img3.sycdn.imooc.com/5db016b100011fb802560192.jpg

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)


查看完整回答
反对 2019-10-23
?
慕婉清6462132

我想在这里解释一个简单的代码,说明如何使用分水岭。我正在使用OpenCV-Python,但是希望您不会有任何困难。


在这段代码中,我将使用分水岭作为前景-背景提取的工具。(此示例是OpenCV食谱中C ++代码的python对应版本)。这是了解分水岭的简单案例。除此之外,您可以使用分水岭来计算该图像中的对象数量。这将是此代码的高级版本。


1-首先,我们加载图像,将其转换为灰度,并使用合适的值对其进行阈值处理。我进行了Otsu的二值化,因此它将找到最佳阈值。


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)

以下是我得到的结果:

//img2.sycdn.imooc.com/5db016c10001051902560192.jpg

(即使结果很好,因为前景和背景图像之间的对比度很高)


2-现在我们必须创建标记。标记是具有与原始图像相同大小的图像,即32SC1(32位带符号单通道)。


现在,您只需确定原始图像中的某些区域就属于前景。在标记图像中用255标记该区域。现在,您确定将作为背景的区域将标记为128。不确定的区域将标记为0。这就是下一步。


A-前景区域:-我们已经有了一个阈值图像,其中药丸是白色的。我们会稍微侵蚀它们,以确保其余区域属于前景。


fg = cv2.erode(thresh,None,iterations = 2)

fg:

//img1.sycdn.imooc.com/5db016d0000175a802560192.jpg

B-背景区域:-在这里我们扩大阈值图像,以便减少背景区域。但是我们确定剩下的黑色区域是100%背景。我们将其设置为128。


bgt = cv2.dilate(thresh,None,iterations = 3)

ret,bg = cv2.threshold(bgt,1,128,1)

现在我们得到bg如下:

//img4.sycdn.imooc.com/5db016da0001962902560192.jpg

C-现在我们同时添加fg和bg:


marker = cv2.add(fg,bg)

以下是我们得到的:

//img1.sycdn.imooc.com/5db016e70001c58d02560192.jpg

现在我们可以从图像上清楚地了解到,白色区域是100%前景,灰色区域是100%背景,而黑色区域我们不确定。


然后我们将其转换为32SC1:


marker32 = np.int32(marker)

3-最后,我们应用分水岭并将结果转换回uint8图片:


cv2.watershed(img,marker32)

m = cv2.convertScaleAbs(marker32)

米:

//img3.sycdn.imooc.com/5db016f00001575d02560192.jpg

4 - 我们的门槛得当拿到面具,并执行bitwise_and与输入图像:


ret,thresh = cv2.threshold(m,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU)

res = cv2.bitwise_and(img,img,mask = thresh)

资源:

//img2.sycdn.imooc.com/5db016fb0001505702560192.jpg

希望能帮助到你!!!



查看完整回答
反对 2019-10-23
?
摇曳的蔷薇

前言


我之所以如此,是因为我发现OpenCV文档(和C ++示例)中的分水岭教程以及上面的mmgp答案都相当令人困惑。我多次重新审视分水岭的方法,最终放弃了挫败感。我终于意识到,我至少需要尝试一下这种方法,并在实际中看到它。这是我整理完所有教程后得出的结论。


除了成为计算机视觉新手之外,我的大部分麻烦可能与我使用OpenCVSharp库而不是Python的要求有关。C#没有像在NumPy中发现的那样内置高功率数组运算符(尽管我意识到这已经通过IronPython进行了移植),因此我在理解和实现C#中的这些操作上费了不少力气。另外,为了记录在案,我真的很鄙视大多数这些函数调用的细微差别和不一致之处。OpenCVSharp是我使用过的最脆弱的库之一。但是,嘿,这是一个港口,所以我期待什么?最重要的是,它是免费的。


事不宜迟,让我们谈谈我对分水岭的OpenCVSharp实施,并希望阐明总体上分水岭实施的一些棘手要点。


应用


首先,确保分水岭是您想要的,并了解其用途。我正在使用染色的细胞板,就像这样:

//img3.sycdn.imooc.com/5db0170a0001441704210338.jpg

我花了好一会儿才弄清楚我不能只打一个分水岭的电话来区分田间的每个单元。相反,我首先必须隔离田野的一部分,然后在那小部分上进行分水岭。我通过多个过滤器隔离了感兴趣的区域(ROI),在此我将对其进行简要说明:

//img1.sycdn.imooc.com/5db0171c000179ba06750128.jpg

  1. 从源图像开始(左图,用于演示)

  2. 隔离红色通道(左中间)

  3. 应用自适应阈值(右中间)

  4. 找到轮廓,然后消除那些面积较小的轮廓(右)

一旦我们清理了上述阈值操作产生的轮廓,就该寻找分水岭的候选对象了。就我而言,我只是简单地遍历大于特定区域的所有轮廓。

假设我们已将上述轮廓与上述字段隔离开来作为我们的投资回报率:

//img1.sycdn.imooc.com/5db017250001b3cd01220112.jpg

让我们看一下如何编写分水岭。


我们将从空白垫开始,仅绘制定义投资回报率的轮廓:


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);

为了使分水岭工作正常,它将需要一些有关ROI的“提示”。如果您是像我这样的完整初学者,建议您查看CMM分水岭页面以快速入门。可以说,我们将通过在右侧创建形状来创建关于ROI的提示:

//img1.sycdn.imooc.com/5db0172f0001cfdc02330095.jpg

要创建此“提示”形状的白色部分(或“背景”),我们将Dilate像这样隔离形状:


var kernel = Cv2.GetStructuringElement(MorphShapes.Ellipse, new Size(2, 2));

var background = new Mat();

Cv2.Dilate(isolatedContour, background, kernel, iterations: 8);

要在中间(或“前景”)中创建黑色部分,我们将使用距离转换和阈值,这使我们从左侧的形状转到右侧的形状:

//img1.sycdn.imooc.com/5db017370001557002460090.jpg

这需要一些步骤,您可能需要尝试一下阈值的下限才能获得适合您的结果:


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 未知,它将看起来像这样:

//img1.sycdn.imooc.com/5db0173f0001174901210095.jpg

真好!这对我来说很容易。然而,下一部分让我很困惑。让我们看一下将“提示”变成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<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);

    }

}

请注意,分水岭功能要求边界区域用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<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));


    }

}

然后,我想用随机颜色打印这些轮廓,因此创建了以下垫子:


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);

    }

}

显示时产生以下内容:

//img1.sycdn.imooc.com/5db017490001c0a601240086.jpg

如果我们在源图像上绘制之前用-1标记的水坝,则会得到以下信息:

//img2.sycdn.imooc.com/5db0175100019bb101660109.jpg

编辑:


我忘了要注意:使用完后,请确保清洁垫子。它们将保留在内存中,并且OpenCVSharp可能会出现一些难以理解的错误消息。我确实应该在using上面使用,但是mat.Release()也是一种选择。


同样,mmgp的答案包括以下这行代码:dt = ((dt - dt.min()) / (dt.max() - dt.min()) * 255).astype(numpy.uint8),这是应用于距离变换结果的直方图拉伸步骤。我出于很多原因而省略了此步骤(主要是因为我认为我所看到的直方图并不狭窄,无法开始),但是您的里程可能会有所不同。


查看完整回答
反对 2019-10-23
  • 3 回答
  • 0 关注
  • 527 浏览

添加回答

回复

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信