Perception/OpenCV

[OpenCV] 허프 변환 (Hough Transform) - 직선 검출

고집호랑이 2023. 2. 13. 07:00

개요 

이전 포스팅에서 물체의 테두리 즉 edge를 검출하는 케니 에지 검출(canny edge detection)에 대해서 배웠다면, 이번에는 영상에서 추출된 에지 정보를 사용해서 직선을 검출하는 방법에 대해서 알아보도록 하겠습니다.

 

영상에서 직선을 검출한다면 이는 자율 주행 자동차에서 차선을 검출하는데도 사용할 수 있을 것입니다. 

 

영상에서 직선을 검출하는 도구로 허프 변환(Hough Transform)이 많이 사용됩니다.

 

허프 변환 (직선 검출 원리)

허프 변환은 2차원 xy 좌표에서 직선의 방정식을 파라미터 공간으로 변환하여 직선을 검출합니다. 

 

일반적으로 직선의 방정식은 xy 좌표에서 다음과 같습니다.

 

$y = a·x + b$ 

 

이를 적당히 이항하면 다음과 같이 ab 좌표 공간에서 기울기가 -x이고, b 절편이 y인 식으로 쓸 수도 있습니다.

 

$b = -x·a + y$

 

xy 공간에서 ab 공간으로의 변환

위의 그림과 같이 xy 공간에서의 어떤 직선 중 한 점 $(x_0, y_0)$을 ab 공간으로 변환하면 기울기가 $-x_0$이고 y절편이 $y_0$인 직선이 됩니다.

 

이는 xy 공간에서 $(x_0, y_0)$을 지날 수 있는 모든 직선의, 기울기 a와 y 절편 b가 ab 공간에서 직선의 방정식으로 표현된 것입니다.

 

그렇다면 이제 xy 공간에서의 2개의 점을 생각해 봅시다. 

 

2개의 점을 ab 공간으로 변형

위의 그림과 같이 xy 공간에서 $y = a'x + b'$라는 직선의 방정식 위의 두 점 $(x_i, y_i)$ $(x_j, y_j)$를 ab 공간으로 변환해 봅시다. 

 

그러면 ab 공간에는 xy 공간에서 각 점을 지날 수 있는 모든 직선의, 기울기 a와 y 절편 b에 대한 직선의 방정식 2개가 그려집니다.

 

이때 두 직선의 방정식은 하나의 교점이 생기는데 그 교점의 좌표는 $(a', b')$가 됩니다. 

 

두 직선의 방정식의 교점 좌표는 xy 공간에서 두 점  $(x_i, y_i)$ $(x_j, y_j)$을 동시에 지나는 직선의 (기울기,  y절편)을 의미하는데, 2개의 점을 동시에 지나는 직선의, 기울기와 y 절편은 $a'$과 $b'$로 유일하기 때문입니다.

 

이 원리를 이용하면, xy 공간에서 영상의 에지 픽셀들을 허프변환하여 표현된 ab 공간 상의 직선들이 많이 교차되는 지점으로 직선을 검출할 수 있습니다. 

 

직선들이 많이 교차하는 점을 찾기 위해서 보통 축적 배열(accumulation array)를 사용합니다. 축적 배열은 0으로 초기화된 2차원 배열에서 직선이 지나가는 위치의 배열 원소 값을 1씩 증가시켜 생성합니다.

 

즉 축적 배열 값이 일정 임계값 이상인 점의 a와 b로 직선을 검출하는 것이죠.

 

허프 변환 직선 검출 원리

하지만 ab 공간으로 변환하면 한 가지 문제가 생깁니다. 바로 xy 공간에서의 직선이 y축과 평행한 수직선이라면 기울기 a가 ∞이고 y 절편은 b이기 때문에 ab 공간에서 교점이 (∞, b)가 되는데 이는 ab 공간에서 표현할 수 없습니다. 

 

