ゲームの録画を見返していると、「このキルシーンだけ切り出したい」と思うことがあります。
ただ、1本ずつ動画を開いて、タイムラインを動かして、良さそうな場面を探して、前後を切り出して……という作業はかなり面倒です。
そこで、FPSの録画ファイルからキルシーンを自動で検知し、その前後だけを動画として保存するツールを作りました。
対象にしたのは、Delta ForceなどのFPS録画です。仕組みとしては、画面中央付近に出る「キル」「ロングショットキル」「ヘッドショットキル」などの日本語テキストをOCRで読み取り、検知できたタイミングの前後をFFmpegで切り出します。
作ったもの
構成はシンプルで、中心になるのはPythonスクリプトです。
video_analyzer/
├── input/
│ └── videos/
├── output/
├── config.json
├── compose.yaml
├── Dockerfile
└── detect_kills.py
実行はPodman Composeから行います。
podman compose run --rm video-analyzer
動画ファイルや出力先は config.json に書いておけるようにしました。
{
"input_dir": "/app/input",
"output_dir": "/app/output",
"videos": [
"/app/input/videos"
],
"frame_skip": 5,
"ocr_engine": "onnxocr",
"ocr_use_gpu": false,
"ocr_lang": "japan",
"ocr_interval_sec": 1.5,
"ocr_scale": 1.5,
"ocr_preprocess_mode": "color",
"kill_group_gap_sec": 4.0,
"kill_offset_sec": 7.5,
"post_kill_sec": 7.5
}
videos には動画ファイルそのものも、動画が入ったディレクトリも指定できます。Windows側のパスをWSL上で扱うこともあるので、C:\Users\... のようなパスは /mnt/c/... に変換する処理も入れています。
なぜOCRで検知するのか
最初に考えたのは、音や映像の変化でキルシーンを検知する方法です。
ただ、FPSの画面は常に動いています。銃声、爆発、UI、味方の通知、目標占領の表示など、キル以外でも派手な変化がたくさんあります。そのため、画面全体の差分や音量だけで判定すると誤検知が増えそうでした。
一方で、キルが発生した瞬間には画面上に「キル」系のテキストが出ます。
つまり、動画を直接理解しようとするより、ゲームが表示している結果テキストを読むほうがシンプルです。
今回のツールでは、OpenCVで動画フレームを読み込み、画面中央下あたりのROIだけを切り出し、そこにOCRをかけています。
ROIを絞ってOCRを軽くする
動画の全画面にOCRをかけると重くなりますし、余計なUIまで読んでしまいます。
そこで、まずは画面中央から下にかけて大きめのROIを作ります。
def get_roi_rect(frame_width, frame_height):
center_x = frame_width // 2
center_y = frame_height // 2
x1 = center_x - 500
x2 = center_x + 500
y1 = center_y - 100
y2 = frame_height
return x1, y1, x2, y2
さらに、そのROIの中でもOCRにかける範囲を比率で指定できるようにしました。
"ocr_subroi_x1_ratio": 0.2,
"ocr_subroi_x2_ratio": 0.8,
"ocr_subroi_y1_ratio": 0.3,
"ocr_subroi_y2_ratio": 0.76
ゲームの解像度やUI位置が少し変わっても、設定値を調整すれば対応しやすくなります。
OCR前処理はカラー寄りにした
OCR前処理では、二値化する方法も試せます。
ただ、ゲーム画面のテキストは背景が動いたり、エフェクトが重なったりします。強く二値化すると文字の一部が欠けて、逆に読み取りづらくなることがありました。
そのため、現在の既定値は color にしています。
if preprocess_mode == "color":
lab = cv2.cvtColor(scaled, cv2.COLOR_BGR2LAB)
l_channel, a_channel, b_channel = cv2.split(lab)
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
enhanced_l = clahe.apply(l_channel)
enhanced = cv2.merge((enhanced_l, a_channel, b_channel))
return cv2.cvtColor(enhanced, cv2.COLOR_LAB2BGR)
色は残したまま、明度だけを軽く補正する形です。
OCRエンジンには onnxocr を使い、保険として tesseract も選べるようにしています。GPUなしでも動かしたかったので、既定ではCPU推論です。
「キル」を含むテキストを拾う
検知ルールはかなり素直です。
def is_kill_scene_ocr_text(text):
text = normalize_ocr_text(text)
if not text:
return False
if "キル" not in text:
return False
if "アシストキル" in text:
return False
if "スキル" in text:
return False
return True
ダブルキル や ロングショットキル のような固定語を全部リストアップするのではなく、「キル」を含むかどうかで広めに拾います。
ただし、アシストキル や スキル はキルシーンとして扱いたくないので除外しています。
OCRでは文字が微妙に崩れることがあるので、先に表記ゆれも少し補正しています。たとえば半角カナの キ や ル を キ、ル に寄せるような処理です。
フレームを全部読まない
動画の全フレームにOCRをかけると、かなり時間がかかります。
そこで、ocr_interval_sec で指定した間隔ごとにOCRを実行するようにしました。
if fps > 0 and ocr_interval_sec > 0:
effective_frame_skip = max(frame_skip, int(round(fps * ocr_interval_sec)))
else:
effective_frame_skip = frame_skip
たとえば60fpsの動画で ocr_interval_sec が 1.5 なら、おおよそ90フレームごとにOCRを実行します。
精度を上げたいときは間隔を短く、速度を優先したいときは間隔を長くできます。
連続キルは1本の動画にまとめる
キルを1回検知するたびに即切り出すと、連続キルの場面が細かい動画に分かれてしまいます。
そこで、最後に検知したキルから一定時間以内に次のキルが来た場合は、同じシーンとして扱います。
設定値は kill_group_gap_sec です。
"kill_group_gap_sec": 4.0
内部では、最初にキルを検知した時刻と、最後にキルを検知した時刻を持っておきます。
キルが途切れて kill_group_gap_sec を超えたら、そこではじめて1本のシーンとして切り出します。
start = max(0, first_kill_ts - kill_offset_sec)
end = last_kill_ts + post_kill_sec
この設計にすると、単発キルは前後だけを短く保存でき、連続キルは流れを残したまま1本にできます。
切り出しはFFmpegに任せる
動画の切り出しはPythonで頑張らず、FFmpegを呼び出しています。
cmd = [
"ffmpeg",
"-y",
"-ss", str(start_time),
"-t", str(duration),
"-i", video_path,
"-c:v", "libx264",
"-preset", "fast",
"-crf", "14",
"-c:a", "copy",
out,
]
動画は libx264 で再エンコードし、音声はそのままコピーします。
録画ファイルの形式によっては完全コピーだけだと切り出し位置がずれることがあるので、画質をある程度保ちながら安定して切り出せる設定にしました。
デバッグ画像を残す
OCR系の処理で一番困るのは、「なぜ検知できなかったのか」が見えづらいことです。
そのため、キルシーンを検知したタイミングでは、次の画像を debug/ に保存します。
- ROI全体
- OCRにかけたsubROI
- OCR前処理後の画像
ファイル名には検知時刻とOCR結果の一部も含めるようにしています。
これにより、あとから「ROIがずれている」「二値化で文字が消えている」「OCRは読めているけど除外ルールに引っかかっている」などを確認できます。
Podmanで動かす理由
このツールはOpenCV、OCR、FFmpegを使います。
ローカル環境に直接入れていくと、OSやPython環境によって動作差が出やすくなります。特にOCRやFFmpegまわりは、あとから再現するのが面倒になりがちです。
そこで、実行環境はPodmanでまとめています。
FROM python:3.11-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
libgl1 \
libglib2.0-0 \
ffmpeg \
tesseract-ocr \
tesseract-ocr-jpn
compose.yaml では、設定ファイル、入力ディレクトリ、出力ディレクトリをコンテナにマウントしています。
services:
video-analyzer:
build: .
entrypoint: ["python", "detect_kills.py"]
volumes:
- ./config.json:/app/config.json
- ./input:/app/input
- ./output:/app/output
- /mnt:/mnt:ro
- /home:/home:ro
WSL上でWindows側の録画ファイルを読むこともあるので、/mnt と /home は読み取り専用でマウントしています。
実装してみて感じたこと
今回のような動画解析では、最初からAIで全部を理解させるより、「画面に出ている確定情報を読む」ほうが実装しやすい場面があります。
キルシーンの検知も、映像の意味を推測するのではなく、ゲーム側が表示した「キル」という結果を拾うだけです。
その代わり、OCRの読み取り位置、前処理、検知間隔、除外語、連続キルのまとめ方など、小さい調整ポイントが効いてきます。
今後改善するなら、次のあたりを入れたいです。
- キル確定に必要な検出回数を設定から切り替える
- OCR結果のログをCSVやJSONで残す
- 検知したシーンを一覧するHTMLレポートを生成する
- ゲームごとにROIプリセットを切り替える
- 抽出したキルシーンを自動で1本のモンタージュにまとめる
手作業で動画を探す時間を減らせるだけでもかなり便利なので、録画をよく残す人にはこういう自動化は相性が良いと思いました。