合成データ生成パイプライン実践ガイド — アノテーション工数ゼロで 5,000 枚
タイル素材 × 背景 × 配置ロジック × augmentation で YOLO ラベル付き合成データを大量生成する実装方針
作成日: 2026-05-14
目的: 訓練データ拡張
期待効果: mAP +5〜10%
前提: 実物 500 枚 + 合成 5,000 枚 (1:10)
← INDEX
設計ボトルネック
公開モデル素性
類似モデル
転移学習
卓上マスキング
1. なぜ合成データが必要か
核心:
実物 M-League フレームへの手動アノテーションは 1 枚あたり 1〜3 分 (34 種 × 14 個の牌 = 約 476 ラベル/フレーム)。
500 枚揃えるには 15〜25 時間 の単純作業が必要。
一方で合成データなら、ラベル生成は配置時に自動付与されるため、
5,000 枚を 計算時間のみ (人手 0) で得られる。
1.1 工数比較
| 方式 | 枚数 | 人手 | 所要時間 | 金額換算 (¥5,000/h) |
| 手動アノテ (実物) | 500 | あり | 15〜25 h | ¥75,000〜¥125,000 |
| Roboflow Auto-Label + 修正 | 500 | あり (1/3 程度) | 5〜8 h | ¥25,000〜¥40,000 |
| 合成生成 | 5,000 | 0 | 2 h (計算) | ¥0 (電気代のみ) |
1.2 合成データが向く問題
- クラス不均衡の是正 — 字牌・赤ドラなど実フレームでの出現頻度が低いカテゴリを均等にサンプル可能
- occlusion / rotation などのエッジケースを系統的に生成 — 「手で隠れた牌」「斜め置きの牌」を意図的に大量投入
- 背景バリエーション — 緑フェルト以外 (木目・赤・青) の卓でも検出可能にする (将来の市販卓対応)
1.3 合成データが向かない問題
- 真の照明変動 / モーションブラー — シミュレーションでは限界がある
- カメラ固有歪み (M-League 特有のレンズ収差・色補正)
- 本番ドメイン特有の人工物 (字幕オーバーレイ・スポンサーバナー)
2. パイプライン全体像
[1. タイル素材ライブラリ] ← 34 種 × N アングル (PNG, 透過背景)
│
▼
[2. 背景ライブラリ] ← 10〜30 種 (緑フェルト / 木目 / 単色 / 実フレーム)
│
▼
[3. 配置サンプラ] ← random / grid / hand-of-13 / discard-row パターン
│
▼
[4. occlusion / rotation] ← 手で隠す / 斜め / 重ね / 倒し
│
▼
[5. augmentation] ← brightness, blur, noise, JPEG re-encode, color jitter
│
▼
[6. YOLO ラベル自動生成] ← 配置時の bbox を YOLO 形式 (cls cx cy w h) で保存
│
▼
[出力: image_NNNN.jpg + image_NNNN.txt 5,000 ペア]
2.1 ディレクトリ構成 (提案)
synthetic/
├── assets/
│ ├── tiles/ # 34 種 PNG (透過)
│ │ ├── m1.png ... m9.png
│ │ ├── p1.png ... p9.png
│ │ ├── s1.png ... s9.png
│ │ └── z1.png ... z7.png
│ └── backgrounds/ # 緑フェルト / 木目 / 実フレーム crop
│ └── *.jpg
├── output/
│ ├── images/ # image_00001.jpg ... image_05000.jpg
│ └── labels/ # image_00001.txt ... image_05000.txt
├── generate.py # メインスクリプト
└── config.yaml # クラス定義 / 配置パラメータ
2.2 想定 CLI
# 5,000 枚生成 (フル素材モード)
python scripts/generate-synthetic-mahjong.py \
--tiles synthetic/assets/tiles \
--backgrounds synthetic/assets/backgrounds \
--out synthetic/output \
--count 5000 \
--seed 42
# 素材なし procedural モード (素材準備が間に合わない場合)
python scripts/generate-synthetic-mahjong.py \
--procedural \
--out synthetic/output \
--count 2000
3. 先行事例: Augmented Startups Playing Cards
トランプ検出の Roboflow データセット augmented-startups/playing-cards-ow27d
は 10,100 枚の合成 + augmentation で 24,240 枚に拡張した代表例。
手法は単純だが効果が高く、麻雀タイルにそのまま転用可能。
3.1 Augmented Startups の手法サマリ
| 段階 | 処理 | 枚数倍率 |
| ① ベース合成 | 52 種カード × N アングル × ランダム配置 | 10,100 枚 |
| ② 90°/180°/270° 回転 | 4 倍化 | 40,400 枚 (相当) |
| ③ rotation ±15° / shear ±10° | 幾何 augmentation | — |
| ④ brightness / contrast / hue | 色変動 | — |
| ⑤ blur / noise / cutout | 劣化系 | — |
| 最終: train/valid/test split | 24,240 / 1,300 / 700 (Roboflow 既定) | — |
転移の根拠: 詳細は
転移学習可能性 Section 3 参照。
トランプと麻雀牌は「平面の長方形物体」「シンボル + 数字」「卓上に並べる」という共通点が強く、
pretraining として有効。本セクションはその合成データ生成手法を麻雀に展開する具体策。
3.2 麻雀との違い
| 項目 | トランプ | 麻雀 |
| クラス数 | 52 | 34 (赤ドラ込みで 37) |
| 形状 | 薄い長方形 (2D) | 厚みのある直方体 (3D) |
| 方向 | 4 方向 (90° 単位) | 4 方向 + 倒れ (立/伏) |
| 配置単位 | ハンド (5 枚) / 卓上ばら | 手牌 13/14 列 + 河 + 副露 |
| 背景 | 緑フェルト / 木目 | 緑フェルト (M-League 専属) |
4. 麻雀での実装方針 (34 種 × 10 背景)
4.1 タイル素材の収集
34 種 × 複数アングルの PNG が必要。入手経路:
- 既存麻雀アプリのスプライト — 雀魂・天鳳のスクリーンショットから crop (要ライセンス確認)
- Wikimedia Commons の Mahjong tile set — public domain SVG (`File:MJ*.svg`) を PNG 化
- 手撮り素材 — 実物の麻雀牌セット 1 セットを buy & shoot (¥3,000〜¥10,000)
- 3D レンダリング — Blender で 34 種モデル + ライティング Variant (上級者向け)
推奨: Wikimedia の SVG セット (CC0/Public Domain) を 256×384 PNG にレンダリング、
+ 実物 1 セットを 4 アングル (真上・斜め前・斜め後・水平) 撮影で組み合わせ。
初期投資 ¥5,000 + 半日作業で揃う。
4.2 配置パターン (4 種)
| パターン名 | 説明 | 出力割合 |
hand-of-13 | 手牌風 (13 枚 1 列 + ツモ 1 枚) | 30% |
discard-row | 河 (6 列 × 1〜4 行) | 30% |
meld | 副露 (ポン/チー/カン) | 15% |
random-scatter | ランダム散布 (occlusion 学習用) | 25% |
4.3 occlusion / rotation の付与
# 配置後にランダム occlusion を適用
def apply_occlusion(image, bboxes, occlusion_rate=0.15):
"""画像にランダム矩形 (手・腕・他牌) を描画して bbox を一部隠す"""
h, w = image.shape[:2]
n_occlusions = np.random.poisson(2)
for _ in range(n_occlusions):
ox = np.random.randint(0, w)
oy = np.random.randint(0, h)
ow = np.random.randint(40, 120)
oh = np.random.randint(40, 120)
color = (np.random.randint(20, 80),) * 3 # 暗色矩形 = 手の影
cv2.rectangle(image, (ox, oy), (ox+ow, oy+oh), color, -1)
# bbox の occlusion 率を計算し、>70% 隠れたものはラベルから削除
return filter_visible_bboxes(image, bboxes, threshold=0.30)
4.4 perspective 変形
def random_perspective(tile_img, max_skew=0.15):
"""牌に台形変形を加えて斜め視点をシミュレート"""
h, w = tile_img.shape[:2]
src = np.float32([[0,0],[w,0],[w,h],[0,h]])
dst = src + np.random.uniform(-max_skew, max_skew, (4,2)) * np.array([w,h])
M = cv2.getPerspectiveTransform(src, dst.astype(np.float32))
return cv2.warpPerspective(tile_img, M, (w, h), borderValue=(0,0,0,0))
4.5 YOLO ラベル自動生成
配置時の bbox を YOLO 形式 (正規化中心座標 + 幅高) で保存:
def write_yolo_label(label_path, bboxes, img_w, img_h):
"""bboxes = [(cls_id, x1, y1, x2, y2), ...]"""
with open(label_path, "w") as f:
for cls_id, x1, y1, x2, y2 in bboxes:
cx = (x1 + x2) / 2 / img_w
cy = (y1 + y2) / 2 / img_h
bw = (x2 - x1) / img_w
bh = (y2 - y1) / img_h
f.write(f"{cls_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}\n")
重要: ラベル生成は 配置時の bbox をそのまま記録するだけ。
検出器を回す必要は一切ない。これが「アノテーション工数 0」の核心。
5. procedural mode (素材なしフォールバック)
用途: 素材ライブラリの収集が間に合わない場合、
あるいは「形状の汎化能力」を先に確認したい場合の中間ステップ。
本物の見た目ではないが、「平面の長方形 + シンボル」のジオメトリだけは学習できる。
5.1 procedural モードの設計
- 背景: 緑フェルト風グラデーション (HSV: H=70±10, S=120±30, V=110±30)
- 牌: 白い角丸長方形 (60×85 px) + 上部にシンボル文字を描画
- シンボル文字:
- 萬子: 漢数字 (一〜九) + 「萬」
- 筒子: 円マーク × n 個 (Unicode `●`)
- 索子: 縦線 × n 本 (1 索だけ鳥アイコン代用)
- 字牌: 東/南/西/北/中/發/白 (1 文字)
- 影: 各牌の右下に半透明黒矩形でドロップシャドウ
5.2 procedural サンプルコード (短縮)
TILE_LABELS = {
"m1": "一萬", "m2": "二萬", ..., "m9": "九萬",
"p1": "①", "p2": "②", ..., "p9": "⑨",
"s1": "鳥", "s2": "‖", ..., "s9": "‖‖‖‖‖‖‖‖‖",
"z1": "東", "z2": "南", ..., "z7": "中",
}
def render_procedural_tile(cls_name, size=(60, 85)):
img = np.full((size[1], size[0], 3), 240, dtype=np.uint8) # ベージュ
cv2.rectangle(img, (0,0), (size[0]-1, size[1]-1), (180,180,180), 2)
label = TILE_LABELS[cls_name]
# PIL で日本語文字描画 (cv2.putText は日本語不可)
pil = Image.fromarray(img)
draw = ImageDraw.Draw(pil)
font = ImageFont.truetype("msgothic.ttc", 28)
draw.text((size[0]//2 - 14, size[1]//2 - 18), label, font=font, fill=(40,40,40))
return np.array(pil)
5.3 procedural モードの限界
procedural で生成した牌は 本物の麻雀牌と見た目が大きく異なる ため、
procedural のみで訓練したモデルは実 M-League フレームで検出率が低い (推定 mAP 0.15〜0.30)。
形状汎化の Pre-training 用途と割り切り、必ず本物素材データと混ぜて使う。
6. 既知の限界とドメインシフト対策
6.1 ドメインシフトの正体
合成データで訓練したモデルが実物データで性能を出さない現象は "sim-to-real gap" と呼ばれ、
主に以下の要因による:
| 要因 | 合成の特徴 | 実物の特徴 |
| 照明 | 均一・きれい | テレビ照明・反射・スポットあり |
| ぼけ | シャープ | カメラ動き・被写界深度のぼけ |
| 圧縮 | 無劣化 PNG/JPEG q=95 | H.264 動画 → JPEG q=70 で圧縮アーティファクト |
| ノイズ | ほぼなし | センサーノイズ・露出変動 |
| 色再現 | RGB そのまま | 放送機器のカラーグレーディング |
6.2 対策 (augmentation 強度を上げる)
# Albumentations による sim-to-real 緩和 augmentation
import albumentations as A
transform = A.Compose([
A.MotionBlur(blur_limit=(3, 9), p=0.4), # 動きぼけ
A.GaussianBlur(blur_limit=(3, 7), p=0.3),
A.GaussNoise(var_limit=(10, 50), p=0.5), # センサーノイズ
A.ImageCompression(quality_lower=50, quality_upper=85, p=0.7), # JPEG 劣化
A.RandomBrightnessContrast(brightness_limit=0.3, contrast_limit=0.3, p=0.6),
A.HueSaturationValue(hue_shift_limit=10, sat_shift_limit=20, val_shift_limit=20, p=0.5),
A.RGBShift(p=0.3), # 放送機器のカラー再現
A.RandomShadow(p=0.2),
A.CLAHE(p=0.2),
], bbox_params=A.BboxParams(format="yolo", label_fields=["class_labels"]))
6.3 段階的 augmentation 強度
- P=0.7 の JPEG 圧縮 — 実フレームの主な劣化要因
- P=0.5 の motion blur / noise — 動画キャプチャの劣化を模倣
- P=0.3 程度の色変動 — テレビ放送のカラーグレーディング
- 過度な augmentation は逆効果 — sanity check として augmented サンプルを 50 枚目視確認
落とし穴: Albumentations の bbox 変形は cutout / shear / rotate で
bbox 範囲外の物体まで bbox にトリミングされる。
検証フェーズで「ラベルなのに物体がない」枚を 10 枚目視確認すること。
7. 実物データとの混合比率
推奨混合比: 実 500 + 合成 5,000 = 1:10
実物データで「本番ドメイン」を、合成データで「クラス均衡 + occlusion バリエーション」を担当させる分業。
純粋合成のみは sim-to-real gap で失敗するため、必ず実物を混ぜる。
7.1 比率と期待性能 (推定)
| 構成 | 実物 | 合成 | 期待 mAP@0.5 | 備考 |
| 実物のみ | 500 | 0 | 0.60〜0.70 | クラス不均衡で字牌が弱い |
| 合成のみ | 0 | 5,000 | 0.15〜0.35 | sim-to-real gap で失敗 |
| 1:1 | 500 | 500 | 0.65〜0.75 | 合成の効果が薄い |
| 1:10 (推奨) | 500 | 5,000 | 0.78〜0.88 | クラス均衡 + 実物ドメイン両立 |
| 1:20 | 500 | 10,000 | 0.75〜0.85 | 合成が支配的になり実物軽視 |
7.2 学習スケジュール (2 段階)
- Stage 1: 合成で warm-up (50 epoch)
- Stage 2: 実物 + 合成混合で fine-tune (30 epoch)
- 実物 500 + 合成 5,000 をバッチサンプリング比 1:1 でサンプリング
- learning rate を Stage 1 の 1/10 に
7.3 evaluation の独立性
必須: 評価セット (test 100 枚) は 実物のみ。
合成データを評価に混ぜると本番性能を過大評価する。
8. 期待効果と評価方法
8.1 mAP 寄与の期待値 (推定)
| 条件 | mAP@0.5 | 差分 |
ベースライン: 公開モデル test-upsgd/mahjong-tiles-oc9zz | 0.70 | — |
| 実物 500 のみで fine-tune | 0.78 | +0.08 |
| 実物 500 + 合成 5,000 (1:10) | 0.83〜0.88 | +0.13〜+0.18 |
| + 卓上マスキング前処理 | 0.86〜0.91 | +0.16〜+0.21 |
8.2 A/B 評価設計
同じ評価セット 100 枚に対して以下 3 モデルを比較:
- Model A: 公開モデル (ベースライン)
- Model B: 実物 500 のみで fine-tune
- Model C: 実物 500 + 合成 5,000 で fine-tune
詳細な評価テンプレートは model-evaluation-template.html 参照。
8.3 デバッグ用 sanity check
- 合成 50 枚をランダムサンプリングして 目視で bbox 整合性を確認
- 合成データのみで訓練した一時モデルの test 性能が 0.10 以上あること (procedural モードでも 0.05 は出ること)
- 合成データのクラス分布が均等であること (各クラス 200 枚以上)
8.4 ROI 試算
| 項目 | 工数 | 金額 |
| 素材収集 (Wikimedia + 実物撮影) | 4 h | ¥20,000 + 素材 ¥5,000 |
| generate.py 実装 | 8 h | ¥40,000 |
| 5,000 枚生成 (計算のみ) | 2 h | ¥0 (電気代) |
| sanity check + 調整 | 3 h | ¥15,000 |
| 合計 | 17 h | ¥80,000 |
| これに対して実物 5,000 枚手動アノテだと | 150〜250 h | ¥750,000〜¥1,250,000 |
結論: 合成データ生成パイプラインは 10 倍以上のコスト効率を持つ。
手動アノテに頼らない訓練ループが構築できれば、モデル改善サイクルが大幅に加速する。