상태: 초안 (2026-10-13), 실측 반영. 정확도는 모두 OpenTTGames 실제 경기 12편(서버를 아는 랠리 275개)에서 직접 잰 값이고, 판마다 날짜와 조건을 붙였습니다. 학습형 이벤트 검출과 랠리 끝 판정 재설계 실험은 진행 중이라 결과를 적지 않았습니다. TT4D·PathCRF 수치는 논문 값입니다. 작성 규칙: 실측 데이터가 기본이고, 못 잰 것은 「미실측」으로 표시합니다.
목차
- 1. 궤적에서 이벤트로
- 2. 심판: 「누가 쳤나」를 보지 않는다
- 3. 66.9% 에서 93.1% 까지: 판마다 바꾼 것과 치른 것
- 4. 오차는 어느 층에서 나나: 정답을 넣어 보기
- 5. 앞으로: 3D 극값 규칙과 제약 Viterbi
- 참고
1. 궤적에서 이벤트로
1층이 주는 것은 프레임마다 (시각, x, y, 신뢰도) 하나입니다. 점수를 매기려면 이것을 사건의 열로 바꿔야 합니다. 엔진이 내는 사건은 넷입니다.
| 이벤트 | 정의(옆면 카메라) |
|---|---|
| bounce(바운드) | 테이블 윗면 띠 안에서 공의 화면 y 가 커지다가(내려옴) 작아지기 시작하는(올라감) 프레임. 단, 가로 방향을 유지할 때만 |
| net(네트) | 공이 네트 근처를 테이블 높이 위로 지나며 가로 속도가 0.7배 아래로 줄거나 뒤집힘 |
| out(아웃) | 바운드 뒤에 공이 테이블 끝 밖으로 나가 테이블 높이 아래로 떨어짐 |
| end(끝) | 공이 1초 동안 안 보이거나, 마지막 바운드 뒤 1.5초 동안 새 바운드가 없음 |
직관: 바운드는 V, 타구도 V
화면의 y 는 아래로 커집니다. 공이 테이블에 떨어졌다 튀어 오르면 y 는 커지다가 작아집니다. 이 「V」가 바운드의 기본 모양입니다. 문제는 테이블 위에서 라켓으로 치는 공도 V 를 그린다는 것입니다. 그래서 하나를 더 봅니다. 테이블에 튄 공은 계속 같은 방향으로 날아가지만, 라켓에 맞은 공은 되돌아갑니다. 실제 경기 12편에서 진짜 바운드의 97.9% 는 가로 방향을 유지했고, 타구 앞뒤 3프레임 안의 가짜 바운드는 73.8% 가 가로 방향이 뒤집혔습니다.
# ttscore/events.py detect_events() 에서 줄임
going_down = before.x is not None and c.y > before.y # 같은 y 가 이어지면 그 앞의 다른 y 와 비교
going_up = n.y < c.y
if (going_down and going_up and table.on_surface(c.x, c.y)
and c.t - last_bounce_t >= MIN_BOUNCE_GAP_S # 0.12초 — 같은 바운드를 두 번 세지 않게
and keeps_direction(i) # 앞뒤 가로 이동이 둘 다 2px 이상이고 반대면 타구
and not near_net_touch(i)): # 네트에 걸린 공의 V 는 버린다
emit(Event(c.t, "bounce", table.ball_side(c.x, c.y), conf=min(p.conf, c.conf, n.conf),
vx=vx_at(i), vx_in=vx_at(i, after=False)))
「같은 y 가 이어지면 그 앞의 다른 y 와 비교」라는 한 줄에도 사연이 있습니다. 히트맵 좌표는 정수 화소라 느린 구간에서 같은 값이 이어지는데, 「같음」을 「내려옴」으로 세면 올라가는 공에서 바운드가 생겼습니다.
상수마다 근거가 있다
규칙 엔진의 약점은 숫자를 감으로 정하기 쉽다는 것입니다. 그래서 상수마다 실측 근거를 코드 주석에 적었습니다.
| 상수 | 값 | 근거(실제 경기 12편, 2026-10-11) |
|---|---|---|
END_AFTER_BOUNCE_S |
1.5초 | 랠리 안 바운드 간격의 98%(1,087개 중 1,066개)가 1.5초 안 |
REVERSAL_MIN_PX |
2px | 진짜 바운드 97.9% 가 방향 유지, 타구 근처 가짜 73.8% 가 방향 뒤집힘 |
NET_SLOW_RATIO |
0.7 | 네트에 걸린 공(Extended 의 net 종료 83개)의 앞뒤 가로 속도 비는 −0.44…−0.03, 네트 위를 그냥 지난 공(OpenTTGames 의 net 1,268개)은 0.75…1.01 |
GAP_ZERO_S |
0.25초 | 메우지 못한 공백이 이만큼이면 신뢰도 0. 메우는 한계(6프레임) 너머는 「놓친 바운드가 없다」고 믿을 수 없다 |
마지막 줄과 짝을 이루는 것이 fill_gaps 입니다. 공이 몸에 가려 6프레임(60fps 에서 0.1초) 이하로 안 보이면 직선으로 메우고, 메운 점의 신뢰도를 양 끝의 절반으로 깎습니다. 그 구간에서 난 바운드가 자동으로 「확인 필요」로 가게 하려는 것입니다.
2. 심판: 「누가 쳤나」를 보지 않는다
심판 엔진(ttscore/referee.py)의 핵심 생각은 머리말 한 문장에 있습니다. 누가 쳤는지는 영상에서 직접 보지 않고 바운드 순서에서 추론합니다. 공이 S 쪽에 바운드했다면, 다음에는 S 쪽 선수가 쳐서 반대쪽에 한 번 떨어뜨려야 합니다. 그러지 못하면(같은 쪽에 또 바운드, 바운드 없이 나감, 아무 일 없이 끝남) S 쪽 선수가 집니다. 서브만 예외로, 서버 쪽에 한 번 그리고 리시버 쪽에 한 번 떨어져야 합니다.
이렇게 하면 「라켓 접촉」이라는, 영상에서 가장 찾기 어려운 사건 없이 점수를 매길 수 있습니다. 대신 바운드 하나를 놓치거나 가짜 바운드 하나가 끼면 「누가 칠 차례인가」가 통째로 어긋납니다. 그래서 심판의 나머지 규칙은 대부분 가짜 바운드를 거르는 장치입니다.
가로 속도라는 숫자 하나
바운드 이벤트에는 바운드 직후의 가로 속도 vx(테이블 길이/초)가 실려 있습니다. 화면 화소가 아니라 테이블 길이로 나눈 값이라 카메라 거리와 해상도에 상관없습니다. 심판은 이 숫자로 세 가지를 가립니다.
- 서브의 시작. 점수판 서버 쪽 바운드이면서 공이 네트 쪽으로 0.7 테이블/초 이상 움직여야 서브로 받습니다. 실제 서브 271개의 첫 바운드는 모두 1.07 이상이었고, 랠리 밖의 서버 쪽 공(서브 전 자기 쪽에 튀기기, 상대에게 넘겨주는 공)은 서브처럼 보인 4개를 빼면 0.61 이하였습니다.
- 랠리 중 가짜 바운드. 랠리 중 S 쪽의 진짜 바운드는 상대가 친 공이라 S 쪽 끝으로, 곧 네트에서 멀어지며 갑니다. 네트 쪽으로 가는 「바운드」는 네트 위를 지나는 공이나 타구를 잘못 읽은 것이라 버립니다. 이 규칙 하나로 실제 랠리 승자가 235 에서 241 로 늘었습니다.
- 넘겨주는 공. 리시버가 서버에게 공을 던져 주면 리시버 쪽에서 네트 쪽으로 튀고 서버 쪽에 떨어집니다. 「서브처럼」 보이지만 점수판 서버 반대쪽에서 시작하므로 받지 않습니다. 양쪽을 다 받는 변형은 실제 5경기의 가짜 판정을 3 에서 16 으로 늘렸습니다.
빠른 투 바운드는 미룬다
같은 쪽 두 바운드가 0.25초보다 가까우면 진짜 투 바운드인지, 바운드 직후의 타구를 바운드로 읽은 것인지 가를 수 없습니다. 실측에서 이런 판정 39개 중 16개(41%)가 틀렸습니다. 나머지 판정의 오답률은 8% 였습니다. 그래서 이 판정은 다음 이벤트까지 미룹니다. 다음 바운드가 반대쪽이면(리턴이 떨어짐) 둘째 바운드가 가짜였으니 버리고 랠리를 잇습니다. 이 규칙으로 판정이 바뀐 실제 랠리 9개가 모두 틀림에서 맞음이 되었습니다. 대가는 시간입니다. 그 판정은 최대 1.5초 늦게 나옵니다.
신뢰도와 「확인 필요」
랠리의 신뢰도는 그 랠리 이벤트들의 신뢰도 중 최솟값입니다. 이벤트의 신뢰도는 검출 신뢰도와 앞 공백의 신뢰도 중 작은 쪽입니다. 0.6 아래면 점수판이 「확인 필요」를 띄웁니다. 판정은 그대로 올리되 사람에게 한 번 보라고 하는 것입니다.
여기서 두 숫자를 구별해야 합니다.
- 확인 필요 비율: 사람이 봐야 하는 랠리의 비율. 높으면 귀찮다.
- 확인 없이 틀림: 「확인 필요」 표시 없이 틀린 판정. 사람이 믿고 넘어가므로 실제로 비용이 되는 오류다.
신뢰도를 보수적으로 깎으면 확인 없이 틀림은 줄지만 확인 필요가 늘고, 반대로 하면 그 반대입니다. 아래 역사는 이 두 숫자 사이의 줄다리기이기도 합니다.
3. 66.9% 에서 93.1% 까지: 판마다 바꾼 것과 치른 것
조건(모든 행 같음): OpenTTGames 실제 경기 12편, Extended OpenTTGames 랠리 종료 라벨, 서버를 아는 랠리 275개, BlurBall 좌표, 녹화본 채점. 날짜는 모두 2026-10-11–12 입니다.
| 판 | 바꾼 것 | 승자 | 확인 필요 | 확인 없이 틀림 | 판정 없음 | 바운드 정밀도 / 재현율 |
|---|---|---|---|---|---|---|
| 처음 | 공 좌표 → 공백 메우기 → 이벤트 → 심판 | 66.9% (184) | 76.4% | 2.9% | 10.2% | 0.789 / 0.971 |
| 2 | 타구≠바운드(가로 방향을 유지할 때만 바운드), 마지막 바운드 뒤 1.5초면 끝 | 79.6% (219) | 85.1% | 1.8% | 1.5% | 0.831 / 0.958 |
| 3 | 먼 변 허용 「8px + 공 반지름」(데이터셋 마스크 모서리) | 82.5% (227) | 85.5% | 1.5% | 0.7% | 0.825 / 0.965 |
| 4 | SAM 2.1 모서리(3편) | 84.7% (233) | 85.8% | 1.5% | 0.0% | 0.823 / 0.965 |
| 5 | 서브 인정 규칙, 랠리 중 네트 쪽 「바운드」 버림, 확인 필요 재설계 | 88.7% (244) | 41.5% | 5.1% | 0.4% | 0.823 / 0.965 |
| 6 | 네트 판정(서브 렛), 빠른 투 바운드 미루기 | 93.1% (256) | 41.1% | 3.6% | — | 0.832 / 0.963 |
판마다 치른 대가를 적으면 이렇습니다.
- 2판. 처음 판에서 틀린 91건 중 65건이 「타구를 바운드로 읽음」과 「랠리가 안 끝남」이었습니다. 이 둘을 고치자 판정 없음이 10.2% 에서 1.5% 로 줄었습니다. 대가는 확인 필요가 76.4% 에서 85.1% 로 늘어난 것과, 1.5초 끝 규칙이 로브가 많은 수비형 경기에서 점수를 일찍 낼 수 있다는 점입니다(1.5초는 98% 분위라 2% 는 그보다 깁니다).
- 3판. 바운드 순간 공 중심은 닿은 점보다 반지름만큼 위라, 먼 변 근처 바운드가 띠 밖으로 빠졌습니다. 띠를 넓히자 승자가 올랐지만 바운드 정밀도는 0.831 에서 0.825 로 조금 내려갔습니다. 띠가 넓으면 가짜도 더 들어옵니다.
- 4판. 모서리를 SAM 2.1 로 바꿨습니다. 대가는 경기 시작 때 분할 모델 한 번과 사람의 탭 두 번입니다.
- 5판. 여기서 성격이 바뀝니다. 「확인 필요」가 85% 면 자동 점수가 아니라 「검토용 초안」입니다. 서브 인정 규칙과 가짜 바운드 거르기로 판정을 더 맞히고 신뢰도 깎기를 다시 설계해 확인 필요를 85.8% 에서 41.5% 로 절반 넘게 줄였습니다. 대가로 확인 없이 틀림이 1.5%(4건)에서 5.1%(14건)로 늘었습니다. 사람이 보는 일은 크게 줄었지만, 사람이 믿고 넘긴 오답은 세 배가 됐습니다.
- 6판. 서브가 네트에 닿고 들어간 렛을 네트 판정으로 잡고, 빠른 투 바운드를 미뤘습니다. 승자가 93.1% 로 오르고 확인 없이 틀림이 5.1% 에서 3.6% 로 다시 줄었습니다. 대가는 셋입니다. 빠른 투 바운드 판정이 최대 1.5초 늦게 나오고, 실제 라벨 서브 하나가 렛으로 잘못 읽혔으며(확인 필요로 표시됨), 합성 시험의 잡음 조건 일부가 조금 나빠졌습니다(83.8 → 83.2%).
그리고 2026-10-13 기준선
6판의 93.1% 는 SAM 모서리로 쟀는데, 그 모서리 파일이 남아 있지 않습니다. 연구를 새로 시작하며 기준선을 데이터셋 마스크 모서리와 최신 엔진으로 다시 잡았고, 이번에는 실제로 배포된 실시간 경로(최근 2.5초를 표본마다 다시 돌리는 방식)로도 쟀습니다.
| 경로 | 승자 | 확인 없이 틀림 | 확인 필요 | 판정 없음 | 바운드 정밀도 / 재현율 |
|---|---|---|---|---|---|
| 녹화본 채점 | 91.3% (251) | 13 | 108 | 3 | 0.835 / 0.965 |
| 배포 실시간 경로 | 90.2% (248) | 15 | 129 | 2 | 0.800 / 0.970 |
같은 엔진, 같은 공 좌표인데 경로만 바꿔도 3랠리가 달라졌습니다. 녹화본은 랠리를 다 본 뒤 판단하고, 실시간은 최근 2.5초만 보고 그때그때 판단하기 때문입니다. 녹화본 채점은 실시간 성능을 부풀립니다. 그래서 앞으로의 모든 실험은 두 경로를 함께 적습니다.
4. 오차는 어느 층에서 나나: 정답을 넣어 보기
층을 나눈 덕분에 같은 채점을 세 가지 입력으로 돌릴 수 있습니다. 모델이 찾은 공, 데이터셋의 정답 공 좌표, 데이터셋의 정답 바운드입니다. 정답 공을 넣었는데도 틀리면 그 오차는 공 찾기 탓이 아닙니다. 정답 바운드를 넣었는데도 틀리면 이벤트 추출 탓도 아닙니다.
| 엔진 판 | 모델 공 | 정답 공 | 정답 바운드 | 범위 |
|---|---|---|---|---|
| 2판(79.6% 판) | 79.6% | 85.8% | 90.2% | 모델 공은 275랠리, 정답 입력은 정답이 있는 랠리 |
| 5판(88.7% 판) | 88.7% | 90.2% | 93.5% | 모델 공은 275랠리, 정답 입력은 정답이 있는 246랠리 |
| 2026-10-13 기준선 | 93.5% | 89.0% | 90.2% | 셋 다 같은 246랠리 |
2판과 5판에서는 정답을 넣을수록 올랐습니다. 그래서 「남은 오차의 대부분은 공 찾기가 아니라 이벤트 추출에서 난다」고 읽었고, 실제로 5판의 가짜 바운드 25건 중 11건이 타구를 바운드로 읽은 것, 5건이 네트 옆에서 쪽이 뒤집힌 것, 4건이 바운드를 놓친 것이었습니다.
그런데 최신 엔진에서는 모델 공이 정답 공과 정답 바운드보다 높습니다. 이 숫자들을 「상한」으로 읽으면 안 되는 이유가 여기서 드러납니다.
- 정답 공은 이벤트 근처에만 있다. OpenTTGames 의 공 표식은 이벤트 앞 4프레임부터 뒤 12프레임까지만 있습니다(7편). 그 밖은 비어 있으니, 정답 공으로는 랠리 끝(공이 사라짐)을 제대로 볼 수 없습니다.
- 정답 바운드에는 가로 속도가 없다. 엔진이 정한 서브·넘겨주기 규칙은 바운드의 가로 속도를 씁니다. 정답 바운드만 넣으면 그 규칙을 못 씁니다.
즉 정답 입력은 「완벽한 입력」이 아니라 「다른 입력」입니다. 엔진이 모델 공의 성질(이벤트 밖까지 이어지는 좌표, 가로 속도)에 맞춰 다듬어진 만큼, 정답 입력과의 비교는 층별 오차를 가르는 도구로만 쓰고 상한으로 쓰지 않습니다.
지금 남은 오차
2026-10-13 기준선(녹화본, 275랠리)에서 틀리거나 판정이 없던 27건의 유형입니다.
| 유형 | 건수 |
|---|---|
| 가짜 바운드 | 7 |
| 바운드 놓침 | 4 |
| 끝 판정의 쪽 어긋남 | 4 |
| 끝(end)이 오지 않음 | 3 |
| 타구를 바운드로 읽음 | 2 |
| 쪽 뒤집힘 · 조기 판정 · 네트 옆 쪽 뒤집힘 · 아웃 오판 | 각 1 |
이벤트(바운드) 쪽이 27건 중 14건, 랠리 끝 쪽이 8건입니다. 득점 원인별로 보면 아웃 92.8%(138개 중 128), 네트 92.6%(81개 중 75), 위너 84.4%(32개 중 27), 투 바운드 75.0%(4개 중 3)였습니다.
나쁜 조건에서
시험 7경기(57랠리)에 열화를 입혀 쟀습니다(녹화본 채점, 승자 맞음 수).
| 조건 | 맞음 / 57 | 확인 없이 틀림 |
|---|---|---|
| 깨끗함 | 50 | 3 |
| 어두움 | 35 | 2 |
| 30fps(엔진도 30fps) | 41 | 9 |
| 저해상도 320×180 | 42 | 2 |
| 긴 노출 | 46 | 2 |
| 휴대폰 360p 60fps 500kbps 압축 | 50 | 1 |
30fps 는 공 찾기가 거의 떨어지지 않는데도(2편) 점수가 크게 떨어졌고, 확인 없이 틀림이 가장 많았습니다. 바운드 순간의 시간 해상도가 반이 되기 때문입니다. 이 엔진은 오답을 숨기는 방향으로 무너집니다. 그래서 촬영 안내는 60fps 를 권합니다.
5. 앞으로: 3D 극값 규칙과 제약 Viterbi
TT4D 식 3D 극값 규칙
지금 규칙은 2D 화면에서 「V 자」를 찾습니다. 화면의 y 에는 공의 높이와 카메라에서의 거리가 섞여 있어서, 시점이 바뀌면 규칙이 흔들립니다(3편의 끝 쪽 카메라 문제).
TT4D(arXiv 2605.01234, ACM MM 2026)는 단일 카메라 중계 영상에서 공 궤적을 3D 로 올린 뒤 사건을 찾습니다. 규칙은 놀랄 만큼 단순합니다.
- 바운드 = 공 높이 z 의 극소.
- 타구 = 테이블 길이 방향 x 의 극값(공이 되돌아가는 자리).
- 같은 종류 극값 사이 0.2초 이상, 극값 위치는 네트에서 0.3m 이상.
3D 정답이 있는 데이터(합성과 TT3D)에서 ±2프레임 기준 바운드 F1 0.92, 타구 F1 0.83 을 보고했습니다(논문, 정밀도·재현율은 따로 없음). 타구가 낮은 이유로 저자들은 선수 근처에서 공이 잘 안 보이는 것을 꼽습니다.
우리 엔진의 2D 규칙과 비교하면 같은 생각의 3D 판입니다. 「V 자」는 z 의 극소이고, 「가로 방향이 뒤집히면 타구」는 x 의 극값입니다. 3D 로 올리면 시점과 상관없이 같은 규칙이 되고, 높이를 아니 공중의 공을 평면에 잘못 옮기는 문제도 사라집니다. 대신 단일 카메라에서 3D 를 복원하는 단계가 새로 필요합니다. 이 복원을 우리 옆면 휴대폰 영상에서 할 수 있는지가 다음 실험의 질문입니다(진행 중, 미실측). 같은 계열의 공개 구현 일부는 GPL 이라 코드를 가져오지 않고 방법만 구현할 계획입니다.
제약 Viterbi: 규칙을 「허용 전이」로
지금 엔진은 이벤트를 하나씩 받아 그때그때 결정합니다. 가짜 바운드 하나가 들어오면 「다음은 누가 칠 차례」가 뒤집히고, 그 뒤의 판단이 모두 어긋납니다. 사람은 이럴 때 뒤를 보고 앞을 고칩니다. 「이 바운드가 진짜라면 그 뒤 순서가 규칙상 불가능하다. 그러니 이 바운드는 가짜였을 것이다.」
이 생각을 계산으로 옮긴 것이 제약 Viterbi 디코딩입니다.
- 시각마다 「서버 쪽 바운드」, 「리시버 쪽 바운드」, 「타구」, 「아무 일 없음」 같은 상태 후보에 점수를 매깁니다. 점수는 공·소리·자세 같은 신호에서 나옵니다.
- 탁구 규칙을 허용된 전이로 적습니다. 서브 → 서버 쪽 바운드 → 리시버 쪽 바운드 → 타구 → 서버 쪽 바운드 … 는 허용, 같은 쪽 바운드 두 번 뒤 랠리 계속은 금지(점수가 나야 함) 같은 식입니다. 금지 전이의 점수는 매우 낮게(사실상 −∞) 둡니다.
- Viterbi 알고리즘으로 점수 합이 가장 큰, 규칙상 가능한 경로 하나를 찾습니다. 동적 계획법이라 랠리 길이에 비례하는 시간으로 끝납니다.
- 가장 좋은 경로와 두 번째 경로의 점수 차가 작으면, 그 랠리는 「확인 필요」입니다. 지금의 「신뢰도 최솟값」보다 근거가 분명한 신뢰도가 됩니다.
축구에서 같은 생각을 보인 PathCRF(arXiv 2602.12080, KDD 2026)는 모델은 그대로 두고 디코딩만 바꿔 이벤트 F1 을 65.34% 에서 73.14% 로, 규칙 위반을 2.80% 에서 0% 로 만들었습니다(논문). 탁구는 규칙이 축구보다 훨씬 엄격하므로 효과가 더 클 수 있다고 기대합니다(추정). PathCRF 코드는 MPL-2.0 이라 가져오지 않고, 방법을 우리 엔진에 맞게 새로 구현하려 합니다.
진행 중인 실험
다음 실험은 결과가 아직 없어 적지 않습니다(진행 중).
- 공 궤적의 짧은 창(뒤로 16프레임, 앞으로 최대 200ms)을 보는 작은 1D 합성곱 모델(파라미터 약 1.8만 개)로 바운드·타구·네트를 내고, 규칙 바운드를 거르거나 보충하기. 앞보기 길이를 바꿔 가며 정확도와 지연의 균형을 잰다.
- 랠리 끝 판정의 「공 사라짐」 문턱과 고정 지연 확정을 바꿔 판정 지연을 줄이기.
- 위 두 가지와 제약 Viterbi 를 결합하기.
실험의 원칙은 정해 두었습니다. 하이퍼파라미터는 학습에 쓰지 않은 검증 경기에서만 고르고, 시험 7경기는 마지막에 한 번만 봅니다. 그리고 규칙 엔진은 시험 경기를 보며 다듬어졌으므로 규칙 기준선의 시험 점수가 낙관적이라는 점을, 새 방법과 비교할 때 함께 적습니다.
티티 한마디. 심판은 라켓을 안 봐요. 공이 어디에 떨어졌는지만 세어도 거의 다 맞힐 수 있어요. 대신 가짜 바운드 하나에 순서가 통째로 흔들리니까, 규칙 대부분은 가짜를 거르는 데 쓰여요.
참고
- TT4D: https://arxiv.org/abs/2605.01234
- PathCRF: https://arxiv.org/abs/2602.12080
- Extended OpenTTGames(랠리 종료 원인 라벨): https://arxiv.org/abs/2512.19327
- 우리 측정: 저장소
docs/tt/PERFORMANCE.md2.2·2.6·2.7, 실험 원장 E00(2026-10-13), 코드ttscore/events.py·referee.py·track.py