アウトペイントで横を伸ばすと境目にうっすら線が残る。feathering を 40 から 80 に上げても体感が変わらない——。ImagePadForOutpaint と VAEEncodeForInpaint を続けて読むと理由が分かる。feathering は「ぼかす幅」ではないのだ。
検証環境と方法:ComfyUI 0.3.76 の nodes.py から ImagePadForOutpaint のクラス定義を逐語で取り出してそのまま実行し、出力された IMAGE と MASK の値を測った(Python 3.12.3 / PyTorch 2.6.0)。マスク値・スキップ条件は実ノードの出力。VAEEncodeForInpaint 側はソースの読み取り(.round() の位置)に加え、膨張処理を逐語で再現して膨張量を測った。測っていない範囲=拡散モデルによる実際の生成結果、モデル別の差、他バージョンでの再現。
結論(ComfyUI 0.3.76 実測)
- 新しく足される領域は 0.5 の中間グレーで埋まる(黒でも白でも、端の色の引き伸ばしでもない)
featheringの値は二次カーブのマスクを作るが、後段で 0/1 に丸められる- だから実効は「ぼかし幅」ではなく 元画像の縁から内側へ約 0.29×feathering ピクセルを消して描き直す幅(f=40 なら約12px)
- 🔴
feathering×2が短辺以上だと、フェザー処理は丸ごとスキップされる(警告もエラーも出ない) - padding の4辺は 8の倍数(
step: 8)
ImagePadForOutpaint は何をしている?
やっているのは2つ。キャンバスを広げて中間グレーで埋めることと、広げた場所を示す MASK を作ることだ。
new_image = torch.ones((d1, d2+top+bottom, d3+left+right, d4)) * 0.5
new_image[:, top:top+d2, left:left+d3, :] = image
* 0.5 がそれだ。足された領域は黒でも白でもなく中間グレー(0.5)で初期化される。端の引き伸ばしも鏡像の折り返しも無い。埋めた色は手掛かりにならない。
出力は IMAGE と MASK の2本。この MASK を VAEEncodeForInpaint へ渡すのが定番の繋ぎ方だ。
feathering はなぜ効いた気がしないのか?
ImagePadForOutpaint は確かになめらかなマスクを作る。端からの距離 d に対し v=(f-d)/f の二乗だ。
| 端からの距離 d | 0 | 8 | 16 | 24 | 32 | 40 |
|---|---|---|---|---|---|---|
| マスク値(f=40) | 1.000 | 0.640 | 0.360 | 0.160 | 0.040 | 0.000 |
問題は次だ。受け取る VAEEncodeForInpaint が、このマスクを丸めてから使う。
m = (1.0 - mask.round()).squeeze(1)
...
return ({"samples": t, "noise_mask": (mask_erosion[:,:,:x,:y].round())}, )
.round() なので 0.5 以上は 1・未満は 0。なめらかな勾配はそこで消える。画素側もマスク側も丸めた値で処理される。
つまり feathering が決めるのは柔らかさではなく 0.5 を跨ぐ位置で、境界は d = f × (1 − √0.5) ≒ 0.293f になる。
| feathering | 丸めた後に「消える」最大距離 | 実効的に描き直される帯 |
|---|---|---|
| 10 | 2px | 約3px |
| 20 | 5px | 約6px |
| 40(既定) | 11px | 約12px |
| 64 | 18px | 約19px |
| 100 | 29px | 約30px |
f=40 なら d=11 でマスク 0.5256 → 丸めて 1(消える)、d=12 で 0.4900 → 0(残る)。1ピクセルで切り替わる。40px かけて混ざる挙動にはならない。
だから feathering を上げても境界は柔らかくならない。 「描き直す帯」が内側へ広がるだけだ。なじませるなら生成後にもう一度かける(できた画像を戻し、境界をまたぐ範囲を低い denoise で再生成)方向になる。⚠ この2パスの有効性は実装からの推論で、生成結果は比較していない。
feathering が丸ごと無視される条件がある
実装にはこの条件が付いている。
if feathering > 0 and feathering * 2 < d2 and feathering * 2 < d3:
d2=高さ・d3=幅(元画像のほうで、広げた後ではない)。feathering×2 が高さか幅のどちらかと同じか大きいと、フェザーは丸ごと飛ばされる。マスクは元画像の領域が全部 0=完全に硬い境界になる。
| 元画像サイズ | feathering | 結果 |
|---|---|---|
| 512×512 | 40 | 適用される |
| 128×128 | 40 | 適用される |
| 80×80 | 40 | 🔴 スキップ(80×2 が 80 未満でないため) |
| 79×512 | 40 | 🔴 スキップ(短いほうの辺だけで決まる) |
何も出ない。 スキップ条件(80×80・feathering=40)で実ノードを走らせ、標準出力・標準エラー・logging・warnings・例外を捕捉したところ全部ゼロ件で、マスクだけが 0.000 になった。⚠ 捕捉したのはノードを呼び出した範囲で、UI 側の通知は見ていない。小さい素材やタイル処理、細長い画像では、ここを疑う価値がある。
padding の値に制約はある?
4辺とも step: 8 と宣言されており、8の倍数で指定する。加えて VAEEncodeForInpaint 側で画像が vae.downscale_ratio(多くのVAEで8)の倍数へ切り詰められるため、元画像も広げ幅も8の倍数で揃えるのが安全だ。
grow_mask_by は既定 6(最大64)。丸めた後のマスクに g×g の畳み込みをかけて膨らませる処理だが、「6なら6px広がる」ではない。 21px幅の矩形マスクで実測した。
| grow_mask_by | 0 / 1 | 3 | 6(既定) | 7 | 16 |
|---|---|---|---|---|---|
| 膨張(左 / 右) | 0 / 0 | +1 / +1 | +2 / +3 | +3 / +3 | +7 / +8 |
合計の広がりはおおよそ g−1 ピクセルで、偶数だと左右が非対称になる(padding = ceil((g−1)/2) で出力が1px大きくなり片側へ寄る)。1 は何も起きない。左右を揃えるなら奇数を選ぶ。⚠ 測ったのは膨張量までで、継ぎ目の減り方は比較していない。
実務の設定はどうする?
- 広げ幅も元画像も8の倍数に揃える
- 短辺が
feathering×2以下なら効かない。小さい素材では値を下げる featheringは「消して描き直す帯の幅」で決める(実効は約 0.29 倍)grow_mask_byは奇数にすると左右が揃う
顔だけの描き直しは FaceDetailer の領分。書き出し時の色は色管理と書き出し、基本操作は使い方完全入門を参照。
よくある質問
仕様どおりの挙動です。マスクは VAEEncodeForInpaint で .round() により 0/1 に丸められるため、feathering を上げても境界は柔らかくなりません。変わるのは「消して描き直す帯の幅」だけです。なじませたい場合は、生成後に境界をまたぐ範囲を低い denoise で再生成する2パス構成が考えられます(生成結果の比較は本記事では行っていません)。
0.5 の中間グレーです。torch.ones(...) * 0.5 で初期化され、端のピクセルを引き伸ばす処理は入っていません。埋めた色は手掛かりにならないため、広げる面積が大きいほどプロンプト側の指示が重要になります。
feathering×2 が元画像の高さまたは幅以上だと、フェザー処理は丸ごとスキップされます。既定値 40 の場合、短辺が 80 以下の画像では一切かかりません。警告は出ないため、値を下げて短辺の半分未満に収めてください。
4辺とも step: 8 で8の倍数です。さらに VAEEncodeForInpaint 側で画像が vae.downscale_ratio(多くのVAEで8)の倍数へ切り詰められるため、元画像も8の倍数に揃えると端が削られません。
VAEEncodeForInpaint の入力で既定は 6、最大 64 です。丸めた後のマスクを数ピクセル膨らませ、潜在空間での継ぎ目を減らす目的で使われます。ただし「6なら6px」ではなく、実測では合計およそ g−1 ピクセルで、偶数だと左右非対称になります(既定の6で左+2・右+3、1では何も起きません)。左右を揃えたい場合は奇数を指定してください。継ぎ目がどれだけ減るかは本記事では比較していません。
まとめ
ComfyUI のアウトペイントは中間グレーで広げて描き直させる素直な仕組みだ。誤解が生まれるのは feathering で、なめらかなマスクを作っておきながら後段で 0/1 に丸めてしまうため、ぼかし幅としては機能しない。実効は約 0.29 倍の「描き直す帯の幅」と考えると値を決めやすい。
そして短辺が feathering×2 以下なら、フェザーは黙って無効になる。値を入れたのに何も起きないときは、まずここを疑いたい。継ぎ目をなじませるなら、丸めの影響を受けない 2パス構成が理屈のうえでは有利だ。⚠ 生成結果そのものは比較していないので、ここは実装の挙動からの推論である。

コメント