技術 約27分で読めます

MacのComfyUIでAnimaのインペイントを動かす、LanPaint + Crop-and-Stitchで32分→2分半

いけさん目次

M1 Max 64GBのComfyUI環境で Anima (Qwen-Image系) のインペイントを実用速度で回したい、というのが今回の動機。普通の人はこれをCUDAでやるのでMacでやろうとした事例はほとんど見当たらず、SDXL流に書かれた既存ガイドが何箇所かで止まる。

ざっくりの流れから書くと、最初に SetLatentNoiseMask が Anima + Anima-Turbo LoRA で黙って動かない症状を踏んで67秒を溶かした。LanPaint に切り替えて公式 Example_26 で動作確認できたものの、M1 Max では1枚に32分45秒かかる。そこへ Inpaint-CropAndStitch を被せて文字差し替えなら2分31秒、ヌード画像にシャツとスカートを着せる服装置換でも7分前後で回るところまでは持っていった。ただし服装置換は速度の問題だけ解決した状態で、出力されるポーズは元画像の contrapposto と胸前傾を引き継いだままになる。プロンプト調整・分割マスク・段階的インペイントをいくら積んでもここから抜けられず、ControlNet OpenPose 等で骨格を直接固定しない限り根本的な解決は難しい、というのが実機検証で出た限界。CUDA環境なら速度面の最適化は要らない話で、公式テンプレ素のまま RTX 3060 12GB クラスで3〜5分の見込み。今回のチューニングは Mac 特有の苦肉の策で、ポーズ問題はその先の宿題として残る。

順番に書く。

検証環境

項目
マシンMacBook Pro M1 Max 64GB unified memory
OSmacOS Darwin 25.3.0
Python3.13.11(miniconda base)
PyTorch2.10.0
バックエンドMPS
ComfyUI0.16.4
フロントエンド1.41.16
追加 custom_nodeLanPaint, ComfyUI-Inpaint-CropAndStitch

検証中に触ったモデル類を整理する。「インペイントで実際に使うのは」列が今回のインペイント側で当てたかどうか。

種類ファイル名インペイントで実際に使うのは用途
diffusion modelanimaOfficial_preview3Base.safetensorsCircleStone Labs公式 Anima preview3-base 派生。インペイントの主役
diffusion modelwaiANIMA_v10.safetensors-(参考)preview3-base のWAI調整版。txt2imgで使うことがある
text encoderqwen_3_06b_base.safetensorsQwen3 0.6B、Anima 同梱の組合せ
VAEqwen_image_vae.safetensorsQwen Image VAE
LoRAkanachan-waianima-rework-v4_epoch150.safetensors×(今回は付けない)別途運用してるキャラLoRA。当てるとマスク内で意図しない方向に学習特徴が出る/出ない問題が混ざるので、今回はLoRA無しの素の挙動だけを見たい
LoRAanima-turbo-lora-v0.1.safetensors×(後述、インペイントで黙って動かなくなる)txt2imgの高速化用。インペイント経路では原則オフ

custom_nodes には今回新しく LanPaintComfyUI-Inpaint-CropAndStitch を入れた。どちらも依存ライブラリは無く、git clone だけで読み込まれる。ComfyUI起動時に既存の comfyui-impact-packsegment_anything 系の依存欠落で import failed になるが、本筋のインペイント には影響しない。rembg-comfyui-node-better 側は OMP の二重ロードでクラッシュするので、起動時に KMP_DUPLICATE_LIB_OK=TRUE を必ず付けて回避する。

KMP_DUPLICATE_LIB_OK=TRUE python ~/ComfyUI/main.py

SDXL流の VAEEncode + SetLatentNoiseMask が無音で動かない

最初に組んだのは教科書通りの構成で、LoadImage を VAEEncode と MaskBlur+ に分岐させ、SetLatentNoiseMask で latent にマスクを刺してから KSampler に流す、という流れ。

flowchart TD
    A[LoadImage] -->|IMAGE| B[VAEEncode]
    A -->|MASK| C[MaskBlur+]
    B --> D[SetLatentNoiseMask]
    C --> D
    D --> E[KSampler]
    E --> F[VAEDecode]
    F --> G[SaveImage]

KSampler 側は普段の Anima txt2img と同じ 10 steps / cfg 1.0 / er_sde / simple + Turbo LoRA。denoise=0.45 で 67.50秒。完了するが、出力画像は入力とほぼ同一。「Turboがマスク付きサンプリングに弱いだけかな」と思ってまず denoise=1.0 にして マスク領域を完全に再生成させようとしたが、それでも結果は同じで、マスクの内側も外側も元画像そのものが返ってくる。

切り分けのために MaskPreview+MaskBlur+ の出力と LoadImage の MASK 出力それぞれに繋いで、サンプラーに届くマスクを目視した。塗ったエリアが正しく白で抽出されている。ここで「マスクは届いているが KSampler が マスク領域を一切触っていない」が確定する。

ComfyUIの comfy/samplers.py:399 を読みに行くと、マスク付きサンプリングは次の経路を通っている。

