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 없이도 실시간에 가까운 속도로 동작하며, 방송 중계 영상에서 동적으로 변하는 카메라 각도에도 대응 가능하다. 완벽한 정확도를 보장하지는 않지만, 선수와 관중을 구분하는 용도로는 충분한 성능을 보인다.