卓上領域マスキング前処理 — 緑フェルト以外を除外して検出

麻雀卓 (緑フェルト) を抽出し、それ以外 (スコアボード・スポンサーロゴ・観客席・背景) を黒塗りしてから YOLO 推論する前処理パイプライン
作成日: 2026-05-14 実装: scripts/mask-table-region.py 適用範囲: 全モデル (A/B/C どのアプローチでも有効)
← INDEX 設計ボトルネック 転移学習

1. なぜ卓上領域マスキングが必要か

核心: M-League sample01 の検出で発生している false positive (緑フェルト・ZOZOTOWN ロゴ・看板・スコアボードへの誤検出) の多くは、 「そもそも卓の上ではない場所」 に検出 bbox が出ているもの。 YOLO の信頼度を上げる前に、探索空間自体を卓上に絞るのが構造的に正しい。

1.1 現状で起きていること

設計ボトルネック資料 Section 3 で示した sample01 event1 (t=81s) の検出結果では、25 件の検出のうち 7〜10 件が false positive。その位置を分析すると:

位置原因物体本前処理での扱い
下部プレイヤーカード (顔写真 + 名前 + 点数)✅ マスクで除外
上部右スポンサー帯 (ZOZOTOWN / Semi Final ロゴ)✅ マスクで除外
上部四局 / 北家マーカー / 局カウンタ✅ マスクで除外
右端得点表示 (29,200 など)✅ マスクで除外
卓上 (緑フェルト)空きスペース・反射ハイライト⚠️ 卓内なので残る (本前処理では対処せず — ファインチューンで解決)

1.2 効果見積

本前処理だけで false positive の 50〜70% を除去可能と推定。 残りはファインチューン (アプローチ A/B/C) で対処する分業構成にする。

2. パイプライン全体像

[入力動画フレーム 1920×1080]
       │
       ▼
[1. HSV 緑域検出]     ← Hue 30-95, S 30-255, V 30-230 で粗マスク
       │
       ▼
[2. ノイズ除去]       ← morphological open + close
       │
       ▼
[3. 最大連結成分抽出] ← cv2.connectedComponentsWithStats
       │
       ▼
[4. 凸包 + dilate 5%] ← 卓の四辺形 + 牌のはみ出しを含める
       │
       ▼
[5. マスク適用]       ← 卓外を黒塗り
       │
       ▼
[YOLO/Roboflow 推論]  ← 卓上だけが検出対象になる
       │
       ▼
[既存の post-process] ← orientation 分類, 鳴き数判定, etc.

本パイプラインは 独立したスクリプト scripts/mask-table-region.py として実装済。 既存の検出パイプラインに「フレーム前処理ステップ」として 1 行差し込むだけで統合可能。

3. アルゴリズム詳細 (5 段階)

3.1 Step 1-2: HSV マスク + ノイズ除去

def green_felt_mask(frame_bgr):
    hsv = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2HSV)
    # 緑フェルト: Hue 30-95 (青緑〜黄緑) / Saturation 30+ / Value 30-230
    mask = cv2.inRange(hsv, (30, 30, 30), (95, 255, 230))
    # ノイズ除去: open でゴマ塩、close で穴埋め
    k = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
    mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, k)
    mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, k, iterations=3)
    return mask

HSV を選ぶ理由は照明変化への頑健性。Saturation を 30 以上に絞ることで、 緑がかったグレーや照明反射を弾く。

3.2 Step 3: 最大連結成分抽出

n, labels, stats, _ = cv2.connectedComponentsWithStats(mask, connectivity=8)
areas = stats[1:, cv2.CC_STAT_AREA]
max_label = 1 + int(np.argmax(areas))
table_only = (labels == max_label).astype(np.uint8) * 255

HSV 緑マスクには「プレイヤーシャツの緑」「観客席の緑バナー」など複数の緑領域が含まれる。 「画面上最大の緑連結成分 = 卓」と仮定して 1 つだけを採用する。 fallback: 最大成分が画像面積の 5% 未満なら「卓は映っていない」と判定し、マスクなしで通す。

3.3 Step 4: 凸包 + dilate

contours, _ = cv2.findContours(table_only, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
hull = cv2.convexHull(max(contours, key=cv2.contourArea))
hull_mask = np.zeros_like(table_only)
cv2.fillPoly(hull_mask, [hull], 255)
# 牌が卓の縁から少しはみ出すことがあるので 5% dilate
d = int(min(h, w) * 0.05)
hull_mask = cv2.dilate(hull_mask, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (d, d)))

最大連結成分はそのままだと「牌で隠れている部分」が穴になる。凸包で四辺形に近づけ、 さらに 5% 膨張させて「卓の縁付近の牌」も含めるようにする。

3.4 Step 5: マスク適用

masked = frame_bgr.copy()
masked[hull_mask == 0] = 0  # 卓外を黒塗り
# YOLO に渡す
detections = roboflow_model.predict(masked)

マスク外を黒で塗りつぶす。YOLO は黒領域に対して通常検出を出さないため、 効率的に false positive を抑制できる。

4. 実装結果デモ (Before/After)

4.1 アガリピークフレームでの効果

M-League sample01 のアガリピークフレームに本前処理を適用した結果。 左上=入力、右上=HSV 緑マスク、左下=凸包 + dilate、右下=マスク適用後 (YOLO へ渡す)。

