Phát hiện đường thẳng bằng Hough Transform (Hough Line)

  May 4, 2019      2m
   

OpenCV - tut 12: Tự hiện thực Hough Transform

Phát hiện đường thẳng bằng Hough Transform (Hough Line)

Môi trường "hành sự"

  • Linux (bài viết sử dụng Ubuntu 16.04)
  • OpenCV (bài viết sử dụng OpenCV 3.4.1)
  • Python (bài viết sử dụng Python 3.5.5)
  • Ảnh mẫu để xử lý: geometry.jpg

Bạn có thể download ảnh mẫu về:

geometry.jpg (Nguồn: Lụm trên mạng)

geometry

Giải thuật phát hiện đường thẳng - Hough Transform

Ý tưởng chính của giải thuật phát hiện đường thẳng Hough Transform đó là:

  • Dựa trên kết quả phát hiện cạnh để tiến hành phát hiện đường thẳng. Giải thuật phát hiện cạnh phổ biến là Canny Edge Detection.
  • Trên mỗi pixel cạnh (pixel thuộc cạnh được phát hiện trong ảnh), ta lần lượt thử các phương trình đường thẳng đi qua pixel đó. Số phương trình đường thẳng ta thử càng nhiều thì sẽ cho ra kết quả phát hiện đường thẳng càng tốt (ít bỏ lỡ đường thẳng có trong ảnh hơn). Pixel cạnh đó sẽ "vote" thêm 1 giá trị vào ma trận thống kê.
  • Sau khi duyệt hết tất cả các pixel cạnh, ta sẽ lọc theo một giá trị ngưỡng (xác định trước) trên ma trận thống kê để giữ lại (để xác định được) các phương trình đường thẳng có trong ảnh.

Sau khi xác định được các đường thẳng, việc còn lại đơn giản ta chỉ việc vẽ các đường thẳng đó lên ảnh.

Tuần tự các bước trong giải thuật Hough Transform phát hiện đường thẳng bạn tham khảo thêm tại link sau đây:

Hough Transform trong OpenCV

Các hàm cần thiết để dùng giải thuật Hough Transform đều đã được hiện thực trong thư viện OpenCV:

  • cv2.Canny: trích cạnh bằng giải thuật Canny
  • cv2.HoughLines: phát hiện đường thẳng bằng giải thuật Hough Transform
  • cv2.line: vẽ đường thẳng

Các siêu tham số để dùng hiệu quả cho trường hợp của bạn, bạn cần phải đọc tài liệu (documentation) của OpenCV để điều chỉnh cho hợp lý nhé. Các siêu tham số này ảnh hưởng rất nhiều đến kết quả đạt được, điều chỉnh thường dựa vào kinh nghiệm hoặc thử sai.

Đoạn code ngắn sau thì bạn đã dùng xong Hough Transorm rồi nhé, quá là siêu cấp đơn giản luôn ^^:

# read image
img = cv2.imread('geometry.jpg')
# convert to gray scale
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # color -> gray
edges = cv2.Canny(gray, 50, 150, apertureSize=3)
lines = cv2.HoughLines(edges, rho=1, theta=np.pi/180, threshold=100)
for line in lines:
    rho, theta = line[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,(x1,y1),(x2,y2),(0,0,255),2)    
cv2.imwrite('geo_hough.jpg',img)

Hiện thực Hough Transorm

Một trong những phần thú vị và thử thách nhất cho người viết blog là đây. Tự hiện thực giải thuật Hough Transorm! Dĩ nhiên, giải thuật mọi người đều đã nắm trong tay. Vậy làm sao để hiện thực Hough Line để phát hiện đường thẳng đây?! Hôm nay mình sẽ viết dài dòng hơn một chút, nói nhiều hơn một chút để mọi người nắm được và có thể áp dụng cho bản thân thay vì chỉ cung cấp code để chạy.