latent_mask = 1. - denoise_mask
x = x * denoise_mask + self.inner_model.inner_model.scale_latent_inpaint(
    x=x, sigma=sigma, noise=self.noise, latent_image=self.latent_image
) * latent_mask

scale_latent_inpaint はモデル個別のメソッドで、Anima/Qwen-Image は BaseModel のデフォルト実装 model_sampling.noise_scaling を使う。理屈上はフローマッチングでもマスク領域に対しては (1-sigma)*latent + sigma*noise でノイズが入り、マスク外領域はクリーンが保たれ、両方をsigma 1→0で降ろせばマスク領域は再生成されるはず。だが実機ではこれが動かない。

仮説は2通り考えられる。Anima-Turbo LoRA がモデルのノイズ予測を「特定の少ステップ用に圧縮した軌道」へ強く張り付けていて、マスク領域に注入される (1-sigma)*latent + sigma*noise の中間状態が訓練分布の外になっていて、モデルがマスク領域を動かさない予測を返している、というのがひとつ。あるいはフローマッチング系で er_sde + simple + denoise=1.0 + 画像由来 latent の組み合わせが、内部的に start_sigma=end_sigma みたいになってサンプリングが空回しになっている、という線。後述するように Anima-Turbo を外した素の構成だと SetLatentNoiseMask 経路でも マスク領域が動き出すので、Turbo LoRA との相性が支配的なところまでは分かる。普通の Anima txt2img では Turbo は問題なく使えるので、インペイント時だけ表面化する。

通常のインペイントガイドはマスク描いて Queue するだけで結果が出る前提で書かれていて、「ピクセル単位で同一画像が返ってくる」ような症状はそもそも想定されていない。エラーも警告も出ないので、SDXL用のガイドをそのまま Anima + Turbo に持ってきた人だけが踏むことになる。検索しても出てこない。

LanPaint公式 Example_26 で動作確認まで持っていく

Civitai と GitHub をしばらく漁って、マスク対応の Anima/Qwen-Image インペイント例で最も成功例が見える形で残っていたのが LanPaint。ComfyUI 用 custom_node の学習不要なインペイントサンプラーで、KSampler を LanPaint_KSampler に差し替えるだけで使える。READMEには対応モデル一覧の中に Anima が明記されている。

Universal Compatibility – Works instantly with almost any model (Z-image, Z-image-base, Hunyuan, Wan 2.2, Qwen Image/Edit, Anima, HiDream, SD 3.5, Flux-series, SDXL, SD 1.5 or custom LoRAs) and ControlNet.

examples/Example_26/ に Anima 用の動作確認済みワークフローが PNG メタデータ込みで置いてあって、ComfyUI に PNG をドラッグするとワークフローがそのままロードされる。

LanPaintのキーアイデアは Multiple iterations before denoising。通常のサンプラーは1ステップで1回モデルを呼ぶが、LanPaint はマスク内側と外側の一貫性を取るための 思考反復 を各ステップで回す(LanPaint_NumSteps で本数を制御)。実コストは大雑把に 通常のstep数 × NumSteps になる代わりに、SetLatentNoiseMask が黙って動かない条件下でも マスク領域がちゃんと更新される。READMEは 蒸留系LoRAについて警告も出していて、Anima-Turbo LoRA も distillation 系なので最初は Turbo をバイパスして動かした。

Warning: LanPaint has degraded performance on distillation models, such as Flux.dev, due to a similar issue with LORA training. Please use low flux guidance (1.0-2.0) to mitigate this issue.

公式 Example_26 をそのまま走らせる

InPainted_Drag_Me_to_ComfyUI.png を ComfyUI 画面にドラッグするとワークフローがロードされる。サンプル入力画像 Masked_Load_Me_in_Loader.png も同梱されていて、LanPaint ロゴが書かれた看板を持つキャラクターの看板部分にマスクが焼かれている。

中身は1個のグループノードに圧縮されているので Convert to Nodes で展開すると、UNETLoader が anima-preview3-base.safetensors、CLIPLoader が qwen_3_06b_base.safetensors、VAELoader が qwen_image_vae.safetensors、LanPaint_KSampler が steps=30 / cfg=5.0 / er_sde / simple / denoise=1.0 / NumSteps=5 / Prompt mode=Image First、内部処理は ImageScale で 512×512 に揃えてある、という構成。手元の checkpoint は animaOfficial_preview3Base.safetensors にリネームされていたのでそこだけ差し替えた。

KMP_DUPLICATE_LIB_OK=TRUE で起動した ComfyUI に投入したときの M1 Max での実測ログ。

0%|          | 0/30 [00:00<?, ?it/s] 
3%|▎         | 1/30 [01:09<33:45, 69.83s/it] 
...
Prompt executed in 1965.18 seconds

1ステップあたり約66秒、合計で 32分45秒。出力画像はマスクされた看板領域に LanPaint の文字が描き込まれていて、品質的にも公式が掲載しているサンプルとほぼ同じ。

LanPaint Example_26 baseline output on M1 Max

