卓上マスキング前処理 統合ガイド — 既存検出スクリプトへの組み込み手順

mask-table-region.py を本番パイプラインに統合するための具体手順。--use-table-mask フラグの追加方法・フォールバック処理・A/B 評価まで網羅。
作成日: 2026-05-14 前提資料: table-region-preprocessing.html 対象: 検出系 5 スクリプト
← INDEX 設計ボトルネック 公開モデル素性 類似モデル 転移学習 卓上マスキング (基礎) 合成データ生成 評価テンプレート

1. 統合対象スクリプト一覧

方針: 検出スクリプトは 全 5 つを統合対象とする。 各スクリプトに --use-table-mask オプションを追加し、デフォルトは OFF (互換性維持)。 本番運用が安定してから ON をデフォルト化する段階リリース。
スクリプト 役割 優先度 期待効果
scripts/detect-agari-visual.py 視覚アガリ検出 (本番) 最高 FP 50〜70% 削減
scripts/analyze-still-frame.py 静止画解析 (デバッグ・確認用) 誤検出表示が減り、可視化が読みやすくなる
scripts/render-3d-overlay-cf.py CF 用 3D オーバーレイ動画 クラウドファンディング映像の品質向上
scripts/evaluate-agari-transition-detector.py 評価フレームワーク dense 推論の精度向上 → 評価指標の信頼性 up
scripts/voice-cli-mahjong.py 音声 CLI (Phase α) 画像入力モードのみ適用

1.1 統合対象外

2. --use-table-mask フラグの追加方法

2.1 共通パターン (argparse 拡張)

import argparse

parser = argparse.ArgumentParser()
# 既存引数...
parser.add_argument("--in-image", required=True)
parser.add_argument("--out-image", default=None)

# 新規: 卓マスキング前処理
parser.add_argument("--use-table-mask", action="store_true",
                    help="卓上領域マスキング前処理を有効化 (FP 削減)")
parser.add_argument("--mask-dilate-pct", type=float, default=5.0,
                    help="凸包の dilate 比率 (%) [default: 5.0]")
parser.add_argument("--mask-min-area-pct", type=float, default=5.0,
                    help="卓と判定する最小面積比率 (%) [default: 5.0]")
parser.add_argument("--mask-debug-out", default=None,
                    help="マスクデバッグ画像の出力パス (4 分割)")

args = parser.parse_args()

2.2 import 例

# scripts/mask-table-region.py から再利用可能関数を import
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))

from mask_table_region import (
    table_region_mask,   # マスク生成
    apply_mask,          # マスク適用
    save_debug_grid,     # デバッグ画像 (4 分割) 保存
)
注意: mask-table-region.py はファイル名にハイフンを含むため Python パッケージとして直接 import mask-table-region は不可。 importlib.util.spec_from_file_location を使うか、 mask_table_region.py (アンダースコア版) にシンボリックリンク or リネームすると簡潔。

2.3 統合の最小コード (3 行差し込み)

# 既存: フレーム読み込み → YOLO 推論
frame = cv2.imread(args.in_image)
detections = model.predict(frame, confidence=0.40)

# 統合版: マスク前処理を挟む
frame = cv2.imread(args.in_image)
if args.use_table_mask:                                                # 1
    mask, info = table_region_mask(                                    # 2
        frame,
        dilate_pct=args.mask_dilate_pct,
        min_area_pct=args.mask_min_area_pct,
    )
    if not info["fallback"]:                                           # 3
        frame = apply_mask(frame, mask)
    if args.mask_debug_out:
        save_debug_grid(frame, mask, info, args.mask_debug_out)
detections = model.predict(frame, confidence=0.40)

2.4 ユースケース別 CLI 例

# 1. detect-agari-visual.py に統合後
python scripts/detect-agari-visual.py \
    --video mleague/sample01.mp4 \
    --start-frame 1800 --end-frame 2400 \
    --use-table-mask \
    --mask-debug-out mleague/output/_agari-mask-debug.png

# 2. analyze-still-frame.py に統合後
python scripts/analyze-still-frame.py \
    --in-image mleague/frames/sample01-peak.jpg \
    --use-table-mask

# 3. 評価フレームワーク
python scripts/evaluate-agari-transition-detector.py \
    --eval-dir mleague/eval/ \
    --use-table-mask \
    --report-out mleague/eval/report-with-mask.json

3. フォールバック処理 (卓検出失敗時)

方針: 卓が映っていない / 検出失敗のフレームでは マスクを適用せず元画像を渡す。 検出機会を失わない方を優先 (リコール重視のフェイルセーフ)。

3.1 フォールバック判定ロジック

