(三)OpenCV中的图像处理之霍夫变换、分水岭算法以及GrabCut算法

注释:本文翻译自OpenCV3.0.0 document->OpenCV-Python Tutorials,包括对原文档种错误代码的纠正


3.13 霍夫变换

第一节:霍夫线变换(Hough Line Transform)

1.目标

  • 理解霍夫变换的概念
  • 学习如何使用霍夫变换检测图像中的行
  • 学习一下函数:cv2.HoughLines(),cv2.HoughLinesP()

2.理论

如果可以用数学形式表示该形状,则Hough变换是检测任何形状的流行技术。即使形状被破坏或扭曲了一点也可以检测到。我们将看到它是如何检测出一条线的。

线可以表示为或以参数形式表示,如,其中是从原点到线的垂直距离,是角度,由此垂直线和水平轴形成的逆时针方向(该方向因你如何表示坐标系而异),此表示法用于OpenCV中,检查下面的图像:

(三)OpenCV中的图像处理之霍夫变换、分水岭算法以及GrabCut算法_第1张图片

所以如果线在原点以下经过,它将具有正的rho和小于180的角度。如果线在原点之上,而不是取大于180度的角,则取小于180度的角,并且将rho取为负。任何垂直线将会是0度,水平线将有90度。

现在让我们看看Hough变换如何用于线条。任何一行都可以用着两个项表示。所以首先它创建一个二维数组或累加器(用以保存两个参数的值),并且它最初设置为0.让行表示,列表是。阵列的大小取决于你需要的准确性,假设你想要角度的精度为1度,则需要180列。对于,可能的最大距离是图像的对角线长度。因此,以一个像素精度,行数可以是图像的对角线长度。

考虑在中间的水平线的100*100图像,采取行的第一点。你知道它的(x,y)值,现在在直线方程中,把值=0,1,2,3,…,180并检查你得到的。对于每个对,你在我们的累加器中的相应单元格中将值递增。所以现在在累加器中,单元(50,90)=1以及一些其它单元。

现在看线上的第二点。按照上面的方法做,增加对应 的单元格中的值。这次,单元格(50,90)=2。你实际做的是增加 的值。你继续这个过程中的每一点。在这个点上,单元格(50,90)将递增或递减,而其它单元格可能会被投票或不投票。这样,最后,单元格(50,90)将获得最高票数。因此,如果你搜索累加器以获得最大票选,则会得到值(50,90),该图像中距离原点的距离为50,角度为90处有一条线。它在下面的动画中很好地显示。
(三)OpenCV中的图像处理之霍夫变换、分水岭算法以及GrabCut算法_第2张图片

这是线条变换的工作原理。这很简单,你可以自己使用Numpy来实现它。以下是显示累加器的图像。某些位置的亮点表示它们是图像中可能线条的参数。(图片提供,维基百科)

(三)OpenCV中的图像处理之霍夫变换、分水岭算法以及GrabCut算法_第3张图片


3.OpenCV中的霍夫变换

上面解释的所有内容都封装在OpenCV函数cv2.HoughLines()中。它只是返回一个值的数组,以像素为单位进行测量,以弧度为单位进行测量。第一个参数,输入图像应该是一个二值图像,所以应用阈值或使用canny边缘检测,然后才应用霍夫变换。第二个和第三个参数分别是,第四个参数是阈值,这意味着最低的投票数应该被视为一条线。记住,票数取决于线上的点数。所以它代表了应该检测到的行的最小长度。

# -*- coding: utf-8 -*-
'''
OpenCV中的霍夫变换
1.cv2.HoughLines(),返回一个(长度,角度)的数组,前者以像素为单位进行测量,后者以弧度为单位进行测量
2.该函数的第一个参数是二值图像,应用阈值或使用canny边缘检测,然后才应用霍夫变换。
  第二个参数和第三个参数分别为长度和角度,第四个参数是阈值。
'''

import cv2
import numpy as np

img = cv2.imread('sudoku.png')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
edges = cv2.Canny(gray, 50, 150, apertureSize=3)

lines = cv2.HoughLines(edges, 1, np.pi / 180, 200)
print('lines', lines, 'len', len(lines))
# 获取霍夫线数组长度
for i in range(len(lines)):
    for rho, theta in lines[i]:
        a = np.cos(theta)
        b = np.sin(theta)
        x0 = a * rho
        y0 = b * rho
        x1 = int(x0 + 1000 * (-b))
        y1 = int(y0 + 1000 * (a))
        x2 = int(x0 - 1000 * (-b))
        y2 = int(y0 - 1000 * (a))

        cv2.line(img, (x1, y1), (x2, y2), (0, 0, 255), 2)

