Analysis of Hough's line transformation and circle transformation principle and python experiment

Posted by xenooreo on Sat, 23 Oct 2021 02:24:08 +0200

Hough transform

In the x-y coordinates of the image, the straight line passing through the point (x_i, y_i) is expressed as y_i=k∗ x_i+b (1), where k is the slope and b is the intercept

If x_i ,y_i is regarded as a constant and K and b are regarded as variables. Formula (1) can be expressed as b = - k * x_i + y_i(2)

In this way, the representation of points (x_i, y_i) in the image coordinate space x-y is transformed into the representation in the parameter space a-b. this process is called Hough transform

Hough line transformation

Principle analysis

A slope and an intercept determine a straight line, but the slope may not exist, so it can be converted into Θ \Theta Θ and r r r means that the equation in parameter space can be obtained by hough transform of (x,y).

So( Θ \Theta Θ , r r r) Is a point in the parameter space, and (x,y) determines the straight line passing through the point. Passing point( Θ \Theta Θ , r r r) The more straight lines, the more points to determine these lines ( x i , y i ) (x_i, y_i) (xi, yi) the greater the likelihood of collinearity, so it is necessary to count each( Θ \Theta Θ , r r r) The number of times a straight line passes on the. This statistic corresponds to the accumulator in the recurrence code.

After obtaining the statistician, you need to determine a threshold value. If the value of the corresponding position on the statistician exceeds the threshold value, it is considered as this parameter( Θ i {\Theta}_i Θi​ , r i r_i ri) a line that can determine the linear coordinate space

According to the determined parameters( Θ i {\Theta}_i Θi​ , r i r_i ri), just make a straight line on the original drawing.

experiment

Original drawing:

canny edge detection:

No maximum suppression:

Non maximum suppression is added:

Code interpretation

Implementation of python code base

     # Implementation of hough line transform python Library
    lines = cv2.HoughLines(imgEdge, 1, np.pi / 180, 160) # 1 and np.pi/180 are the smallest units of the voting matrix, respectively
    lines1 = lines[:, 0, :]  # Extract as 2D
    for rho, theta in lines1[:]: # Take out two numbers in each line of lines
        a = np.cos(theta)
        b = np.sin(theta)
        x0 = a * rho # (x0,y0) is the constant crossing point of the parametric equation r = x * cos(theta) + y * sin(theta))
        y0 = b * rho
        # Using theta, rho and a known point (x0,y0) to determine the other two points on the straight line, 1000 is a random value
        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 is to adjust the width of the line
	plt.imshow(img, cmap="gray")
    plt.axis("off")  # Close coordinates
    plt.show()

be careful ( x 0 , y 0 ) (x_0 , y_0) (x0, y0) is the point obtained by using polar coordinates (high school knowledge)