つまり LanPaint + Anima は動くが、M1 Max でこの設定だと1枚に32分かかるので、日常運用で何枚も回す気にはならない。LanPaintの思考反復の効果は大きく、SetLatentNoiseMask が単体では動かなかった条件でもここでは マスク領域がちゃんと別物に置き換わる。マスク内側と外側の整合を取る反復処理が、蒸留による軌道のズレを補正してマスク領域でも収束させているように見える。

Inpaint-CropAndStitch で 13倍速にする

LanPaint の重さの大部分はフル画像にサンプリングを掛けているところからきている。Anima/Qwen-Image は1024×1024相当の latent を回すと M1 Max の MPS バックエンドで 1ステップあたり60秒級になる。マスク領域が画像のごく一部しかない場合、マスク外の領域を全部サンプリングするのは無駄になる。

lquesada/ComfyUI-Inpaint-CropAndStitch は、マスクのバウンディングボックスにコンテキスト分の余白を足してクロップし、その小さい画像にサンプリングを掛けて、結果を元の位置にステッチして戻すノード群。InpaintCropImproved が image と mask を受け取って stitcher ハンドルと cropped_image / cropped_mask を返し、最後に InpaintStitchImproved が stitcher と inpainted_image から元サイズの IMAGE を組み直す。組んだフローは以下。

flowchart TD
    A[LoadImage] --> B[InpaintCropImproved]
    B -->|cropped image| C[VAEEncode]
    B -->|cropped mask| D[SetLatentNoiseMask]
    C --> D
    D --> E[LanPaint_KSampler]
    E --> F[VAEDecode]
    F --> G[InpaintStitchImproved]
    B -.->|stitcher handle| G
    G --> H[SaveImage]

SetLatentNoiseMask は LanPaint と組み合わせる場合もそのまま経路に残しておく。クロップ後の小さなマスクが LanPaint に渡る。

公式デフォルトから削ったのは、LanPaint の steps を30→15に、LanPaint_NumSteps を5→3に、output_target_width/height をフルから 512×512 に、の3点。

項目デフォルト採用値効果
LanPaint steps3015サンプリング回数を半分に
LanPaint_NumSteps53思考反復を6割に
output_target_width/height(フル)512クロップ後の処理解像度を絞る

これで LanPaint_Anima_Example26_Masked.png をクロップインペイントした結果のログ。

Prompt executed in 151.03 seconds

2分31秒で完走。元の32分45秒から約13倍の短縮。1step あたり約10秒、最初は約66秒だったので、解像度と 思考反復の削減で 1ステップあたりが6倍以上稼げている。

クロップ+ステッチ optimized LanPaint result on M1 Max

品質は若干落ちる。公式デフォルトでは看板の文字が LanPaint の大文字小文字混じりで正確に描かれていたのが、削った設定では LANPAINT のオールキャップスになった。文字を正確に描くようなディテール要求は 思考反復 を削ると弱るが、髪型・服装・小物程度の置換なら変化に気付けないレベルに収まる。

服装置換に応用する

文字インペイント はマスクが小さいので クロップの旨味が大きいケース。実運用に近いのは服装置換のほうで、試したのは genserver_00195_.png という Anima 素のtxt2imgで出した立ち姿のヌード画像(832×1312)に、白い襟付き半袖シャツ・赤いネクタイ・紺色のプリーツスカートを後付けで着せるケース。kanachan のような独自LoRAを当てて動かすのは適用の敷居が別途あるので、まずはLoRA無しの素の挙動でインペイントそのものを見るのが目的。マスクは胸〜膝中ほどまで覆うのでサンプリング領域が広く、クロップの旨味は薄い。

source image with programmatic ellipse mask overlay

最初は試しにPILで楕円マスクを生成して焼き込んだ。上の画像で赤の領域がインペイント対象になる。

服はディテールが多いので解像度を上げる。マスクが広いぶん context_from_mask_extend_factor の拡張は控えめにしてサンプリング領域を絞り、mask_blend_pixels は肌との境界を滑らかにするために増やす。LanPaint_NumSteps も少し戻して品質寄りにした。

項目文字インペイント服装置換理由
output_target_width/height512768襟・ネクタイの結び目・プリーツのディテール用
context_from_mask_extend_factor1.51.3マスクが既に大きいので過拡張を抑制
mask_blend_pixels3248〜64肌との境界を滑らかに
LanPaint_NumSteps34思考反復を少し戻して品質寄り
steps1515維持
cfg4.04.0維持

プロンプトはポジティブ側に服装を明示、ネガティブ側にヌード抑制と体型暴走抑制を入れる。

masterpiece, best quality, score_7, safe. 1girl, long blonde hair, slim figure,
standing pose, white collared button-up shirt with short sleeves, red necktie,
navy blue pleated skirt, bare legs, white background, full body.

(negative)
worst quality, low quality, score_1, score_2, score_3, blurry, jpeg artifacts,
sepia, nude, naked, topless, exposed breasts, nipples, pubic, censor