cv2.imwrite('houghlines3.png', img)
cv2.imshow('res', img)
cv2.waitKey(0) & 0xFF
cv2.destroyAllWindows()
(三)OpenCV中的图像处理之霍夫变换、分水岭算法以及GrabCut算法_第4张图片

4.概率霍夫变换(probabilistic Hough Transform)

在霍夫变换中,即使对于有两个参数的行,也可以看到很多计算。概率霍夫变换是我们看到霍夫变换的一种优化。它没有考虑所有的要点,而是只采用随机子集的点数,这足以进行在线检测。只是我们必须降低阈值。看下面的图像,比较霍夫空间中的霍夫变换和概率霍夫变换。

(三)OpenCV中的图像处理之霍夫变换、分水岭算法以及GrabCut算法_第5张图片

OpenCV中的实现是基于Matas, J. and Galambos, C. and Kittler, J.V.使用逐行概率霍夫变换的线的鲁棒检测。所使用的函数是cv2.HoughLinesP。它有两个参数:

l  MinLineLength:行的最小长度。比这更小的线段被拒绝。

l  MaxLineGap:线段之间允许的最大间距,将它们视为单行。

最好的是,它将直接返回行的两个端点。在前面的例子中,你只有线的参数,你必须找到所有的点。在这里,一切都是直接而简单的。

# -*- coding: utf-8 -*-
'''
概率霍夫变换:
1.概率霍夫变换是我们看到霍夫变换的一种优化。它没有考虑所有的点,而是只采用随机子集的点数。
2.OpenCV中使用cv2.HoughLinesP(MinLinelength,MaxLineGap)
MinLinelength:行的最小长度。
MaxLineGap:线段之间允许的最大间距
'''

import cv2
import numpy as np

img = cv2.imread('sudoku.png')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
edges = cv2.Canny(gray, 50, 150, apertureSize=3)
minLineLength = 100
maxLineGap = 10

lines = cv2.HoughLinesP(edges, 1, np.pi / 180, minLineLength, maxLineGap)
print("lines", lines, "len", len(lines))
for i in range(len(lines)):
    for x1, y1, x2, y2 in lines[i]:
        cv2.line(img, (x1, y1), (x2, y2), (0, 255, 0), 1)

cv2.imwrite('houghLines5.png', img)
cv2.imshow('res', img)
cv2.waitKey(0) & 0xFF
cv2.destroyAllWindows()

结果如下:

(三)OpenCV中的图像处理之霍夫变换、分水岭算法以及GrabCut算法_第6张图片


第二节:霍夫圆变换(Hough Circle Transform)

1.目标

  • 使用霍夫变换来查找图像中的圆
  • 学习这些函数:cv2.HoughCircles()

2.理论

一个圆在数学上表示为 其中 表示圆点,表示圆的半径。在等式中,我们可以看到有三个参数,所以需要一个3D累加器来进行霍夫变换,这个效率很低的。所以OpenCV使用更棘手的方法,即使用边缘梯度信息的Hough Gradient Method

这里我们使用的函数是cv2.HoughCircles()。它有很多参数,在文档中有很好的解释。

直接看代码吧!

# -*- coding: utf-8 -*-
'''
霍夫圆变换:
1.cv2.HoughCircles()
'''

import cv2
import numpy as np

img = cv2.imread('opencv-logo.png', 0)
img = cv2.medianBlur(img, 5)  # 中值滤波
cimg = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)

circles = cv2.HoughCircles(img, cv2.HOUGH_GRADIENT, 1, 20, param1=50, param2=30, minRadius=0, maxRadius=0)

circles = np.uint16(np.around(circles))

print('circles', circles)
for i in circles[0]:
    # 绘制外接圆
    cv2.circle(cimg, (i[0], i[1]), i[2], (0, 255, 0), 2)
    # 绘制中心的圆
    cv2.circle(cimg, (i[0], i[1]), 2, (0, 0, 255), 3)

cv2.imshow('res', cimg)
cv2.waitKey(0) & 0xFF
cv2.destroyAllWindows()

结果:

(三)OpenCV中的图像处理之霍夫变换、分水岭算法以及GrabCut算法_第7张图片


3.14 图像分割和分水岭算法

1.目标

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

2.理论

任何灰度图像都可以看作是一个地形表面,其中高强度表示峰和山,而低强度表示山谷。你开始用不同颜色的水(标签)填充每个孤立的山谷(山谷为最低点)。随着水位上升,根据附近的山峰(梯度),来自不同山谷的水,显然会以不同的颜色开始合并。为了避免这种情况,你在水合并的地方建立了障碍。你继续进行填充水和筑垒的工作,直到所有的峰都在水中。然后你创建的障碍为你提供了分割结果。这是分水岭背后的“哲学”。你可以通过一些动画来直观了解分水岭。

