32blogby Studio Mitsu

fqmpegでサムネイル・フレーム抽出・スライドショー: 12動詞を実装読みで全網羅

fqmpegの12動詞でサムネ・コンタクトシート・全フレーム抽出・シーン分割・スライドショー・横並び比較まで。実装由来の挙動と --dry-run 出力を全部載せる。

by omitsu38 min read
FFmpegfqmpegCLIthumbnails
目次

fqmpeg の C12 クラスタは 動画 ↔ 個別フレーム の往復だ。単発サムネ、定期的なスナップショット、コンタクトシート、フィルムストリップ、シーン検出による自動分割、静止画からのスライドショー、横並び比較、ffprobe でのフレーム数カウント — 全部で 12 動詞。1 つだけ ffprobe で、残りは全部 ffmpeg が動くが、生のフィルタチェーンよりはるかに単純な引数を露出している。

この記事では fqmpeg 3.0.3src/commands/ を実読しながら、各動詞の元 FFmpeg フィルタ、デフォルト値、出力ファイル名、--help だけでは見えない罠(thumbnail-gridtile はサンプリングは同じだがデフォルトファイル名が違う、snapshotvideo-to-frames は両方とも定期スチル抽出だが出力ディレクトリのデフォルトが違う、frames-to-videoslideshow は両方とも画像から動画を作るが内部機構は全然違う)を全部書く。