이 문제를 해결하기 위해서 허프 변환은 ρ와 θ을 사용하는 극좌표계 직선의 방정식을 사용합니다.

 

$y = ax + b$ 형태의 직선의 방정식을 다음 그림과 같이 변형하면 $xcosθ + ysinθ = ρ$의 극좌표계 직선의 방정식으로 만들 수 있습니다.

 

극좌표계 직선의 방정식 사용

 

그러면 이제 저희는 직선의 방정식을 xy 공간에서 ρ와 θ의 공간으로 변환할 수 있겠죠.

 

극좌표계를 이용한 허프 변환

 

변환했을 때 직선이 아닌 곡선으로 표현되는 것만 제외하면 원리는 ab공간으로 변환했을 때와 같습니다. xy 공간에서의 점들을 ρ와 θ의 공간으로 변환했을 때 만들어지는 곡선들이, 많이 교차되는 점을 축적 배열로 찾아 직선을 검출하게 됩니다. 

 

허프 변환 직선 검출 함수

OpenCV에서는 허프 변환 직선 검출을 수행하기 위해서 HoughLines() 함수를 제공합니다.

 

HoughLines() 함수는 다음과 같습니다.

void HoughLines(InputArray image, OutputArray lines, double rho, 
            	double theta, int threshold, double srn = 0,
                double stn = 0, double min_theta = 0, doubel max_theta = CV_PI);
# => lines
  • image: 입력 영상, 주로 에지 영상을 지정
  • lines: 직선 정보(rho, theta)를 저장할 출력 벡터
  • rho: 축적 배열에서 거리(ρ) 간격(픽셀 단위)
  • theta: 축적 배열에서 각도(θ) 간격(라디안 단위)
  • threshold: 축적 배열에서 직선으로 판단할 임계값
  • srn, stn: 검출한 직선의 값이 더 정확한 값으로 반환되도록 거리(ρ)와 각도(θ)의 값을 조절해야 하는데, 이때 각각 거리간격(rho)과 각도 간격(theta)을 나누는 변수 
  • min_theta: 검출할 직선의 최소 theta 
  • max_theta: 검출할 직선의 최대 theta 

 

직선을 검출하기 위해서 보통 입력 영상은 전에 배운 Canny() 함수에서 출력된 에지 영상을 입력합니다. 

 

threshold 인자에는 직선으로 판단할 임계값을 지정하는데, 이 값을 적게 지정한다면 많은 직선이 검출될 수 있지만 원치 않는 짧은 선까지 직선으로 검출할 수 있기 때문에 적절한 값으로 지정해야 합니다.

 

이 함수를 이용해서 아래의 바둑판 사진에서의 직선을 검출해 봅시다.

 

HoughLines() 함수를 적용시켜볼 예제

 

import numpy as np
import cv2

# 바둑판 영상 불러오기
image = cv2.imread("../repos/Line_detection/Hough_Transformation/game.jpg")
# 원본 영상을 복사
image2 = image.copy()

# 흑백 영상으로 변환
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# canny edge detection을 이용해 edge 검출
edges = cv2.Canny(gray, 100, 200)

# HoughLines 함수를 이용해 직선 정보를 저장 
lines = cv2.HoughLines(edges, 1, np.pi/180, 80)

# 출력된 직선 정보를 이용해 원본 사진에 표현 
for line in lines:
    rho, theta = line[0]
    cos, sin = np.cos(theta), np.sin(theta)
    cx, cy = rho * cos, rho * sin
    x1, y1 = int(cx + 1000 * (-sin)), int(cy + 1000 * cos)
    x2, y2 = int(cx + 1000 * sin), int(cy + 1000 * (-cos))
    # 원본 사진에 초록색 선으로 표시
    cv2.line(image2, (x1, y1), (x2, y2), (0,255,0), 1)