但是这种方法可以为噪声或图像中的任何其它不规则情况提供过份的结果。所以,OpenCV实现了一个基于标记的分水岭算法,你可以指定哪些山谷要被合并,哪些不被合并。这是一个交互式图像分割。我们所做的就是为我们所知的对象提供不同的标签。用一种颜色(或强度)标记我们确定为前景或对象的区域,用另一种颜色标记我们确定为背景或非对象的区域,最后标记我们不确定任何东西的区域,用0标记它。这是我们的标记。然后应用分水岭算法。然后我们的标记更新为我们给出的标签,并且对象的边界将具有-1的值.


3.代码

下面我们将看到一个关于如何使用距离变换和分水岭来分割相互接触对象的例子。

看下面的硬币图像,硬币互相接触。即使设定了阈值,也会互相接触。

(三)OpenCV中的图像处理之霍夫变换、分水岭算法以及GrabCut算法_第8张图片

我们首先找到硬币的估算值。为此,我们可以使用大津的二值化(Otsu’sbinarization),结果:

(三)OpenCV中的图像处理之霍夫变换、分水岭算法以及GrabCut算法_第9张图片

现在我们需要去除图像中的任何小的白色噪音。为此,我们可以使用形态学开放(morphological opening),要去除对象中的任何小孔,我们可以使用形态学关闭(morphological closing)。所以,现在我们知道对象中心附近的区域是前景区域,远离对象区域的是背景区域。只有我们不确定的区域是硬币的边界区域。

所以我们需要提取我们确信它们是硬币的区域。侵蚀会去除边界像素所以无论剩下什么,我们可以肯定它是硬币。如果物体不相互接触,那么将会起作用。但是由于它们彼此接触,另一个很好的选择是找到距离变换并应用适当的阈值。接下来我们需要找到我们确信它们不是硬币的区域。为此,我们扩大了结果 。扩张将对象的边界增加到背景。这样,我们就可以确定结果背景中的任何区域都是背景,因此边界区域被删除。

看下面的图片:

(三)OpenCV中的图像处理之霍夫变换、分水岭算法以及GrabCut算法_第10张图片

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

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

现在我们肯定知道哪些是前景和所有硬币的区域。所以我们创建标记(它是一个与原始图像大小相同的数组,但使用int32数据类型)并在其中标记区域。我们知道的区域(无论是前景还是背景)都标有任何正整数,但是不同的整数,我们不知道的区域只剩下零。为此,我们使用cv2.connectedComponents()。它用0标记图像的背景,然后其它对象用从1开始的整数标记。但是我们知道如果背景标记为0,分水岭会将其视为未知区域,所以我们想用不同的整数标记它。相反,我们将用标记未知区域为unknown。

(三)OpenCV中的图像处理之霍夫变换、分水岭算法以及GrabCut算法_第11张图片

现在我们的标记已准备好。现在是最后一步,应用分水岭算法的时候了,然后标记图像将被修改。边界区域将被标记为-1.

详细代码:

# -*- coding: utf-8 -*-
'''
图像分割和分水岭算法:
1.函数cv2.watershed()

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

img = cv2.imread('coin.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 先找到硬币的预估值,使用大津的二值化
ret, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)

# 现在需要去除图像中任何小的白色噪声,可以使用形态学open;去除对象中的任何小孔,可以使用形态学关闭close
# 从两张图片的bg减去fg可得到边界

# 去除噪声
kernel = np.ones((3, 3), np.uint8)
opening = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=2)
closing = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel, iterations=2)

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

# 找到确定的前景区域
dist_transform = cv2.distanceTransform(opening, cv2.DIST_L2, 5)
ret, sure_fg = cv2.threshold(dist_transform, 0.7 * dist_transform.max(), 255, 0)

# 找到不确定区域
sure_fg = np.uint8(sure_fg)
unknown = cv2.subtract(sure_bg, sure_fg)
# 这是opencv中的jet颜色映射,将灰色图像映射为彩色图像,原文档代码中没有写
unknownJET = cv2.applyColorMap(unknown, cv2.COLORMAP_JET, None)


# q区域标记
ret, markers = cv2.connectedComponents(sure_fg)

# 为所有标签添加一个,以确保背景不是0,而是1
markers = markers + 1

# 现在,用零标记未知区域
markers[unknown == 255] = 0
markers = cv2.watershed(img, markers)
img[markers == -1] = [255, 0, 0]
imgJET = cv2.applyColorMap(img, cv2.COLORMAP_OCEAN, None)


# 最后的结果
cv2.imshow('dist_transform', dist_transform)
cv2.imshow('sure_bg', sure_bg)
cv2.imshow('sure_fg', sure_fg)
cv2.imshow('unknown', unknown)
cv2.imshow('unknownJET', unknownJET)
cv2.imshow('imgJET', imgJET)
cv2.imshow('res', img)
cv2.waitKey(0) & 0xFF
cv2.destroyAllWindows()

结果:

(三)OpenCV中的图像处理之霍夫变换、分水岭算法以及GrabCut算法_第12张图片

一些硬币接触的区域正常分割,一些则没有。


3.15 使用GrabCut算法进行交互式前景提取

1.目标

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


2.理论

GrabCu算法由微软研究院剑桥的arstenRother, Vladimir Kolmogorov & Andrew Blake设计。在他们的论文中,“GrabCut”使用迭代图切割的交互式前景提取。需要一种算法来进行最小用户交互的前景提取。结果是GrabCut。

从用户的角度来看它是如何工作的?最初,用户在前景区域周围绘制矩形(前景区域的手部完全位于矩形内部)。然后算法将其迭代分段以获得最佳结果。但在某些情况下,分割效果不会很好,例如,它可能会将某些前景区域标记为背景,反之亦然。在这种情况下,用户需要做很好的修改。只需在图像上画一些有缺陷的结果。笔画基本上说:“嘿,这个区域应该是前景,你标记它的背景,在下一次迭代中纠正它”或其背景相反。然后在下一次迭代中,你会得到更好的结果。

后台的事情:

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

如下图所示:

(三)OpenCV中的图像处理之霍夫变换、分水岭算法以及GrabCut算法_第13张图片


3.Demo

现在我们开始使用OpenCV的抓取算法。OpenCV由这个函数cv2.grabCut().首先看它的参数:

  •  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,fbgModel:这些是算法在内部使用的数组。您只需创建两个大小为(1,65)的np.float64类型的零数组
  • IterCount:算法应该迭代的次数
  •  Mode:它应该是cv2.GC_INIT_WITH_RECT或cv2.GC_INIT_WITH_MASK或它们的组合,它决定了我们是绘制矩形还是最终的触感笔画

首先让我们看看矩形模式。我们加载图像,创建一个类似的蒙版图像。我们创建fgdModel和bgdModel。我们给矩形设置参数,一切都很简单。让算法迭代运算5次。由于我们使用的是矩形,所以模式应该是cv2.GC_INIT_WITH_RECT,然后运行抓取。它修改蒙版图像。在新的蒙版图像中,像素被标记为具有如上所述的指示背景/前景的四个标记。所以我们修改掩膜,使所有的0像素和2像素都被置为0(即背景),并且所有1像素和3像素被置于1(即前景像素)。现在我们的最终面具已准备就绪 只需将它与输入的图像相乘即可获得分割图像。

代码:

# -*- coding: utf-8 -*-
'''
使用GrabCur算法进行交互式前景提取
1.OpenCV中cv2.grabCut(),参数:img,mask,Rect,BgModel,fbgModel,IterCount,Mode
'''

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

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

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

rect = (50, 50, 600, 600)
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()
'''
在绘画应用程序中打开了输入图像,并在图像中添加了另一个图层。
在绘画中使用画笔工具,我用白色和不需要的背景(如徽标、地面等)在这个新图层上标记为缺失的前景(头发、鞋子、球等)。
然后用灰色填充剩余背景。然后在OpenCV中加载该遮罩图像,编辑我们在新添加的遮罩图像中获得的相应值的原始遮罩图像。 
#newmask是我手动标记的蒙版图像
newmask=cv2.imread('newmask.png',0)

#只要是被标记为白色的部分(确定前景),更改掩码=1
#只要被标记为黑色的部分(确定背景),更改掩码=0
mask[newmask==0]=0
mask[newmask==255]=1
mask,bgdModel,fgdModel=cv2.grabCut(img,mask,None,fgdModel,5,cv2.CG_INIT_WITH_MASK)
mask=np.where((mask==2)|(mask==0),0,1).astype('uint8')
img=img*mask[:,:,np.newaxis]
plt.imshow(img),plt.colorbar(),plt.show()
'''

结果:提取出的阿珂

(三)OpenCV中的图像处理之霍夫变换、分水岭算法以及GrabCut算法_第14张图片



你可能感兴趣的:(python,opencv,python,opencv,image,process)