768解像度 + マスクが広いと 1ステップあたり30秒前後まで上がる。15steps で1枚 約7分

1枚目(プログラム楕円マスク)で違和感が同時に出る

PILで作った楕円マスクをLoadImageに焼いて1枚目を回した結果。指定どおりの服はちゃんと出ているが、見れば見るほど違和感がある。

1枚目: ellipse-mask attempt, neck looks shrugged because the mask cut into the collarbone area

詰まった違和感を順に書く。まず首をすくめている。元画像と 1枚目 で頭部を並べると、1枚目はシャツの襟が首に食い込んで実際に首が短く詰まって見える。

original (left) and 1枚目 (right): the ellipse mask cut into the collarbone, so the inpaint placed the shirt collar high and compressed the neck

楕円マスクの上端が鎖骨を越えて首の付け根近くまで侵入していて、インペイントがシャツの襟をその位置から立ち上げてしまった結果。同じ理由でスカートのウエスト位置も胸の真下から始まっている。マスク形状が引き起こした問題で、マスクの上端を鎖骨より明らかに下まで引けば消える症状になる。

加えて、太もも・腰幅が アニメ系モデルの学習バイアスで盛られていて、ポーズも contrapposto(重心一脚、腰ひねり、肩傾き)+ 胸前傾になっている。これは 1枚目に限らず後の試行でも残り続ける問題で、マスク形状の話とは別レイヤー。

2枚目(手描きマスク)で首とスカート位置は治る、ポーズは残る

楕円ではなく手描きでマスクを塗り直したのが2枚目。意図的に「首を外そう」とした訳ではなく、単に手で塗ったら鎖骨より下に収まり、結果として 1枚目で出ていた首食い込みとスカートの胸下スタートが両方とも消えた。

2枚目: hand-painted mask removes the shrugged neck and high skirt; chest-forward pose and thicker thighs still remain

シャツの襟は鎖骨の下から立ち上がるようになって首詰まりは消えた。スカート位置も胸の真下スタートから降りた。ただし太もも・腰幅・contrapposto・胸前傾はそのまま。マスク形状で消える問題と、マスク外の保存領域から伝わって消えない問題、ふたつの層が分かれた。

3枚目(手描き + slim modifier)で体型は細くなるがポーズは変わらない

太もも・腰幅の アニメ系の盛りバイアスを抑えるため、ポジティブ側に slim hips, slim thighs, slender figure, narrow waist、ネガティブ側に thick thighs, wide hips, hourglass figure, voluptuous, curvy, large hips, thick waist を盛って3枚目を回した。

3枚目: slim modifiers reduce thigh thickness, contrapposto pose unchanged

太もも・腰幅は明らかに細くなった一方で、ポーズの contrapposto はそのまま残る。「煽り」感の正体は太ももの太さではなくポーズの捻りだった、というのがここで確定する。プロンプトの slim 系は体型を細くしてもポーズは変えない。

4枚目(手描き + ポーズネガ盛り過ぎ)で胸元に小さい人物が2体現れる

ポーズの contrapposto をプロンプトで消そうとして、ポジティブ側に standing straight, symmetrical pose, weight on both legs, facing forward、ネガティブ側に contrapposto, hip out, twisted torso, weight on one leg, leaning, sexy pose, S-curve, hand on hip を追加した4枚目。

4枚目: aggressive negatives produced two small clothed figures overlaid on the chest

シャツの胸元に、シャツを着た小さい人物が2体重なって描かれている。ゴーストというより単純な解剖崩壊で、sexy poseS-curve のような抽象表現をネガに盛ったことで、モデルが「ポーズを取らない」「重心を片方に置かない」のような相反する制約を同時に満たそうとして マスク領域内で構図が破綻したと見える。ネガティブを増やすほど挙動が暴れる方向に振れる、というのが教訓。

並べて見ても太ももや腰幅は3枚目で多少細くなっただけで、ポーズと胸前傾は2〜4枚目でまったく変わっていない。4枚目では別の崩れまで顔を出していて、対策を重ねるほど「改善」より「新しい崩れ方が増える」状態に向かう。

original / 3枚目 / 4枚目 side by side: all three keep the same chest-forward contrapposto pose

左から元画像、3枚目(slim追加)、4枚目(ポーズネガ盛り過ぎでゴースト発火)。胸前傾と片足重心はどれも同じ。ポーズの contrapposto を確実に消したいなら、プロンプトではなく ControlNet OpenPose で骨格を直接固定するのが正攻法で、今回は Anima/Qwen-Image 用 OpenPose ControlNet の導入までは手を付けていない。プロンプト調整で頭打ちになったので、次の段はそちらに移すしかない。

変数を1つに絞る、スカート単独試行

「全部いっぺんに着せる」と「ヌードを服に置換する」の組み合わせは変数が多すぎる。試しにスカートだけ生成する分離テストに切り替えて、マスクを下半身だけに塗り直し、プロンプトもスカート系のみに削った。上半身は元のまま残る。

最初に試したのがポジティブ側に low-rise skirt, skirt sitting at hip level, skirt below the navel の3点盛り。