アガリピークでの卓マスキング
📷 sample01 アガリピーク — スポンサーバナー (ZOZOTOWN / Semi Final / Apr.24) と下部プレイヤーカード (鈴木たろう / 本田朋広 / 佐々木寿人 / 竹内元太) が 黒塗りで除外 されている。卓上の牌だけが残る。

4.2 中盤フレームでの効果

中盤フレームでの卓マスキング
📷 中盤フレーム — 上部の「REIN」スポンサー帯、下部のプレイヤーカードが除外される。牌の整列が見える領域だけが残る。

4.3 数値結果

項目備考
HSV 緑マスク占有率34.7%緑検出された全ピクセル
最大連結成分占有率33.1%= 卓本体
凸包 + 5% dilate 後61.9%YOLO 推論対象領域
除外領域38.1%= 探索空間の 38% を構造的に排除

5. 既存システムへの統合方法

5.1 既存検出スクリプトへの差し込み

本前処理は既存の検出パイプラインに 1 関数呼び出し で統合可能:

# 既存
frame = cv2.imread(image_path)
detections = roboflow_model.predict(frame)

# 本前処理を追加した版
from mask_table_region import table_region_mask, apply_mask

frame = cv2.imread(image_path)
mask, info = table_region_mask(frame, dilate_pct=5.0, min_area_pct=5.0)
if not info["fallback"]:
    frame_masked = apply_mask(frame, mask)
else:
    frame_masked = frame  # 卓が映ってないなら元のまま
detections = roboflow_model.predict(frame_masked)

5.2 適用すべき場所

スクリプト適用備考
scripts/detect-agari-visual.py✅ 推奨視覚アガリ検出で false positive 削減
scripts/analyze-still-frame.py✅ 推奨静止画解析で誤検出表示が減る
scripts/render-3d-overlay-cf.py✅ 推奨CF 用デモ動画でクリーンな表示
scripts/evaluate-agari-transition-detector.py✅ 推奨評価フレームワークの dense 推論にも適用
scripts/voice-cli-mahjong.py✅ 推奨音声 CLI の画像入力 mode に適用
Roboflow Auto-Label⚠️ 検討マスクしてからアップロードすれば Auto-Label 精度向上

5.3 訓練データへの応用

応用アイデア: scripts/extract-roboflow-frames.py で抽出した 499 枚に本前処理を適用してから Roboflow にアップロードすると、 アノテーション工数の削減+Auto-Label 精度向上が見込める。 マスク後の画像で訓練すれば、推論時もマスク後を期待するので一貫性が保たれる。

5.4 CLI ワンライナー例

# 1 枚処理
phase0/.venv/Scripts/python.exe scripts/mask-table-region.py \
    --in-image  mleague/frames/sample01-visual-v1-peak.jpg \
    --out-image mleague/output/masked-peak.jpg \
    --debug     mleague/output/_table-mask-debug.png

# 一括処理 (PowerShell)
Get-ChildItem mleague/frames-roboflow-filtered/*.jpg | ForEach-Object {
    phase0/.venv/Scripts/python.exe scripts/mask-table-region.py `
        --in-image $_.FullName `
        --out-image "mleague/frames-roboflow-masked/$($_.Name)"
}

6. 既知の限界と改善方向

6.1 限界 (現状実装)

限界 1: 卓と同じ緑のオブジェクト
プレイヤーシャツが緑系の場合、卓と連結してマスクに含まれてしまう可能性。 M-League では選手シャツがチーム別カラーなので低リスクだが、緑ベースのチームがあると影響を受ける。
限界 2: 卓上の緑フェルト False Positive は除けない
緑フェルト自体の空きスペースで YOLO が誤検出するケースは、マスク内なので残る。 これはモデル側 (自前ファインチューン) で対処すべき問題。
限界 3: カメラ切替時のフォールバック
M-League ではインタビュー・タイトルカードに切り替わるカメラショットがある。 こうしたフレームでは緑領域が小さく fallback (マスクなし) になる。 フォールバック時は min_area_pct を厳しくして「卓なし = 全てを黒塗り」する選択肢もある。

6.2 改善方向

改善案期待効果実装コスト
ChArUco マーカーで卓四隅を取得マスクが正確な四辺形にマーカーを物理的に貼る運用が必要
Segment Anything (SAM) で卓セグメント緑シャツでも卓だけを抽出SAM API/ローカル推論コスト
Roboflow で「table vs non-table」セグ モデルを訓練M-League 専用に最適化セグメンテーション用アノテ 100 枚
時間軸フィルタ (前後 30 フレームのマスク平均)カメラ切替フレームのノイズ抑制パイプラインへの状態管理追加
カメラキャリブと組み合わせた 3D 卓平面投影動的視点でも正確キャリブ完了が前提

6.3 今すぐ着手すべき改善

  1. パラメータ調整: 各 M-League 動画で --dilate-pct, --min-area-pct を最適化
  2. ハイパー試行: HSV 範囲 (現状 30-95) を sample01/02 動画 ごとに微調整
  3. パイプライン統合: scripts/detect-agari-visual.py に組み込み、A/B 評価 (マスクあり vs なし) で false positive 削減数を測定