know ( x 0 , y 0 ) (x_0 , y_0) After (x0, y0), any two points of the line can be calculated according to the principle of the following figure ( x 1 , y 1 (x_1,y_1 (x1​,y1​), ( x 2 , y 2 (x_2,y_2 (x2​,y2​):

Code recurrence:

def lineHough(imgEdge, thetaDim = None,threshold = None):
    """
    :param imgEdge: Input image after edge detection
    :param ThetaDim: Theta Number of scales on the axis (0, pi)How many copies are divided into), if it is 90, it is divided into 90 copies, and each copy is 2
    :param threshold: Threshold for voting to define whether it is a straight line
    :return: Ballot box results that meet the threshold
    """
    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))  # The range of theta is [0,pi). Here [0,pi) is linearly mapped. Similarly, Dist axis is linearly mapped

    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))  # Thresholding
    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 Returned results
    :param img: Original picture
    :param color: Color of line drawing
    :param err: Error size, adjustable
    :return: Returns the picture after the line is drawn
    """
    Cos = np.cos(line[0])
    Sin = np.sin(line[0])

    # Traverse all positions of pixels in img, and then bring the position coordinates into the parameter equation (because the two parameters can determine the straight line, meeting the equation proves that the point must be on a straight line)
    # Here. any() needs one of the two matrices to be equal. It belongs to the skill of judging the equality of two matrices
    # The error here is caused by some digital operations such as rounding and rounding when hough transform is performed, so there must be an error
    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

Note that the drawing method here is different from that of the python library. Here, each point is traversed to judge whether each point conforms to the parameter equation, while the python library randomly selects two points and connects them to draw

Hoff circle transformation

Principle analysis

Hough gradient method

Circle detection needs to determine three parameters: (abscissa of the center of the circle, coordinates in the center of the circle, radius length). If it is calculated in three-dimensional space, the time is very long. Hough transform can be divided into two stages to reduce the dimension.

The first stage is to detect the center of the circle: if there are points on the same circumference, the gradient direction of these points should intersect the center of the circle. Therefore, the gradient of the edge points can be calculated to obtain the straight line in the gradient direction, and the intersection points of the straight lines can be accumulated. The position points with more accumulated numbers are considered as the candidate center of the circle.

The second stage detects the radius: after the position of the center of the circle is obtained, the distance from the point on the circumference to the center of the circle is equal. Therefore, the distance from each edge point to the candidate center can be calculated once, and the points of the same distance are accumulated. The distance with more accumulated numbers is considered as the candidate radius.

Determine the center of the circle

The drawing is the original image, and the right figure is the gradient direction straight line of the edge points of the original image. The bright part is the point with the most intersection times, so as to determine the center of the circle.


experiment


Fig. 1 is the original image, Fig. 2 is the image after canny extracting the edge, Fig. 3 is the straight line in the gradient direction of the edge point, and Fig. 4hough circle detection result

Code interpretation:

Implementation of python code base

gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
cv2.waitKey(0)
# Parameters: Gray: input gray image; solution method; ratio of image pixel resolution to parameter spatial resolution; center of detected circle; minimum distance between (x,y) coordinates; param1: gradient value method for processing edge detection;
# param2: accumulator threshold. The smaller the threshold, the more circles are detected; the minimum size of the radius in pixels; the maximum size of the radius in pixels
circles = cv2.HoughCircles(gray, cv2.HOUGH_GRADIENT, 1, 100, param1 = 150,
                           param2=30, minRadius=350, maxRadius=600)
circle = circles[0, :, :] # Extract as 2D
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) # The radius takes two pixels, which is equivalent to adjusting the thickness of the center of the circle. The last parameter is thickness
    plt.imshow(img)
    plt.axis('off')
    plt.show()

Disadvantages: the function contains many parameters. Finally, the quality of circle detection is too correlated with the parameters, and the parameters to be adjusted in each picture are often far from each other

Code reproduction

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]])
    # The gradients in the x and y directions are obtained respectively
    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 = [[],[]] # Subscript of pixel position stored as edge and point gradient not 0
    # ((1,1), (1,1)): the front (1,1) is filled with rows, the first 1 is filled above edge_x, the second 1 is filled below edge_x, and the following (1,1) is the same
    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)])
    # Vote according to gradient direction
    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 and dy are both 0. They are definitely not points on the circumference and can be excluded
                    t1 = (k/center_axis_scale-i)
                    t2 = (l/center_axis_scale-j)
                    t3 = t1**2 + t2**2
                    # Why do you find it here? t3 is the gradient of all edge points, 512 * 512, and temp is the bool matrix of [512512],
                    # Center_calculator [temp] + = 1 directly adds and subtracts the entire matrix
                    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
    #                                                                     Non maximum suppression
    # Distribution of circle center on the original drawing
    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 # The threshold value is obtained by using the mean value
    possible_centers = np.array(np.where(center_accumulator > center_threshold))  # Thresholding


    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]])
    # Finally, each list in sort_centers is [pixel matrix x, pixel matrix y, number of votes]
    sort_centers.sort(key=lambda x:x[2],reverse=True) # Sort according to the number of votes, from large to small

    centers = [[],[],[]]
    points = np.array(points)
    for i in range(len(sort_centers)): # After determining the center of the circle, determine the radius. The radius here is a one-dimensional vector, because only the size of r needs to be determined, while the center_accumulator needs to determine two values, so it is a two-dimensional vector
        radius_accumulator = np.zeros( # The maximum value of radius statistical matrix is diagonal
            (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 is the distance from each point in points to the center of the candidate circle
        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 is a vector
        temp = (np.round(radius_scale * temp)).astype(np.int32)
        for j in range(temp.shape[0]):
            if temp2[j]:
                radius_accumulator[temp[j]] += 1 # Points at this distance + 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) is e ^ J, and log is the logarithm with E as the base, which feels a little normalized
                radius_accumulator[j] = radius_accumulator[j]*radius_scale/np.log(j) #radius_accumulator[j]*radius_scale/j
        score_i = radius_accumulator.argmax(axis=-1) #Which position has the largest value
        if radius_accumulator[score_i] < score_threhold: # Compared with the threshold, if it is less than the threshold, it is not judged as the center of the circle
            iscenter = False

        if iscenter:
            centers[0].append(sort_centers[i][0]/center_axis_scale)
            centers[1].append(sort_centers[i][1]/center_axis_scale)
            # Here should be the radius, but why did you return the position of the maximum radius_accumulator?
            # kang: because the length of radius_accumulator is 0-diagonal, it stores the number of occurrences at each position, so score_i is the corresponding radius
            centers[2].append(score_i/radius_scale)


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

    return centers

Analysis: sobel operator is used to calculate the gradient here, and the solution process is much slower than direct library adjustment

reference material:

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

Topics: Python Computer Vision