霍夫改换和分水岭图画切割

一、霍夫改换

1. 直线检测

关于一幅图画中的直线信息,人们的肉眼很容易就将其提取出来,可是关于核算机而言,这是难以完成的。因而咱们需求对图画进行一些操作后提取出直线信息。前面提到的边际检测或许阈值操作会因为噪声的存在而导致最后得到的成果存在必定的差错,比如说误判、错判和断线等。所以咱们要运用根据边际检测的成果运用霍夫改换来提取图画一切的直线信息。

  1. 原图和成果图如下:

OpenCV Extra 01 - 分水岭算法与霍夫变换

  1. 边际检测成果:

OpenCV Extra 01 - 分水岭算法与霍夫变换

  1. 霍夫改换直线标明:

OpenCV Extra 01 - 分水岭算法与霍夫变换

  1. 详细标明办法:y=(−cos⁡sin⁡)x+(rsin⁡),namely,y=kx+br=xcos⁡+ysin⁡\begin{array}{l} y=\left(-\frac{\cos \theta}{\sin \theta}\right) x+\left(\frac{r}{\sin \theta}\right) , namely, y = kx + b\\ r=x \cos \theta+y \sin \theta \end{array}

  2. 留意:

    1. \rho\theta 并不是极坐标标明办法,首要关于极坐标标明,\rho值会不断改换,而霍夫改换中的直线标明\rho则不会变化。
    2. \rho是原点到直线的最短间隔(既然最短则应为不变)
    3. 一个 \rho\theta就能确认一条直线
  3. 直线检测的基本流程

    1. 读入图画

    2. 转化为灰度图,便于后续的阈值处理或边际检测

    3. 霍夫改换检测直线

      1. 设定 \rho的取值范围,关于一副图画而言,图中直线最长只能到达对角线长度。分为有限个取值区间。
      2. \theta 的取值范围分为 若干个有限区间。
      3. 假设上面的 \rho\theta的区间个数分别为 8, 4。如下所示,终究会构成一个 8 * 4 的阵列。
      4. 遍历原图,遇到非边际点则跳过,不然进入核算:将该点坐标 (xi,yi)(x_i,y_i)代入r=xcos+ysinr = xcos\theta + y sin\theta,并将\theta区间的一切值顺次代入核算,会得到90个 r值,将其分别填入到 8 个区间之内。直到原图都被遍历结束(想象为投票进程)。
      5. 得票数越多,则对应的 \rho\theta就越有或许确认一条直线。 如下图所示:

