opencv图像处理之Hough变换+图像分割案例

1.什么是Hough直线变换

一条直线可以用数学表达式 y = m x + c y = mx + c (斜截式) 或者 ρ \rho = x c o s =x cos θ \theta  + y s i n + y sin θ \theta 法线式)  表示。 ρ \rho 是从原点到直线的垂直距离, θ \theta 是直线的垂线与横轴顺时针方向的夹角(OpenCV 坐标系)。如下图所示:
在这里插入图片描述
Hough变换检测形状的方法:

  • 如果一条线在原点下方经过, ρ \rho 的值就应该大于0,角度小于180。但是如果从原点上方经过的话,角度可能大于180,也可能小于180,但 ρ \rho 的值小于0。垂直的线 θ \theta 为0 度,水平线的 θ \theta 为90 度。
  • 图像上的每一条直线都可以用( ρ \rho , θ \theta ) 表示。可以创建一个2D 数组(accumulator),行表示 ρ \rho ,列表示 θ \theta ,初始所有的值都为0。这个数组的大小决定了最后结果的准确性。如果你希望角度精确到1 度,你就需要180 列。对于 ρ \rho ,最大值为图片对角线的距离。所以如果精确度要达到一个像素的级别,行数就应该与图像对角线的距离相等。
  • 如下图,有一个大小为100x100 的图像,有一条直线位于中央。取直线上的第一个点,我们知道此处的(x,y)值。把x 和y 带入上边的方程组,然后遍历 θ \theta 的取值:0,1,2,3,…180。分别求出与其对应的 ρ \rho 的值,这样我们就得到一系列( ρ \rho , θ \theta )的数值对,如果这个数值对在accumulator中也存在相应的位置,就在这个位置上加1。所以现在accumulator中的(50,90)=1。(一个点可能存在与多条直线中,所以对于直线上的每一个点可能是accumulator中的多个值同时加1)。
    在这里插入图片描述
accumulator 90° 180°
1
50 1
142
  • 现在取直线上的第二个点。重复上边的过程。更新accumulator中的值。现在accumulator中(50,90)的值为2。你每次做的就是更新accumulator中的值。对直线上的每个点都执行上边的操作,每次操作完成之后,accumulator中的值就加1,但其他地方有时会加1, 有时不会。按照这种方式下去,到最后accumulator中(50,90)的值肯定是最大的。如果你搜索accumulator中的最大值,并找到其位置(50,90),这就说明图像中有一条直线,这条直线到原点的距离为50,它的垂线与横轴的夹角为90 度。

2.opencv中的Hough直线变换

代码速记:

  • cv2.HoughLines()

参数解释:

lines = cv2.HoughLines(edges,1,np.pi/180,200)
  • 第一个参数是一个二值化图像,所以在进行霍夫变换之前要首先进行二值化,或者进行Canny 边缘检测。
  • 第二和第三个值分别代表 ρ \rho θ \theta  的精确度。
  • 第四个参数是阈值,只有累加其中的值高于阈值时才被认为是一条直线,也可以把它看成能检测到的直线的最短长度(以像素点为单位)。
  • 返回值就是( ρ \rho , θ \theta )的列表。 ρ \rho 的单位是像素, θ \theta  的单位是弧度

实战:

已知 ρ \rho = x c o s =x cos θ \theta  + y s i n + y sin θ \theta ,先求出 ρ \rho 和直线的垂点处的坐标 ( x 0 , y 0 ) (x_0,y_0) ( ρ c o s θ , ρ s i n θ ) (\rho cos\theta,\rho sin\theta) ,直线上取两个点的坐标: ( x 1 , y 1 ) (x_1,y_1) ( x 0 + 1000 ( s i n θ ) , y 0 + 1000 c o s θ ) (x_0+1000\cdot(- sin\theta),y_0+1000\cdot cos\theta) ,这个点是在x轴的负方向,y轴的正方向的极远处。 ( x 2 , y 2 ) (x_2,y_2) ( x 0 1000 ( s i n θ ) , y 0 1000 c o s θ ) (x_0-1000\cdot(- sin\theta),y_0-1000\cdot cos\theta) ,这个点是在x轴的正方向,y轴的负方向的极远处。
在这里插入图片描述

