こんにちは、パレイド技術部です。
前回、高画質化パイプライン(色補正→アップスケール→フレーム補間→エンコード)を構築しました。
今回は数秒の素材を複数つなぎ合わせて、長尺BGVを完成させます。
クリップ結合時の問題
前回、1つ数秒程度の映像を Wan2.2 で生成し、後処理で均質化・高画質化を行うパイプラインを構築しました。 パイプラインを連続で動かし10程度の動画kリップを生成し、これを1つに結合することで長尺のBGVが生成します。
しかし、実際に複数クリップを量産して長尺BGVを作ろうとすると、2つの問題にぶつかりました。
- プロンプトに反した映像の混入: 「夕焼けの海」を指定しても、太陽が突然出現したり、人物や謎のオブジェクトが現れるクリップが一定数発生する
- ループのつなぎ目: 短尺クリップをそのまま繰り返すとジャンプして不自然
問題1: プロンプト無視の映像が混ざる
Wan2.2でシード違いのクリップを10本生成すると、そのうち2〜3本はプロンプトの指示に反した映像になります。よく起きるパターンは以下の通りです。
- 太陽の出現: 「夕焼け」と指定しただけで、フレーム途中から太陽が急に昇ってくる
- レンズフレア: 光源が動いてフレーム全体が白飛びする
- オブジェクトの唐突な出現: プロンプトに含まれない物体が途中から現れる
これらは輝度や色の急激な変化として検出できます。そこで、アップスケール前にクリップをフィルタリングする処理を入れることにしました。
filter_clips.py による自動除外
各クリップのフレーム間で輝度変化・色相変化を分析し、しきい値を超えるクリップを自動で振り分けます。
判定に使う指標は3つです。
| 指標 | 検出対象 | デフォルトしきい値 |
|---|---|---|
| フレーム間最大輝度変化 | 太陽の出現、フラッシュ | 8.0 |
| クリップ全体の輝度レンジ | 明→暗の大きなドリフト | 30.0 |
| フレーム間最大色相変化 | 色の急変、レンズフレア | 10.0 |
いずれか1つでもしきい値を超えたクリップが除外対象になります。
内部では OpenCV を使い、各フレームを2つの色空間に変換して指標を算出しています。
- グレースケール変換(
cv2.COLOR_BGR2GRAY)→ フレーム全体の平均輝度(0〜255) - HSV変換(
cv2.COLOR_BGR2HSV)→ H(色相)チャンネルの平均値(0〜180)
これを全フレームについて行い、隣接フレーム間の差分(デルタ)を取ります。太陽が突然現れるとグレースケールの平均輝度が数フレームで急上昇し、デルタの最大値が跳ね上がります。同様に、レンズフレアや色の急変はHSV色相のデルタに現れます。
コアの分析ロジックは以下の通りです。
import cv2
import numpy as np
def analyze_clip(path: str) -> dict:
cap = cv2.VideoCapture(path)
brightnesses = []
hue_means = []
while True:
ret, frame = cap.read()
if not ret:
break
# グレースケール → 平均輝度(0〜255)
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
brightnesses.append(float(gray.mean()))
# HSV → 色相チャンネルの平均値(0〜180)
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
hue_means.append(float(hsv[:, :, 0].mean()))
cap.release()
b = np.array(brightnesses)
h = np.array(hue_means)
return {
# 隣接フレーム間の最大輝度変化(太陽の出現を検出)
"max_delta": float(np.abs(np.diff(b)).max()),
# クリップ全体の輝度レンジ(大きなドリフトを検出)
"brightness_range": float(b.max() - b.min()),
# 隣接フレーム間の最大色相変化(レンズフレアを検出)
"max_color_delta": float(np.abs(np.diff(h)).max()),
}
たとえば81フレーム(約3秒)のクリップで max_delta が 12 ということは、あるフレーム間で平均輝度が255段階中12も跳ねた(約5%の急変)ことを意味します。穏やかな風景動画では通常2〜3程度なので、8を超えたら異常と判断できます。
実行結果
10本のクリップに対して実行したところ、3本がNGと判定されました。目視確認すると、いずれも太陽の出現やレンズフレアを含むクリップでした。
クリップ数: 10
しきい値: 輝度変化=8.0, 輝度レンジ=30.0, 色相変化=10.0
除外: 3本 / 全10本 (残り: 7本)
--- 除外クリップ ---
NG sunset_02.mp4 max_d= 12.3 range= 35.2 color_d= 6.1 (輝度変化=12.3, 輝度レンジ=35.2)
NG sunset_05.mp4 max_d= 9.8 range= 28.1 color_d= 11.4 (輝度変化=9.8, 色相変化=11.4)
NG sunset_08.mp4 max_d= 15.1 range= 42.0 color_d= 8.3 (輝度変化=15.1, 輝度レンジ=42.0)
まず --dry-run で分析結果を確認し、しきい値を調整してから本実行する運用がおすすめです。太陽の出現を厳しめに弾くなら -t 5.0 --range-threshold 20.0 あたりまで絞ります。
この除外処理は、前回紹介した色調補正の後、アップスケールの前に入れます。
問題2: ループのつなぎ目
「逆再生ピンポン」は不採用
当初は「正方向→逆方向→正方向…」と交互に再生するピンポンループを試しました。つなぎ目は完全にシームレスになりますが、今回の海の波など一方向の動作では「行って戻る」動きは不自然に映ります。プロンプトで前後対称な動きを意識すれば軽減できますが、制約が大きすぎるため不採用としました。
採用手法: 始点=終点生成 + ランダム結合
代わりに採用したのは、ComfyUIのI2Vワークフローで始点画像と終点画像を同じ画像に設定する方法です。こうすると生成動画の最初と最後のフレームが同じ画像に収束するため、クリップ単体でシームレスにループできます。
この方式の利点は以下の通りです。
- 動きの方向に制約がない(波が一方向に進んでもOK)
- 逆再生の不自然さがない
- シード違いで複数バリエーションを作れば、並べるだけで長尺化できる
この方式で、品質が合格のクリップをランダムに選び、クロスフェード結合して任意の長さの動画を生成します。クリップが目標長に足りない場合は、シャッフルしてループ使用します。
たとえば、10本中合格とした7本のクリップ(各約3秒)をランダムに組み合わせて3分のBGVを生成します。この場合、各クリップは平均2〜3回使用されますが、体感的には繰り返しとは感じにくい仕上がりになります。
最終的なパイプライン
前回のパイプラインに除外処理と結合処理を組み込んだ全体像は以下の通りです。
入力: 画像ファイル(.png/.jpg)
│
├─ ComfyUI I2V生成(始点=終点、seed違いで複数クリップ)
│
├─ color_normalize(色調・明度ドリフト補正)
│
├─ filter_clips(不安定クリップの除外) ← 今回追加
│
├─ Real-ESRGAN(4xアップスケール)
│
├─ merge_clips(ランダム結合で長尺化) ← 今回追加
│
├─ Flowframes(FPS補間、手動1回)
│
└─ FFmpeg(H.265最終エンコード)
出力: 約2880×1620 / 60fps / 長尺ループBGV
pipeline.py に全ステップを統合済みなので、一括実行もできます。
python pipeline.py -i images/ -o output/final.mp4 \
--prompt "sunset ocean waves" \
--clips-per-image 3 --target-duration 180
最終的に生成された動画がこちらです。
(後日追記)本記事の動画は、本来fps=16とすべきところを、誤ってfps=24で生成しています。最後の結合した動画のみfpsを半分に調整してあります。
まとめ
- AI動画生成ではプロンプトに反した映像(太陽の出現、レンズフレア等)が一定数発生する
- クリップごとに輝度・色相の急変を検出し、アップスケール前に自動除外することで品質と処理効率を両立できた
- 逆再生ピンポンは「行って戻る」動きが不自然なため不採用
- 始点=終点で生成したクリップをシード違いで複数作り、ランダム結合する方式がBGV制作に最適
- 7本程度のクリップで3分の長尺BGVが実用品質で作れることを確認した
次回は、このパイプラインをMacBook Air M5環境でも動かせるかを検証します。