skirt placed too low: low-rise overshoot

スカートが股下まで下がりきって、ローライズ過剰の典型に。3つの low 系トークンを同時に投げたせいで暴走したかたち。

「low」トークンを引っ込めて、ポジティブ側に mid-rise skirt, waistband at natural waist, waistband covering the navel、ネガティブ側に high-waisted skirt, low-rise skirt, hip hugger, micro skirt を入れ、両極をネガで挟む構成に変更したのが次。

skirt waist normalized but flank seam and extra hand at the waistband

ウエスト位置は良くなった一方で、脇腹に段差が出て、さらにスカートのウエスト右側からは余計な手が生えている。段差はマスク内のシルエットとマスク外のシルエットが境界線でわずかにズレて横腹に肉付きの差として表れたもので、余計な手はマスク領域内でモデルが予期せぬ解剖を足してしまった別問題。段差のほうは mask_blend_pixels を48→64、mask_expand_pixels を0→8でマスク境界を肌側へ少し押し出し、ポジティブから narrow waist を削除して対処した(マスク内の胴体だけが細くなって段差を悪化させていた)。

これで回した結果。

skirt-only final: clean silhouette but not the intended skirt design

違和感は消えた。ただスカートのデザインが想定外で、短いnavyベースに赤いラインが縦に入った造形になっている。プリーツとして指定していたのに、見た目は艦これの島風のスカートに近い別物。プロンプトでスカートを描かせる時のモデルの学習傾向は「ウエストの位置」を動かすと「スカートの種類」も連動して変わるらしく、腰位置は安定したぶん、デザインのほうが当初想定から外れた。

2パス目で上半身を足す

ここまで来ると、次の作業はこのスカート画像の上に白シャツ + 赤ネクタイだけを別マスクで足す2パス目になる。スカート単独結果(clothing_inpaint_00004)を入力にして、マスクを上半身だけ(鎖骨下〜既存のスカートウエスト上端)に手で塗り直し、プロンプトを白シャツ + 赤ネクタイだけに絞った。1パス目のスカートと下半身の素肌はマスク外なのでそのまま残る。

masterpiece, best quality, score_7, safe. 1girl, long blonde hair, slim figure,
standing pose, white collared button-up shirt with short sleeves,
red necktie tied neatly at collar, untucked shirt hem at hip,
white background, full body, slim figure, slender shoulders.

(negative)
worst quality, low quality, score_1, score_2, score_3, blurry, jpeg artifacts,
sepia, censor, nude, naked, topless, exposed breasts, nipples, school uniform,
sailor uniform, deep cleavage, see-through shirt, wet shirt, open shirt

768crop / 15step / NumSteps=4 / cfg=4.0 で 413秒(6分53秒)。

two-pass result still shows the same pushed-forward chest pose as earlier attempts

胴体ゴーストも脇腹段差も出ず、ネクタイの結び目とシャツの襟もそれらしく描かれている。1パス目で確立したスカートと下半身がそのまま残っているのもマスク手描きの効果。ただしポーズは2〜4枚目と何も変わっていない。胸が前に張り出した不自然な構図、片足重心の contrapposto、ともに引き継がれている。服のパーツごとに別マスクで段階的にインペイントすることでネガティブ盛り過ぎ起因のトークン衝突は避けられたが、根本のポーズと胸前傾は手付かずのままで、ControlNet を入れない限りこの構図から抜けられない見通し。

9枚目(ブラだけ)で胸前傾が薄くなる、シャツプロンプトそのものが犯人説

ここまで「ポーズは元画像のマスク外領域から伝わる」とずっと言ってきたが、実は別の要因が混ざっているかもしれない、という疑いが出てきた。シャツ + ヌード体の組合せ自体が アニメ系モデルの「胸を強調する服装ポーズ」へのバイアスを強く呼んでいる可能性。
それを切り分けるために、シャツの代わりにシンプルなブラだけを胸元に描く試行をやってみる。

ワークフローの LoadImage を原画 genserver_00195_.png に戻し、ポジティブ側を plain white bra, simple cotton bra, soft bra cups, natural breast shape, bare midriff だけに絞り、ネガティブ側にも push-up bra, padded bra, deep cleavage, pushed-up chest, voluptuous のような体型誇張抑制を盛った。マスクは胸元のごく狭い領域のみ。

bra-only pass: simpler garment, more natural body silhouette, but contrapposto still present

結果はスポブラ風のシンプルな細いタンクトップ。注目したいのは胸前傾の度合いがあきらかに薄いことで、シャツ系の試行 2〜4枚目では胸が強く前に張り出していたのが、ブラだとそれが弱まる。バンドゥで隠れて見えなくなっただけかもしれないし、マスク内で実際に平面側に寄せられたのかもしれない、これだけでは判別不能。だが少なくとも「ブラ」プロンプトと「シャツ」プロンプトでモデルの呼び出される学習バイアスの方向が違う、ということだけは観測できた。
ポーズ自体は片足重心と肩の傾きを継いだ contrapposto のままで、これはマスク外領域から伝わっている部分。

