これは何?
スマホのカメラ映像から、テニスコートのライン・ボールをリアルタイム検出してIN/OUTを判定するブラウザアプリです。サーバへ映像を送らず、すべて端末内で処理します。
クイックスタート(主審)
アプリを開く(iOS 16.4+ Safari / Chrome / Edge 推奨)
画面下の カメラ開始 をタップして、カメラ権限を許可
コートを画面いっぱいに映す。コート枠が黄色く検出されると準備完了
ラリー中、ボールがバウンドするたびにヘッダ右に大きく IN / OUT が表示され、左の履歴に積み上がります
自動検出が外れたとき:手動コート
屋外の逆光・コントラスト不足・特殊なアングルでコートを自動検出できないことがあります。その場合:
- フッタの ○ 手動コート をタップして ON
- 画面のコート4隅を左上 → 右上 → 右下 → 左下 の順に4回タップ
- 4点揃った瞬間に自動で並び替えられて、IN/OUT 判定が動き出します
- 位置がズレているときは隅マーカーをドラッグして微調整。間違えたら 1つ戻す または リセット
指定した4隅は端末に保存され、次回起動時に同じコートなら自動で復元されます(不要なら「リセット」)。
他のスマホ・ウォッチで判定を受け取る(中継)
主審スマホをコート脇に置いて、別端末で判定だけを大きく表示できます。
主審側
- フッタの ○ 中継 を ON
- シアンの帯にルームID(6文字、例
KX7M9P)が表示される - URLコピー をタップ → LINE 等で受信側に送る
受信側
- 送られた URL を Safari で開く(自動でルームIDが入る)
- 接続 → 緑の「受信中」になれば成功
- 主審側で判定が出るたびに画面いっぱいに IN / OUT がフラッシュ+バイブ+ビープ(バイブは Android のみ)
ロック画面 / Apple Watch 通知(Web Push)
受信端末をロックしてポケットに入れていても、判定が出るたびに ロック画面・通知センター・Apple Watch(iPhone通知のミラー)に 通知が出るように設定できます。
- 受信側スマホで
/watch?room=XXXを開いて Safari の共有 →「ホーム画面に追加」 (iOS は PWA 必須) - ホーム画面のアイコンから受信ページを起動 → 接続
- ヘッダの 🔕 ボタンをタップして通知許可 → 🔔 に切り替わったら登録完了
- 主審側の ● 中継 シアン帯に 🔔 N が出れば、N 端末がロック画面通知を受け取る状態
- 受信端末がロック中でも、判定の度にロック画面に通知が出てバイブ。Apple Watch でも同じ通知がミラー表示される
通知配信は Vercel API Route + Web Push (VAPID) 経由。subscription は端末内にのみ保持され、judge セッション中だけ主審側のメモリに渡ります。
ホーム画面に追加(PWA / Apple Watch 連携の前提)
Safari の共有 📤 → 「ホーム画面に追加」 で、URL バーのない全画面アプリとして使えます。
- 主審側はそのままトップ(
/)をホーム画面追加 - 受信側は接続済みの URL
/watch?room=XXXを追加すると、次回アイコンタップで同じルームに即接続 - 受信スマホを iPhone にして近くに置けば、フラッシュと音で気付ける(Apple Watch 専用通知は次フェーズの Web Push で対応予定)
スナップショット(判定瞬間の画像保存)
フッタ「補助」グループの ● 撮影 を ON にすると、判定が出るたびにカメラ映像とオーバーレイを合成した画像が端末内(IndexedDB)に保存されます。直近100件まで自動で保持。
- ヘッダの 📷 からギャラリー画面にアクセスして一覧表示
- 各画像をタップで拡大、個別 DL or 削除が可能
- 全件削除は右上ボタン。100件超は古い順に自動で消える
- 端末内のみ。サーバには送信されません
判定履歴のエクスポート
判定履歴は最新50件まで内部で保持。左上の履歴パネル右の CSV ボタンで timestamp_ms,iso,verdict,x_px,y_px 形式の CSV をダウンロードできます。試合後にスプレッドシートで振り返り可。
OUTのみ通知(リアル審判モード)
出力グループの ● OUTのみ を ON にすると、本物の審判のようにOUT のときだけ 音・読み上げ・全画面フラッシュが出ます。IN は無音で進行 → サーブイン → ラリー → アウト の自然な流れに合います。履歴・スナップショット・受信側への中継は IN/OUT 両方記録されるので、後で振り返り可能。
受信ページ /watch にも同じトグル(ヘッダの「全部 / OUTのみ」)があります。受信端末ごとに通知の細かさを切り替え可。
読み上げ(音声合成)
出力グループの ● 読み上げ を ON にすると、判定が出るたびに「イン」「アウト」 と日本語で読み上げます(Web Speech API、ja-JP)。サーブモード時は 「アウト 相手アド」 のようにゾーン名も付加。詳細パネルで試聴可。
ブラウザの音声合成機能を使うので、サイレントモード/Bluetoothスピーカーの状態に依存します。読み上げ品質は端末搭載の TTS 次第。
カメラ選択 + ズーム + ピンチ
起動後、画面右側に縦のズームスライダーが出ます。+ / − ボタンで微調整、スライダーで一気に変更。二本指のピンチイン/アウトでも直感的にズーム可能です。
1.0x* 表示(末尾アスタリスク)は、カメラがハードウェアズーム未対応のためデジタル拡大(CSS scale)を使っているサイン。 アスタリスクなし = カメラの光学/ネイティブデジタルズームを使用。
一度カメラを起動してから停止すると、複数カメラ搭載端末でカメラ選択 プルダウンが表示されます。iPhoneの超広角(0.5x)に切り替えると、ベースライン後ろのスペースが狭い日本のコートでも全体を画角に収めやすくなります。
ベースライン後ろが狭い・横から撮影する場合
日本のテニスコートはベースラインの後ろが狭くて、コート4隅全部を画角に入れるのが難しい場合があります。対処:
- カメラ選択でiPhoneの超広角(0.5x)に切替
- それでも入らない場合は 横から撮影(サイドラインの外)に切り替える
- 横から撮ると自動検出が乱れがちなので、迷わず ● 手動コート ON で4隅をタップ。順序は左上→右上→右下→左下 で OK(撮影方向に関係なく、画面上の4隅をその順に)
- 4点指定後にドラッグで微調整、保存したコートは次回起動で自動復元
設定の畳み込み(コート4隅をタップする時)
カメラ起動中、フッタは停止 と ⚙ 設定 ▲ の2ボタンに自動で畳まれます。設定が必要な時だけ⚙ 設定 ▲ を押して展開。手動コート4隅を画面下端でタップしたい時にトグル群が邪魔にならないようになっています。
ダブルタップでコート再認識
手動コートが OFF のとき、画面(カメラ映像エリア)を素早く2回タップすると、コーナー平滑化バッファをリセットして自動検出をやり直します。 ライティング変化やカメラ位置調整後にすぐ追従させたい時に。 画面上部に🔄 コート再認識中… のバナーが一瞬表示されます。
手動コート ON 時はタップが4隅登録/ドラッグの操作と衝突するため無効。
フッタのトグル
- 白線フィルタ
- HSV で白色だけを抜き出してから検出。屋外で確実に効かせたいときON、屋内で枠が映らないときはOFFを試す
- コート枠
- コート4隅の自動検出を行う。手動コートONのときは自動検出は使われない
- ボール
- 黄色ボールの検出
- デバッグ
- マスク数・エッジ数・フレーム平均RGB・解像度・readyState を表示。検出されない原因の切り分けに
- 手動コート
- 4タップでコート枠を手動指定。ドラッグで微調整
- 通知音
- 判定が出たときにビープ音 + バイブ(iOS は音のみ)
- 中継
- ルームID付きで判定を別端末にリアルタイム送信
- 読み上げ
- 「イン」「アウト」を日本語で読み上げ(サーブ時はゾーンも)
判定モードはカメラ開始前に選べる
「カメラ開始」ボタンの上にモード選択UIがあるので、撮影前にダブルス / シングルス / サーブを選んでから始められます。選んだモードは端末に保存され、次回起動時にも復元。
モード切替・自陣の向き・初弾のみ・ROI追跡・しきい値スライダーなどは全て自動で永続化されます。
サンプルで試す(カメラなしデモ)
「カメラ開始」ボタンの下に🎾 サンプルで試す ボタンがあります。実カメラの代わりに HTML Canvas でテニスコートとボールをシミュレートし、本番と同じ検出パイプラインを動かせます。判定モード切替・コート4隅検出・ボール追跡・通知音・中継・スナップショットなど一通りのフローを、カメラのない PC からでも試せます。
バウンドフィルタ(自陣 / 相手)
判定モードの隣に 全部 自陣 相手 のセグメントがあります。「自陣」を選ぶと、自陣側にバウンドした打球の判定だけを残し、相手陣側の打球はスキップ(履歴・通知音・全画面フラッシュ・中継のすべて)。練習時に自分の球だけ振り返りたい時に。
自動方位
補助グループの ● 自動方位 を ON にすると、コート4隅の重心が画面の上半分にあれば自陣下、下半分にあれば自陣上、と推定してcourtSide を自動セット。普通にカメラを自陣ベースライン側に置けば、ほぼ自陣下に揃います。違うと感じたら OFF にして手動指定。
判定モード(ダブルス / シングルス / サーブ)
フッタの「判定」グループでモードを切替:
- ダブルス — ダブルスサイドラインで囲まれた長方形に対する IN/OUT
- シングルス — シングルスサイドラインの内側だけを IN として判定(外側はOUT)
- サーブ — 4つのサービスエリア(上左/上右/下左/下右)で判定。エリア内なら IN + ゾーン名、外は OUT(フォルト)として表示。CSV および中継先にもゾーン名を含める
コート4隅が確定すれば、ITF標準寸法(10.97m × 23.77m, シングルス幅8.23m, サービスライン6.40m)で内側のラインは自動算出。
サーブモード時、判定セグメントの隣に自陣↓自陣↑ が出ます。自陣の位置を選ぶと、ゾーン名が 「上左/上右/下左/下右」から「相手デュース・相手アド・自陣デュース・自陣アド」に変換されます。
検出を安定させる細かい調整
- ROI追跡(補助グループ) — ボールを見つけたあとは前フレームの近くだけを探すので軽量化。 見失った時は自動で全画面探索に戻る
- 初弾のみ(補助グループ) — 1ラリーの最初のバウンドだけを判定。ボールがラリー区切りms (詳細パネル、デフォルト1000ms)以上消えるとラリー終了とみなし、次の最初のバウンドを判定対象に。サーブ判定向け: 1stフォルト→2ndサーブも各々1発として判定、ポイント終了後の次のサーブも自動で次の判定対象に
- バウンド最小間隔ms(詳細パネル) — 直前のバウンドから指定ms以内の再検出を無視。連続バウンドや 振動による多重検出を抑える。デフォルト 250ms
シーンプリセット
フッタの 詳細▼ を開くと、上部にシーンプリセットが並びます。光の条件に応じて ワンタップで全閾値を切替:
- 屋内 — 蛍光灯下のインドアコート、白線がはっきり
- 屋外晴れ — 順光の昼。デフォルト相当
- 曇り/夕方 — 暗めの環境、白線がくすんで見える条件
プリセット適用後、個別スライダーで微調整できます。
それでも検出されないとき
○ デバッグ をONにしてヘッダ右に出る帯の色をチェック:
- 🔴 赤帯(
0×0 rs0/1): カメラ映像が来ていない → 権限・他アプリ占有 - 🔴 赤帯(
RGB(2,3,4)): 真っ黒 → レンズが覆われている / 露出 - 🟡 黄帯(
RGB(245,250,248)): 白飛び → 露出を下げる、順光に - 🟣 紫帯 + マスク0: 白マスク閾値が厳しすぎる → 詳細パネルで
白さ閾値Vを下げる - 🟣 紫帯 + マスク>0 + エッジ0: Cannyが厳しい →
Canny低/高を下げる
それでもダメなら 手動コート に切り替えるのが一番確実です。