OpenCV Extra 01 - 分水岭算法与霍夫变换

  1. 关于cv.HoughLines得到的直线集,咱们要从其每个元素的第一个元素的也便是 line in lines, line[0]中提取\rho\theta信息。进而得到 直线上间隔远点最近的点的坐标x0=cos,y0=sin x_0 = \rho cos\theta , y_0 = \rho sin\theta,因为cv.line需求运用两个点才干绘制出一条直线,那么咱们要依据 (x0,y0)(x_0,y_0)和已知的\theta来找出两个点确认此条直线。利用几许知识,咱们能够选取一个足够大的沿直线的偏移量 bias, 使x1=x0+bias∗sin,x2=x0−bias∗sinx_1 = x_0 + bias * sin \theta, x_2 = x_0 – bias * sin \theta 以及 y1=y0+bias∗cos,y2=y0−bias∗cosy_1 = y_0 + bias * cos \theta, y_2 = y_0 – bias * cos \theta 。由此能够确认终究成果。,要留意霍夫改换的坐标轴标明办法:

    OpenCV Extra 01 - 分水岭算法与霍夫变换

  2. 完成代码:

    #coding:utf-8
    import numpy as np
    import matplotlib.pyplot as plt
    import os
    import cv2
    def lines_detector_hough(edge,ThetaDim = None,DistStep = None,threshold = None,halfThetaWindowSize = 2,halfDistWindowSize = None):
        '''
        :param edge: 经过边际检测得到的二值图
        :param ThetaDim: hough空间中theta轴的刻度数量(将[0,pi)均分为多少份),反应theta轴的粒度,越大粒度越细
        :param DistStep: hough空间中dist轴的区分粒度,即dist轴的最小单位长度
        :param threshold: 投票表决确认存在直线的开始阈值
        :return: 返回检测出的一切直线的参数(theta,dist)
        @author: bilibili-会飞的吴克
        '''
        imgsize = edge.shape
        if ThetaDim == None:
            ThetaDim = 90
        if DistStep == None:
            DistStep = 1
        MaxDist = np.sqrt(imgsize[0]**2 + imgsize[1]**2)
        DistDim = int(np.ceil(MaxDist/DistStep))
        if halfDistWindowSize == None:
            halfDistWindowSize = int(DistDim/50)
        accumulator = np.zeros((ThetaDim,DistDim)) # theta的范围是[0,pi). 在这里将[0,pi)进行了线性映射.类似的,也对Dist轴进行了线性映射
        sinTheta = [np.sin(t*np.pi/ThetaDim) for t in range(ThetaDim)]
        cosTheta = [np.cos(t*np.pi/ThetaDim) for t in range(ThetaDim)]
        for i in range(imgsize[0]):
            for j in range(imgsize[1]):
                if not edge[i,j] == 0:
                    for k in range(ThetaDim):
                        accumulator[k][int(round((i*cosTheta[k]+j*sinTheta[k])*DistDim/MaxDist))] += 1
        M = accumulator.max()
        if threshold == None:
            threshold = int(M*2.3875/10)
        result = np.array(np.where(accumulator > threshold)) # 阈值化
        temp = [[],[]]
        for i in range(result.shape[1]):
            eight_neiborhood = accumulator[max(0, result[0,i] - halfThetaWindowSize + 1):min(result[0,i] + halfThetaWindowSize, accumulator.shape[0]), max(0, result[1,i] - halfDistWindowSize + 1):min(result[1,i] + halfDistWindowSize, accumulator.shape[1])]
            if (accumulator[result[0,i],result[1,i]] >= eight_neiborhood).all():
                temp[0].append(result[0,i])
                temp[1].append(result[1,i])
        result = np.array(temp)    # 非极大值按捺
        result = result.astype(np.float64)
        result[0] = result[0]*np.pi/ThetaDim
        result[1] = result[1]*MaxDist/DistDim
        return result
    def drawLines(lines,edge,color = (255,0,0),err = 3):
        if len(edge.shape) == 2:
            result = np.dstack((edge,edge,edge))
        else:
            result = edge
        Cos = np.cos(lines[0])
        Sin = np.sin(lines[0])
        for i in range(edge.shape[0]):
            for j in range(edge.shape[1]):
                e = np.abs(lines[1] - i*Cos - j*Sin)
                if (e < err).any():
                    result[i,j] = color
        return result
    if __name__=='__main__':
        pic_path = './HoughImg/'
        pics = os.listdir(pic_path)
        for i in pics:
            if i[-5:] == '.jpeg' or i[-4:] == '.jpg':
                img = plt.imread(pic_path+i)
                blurred = cv2.GaussianBlur(img, (3, 3), 0)
                plt.imshow(blurred,cmap='gray')
                plt.axis('off')
                plt.show()
                if not len(blurred.shape) == 2:
                    gray = cv2.cvtColor(blurred, cv2.COLOR_RGB2GRAY)
                else:
                    gray = blurred
                edge = cv2.Canny(gray, 50, 150)   #  二值图 (0 或 255) 得到 canny边际检测的成果
                lines = lines_detector_hough(edge)
                final_img = drawLines(lines,blurred)
                plt.imshow(final_img,cmap='gray')
                plt.axis('off')
                plt.show()
    

2. 圆形检测

  1. 霍夫改换圆标明办法