ここから次の戦略が決まる。ブラ → ワンピース → ワイシャツ の3段階で段階的に被覆領域を広げる。ワンピースで腹までクロスを伸ばしておけば、最後のシャツ置換は「衣服 → 別衣服」の置き換えになり、アニメ系モデルの「素肌に新規服装」というバイアスを踏み抜かずに済むのではないか、という見通し。次パスはこのワンピース化を試す。

10枚目(ワンピース)で被覆領域を腹まで広げる

LoadImage を 9枚目の結果(bra_pass_inpaint_00001_.png)に切り替え、ポジティブを simple white sleeveless one-piece dress, plain cotton dress, knee length, casual sundress, covering torso and hip naturally、ネガティブにも tight dress, sexy dress, mini dress, see-through dress を入れて10枚目を回した。マスクは胸〜腰下、既存のスカート上端少しまで含めて手で塗る。

dress pass: tunic-style top covering chest to hip, natural silhouette

結果としては「ワンピース」ではなく、上半身を覆う袖なしのチュニック上と既存スカートの組合せに着地した。マスクが既存スカートの上端だけしか覆っていなかったので、モデルはマスク外のスカートをそのまま残し、マスク内には上半身分の生地だけを描いた。マスク通り、と言える挙動。
体型の暴走も特になく、ブラパスと同様に胸前傾の度合いは薄いまま。重要なのは「上半身が連続した布で覆われた状態」が確立されたことで、次のシャツ置換で「素肌 + シャツ」のバイアスを踏まずに「衣服 → 衣服」の置き換えにできる前提条件が揃った。

11枚目(チュニック → ワイシャツ置換)で破綻、ただし観測は得られた

10枚目を入力にして、マスクをチュニック領域だけに塗り、ポジティブを white collared button-up shirt with short sleeves, red necktie tied neatly at collar, untucked shirt hem at hip、ネガティブに nude, naked, topless, exposed breasts, nipples, pushed-up chest, deep cleavage, voluptuous, see-through shirt, wet shirt, open shirt, dress, sundress, tunic を入れて11枚目を回した。狙いは「衣服 → 衣服置換」で胸前傾バイアスを回避すること。

11枚目: white shirt + tie underneath, with an extra unrequested cardigan/vest layered on top

白シャツ自体はちゃんと出ている。襟・前ボタン・ネクタイの位置もそれらしい。問題は、その上に赤いカーディガンかパーカー風の羽織りが勝手に追加されていること。前面が縦に開いていて中の白シャツが見える、ベスト/カーディガンを羽織って前を開けた状態に近い。マスクが下のチュニックを完全に覆い切らず、前段の上半身の色情報が「シャツの上に羽織るもう一枚」としてモデルに解釈された結果と見える。指定したのは「白シャツ + 赤ネクタイ」だけで、羽織りは要求していない。
さらに観察すると、9〜10枚目までは少し体が斜めの3/4向きだったのに、11枚目では体が正面に回転している。シャツ + ネクタイのプロンプトが「ネクタイを正面に見せる立ち姿」の学習バイアスを呼んだか、別経路で体の向きまで動かしたかは特定できないが、結果として正面向きに変わった。これがあると「胸前傾が薄くなった」ように見えるのも、重ね着戦略の効果ではなく正面向きになって胸の凸が視覚的に見えにくくなっただけの可能性が出てくる。胸自体のボリュームも10枚目より萎んで見え、シャツとカーディガンの2枚分の布で押し込まれた格好。重ね着戦略の検証としては結論を出せない、というのが妥当な評価。

12〜13枚目(ネクタイ無しシャツ)でネクタイ寄与を切り分ける

11枚目で体が完全に正面回転した原因を切り分けるため、ネクタイを抜いて同じ条件で回す。LoadImage は10枚目(dress_pass_inpaint_00001_.png)のまま、ポジティブから red necktie tied neatly at collar を抜き、open collar, top buttons unbuttoned, collar slightly spread, no necktie を加える。ネガティブには necktie, tie, bow tie, neckwear, ribbon, cardigan, vest, jacket, hoodie を入れて、ネクタイの再侵入と11枚目の謎の羽織りの再発の両方を抑える。

12枚目: no necktie, top button unbuttoned, body is partly turned

12枚目はパッと見では正面向きに見えるが、襟の左右の高さや角度を細かく見ると左右非対称で、ワンピース(10枚目)と同じ程度の3/4向きが残っている。シャツの開襟が左右対称に近く見えるため正面臭く見えるが、実際の体の向きは11枚目より戻っている。

続けて、ボタン列の縦線がまだ正面感を作っているという観察から、もう一段大きく前を開けた版が13枚目。プロンプトに front buttons mostly unbuttoned down to chest, shirt front open, button placket spread apart, casually open shirt を盛った。

13枚目: aggressively unbuttoned shirt, button row partly broken up

開襟は広がってボタンの見える数は減ったが、残ったボタン列はまだ中央を縦に貫いていて、シャツ全体の正面感は完全には消えない。体の向きは正面〜やや3/4の中間。

