TT Lab
요금제

한국어

시작하기
블로그

블로그

탁구대 찾기: SAM 2.1, EfficientTAM, SAM 3 와 네 모서리에서 호모그래피까지

상태: 초안 (2026-10-13), 실측 반영. SAM 2.1 tiny 의 정확도·속도는 2026-10-11–12 에 서버 GPU(다른 작업과 나눠 쓰던 때)와 CPU 임시 파드에서 잰 값입니다. EfficientTAM 과 SAM 3 는 우리 환경에서 돌려 보지 않았고(미실측) 논문과 공식 저장소의 수치를 조건과 함께 옮겼습니다. 탁구대를 자동으로 찾는 기능은 다른 작업에서 만들고 있어(진행 중) 결과가 나오면 덧붙입니다. 작성 규칙: 실측 데이터가 기본이고, 못 잰 것은 「미실측」으로 표시합니다.

목차

1. 왜 모서리 네 개가 필요한가

공 좌표만으로는 점수를 매길 수 없습니다. 「공이 테이블에 닿았나」, 「어느 쪽 반에 닿았나」, 「테이블 끝을 넘어갔나」를 알려면 테이블이 화면 어디에 있는지 알아야 합니다. 탁구대는 크기가 규정으로 정해진 직사각형입니다. 길이 2.74m, 폭 1.525m, 네트 높이 15.25cm 입니다. 그래서 화면에서 네 모서리만 찾으면 나머지는 기하로 계산할 수 있습니다.

다행히 TT Score 는 카메라를 삼각대에 고정한다고 가정합니다. 탁구대는 움직이지 않으니 경기 시작 때 한 번 찾으면 됩니다. 프레임마다 돌아야 하는 공 찾기와 달리, 여기서는 수백 ms 가 걸려도 괜찮습니다. 이 차이가 모델 선택을 바꿉니다.

지금 실시간 화면은 사람이 네 모서리를 직접 탭합니다. 가장 확실하고 계산이 들지 않습니다. 여기에 테이블 두 반을 한 번씩 더 탭하면, 비스듬한 카메라에서도 어느 변이 끝(엔드라인)인지 엔진이 고를 수 있습니다(아래 5절). 이 글의 모델들은 「탭을 줄이거나 없애는 길」입니다.

2. 프롬프트 분할: 클릭 한 번이 질문이 된다

SAM 의 생각

Segment Anything(SAM) 계열은 「무엇을 칠할지」를 학습 때 정하지 않습니다. 대신 사람이 준 프롬프트(점 클릭, 상자, 대략의 마스크)를 받아 그 자리의 물체를 칠합니다. 구조는 세 부분입니다.

  1. 이미지 인코더: 무거운 부분. 그림 한 장을 한 번만 돌려 특징 지도를 만듭니다.
  2. 프롬프트 인코더: 클릭 좌표나 상자를 작은 벡터로 바꿉니다.
  3. 마스크 디코더: 가벼운 부분. 특징 지도와 프롬프트를 받아 마스크를 냅니다.

무거운 인코더를 한 번만 돌리고 가벼운 디코더를 클릭마다 다시 돌리므로, 사람이 클릭을 바꿔 가며 고치는 화면에 잘 맞습니다. TT Score 에서는 사람이 탁구대를 짚는 탭이 그대로 프롬프트가 됩니다.

SAM 2: 영상에서는 메모리로 잇는다

SAM 2(arXiv 2408.00714)는 같은 생각을 영상으로 넓혔습니다. 첫 프레임에서 한 번 짚은 물체를 다음 프레임에서도 따라가야 합니다. 이를 위해 메모리를 둡니다.

직관적으로는 「지난 몇 장에서 그 물체가 어떻게 보였는지를 참고해 이번 장에서 찾는다」입니다. 선수가 테이블 모서리를 잠깐 가려도 기억이 있으니 마스크가 끊기지 않습니다.

공식 저장소가 보여 주는 영상 API 는 세 단계입니다.

# facebookresearch/sam2 README.md(Apache-2.0)에서
predictor = build_sam2_video_predictor(model_cfg, checkpoint)
with torch.inference_mode(), torch.autocast("cuda", dtype=torch.bfloat16):
    state = predictor.init_state(<your_video>)
    frame_idx, object_ids, masks = predictor.add_new_points_or_box(state, <your_prompts>)
    for frame_idx, object_ids, masks in predictor.propagate_in_video(state):
        ...

SAM 2.1(2024-09)의 크기별 수치입니다(공식 README, 데이터센터급 GPU 한 장, 모든 부품 컴파일).

체크포인트 파라미터 FPS(논문 환경) SA-V 시험 J&F
tiny 3,890만 91.2 76.5
small 4,600만 84.8 76.6
base+ 8,080만 64.1 78.2
large 2억 2,440만 39.5 79.5

코드와 체크포인트가 모두 Apache-2.0 입니다. 우리는 가장 작은 tiny 를 골랐습니다. 탁구대는 크고 단순한 물체라 큰 모델의 이득이 작을 것이라고 보았고, 공 찾기와 같은 카드를 나눠 써야 하기 때문입니다.

EfficientTAM: 메모리를 줄여 휴대폰으로

EfficientTAM(arXiv 2411.18933, 코드·체크포인트 Apache-2.0)은 SAM 2 를 가볍게 만드는 두 가지를 보였습니다.

  1. 평평한 ViT 인코더. SAM 2 의 계층형 인코더(Hiera) 대신 해상도 하나짜리 작은 ViT(Tiny·Small)를 씁니다.
  2. 메모리 교차 어텐션을 거칠게. 메모리의 공간 토큰을 2×2 창으로 평균해 4분의 1로 줄인 뒤 어텐션합니다. 이웃한 화소의 기억은 서로 비슷하다는 「국소적 매끄러움」 덕분에, 줄인 토큰이 원래 어텐션을 잘 흉내 낸다고 논문은 설명합니다(전체 어텐션과의 상대 오차 0.03). object pointer 는 줄이지 않습니다.

논문 표(1024×1024 입력)에 따르면 EfficientTAM-S/2 는 3,400만 파라미터로 SA-V 74.0 J&F, 데이터센터급 GPU 109.4 FPS, 휴대폰(iPhone 15 Pro Max) 프레임당 450ms 입니다. 비교한 SAM 2 는 8,100만 파라미터, 74.7 J&F, 43.8 FPS 입니다(논문). 휴대폰 측정의 정밀도·변환 방식은 논문에 적혀 있지 않고, SAM 2.1 과의 비교는 없습니다.

우리에게 의미는 「언젠가 휴대폰 안에서 탁구대를 찾는 길」입니다. 경기 시작 때 한 번이면 되니 프레임당 450ms 도 쓸 만합니다. 아직 우리 환경에서 돌려 보지 않았습니다(미실측).

SAM 3: 글자로 부르기

SAM 3(arXiv 2511.16719, 2025-11)는 프롬프트 개념 분할을 내세웁니다. 「table tennis table」 같은 짧은 글이나 예시 그림을 주면, 그 개념에 해당하는 모든 물체를 찾아 칠하고 영상에서 따라갑니다. 클릭 없이 탁구대를 찾을 수 있다는 점에서 매력적입니다.

그러나 이번에는 넣지 않았습니다.

3. 마스크에서 네 모서리로

분할 모델이 주는 것은 마스크(칠한 화소 집합)입니다. 우리가 필요한 것은 꼭짓점 네 개입니다. 이 변환이 생각보다 까다로웠습니다.

실패한 방법 둘

처음에는 마스크 외곽선을 다각형으로 단순화해 꼭짓점을 골랐습니다(approxPolyDP). 네트가 윗면을 두 반으로 가르면 반쪽 테이블을, 선수가 모서리를 가리면 안쪽의 엉뚱한 점을 모서리로 냈습니다.

다음에는 「마스크의 각 열에서 맨 위 = 먼 변, 맨 아래 = 가까운 변」으로 변을 맞췄습니다. 옆면에서는 잘 됐지만, 테이블이 화면에서 비스듬히 돌아간 영상(TT3D 연구실의 비스듬한 카메라)에서 꼭짓점 하나가 370–754px 어긋났습니다. 끝 변을 먼 변으로 읽은 것입니다. 쪽 정확도가 0.99 에서 0.80 으로 떨어졌습니다.

지금 방법: 변마다 직선을 다시 맞춘다

① 화면: 마스크의 바깥 경계 → 변마다 직선 → 교점 먼 오른쪽 가까운 왼쪽 가까운 오른쪽 선수 팔이 가림 먼 왼쪽 = 두 변의 교점 바운드 가려진 꼭짓점도 보이는 변 두 개의 교점으로 정해진다 호모그래피 H (네 점 대응) ② 테이블 평면: 2.74m × 1.525m L 끝 (u < 0) R 끝 (u > 0) 네트 = u 0 · 끝 = |u| 1.37m · 바운드의 쪽 = 닿은 점의 u 부호
그림: 마스크의 네 변에 직선을 맞춰 모서리를 얻고, 네 점 대응으로 테이블 평면에 옮기는 과정

ttscore/calib.py 는 이렇게 합니다.

  1. 마스크의 바깥 경계 점을 모은다.
  2. 볼록 껍질을 감싸는 사각형으로 네 변의 대략을 잡는다.
  3. 변마다 그 근처의 경계 점만으로 직선을 다시 맞춘다. 네트 기둥이나 선수 팔 때문에 점의 절반 가까이가 어긋나도 버티도록, 먼저 반복 중앙값(Siegel) 기울기로 직선을 잡고, 그 직선에서 3px 안의 점만으로 최소제곱을 되풀이한다.
  4. 이웃한 두 변의 교점을 꼭짓점으로 한다.
  5. 프레임 하나만 쓰지 않고 몇 프레임(실측은 5장)의 꼭짓점을 중앙값으로 합친다.
# ttscore/calib.py _fit() 에서 줄임 — v = a + b·u 직선, 순수 파이썬
slopes = [_median([(vs[j] - vs[i]) / (us[j] - us[i]) for j in idx if us[j] != us[i]]) for i in idx]
b = _median(slopes)                       # 반복 중앙값: 점의 절반이 어긋나도 기울기가 버틴다
a = _median([vs[i] - b * us[i] for i in idx])
for _ in range(it):                        # 그 직선에서 tol(3px) 안의 점만으로 최소제곱을 되풀이
    keep = [i for i in range(n) if abs(vs[i] - (a + b * us[i])) <= tol]
    ...

이 방법의 장점은 가려진 꼭짓점도 보이는 변 두 개가 정한다는 것입니다. 선수가 모서리를 덮고 있어도 그 모서리를 이루는 두 변의 나머지 부분이 보이면 교점은 제자리에 나옵니다. 같은 SAM 마스크에서 비스듬한 영상의 꼭짓점 최대 오차가 370–754px 에서 25–28px 로 줄었고, 쪽 정확도는 0.999 가 되었습니다.

CI 에 numpy 가 없어서 이 모듈은 순수 파이썬입니다. 마스크는 행마다 칠한 구간 {y: [(x0, x1), ...]} 로 받습니다.

윗면만 남기기

SAM 은 테이블 윗면만이 아니라 옆판과 다리까지 칠하거나, 반대로 네트에 막혀 한 반만 칠하기도 합니다. 그래서 탭을 두 번(네트로 갈린 두 반에 하나씩) 주고, 탭한 자리와 색이 비슷한 화소만 남겨 윗면을 옆판과 가릅니다. 이 단계가 그늘에 약했습니다. 연구실 옆면 영상 두 편에서 그늘진 끝을 색 문턱이 잘라 꼭짓점이 87–97px 빗나갔습니다. 이런 사각형은 직사각형으로 읽히지 않는다는 검사(quad_ok)에서 걸러져 예전 규칙으로 돌아가고, 쪽 정확도 0.99 를 지켰습니다.

녹화 영상 분석 작업자에는 아직 SAM 2.1 이 실려 있지 않아, 지금은 탭 자리에서 비슷한 색을 키우는 색 단계만 합니다(batch/calib.py). 이 방법의 모서리 정확도는 재지 않았고(미실측), 그래서 결과에 방법을 남기고 화면은 「네 모서리 직접 짚기」를 권합니다.

4. 네 모서리에서 테이블 평면으로: 호모그래피

직관

평평한 면을 카메라로 찍으면 직사각형이 사다리꼴로 보입니다. 이 「평면 → 평면」 원근 변환을 호모그래피라고 합니다. 3×3 행렬 하나로 표현되고, 마지막 원소를 1 로 고정하면 미지수가 8개입니다. 점 하나의 대응이 식 두 개(x, y)를 주므로 네 점이면 정확히 풀립니다. 화면의 네 모서리와 테이블의 네 꼭짓점(0, 0) … (2.74, 1.525) 을 짝지으면 됩니다.

우리 코드

OpenCV 의 getPerspectiveTransform(네 점)이나 findHomography(여러 점, RANSAC)를 쓰면 한 줄이지만, 엔진은 numpy·OpenCV 없이 돌아야 해서 8×8 연립방정식을 직접 풉니다.

# ttscore/table.py
def homography(src, dst):
    """네 점 대응 src[i] → dst[i] 의 3×3 변환(h33 = 1)."""
    a, b = [], []
    for (x, y), (u, v) in zip(src, dst):
        a.append([x, y, 1, 0, 0, 0, -u * x, -u * y]); b.append(u)
        a.append([0, 0, 0, x, y, 1, -v * x, -v * y]); b.append(v)
    h = _solve(a, b)                       # 부분 피벗 가우스 소거
    return ((h[0], h[1], h[2]), (h[3], h[4], h[5]), (h[6], h[7], 1.0))

이 변환으로 화면의 점을 테이블 평면 좌표 (u, v) 로 옮깁니다. u 는 길이 방향이고 가운데(네트)가 0 입니다. 그러면 시점과 상관없이 같은 식이 됩니다.

두 가지 함정

첫째, 평면 투영은 공이 테이블에 닿은 순간에만 맞습니다. 공중의 공을 평면에 옮기면 「카메라에서 공을 지나는 직선이 테이블 평면과 만나는 점」이 나와, 공이 높을수록 멀리 밀립니다. 끝 쪽 카메라에서 네트 높이의 공은 50cm 넘게 밀립니다. 그래서 평면 좌표는 바운드 순간의 쪽에만 쓰고, 공중 구간의 판단은 5편의 이벤트 규칙이 따로 합니다. 바운드 순간에도 공 중심은 닿은 점보다 반지름(20mm)만큼 위에 있어서, 그 아래의 닿은 점을 계산해 쪽을 정합니다. 이 보정 하나로 합성 비스듬한 카메라의 승자 정확도가 79.5% 에서 85.6% 로 올랐습니다.

둘째, 네 점만으로는 어느 변이 끝(엔드라인)인지 모릅니다. 어떤 사각형이든 두 가지 방법으로 직사각형에 옮길 수 있기 때문입니다. 엔진은 다음 순서로 정합니다.

  1. 데이터나 사람이 끝 변을 알려 주면 그것.
  2. 테이블 두 반에 하나씩 찍은 탭 두 점이 있으면, 그 두 점을 u 로 가장 멀리 가르는 짝.
  3. 화면 크기를 알면, 화면 가운데를 주점으로 보고 두 소실점이 직교한다는 조건으로 초점거리를 풀어 실제 가로세로 비를 복원(직사각형 복원). 2.74 : 1.525 에 가까운 짝이 맞다.
  4. 아무것도 없으면 예전 규칙(옆면 가정: x 가 작은 두 점이 한 끝).

왜 평면까지 갔나

예전 엔진은 대각선 교점의 x 좌표(「네트 x」)로 화면을 좌우로 갈랐습니다. 옆면에서는 맞지만 두 가지 문제가 있었습니다. 대각선 교점은 모서리의 세로 오차를 약 8배로 키웁니다(실측: 모서리 6–15px 오차 → 네트 x 27–36px). 그리고 끝 쪽 카메라에서는 그 세로선이 테이블 폭의 가운데 흰 선이 되어, 쪽 판정이 동전 던지기가 됐습니다(합성 끝 쪽 카메라 0.55, 실제 중계 영상 0.60). 평면 좌표로 바꾼 뒤 시점과 상관없이 같은 규칙이 됐습니다.

5. 실측

SAM 2.1 tiny 의 모서리(2026-10-11)

조건: OpenTTGames 실제 경기 12편, 탭 두 점 + 색으로 윗면만 + 네 변 직선 맞춤, 5프레임 중앙값. 비교 대상은 데이터셋이 준 분할 마스크로 구한 모서리(1920×1080).

