我正在写为Android与OpenCV的。 我分割类似于以下,使用标记控制的分水岭的图像,而无需用户手动地标记图像。 我打算利用区域最大值作为标记。
minMaxLoc()
会给我的价值,但我怎么能限制这正是我感兴趣的斑点? 我可以利用从结果findContours()
或cvBlob斑点限制的投资回报率,并应用到最大BLOB每?
我正在写为Android与OpenCV的。 我分割类似于以下,使用标记控制的分水岭的图像,而无需用户手动地标记图像。 我打算利用区域最大值作为标记。
minMaxLoc()
会给我的价值,但我怎么能限制这正是我感兴趣的斑点? 我可以利用从结果findContours()
或cvBlob斑点限制的投资回报率,并应用到最大BLOB每?
首先:功能minMaxLoc
发现只在一定的输入全球最小和全局最大值,因此它是确定极小区域和/或区域的最大值大多无用。 但是,你的想法是对的,提取基于区域最小/最大值为执行分水岭变换基于标记标记是完全正常。 让我尝试澄清什么是分水岭,你应该如何正确使用目前在OpenCV的执行情况。
与分水岭处理的文件有些体面的形容它类似于接下来(我可能会错过一些细节,如果你不确定:问)。 想想你知道的一些区域的表面,它含有谷和峰(不相关的对我们这里的其他细节之中)。 假设这面你只有水,颜色的水下面。 现在,让孔在表面的每一个山谷,然后水开始,以填补所有的区域。 在某一点上,不同颜色的水将满足,而当这种情况发生,你建造水坝等,他们不互相接触。 最后,你有水坝的集合,这是该流域所有分离不同颜色的水。
现在,如果你在那面做太多窟窿,你结束了太多的国家和地区:过度分割。 如果您太少你欠分割。 所以,实际上是建议使用分水岭任何纸张实际上呈现技术来避免这些问题的纸张处理中的应用。
我写了这一切(这可能是对任何人都知道什么分水岭是太天真了),因为它直接反映了你应该如何使用分水岭实现(其当前接受的答案是完全错误的方式这样做)。 现在让我们开始OpenCV的例子,使用Python绑定。
题目中的图像是由这大多是太接近,并在某些情况下,许多重叠的对象。 这里流域的用处是正确分离这些对象,而不是将它们分组到单个组件中。 因此,你需要为每个对象和良好标记为背景的至少一个标记。 作为一个例子,第一二进制化由大津的输入图像,并执行用于去除小物体形态开口。 该步骤的结果是左图像中如下所示。 现在,与二进制图像考虑应用距离变换到它,导致右。
随着距离变换的结果,我们可以考虑一些阈值,使得我们只考虑最遥远的背景(左下图)的区域。 这样,我们就可以通过前面的阈值之后标记的不同区域获得每个对象的标记。 现在,我们也可以考虑的左图像的版本扩张上方边框组成我们的标志。 完整的标记物在右边显示如下(一些标记太暗待观察,但左图像中的每个白色区域在合适的图像被表示)。
这标志我们在这里做了很大的意义。 每个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)
我想解释一下如何使用此分水岭一个简单的代码。 我使用的OpenCV的Python,但我希望你不会有任何困难理解。
在该代码中,我将使用分水岭作为前景背景提取的工具。 (这个例子是在OpenCV中食谱C ++代码的蟒对应)。 这是一个简单的例子来了解分水岭。 除此之外,您可以使用分水岭来算这个图像中的对象的数量。 这将是该代码的一个稍微高级的版本。
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)
下面是我得到的结果是:
(这样的结果是好的,因为前景和背景图像之间的巨大反差)
2 - 现在我们要建立标记。 标记是具有相同的尺寸,因为这其中是32SC1原始图像的图像(32位带符号的单信道)。
现在会有,你只是确定原始图像中的某些区域,这部分属于前景。 标记这样的区域与标记图像255。 现在,你肯定为背景,其中区域标有128你不知道标有0。这是我们下一步打算做的区域。
A -前景区域 -我们已经得到了其中药是白色的阈值图像。 我们削弱他们一点,让我们相信剩余的区域属于前景。
fg = cv2.erode(thresh,None,iterations = 2)
FG:
乙-背景区域 : -在这里,我们扩张阈值的图像,使得背景区域被减小。 但我们相信剩余的黑色区域是100%的背景。 我们将其设置为128。
bgt = cv2.dilate(thresh,None,iterations = 3)
ret,bg = cv2.threshold(bgt,1,128,1)
现在,我们得到BG如下:
Ç -现在我们既增加FG和BG:
marker = cv2.add(fg,bg)
下面就是我们得到:
现在,我们可以清楚地从上面的图像理解,说白了区域是100%的前景,灰色区域是100%的背景下,和黑色的区域,我们不知道。
然后我们把它转换成32SC1:
marker32 = np.int32(marker)
3 -最后,我们运用流域和结果转换回UINT8图像:
cv2.watershed(img,marker32)
m = cv2.convertScaleAbs(marker32)
L:
4 - 我们的门槛得当拿到面具和执行bitwise_and
与输入图像:
ret,thresh = cv2.threshold(m,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU)
res = cv2.bitwise_and(img,img,mask = thresh)
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);
为了使分水岭调用的工作,它需要一对夫妇的“暗示”关于投资回报率的。 如果你是像我这样的初学者,我建议检查出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<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标记的边境地区。
在这一点上,我们应该都设置为来电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);
}
}
这产生所示,当以下几点:
如果我们绘制源图像是由-1早些时候,标志着大坝,我们得到这样的:
编辑:
我忘了要注意:确保你清理你的垫子你与他们做了。 他们将留在记忆和OpenCVSharp可能与一些不知所云的错误信息出现。 我真的应该使用using
上述,但mat.Release()
是一个选项,以及。
此外,mmgp的回答以上包括该行: dt = ((dt - dt.min()) / (dt.max() - dt.min()) * 255).astype(numpy.uint8)
这是一个直方图拉伸施加到距离变换的结果步骤。 我省略此步骤多种原因(主要是因为我不认为我看到了直方图过于狭窄,开始),但您的里程可能会有所不同。