OpenCV Extra 01 - 分水岭算法与霍夫变换
与直线相比(只需求两个参数\rho\theta),霍夫改换标明圆需求用到三个参数:圆心:(xcenter,ycenter)(x_{center}, y_{center}),半径:radiusradius。那么咱们的投票阵列便是 3 维矩阵,每一个(xi,yi,zi)(x_i,y_i,z_i)处记载了圆心为(xi,yi)(x_i,y_i),半径为ziz_i的圆的投票数。同属一个圆的边际点都会为此圆进行投票。,如下所示:

OpenCV Extra 01 - 分水岭算法与霍夫变换

  1. 留意:若是依照之前检测直线的办法来对圆形进行检测,那么咱们最后的时刻复杂度将会为O(n4)O(n^4)左右,一旦图画像素点比较多,那么所需时刻是十分大的,所以咱们就采用霍夫梯度法来对圆进行检测。

  2. 霍夫梯度法的基本思想

    一个正圆周上面的一切点的梯度方向会交于一点,这个点便是圆心。确认圆心之后咱们能够运用圆心取得半径的大小。而关于圆周边际点的梯度方向,咱们能够运用Sobel算子进行确认。

  3. 圆形检测的基本流程

    1. 确认圆心:遍历图画,遇到边际点,则运用Sobel算子核算出对 x 和 y 的偏导数,然后得出该点的梯度方向,然后能够利用这个方向和该定点(x,y)确认的直线将直线所在的一切像素点都投一票。终究票数最多的点必定是圆心。
    2. 确认半径:利用挑选出来的圆心,一切或许的半径再次构成投票阵列。再次遍历一切边际点,核算其到圆心的间隔,然后依据间隔为对应的半径长度进行投票。如下所示:
      OpenCV Extra 01 - 分水岭算法与霍夫变换
  4. 完成代码:

    #coding:utf-8
    import numpy as np
    import matplotlib.pyplot as plt
    import os
    import cv2
    def convlove(filter,mat,padding,strides):
        '''
        :param filter:卷积核,有必要为二维(2 x 1也算二维) 不然返回None
        :param mat:图片
        :param padding:对齐
        :param strides:移动步长
        :return:返回卷积后的图片。(灰度图,彩图都适用)
        @author:bilibili-会飞的吴克
        '''
        result = None
        filter_size = filter.shape
        mat_size = mat.shape
        if len(filter_size) == 2:
            if len(mat_size) == 3:
                channel = []
                for i in range(mat_size[-1]):
                    pad_mat = np.pad(mat[:,:,i], ((padding[0], padding[1]), (padding[2], padding[3])), 'constant')
                    temp = []
                    for j in range(0,mat_size[0],strides[1]):
                        temp.append([])
                        for k in range(0,mat_size[1],strides[0]):
                            val = (filter*pad_mat[j*strides[1]:j*strides[1]+filter_size[0],
                                          k*strides[0]:k*strides[0]+filter_size[1]]).sum()
                            temp[-1].append(val)
                    channel.append(np.array(temp))
                channel = tuple(channel)
                result = np.dstack(channel)
            elif len(mat_size) == 2:
                channel = []
                pad_mat = np.pad(mat, ((padding[0], padding[1]), (padding[2], padding[3])), 'constant')
                for j in range(0, mat_size[0], strides[1]):
                    channel.append([])
                    for k in range(0, mat_size[1], strides[0]):
                        val = (filter * pad_mat[j * strides[1]:j * strides[1] + filter_size[0],
                                        k * strides[0]:k * strides[0] + filter_size[1]]).sum()
                        channel[-1].append(val)
                result = np.array(channel)
        return result
    def AHTforCircles(edge,center_threhold_factor = None,score_threhold = None,min_center_dist = None,minRad = None,maxRad = None,center_axis_scale = None,radius_scale = None,halfWindow = None,max_circle_num = None):
        if center_threhold_factor == None:
            center_threhold_factor = 10.0
        if score_threhold == None:
            score_threhold = 15.0
        if min_center_dist == None:
            min_center_dist = 80.0
        if minRad == None:
            minRad = 0.0
        if maxRad == None:
            maxRad = 1e7*1.0
        if center_axis_scale == None:
            center_axis_scale = 1.0
        if radius_scale == None:
            radius_scale = 1.0
        if halfWindow == None:
            halfWindow = 2
        if max_circle_num == None:
            max_circle_num = 6
        min_center_dist_square = min_center_dist**2
        sobel_kernel_y = np.array([[-1.0, -2.0, -1.0], [0.0, 0.0, 0.0], [1.0, 2.0, 1.0]])
        sobel_kernel_x = np.array([[-1.0, 0.0, 1.0], [-2.0, 0.0, 2.0], [-1.0, 0.0, 1.0]])
        edge_x = convlove(sobel_kernel_x,edge,[1,1,1,1],[1,1])
        edge_y = convlove(sobel_kernel_y,edge,[1,1,1,1],[1,1])
        center_accumulator = np.zeros((int(np.ceil(center_axis_scale*edge.shape[0])),int(np.ceil(center_axis_scale*edge.shape[1]))))
        k = np.array([[r for c in range(center_accumulator.shape[1])] for r in range(center_accumulator.shape[0])])
        l = np.array([[c for c in range(center_accumulator.shape[1])] for r in range(center_accumulator.shape[0])])
        minRad_square = minRad**2
        maxRad_square = maxRad**2
        points = [[],[]]
        edge_x_pad = np.pad(edge_x,((1,1),(1,1)),'constant')
        edge_y_pad = np.pad(edge_y,((1,1),(1,1)),'constant')
        Gaussian_filter_3 = 1.0 / 16 * np.array([(1.0, 2.0, 1.0), (2.0, 4.0, 2.0), (1.0, 2.0, 1.0)])
        for i in range(edge.shape[0]):
            for j in range(edge.shape[1]):
                if not edge[i,j] == 0:
                    dx_neibor = edge_x_pad[i:i+3,j:j+3]
                    dy_neibor = edge_y_pad[i:i+3,j:j+3]
                    dx = (dx_neibor*Gaussian_filter_3).sum()
                    dy = (dy_neibor*Gaussian_filter_3).sum()
                    if not (dx == 0 and dy == 0):
                        t1 = (k/center_axis_scale-i)
                        t2 = (l/center_axis_scale-j)
                        t3 = t1**2 + t2**2
                        temp = (t3 > minRad_square)&(t3 < maxRad_square)&(np.abs(dx*t1-dy*t2) < 1e-4)
                        center_accumulator[temp] += 1
                        points[0].append(i)
                        points[1].append(j)
        M = center_accumulator.mean()
        for i in range(center_accumulator.shape[0]):
            for j in range(center_accumulator.shape[1]):
                neibor = \
                    center_accumulator[max(0, i - halfWindow + 1):min(i + halfWindow, center_accumulator.shape[0]),
                    max(0, j - halfWindow + 1):min(j + halfWindow, center_accumulator.shape[1])]
                if not (center_accumulator[i,j] >= neibor).all():
                    center_accumulator[i,j] = 0
                                                                            # 非极大值按捺
        plt.imshow(center_accumulator,cmap='gray')
        plt.axis('off')
        plt.show()
        center_threshold = M * center_threhold_factor
        possible_centers = np.array(np.where(center_accumulator > center_threshold))  # 阈值化
        sort_centers = []
        for i in range(possible_centers.shape[1]):
            sort_centers.append([])
            sort_centers[-1].append(possible_centers[0,i])
            sort_centers[-1].append(possible_centers[1, i])
            sort_centers[-1].append(center_accumulator[sort_centers[-1][0],sort_centers[-1][1]])
        sort_centers.sort(key=lambda x:x[2],reverse=True)
        centers = [[],[],[]]
        points = np.array(points)
        for i in range(len(sort_centers)):
            radius_accumulator = np.zeros(
                (int(np.ceil(radius_scale * min(maxRad, np.sqrt(edge.shape[0] ** 2 + edge.shape[1] ** 2)) + 1))),dtype=np.float32)
            if not len(centers[0]) < max_circle_num:
                break
            iscenter = True
            for j in range(len(centers[0])):
                d1 = sort_centers[i][0]/center_axis_scale - centers[0][j]
                d2 = sort_centers[i][1]/center_axis_scale - centers[1][j]
                if d1**2 + d2**2 < min_center_dist_square:
                    iscenter = False
                    break
            if not iscenter:
                continue
            temp = np.sqrt((points[0,:] - sort_centers[i][0] / center_axis_scale) ** 2 + (points[1,:] - sort_centers[i][1] / center_axis_scale) ** 2)
            temp2 = (temp > minRad) & (temp < maxRad)
            temp = (np.round(radius_scale * temp)).astype(np.int32)
            for j in range(temp.shape[0]):
                if temp2[j]:
                    radius_accumulator[temp[j]] += 1
            for j in range(radius_accumulator.shape[0]):
                if j == 0 or j == 1:
                    continue
                if not radius_accumulator[j] == 0:
                    radius_accumulator[j] = radius_accumulator[j]*radius_scale/np.log(j) #radius_accumulator[j]*radius_scale/j
            score_i = radius_accumulator.argmax(axis=-1)
            if radius_accumulator[score_i] < score_threhold:
                iscenter = False
            if iscenter:
                centers[0].append(sort_centers[i][0]/center_axis_scale)
                centers[1].append(sort_centers[i][1]/center_axis_scale)
                centers[2].append(score_i/radius_scale)
        centers = np.array(centers)
        centers = centers.astype(np.float64)
        return centers
    def drawCircles(circles,edge,color = (0,0,255),err = 600):
        if len(edge.shape) == 2:
            result = np.dstack((edge,edge,edge))
        else:
            result = edge
        for i in range(edge.shape[0]):
            for j in range(edge.shape[1]):
                dist_square = (circles[0]-i)**2 + (circles[1]-j)**2
                e = np.abs(circles[2]**2 - dist_square)
                if (e < err).any():
                    result[i,j] = color
                if (dist_square < 25.0).any():
                    result[i,j] = (255,0,0)
        return result
    if __name__=='__main__':
        pic_path = './HoughCircleImg/'
        pics = os.listdir(pic_path)
        params = {
            '1.jpeg':{
            'center_threhold_factor': 3.33,
            'score_threhold':15.0,
             'min_center_dist':80.0,
             'minRad':0.0,
             'maxRad':1e7*1.0,
             'center_axis_scale':1.0,
             'radius_scale':1.0,
             'halfWindow':2,
            'max_circle_num':1
             },
            '4.jpg':{
            'center_threhold_factor': 2.0,
            'score_threhold': 15.0,
             'min_center_dist': 80.0,
             'minRad': 0.0,
             'maxRad': 1e7 * 1.0,
             'center_axis_scale': 1.0,
             'radius_scale': 1.0,
             'halfWindow': 2,
             'max_circle_num': 6
             },
            '2.jpeg':{
            'center_threhold_factor': 3.33,
            'score_threhold': 50.0,
             'min_center_dist': 80.0,
             'minRad': 0.0,
             'maxRad': 1e7 * 1.0,
             'center_axis_scale': 0.9,
             'radius_scale': 1.0,
             'halfWindow': 2,
             'max_circle_num': 1
             },
            '3.jpeg':{
            'center_threhold_factor': 1.5,
            'score_threhold': 56.0,
             'min_center_dist': 80.0,
             'minRad': 0.0,
             'maxRad': 1e7 * 1.0,
             'center_axis_scale': 0.8,
             'radius_scale': 1.0,
             'halfWindow': 2,
             'max_circle_num': 1
             },
            '0.jpg':{
            'center_threhold_factor': 1.5,
            'score_threhold': 30.0,
             'min_center_dist': 80.0,
             'minRad': 0.0,
             'maxRad': 1e7 * 1.0,
             'center_axis_scale': 1.0,
             'radius_scale': 1.0,
             'halfWindow': 2,
             'max_circle_num': 1
             }
        }
        for i in pics:
            if i[-5:] == '.jpeg' or i[-4:] == '.jpg':
                img = plt.imread(pic_path+i)
                blurred = cv2.GaussianBlur(img, (3, 3), 0)
                plt.imshow(blurred)
                plt.axis('off')
                plt.show()
                if not len(blurred.shape) == 2:
                    gray = cv2.cvtColor(blurred, cv2.COLOR_RGB2GRAY)
                else:
                    gray = blurred
                edge = cv2.Canny(gray, 50, 150)   #  二值图 (0 或 255) 得到 canny边际检测的成果
                circles = AHTforCircles(edge,
                                        center_threhold_factor=params[i]['center_threhold_factor'],score_threhold=params[i]['score_threhold'],min_center_dist=params[i]['min_center_dist'],minRad=params[i]['minRad'],
                                        maxRad=params[i]['maxRad'],center_axis_scale=params[i]['center_axis_scale'],radius_scale=params[i]['radius_scale'],
                                        halfWindow=params[i]['halfWindow'],max_circle_num=params[i]['max_circle_num'])
                final_img = drawCircles(circles,blurred)
                plt.imshow(final_img)
                plt.axis('off')
                plt.show()
    
  5. 成果:

OpenCV Extra 01 - 分水岭算法与霍夫变换

能够看出差错仍是很大。

二、分水岭算法

1. 概念

  1. 处理目标:分水岭算法针对的是灰度图。

  2. 下雨思想:将灰度图视为一张三维的地形图,俯视图自然便是灰度图自身,高度的话便是灰度值。 灰度值较低的区域会构成山沟,灰度值较高的区域会构成山巅。给每一个山沟区域的“积水”涂上不同的色彩,然后咱们不断的仿照“下雨”(水不会溢出到图画外),那么这些山沟中的水会逐步聚集,聚集的一起,这些色彩会变黑,也便是边际会变黑构成屏障,一起两边的水就不会互相交融。终究,直到水位超过最高的山巅,得到的图画就会有各种色彩以及黑色鸿沟。,如图:

  3. 洪泛思想:将灰度图的山沟下面打穿留一个洞,然后将这些地形图放入无穷深的大海中,那么海水就会从这些洞中涌出,接下来就和下雨一致了,如下图所示:

2. 算法流程

  1. 找出一切的极小值区域
  2. 被水吞没

三、分水岭算法详细阐明(转自知乎@程序员阿德)

分水岭(Watershed)是根据地理形状的分析的图画切割算法,仿照地理结构(比如山川、沟壑,盆地)来完成对不同物体的分类。