Trước tiên, các bạn đọc kỹ hướng dẫn giải thuật trong tutorial Hough Transform của OpenCV. Mình sẽ dẫn dắt cách hiện thực từng bước một. Đầu tiên, phương trình một đường thẳng có thể được mô tả bằng rhotheta. Góc theta có giá trị từ 0 -> 180, do đó nếu ta muốn thử các đường thẳng mà mỗi đường thẳng lệch nhau chỉ 1 độ (tức đường thẳng 1, theta = 0 độ; đường thẳng 2, theta = 1 độ, đường thẳng 3, theta = 2 độ; …) thì ta sẽ có 180 đường thẳng có thể thử. Góc theta để đặc tả một đường thẳng là chưa đủ, ta cần thêm khoảng cách rho (đọc là rô) để xác định khoảng cách từ đường thẳng đến gốc tọa độ O (từ O ta chiếu vuông góc đến đường thẳng). Giá trị tối đa của rho bằng đường chéo của ảnh (diagonal_length), mà rho lại có thể mang giá trị âm, do đó rho có giá trị trong đoạn [-diagonal_length, diagonal_length]. Dĩ nhiên, để đạt kết quả tốt, ta có thể set độ khít của rho = 1 pixel thì ta sẽ có tổng số rho có thể thử trên ảnh là 2 x int(diagonal_length / rho) + 1 (cộng thêm một là do ta phải tính luôn cả giá trị zero).

Từ các thông tin trên, ta xác định được ma trận thống kê (khởi tạo các phần tử trong ma trận này có giá trị zero) có kích thước là: số trường hợp rho x số trường hợp theta. Tiếp theo, ta cần phải duyệt trên mỗi pixel cạnh xem có thể có bao nhiêu phương trình đường thẳng (xác định bởi cặp số (rho, theta)) đi qua được nó. Ý tưởng cho việc hiện thực đó là với mỗi pixel cạnh (x,y), ta tính giá trị rho tương ứng cho chúng lần lượt cho các giá trị theta (180 giá trị theta). Ví dụ cụ thể cho dễ hiểu, giả sử ta có 4000 pixel được cho là cạnh bởi giải thuật Canny, thì ta sẽ có 4000 cặp giá trị (x,y) –> mô tả bằng ma trận A có kích thước [4000, 2]. Sau đó, với mỗi cặp (x,y) ta lại muốn thử 180 giá trị theta theo phương trình đường thẳng ρ = xcosθ + ysinθ để tính ra 180 giá trị rho. Để làm được điều đó, ta chỉ cần dựng ma trận B có kích thước [2, 180] chứa các cosθ (dòng 1) và sinθ (dòng 2) để sau đó tính theo dot product nhằm thu được một ma trận chứa các giá trị rho là C có kích thước [4000, 180].

Ta chỉ cần duyệt mỗi phần tử trên ma trận C để "vote" vào ma trận thống kê X bằng cách xác định dòng và cột phù hợp để +1 đơn vị. Giá trị rho nằm từ [-num_rho, num_rho] trong khi index các dòng của X bắt đầu từ 0, vì vậy rho_pos đối với X sẽ bằng int(round(vote_matrix[vr, vc]))+num_rho. Sau khi duyệt xong để vote vào X thì ta lọc theo ngưỡng xác định trước để giữ lại các đường thẳng có trong ảnh. Lọc theo ngưỡng trên ma trận X (edge_matrix) ta sẽ được các index của dòng và index của cột. Từ các giá trị index này ta phải convert về rhotheta (radian) để hàm cv2.line có thể vẽ đúng!

Phí trên là dòng suy nghĩ để tiến đến hiện thực thành code như bên dưới, các bạn có thể chạy thử thì hàm tự hiện thực sẽ cho ra kết quả xấp xỉ hàm của OpenCV. Cụ thể OpenCV vẽ ra được 18 line, trong khi hàm của mình vẽ tận 21 line. Điểm khác biệt là do chi tiết hiện thực. OpenCV cũng đã được tối ưu, nó chạy nhanh hơn giải thuật tự hiện thực của mình.

Mục đích cuối cùng của việc tự hiện thực đó là giúp mình hiểu rõ đến từng chi tiết của giải thuật Hough Transform!

hough.py

import math
import cv2
import numpy as np

