상태: 초안 (2026-10-13), 실측 반영. 우리 수치는 2026-10-11–12 에 서버 GPU(24GB급)에서 BlurBall 사전학습 가중치를 미세조정 없이 돌려 잰 값입니다. 논문 수치는 「논문」으로 따로 표시했습니다. 인용한 원 저장소 코드는 커밋을 적었고 짧게 줄였습니다. 작성 규칙: 실측 데이터가 기본이고, 못 잰 것은 「미실측」으로 표시합니다.
목차
- 1. 왜 어려운가
- 2. 직관: 좌표를 맞히지 말고 지도를 그린다
- 3. 계보: TrackNet → TTNet → TrackNetV2 → WASB → BlurBall
- 4. HRNet: 해상도를 끝까지 지킨다
- 5. 우리 코드에서는
- 6. 실측: 얼마나 찾나
- 7. 왜 BlurBall 인가, 그리고 다음
- 참고
1. 왜 어려운가
탁구공은 지름 4cm 입니다. 옆면에서 테이블 전체가 보이게 찍은 1080p 연구실 영상(TT3D)에서 잰 공 지름은 중앙 11–12px 였고, 모델에 넣으려고 512×288 로 줄이면 3px 남짓이 됩니다. 게다가 빠릅니다. 시속 70km(약 20m/s)로 날아가는 공은 노출 1/120초 동안 17cm, 1/60초 동안 33cm 를 지나갑니다. 공 지름의 4–8배 길이로 번진 자국이 찍힌다는 뜻입니다. 그 위에 선수의 손과 라켓이 공을 가리고, 천장 형광등과 흰 신발이 공처럼 보입니다.
그래서 일반 물체 검출기(사람·자동차를 찾는 상자 검출기)를 그대로 쓰면 잘 안 됩니다. 이 분야에는 공만 찾는 계보가 따로 있고, TT Score 가 쓰는 BlurBall 은 그 끝에 있습니다.
2. 직관: 좌표를 맞히지 말고 지도를 그린다
공 찾기 모델은 「공은 (x, y) 에 있다」고 숫자 두 개를 바로 내지 않습니다. 대신 입력과 같은 크기의 흑백 그림, 곧 히트맵을 그립니다. 화소마다 「여기가 공일 확률」을 0–1 로 칠한 지도입니다. 정답도 공 자리에 작은 가우시안 언덕을 그린 지도로 만들어 화소마다 비교합니다.
이렇게 하는 이유는 셋입니다.
- 위치 오차를 화소마다 배운다. 좌표 두 개만 맞히면 「얼마나 틀렸나」라는 신호가 숫자 두 개뿐이지만, 지도는 수만 화소가 각자 학습 신호를 줍니다. 작은 물체일수록 이 차이가 큽니다.
- 「공 없음」이 자연스럽다. 지도 전체가 낮으면 공이 없는 것입니다. 좌표 회귀는 공이 없을 때도 어딘가를 가리킵니다.
- 후보가 여럿일 수 있다. 공과 형광등이 둘 다 밝게 칠해지면 둘 다 후보로 남기고, 뒤에서 궤적이 이어지는 쪽을 고를 수 있습니다.
두 번째 생각은 여러 프레임을 함께 넣는 것입니다. 한 장만 보면 흰 점이 공인지 형광등인지 모릅니다. 연속 3장을 보면 움직이는 것과 서 있는 것이 갈립니다. 사람이 정지 화면보다 영상에서 공을 더 쉽게 찾는 것과 같습니다.
3. 계보: TrackNet → TTNet → TrackNetV2 → WASB → BlurBall
TrackNet(2019): 히트맵과 여러 프레임
테니스 중계에서 공을 찾으려고 만든 모델입니다(arXiv 1907.03698). VGG-16 의 앞 13층으로 특징을 뽑고, 디컨볼루션으로 다시 키워 640×360 히트맵을 냅니다. 한 장을 넣는 판(Model I)과 연속 3장을 넣는 판(Model II)을 비교했는데, 정밀도·재현율이 95.7% / 89.6% 에서 99.8% / 96.6% 로 올랐습니다(논문, 같은 경기를 7:3 으로 나눈 평가). 여러 프레임의 효과를 숫자로 보인 첫 사례입니다.
디코딩은 단순합니다. 히트맵을 128(0–255 중)에서 잘라 흑백으로 만들고, 원 검출(Hough)로 원을 찾아 정확히 하나일 때만 그 중심을 공으로 냅니다. 같은 논문에서 경기를 섞지 않은 10겹 교차 검증은 재현율 75.7% 였습니다. 같은 경기 안에서 잘 되던 모델이 처음 보는 경기에서는 크게 떨어진다는, 이 분야에서 반복되는 교훈이 여기서 이미 나옵니다.
라벨 규칙도 기억해 둘 만합니다. TrackNet 은 공이 번져 있으면 자국의 **최신 위치(앞쪽 끝)**에 점을 찍었습니다. BlurBall 이 바로 이 규칙을 문제 삼습니다.
TTNet(2020): 탁구, 120fps, 그리고 OpenTTGames
탁구 경기를 실시간으로 분석하려고 만든 모델이고(arXiv 2004.09927), 우리가 평가에 쓰는 OpenTTGames 데이터셋이 이 논문과 함께 공개되었습니다. 120fps 풀HD 영상에서 연속 9장을 채널로 쌓아(입력 27채널) 넣습니다. 전체 화면을 320×128 로 줄여 대략의 자리를 찾고, 원본에서 그 자리를 다시 잘라 정밀하게 찾는 2단 구조입니다.
출력이 흥미롭습니다. 2D 히트맵이 아니라 길이 320 과 128 인 1D 벡터 두 개(x 축, y 축)입니다. 2D 지도를 두 축의 1D 분포로 쪼개면 계산이 줄어듭니다. 4편의 RTMPose(SimCC)도 같은 생각을 씁니다. 논문은 이벤트 인식 97.0%, 공 검출 97.5%, 전체 파이프라인 6.0ms(데스크톱급 GPU 한 장)를 보고했습니다(논문). 우리 60fps 휴대폰 영상에서는 미실측입니다.
TrackNetV2(2020): 3장 넣고 3장 받기
배드민턴용으로 고친 판입니다. U-Net 구조로 바꾸고, 3장을 넣어 3장의 히트맵을 한꺼번에 받는(MIMO) 방식으로 속도를 2.6 에서 31.8 FPS 로 올렸습니다(논문 초록). 입력 크기 288×512 와 「3장 넣고 3장 받기」는 이후 WASB 와 BlurBall 이 그대로 물려받습니다.
WASB(2023): 작은 HRNet 과 덩어리 후처리
축구·테니스·배드민턴·배구·농구 다섯 종목에서 두루 쓰려고 만든 기준 모델입니다(BMVC 2023, arXiv 2311.05237, 코드 MIT). 탁구는 그 다섯에 없습니다. 몸통은 작은 HRNet(아래 4절)이고 파라미터는 150만 개입니다. 입력은 (B, 9, 288, 512), 곧 RGB 3채널 × 3프레임이고, 출력은 시그모이드를 거친 히트맵 3장입니다.
후처리는 TrackNet 의 원 검출 대신 이어진 덩어리 찾기를 씁니다. 원 저장소의 해당 부분입니다.
# nttcom/WASB-SBDT src/detectors/postprocessor.py @923462ca, 49–59행(MIT, NTT Communications)
if np.max(hm) > self._score_threshold:
th, hm_th = cv2.threshold(hm, self._score_threshold, 1, cv2.THRESH_BINARY)
n_labels, labels = cv2.connectedComponents(hm_th.astype(np.uint8))
for m in range(1,n_labels):
ys, xs = np.where( labels==m )
ws = hm[ys, xs]
if self._use_hm_weight:
score = ws.sum()
x = np.sum( np.array(xs) * ws ) / np.sum(ws)
y = np.sum( np.array(ys) * ws ) / np.sum(ws)
0.5 에서 잘라 덩어리마다 히트맵 값으로 가중한 중심을 위치로, 값의 합을 점수로 냅니다. 합을 쓰면 크고 진한 덩어리가 이깁니다. 프레임 사이를 잇는 추적기는 논문에서 「위치 + 속도 + 가속도/2」로 다음 자리를 예측한다고 설명하지만, 공개 코드는 예측을 쓰지 않고 직전 위치에서 300px(원본 기준) 안의 후보 중 점수가 가장 높은 것을 고릅니다. 논문과 코드가 다를 때는 코드가 실제 동작입니다.
BlurBall(2025–2026): 번짐의 가운데
탁구용으로 WASB 를 고친 모델입니다(arXiv 2509.18387, CVPRW 2026, 코드 MIT). 바뀐 것은 주로 라벨입니다.
- 공이 번져 있으면 자국의 가운데에 점을 찍습니다. 끝점(TrackNet 방식)은 셔터가 닫히는 순간에 따라 앞뒤로 흔들리지만, 가운데는 노출의 중간 시각이라 일정합니다. 프레임마다 같은 「시각의 공」을 가리키니 궤적이 매끄러워집니다.
- 번짐의 방향 θ 와 반길이 l 도 함께 라벨로 답니다. 정답 히트맵은 점 하나가 아니라 자국 전체를 덮는 길쭉한 언덕입니다.
- 몸통의 블록마다 채널 주의(SE 블록)를 넣고, 손실을 quality focal loss 로 바꿨습니다.
입력과 출력 모양은 WASB 와 같습니다((B, 9, 288, 512) → 히트맵 3장). 번짐의 길이·각도는 별도 출력 머리가 아니라 히트맵 덩어리의 모양에서 후처리로 읽습니다.
# cogsys-tuebingen/BlurBall src/detectors/blurball_postprocessor.py @2f0f5496, 48–50·60–71행에서 줄임(MIT)
angle = 0.5 * np.arctan2(2 * moments["mu11"], moments["mu20"] - moments["mu02"])
mean, eigenvectors = cv2.PCACompute(coords.astype(np.float32), mean=None)
principal_axis = eigenvectors[0] # 가장 긴 축
projected = np.dot(coords - mean, principal_axis)
l = projected.max() - projected.min() # 79행에서 l / 2 를 반길이로 저장
여기에도 논문과 코드의 차이가 둘 있습니다. 논문은 덩어리의 신뢰도를 히트맵의 평균이라고 설명하지만 코드는 WASB 처럼 합입니다. 각도도 논문은 주축 벡터로, 코드는 영상 모멘트로 구합니다.
논문 수치(자체 탁구 시험셋, 288×512, 데스크톱급 GPU)는 F1 96.52, 79 FPS 로 자주 함께 인용되는데, 두 숫자는 다른 설정의 값입니다. 96.52 는 창을 한 프레임씩 미는 1-step 추론, 79 FPS 는 세 프레임씩 미는 3-step 추론입니다(3-step F1 은 96.16). 1-step 의 FPS 는 논문 표에 없습니다.
4. HRNet: 해상도를 끝까지 지킨다
WASB 와 BlurBall 의 몸통은 HRNet(CVPR 2019, arXiv 1902.09212) 계열입니다. 보통의 분류 모델은 그림을 1/2, 1/4, … 1/32 로 줄여 가며 「무엇인가」를 압축합니다. 3px 짜리 공은 1/8 만 줄여도 화소 하나보다 작아집니다. 다시 키워도 위치가 흐려집니다.
HRNet 은 원래 해상도의 갈래를 끝까지 유지하고, 낮은 해상도의 갈래를 옆에 하나씩 덧붙여 나란히 돌리며, 갈래끼리 정보를 반복해서 섞습니다. 낮은 해상도 갈래는 「이 근처에 사람이 있고 테이블이 있다」는 넓은 맥락을, 높은 해상도 갈래는 「정확히 이 화소」를 맡습니다. 우리가 쓰는 설정은 갈래 4개, 채널 16·32·64·128 이고, 처음 줄기(stem)의 보폭을 1 로 두어 입력 해상도 그대로 갈래에 넣습니다(우리 코드의 "STEM": {"STRIDES": [1, 1]}).
5. 우리 코드에서는
모델 코드(MIT)는 저장소에 넣지 않고 서버의 저장 공간에 두고 읽습니다. 남의 코드를 들이면 그 사본을 우리가 따라 고쳐야 하기 때문입니다. 저장소에는 그 둘레만 있습니다(live/blurball.py, batch/model_blurball.py).
가중치 검증. 내려받은 가중치는 sha256 을 대조한 뒤 torch.load(weights_only=True) 로만 읽습니다. PyTorch 체크포인트는 피클이라 임의 코드를 실행할 수 있습니다. 대조가 틀리거나 weights_only 로 못 읽으면 기동을 멈춥니다.
전처리. RGB, 512×288, ImageNet 평균·표준편차로 정규화합니다. 브라우저가 이미 512×288 로 줄여 보내므로 서버는 크기를 바꾸지 않습니다. 한 번 크게 당한 일이 있습니다. 디코딩한 배열을 transpose 로 축만 바꿔 쌓았더니 텐서가 channels_last 배치가 되어, cuDNN 이 예열 때와 다른 계산 경로를 탔습니다. 예열 뒤 첫 실제 프레임이 12.5초 걸렸습니다. np.ascontiguousarray 한 줄로 고쳤습니다.
후처리. 원 저장소와 같은 덩어리 찾기에, 화면 표시용 값을 하나 더 냅니다.
# live/blurball.py blobs() 에서 줄임
_, th = cv2.threshold(hm, SCORE_T, 1, cv2.THRESH_BINARY) # SCORE_T = 0.5
n, labels = cv2.connectedComponents(th.astype(np.uint8))
for m in range(1, n):
ys, xs = np.where(labels == m); ws = hm[ys, xs]; sw = float(ws.sum())
x, y = float((xs * ws).sum() / sw), float((ys * ws).sum() / sw)
pts = np.stack([xs, ys], 1).astype(np.float64); pts -= pts.mean(0)
vals, vecs = np.linalg.eigh(np.cov(pts.T)) # 주축 = 번짐 방향
pm = pts @ vecs[:, 1]; length = float(pm.max() - pm.min()) / 2.0
out.append({"x": x, "y": y, "score": sw, "peak": float(ws.max()), "len": length, ...})
score(합)는 추적기가 후보를 고를 때, peak(최댓값, 0–1)는 화면의 신뢰도 표시에 씁니다. 합은 덩어리 크기에 따라 1 을 넘으므로 사람에게 보여 줄 값으로는 맞지 않습니다. 각도는 원 저장소와 달리 공분산의 주축으로 구합니다.
실시간과 녹화본의 차이. 실시간은 새 프레임마다 최근 3장 창을 돌려 마지막 히트맵만 씁니다(OUT_INDEX = 2). 녹화본은 겹치지 않는 3장 창으로 세 장을 모두 씁니다. 그래서 실시간은 프레임당 GPU 일이 세 배입니다(1편 3절).
6. 실측: 얼마나 찾나
기준선(2026-10-11)
조건: OpenTTGames 시험 7경기, 원본 120fps 의 짝수 프레임만(60fps), 표식이 있는 프레임 3,483장만, 512×288 에서 정답과 5px 안이면 맞음. 가중치는 사전학습 그대로.
| 지표 | 값 |
|---|---|
| 정밀도 / 재현율 | 0.9817 / 0.9785 |
| 위치 오차(RMSE) | 0.594px(512 기준), 원본 1920 기준 2.23px |
| 바운드 앞뒤 ±2프레임의 재현율 | 0.988 |
| 시간(창 하나, 녹화본 방식) | 프레임당 3.42ms(293fps), 8창 묶음 2.81ms |
| 시간(창 하나, 실시간 방식, 빈 카드) | 10.5ms(fp32), 8.0ms(fp16) |
| GPU 메모리(torch 최대 예약량) | fp32 창 1개 0.35GB, 8개 2.6GB, 16개 5.2GB. fp16 창 1개 0.22GB |
| 모델 | 가중치 5.8MB, 149만 파라미터 |
약점은 세 군데였습니다.
| 공의 상태 | 재현율 |
|---|---|
| 거의 멈춘 공(60fps 프레임 사이 원본 기준 10px 미만) | 0.790 |
| 선수와 겹친 공(데이터셋의 사람 마스크와 겹침) | 0.863 |
| 아주 빠른 공(80px 이상) | 0.855 |
멈춘 공은 대부분 서브 전 손에 든 공이라 점수에는 영향이 작습니다. 겹친 공과 빠른 공이 실제로 아픈 자리입니다.
test_7 의 정밀도는 0.882 로 혼자 낮았는데, 모델 탓이 아니었습니다. 이 경기에는 「공 없음」 표식이 104곳 있는데, 그중 41곳은 공이 실제로 보입니다(6장을 눈으로 확인). 모델이 맞게 찾은 공이 「없는 공을 찾았다」고 채점된 것입니다. 이 41곳을 빼면 0.9937 입니다. 데이터셋 이야기는 7편에서 더 합니다.
다른 모델과 비교(2026-10-11–12)
BlurBall 저자가 공개한 비교 모델 가중치로 같은 조건에서 쟀습니다.
| 모델 | 정밀도 / 재현율 | 겹친 공 | 멈춘 공 | 빠른 공 |
|---|---|---|---|---|
| BlurBall(문턱 0.5) | 0.982 / 0.979 | 0.863 | 0.790 | 0.855 |
| BlurBall(문턱 0.3) | 0.980 / 0.985 | 0.903 | 0.862 | 0.868 |
| WASB, 가운데 라벨로 학습 | 0.985 / 0.964 | 0.879 | 0.707 | 0.855 |
| WASB, 끝점 라벨로 학습 | 0.976 / 0.934 | 0.774 | 0.696 | 0.513 |
| MonoTrack | 0.983 / 0.949 | 0.798 | 0.646 | 0.829 |
| TrackNetV2 | 0.992 / 0.822 | 0.669 | 0.398 | 0.566 |
| ResTrackNetV2 | 0.993 / 0.712 | 0.532 | 0.160 | 0.237 |
| BlurBall + WASB, 히트맵 최댓값으로 합침(문턱 0.3) | 0.980 / 0.989 | 0.935 | 0.895 | 0.882 |
읽을 것이 셋입니다.
- 같은 WASB 라도 가운데 라벨로 배운 쪽이 끝점 라벨보다 낫다. 빠른 공에서 0.855 대 0.513 입니다. 번짐 가운데 라벨이 효과가 있다는 직접 근거입니다(OpenTTGames 표식이 가운데 쪽에 가까운 것도 한몫합니다).
- 오른 몫의 대부분은 모델을 더해서가 아니라 문턱을 낮춰서 생겼다. BlurBall 단독도 문턱만 0.3 으로 내리면 재현율이 0.979 에서 0.985 로 오릅니다. WASB 를 더해 생긴 순수 이득은 재현율 +0.35%p 입니다.
- 깨끗한 영상에서는 공을 더 찾아도 점수가 오르지 않았다. 시험 7경기 57랠리의 승자 맞음이 BlurBall 53, 문턱 0.3 은 52, 두 모델 합침도 52 였습니다. 문턱을 낮추면 가짜 바운드도 늘기 때문입니다.
나쁜 조건에서(2026-10-11–12)
같은 시험 영상에 결정적인 열화를 입혀 쟀습니다(공 검출 재현율).
| 조건 | BlurBall 단독 | BlurBall + WASB 합침(0.3) |
|---|---|---|
| 깨끗함 | 0.979 | 0.989 |
| 어두움 + 센서 잡음 | 0.685 | 0.820 |
| 저해상도(320×180 으로 줄였다 키움) | 0.898 | 0.957 |
| 긴 노출(1/60초 번짐) | 0.841 | 0.869 |
| 공 가림(살색 타원) | 0.842 | 0.868 |
| 공 닮은 방해물 | 0.969 | 0.927 |
| 30fps | 0.984 | 0.985 |
어두운 곳에서는 두 모델을 합치는 것이 크게 도왔지만(시험 57랠리 승자 36 → 45), 공 닮은 방해물 앞에서는 WASB 가 가짜를 많이 내서 오히려 나빠졌습니다. 언제나 이기는 조합은 없었습니다. 그리고 합치면 GPU 시간이 두 배(창 하나 10.5 → 20.0ms)입니다. 그래서 지금은 BlurBall 하나만 상시로 돌리고, 「공 신호가 약할 때만 둘째 모델을 켜는」 길을 실험 중입니다(진행 중).
OpenTTGames 밖에서(2026-10-11)
| 영상 | 정밀도 / 재현율 | 무엇이 틀렸나 |
|---|---|---|
| OpenTTGames 시험(기준) | 0.982 / 0.979 | — |
| TT3D 연구실, 옆면 50fps | 0.929 / 0.860 | 놓친 것 대부분이 서브 전 손에 든 공 |
| TT3D 연구실, 비스듬한 60fps | 0.894 / 0.820 | 먼 오답 20장 중 15장이 천장 형광등. 화면 위 띠를 빼면 정밀도 0.956 |
| LATTE-MV 국제 경기 중계, 30fps 640×360 | 0.815 / 0.722(일치도) | 불일치 40장 중 25장은 같은 번짐 자국의 다른 끝을 가리킨 것 |
마지막 줄이 이 글의 주제를 그대로 보여 줍니다. LATTE-MV 의 공 좌표는 사람 라벨이 아니라 그 데이터 파이프라인의 추적 결과이고, 자국의 한쪽 끝을 가리킵니다. BlurBall 은 가운데를 가리킵니다. 둘이 「다르다」고 채점된 것의 62% 는 사실 같은 공이었습니다. 라벨 규칙이 다르면 같은 모델도 다르게 채점됩니다.
7. 왜 BlurBall 인가, 그리고 다음
- 60fps 휴대폰 영상에 맞는 라벨. 셔터가 길수록 번짐이 길어지고, 그때 가운데 라벨의 이점이 커집니다.
- 작고 빠르다. 149만 파라미터, 창 하나 10.5ms. 같은 카드에서 세션 여럿을 받을 여지가 있습니다.
- 코드가 MIT. 다만 가중치와 학습 데이터에는 라이선스 표기가 없습니다. 논문 부록은 「크리에이티브 커먼즈 라이선스의 공개 온라인 영상」으로 데이터를 만들었다고 적었고, 학습 영상 목록이 공개되지 않아 OpenTTGames 와 겹치는지 모릅니다. 그래서 우리 실측을 일반화 성능으로 읽지 않습니다.
다음 단계로는 가림에 강한 모델(가림 프레임에서만 켜는 조건부 모델)과 휴대폰 영상으로 하는 미세조정을 조사·실험하고 있습니다. 결과가 나오면 이 글에 덧붙입니다(진행 중). 휴대폰 영상의 사람 라벨 정답셋이 아직 없다는 것이 가장 큰 숙제입니다.
티티 한마디. 같은 WASB 모델인데, 번진 공의 끝 대신 「가운데」를 찍어 가르쳤더니 빠른 공 재현율이 0.51 에서 0.86 이 됐어요. 라벨 약속 하나의 힘이에요.
원리 더 읽기. 히트맵과 해상도를 끝까지 지키는 구조는 비전 모델 읽기 1, 3편: U-Net 과 HRNet, 히트맵 손실과 학습 기법은 4편: 손실 함수와 학습 기법에 원 논문과 함께 정리했습니다.
참고
- TrackNet: https://arxiv.org/abs/1907.03698
- TTNet: https://arxiv.org/abs/2004.09927
- WASB: https://arxiv.org/abs/2311.05237, https://github.com/nttcom/WASB-SBDT (MIT)
- BlurBall: https://arxiv.org/abs/2509.18387, https://github.com/cogsys-tuebingen/BlurBall (코드 MIT, 가중치·데이터 라이선스 표기 없음)
- HRNet: https://arxiv.org/abs/1902.09212
- 우리 측정: 저장소
docs/tt/PERFORMANCE.md2.1·2.8·2.9, 실시간 데모 설계 문서 9절