logo

허프 변환

허프 변환을 이용한 직선과 원 검출

실습 준비

import cv2 as cv
import numpy as np
from yeom import to_pil

허프 변환(Hough transform)은 경계점의 투표로 직선이나 원의 파라미터를 찾는 방법.

닫힌 윤곽선이 없어도 끊긴 경계점에서 도형 검출 가능. 대신 찾을 도형의 파라미터 범위를 먼저 지정.

직선의 허프 표현

기울기와 y절편은 수직선 표현이 어려움. 허프 변환은 직선을 원점에서의 거리 와 법선 각도 로 표현.

경계점 는 자신을 지나는 여러 직선의 에 투표. 같은 직선 위의 점들이 만든 누적 공간의 봉우리가 직선 후보.

확률적 허프 직선 검출

cv.HoughLinesP()는 무한 직선의 대신 선분의 시작점과 끝점 를 반환.

먼저 도로 이미지를 절반 크기로 축소.

road_image = cv.imread("road.webp")
road_image = cv.resize(
    road_image,
    None,
    fx=0.5,  # 가로 절반
    fy=0.5,  # 세로 절반
    interpolation=cv.INTER_AREA,
)

road_image.shape  # 축소 후 형태
실행 결과
(630, 960, 3)

축소 후 높이 630픽셀, 너비 960픽셀. 필터링에서 다룬 Canny 경계 검출 적용.

road_gray = cv.cvtColor(road_image, cv.COLOR_BGR2GRAY)
road_blur = cv.GaussianBlur(
    road_gray,
    (5, 5),  # 5×5 가우시안 커널
    0,
)
road_edges = cv.Canny(
    road_blur,
    50,   # 하한 임계값
    150,  # 상한 임계값
)

np.count_nonzero(road_edges)  # 경계 픽셀 수
실행 결과
10340

Canny가 경계 픽셀 10,340개를 검출. 산과 지평선의 영향을 줄이기 위해 도로를 감싸는 사다리꼴 좌표 지정.

height, width = road_edges.shape
roi_polygon = np.array(
    [[
        (0, height),                            # 왼쪽 아래
        (int(width * 0.42), int(height * 0.34)),  # 왼쪽 위
        (int(width * 0.58), int(height * 0.34)),  # 오른쪽 위
        (width, height),                        # 오른쪽 아래
    ]],
    dtype=np.int32,  # fillPoly()가 사용할 정수 좌표
)

roi_polygon.shape  # 다각형 배열 형태
실행 결과
(1, 4, 2)

(1, 4, 2)는 다각형 1개, 꼭짓점 4개, 좌표 성분 2개. 이 영역 안의 경계만 유지.

roi_mask = np.zeros_like(road_edges)
cv.fillPoly(roi_mask, roi_polygon, 255)  # 관심 영역을 흰색으로 채움
road_roi_edges = cv.bitwise_and(
    road_edges,
    roi_mask,
)

np.count_nonzero(road_roi_edges)  # 관심 영역의 경계 픽셀 수
실행 결과
3927

관심 영역에는 경계 픽셀 3,927개. 이 경계점의 투표로 선분 검출.

lines = cv.HoughLinesP(
    road_roi_edges,
    1,             # 거리 해상도 1픽셀
    np.pi / 180,   # 각도 해상도 1도
    threshold=70,  # 누적 투표 임계값
    minLineLength=100,  # 최소 선분 길이
    maxLineGap=30,      # 연결할 최대 간격
)

lines.shape  # 선분 배열 형태
실행 결과
(12, 1, 4)

선분 12개 검출. 각 선분은 좌표 4개로 구성.

  • rho=1: 거리 해상도 1픽셀
  • theta=np.pi/180: 각도 해상도 1도
  • threshold=70: 누적 투표 임계값을 넘긴 후보 유지
  • minLineLength=100: 100픽셀보다 짧은 선분 제외
  • maxLineGap=30: 30픽셀 이하 간격의 선분 연결
line_result = road_image.copy()