def Line(self):
    img_copy1=self.img.copy()
    img_copy2 = self.img.copy()
    #【1】通过边缘检测,得到二值化图像
    img_gray = cv2.cvtColor(self.img, cv2.COLOR_BGR2GRAY)    
    edges = cv2.Canny(img_gray, 50, 150, apertureSize=3)
    #【2】霍夫直线检测
    lines = cv2.HoughLines(edges, 1, np.pi / 180, 200)
    #【3】确定直线上的两点,画出其中一条直线
    for rho, theta in lines[0]:
        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_copy1, (x1, y1), (x2, y2), (0, 0, 255), 2)
    #画出图像上检测到的所有直线
    for line in lines:
        for rho, theta  in line:
            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_copy2, (x1, y1), (x2, y2), (0, 0, 255), 2)
    titles = ['raw', 'line_one','line_all']
    imgs = [self.img, img_copy1,img_copy2]
    for i in range(3):
        plt.subplot(1, 3, i + 1), plt.imshow(cv2.cvtColor(imgs[i],cv2.COLOR_BGR2RGB))
        plt.title(titles[i])
        plt.xticks([]), plt.yticks([])
    plt.show()

在这里插入图片描述

3.优化的Hough直线变换

从上边的过程我们可以发现:仅仅是一条直线都需要两个参数,这需要大量的计算。Probabilistic_Hough_Transform 是对霍夫变换的一种优化。它不会对每一个点都进行计算,而是从一幅图像中随机选取一个点集进行计算,对于直线检测来说这已经足够了。但是使用这种变换我们必须要降低阈值(总的点数都少了,阈值肯定也要小呀!)

代码速记:

  • cv2.HoughLinesP()

参数解释:

lines = cv2.HoughLinesP(edges, 1, np.pi / 180, 100, minLineLength, maxLineGap)
  • minLineLength:线的最短长度。比这个短的线都会被忽略。
  • maxLineGap:两条线段之间的最大间隔。如果小于此值,这两条直线就被看成是一条直线。
  • 很给力的是:返回值就是直线的起点和终点!

实战:

def LineP(self):
    img_copy1=self.img.copy()
    img_copy2 = self.img.copy()
    #【1】边缘检测得到二值化图像
    img_gray = cv2.cvtColor(self.img, cv2.COLOR_BGR2GRAY)
    edges = cv2.Canny(img_gray, 50, 150, apertureSize=3)
    #【2】优化霍夫直线变换
    minLineLength = 100 
    maxLineGap = 10  
    lines = cv2.HoughLinesP(edges, 1, np.pi / 180, 100, minLineLength, maxLineGap)
    #【3】画出一条直线、所有直线
    for x1, y1, x2, y2 in lines[0]:
        cv2.line(img_copy1, (x1, y1), (x2, y2), (0, 255, 0), 2)
    for line in lines:
        for x1, y1, x2, y2 in line:
            cv2.line(img_copy2, (x1, y1), (x2, y2), (0, 255, 0), 2)
    titles = ['raw', 'line_p_one','line_p_all']
    imgs = [self.img, img_copy1,img_copy2]
    for i in range(3):
        plt.subplot(1, 3, i + 1), plt.imshow(cv2.cvtColor(imgs[i],cv2.COLOR_BGR2RGB))
        plt.title(titles[i])
        plt.xticks([]), plt.yticks([])
    plt.show()

在这里插入图片描述

