Hough直线变换、圆变换原理解析与python实验

霍夫变换

在图像x-y坐标中,经过点(x_i, y_i)的直线表示为y_i=k∗ x_i+b (1),其中k为斜率,b为截距

如果将x_i 、y_i 看成常数,把k和b看成变量,式子(1)可以表示为b = - k∗ x_i + y_i(2)

这样就把点(x_i, y_i)在图像坐标空间x-y中的表示转变成为了参数空间a-b中的表示,这个过程被称为Hough变换

霍夫直线变换

原理解析

在这里插入图片描述

一个斜率和一个截距确定一条直线,但是斜率有可能不存在,因此可以转化为用 Θ \Theta Θ r r r 表示 ,将(x,y)做hough变换可以得到参数空间下的方程。

那么,( Θ \Theta Θ , r r r)就是参数空间的一个点,(x,y)则确定了过该点的直线。经过点( Θ \Theta Θ , r r r)的直线越多,证明确定这些直线的点 ( x i , y i ) (x_i, y_i) (xi,yi)共线的可能性越大,因此需要统计每个( Θ \Theta Θ , r r r)上有直线经过的次数。这个统计器就对应了复现代码中的accumulator。

得到统计器后,则需要确定一个阈值,统计器上对应位置的值若超过该阈值,则认为此参数( Θ i {\Theta}_i Θi , r i r_i ri)可以确定直线坐标空间的一条直线

根据确定的参数( Θ i {\Theta}_i Θi , r i r_i ri),在原图上做出直线即可。

实验

原图:

在这里插入图片描述

canny边缘检测:

在这里插入图片描述

不加极大值抑制:

在这里插入图片描述

加了非极大值抑制:

在这里插入图片描述

代码解读

python代码库实现

     # hough直线变换python库实现
    lines = cv2.HoughLines(imgEdge, 1, np.pi / 180, 160) # 1和np.pi / 180分别是投票矩阵的最小单位
    lines1 = lines[:, 0, :]  # 提取为二维
    for rho, theta in lines1[:]: # 取出lines每一行的两个数
        a = np.cos(theta)
        b = np.sin(theta)
        x0 = a * rho # (x0,y0)是参数方程r = x * cos(theta) + y * sin(theta)恒过的点)
        y0 = b * rho
        # 利用theta和rho、已知的一个点(x0,y0)来确定直线上的另外两个点,1000是随便取的值
        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), (255, 0, 0), 1) # 1是调节直线的宽度
	plt.imshow(img, cmap="gray")
    plt.axis("off")  # 关闭坐标
    plt.show()

注意 ( x 0 , y 0 ) (x_0 , y_0) (x0,y0)是利用极坐标求出来的点(高中知识)

在这里插入图片描述

