테니스 경기 영상을 올리면 선수를 추적해 이동 경로 · 이동거리 · 속도 · 코트 히트맵을 보여주고, 흩어진 국내 아마추어 대회 일정을 한곳에 모아 보여주는 웹 서비스입니다.
컴퓨터 비전, 크롤링, 웹 서비스 개발, 모델 경량화를 하나의 프로젝트로 묶어 처음부터 끝까지 만들어 본 개인 프로젝트입니다. 구현은 AI(Claude)와 협업했고, 요구사항 정의 · 결과 검토 · 검증 질문 · 실험 설계 판단은 제가 맡았습니다.
| 역량 | 이 프로젝트에서 한 일 |
|---|---|
| 컴퓨터 비전 | YOLO11n 검출 + BoT-SORT 추적을 Homography로 실제 코트 좌표(m)에 연결해 이동거리·속도·히트맵 산출 |
| 모델 학습·평가 | 공 검출 모델을 v1~v3a까지 파인튜닝하고 같은 영상 기준으로 적중률·오검출률 비교, 악화된 가설까지 검증해 기록 |
| 모델 경량화 | NNCF + OpenVINO INT8 양자화로 모델 크기 68%↓(10.9→3.5MB), CPU 추론 약 1.7배 빠름, 서비스 지표 변화까지 확인 |
| 크롤링 · 데이터 | KATO·KTA 일정 수집, SQLite 저장, upsert() 변경 감지와 이력 관리, 무작위 시각 일 1회 수집 |
| 백엔드 · 프론트 | FastAPI 로그인·세션·분석 기록 API, 프레임워크 없는 단일 페이지 UI(다크/라이트·모바일) |
| 배포 환경 | Dockerfile·compose 작성, 컨테이너 안에서 서비스·크롤러 동작 확인 (로컬 한정) |
| 실험 태도 | 측정 조건 불일치로 1차 결과를 스스로 폐기·재측정, 한계와 못 한 것을 문서에 명시 |
| 문제 | 아마추어는 경기 영상을 찍어도 분석할 방법이 없고, 대회 정보는 협회 사이트마다 흩어져 있다 |
| 해결 | 영상 → 선수 추적 → 실제 코트 좌표(m)로 변환 → 통계·히트맵. 대회 일정은 자동 수집해 DB에 저장 |
| 스택 | FastAPI · SQLite · YOLO11n · BoT-SORT · OpenCV · OpenVINO · Docker |
| 상태 | 로컬 동작, Docker 컨테이너 실행 확인. 외부 서버 배포는 아직 |
영상 분석 — 영상을 올리고 코트 4점을 클릭하면 선수의 이동거리, 평균/최고 속도, 궤적, 히트맵을 만듭니다. 4단계 스텝 위저드로 한 번에 한 단계만 보이게 설계했습니다.
대회 일정 — KATO, KTA의 공개 일정을 수집해 출처·부별·접수중 필터로 보여줍니다. 매일 하루 한 번, 매번 다른 무작위 시각에 수집하며, 접수상태 등이 바뀌면 변경 이력을 남깁니다.
로그인 · 마이페이지 — 분석 결과를 촬영일별로 저장하고 다시 볼 수 있습니다. 다크/라이트 테마, 모바일 화면을 지원합니다.
영상 업로드 → 코트 4점 클릭 → YOLO11n 선수 검출 → BoT-SORT 추적
→ Homography로 화면 좌표를 코트 좌표(m)로 변환
→ 이동거리 · 속도 · 궤적 · 히트맵
수치와 한계는 모두 문서로 남겼습니다. 아래 값은 해당 문서에 있는 그대로입니다.
1. 공 검출 모델 파인튜닝 — results/v2_ball_experiment.md
video9 기준, 20px 이내 적중률 / 공이 없는 프레임의 오검출률 (conf 0.35)
| 모델 | 적중률 | 오검출률 | 비고 |
|---|---|---|---|
| v1 | 92.9% | 37.6% | |
| v2 (채택) | 95.3% | 15.6% | 공 없는 배경 이미지 571장 추가 |
| v3 | 92.9% | 21.8% | 공개 데이터 추가 → 오히려 악화 |
| v3a | 99.4% | 31.6% | 배경 보정 가설 → 기각 |
배운 점: 데이터를 더 넣는다고 좋아지지 않았고, 원인을 가설로 세워 다시 검증해 기각까지 확인했습니다.
2. INT8 경량화 (선수 검출) — results/int8_experiment.md
CPU(Xeon W-2223), 입력 544×960, 300프레임
| PyTorch FP32 | OpenVINO FP32 | OpenVINO INT8 | |
|---|---|---|---|
| 프레임당 시간 | 72.2 ms | 85.7 ms | 50.0 ms |
| 모델 크기 | 5.6 MB | 10.9 MB | 3.5 MB |
서비스 지표까지 비교한 결과(video9 전체, 코트 4점은 근사값을 양쪽에 동일 적용):
| FP32 | INT8 | |
|---|---|---|
| 총 이동거리 | 33.83 m | 33.99 m (+0.5%) |
| 평균 속도 | 3.41 m/s | 3.40 m/s |
| 최고 속도 | 14.60 m/s | 13.43 m/s (-8%) |
핵심 지표(이동거리·평균 속도)는 경량화 후에도 거의 그대로였고, 순간값인 최고 속도만 크게 흔들렸습니다.
실패에서 배운 것: 1차 측정은 입력 크기가 달라(정사각형 960×960 vs 직사각형 960×544) 같은 가중치인데 결과가 달랐습니다. 이상해서 원인을 찾아 조건을 맞춰 다시 쟀고, 1차 결과는 폐기하되 로그를 남겼습니다.
- 크롤링 도구: 대상 페이지(KATO, KTA)가 서버 렌더링이라 응답 HTML에 표가 그대로 있는 것을 확인하고
requests + BeautifulSoup을 선택했습니다. Selenium은 필요하지 않아 쓰지 않았습니다. - 정중한 크롤링: User-Agent에 프로젝트 정보 명시, 요청 간 지연, 하루 1회 수집.
- 변경 감지:
upsert()가 신규/변경/동일을 구분해 반환하고, 변경은 이력 테이블에 남깁니다. - 외부 텍스트는
textContent로만 삽입(XSS 방지), https 링크만 허용. - 프론트는 프레임워크 없이 단일 HTML/JS: 서비스 규모에 맞춰 단순하게 유지했습니다.
- 공은 검출까지만 했고, 프레임 간 추적(궤적)은 하지 않았습니다. 서비스에도 연결돼 있지 않습니다.
- 이동거리·속도는 참고용 추정값입니다. 실측값과 비교 검증하지 않았습니다.
- 코트 보정은 4점을 직접 클릭해야 합니다 (자동 인식 아님).
- INT8 모델은 실험만 했고 서비스에는 연결하지 않았습니다.
- Docker는 로컬 컨테이너 실행까지만 확인했고, 외부에서 접속 가능한 서버 배포는 아직입니다.
- 실험은 대부분 영상 1개, 측정 1회 기준이라 일반화하기 어렵습니다.
cd webapp/backend
pip install -r requirements.txt
uvicorn main:app --port 8000
http://localhost:8000 접속. 첫 실행 시 YOLO11n 가중치를 자동으로 내려받습니다. 세션이 메모리에 있어 워커는 1개로 실행합니다.
docker compose up --build
환경변수(선택): COURTVISION_DB, UPLOAD_DIR, RESULTS_DIR, YOLO_IMGSZ, COOKIE_SECURE(HTTPS 배포 시 1)
webapp/backend/ FastAPI 서버, 파이프라인, 크롤러, DB
webapp/frontend/ 단일 HTML/JS 프론트
src/ 실험·검증 스크립트 (공 학습/평가, INT8 등)
results/ 실험 기록
docs/ 스크린샷, 발표 자료
대회 정보는 한국테니스발전협의회(KATO)와 대한테니스협회(KTA)가 공개한 일정을 요약한 것이며, 정확한 내용은 각 대회의 원본 링크에서 확인해야 합니다. 화면의 사진은 분위기를 위한 AI 생성 이미지입니다.


