Practical.kr

실용적인 소프트웨어를 만듭니다.

OpenCV 기반으로 농구 코트 찾기

배경

농구 경기를 분석하여 선수의 움직임을 추출하기 위해서는 우선 농구 코트를 ROI(Region of Interest)로 설정해야 한다. 그래야만 관중과 선수들을 구분할 수 있기 때문이다. AI 분석 방법이 많은 GPU를 요구하고 느린 관계로 OpenCV를 이용하여 기본적인 ROI를 구현하는 부분을 테스트했다.

고정된 카메라에서는 정해진 ROI를 사용할 수 있지만, 방송 중계 화면은 카메라 이동이 빈번히 일어나고 선수들이 움직여 코트의 외곽선 라인을 가리는 상황이 매우 빈번하게 발생하기 때문에 쉽지는 않지만, 코트 바닥의 ROI를 설정하는 것은 가장 기본적이고 꼭 필요한 기능이다.

결과적으로 모든 프레임에서 완전히 정확하지는 않지만 선수와 관중의 구분이 가능할 정도의 성능이 나온다.

동영상

다음 링크에서 실제 동작하는 동영상을 볼수있다.
https://youtu.be/0RT8Iq9kUzA

구현 방법

1. 그레이스케일 변환 및 이진화

image_gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
_, image_binary = cv2.threshold(image_gray, 127, 255, cv2.THRESH_BINARY)

입력 이미지를 그레이스케일로 변환한 후, 127을 기준으로 이진화한다. 이는 후속 처리 단계의 기본이 된다.

2. 흰색 영역 검출

white_mask = cv2.inRange(image_gray, 160, 255)

농구 코트의 라인은 일반적으로 흰색이므로, 160~255 범위의 밝은 영역을 추출한다. 이 마스크는 코트 라인 영역을 강조한다.

3. 엣지 검출

edges = cv2.Canny(image_binary, 50, 150, apertureSize=3)

Canny 엣지 검출기를 사용하여 이진화된 이미지에서 엣지를 추출한다. 임계값은 50과 150으로 설정했다.

4. Hough Line Transform

lines = cv2.HoughLinesP(edges, 1, np.pi/180, threshold=80, 
                        minLineLength=50, maxLineGap=30)

for line in lines:
    x1, y1, x2, y2 = line[0]
    angle = np.abs(np.arctan2(y2 - y1, x2 - x1) * 180 / np.pi)

    if angle < 20 or angle > 160:
        horizontal_lines.append((x1, y1, x2, y2))
    elif 70 < angle < 110:
        vertical_lines.append((x1, y1, x2, y2))

확률적 Hough 변환을 사용하여 직선을 검출한다. 검출된 라인은 각도를 기준으로 수평선과 수직선으로 분류한다. 이는 코트의 기하학적 구조를 파악하는 데 활용될 수 있다.

5. 모폴로지 연산

scale_factor = min(width, height) / 720.0
kernel_size = max(3, int(3 * scale_factor))
if kernel_size % 2 == 0:
    kernel_size += 1

kernel = np.ones((kernel_size, kernel_size), np.uint8)
dilated = cv2.dilate(white_mask, kernel, iterations=1)

erode_kernel_size = max(3, int(3 * scale_factor))
if erode_kernel_size % 2 == 0:
    erode_kernel_size += 1
erode_kernel = np.ones((erode_kernel_size, erode_kernel_size), np.uint8)
eroded = cv2.erode(dilated, erode_kernel, iterations=1)

해상도에 따라 커널 크기를 동적으로 조정한다. Dilation 후 Erosion을 수행하여 작은 노이즈를 제거하고 끊어진 라인을 연결한다. 720p를 기준으로 스케일 팩터를 계산하며, 커널 크기는 최소 3으로 설정한다.

6. 컨투어 검출 및 필터링

contours, _ = cv2.findContours(eroded, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

court_candidates = []
for cnt in contours:
    area = cv2.contourArea(cnt)
    if area > (width * height * 0.10):
        x, y, w, h = cv2.boundingRect(cnt)
        court_candidates.append((cnt, area, y))

외부 컨투어만 검출하여 화면의 10% 이상 면적을 차지하는 후보들만 선택한다. 이는 작은 노이즈를 제거하고 코트 영역만 추출하기 위함이다.

7. 최적 컨투어 선택

court_candidates.sort(key=lambda x: (x[2] + x[1] / 10000), reverse=True)
best_contour = court_candidates[0][0]

y 좌표와 면적을 조합한 점수로 정렬하여 최적의 코트 후보를 선택한다. 일반적으로 코트는 화면 하단에 위치하며 큰 면적을 차지한다.

8. Convex Hull

convex_hull = cv2.convexHull(best_contour)

선택된 컨투어를 볼록 다각형(Convex Hull)으로 변환한다. 이는 안쪽으로 꺾인 부분을 제거하여 코트의 외곽선을 단순화한다.

9. 폴리곤 근사화

epsilon = 0.01 * cv2.arcLength(convex_hull, True)
approx_polygon = cv2.approxPolyDP(convex_hull, epsilon, True)

Douglas-Peucker 알고리즘을 사용하여 폴리곤을 근사화한다. epsilon 값은 컨투어 둘레의 1%로 설정하여 단순화 정도를 조절한다.

파라미터 튜닝 포인트

  • 이진화 임계값: 127 (밝기 기준)
  • 흰색 검출 범위: 160~255
  • Canny 엣지 임계값: 50, 150
  • Hough 변환 파라미터: threshold=80, minLineLength=50, maxLineGap=30
  • 각도 분류: 수평선(< 20° 또는 > 160°), 수직선(70° ~ 110°)
  • 커널 크기: 해상도에 비례, 최소 3×3
  • 모폴로지 반복: dilation 1회, erosion 1회
  • 면적 임계값: 전체 화면의 10%
  • 폴리곤 근사화 epsilon: 둘레의 1%
  • 확대 비율: 1.05 (5% 확대)

제한사항

  • 코트 라인이 흰색이라는 가정에 의존
  • 조명이 어둡거나 코트 색상이 특이한 경우 성능 저하 가능
  • 카메라 각도가 극단적인 경우 검출 실패 가능
  • 프레임마다 안정성은 보장되지 않으나, 대부분의 경우 유효한 ROI 제공

결론

OpenCV의 기본 이미지 처리 기법을 조합하여 농구 코트를 검출할 수 있다. GPU 없이도 실시간에 가까운 속도로 동작하며, 방송 중계 영상에서 동적으로 변하는 카메라 각도에도 대응 가능하다. 완벽한 정확도를 보장하지는 않지만, 선수와 관중을 구분하는 용도로는 충분한 성능을 보인다.