この記事で得られるもの

  • 12 動詞をタスクで選ぶ早見表(単発スチル / 複数スチル / コンタクトシート / 動画再構築 / 解析)
  • 各動詞が出す FFmpeg 起動行(--dry-run 検証済み)
  • デフォルト・単位・出力ファイル名 — そして似た動詞同士の違い(thumbnail-grid vs tilesnapshot vs video-to-frames
  • 実例 3 本: YouTube サムネ作成、防犯カメラからのタイムラプス、フィルタ前後比較動画

12 動詞の全体図

クラスタは 4 タスクグループに分かれる。グループを選んで、動詞を選ぶ。

グループ動詞やること
単発・定期スチルthumbnail, snapshot, video-to-frames, count-frames任意時点の 1 枚、定期間隔の連番、全フレーム抽出、フレーム数カウント
コンタクトシート・フィルムストリップthumbnail-grid, thumbnail-strip, tile複数フレームを 1 枚の画像に合成(グリッド or 横一列)
フレーム ↔ 動画再構築frames-to-video, slideshow連番パターンから動画、画像リストからスライドショー
解析・合成scenes, preview, compareシーン切れ検出、ハイライト動画、横並び比較

読み始める前に押さえておきたい 5 つのポイント:

  1. thumbnail-gridtile はどちらもコンタクトシートで、サンプリングは同じ時間ベース。 両方とも select='isnan(prev_selected_t)+gte(t-prev_selected_t, 1)' で 1 秒に 1 フレーム選ぶ。短い動画も長い動画も予測通り。2 つの動詞は発見性のために残してある別名関係 — 「grid」で検索しても「tile」で検索してもここに辿り着くようにしている。違うのはデフォルト出力ファイル名だけ(-grid.jpg vs -tile<C>x<R>.jpg)。
  2. snapshot は入力と同じディレクトリに出力、video-to-frames はカレントディレクトリに出力。 どちらも定期スチル抽出だが、デフォルトの出力パスが違う — snapshot<入力dir>/<stem>-snap-%04d.jpgvideo-to-frames は cwd の ./frame_%04d.png。予測可能なパスにしたいなら必ず -o を渡すこと。
  3. count-frames は C12 で唯一 ffmpeg ではなく ffprobe を使う動詞。 ffprobe -count_frames -select_streams v:0 を実行して整数を 1 つ標準出力に書く。ファイルは作らない。
  4. frames-to-video-r(出力レート)ではなく -framerate(入力レート)を使う。 これは重要で、-framerate は静止画列を「秒間何枚として解釈するか」を FFmpeg に教える指定で、-r は出力ストリームを再タイミングする指定。一定レートで画像→動画を作るなら -framerate が正解で、fqmpeg もこれを使っている。
  5. scenes はシーン切れで自動分割する。 各シーンが独立ファイルになる(<stem>-scene000.mp4<stem>-scene001.mp4、...)— 長い録画から自動でクリップを切り出すのに便利。閾値は 0.01.0、低いほど敏感(カット数が増える)。

単発・定期スチル

thumbnail — 任意時点の 1 フレームを 1 枚の画像に

クラスタで一番シンプルな動詞: 指定タイムスタンプにシークして JPEG を 1 枚書く。動画サムネ(YouTube アップロード、ギャラリーカバー、OG 画像タグ)に使う。

引数 / オプションデフォルト備考
<input>必須入力動画
-s, --start <sec>1秒数(小数可)
-o, --output <path><入力stem>-thumb.jpg出力上書き
bash
$ npx fqmpeg thumbnail input.mp4 --dry-run

  ffmpeg -ss 1 -i input.mp4 -frames:v 1 -q:v 2 input-thumb.jpg
bash
$ npx fqmpeg thumbnail input.mp4 -s 45.5 -o cover.jpg --dry-run

  ffmpeg -ss 45.5 -i input.mp4 -frames:v 1 -q:v 2 cover.jpg

-ss-i の前に来るのは高速シーク形式。 -ss-i より前に置くと FFmpeg はキーフレーム位置にスナップしてからデコードする。長尺ファイルでは劇的に速いが、指定タイムスタンプぴったりではなく最寄りのキーフレームに着地する。コンテンツサムネの用途なら問題ない — キーフレームは普通 0.5 秒間隔くらい。フレーム精度が必要なら -ss-i の後に置く必要があるが、その場合は先頭から全部デコードするのでずっと遅い(fqmpeg はそのモードを露出していない)。

-q:v 2: JPEG クオリティスケールで 2 はほぼロスレス(スケールは 1–31、低いほど高画質)。サムネとしては合理的な最高画質 — ファイルは大きいが鮮明。ギャラリータイル用に小さいサムネが欲しいなら、ここで最高画質を作って resize で別途縮小する。

snapshot — 一定間隔で連番フレーム抽出

動画全体から N 秒ごとに 1 フレーム抜き出す。出力は連番(<stem>-snap-0001.jpg0002.jpg、...)で、入力ファイルと同じディレクトリに保存される。

引数 / オプションデフォルト許可値備考
<input>必須入力動画
--interval <seconds>1正の数キャプチャ間隔(--interval 5 = 5 秒に 1 枚)
--format <fmt>jpgjpg, png画像フォーマット
-o, --output <pattern><入力dir>/<stem>-snap-%04d.<format>printf パターン上書き。%d%0Nd を含むこと
bash
$ npx fqmpeg snapshot lecture.mp4 --dry-run

  ffmpeg -i lecture.mp4 -vf fps=1 -q:v 2 lecture-snap-%04d.jpg
bash
$ npx fqmpeg snapshot lecture.mp4 --interval 30 --format png --dry-run

  ffmpeg -i lecture.mp4 -vf fps=0.03333333333333333 -q:v 2 lecture-snap-%04d.png

select= ではなく fps= を使う理由: fps フィルタは「出力レートを一定に保つ」処理として最もシンプル — FFmpeg がターゲットレートに合わせてフレームをドロップ/重複させる。fps=0.0333... は「30 秒に 1 フレーム」を意味し、FFmpeg は各 30 秒チック直近のフレームを選ぶ。

-q:v 2 は PNG にも付与されるが無視される — PNG の品質は圧縮レベルで決まり、q スケールには反応しない。意味があるのは JPEG だけ。

出力枚数の見積もり: 60 分の動画を --interval 30 で処理すると 120 ファイル(-snap-0001.jpg から -snap-0120.jpg)。同じファイルを --interval 1 で処理すると 3600 ファイル。ディスク容量に注意。

video-to-frames — 全フレーム(または fps 制限付き)を画像に

ソースのフレームレートでデフォルト動作するか、--fps を渡せばそのレートで間引く。フレーム単位レタッチや ML 学習データなど 全フレーム が必要な場合や、既知の毎秒サンプリングレートが必要な場合に使う。

  • ソース: src/commands/video-to-frames.js
  • フラグ: -i <input> (+ オプションで -vf fps=<n>)
  • 出力: ./frame_%04d.<format>カレントディレクトリ
引数 / オプションデフォルト許可値備考
<input>必須入力動画
--fps <n>ソースレート(フィルタなし)正の数毎秒 n フレームに間引く
--format <fmt>pngpng, jpg画像フォーマット。PNG デフォルト(ロスレス)で ML / 編集向け
-o, --output <pattern>./frame_%04d.<format>printf パターン上書き
bash
$ npx fqmpeg video-to-frames input.mp4 --dry-run

  ffmpeg -i input.mp4 frame_%04d.png
bash
$ npx fqmpeg video-to-frames input.mp4 --fps 5 --format jpg --dry-run

  ffmpeg -i input.mp4 -vf fps=5 frame_%04d.jpg

デフォルトは「全フレーム」。 30 fps × 60 秒なら 1800 枚の PNG — 数百 MB に簡単に達する。定期サンプルで足りるなら snapshot(こちらは妥当なデフォルト)か --fps を渡す。

出力は入力ディレクトリではなく cwd。 これは意図的で、フレームダンプは作業用フォルダに置きたいスクラッチデータであることが多く、動画のあるディレクトリを汚したくないという思想。ただし snapshot との振る舞いの違いになるので、入力の隣に置きたいなら -o 入力/dir/frame_%04d.png を渡すこと。

PNG vs JPG: 後段の画像処理(ロスレス、アルファ保持)には PNG が安全なデフォルト。JPG は 5–10 倍小さいがロッシー — プレビュー用途や ML 学習でモデルがダウンサンプリングする場合は十分。フォーマットフラグが拡張子を駆動し、FFmpeg はそれからコーデックを選ぶ。

count-framesffprobe でフレーム数を正確にカウント

動画ストリームの正確なフレーム数を返す。C12 で唯一 ffmpeg を使わない動詞 — ffprobe -count_frames を実行して整数 1 つを標準出力に書く。出力ファイルなし。

引数 / オプション備考
<input>入力動画
bash
$ npx fqmpeg count-frames input.mp4 --dry-run

  ffprobe -v error -count_frames -select_streams v:0 -show_entries stream=nb_read_frames -of csv=p=0 input.mp4
bash
$ npx fqmpeg count-frames input.mp4

1798

-count_frames はストリーム全体をデコードする。 正確だが遅い — 長尺ファイルでは count-frames が動画の全バイトを読む。ヘッダから duration × frame_rate を引いて推定したほうが速い(デコード不要):

bash
ffprobe -v error -select_streams v:0 -show_entries stream=duration,r_frame_rate -of csv=p=0 input.mp4

これで duration と r_frame_rate が返る(例: 60.5,30000/1001)ので掛け算すれば(60.5 × 29.97 ≈ 1814 フレーム)— 近いが可変フレームレート(VFR)動画では完全一致しない。

正確なフレーム数が必要な場面: N 等分のフレーム分割を自動化する動画編集、フレームごとにインデックスが必要なポーズ推定ワークフロー、フレーム数そのものがフィンガープリントになる不正検知/フォレンジック解析など。普通のニーズならヘッダ推定で十分。

コンタクトシート・フィルムストリップ

thumbnail-grid — コンタクトシート(tile の別名)

複数フレームをグリッド配置したサムネ合成画像。DVD のチャプター選択画面みたいな見た目。サンプリングは tile と同じ時間ベース(1 秒に 1 フレーム)。「grid」で検索したユーザー向けにこの名前で残している。

  • ソース: src/commands/thumbnail-grid.js
  • フィルタ: select='isnan(prev_selected_t)+gte(t-prev_selected_t, 1)', scale=<W>:-1, tile=<C>x<R>
  • 出力: <入力stem>-grid.jpg
引数 / オプションデフォルト備考
<input>必須入力動画
--cols <n>4グリッドの列数
--rows <n>4グリッドの行数
--width <n>320各タイルの幅(px)
-o, --output <path><入力stem>-grid.jpg上書き
bash
$ npx fqmpeg thumbnail-grid input.mp4 --dry-run

  ffmpeg -i input.mp4 -frames:v 1 -vf select='isnan(prev_selected_t)+gte(t-prev_selected_t\,1)',scale=320:-1,tile=4x4 -q:v 2 input-grid.jpg

1 秒に 1 フレーム、上限は cols × rows デフォルト 4×4 = 16 タイルなら最初の 16 秒分をカバーする。動画がもっと長い場合は --cols/--rows を増やして cols × rows ≥ 動画秒数 にする、または tile 動詞(同じ挙動)を使って -o でファイル名を制御する。prev_selected_t の詳しい仕組みは下の tile セクション参照。

tile と同じアルゴリズム、違うのはデフォルト出力名だけ。 「grid」という単語が思い浮かんだら thumbnail-grid、「tile」「コンタクトシート」が思い浮かんだら tile を使う。tile のデフォルト名は寸法を含む(-tile4x4.jpg)ので、--cols/--rows を変えながら再実行するときに便利。

thumbnail-strip — フィルムストリップ(時間サンプリング)

thumbnail-grid の 1 列バリアント。同じ時間ベースサンプリングで、rows=1 固定、幅ではなく高さをパラメータに取る。

  • ソース: src/commands/thumbnail-strip.js
  • フィルタ: select='isnan(prev_selected_t)+gte(t-prev_selected_t, 1)', scale=-1:<H>, tile=<N>x1
  • 出力: <入力stem>-strip.jpg
引数 / オプションデフォルト備考
<input>必須入力動画
--frames <n>10ストリップ内のフレーム数
--height <n>120各フレームの高さ(px)
-o, --output <path><入力stem>-strip.jpg上書き
bash
$ npx fqmpeg thumbnail-strip input.mp4 --dry-run

  ffmpeg -i input.mp4 -frames:v 1 -vf select='isnan(prev_selected_t)+gte(t-prev_selected_t\,1)',scale=-1:120,tile=10x1 -q:v 2 input-strip.jpg

1 秒に 1 フレーム、上限は --frames デフォルト --frames 10 なら最初の 10 秒分のストリップになる。もっと長いランタイムを 1 枚でカバーしたいなら --frames を増やして 動画秒数 以上にする、または snapshot の出力を tile/montage(ImageMagick)で比例配分してつなげる。

フィルムストリップの用途: 動画エディタのスクラバプレビュー、動画プレイヤーのホバープレビュー(YouTube の WebVTT サムネイルストリップ)、SNS 風の「タイムラインスクロール」アート。

tile — コンタクトシート(時間サンプリング)

時間ベース(prev_selected_t 経由)で 1 秒に 1 フレーム選ぶ。thumbnail-grid と同じアルゴリズム。違いはデフォルト出力名(-tile<C>x<R>.jpg)に寸法が入る点。

  • ソース: src/commands/tile.js
  • フィルタ: select='isnan(prev_selected_t)+gte(t-prev_selected_t, 1)', scale=<W>:-1, tile=<C>x<R>
  • 出力: <入力stem>-tile<C>x<R>.jpg
引数 / オプションデフォルト備考
<input>必須入力動画
--cols <n>4グリッドの列数
--rows <n>4グリッドの行数
--width <n>320各タイルの幅(px)
-o, --output <path><入力stem>-tile<C>x<R>.jpg上書き(デフォルト名に寸法が入る)
bash
$ npx fqmpeg tile input.mp4 --dry-run

  ffmpeg -i input.mp4 -frames:v 1 -vf select='isnan(prev_selected_t)+gte(t-prev_selected_t\,1)',scale=320:-1,tile=4x4 -q:v 2 input-tile4x4.jpg

prev_selected_t 述語の意味:

  • isnan(prev_selected_t) は最初の 1 フレームで真(まだ選択履歴がない → prev_selected_t は未定義 → NaN)。
  • gte(t - prev_selected_t, 1) は前回選択から 1 秒以上経った時に真。

この 2 つの和が select フィルタをゲートし、1 秒に 1 回選択を発生させる。デフォルト 4×4 = 16 タイルなら最初の 16 秒をカバーする。動画がもっと長い場合は tile=4x4 の上限で 17 番目以降は捨てられるので、結局最初の 16 秒分。全尺に分散したコンタクトシートが欲しいなら --cols / --rows を増やして cols × rows ≥ 動画秒数 にする、または生の FFmpeg で select='gte(t, X)' を比例配分で書く。

tilethumbnail-grid が両方存在する理由: 発見性 — 「grid」で検索しても「tile」で検索してもここに辿り着くようにしている。フィルタとサンプリングは同じ、違うのはデフォルト出力ファイル名だけ(tile-tile4x4.jpgthumbnail-grid-grid.jpg)。思い浮かんだ方を使えば良い。実行間でファイル名を固定したい場合は -o で指定。

フレーム ↔ 動画再構築

frames-to-video — 画像列 → 動画

video-to-frames の逆。printf スタイルの連番(またはグロブパターン)の静止画列から 1 本の動画ファイルを作る。

引数 / オプションデフォルト備考
<pattern>必須printf パターン(frame_%04d.png)or シェルグロブ(img_*.jpg
--fps <n>30出力フレームレート
--codec <name>libx264動画コーデック
-o, --output <path><pattern-base>-video.mp4上書き(デフォルトはパターンから %d と拡張子を除いた残り)
bash
$ npx fqmpeg frames-to-video frame_%04d.png --dry-run

  ffmpeg -framerate 30 -i frame_%04d.png -c:v libx264 -pix_fmt yuv420p frame-video.mp4
bash
$ npx fqmpeg frames-to-video 'img_*.jpg' --fps 60 --dry-run

  ffmpeg -framerate 60 -i img_*.jpg -c:v libx264 -pix_fmt yuv420p img_*-video.mp4

-framerate vs -r: -framerate入力の画像列レート(毎秒何枚消費するか)を設定する、出力フレームレートではない。画像→動画のほぼすべての用途でこれが正解 — デフォルトでは出力レートが入力レートにマッチする。出力レートを変えたい(補間やフレーム複製など)なら -r を入力の後に追加する。

互換性のための yuv420p: 生の画像ファイル(特に PNG)は yuva420prgb24 にデコードされることがあり、これを MP4 コンテナに入れると拒否するプレイヤーがある。yuv420p を強制すれば QuickTime、モバイルブラウザ、組み込みプレイヤーで再生できる。アーカイブ用に高品質(10-bit、フル RGB)を狙うなら --codec libx264rgb か生の FFmpeg に降りる。

パターンマッチング: printf パターン(frame_%04d.png)は連番が 00001 始まり(-start_number 000000 始まり)の整列が必要。グロブパターン(img_*.jpg)も動くが、シェルが展開する前に渡す必要がある — スクリプトではクオートで囲む。混在拡張子のグロブ(.jpg.png 同居)は image2 デマクサが同一フォーマットを期待するので動かない。

slideshow — 複数画像 → 各画像に表示時間付き動画

静止画のリストから、各画像を設定可能な時間表示する動画を作る。内部で FFmpeg の concat デマクサを使い、自動生成リストファイル(終了時に削除)で結合する。

  • ソース: src/commands/slideshow.js
  • フラグ: -f concat -safe 0 -i <listfile> -vf fps=<n>,format=yuv420p -c:v libx264 -pix_fmt yuv420p
  • 出力: <入力dir>/slideshow.mp4
引数 / オプションデフォルト備考
<images...>必須(≥2)順番に 2 枚以上の画像パス
--duration <sec>31 枚あたりの表示秒数(全画像共通)
--fps <n>30出力フレームレート
-o, --output <path><入力dir>/slideshow.mp4上書き
bash
$ npx fqmpeg slideshow img1.jpg img2.jpg img3.jpg --dry-run

  # Image list (auto-generated):
  # file '/abs/path/img1.jpg'
  # duration 3
  # file '/abs/path/img2.jpg'
  # duration 3
  # file '/abs/path/img3.jpg'
  # duration 3
  # file '/abs/path/img3.jpg'

  ffmpeg -f concat -safe 0 -i imagelist.txt -vf fps=30,format=yuv420p -c:v libx264 -pix_fmt yuv420p slideshow.mp4

エントリ単位 durationconcat デマクサ: リストファイルの文法 file '<path>' \n duration <sec> で、静止画を正確な時間でつなぐ。クリーンアップは自動(fqmpeg は process.on("exit") でタイムスタンプ付きリストファイルを削除)。

最後の画像が 2 回書かれている理由: concat デマクサの duration 行は「次のファイルへの遷移時刻」を指定するため、そのままだと最後の画像の表示時間がゼロになる。fqmpeg は最後の画像を duration 行なしのトレーリングエントリとしてもう 1 回書くことで、最後の画像も --duration 秒間表示される。総尺は 画像枚数 × duration 秒で option の約束通りになる。

リストファイルは絶対パス: fqmpeg は書く前に各画像を絶対パスに解決するので、どこから起動しても concat が動く。シングルクォートで内部のクォートはエスケープ('\\'')するので、アポストロフィを含むパス(O'Brien.jpg など)も通る。

画像 1 枚のケース: コマンドは ≥ 2 枚を要求し、1 枚だとエラーで終わる。「任意長の動画として 1 枚の静止画を伸ばしたい」場合は生の FFmpeg を使う: ffmpeg -loop 1 -i img.jpg -t 10 -c:v libx264 -pix_fmt yuv420p out.mp4

ビルトインの遷移効果なし — ハードカットのみ。 slideshow は FFmpeg の concat デマクサで画像を端から端へ繋ぐだけで、クロスフェードは出さない。フェードを入れたいなら、生 FFmpeg の xfade フィルタ(filter_complex で各ペアを xfade=transition=fade:duration=1:offset=… で連鎖)に降りる、または slideshow 実行後に別パスでフェードを足す。xfade 連鎖をビルトイン化すると concat のシンプルな流れを画像ごとの segment chain に置き換えることになり、「quick」の表面から外れるのでサポートしない。

解析・合成

scenes — シーン切れ検出して自動分割

scene メタデータ変数(フレームごとの差分スコア、0–1)でシーン変化を検出し、segmenter で各シーンを別ファイルに書く。

  • ソース: src/commands/scenes.js
  • フィルタ: select='gt(scene,<threshold>)',setpts=N/FRAME_RATE/TB + -f segment -reset_timestamps 1
  • 出力: <入力dir>/<stem>-scene%03d<ext>
引数 / オプションデフォルト範囲備考
<input>必須入力動画
--threshold <n>0.30.01.0低いほど敏感(カット数増)
-o, --output <pattern><入力dir>/<stem>-scene%03d<ext>printf パターン上書き
bash
$ npx fqmpeg scenes movie.mp4 --dry-run

  ffmpeg -i movie.mp4 -filter_complex select='gt(scene,0.3)',setpts=N/FRAME_RATE/TB -f segment -reset_timestamps 1 movie-scene%03d.mp4

scene メタデータ変数: FFmpeg が前フレームとの色ヒストグラム差分から 0–1 のスコアをフレームごとに計算する。閾値超過がカットとみなされる。典型値:

  • 0.10.2: 攻撃的(ディゾルブやクロスフェードも拾う、モーション誤検知多い)
  • 0.3: バランス(デフォルト — ナラティブ動画のハードカットのほとんどを拾う)
  • 0.40.5: 保守的(明確な遷移のみ。正当なカットの一部を見落とす)
  • 0.7+: 最も激しいカットだけ(脚本もののチャプター境界)

-reset_timestamps 1: 各出力セグメントが PTS 0 から始まる(ソースのタイムラインの続きではない)。これでセグメントが個別に再生可能になる。

ストリームコピー不可。 フィルタが全フレームに触るので出力は再エンコードされる。再エンコなしで高速にカットリストが欲しいなら、生の FFmpeg で検出後に -ss/-to を使う — まず scenes --dry-run で閾値を確認、ffprobe でタイムスタンプを抽出、ffmpeg -ss <t1> -to <t2> -c copy でセグメント分割。

preview — 短いハイライト動画生成

ソース全体に均等分散させた N 個の短いクリップをサンプリングし、それらを連結して短いプレビュー動画を作る — 「SNS 用予告編」スタイル。

  • ソース: src/commands/preview.js
  • フィルタ: 均等間隔の位置に <clip-duration> 秒のクリップを N 個 select、-t <clips × clip-duration> で連結
  • 出力: <入力stem>-preview.<ext>
引数 / オプションデフォルト備考
<input>必須入力動画
--clips <n>5サンプルクリップ数
--clip-duration <sec>2各クリップの秒数
-o, --output <path><入力stem>-preview.<ext>上書き
bash
$ npx fqmpeg preview input.mp4 --dry-run

  # Note: could not probe input.mp4 for duration. Using placeholder total=60s.
  # Run on a real file (or after creating input.mp4) to get exact clip offsets.

  ffmpeg -i input.mp4 -vf select='between(t,0,2)+between(t,12,14)+between(t,24,26)+between(t,36,38)+between(t,48,50)',setpts=N/FRAME_RATE/TB -af aselect='between(t,0,2)+between(t,12,14)+between(t,24,26)+between(t,36,38)+between(t,48,50)',asetpts=N/SR/TB -t 10 input-preview.mp4

出力長は決定的: clips × clip-duration 秒、ソース長に関係なく。デフォルト(5 × 2 = 10 秒)は 10 秒のハイライト — Twitter / Instagram に短く、30 分のトークの要旨を伝えるには十分な長さ。

ソース全体に均等分散: preview はまず ffprobe でソースの動画長 T を読み、--clips 個のセグメントを均等間隔で選ぶ — クリップ 1 は 0 × T/clips、クリップ 2 は 1 × T/clips、...、クリップ N は (N−1) × T/clips から始まり、各 clip-duration 秒。60 分動画のデフォルトなら、0 分、12 分、24 分、36 分、48 分の各 2 秒。上の dry-run 出力は input.mp4 が実在しないので placeholder T = 60 で計算しているだけ、実ファイルなら実値の offset が入る。

クリップ間に音声フェードなし。 出力は各サンプリングクリップ間でハードカットなので、音楽中心のコンテンツでは音のポップが出やすい。SNS 投稿レベルの仕上がりが欲しいなら、trim + crossfade + audio-fade を手で組み合わせてクロスフェードと BGM を入れたほうがいい。

compare — 横並び(または縦並び)の前後比較

2 つの動画を横(デフォルト)または縦に並べて 1 本の動画にする。「このフィルタでこれだけ良くなった」を見せる定番のデモ手法。

  • ソース: src/commands/compare.js
  • フィルタ(horizontal): [0:v]scale=iw/2:ih[left];[1:v]scale=iw/2:ih[right];[left][right]hstack
  • フィルタ(vertical): [0:v]scale=iw:ih/2[top];[1:v]scale=iw:ih/2[bottom];[top][bottom]vstack
  • 出力: <入力1stem>-compare.<ext>
引数 / オプションデフォルト許可値備考
<input1>必須左 / 上の動画
<input2>必須右 / 下の動画
--direction <dir>horizontalhorizontal, verticalレイアウト
-o, --output <path><入力1stem>-compare.<ext>上書き
bash
$ npx fqmpeg compare before.mp4 after.mp4 --dry-run

  ffmpeg -i before.mp4 -i after.mp4 -filter_complex [0:v]scale=iw/2:ih[left];[1:v]scale=iw/2:ih[right];[left][right]hstack -c:a copy before-compare.mp4
bash
$ npx fqmpeg compare original.mp4 stabilized.mp4 --direction vertical --dry-run

  ffmpeg -i original.mp4 -i stabilized.mp4 -filter_complex [0:v]scale=iw:ih/2[top];[1:v]scale=iw:ih/2[bottom];[top][bottom]vstack -c:a copy original-compare.mp4

各入力は連結前に半分にスケール: 出力キャンバスは元の幅(horizontal)または高さ(vertical)を保ち、各入力は半分のサイズにスケールされる。全体のアスペクト比は維持される。2 つの入力が解像度違いだと scale が半サイズに正規化する — 内容に歪みが出る可能性。仕上げ用には 2 入力をあらかじめ同解像度・同尺にしておくのが望ましい。

音声は入力 1 からコピー。 -c:a copy で最初の入力の音声をストリームコピーし、入力 2 の音声は破棄。これは通常の前後比較フレーム(映像を比較しつつ 1 本の音声を残す)に合っている。

尺の不一致: 2 動画の尺が違うと、出力は短い方が終わった時点で終わる(FFmpeg の hstack/vstack のデフォルト動作)。長い方の残りフレームは切り捨てられる。厳密な整列が必要なら事前に両方を同じ尺にトリムすること。

ビルトインのラベル機能なし。 「Left」/「Right」「Before」/「After」のようなテキストオーバーレイが欲しいなら、まず --dry-run でフィルタを出してから生の FFmpeg に降り、各 scale ステップに drawtext を組み込む — 具体テンプレートは下の Recipe 2 参照。--label option を入れると fontfile 依存(drawtext は libfreetype + フォントパスが必要)の表面が増えるので、quick の範囲外。

実用ユースケース

レシピ 1: YouTube サムネ作成ワークフロー

完成した 12 分の動画があり、カスタムサムネを作りたい。目標: ベストなフレームを選ぶ → YouTube 仕様(1280×720)にリサイズ → コンタクトシートで選択を確認。

bash
# ステップ 1: コンタクトシートでベストな瞬間を選ぶ(時間ベース)
npx fqmpeg tile video.mp4 --cols 6 --rows 8 --width 480
# → video-tile6x8.jpg、最初の 48 秒分の 48 フレーム

# 12 分(720 秒)動画の全尺カバーには cols×rows ≥ 720 が必要なので、
# 30×24 グリッド(720 タイル)。幅 200 なら 6000×~3375 px のシート。
npx fqmpeg tile video.mp4 --cols 30 --rows 24 --width 200

# ステップ 2: 選んだフレームをぴったりのタイムスタンプで抽出
npx fqmpeg thumbnail video.mp4 -s 374 -o thumbnail-raw.jpg

# ステップ 3: YouTube 推奨サイズにリサイズ
npx fqmpeg resize thumbnail-raw.jpg 1280x720 -o thumbnail-final.jpg

tile ステップが遅い部分 — ソースを全部デコードする必要がある。タイムスタンプが決まれば、thumbnail はキーフレーム整列 -ss のおかげでほぼ瞬時。

レシピ 2: 防犯カメラのダンプからタイムラプス作成

防犯カメラから 86,400 枚の JPEG(1 秒に 1 枚、24 時間分)が出てきた。これを 60 秒、30 fps のタイムラプス動画にしたい。

bash
# 入力 86400 枚を 30 fps で出すと 86400/30 = 2880 秒 = 48 分。
# 60 秒、30 fps に圧縮するには 1800 フレーム出力が必要。
# 入力フレームから 48 枚に 1 枚サンプリング: 86400 / 1800 = 48。

# ステップ 1: 48 枚ごとに 1 枚をシンボリックリンクで連番化
i=1
ls *.jpg | awk 'NR%48==1' | while read f; do
  printf -v new "frame_%04d.jpg" "$i"
  ln -s "$(realpath "$f")" "$new"
  ((i++))
done

# ステップ 2: 30 fps タイムラプスとして結合
npx fqmpeg frames-to-video frame_%04d.jpg --fps 30
# → frame-video.mp4

シンボリックリンクを使わない別解: slideshow--duration 0.0333(1/30 秒/枚)を使う — ただし slideshowconcat デマクサで再エンコードするので frames-to-video の直接 -i pattern より遅い。この規模のデータセットなら frames-to-video が正解、slideshow は数十枚程度で各画像表示時間を指定したい用途。

レシピ 3: ポートフォリオ向けのフィルタ前後比較

stabilize の効果を手ぶれドローン映像で示したい。ポートフォリオ用に横並び比較動画をラベル付きで作る:

bash
# ステップ 1: 元映像を手ぶれ補正
npx fqmpeg stabilize drone-raw.mp4 -o drone-stable.mp4

# ステップ 2: 横並び比較
npx fqmpeg compare drone-raw.mp4 drone-stable.mp4 \
  -o drone-comparison.mp4

# ステップ 3: ケーススタディの表紙用にサムネを 1 枚抽出
npx fqmpeg thumbnail drone-comparison.mp4 -s 3 -o drone-cover.jpg

compare 自体にラベル機能はない — しっかりしたケーススタディなら、--dry-run でフィルタを出して生の FFmpeg で再レンダリングし、各 scale ステップに drawtext を組み込む:

bash
ffmpeg -i drone-raw.mp4 -i drone-stable.mp4 -filter_complex \
  "[0:v]scale=iw/2:ih,drawtext=text='Original':x=20:y=20:fontsize=36:fontcolor=white:box=1:boxcolor=black@0.5[left];[1:v]scale=iw/2:ih,drawtext=text='Stabilized':x=20:y=20:fontsize=36:fontcolor=white:box=1:boxcolor=black@0.5[right];[left][right]hstack" \
  -c:a copy drone-portfolio.mp4

よくある質問

thumbnail-gridtile のどちらを使うべき?

どちらでも — 別名関係。両方とも同じ時間ベースサンプリング(select='isnan(prev_selected_t)+gte(t-prev_selected_t, 1)')で、同じ --cols/--rows/--width なら同じコンタクトシートを生成する。違いはデフォルト出力名だけ:thumbnail-grid<stem>-grid.jpgtile<stem>-tile<C>x<R>.jpg。グリッドサイズをファイル名に残したいなら tile、固定の -grid.jpg のほうが扱いやすいなら thumbnail-grid。どちらにせよ -o で上書き可能。

snapshotvideo-to-frames の違いは?

両方とも定期的にスチル抽出するが、デフォルトと出力先が違う:

  • snapshot は 1 秒に 1 フレーム、JPEG 形式、入力と同じディレクトリに出力(<入力dir>/<stem>-snap-%04d.jpg)。
  • video-to-frames はソースレートで全フレーム、PNG 形式、カレントディレクトリに出力(./frame_%04d.png)。

「動画の隣に時々参照用のスチル」なら snapshot が適切。「ML 編集用に全フレーム」なら video-to-frames。どちらも別の場所に書きたければ -o を渡す。

count-frames が長い動画で遅いのは?

-count_frames がストリーム全体をデコードするから。このフラグは ffprobe に「実際にパケットを全部歩いてデコード成功フレームを数えろ」と指示する — 正確性のために必要(特に VFR ファイル)だが、フル デコードパスのコストがかかる。4 時間 4K ソースだと数分かかることもある。推定で良ければヘッダから durationr_frame_rate を取って掛け算するほうが速い(デコード不要):

bash
ffprobe -v error -select_streams v:0 -show_entries stream=duration,r_frame_rate -of csv=p=0 input.mp4

frames-to-videoimg_*.jpg のようなグロブは使える?

使えるが、クオートが必要。FFmpeg の image2 デマクサは printf パターン(frame_%04d.png)とシェルグロブ('img_*.jpg')の両方を受ける。クオートなしのグロブはシェルが先に展開してしまうので普通はコマンドが壊れる — シングルクォートで囲んでリテラルパターンを FFmpeg に渡す。グロブモードは全ファイルが同じ拡張子である必要(.jpg.png 混在は不可)。

tile で長い動画の最初の 16 秒しかカバーされないのはなぜ?

デフォルトの 4×4 = 16 タイル × 1 秒間隔の時間サンプリングが、ちょうど 16 秒分を生成するから。長い動画全体をカバーしたいなら、--cols--rows を増やして cols × rows ≥ 動画秒数 にする。5 分(300 秒)動画を均等にサンプリングするには約 17×18 グリッド(306 タイル)が必要。長尺動画でもっと荒くサンプリングしたいなら生の FFmpeg に降りて select 述語を調整する — 例: select='gte(t - prev_selected_t, 60)' で 1 分に 1 フレーム。

frames-to-videoslideshow の違いは?

frames-to-video連番(またはグロブ)の画像列frame_%04d.png)を消費し、単一のフレームレートで動画にする — 入力画像 1 枚 = 出力 1 フレーム、均一。slideshow明示的な画像リストimg1.jpg img2.jpg img3.jpg)を消費し、画像ごとの表示時間(各画像 N 秒)を設定できる、画像間はハードカット(ビルトインのクロスフェードはなし — フェードは生 FFmpeg の xfade で追加、slideshow セクション参照)。タイムラプス、ML 出力の再構築、レンダリングからの画像列には frames-to-video。写真プレゼンなど 1 枚を数秒ずつ見せたい用途には slideshow