for x1, y1, x2, y2 in lines[:, 0]:
    cv.line(
        line_result,
        (x1, y1),
        (x2, y2),
        (0, 255, 0),  # 초록색 선분
        3,            # 선 두께
    )

to_pil(line_result)

도로 원본, 관심 영역의 Canny 경계, 허프 변환으로 검출한 12개 선분 비교

왼쪽은 원본, 가운데는 관심 영역의 Canny 경계, 오른쪽은 검출 선분. 선분은 도로 경계와 중앙선에 집중. 같은 실제 선의 중복 결과는 기울기와 위치로 통합 가능.

허프 원 검출

원은 중심 와 반지름 로 표현.

cv.HoughCircles()HOUGH_GRADIENT 방식은 경계의 기울기 방향으로 중심 후보를 줄이고 반지름을 평가. 동전 표면 무늬의 작은 경계를 줄이기 위해 중앙값 필터 적용.

coins_image = cv.imread("coins.jpg")
coins_gray = cv.cvtColor(coins_image, cv.COLOR_BGR2GRAY)
circle_gray = cv.medianBlur(coins_gray, 5)  # 5×5 이웃의 중앙값

coins_image.shape  # 원본 형태
실행 결과
(452, 555, 3)

밝기 영상의 잡음을 완화한 뒤 중심 간 거리와 반지름 범위를 제한.

circles = cv.HoughCircles(
    circle_gray,
    cv.HOUGH_GRADIENT,
    dp=1.2,        # 입력과 누적 공간의 해상도 역비율
    minDist=55,   # 중심 사이 최소 거리
    param1=120,   # 내부 Canny 상한 임계값
    param2=40,    # 중심 후보 누적 임계값
    minRadius=25,  # 최소 반지름
    maxRadius=60,  # 최대 반지름
)

circles.shape  # 검출 배열 형태
실행 결과
(1, 13, 3)

원 후보 13개 검출. 각 후보는 중심 , 중심 , 반지름으로 구성. 검출 실패 시 circlesNone.

  • dp=1.2: 입력보다 낮은 해상도의 누적 공간 사용
  • minDist=55: 검출 중심 사이의 최소 거리
  • param1=120: 내부 Canny 연산의 상한 임계값
  • param2=40: 원 중심 후보의 누적 임계값
  • minRadius, maxRadius: 검출할 반지름 범위

그리기에 사용할 정수 좌표와 반지름으로 변환.

circles_rounded = np.round(
    circles[0]
).astype(int)

len(circles_rounded)  # 검출한 원 수
실행 결과
13

반지름 범위도 확인.

(
    circles_rounded[:, 2].min().item(),
    circles_rounded[:, 2].max().item(),
)  # 최소·최대 반지름
실행 결과
(36, 56)

param2를 낮추면 약한 후보와 거짓 검출 증가. 높이면 강한 원만 남아 실제 원 누락 가능.

circle_result = coins_image.copy()

for center_x, center_y, radius in circles_rounded:
    cv.circle(
        circle_result,
        (center_x, center_y),
        radius,
        (0, 255, 0),  # 초록색 원 둘레
        3,            # 선 두께
    )

같은 좌표에 빨간색 중심점 추가.

for center_x, center_y, radius in circles_rounded:
    cv.circle(
        circle_result,
        (center_x, center_y),
        4,            # 중심점 반지름
        (0, 0, 255),  # 빨간색 중심점
        -1,           # 내부 채우기
    )

to_pil(circle_result)

동전 원본과 허프 원 변환으로 검출한 13개 동전의 둘레·중심 비교

왼쪽은 원본, 오른쪽은 검출 결과. 동전 13개를 모두 검출했고 반지름 범위는 36–56픽셀.

윤곽선 분류와 허프 변환의 선택

닫힌 객체가 분리되어 있으면 도형 탐지의 다각형 근사와 원형도가 적합. 경계가 끊겼거나 장면 전체에서 직선·원을 직접 찾을 때는 허프 변환이 적합.

Previous
신분증 스캔