# reference: https://docs.opencv.org/master/d6/d10/tutorial_py_houghlines.html
def my_hough(img, rho=1, theta=np.pi/180, threshold=100):
    img_height, img_width = img.shape[:2]
    diagonal_length = int(math.sqrt(img_height*img_height + img_width*img_width))
    
    print('[My Hough] Img Height: %d | Img Width: %d | Img Diagonal Length: %d' % (img_height, img_width, diagonal_length))
    
    num_rho = int(diagonal_length / rho)
    num_theta = int(np.pi / theta)
    
    edge_matrix = np.zeros([2*num_rho+1, num_theta]) # dim: num_rho x num_theta
    
    print('[My Hough] Edge Matrix Dim: %d x %d' % (edge_matrix.shape[0], edge_matrix.shape[1]))
    
    idx	= np.squeeze(cv2.findNonZero(img)) # dim: 4468 x 2 (example, number of rows = number of white pixel on image processed by canny edge algorithm!)
    
    range_theta = np.arange(0, np.pi, theta)
    theta_matrix = np.stack((np.cos(np.copy(range_theta)), np.sin(np.copy(range_theta))), axis=-1) # dim: 180 x 2
    
    vote_matrix = np.dot(idx, np.transpose(theta_matrix)) # => (4468 x 2) * (180 x 2)T = (4468 x 2) * (2 x 180) = 4468 x 180
    print('[My Hough] Vote Matrix Dim: %d x %d' % (vote_matrix.shape[0], vote_matrix.shape[1]))
    
    # loop on vote matrix and accumulate values on edge matrix
    for vr in range(vote_matrix.shape[0]):
        for vc in range(vote_matrix.shape[1]):
            rho_pos = int(round(vote_matrix[vr, vc]))+num_rho
            edge_matrix[rho_pos, vc] += 1
    
    print('[My Hough] Sum of Edge Matrix = %d | Max = %d | Min = %d' % (int(np.sum(edge_matrix)), int(np.max(edge_matrix)), int(np.min(edge_matrix))))
    
    line_idx = np.where(edge_matrix > threshold)
    
    rho_values = list(line_idx[0])
    rho_values = [r-num_rho for r in rho_values]
    theta_values = list(line_idx[1])
    theta_values = [t/180.0*np.pi for t in theta_values]
    
    line_idx = list(zip(rho_values, theta_values))
    line_idx = [[li] for li in line_idx]
    return line_idx

def main():
    # read image
    img = cv2.imread('geometry.jpg')
    # convert to gray scale
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # color -> gray
    edges = cv2.Canny(gray, 50, 150, apertureSize=3) # https://docs.opencv.org/2.4/modules/imgproc/doc/feature_detection.html?highlight=canny#canny
    cv2.imwrite('geo_canny.jpg', edges)

    # USE BUILT-IN OPENCV HOUGH ALGORITHM
    lines = cv2.HoughLines(edges, rho=1, theta=np.pi/180, threshold=100) # https://docs.opencv.org/master/dd/d1a/group__imgproc__feature.html#ga46b4e588934f6c8dfd509cc6e0e4545a
    for line in lines:
        rho, theta = line[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,(x1,y1),(x2,y2),(0,0,255),2)    
    cv2.imwrite('geo_hough.jpg',img)
    print('[OpenCV Hough] Number of lines: %d' % len(lines))
    
    # IMPLEMENT HOUGH ALGORITHM MYSELF!
    lines = my_hough(edges, rho=1, theta=np.pi/180, threshold=100)
    for line in lines:
        rho, theta = line[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,(x1,y1),(x2,y2),(0,0,255),2)    
    cv2.imwrite('geo_myhough.jpg',img)
    print('[My Hough] Number of lines: %d' % len(lines))

if __name__ == "__main__":
    main()
    print('* Follow me @ ' + "\x1b[1;%dm" % (34) + ' https://www.facebook.com/minhng.info/' + "\x1b[0m")
    print('* Join GVGroup for discussion @ ' + "\x1b[1;%dm" % (34) + 'https://www.facebook.com/groups/ip.gvgroup/' + "\x1b[0m")
    print('* Thank you ^^~')    

geo_canny.jpg

geometry canny

geo_hough.jpg

geometry hough line


Cảm ơn bạn đã theo dõi bài viết. Hãy kết nối với tớ nhé!


Danh sách bài viết series OpenCV: