ステータス:下書き(2026-10-13)、実測値を反映。 SAM 2.1 tinyの精度と速度は、2026-10-11~12にサーバーGPU(他のタスクと共用していた時期)とCPUの一時的なポッドで測定した値です。EfficientTAMとSAM 3は私たちの環境では実行しておらず(未実測)、論文および公式リポジトリの数値を条件とともに引用しています。卓球台を自動で探す機能は別のタスクとして開発中であり(進行中)、結果が出次第追加します。 作成ルール:実測データを基本とし、測定できなかったものは「未実測」と表記します。
目次
- 1. なぜ4つの角が必要なのか
- 2. プロンプトベースのセグメンテーション:1回のクリックが質問になる
- 3. マスクから4つの角へ
- 4. 4つの角からテーブル平面へ:ホモグラフィ
- 5. 実測
- 6. まとめと今後
- 参考文献
1. なぜ4つの角が必要なのか
ボールの座標だけではスコアをつけることができません。「ボールがテーブルに触れたか」、「どちら側のコートに触れたか」、「テーブルの端を越えたか」を知るには、テーブルが画面上のどこにあるかを知る必要があります。卓球台はサイズが規定されている長方形です。長さ2.74m、幅1.525m、ネットの高さは15.25cmです。そのため、画面から 4つの角さえ見つければ、残りは幾何学で計算できます。
幸いなことに、TT Scoreではカメラを三脚に固定すると仮定しています。卓球台は動かないため、 試合開始時に1回 見つければ十分です。フレームごとに実行しなければならないボール検出とは異なり、ここでは数百msかかっても問題ありません。この違いがモデルの選択を変えます。
現在のリアルタイム画面では、 人間が4つの角を直接タップしています。これが最も確実であり、計算コストがかかりません。ここにテーブルの2つのコートをそれぞれもう一度タップすると、斜めからのカメラアングルであっても、どの辺がエンドラインであるかをエンジンが判断できるようになります(以下のセクション5)。この記事で紹介するモデルは、「タップを減らす、またはなくすためのアプローチ」です。
2. プロンプトベースのセグメンテーション:1回のクリックが質問になる
SAMの考え方
Segment Anything(SAM)シリーズは、「何を塗りつぶすか」を学習時には決めません。その代わりに、人間が与えた プロンプト(点のクリック、バウンディングボックス、おおよそのマスク)を受け取り、その場所にある物体を塗りつぶします。構造は3つの部分から成ります。
- 画像エンコーダ:重い部分。1枚の画像を一度だけ処理して特徴マップを作成します。
- プロンプトエンコーダ:クリック座標やボックスを小さなベクトルに変換します。
- マスクデコーダ:軽い部分。特徴マップとプロンプトを受け取り、マスクを出力します。
重いエンコーダを一度だけ実行し、軽いデコーダを毎回のクリックごとに再実行するため、人間がクリックを変更しながら修正する画面によく適しています。TT Scoreでは、 人間が卓球台を指定したタップがそのままプロンプトになります。
SAM 2:映像ではメモリでつなぐ
SAM 2(arXiv 2408.00714)はこの考え方を動画に拡張しました。最初のフレームで一度指定した物体を、次のフレームでも追跡し続けなければなりません。そのために メモリを持たせます。
- メモリエンコーダ:フレームごとに予測されたマスクを縮小し、そのフレームの画像特徴に加算し、軽量な畳み込みで混合することで、「このフレームにおいてその物体はこのような形をしている」という記憶を作ります。
- メモリバンク:直近のフレームの記憶(デフォルトで6個)と、人間がプロンプトを与えたフレームの記憶を別々に並べて保持します。ここにマスクデコーダの出力トークンから抽出した小さなベクトル(object pointer)も一緒に保持します。
- メモリ・アテンション:新しいフレームの特徴がまず自分自身を参照し(self-attention)、次にメモリバンクの記憶とobject pointerを参照します(cross-attention)。これら4つのブロックを通過した特徴がマスクデコーダへと送られます。
直感的に言えば、「過去数枚の画像でその物体がどのように見えていたかを参考にして、今回の画像で探す」ということです。選手がテーブルの角を一時的に隠したとしても、記憶があるためマスクが途切れなくなります。
公式リポジトリが示すビデオAPIは3つのステップから構成されます。
# 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 1枚、すべてのコンポーネントをコンパイル済み)。
| チェックポイント | パラメータ数 | 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を選択しました。卓球台は大きくてシンプルな物体であるため、大きなモデルによるメリットが小さいと考えたこと、そしてボール検出などの他のタスクとGPUリソースを共有する必要があるためです。
EfficientTAM:メモリを削ってスマートフォンへ
EfficientTAM(arXiv 2411.18933、コード・チェックポイントはApache-2.0)は、SAM 2を軽量化する2つのアプローチを示しています。
- フラットなViTエンコーダ。 SAM 2の階層型エンコーダ(Hiera)の代わりに、単一解像度の小さなViT(Tiny・Small)を使用します。
- メモリクロスアテンションの粗視化。 メモリの空間トークンを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)で1フレームあたり450msです。比較対象のSAM 2は8,100万パラメータ、74.7 J&F、43.8 FPSです(論文)。スマートフォンでの測定精度や変換方式は論文に記載されておらず、SAM 2.1との比較もありません。
私たちにとっての意味は、「いつかスマートフォン内で卓球台を見つけるための道筋」です。試合開始時に1回だけ実行すればよいため、1フレームあたり450msでも実用に耐えます。まだ私たちの環境では実行していません(未実測)。
SAM 3:テキストで指定する
SAM 3(arXiv 2511.16719、2025-11)は プロンプトベースの概念セグメンテーションを掲げています。「table tennis table」のような短いテキストやサンプルの画像を与えると、その概念に該当する すべての 物体を見つけて塗りつぶし、動画内で追跡します。クリックなしで卓球台を見つけられるという点で魅力的です。
しかし、今回は採用しませんでした。
- ライセンス。 Apacheライセンスではなく、独自の「SAM License」です。再配布時には同じ契約を付帯させる必要があり、輸出管理・軍事利用の禁止、リバースエンジニアリングの禁止、訴訟時の契約解除などの条件が含まれます。商用利用については明示的に扱われていません。
- アクセス承認。 チェックポイントをダウンロードするには、Hugging Faceでアクセスを申請する必要があります。
- サイズ。 8億4,800万パラメータ(公式README)です。tinyの20倍以上です。論文では、データセンター向けGPUで100個以上の物体が含まれる1枚の画像に対して30msと報告されています。
- 必要性。 卓球台は画面上に1つしかなく、人間が一度指定すれば済みます。「すべての概念を見つける」という利点が小さいためです。
3. マスクから4つの角へ
セグメンテーションモデルが提供するのはマスク(塗りつぶされた画素の集合)です。私たちに必要なのは4つの頂点です。この変換は想像以上に厄介でした。
失敗した2つの方法
最初は、マスクの外郭線を多角形に単純化して頂点を選びました(approxPolyDP)。ネットが上面を2つの半分に分割している場合は半分のテーブルを、選手が角を隠している場合は内側の見当違いな点を角として出力してしまいました。
次に、「マスクの各列において一番上=遠い辺、一番下=近い辺」として辺を対応付けました。側面からの映像ではうまく機能しましたが、テーブルが画面に対して斜めに傾いている映像(TT3D研究室の斜めからのカメラ)では、 1つの頂点が370~754px ずれてしまいました。端の辺を遠い辺として読み取ってしまったためです。セグメンテーションの精度が0.99から0.80に低下しました。
現在の手法:辺ごとに直線を再フィットする
ttscore/calib.py は次のように行います。
- マスクの外周境界点を収集する。
- 凸包を囲む長方形によって4辺の大まかな位置を捉える。
- 辺ごとに その近傍の境界点のみを使って直線を再フィットする。 ネットの支柱や選手の腕によって点の半分近くがずれてしまっても耐えられるよう、まず反復メディアン(Siegel)傾き推定で直線を捉え、その直線から3px以内の点のみを用いて最小二乗法を繰り返す。
- 隣接する2辺の 交点を頂点とする。
- 1フレームだけでなく、数フレーム(実測では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]
...
この方法の利点は、 隠された頂点であっても、見える2つの辺がそれを決定するという点です。選手が角を覆っていても、その角を構成する2辺の残りの部分が見えていれば、交点は正しい位置に現れます。同じSAMマスクを用いた斜めからの映像における頂点の最大誤差が370~754pxから 25~28px に減少し、セグメンテーション精度は0.999になりました。
CI環境にnumpyがないため、このモジュールは純粋なPythonで実装されています。マスクは行ごとに塗りつぶされた区間 {y: [(x0, x1), ...]} として受け取ります。
上面だけを残す
SAMはテーブルの上面だけでなく、側面や脚まで塗りつぶしたり、逆にネットに遮られて片側半分だけを塗りつぶしたりします。そのため、タップを2回(ネットで分割された2つの半分にそれぞれ1回ずつ)行い、 タップした場所と色が似ている画素のみを残すことで 上面を側面と切り離します。このステップが影に弱いという弱点がありました。研究室の側面からの2つの映像において、影になった端の色をしきい値処理で切り離してしまい、頂点が87~97pxずれてしまいました。このような長方形として認識されない図形は、検査(quad_ok)によって排除され、以前のルールに戻ることで、セグメンテーション精度0.99を維持しました。
録画映像の分析ワーカーにはまだSAM 2.1が導入されていないため、現在はタブの位置で似た色を拡張する 色段階のみ を行います(batch/calib.py)。この方法のエッジ精度は測定しておらず(未実測)、そのため結果に方法を残し、画面は「四隅の直接指定」を推奨します。
4. 四隅からテーブル平面へ:ホモグラフィ
直観
平らな面をカメラで撮影すると、長方形が台形に見えます。この「平面 → 平面」の射影変換を ホモグラフィと呼びます。3×3の行列1つで表現され、最後の要素を1に固定すると未知数は8つになります。1つの点の対応が2つの方程式(x, y)を与えるため、 4点があれば一意に解けます。 画面上の四隅とテーブルの4つの頂点(0, 0) … (2.74, 1.525)を対応付ければよいのです。
私たちのコード
OpenCVの getPerspectiveTransform(4点)や findHomography(複数点、RANSAC)を使用すれば1行で済みますが、エンジンは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となります。これにより、視点に関係なく同じ数式になります。
- サイド:u < 0ならL側、u > 0ならR側。
- ネット:u = 0。
- エンドライン外|u| > 1.37m。
2つの罠
第一に、平面投影はボールがテーブルに接触した瞬間のみ有効です。 空中のボールを平面に写像すると、「カメラからボールを通る直線がテーブル平面と交差する点」が得られ、ボールが高いほど遠くに押し出されます。エンド側のカメラでは、ネットの高さにあるボールが50cm以上押し出されます。そのため、平面座標は バウンドした瞬間のサイドにのみ使用し、空中区間の判定は第5話のイベントルールが個別に行います。バウンドの瞬間であっても、ボールの中心は接触点よりも半径(20mm)分だけ上に位置するため、その真下にある 接触点を計算してサイドを決定します。この補正1つにより、合成された斜めカメラの勝者精度が79.5%から85.6%に向上しました。
第二に、4点だけではどの辺がエンドライン(端の線)なのか分かりません。 どのような四角形であっても、2つの方法で長方形に写像できるためです。エンジンは次の順序で決定します。
- データや人間がエンドラインを指定している場合はそれを使用する。
- テーブルの2つの半分にそれぞれ1つずつ打たれたタブの2点がある場合、その2点をuで最も遠くへ分ける組み合わせ。
- 画面サイズが分かっている場合、画面の中央を主点と見なし、2つの消失点が直交するという条件で焦点距離を解いて実際の縦横比を復元(長方形の復元)。2.74 : 1.525に近い組み合わせが正しい。
- 何も情報がない場合は従来のルール(側面を仮定:xが小さい2点が一方の端)。
なぜ平面まで変換したのか
従来のエンジンは、対角線の交点的のx座標(「ネットx」)で画面を左右に分割していました。側面視では機能するものの、2つの問題がありました。対角線の交点は、エッジの垂直方向の誤差を約8倍に拡大させます(実測値:エッジの6〜15pxの誤差 → ネットxの27〜36px)。さらに、エンド側のカメラではその垂直線がテーブル幅の中央の白い線になってしまい、サイド判定がコイントスになっていました(合成エンド側カメラ0.55、実際の放送映像0.60)。平面座標に変換したことで、視点に関係なく同じルールが適用できるようになりました。
5. 実測
SAM 2.1 tinyのエッジ(2026-10-11)
条件:OpenTTGamesの実試合12本、タブ2点+色による上面のみ+4辺の直線フィッティング、5フレームの中央値。比較対象はデータセットが提供するセグメンテーションマスクから得られたエッジ(1920×1080)。
| 項目 | 値 |
|---|---|
| エッジの差の中央値 / 90% / 最大 | 4.7px / 11.2px / 21px |
| 1フレームのみを使用する場合 | 60枚中6枚で20px以上のズレが発生(選手の腕がエンドラインを遮っているフレーム) |
| 正解バウンドのうち「テーブル上面の帯」の外側 | 許容8pxで0.73%、8px+ボール半径で0.11% |
データセットのマスクが常に正解とは限りませんでした。3倍拡大してネットの底線を目で照合すると、2つの試合(test_1, game_5)ではSAM側が正しかったのです。
速度とメモリ
| 条件 | 値 |
|---|---|
| サーバーGPU、他のタスクと共有時(2026-10-11) | 画像エンコーディング 40ms、プロンプト予測 26ms、動画追跡オブジェクト1つあたり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。1つのクリップは観客席まで塗りつぶされており158px |
| 研究室の斜めカメラ3本 | 頂点の最大誤差 25–28px(約20°の鋭角な頂点であるため、わずかな直線の誤差が交点を大きく押し出す) |
| 研究室の側面カメラ3本 | 2本は影の影響で87–97px。 quad_ok が作用し、サイドの精度は0.99 |
6. まとめと今後
- カメラが固定されている場合、卓球台は 一度だけ 見つければ十分です。そのため、巨大なモデルではなく 最小限のSAM 2.1と幾何学で十分でした。
- 精度を決定づけたのはセグメンテーションモデルではなく、 マスクを頂点に変換する幾何学でした。辺ごとに直線フィッティングをやり直すアプローチが、オクルージョン(遮蔽)や斜めの視点に耐え抜きました。
- 頂点が決定されれば、ホモグラフィにより画面をテーブル平面に写像し、視点に関係なくサイド・ネット・エンドを同一のルールで判定します。ただし、バウンドの瞬間のみに限ります。
タブなしで卓球台を自動検出する機能(試合映像から卓球台を見つけて追跡する)は別の作業で開発中です。その結果とEfficientTAMの実測値が出たら、この記事に追記します(進行中)。
ティティの一言。 テーブルのエッジが選手の腕で隠れていても大丈夫です。見えている2つの辺を長く延長して交点を見つければよいからです。
原理をさらに読む。 SAMシリーズの原論文と学習原理は、 ビジョンモデルを読む 第1、9話:Segment Anything, 第10話:SAM 2とEfficientTAM, 第11話:SAM 3にまとめています。
参考
- SAM 2: https://arxiv.org/abs/2408.00714, https://github.com/facebookresearch/sam2 (Apache-2.0)
- EfficientTAM: https://arxiv.org/abs/2411.18933, https://github.com/yformer/EfficientTAM (Apache-2.0)
- SAM 3: https://arxiv.org/abs/2511.16719, https://github.com/facebookresearch/sam3 (SAM License)
- OpenCV
findHomography·getPerspectiveTransform: https://docs.opencv.org/4.x/d9/d0c/group__calib3d.html - 卓球台の規格(長さ2.74m、幅1.525m、ネットの高さ15.25cm):ITTF Handbook https://www.ittf.com/handbook/
- 私たちの測定:リポジトリ
docs/tt/PERFORMANCE.md2.4·2.8·2.10