def table_region_mask(frame, dilate_pct=5.0, min_area_pct=5.0):
    h, w = frame.shape[:2]
    total_area = h * w
    # ... HSV → connectedComponents ...
    if largest_area_pct < min_area_pct:
        return None, {"fallback": True, "reason": "no_table_detected",
                      "largest_area_pct": largest_area_pct}
    # ... 凸包 + dilate ...
    return mask, {"fallback": False, "table_area_pct": table_pct, ...}

3.2 フォールバック発生パターン

発生シーン挙動対処
インタビューカメラ卓なし → fallback元画像で推論。アガリ検出は他の信号で代替
タイトルカード / オープニング卓なし → fallback同上。多くは検出 0 件で良い
俯瞰超ロングショット卓小さい → 5% 未満で fallback--mask-min-area-pct 2.0 で閾値下げ
暗転 / フェード緑領域なし → fallbackそのまま fallback で問題なし
HSV 範囲外の緑 (照明異常)緑として認識されず fallbackHSV 範囲を動画ごとに調整 (sec4 参照)

3.3 フォールバック発生率の監視

評価フレーム群全体に対する fallback 発生率を 5% 以下に保つことを目標とする。 それ以上なら HSV 範囲 / min_area_pct を見直す必要がある。

# 監視用ログ収集
fallback_count = 0
for frame in test_frames:
    _, info = table_region_mask(frame)
    if info["fallback"]:
        fallback_count += 1
print(f"fallback rate: {fallback_count / len(test_frames) * 100:.1f}%")

4. パイプライン順序 (マスキング → YOLO → 後処理)

[入力動画フレーム]
       │
       ▼
[1. 卓マスキング前処理]    ← 本ガイドの主題
       │   (fallback 時は素通し)
       ▼
[2. YOLO 推論 (公開モデル / MVP)]
       │   - 34 牌種 bbox + 信頼度
       │
       ▼
[3. orientation 分類]      ← 立/伏 二値判定 (Pose3D 連携)
       │
       ▼
[4. 鳴き数判定]            ← 横向き bbox を副露としてカウント
       │
       ▼
[5. アガリ判定 / 点数推定] ← 視覚 + 状態遷移
       │
       ▼
[出力: 検出結果 JSON / 可視化 PNG]

4.1 順序の根拠

4.2 鳴き対応との相互作用

注意: 副露 (ポン/チー) は卓の縁に近い位置に並べられることが多く、 dilate_pct=5.0 で十分含まれるが、念のため副露多発フレームで動作確認すること。 マスクされて副露が消える場合は --mask-dilate-pct 8.0 に上げて再評価。

4.3 動画 (時系列) パイプラインでの最適化

動画では卓位置がほぼ固定 (カメラ切替時のみ変動)。マスクを 1 秒間隔でキャッシュすることで 計算コストを削減できる:

cached_mask = None
cached_fnum = -10000

for fnum, frame in enumerate(video_frames):
    if fnum - cached_fnum >= 30:  # 30 fps 動画で 1 秒
        cached_mask, info = table_region_mask(frame)
        cached_fnum = fnum
    if cached_mask is not None:
        frame = apply_mask(frame, cached_mask)
    detections = model.predict(frame)

5. 訓練データへの適用 (Roboflow Auto-Label 精度向上)

核心アイデア: Roboflow の Auto-Label (自動アノテーション) は 背景に含まれる文字・ロゴに引きずられて誤ラベルを付ける。 マスク済画像を上げれば、Auto-Label は卓上だけを見るので精度が劇的に向上する。

5.1 Roboflow へのアップロード前処理

# 一括マスキング (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)"
}

# その後 Roboflow にアップロード (CLI)
roboflow upload \
    --project mahjong-mleague-v2 \
    --version pending \
    --workspace shimanto-ai \
    --image-dir mleague/frames-roboflow-masked/

5.2 Auto-Label 精度比較 (想定)

条件Auto-Label 正解率手動修正工数 (100 枚)
マスクなし60〜70%3〜5 h
マスクあり80〜90%1〜2 h

5.3 訓練と推論の一貫性

マスク済画像で訓練すれば、推論時もマスクを期待するので一貫性が保たれる。 マスクなしで推論する場合は別途 fine-tune を実施 (混合訓練でドメイン頑健性を獲得)。

5.4 リスク

リスク: マスク済画像のみで訓練すると、フォールバック (マスクなし) 時に性能が落ちる。 対策: 訓練データの 20% はマスクなしを含める (augmentation 的に混ぜる)。

6. A/B 評価方法

6.1 評価フレーム群の準備

  1. 同じテストフレーム 100 枚を用意 (mleague/eval/ 配下)
  2. 各フレームに対して 2 通り実行: マスクあり / マスクなし
  3. 同一モデル ID で公平に比較 (test-upsgd/mahjong-tiles-oc9zz 等)

6.2 評価スクリプト雛形

def evaluate_ab(test_frames, model, output_dir):
    results = {"with_mask": [], "without_mask": []}
    for frame_path in test_frames:
        frame = cv2.imread(frame_path)
        # Without mask
        dets_raw = model.predict(frame)
        results["without_mask"].append({"path": frame_path, "n": len(dets_raw), "dets": dets_raw})
        # With mask
        mask, info = table_region_mask(frame)
        frame_m = apply_mask(frame, mask) if not info["fallback"] else frame
        dets_masked = model.predict(frame_m)
        results["with_mask"].append({"path": frame_path, "n": len(dets_masked), "dets": dets_masked})
    # 集計
    summary = {
        "without_mask_total": sum(r["n"] for r in results["without_mask"]),
        "with_mask_total":    sum(r["n"] for r in results["with_mask"]),
        "reduction":          sum(r["n"] for r in results["without_mask"]) -
                              sum(r["n"] for r in results["with_mask"]),
    }
    return results, summary

6.3 比較指標 (記録項目)

指標マスクなしマスクあり差分備考
検出件数合計絶対件数の総数
FP 件数GT 比較が必要
TP 件数マスクで TP が減らないことを確認
1 枚平均検出数
fallback 発生率マスクありのみ

6.4 結果の判定基準

7. 既知の問題と対策

7.1 緑シャツのプレイヤー問題

問題: プレイヤーが緑系のシャツ (チームカラー) を着ている場合、 シャツが卓と HSV 緑域で連結し、マスクに含まれてしまう可能性。 その結果、シャツ上の文字 / ロゴで FP が発生する。

7.2 連結回避の対策案

対策実装難易度効果
connectedComponents の connectivity=4 低 (1 行) 細い橋での連結を切断
shape 分析: 卓は楕円フィット率高、シャツは低 誤連結時に分割可能
top-N 連結成分の 2 番目がシャツの場合は除外 シャツが第 1 候補にならない限り有効
ChArUco マーカーで卓の幾何を強制 高 (運用変更) 確実だが M-League 制作側の協力が必要
SAM (Segment Anything) でクラス無しセグ 高 (推論コスト) 緑連結問題を構造的に解消

7.3 ハイライト / 反射の問題

問題: スタジオ照明の反射で、卓の一部が「ハイライト = HSV 緑域外」になり、マスクに穴が空く。
対策: Step 4 (凸包 + dilate) が既に穴を埋める設計のため通常問題ないが、 ハイライトが広範な場合は dilate_pct を 8〜10% に上げる。

7.4 卓の左右端切れ問題

問題: カメラショットで卓が画面外まではみ出している場合、凸包が画面端で切れる。
対策: 凸包の代わりに 外接矩形 (axis-aligned bounding box) を使うと、 画面端まで矩形が広がる。トレードオフは余分な背景が含まれること。

7.5 倍速 / スロー再生時の動的ブレ

問題: 動的シーンでカメラがパンすると HSV 範囲が一時的にズレる。
対策: 時間軸スムージング (前後 N フレームのマスク平均) で対応。 Section 4.3 のキャッシュ機構と組み合わせると安定する。

8. 改善方向

8.1 短期改善 (1〜2 週間)

項目工数期待効果
connectivity=4 + 第 2 連結成分除外ロジック2 h緑シャツ問題の 70% を解消
動画ごとの HSV 範囲チューニング (sample01/02 個別)3 hfallback 発生率を 5% → 2% に削減
時間軸スムージング (キャッシュ 1 秒)4 hカメラ切替時のジッタ低減

8.2 中期改善 (1〜2 ヶ月)

項目工数期待効果
ChArUco マーカー運用 (物理マーカー設置)運用調整 + 1 週マスク精度 100% (運用協力が前提)
SAM (Segment Anything) ベースのセグ1 週 (推論統合)緑問題を構造的に解消、ただし推論コスト増
Roboflow で table vs non-table セグモデル訓練2 週 (アノテ 100 枚 + 訓練)M-League 専用最適化

8.3 長期改善 (3〜6 ヶ月)

8.4 推奨ロードマップ

  1. Week 1: 5 スクリプトに --use-table-mask 統合 (本ガイド)
  2. Week 2: A/B 評価実施 (評価テンプレート 使用)
  3. Week 3: 緑シャツ対策 (connectivity=4 + 第 2 連結成分除外)
  4. Week 4: マスク済画像で Roboflow 再アノテ → MVP v2 訓練データ準備
  5. Month 2-3: SAM ベース置換を検討。MVP v2 で十分なら見送り
結論: 本ガイドの統合作業は 1 週間で完了する低リスク改善。 その後の A/B 評価で効果が確認できれば、評価テンプレート の Section 8 (前処理 A/B) に結果を記録し、 MVP モデルとの相乗効果を本番展開の判断材料とする。