分水岭算法中会用到一个重要的概念——测地线间隔。

1. 测地线间隔(Geodesic Distance)

测地线间隔便是地球外表两点之间的最短途径(可执行途径)的间隔,在图论中,Geodesic Distance 便是图中两节点的最短途径的间隔,这与平常在几许空间通常用到的 Euclidean Distance(欧氏间隔),即两点之间的最短间隔有所区别。前者是沿着圆走,后者两点之间直线最短。

在下图中,两个黑点的 Euclidean Distance 是用虚线所标明的线段的长度 d15d_{15} ,而 Geodesic Distance 作为实际途径的最短间隔,其间隔应为沿途实线段间隔之和的最小值,即 d12+d23+d34+d45d_{12}+d_{23}+d_{34}+d_{45}

OpenCV Extra 01 - 分水岭算法与霍夫变换

在三维曲面空间中两点间的测地间隔便是两点间沿着三维曲面的外表走的最短途径。

2. 分水岭算法

图画的灰度空间很像地球外表的整个地理结构,每个像素的灰度值代表高度。其间的灰度值较大的像素连成的线能够看做山脊,也便是分水岭。其间的水便是用于二值化的gray threshold level,二值化阈值能够理解为水平面,比水平面低的区域会被吞没,刚开始用水填充每个孤立的山沟(部分最小值)。