항목 값
모서리 차이 중앙 / 90% / 최대 4.7px / 11.2px / 21px
프레임 하나만 쓸 때 60장 중 6장이 20px 넘게 빗나감(선수 팔이 끝 변을 가린 프레임)
정답 바운드 중 「테이블 윗면 띠」 밖 허용 8px 에서 0.73%, 8px + 공 반지름에서 0.11%

데이터셋 마스크가 늘 정답은 아니었습니다. 3배 확대로 네트 바닥선을 눈으로 대조하면 두 경기(test_1, game_5)에서는 SAM 쪽이 맞았습니다.

속도와 메모리

조건 값
서버 GPU, 다른 작업과 나눠 쓸 때(2026-10-11) 이미지 인코딩 40ms, 프롬프트 예측 26ms, 영상 추적 물체 하나 36–38ms/프레임, GPU 메모리 0.68–0.96GB
CPU 임시 파드(2026-10-12) 프레임당 중앙 1.14초. GPU 조각이 다 찼던 다른 날에는 5.9초

경기 시작 때 한 번이면 CPU 로도 몇 초 안에 끝납니다. 프레임마다 돌릴 일이 없어서 GPU 를 꼭 쓸 필요가 없다는 것이 이 숫자의 의미입니다.

점수에 미친 영향

같은 날 같은 엔진에서 모서리만 바꿨을 때 실제 경기 275랠리의 승자 정확도가 82.5%(마스크 모서리) → 84.7%(SAM 모서리) 였습니다. 먼 변 근처 바운드가 띠 안에 들어온 것이 주된 이유입니다. 그 뒤 엔진이 바뀌면서 SAM 모서리로 93.1% 까지 올랐는데, 그 모서리 파일이 남아 있지 않아 2026-10-13 기준선(91.3%, 녹화본 채점)은 마스크 모서리로 다시 쟀습니다. 둘은 엔진도 달라서 모서리 몫만 떼어 말할 수 없습니다. 「점수판이 서버를 정하는」 경기 전체 모드에서는 마스크 모서리 쪽이 SAM 기록보다 분명히 낮았습니다. 모서리 품질이 점수판 흐름에 크게 걸린다는 뜻입니다.

넣지 않은 것

SAM 2 로 탁구대 말고 다른 것도 해 보았지만 실측 이득이 없어 넣지 않았습니다.

시도 결과 판단
SAM 2 로 공 추적 선수와 겹친 공 재현율 0.879 → 0.926. 그러나 바운드 정밀도가 떨어져 랠리 승자 이득 0 넣지 않음
선수 마스크로 「가려서 안 보임」 판정 겹친 공의 60% 만 잡음 넣지 않음
물리 궤적으로 빈 곳 메우기 랠리 승자 −2 ~ −6 넣지 않음

OpenTTGames 밖에서

영상 결과
국제 경기 중계 30클립(640×360) 데이터의 모서리와 꼭짓점 최대 차 중앙 4.5px, 90% 16px. 한 클립은 관중석까지 칠해 158px
연구실 비스듬한 카메라 3편 꼭짓점 최대 오차 25–28px(약 20° 의 날카로운 꼭짓점이라 작은 직선 오차가 교점을 멀리 민다)
연구실 옆면 3편 두 편이 그늘 때문에 87–97px. quad_ok 가 걸러 쪽 정확도 0.99

6. 정리와 다음

탭 없이 탁구대를 자동으로 찾는 기능(경기 영상에서 탁구대를 찾고 따라가기)은 다른 작업에서 만들고 있습니다. 그 결과와 EfficientTAM 의 실측이 나오면 이 글에 덧붙입니다(진행 중).

티티 한마디. 테이블 모서리가 선수 팔에 가려도 괜찮아요. 보이는 두 변을 길게 이어 만나는 점을 찾으면 되거든요.

원리 더 읽기. SAM 계열의 원 논문과 학습 원리는 비전 모델 읽기 1, 9편: Segment Anything, 10편: SAM 2 와 EfficientTAM, 11편: SAM 3에 정리했습니다.

참고

로그인하면 좋아요를 누를 수 있습니다

댓글

아직 댓글이 없습니다.

로그인하면 댓글을 쓸 수 있습니다