典型的なナラティブ動画で scenes の閾値はいくつにすべき?

デフォルト 0.3 から始める。カット検出が少なすぎる(ハード遷移を見落とす)なら 0.20.15 に下げる。多すぎる(カメラ動きやディゾルブで誤検知)なら 0.40.5 に上げる。閾値の感度はコンテンツ次第 — モーションが速い MV はショット内動きを無視するのに高めの閾値(0.5+)が要る。トーキングヘッドのインタビューは変化が本当のカットだけなので低め(0.2)でも使える。

compare で両方の音声を同時に出せる?

出せない。実装は -c:a copy で入力 1 の音声ストリームしか取らない。入力 2 の音声は破棄される。通常のワークフロー(前後映像比較で 1 本の音声)に合っている。デュアル音声比較(例: 2 つの音声ミックスを並べて比較)には生の FFmpeg で amergeamix:

bash
ffmpeg -i a.mp4 -i b.mp4 -filter_complex \
  "[0:v]scale=iw/2:ih[L];[1:v]scale=iw/2:ih[R];[L][R]hstack;[0:a][1:a]amerge=inputs=2[a]" \
  -map "[a]" -ac 2 compare-dual-audio.mp4

まとめ

C12 の 12 動詞は動画 ↔ 個別フレームの往復をカバーする:

  • thumbnail, snapshot, video-to-frames, count-frames で単発・定期スチル(-q:v 2 が JPEG クオリティのデフォルト、count-frames だけがクラスタ内で唯一の ffprobe 動詞)
  • thumbnail-grid, thumbnail-strip, tile でコンタクトシート・フィルムストリップ(3 つとも時間ベースサンプリング — 1 秒に 1 フレーム — で、--cols × --rows(grid/tile)または --frames(strip)が上限)
  • frames-to-video, slideshow で画像→動画再構築(frames-to-video は連番を一定レートで結合、slideshow は画像ごとの表示時間でハードカット連結 — フェードが必要なら生 FFmpeg の xfade で追加)
  • scenes, preview, compare で解析・合成(scenes は検出カットで分割、preview は均等サンプリングのハイライト動画、compare は前後比較のスタック)

各動詞は --dry-run で内部の FFmpeg 起動行を出力するので、簡略化された CLI で足りない場合(独自のコンタクトシートサンプリング、デュアル音声比較、キーフレーム整列ではないフレーム精度シーク)は、フィルタをコピーして編集し、生の FFmpeg を直接呼ぶ。fqmpeg の全体マップは fqmpeg complete guide を参照。