当水平面上升到必定高度时,水就会溢出当前山沟,能够经过在分水岭上修大坝,从而避免两个山沟的水聚集,这样图画就被分成2个像素集,一个是被水吞没的山沟像素集,一个是分水岭线像素集。终究这些大坝构成的线就对整个图画进行了分区,完成对图画的切割。

OpenCV Extra 01 - 分水岭算法与霍夫变换

在该算法中,空间上相邻并且灰度值附近的像素被区分为一个区域。

3. 分水岭算法的整个进程

  1. 把梯度图画中的一切像素依照灰度值进行分类,并设定一个测地间隔阈值。
  2. 找到灰度值最小的像素点(默认符号为灰度值最低点),让threshold从最小值开始增加,这些点为开始点。
  3. 水平面在增加的进程中,会碰到周围的邻域像素,测量这些像素到开始点(灰度值最低点)的测地间隔(沿着山坡走),假如小于设定阈值,则将这些像素吞没,不然在这些像素上设置大坝,这样就对这些邻域像素进行了分类。

OpenCV Extra 01 - 分水岭算法与霍夫变换

  1. 跟着水平面越来越高,会设置更多更高的大坝,直到灰度值的最大值,一切区域都在分水岭线上相遇,这些大坝就对整个图画像素的进行了分区。