4.什么是Hough圆环变换

  • 圆形的数学表达式为 ( x x c e n t e r ) 2 + ( y y c e n t e r ) 2 = r 2 (x - x_{center})^2+(y-y_{center})^2 = r^2 ,其中 ( x c e n t e r , y c e n t e r (x_{center},y_{center}) 为圆心的坐标, r r 为圆的半径。从这个等式中我们可以看出:一个圆环需要3个参数来确定。所以进行圆环霍夫变换的累加器必须是3 维的,这样的话效率就会很低。所以OpenCV 用了一个比较巧妙的办法,霍夫梯度法,它可以使用边界的梯度信息。
  • 它的原理依据是圆心一定是在圆上的每个点的模向量上, 这些圆上点模向量的交点就是圆心, 霍夫梯度法的第一步就是找到这些圆心, 这样三维的累加平面就又转化为二维累加平面. 第二部根据所有候选中心的边缘非0像素对其的支持程度来确定半径。

代码速记:

  • cv2.HoughCircles()

参数解释:

circles = cv2.HoughCircles(img_gray, cv2.HOUGH_GRADIENT, 1, 20, param1=50, param2=30, minRadius=0, maxRadius=0)
  • img:输入的灰度图像(不要求是二值图像)
  • method:检测方法,通常为cv2.HOUGH_GRADIENT
  • dp:累加器分辨率与图像分辨率的反比。如dp=1,累加器具有与输入图像相同的分辨率,dp=2 ,累加器是输入图像1/2的分辨率.
  • minDist:检测到的圆的圆心之间的最小距离。如果参数太小,可能会导致多个错误的相邻的圆。如果太大,则会错误一些圆。
  • param1:第一个关于method的特定参数。如果是HOUGH_GRADIENT ,它是传递给Canny()边缘检测器的两个阈值中的较高阈值(较低的阈值会小两倍)。
  • param2:第二个关于method的特定参数. 如果是HOUGH_GRADIENT ,
    它是检测阶段圆心的累加器阈值。它越小,可能检测到的假圆圈越多,对应较大累加器值的圆圈将首先返回。
  • minRadius:圆圈的最小半径。
  • maxRadius :圆圈的最大半径。

实战:

def circle(self):
	#【1】变灰度图像、中值模糊
	#*****因为霍夫圆检测对噪声比较敏感,所以首先要对图像做中值滤波
    img_gray = cv2.cvtColor(self.img, cv2.COLOR_BGR2GRAY)
    img_gray = cv2.medianBlur(img_gray, 5)
	#【2】霍夫梯度圆形检测
    circles = cv2.HoughCircles(img_gray, cv2.HOUGH_GRADIENT, 1, 20,
                               param1=50, param2=30, minRadius=0, maxRadius=0)
    circles = np.uint16(np.around(circles))#四舍五入
    #【3】画图
    cimg = cv2.cvtColor(img_gray, cv2.COLOR_GRAY2BGR)#画图函数的img要求是三通道的,所以转BGR
    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)
    titles = ['raw', 'circle']
    imgs = [self.img, cimg]
    for i in range(2):
        plt.subplot(1, 2, i + 1), plt.imshow(cv2.cvtColor(imgs[i],cv2.COLOR_BGR2RGB))
        plt.title(titles[i])
        plt.xticks([]), plt.yticks([])
    plt.show()

在这里插入图片描述

5.分水岭算法图像分割

原理:

  • 任何一副灰度图像都可以被看成拓扑平面,灰度值高的区域可以被看成是山峰,灰度值低的区域可以被看成是山谷。我们向每一个山谷中灌不同颜色的水。随着水的位的升高,不同山谷的水就会相遇汇合,为了防止不同山谷的水汇合,我们需要在水汇合的地方构建起堤坝。不停的灌水,不停的构建堤坝知道所有的山峰都被水淹没。我们构建好的堤坝就是对图像的分割。这就是分水岭算法的背后哲理。
  • 但是这种方法通常都会得到过度分割的结果,这是由噪声或者图像中其他不规律的因素造成的。为了减少这种影响,OpenCV 采用了基于掩模的分水岭算法,在这种算法中我们要设置哪些山谷点会汇合,哪些不会。这是一种交互式的图像分割。我们要做的就是给我们已知的对象打上不同的标签。如果某个区域肯定是前景或对象,就使用某个颜色(或灰度值)标签标记它。如果某个区域肯定不是对象而是背景就使用另外一个颜色标签标记。而剩下的不能确定是前景还是背景的区域就用0 标记。这就是我们的标签。然后实施分水岭算法。每一次灌水,我们的标签就会被更新,当两个不同颜色的标签相遇时就构建堤坝,直到将所有山峰淹没,最后我们得到的边界对象(堤坝)的值为-1。
  • 原理演示

代码速记:

  • cv2.distanceTransform()
  • cv2.connectedComponents()
  • cv2.watershed()

参数解释:

dist_transform = cv2.distanceTransform(opening, 1, 5)#source,distanceType,maskSize
  • 距离变换的基本含义是计算图像中非零像素点到最近的零像素点的距离。
  • 根据各个像素点的距离值,设置为不同的灰度值。这样就完成了二值图像的距离变换。
  • 第二个参数0,1,2 分别表示CV_DIST_L1, CV_DIST_L2 , CV_DIST_C

实战:

def water_shed(self):
    copy=self.img.copy()
    gray = cv2.cvtColor(copy, cv2.COLOR_BGR2GRAY)
    # 【1】Otsu's二值化:找到硬币的近似估计
    ret, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
    # 【2】开运算:去除图像中的所有的白噪声
    kernel = np.ones((3, 3), np.uint8)
    opening = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=2)
    
    #已知靠近对象中心的区域肯定是前景,而远离对象中心的区域肯定是背景
    #不能确定的区域就是硬币之间的边界(前景与背景的交界)
    #当硬币之间没有接触时,可以用【腐蚀】去除边缘像素。但是现在硬币之间是接触的。所以要用距离变换再加上合适的阈值。
    
    # 【3】膨胀:将对象的边界延伸到背景中去。
    #这样由于边界区域被去处理,我们就可以知道哪些区域肯定是前景,哪些肯定是背景。
    sure_bg = cv2.dilate(opening, kernel, iterations=3)
    # 【4】确定前景区域:先进行距离变换,确定每个像素到零像素的最短距离。设置阈值筛选前景(距离越短越好)    
    dist_transform = cv2.distanceTransform(opening, 1, 5)#source,distanceType,maskSize    
    ret, sure_fg = cv2.threshold(dist_transform, 0.7 * dist_transform.max(), 255, 0)
    # 【5】从sure_bg中减去肯定sure_fg就得到了边界区域
    sure_fg = np.uint8(sure_fg)
    unknown = cv2.subtract(sure_bg, sure_fg)
    # 【6】创建标签,标记标签(一个与原图像大小相同,数据类型为in32 的数组)
    ret, markers1 = cv2.connectedComponents(sure_fg)
    #这个函数会把将背景标记为0,其他的对象使用从1 开始的正整数标记。
    #但在分水岭算法中unknown才应该是0。所有标签先加一。
    markers = markers1 + 1
    #现在把是unknown的像素标记为0
    markers[unknown == 255] = 0
    # 【7】实施分水岭算法
    markers3 = cv2.watershed(copy, markers)#source,markers
    #这个函数会修改标签图像,边界区域的标记将变为-1.
    copy[markers3 == -1] = [255, 0, 0]#在原图中把边界区域设置为红色
    #### 画图
    titles = ['raw', 'thresh','opening','sure_bg','dist_transform','sure_fg',
              'unknown','markers1','markers','markers3','result']
    imgs = [self.img,thresh, opening,sure_bg,dist_transform,sure_fg,
            unknown,markers1,markers,markers3,copy]
    for i in range(11):
        plt.subplot(2, 6, i + 1), plt.imshow(imgs[i],'gray')
        plt.title(titles[i])
        plt.xticks([]), plt.yticks([])
    plt.show()

在这里插入图片描述)

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

  • GrabCut 算法是由微软剑桥研究院提出的。此算法在提取前景的操作过程中需要很少的人机交互,结果非常好。
  • 开始时用户需要用一个矩形将前景区域框住(前景区域应该完全被包括在矩形框内部)。然后算法进行迭代式分割直达达到最好结果。但是有时分割的结果不够理想,比如把前景当成了背景,或者把背景当成了前景。在这种情况下,就需要用户来进行修改了。用户只需要在不理想的部位画一笔(点一下鼠标)就可以了。

代码速记:

  • cv2.grabCut()

参数解释:

cv2.grabCut(img,mask,rect,bgdModel,fgdModel,5,cv2.GC_INIT_WITH_RECT)
#原图、mask、包含前景的矩形、算法内部使用两个的数组、迭代次数、
#进行修改的方式(cv2.GC_INIT_WITH_RECT 或cv2.GC_INIT_WITH_MASK,也可以联合使用。)
  • mask:掩模图像,用来确定那些区域是背景,前景,可能是前景/背景等。可以设置为cv2.GC_BGD,cv2.GC_FGD,cv2.GC_PR_BGD,cv2.GC_PR_FGD,或者直接输入0,1,2,3 也行。
  • 运行grabcut。算法会修改掩模图像,在新的掩模图像中,所有的像素被分为四类:背景,前景,可能是背景/前景使用4 个不同的标签标记。
  • 然后我们来修改掩模图像,所有的0 像素和2 像素都被归为0(背景),所有的1 像素和3 像素都被归为1(前景)。我们最终的掩模图像就这样准备好了。用它和输入图像相乘就得到了分割好的图像。

实战:

def grab_cut(self):
    img = cv2.imread('../images/messi.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, 450, 290)
    # 函数的返回值是更新的mask, bgdModel, fgdModel
    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(cv2.cvtColor(img,cv2.COLOR_BGR2RGB)), plt.colorbar()
    plt.xticks([]), plt.yticks([])
    plt.show()

在这里插入图片描述
用ps重新制作一张mask图像。如果用cv2.GC_INIT_WITH_MASK参数,bgdModel和fgdModel为零会报错。如果用cv2.GC_INIT_WITH_RECT则没有什么反应。(此处留坑)

# newmask is the mask image I manually labelled
newmask = cv2.imread('newmask.png',0)
# whereever it is marked white (sure foreground), change mask=1
# whereever it is marked black (sure background), change mask=0
mask[newmask == 0] = 0
mask[newmask == 255] = 1
mask, bgdModel, fgdModel = cv2.grabCut(img,mask,None,bgdModel,fgdModel,5,cv2.GC_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()

在这里插入图片描述

发布了154 篇原创文章 · 获赞 45 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/qq_36622009/article/details/104649026