# 에지 검출 영상 출력
cv2.imshow("image", image2)
# 직선 검출 영상 출력
cv2.imshow("edges", edges)
# 키를 입력할 때까지 대기
cv2.waitKey(0)

직선을 검출하는 작업은 Canny() 함수와 HoughLines() 함수를 호출하면서 완료됩니다. 이후 코드는 원본 영상에 직선을 표시하기 위한 코드입니다.

 

직선을 그리기 위해서 임의로 큰 값을 곱해줘 영상 바깥쪽에 직선의 시작점과 끝점을 위치시켜 이어주었습니다.

 

출력 결과 아래와 같이 직선이 잘 검출된 것을 확인할 수 있습니다.

허프 변환 직선 검출 결과

 

확률적 허프 변환 함수

위의 HoughLines() 함수는 영상 내의 모든 픽셀 좌표에 대해서 허프 변환해 주기 때문에 계산량이 많고 시간이 오래 걸립니다. 

 

이를 위해서 OpenCV에서는 확률적으로 임의의 점만을 허프 변환시켜 선 검출 시간을 줄여주는 확률적 허프 변환( Probabilistic Hough Transform) 함수HoughLinesP()도 제공합니다.

 

HoughLinesP() 함수는 다음과 같습니다.

 

void HoughLinesP(InputArray image, OutputArray lines, double rho, 
            	double theta, int threshold, double minLineLength = 0,
                double maxLineGap = 0);
# => lines
  • image: 입력 영상, 주로 에지 영상을 지정
  • lines: 출력될 선분의 시작점과 끝점의 정보
  • rho: 거리(ρ) 간격(픽셀 단위)
  • theta: 각도(θ) 간격(라디안 단위)
  • threshold: 직선으로 판단할 임계값
  • minLineLength: 검출할 선분의 최소 길이 
  • maxLineGap: 직선으로 간주할 최대 에지 점 간격 

 

HoughLinesP() 함수는 HoughLines() 함수와 다르게  ρ와 θ을 출력해 주는 것이 아니라 선분의 시작점과 끝점 좌표를 반환합니다. 

 

즉 HoughLinesP() 함수 결과는 직선이 아닌 선분으로 나오겠죠. 원하는 선을 검출하기 위해서는 threshold, minLineLength, maxLineGap 값을 잘 조절해야 합니다.

 

동일하게 바둑판 영상에서 HoughLinesP() 함수를 사용해 보겠습니다.

 

import numpy as np
import cv2

image = cv2.imread("../repos/Line_detection/Hough_Transformation/game.jpg")
image2 = image.copy()

gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
edges = cv2.Canny(gray, 100, 200)
# 위는 HoughLines() 함수와 동일

lines = cv2.HoughLinesP(edges, 1, np.pi/180, 25, None, 40, 5)
for line in lines:
    # 검출된 선분 초록색으로 그리기
    x1, y1, x2, y2 = line[0]
    cv2.line(image2, (x1,y1), (x2, y2), (0,255,0), 1)

cv2.imshow("image", image2)
cv2.imshow("edges", edges)
cv2.waitKey(0)

바로 시작점과 끝점을 반환해 주기 때문에 HoughLines() 함수보다 훨씬 간단한 것을 확인할 수 있습니다. 

 

출력 결과 모든 선을 검출하진 못했지만 아래와 같이 선분을 잘 검출한 것을 확인할 수 있습니다. 

확률적 허프 변환에 의한 직선 검출 결과

 

허프 변환을 잘 이용한다면 자율주행 자동차를 위한 차선 인식도 할 수 있을 것입니다.

 

하지만 허프 변환만을 사용했을 때의 검출 결과가 완벽하지 않은 걸로 봐선 다른 잡음 제거 함수나 차선을 제외한 다른 직선은 검출하지 못하게 관심영역을 지정하는 등의 처리가 필요해 보입니다. 

 

추후 허프 변환을 이용해 차선만을 인식하는 코드를 작성해 보도록 하겠습니다.