整个进程能够查看下面这个动图:

OpenCV Extra 01 - 分水岭算法与霍夫变换

用上面的算法对图画进行分水岭运算,因为噪声点或其它因素的搅扰,或许会得到密密麻麻的小区域,即图画被分得太细(over-segmented,过度切割),这因为图画中有十分多的部分极小值点,每个点都会自成一个小区域。

其间的解决办法:

  1. 对图画进行高斯平滑操作,抹除许多小的最小值,这些小分区就会合并。
  2. 不从最小值开始增加,能够将相对较高的灰度值像素作为开始点(需求用户手动符号),从符号处开始进行吞没,则许多小区域都会被合并为一个区域,这被称为根据图画符号(mark)的分水岭算法

下面三个图分别是原图,分水岭过切割的图以及根据符号的分水岭算法得到的图:

OpenCV Extra 01 - 分水岭算法与霍夫变换

其间符号的每个点就相当于分水岭中的灌水点,从这些点开始灌水使得水平面上升,可是如上图所示,图画中需求切割的区域太多了,手动符号太麻烦,咱们可运用间隔转化的办法进行符号,OpenCV中便是运用的这种办法。

四、 OpenCV中分水岭算法

在OpenCV中,咱们需求给不同区域贴上不同的标签。用大于1的整数标明咱们确认为远景或目标的区域,用1标明咱们确认为布景或非目标的区域,最后用0标明咱们无法确认的区域。然后运用分水岭算法,咱们的符号图画将被更新,更新后的符号图画的鸿沟像素值为-1。

下面临互相触摸的硬币运用间隔改换和分水岭切割。

OpenCV Extra 01 - 分水岭算法与霍夫变换

先运用 Otsu’s 二值化对图画进行二值化。

import cv2
import numpy as np
img = cv2.imread('coins.png')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU)

OpenCV Extra 01 - 分水岭算法与霍夫变换

先运用开运算去除图画中的细小白色噪点(此处采用中值滤波也能够),然后经过腐蚀运算移除鸿沟像素,得到的图画中的白色区域肯定是实在远景,即靠近硬币中心的区域(下面左面的图);胀大运算使得一部分布景成为了物体的鸿沟,得到的图画中的黑色区域肯定是实在布景,即远离硬币的区域(下面中间的图)。

剩余的区域(硬币的鸿沟附近)还不能确认是远景仍是布景。可经过胀大图减去腐蚀图得到,下图中的白色部分为不确认区域(下面右边的图)。

# noise removal
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3,3))
opening = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=2)
sure_bg = cv2.dilate(opening, kernel, iterations=2)  # sure background area
sure_fg = cv2.erode(opening, kernel, iterations=2)  # sure foreground area
unknown = cv2.subtract(sure_bg, sure_fg)  # unknown area

OpenCV Extra 01 - 分水岭算法与霍夫变换

