【AI】透過動画の輪郭に生成背景色が残る【AIマッティングにクロマキー色は要らない】


こんにちは、フリーランスエンジニアのmohです。

キャラクターの待機ループ動画を、単色背景で生成 → AI マッティングで切り抜き → VP9 alpha の webm と HEVC alpha の mov で配信、という経路で作っています。生成時の背景にマゼンタ(#FF00FF)と緑(#00B140)を使っていたら、暗い背景の上に載せたときに輪郭が色の線として見えました。

原因を切り分けて、他モデルと比べて、後処理を 3 回書いて、最後に生成時の背景色を無彩色のグレーに変えて 27 本作り直しました。効いたのは最後の 1 手で、前の 3 つは部分的にしか効いていません。

症状

切り抜いた動画を暗い背景に合成した4倍拡大クロップ。箒の穂先の半透明部分と輪郭の最外周にマゼンタが乗っている

半透明の毛先と、不透明の最外周 1〜2px にマゼンタが残っています。明るい UI 背景では気になりませんでしたが、暗い背景に切り替えたら輪郭線として見えるようになりました。

色はどこで乗るか

最初は webm へ変換するときのエンコードを疑いましたが、切り抜き直後の ProRes 4444 の時点で既に残っていました。変換工程は色を足しておらず、乗っている色をそのまま運んでいるだけです。

半透明画素の色の偏りを (R+B)/2 − G で測ります(マゼンタ背景の場合。緑背景なら符号を反転します)。同じ 1 本を工程ごとに測るとこうなりました。

段階半透明画素の偏り
切り抜き直後の ProRes 4444+20.5
配布用 VP9 alpha webm+18.8
配布用 HEVC alpha mov+27.0
参考: 生成り色(#f6f1e7)の背景で作った旧作+3.2

深さ別に見ると、不透明の最外周 1px で偏りが +20 を超える画素が 19.6%、2px 目より内側はほぼ 0 です。症状は半透明帯と最外周 1px に集中しています。なお切り抜きは refine_foreground を有効にした出力で、このオプションでは消えませんでした。

他のモデルでも同じか

同じ入力を fal の 3 つの endpoint に通して比べました。全 124 フレームのうち frame 0 と frame 61 です。

モデル半透明の偏り f0 / f61最外周1pxで偏り+20超 f0 / f61
birefnet v2 video(現行)+20.5 / +15.719.6% / 21.7%
ben v2+72.7 / +90.56.0% / 18.6%
bria v3+127.3 / +115.115.7% / 22.3%
veed−3.9 / +0.44.4% / 14.0%

深さ 3px より内側の平均 RGB は 4 本とも差 1.5 未満で、意匠はどれも壊していません。色については veed だけがまともで、ben と bria は現行より悪化しました。

ただし veed には別の問題がありました。あるキャラは頭上に湯気が立っていて、これが消えます。

同じフレームを veed と birefnet で切り抜いて暗い背景に合成した比較。veed 側は頭上の湯気が消えていて、birefnet 側は渦巻きが残っている

比較相手の birefnet 側は、薄い湯気が一部のフレームで消える問題に対して既に補完の後処理を入れた採用版です。

指標(湯気の領域・全124フレーム)veedbirefnet(採用版)
取りこぼし率 最大55.1%13.6%
取りこぼし率 平均25.4%1.9%
取りこぼし 45% 以上のフレーム数400
キャラ面積の変動係数3.85%4.42%
ループ継ぎ目の alpha 平均差0.27/2550.17/255

面積の変動係数とループ継ぎ目は両者同水準で、そこだけ見ると veed は通ります。湯気はキャラ全体の面積の数 % しかないので、まるごと消えても面積の統計に出ません。「元動画で前景だった画素のうち、切り抜き後に透明になった割合」を別に測って初めて出ました。毛先のような密なエッジでは veed と現行の差は 0.1〜0.3% で誤差レベルです。薄い半透明を持つキャラには単純に差し替えできない、という結論になりました。

後処理で消す

切り抜き後の RGBA に対して後処理を書きました。3 通り試しています。

v1: 既知の背景色から逆算する。 合成は C = αF + (1−α)B なので、B が既知なら F = (C − (1−α)B) / α で前景色が出るはずです。偏りは 4〜6 割減りましたが、目標には届きません。原因は、マッティング側の出力が既に foreground refine 済みで、透明部の RGB が黒に潰され、残っている背景色が α 240〜253 に集中していたことです。混色のモデルが成り立っていませんでした。

v2: 逆算の材料を元動画に変える。 切り抜き前の生成動画の RGB と、マッティングが出した α で同じ式を解きます。v1 より悪化しました。生成動画は H.264 の 4:2:0 で、クロマが隣の画素と平均されています。α が小さいほど 1/α で誤差が増幅し、α<10 の画素は色が背景そのものなので F が決まりません。

v3: 逆算をやめる。 輪郭帯(半透明画素と、不透明の深さ 1〜2px)の色を、深さ 3px 以上の内部色を外側へ伝播させた参照色へ、背景色方向の超過分だけ寄せます。背景色は方向にしか使わず、値の逆算には使いません。

同じ素材に 3 通りを掛けた結果です。

方式半透明画素の偏り(全フレーム平均)
処理前+18.9
v1 切り抜き結果から逆算+8.3
v2 元動画の RGB と α から逆算+27.3
v3 内部色へ寄せる−12.1

v3 を 3 本に掛けた結果です。

素材半透明の偏り 前→後最外周1pxで+20超 前→後
A+18.9 → −12.119.6% → 6.7%
B+24.9 → −10.326.9% → 8.7%
C(意匠に赤を含む)+42.6 → +14.944.9% → 35.8%

C は赤い布が意匠に入っているキャラで、指標が意匠の色を拾っています。色だけで判定すると意匠を壊すので、深さ 3px 以上の画素が処理前後でバイト一致することを毎回確認しています。

それでも消えない画素が残りました。

うちわと頬のあいだの隙間の4倍拡大。左が処理前でマゼンタの塊が残り、右が処理後で薄いマゼンタと緑のにじみに変わっている

うちわと頬のあいだの狭い隙間です。α=255 なのに色が背景色そのもの、という画素で、輪郭ではないので内部色の伝播が届きません。ここは「色が背景色とほぼ区別できない輪郭帯の画素は α を 0 にする」という処理を前段に足して抜きました。うちわの白い縁が緑寄りに振れる(−10 前後の過補正)副作用も残りました。

背景色を変える

ここで前提を見直しました。

AI マッティングは色でキーを抜くのではなく被写体を推定しているので、背景にクロマキーの派手な色を付ける理由がありません。派手にしたのは、以前に白い毛のキャラが生成り色の背景と同化したからで、そのとき必要だったのは彩度ではなく明度差でした。

背景を無彩色のグレーにして 27 本を作り直しました。既定は #808080 で、衣装にグレーの陰影が多い 4 本だけ #4a4a4a です。無彩色では (R+B)/2 − G が常に 0 付近になるので、指標を「輪郭帯のうち背景色とのユークリッド距離が 30 未満の画素の割合」に変えました。

指標(全フレーム)#808080 の 23 本#4a4a4a の 4 本
輪郭帯のうち背景色に近い画素の割合0.11〜7.27%(中央値 1.78%)2.57〜30.84%
前景の取りこぼし 最大0.17〜1.29%(中央値 0.51%)0.54〜18.21%
ループ継ぎ目の alpha 平均差0.09〜0.90 /2550.20〜1.12 /255

#4a4a4a の 4 本で「背景色に近い画素」が多いのは、衣装自体が暗いグレーで指標が意匠を数えているためです。取りこぼしの 18.21% は、笛を吹く動作で生成側が白い吐息を描き、切り抜き側がそれを落としたものです。どちらも輪郭の色とは別件でした。

暗い背景と明るい背景の両方に実寸で合成して、輪郭に線は出ませんでした。

尻尾の4倍拡大クロップ。暗い背景の上で毛の輪郭に色の線がなく、白い毛先も残っている

頭上の湯気の4倍拡大クロップ。薄い半透明の渦がそのまま残っている

白い毛先と薄い湯気は残っています。

副産物: 髪に囲まれた背景

グレーにしたら別のことも楽になりました。1 本だけ、後れ毛と首のあいだの背景をマッティングが不透明で拾ったのですが、そのキャラの配色に中間グレーが無いので、背景色との色差と彩度でその画素だけ狭く選んで消せました。マゼンタや緑だと、たまたま似た色の意匠との衝突を毎回心配することになります。

生成側で分かったこと

  • 画像から動画を作って first=last のループにすると、終端が完全静止に落ち着きがちです(MiniMax H3)。プロンプトに静止を書かなくても起きます。対策は、先頭と終端の「frame 0 との差が閾値未満」の区間だけ ffmpeg で落とすことです。途中は切りません。終端の静止は frame 0 と同じ絵なので、切ってもループの継ぎ目は崩れません
  • ただし切ると短くなりすぎる本が出ます。trim 後の尺は 3.3〜5.1 秒(中央値 4.6 秒)で、27 本中 8 本が 4 秒を切りました。その 8 本は trim せず 5.2 秒のまま採用しています
  • しぐさで動かす手は「右手」「左手」で指定しませんでした。キャラの右か画面の右かが解釈で割れるので、アンカー画像での位置(前に出ているほう、腰にあるほう)で指しています。前後の腕を入れ替える突きのような動きは、型の先入観に引かれて腕の役割が入れ替わったので、「腕の役割を入れ替えない」と明記して通しました

教訓

  • 生成原本を捨てない。配布用の lossy に変換したあとで中間物を消すと、切り抜き工程のやり直しができません。今回、原本が残っていなかった分は作り直すしかありませんでした
  • 数字は目的別に用意する。面積の変動係数とループ継ぎ目の差だけでは湯気の消失を検出できませんでした
  • 対策より前に前提を見る。後処理を 3 通り書いたあとで、背景に派手な色を使う理由がそもそも無いことに気づきました

まとめ

切り抜き後のフレームには、前景色と背景色を分離するための情報がもう十分に残っていません。既知の背景色からの逆算も、内部色への寄せも、上限がありました。生成時の背景色を素材と紛れない低彩度の無彩色にするほうが、後処理より確実です。

縁の色偏りを測るスクリプトと、背景色近傍の α カットの最小実装は blog-examples に置いてあります。

参考