ここで観察として「シャツの構造要素それぞれが独立に正面方向の学習バイアスを呼んでいる」のではないかという仮説に至る。襟は左右対称な造形なので正面から描かれる方が自然、ボタン列は中央に縦に並ぼうとして正面構図を作る、ネクタイは見える位置に置こうとして体を正面に向ける、というように要素ごとに「正面に見せる立ち姿」を独立に呼び寄せる構造になっている、という見方。袖は腕がマスク外で保存されているので関係がない。

14枚目(Tシャツ)でシャツ構造要素を全部落とす

仮説を試すため、シャツ全体を クルーネックのプレーンTシャツ に振る。ポジティブを plain white t-shirt with short sleeves, crew neck, no buttons, no collar, no necktie に切り替え、ネガティブにも button-up shirt, collared shirt, polo shirt, henley, v-neck を入れてシャツ構造の再侵入を抑える。

14枚目: plain white t-shirt, crew neck, no shirt structure elements

結果はプレーンな短袖Tシャツ。胸の凸は11〜13枚目より明らかに薄く、原画寄りの自然な体型に近い。ただしTシャツが左右対称の平板な造形なので、体の向き自体は判定不能 に陥る。3/4向きに戻ったのか正面のままなのかが見た目から決められない、という別の問題。

仮説の最終決着は付かなかった。シャツ構造要素を全部落とせば「胸前傾を作る要素は消える」ところまでは観測できたが、「Tシャツに換えれば3/4向きに完全に戻る」かどうかは、Tシャツの情報量の少なさのせいで判別できないまま。影の落ち方を見る限り完全な正面ではないが、確信できるほどの非対称性も画面上に出てこない。シャツ系プロンプトが正面方向の学習バイアスを呼ぶ傾向は弱く支持される、というのが現時点の評価。

ここまでの全試行を一覧で並べる

途中の判断ミスを含めて、15枚を1枚にまとめて並べる。各セルが何の試行かはラベル参照。

15 attempts side by side: original, 1-4枚目 direct clothing, 5-7枚目 skirt-only, 8枚目 2-pass, 9枚目 bra, 10枚目 dress, 11枚目 shirt with necktie, 12-13枚目 shirt no necktie, 14枚目 plain t-shirt

このグリッドから読めるのは、

  • 胸前傾の強度シャツ系プロンプト が強く相関している(1〜4枚目・8枚目・11〜13枚目)。一方、ブラ(9枚目)・チュニック(10枚目)・Tシャツ(14枚目)では胸前傾が薄い
  • 腕の位置は全画像でほぼ同じ。マスク外で保存されているため、袖の有無は腕の角度に影響しない。袖起因仮説は支持されない
  • 元画像のポーズ(contrapposto・片足重心・左手胸元)も全画像で保存されている
  • シャツの構造要素(襟・ボタン列・ネクタイ)がそれぞれ独立に正面方向の学習バイアスを呼ぶ という仮説に着地した。襟は左右対称な造形ゆえに正面描画寄り、ボタン列は中央に縦に並ぶ性質ゆえに正面構図を作り、ネクタイは見える位置に置こうとして体を前へ回す。要素を全部落としたTシャツでは胸の凸が薄くなったが、Tシャツ自体が平板で情報量が少ないため、3/4向きに完全に戻ったかどうかは判定できない

つまり重ね着戦略は「素肌 → 服」のトリガーを回避する手段としては筋が通るが、最終的にシャツ系プロンプトが構造要素ごとに呼ぶ学習バイアスは別経路で残り、プロンプトだけで完全には抜けない、というのが現時点の理解。Mac環境でのこれ以上の追求はコスト対効果が悪く、ControlNet OpenPose で骨格を直接固定するルート以外には現実的な解が見えていない。

1ステップあたりの所要時間

各設定の1ステップあたりと総時間を並べる。

設定1ステップあたり総時間用途
公式 Example_26 デフォルト(30step / NumSteps=5 / フル画像)約66秒32分45秒動作確認、テンプレ参照用
クロップ+ステッチ (15step / NumSteps=3 / 512crop / 小マスク)約10秒2分31秒文字差し替え、髪型微修正
クロップ+ステッチ (15step / NumSteps=4 / 768crop / 大マスク)約30秒7分前後服装置換、衣装パーツ追加

Turbo LoRA はインペイント経路では原則オフ。txt2img では同じ環境で 10step / cfg 1 / er_sde / simple の Turbo 設定で約50秒/枚なので、生成とインペイントで完全に別レシピになる。

MPS の autocast 制限

LanPaintを実行すると毎回出る警告。

UserWarning: In MPS autocast, but the target dtype is not supported. Disabling autocast.
MPS Autocast only supports dtypes of torch.bfloat16, torch.float16 currently.

LanPaintの内部演算で torch.float32 への autocast を試みているが、MPS は fp32 autocast を未サポートで素のfp32実行にフォールバックしている。CUDAなら同じ箇所がfp16/bf16 autocastに乗って大幅に速い。MPS版PyTorchの成熟度がそのまま1ステップあたりの時間に出ているところで、現状この警告は気にしても直しようがない。今後のPyTorch / ComfyUI更新で改善される余地はある。