剩余的区域不确认是硬币仍是布景,这些区域通常在远景和布景触摸的区域(或许两个不同硬币触摸的区域),咱们称之为鸿沟。经过分水岭算法应该能找到确认的鸿沟。

  • 留意:为什么硬币之间互相触摸就不能直接运用分水岭算法呢?

    首要硬币之间互相触摸会造成不确认区域的非联通,假如咱们直接运用分水岭算法则会呈现漏选状况,也便是分水岭算法只能根据未确认的区域进行操作,若是未确认区域没有囊括或许的一切边际,那么分水岭算法也不会得出对应的边际。

因为硬币之间互相触摸,咱们运用另一个确认远景的办法,便是带阈值的间隔改换

下面左面的图为得到的间隔转化图画,其间每个像素的值为其到最近的布景像素(灰度值为0)的间隔,能够看到硬币的中心像素值最大最亮(中心离布景像素最远)。对其进行二值处理就得到了别离的远景图(下面中间的图),白色区域肯定是硬币区域,而且还互相别离,下面右边的图为之前的胀大图减去中间这个标明远景的图。

# Perform the distance transform algorithm
# cv2.DIST_L2标明运用欧式间隔核算
dist_transform = cv2.distanceTransform(opening, cv2.DIST_L2, 5)
# Normalize the distance image for range = {0.0, 1.0}
cv2.normalize(dist_transform, dist_transform, 0, 1.0, cv2.NORM_MINMAX)
# Finding sure foreground area
ret, sure_fg = cv2.threshold(dist_transform, 0.5*dist_transform.max(), 255, 0)
# Finding unknown region
sure_fg = np.uint8(sure_fg)
unknown = cv2.subtract(sure_bg,sure_fg)

OpenCV Extra 01 - 分水岭算法与霍夫变换

现在咱们能够确认哪些是硬币区域,哪些是布景区域。然后创立符号(marker,它是一个与原始图画大小相同的矩阵,int32数据类型),标明其间的每个区域。分水岭算法将符号的0的区域视为不确认区域,将符号为1的区域视为布景区域,将符号大于1的正整数标明咱们想得到的远景。

咱们能够运用 cv2.connectedComponents() 来完成这个功用,它是用0符号图画的布景,用大于0的整数符号其他目标。所以咱们需求对其进行加一,用1来符号图画的布景。

cv2.connectedComponents() 将传入图画中的白色区域视为组件(远景)。

# Marker labelling
ret, markers = cv2.connectedComponents(sure_fg)
# Add one to all labels so that sure background is not 0, but 1
markers = markers+1
# Now, mark the region of unknown with zero
markers[unknown==255] = 0

留意:得到的markers矩阵的元素类型为 int32,要运用 imshow() 进行显现,需求将其转化为 uint8 类型( markers=np.uint8(markers) )。

咱们对得到的markers进行显现:

markers_copy = markers.copy()
markers_copy[markers==0] = 150  # 灰色标明布景
markers_copy[markers==1] = 0    # 黑色标明布景
markers_copy[markers>1] = 255   # 白色标明远景
markers_copy = np.uint8(markers_copy)

OpenCV Extra 01 - 分水岭算法与霍夫变换

符号图画现已完成了,最后运用分水岭算法。然后符号图画将被修改,鸿沟区域将被符号为-1。

# 运用分水岭算法执行根据符号的图画切割,将图画中的目标与布景别离
markers = cv2.watershed(img, markers)
img[markers==-1] = [0,0,255]  # 将鸿沟符号为红色

经过分水岭算法得到的新的符号图画和切割后的图画如下图所示:

OpenCV Extra 01 - 分水岭算法与霍夫变换

任何两个相邻衔接的组件不必定被分水岭鸿沟(-1的像素)分隔;例如在传递给 watershed 函数的初始符号图画中的物体互相触摸。

1. 总结

咱们经过一个例子介绍了分水岭算法的整个进程,主要分为以下几步:

  1. 对图进行灰度化和二值化得到二值图画
  2. 经过胀大得到确认的布景区域,经过间隔转化得到确认的远景区域,剩余部分为不确认区域
  3. 对确认的远景图画进行衔接组件处理,得到符号图画
  4. 依据符号图画对原图画运用分水岭算法,更新符号图画