허프 변환을 활용해 이미지에서 직선이나 원과 같은 다양한 모양을 인식 할 수 있습니다. 여기서는 직선과 원을 검출하는 함수에 대해 배워보도록 하겠습니다.

허프 변환에 대한 이론적인 설명은 OpenCV 한글 문서에 자세하게 설명되어 있습니다.

컨투어와 가장 큰 차이점은 컨투어는 도형 외곽선을 찾는데 집중한다면 허프 변환은 특정 기하하적 형태를 방정식으로 매칭 할 수 있을때 공간을 누적하여 패턴을 찾는 알고리즘입니다.

허프 선 변환

이미지의 많은 픽셀중에 서로 직선 관계를 갖는 픽셀들을 골라내어 매칭하는것이 허프 선 변환의 핵심입니다.

OpenCV에서는 허프 변환을 위해서 다양한 함수들을 제공합니다.

lines = cv2.HoughLines(img, rho, theta, threshold, lines, srn=0, stn=0, min_theta, max_theta)
img: 입력 이미지, 1 채널 바이너리 스케일
rho: 거리 측정 해상도, 0~1
theta: 각도, 라디안 단위 (np.pi/0~180)
threshold: 직선으로 판단할 최소한의 동일 개수 (작은 값: 정확도 감소, 검출 개수 증가 / 큰 값: 정확도 증가, 검출 개수 감소)
lines: 검출 결과, N x 1 x 2 배열 (r, Θ)
srn, stn: 멀티 스케일 허프 변환에 사용, 선 검출에서는 사용 안 함
min_theta, max_theta: 검출을 위해 사용할 최대, 최소 각도

매칭할 선의 거리와 각도를 얼마나 세밀하게 계산할 것인지를 rho와 theta 파라미터로 조정할 수 있습니다. threshold는 같은 직선에 몇 개의 점이 등장해야 직선으로 판단할지를 나타내는 최소한의 개수를 말합니다.

바로 구현해 보겠습니다.

import cv2
import numpy as np
import matplotlib.pyplot as plt

IMG_PATH = r"C:\\Users\\danny\\Desktop\\Hough.png"

# 0. 이미지 읽기
img = cv2.imread(IMG_PATH, cv2.IMREAD_COLOR)
if img is None:
    raise FileNotFoundError(f"이미지를 찾을 수 없습니다: {IMG_PATH}")

# 0-1. 너무 크면 축소 (최대 한 변 800px)
h0, w0 = img.shape[:2]
max_side = 800
scale = min(1.0, max_side / max(h0, w0))
if scale < 1.0:
    img = cv2.resize(img, (int(w0 * scale), int(h0 * scale)), interpolation=cv2.INTER_AREA)

img2 = img.copy()
h, w = img.shape[:2]

# 1. 그레이 스케일 + 블러 + Canny
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
gray = cv2.GaussianBlur(gray, (5, 5), 1)
edges = cv2.Canny(gray, 50, 150)

# 2. 허프 선 검출 (무한 직선)
lines = cv2.HoughLines(edges, 1, np.pi / 180, 120)

unique_lines = []
seen = set()

if lines is not None:
    for line in lines:
        rho, theta = line[0]

        # 2-1. 수평/수직 근처의 각도만 사용 (대각선 제거)
        deg = theta * 180.0 / np.pi
        if not (
            abs(deg - 0) < 10 or       # 수평
            abs(deg - 180) < 10 or     # 수평 (반대 방향)
            abs(deg - 90) < 10         # 수직
        ):
            continue

        # 2-2. rho/theta를 좀 거칠게 양자화해서 중복 제거
        rho_q = int(round(rho / 5))                 # 5픽셀 단위
        theta_q = int(round(theta / (np.pi / 180 * 2)))  # 2도 단위
        key = (rho_q, theta_q)
        if key in seen:
            continue
        seen.add(key)
        unique_lines.append((rho, theta))

# 2-3. 선 그리기
for rho, theta in unique_lines:
    a = np.cos(theta)
    b = np.sin(theta)
    x0 = a * rho
    y0 = b * rho

    # 화면 끝까지 연장한 두 점 계산
    x1 = int(round(x0 + w * (-b)))
    y1 = int(round(y0 + h * (a)))
    x2 = int(round(x0 - w * (-b)))
    y2 = int(round(y0 - h * (a)))

    cv2.line(img2, (x1, y1), (x2, y2), (0, 255, 0), 1)

print(f"검출된 선 개수 (중복 제거 후): {len(unique_lines)}")

# 3. Matplotlib으로 출력 (BGR -> RGB 변환)
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img2_rgb = cv2.cvtColor(img2, cv2.COLOR_BGR2RGB)

plt.figure(figsize=(10, 5))
plt.subplot(1, 2, 1)
plt.title("Original")
plt.imshow(img_rgb)
plt.axis("off")

plt.subplot(1, 2, 2)
plt.title("Hough Lines (Filtered)")
plt.imshow(img2_rgb)
plt.axis("off")

plt.tight_layout()
plt.show()

image.png

image.png

우선 캐니 엣지로 경계값을 검출한 뒤 허프 선 검출을 했습니다.

허프 원 변환

허프 변환을 통해 원을 검출 할 수도 있습니다.

함수들은 다음과 같습니다.