知道 ( x 0 , y 0 ) (x_0 , y_0) (x0,y0) 后,就可以根据下图的原理,求出该直线任意的两个点 ( x 1 , y 1 (x_1,y_1 (x1,y1)、 ( x 2 , y 2 (x_2,y_2 (x2,y2):

在这里插入图片描述

代码复现:

def lineHough(imgEdge, thetaDim = None,threshold = None):
    """
    :param imgEdge: 输入的经过边缘检测后的图像
    :param ThetaDim: Theta轴的刻度数量(将(0,pi)划分成多少份),若为90则分为90份,每份为2
    :param threshold: 投票界定是否为直线的阈值
    :return: 符合阈值的投票箱结果
    """
    imgsize = imgEdge.shape
    if thetaDim == None:
        ThetaDim = 180
    MaxDist = int(np.ceil(np.sqrt(imgsize[0] ** 2 + imgsize[1] ** 2)))

    accumulator = np.zeros((ThetaDim, MaxDist))  # 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 imgEdge[i, j] == 0:
                for k in range(ThetaDim):
                    accumulator[k][int(round((i * cosTheta[k] + j * sinTheta[k])))] += 1

    M = accumulator.max()

    if threshold == None:
        threshold = int(M * 2.3875 / 10)
    result = np.array(np.where(accumulator > threshold))  # 阈值化
    result = result.astype(np.float64)
    result[0] = result[0] * np.pi / ThetaDim
    return result

def drawLine(line, img, color = (255, 0, 0), err = 6):
        """
    
    :param line: lineHough返回的结果
    :param img: 原图片
    :param color: 画线的颜色
    :param err: 误差大小,可调
    :return: 返回画线后的图片
    """
    Cos = np.cos(line[0])
    Sin = np.sin(line[0])

    # 遍历img中像素的所有位置,再将该位置坐标带入参数方程中(因为该俩参数是可以确定直线的,所以满足该方程则证明该点一定在某条直线上)
    # 这里.any()是需要两个矩阵中有一个数相等就行,是属于判断两个矩阵相等的技巧
    # 这里的误差是做hough变换的时候,做一些数字运算例如四舍五入,取整之类带来的,所以一定是有误差存在的
    for i in range(img.shape[0]):
        for j in range(img.shape[1]):
            e = np.abs(line[1] - i * Cos - j * Sin)
            if (e < err).any():
                img[i, j] = color
    return img

注意这里画图的方式与python库画图的方式不一样,这里是遍历每个点,判断每个点是否符合参数方程,而python库是随机挑选两个点,然后连接这两点画图

霍夫圆变换

原理解析

Hough梯度法

圆检测需要确定三个参数(圆心横坐标,圆心中坐标,半径长度),若在三维空间计算,时间很长。可以把霍夫变换分解为两个阶段,减少维度。

第一阶段检测圆心:若同一个圆周上的点,则这些点的梯度方向都应该交于圆心。因此可以对边缘点求梯度,获得梯度方向上的直线,对直线相交点数进行累加,累加数较多的位置点则认为是候选圆心。

第二阶段检测半径:求得圆心的位置后,圆周上的点到圆心的距离相等。因此可以对每个边缘点求一次到候选圆心的距离,同一个距离的点数进行累加累加数较多的距离则认为是候选半径。

确定圆心

作图是原图像,右图是原图像边缘点的梯度方向直线。亮处为相交次数最多的点,以此确定圆心。

在这里插入图片描述
在这里插入图片描述

实验

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

图一是原图像,图2是经过canny提取边缘后的图像,图3是边缘点梯度方向的直线,图4hough圆检测结果图

代码解读:

python代码库实现

gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
cv2.waitKey(0)
# 参数:gray:输入的灰度图像;求解方法;图像像素分辨率与参数空间分辨率的比值;检测到的圆的中心,(x,y)坐标之间的最小距离;param1:用于处理边缘检测的梯度值方法;
# param2:累加器阈值。阈值越小,检测到的圆越多;半径的最小大小(以像素为单位;半径的最大大小(以像素为单位)
circles = cv2.HoughCircles(gray, cv2.HOUGH_GRADIENT, 1, 100, param1 = 150,
                           param2=30, minRadius=350, maxRadius=600)
circle = circles[0, :, :] # 提取为二维
circle = np.uint16(np.around(circle))
for i in circle[:]:
    cv2.circle(img, (i[0], i[1]), i[2], (0,0,255),5)
    cv2.circle(img, (i[0], i[1]), 2, (0, 0, 255), 10) # 半径取两个像素点,相当于调整圆心的厚度,最后一个参数是thickness
    plt.imshow(img)
    plt.axis('off')
    plt.show()

缺点:函数中含有很多参数,最后圆检测的好坏跟参数相关性太大,往往每张图片需要调整的参数都相差较远

代码复现

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]])
    # 分别求得在x和y方向上的梯度
    edge_x = convolve(sobel_kernel_x, edge, [1,1,1,1], [1,1])
    edge_y = convolve(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且点梯度不为0的像素位置下标
    # ((1,1),(1,1)):前面的(1,1)是行填充,第一个1是edge_x的上面填,第二个1是在edge_x的下面填,后面的(1,1)同理
    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): # dx和dy都为0,肯定不是圆周上的点,可以排除
                    t1 = (k/center_axis_scale-i)
                    t2 = (l/center_axis_scale-j)
                    t3 = t1**2 + t2**2
                    # 这里为什么这样求? 这里求出来的t3是所有边缘点的梯度,是512*512,temp是[512,512]的bool矩阵,
                    # center_accumulator[temp] += 1 直接对整个矩阵进行加减了
                    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)
    plt.axis('off')
    cwd = os.getcwd()
    saveFile = Path(cwd,"image/hough")
    plt.savefig(str(Path(saveFile, "circleImg")))
    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中的每个list中是[像素矩阵x,像素矩阵y,投票数]
    sort_centers.sort(key=lambda x:x[2],reverse=True) # 按照投票数排序,从大到小

    centers = [[],[],[]]
    points = np.array(points)
    for i in range(len(sort_centers)): # 确定完圆心后,确定半径,这里的radius是一个一维向量,因为只需要确定r的大小即可,而center_accumulator需要确定两个值,所以为2维的
        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 是points中每个点到候选圆心的距离
        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是一个向量
        temp = (np.round(radius_scale * temp)).astype(np.int32)
        for j in range(temp.shape[0]):
            if temp2[j]:
                radius_accumulator[temp[j]] += 1 # 该距离上的点数+1
        for j in range(radius_accumulator.shape[0]):
            if j == 0 or j == 1:
                continue
            if not radius_accumulator[j] == 0: # np.log(j)是e^j,log是以e为底的对数,有点归一化的感觉
                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)
            # 这里应该是半径,但是为什么返回了radius_accumulator最大值的位置?
            # kang:因为radius_accumulator的长度是0-对角线,存储的是每个位置出现的次数,因此score_i就是对应的半径
            centers[2].append(score_i/radius_scale)


    centers = np.array(centers)
    centers = centers.astype(np.float64)

    return centers

解析:这里的求梯度用了sobel算子,求解过程比直接调库慢许多

参考资料:

https://zengdiqing.blog.csdn.net/article/details/86104053
https://www.bilibili.com/video/BV1bb411b7VQ

  • 3
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值