検証中に Activity Monitor で GPU と RAM の使用量を見ると、ロード後の RAM 使用は約30GB(unified memoryの半分以下)で、GPU使用率は80〜95%。メモリは余裕があるのに1ステップあたりが伸びる、典型的な計算律速パターン。M1 Max の GPU コアは32コアで tensor core 相当の行列積専用ハードがなく、拡散モデルの計算量はほぼ行列積なので、ここが律速していると見える。CUDA環境との性能差はメモリ容量ではなく行列積スループットと fp16/bf16 autocast の有無に支配される。RTX 3060 12GB(行列積スループットがM1 Maxの数倍、tensor core あり、bf16 autocast対応)に持っていけば、公式 Example_26 デフォルトのままで3〜5分/枚に収まる見込みで、クロップ+ステッチのチューニングは要らなくなる。今回の最適化はM1 Max特有の計算律速を回避するためのもので、CUDA環境では大部分が無意味になる。

検索しても出てこなかったところ

ここまでで踏んだ問題のうち、ドキュメントや既存記事に書かれていなかった、または個別に書かれていても繋がっていなかったポイントをまとめておく。

  • Anima/Qwen-Image + Anima-Turbo LoRA + SetLatentNoiseMask は黙って動かなくなる。マスクが正しく届いていてもKSamplerがマスク領域を一切更新せず、エラーも警告も出ない。denoise=1.0 でも同じ
  • LanPaint は ComfyUI + MPS で Anima に対して動く。Example_26 がそのまま使える
  • M1 Max では LanPaint 公式デフォルトで32分台かかるが、Inpaint-CropAndStitch + step/NumSteps削減で実用速度(1〜8分)まで縮められる
  • LanPaint のネガティブプロンプトに sexy pose S-curve のような抽象表現を盛ると、マスク領域内に複数解剖が重なった幽霊画像が出る。アスペクトの多いプロンプト同士を強く負方向に押すと、モデルが矛盾解を重ね描きする形で出力するため
  • スカートの腰位置を狙う場合、low-rise/mid-rise/high-waisted を片側だけ指定すると暴れるので、両極をネガティブ側に置いて中間にしか答えがないようにする。ただし腰位置を動かすとスカートの種類もつられて変わる(プリーツ→巻きスカート風)
  • マスク境界のシルエット段差は mask_blend_pixels 増 + mask_expand_pixels 増で緩和できる
  • 元画像のポーズ(contrapposto + 胸前傾)はマスク外の体型・腕の位置からモデルに伝わり、マスク領域だけ服を生成してもポーズはこの構図から抜けない。プロンプトの slim 系も pose 系も効かず、段階的にマスクを切ってインペイントしても結果は変わらない。今回のMac向け最適化は速度の問題は解決するが、ポーズ固定にはControlNet等で骨格を直接拘束する必要がある
  • KMP_DUPLICATE_LIB_OK=TRUE を環境変数に入れないと rembg-comfyui-node-better 系の依存が OMP の二重ロードでクラッシュする

ここまでで確立したのは「Mac でもとりあえず動くインペイントパイプライン」と「プロンプト調整で抜けられる問題と、抜けられない問題の境界線」の二つで、最終出力の品質そのものではない。

結論と次の段

プロンプト側のチューニングだけで詰められたのは、文字インペイントの圧縮(1ステップあたり66秒 → 10秒)と、服装置換の速度確保(1枚7分前後)まで。出力ポーズと胸前傾は最後まで抜けなかった。原因は二層あり、ひとつはマスク外で保存される元画像のポーズ(contrapposto・片足重心・腕の位置)、もうひとつはシャツ系プロンプトに固有の正面学習バイアスで、襟・前ボタン・ネクタイそれぞれが独立に「正面に見せる立ち姿」を呼ぶ構造になっている。これは袖の有無や段階的マスク描画では切れない。

この先に進むなら、骨格そのものを ControlNet OpenPose で直接拘束するしかない。Anima/Qwen-Image 用の OpenPose ControlNet モデルの導入、骨格抽出ノード、骨格編集 UI、そのいずれもこの記事の検証範囲外で、また別記事として書く。本記事はそこまでのプロンプト主導での到達限界と、その境界の正体を残すところで終わらせる。

同じワークフローは CUDA 環境にそのまま持っていけて、RTX 3060 12GBクラスでも 1ステップあたりが落ちて クロップ+ステッチ や step/NumSteps 削減が要らなくなる見込み。Mac で詰め続けるより月数千円のクラウドGPUに移して ControlNet も含めて本来の使い方に戻すほうが手間と速度のバランスは良い、というのが現実的な落とし所。


部屋の温度が上がって扇風機を初稼働

この作業中Macも部屋も温度が上昇してるのに、外気温は普通に寒いっていう謎の状況、扇風機を今年はじめて使う事態に。