生成した画像を Photoshop で開いたら色が違う。Web に上げたらくすんだ。犯人はプロンプトでもサンプラーでもなく書き出しの側にいることがある。ComfyUI の SaveImage の実装を読むと、色に関する情報が1バイトも書かれていないことが分かる。
検証環境と方法:ComfyUI 0.3.76 の nodes.py / comfy_extras/nodes_images.py を直接読み、保存処理と同じ式・同じ引数をローカルで再実行して出力バイトを検査した(Python 3.12.3 / numpy 2.4.4 / Pillow 12.2.0)。チャンク一覧と数値は実測値。測っていない範囲=モデルを読み込む実際の生成、GPU 経路、他バージョンでの再現、ビューア側の表示差。
結論(ComfyUI 0.3.76 実測)
SaveImageの PNG に ICC プロファイルも sRGB チャンクも入らない。入るのはtEXt(prompt / workflow)だけ- 出力は 8bit トゥルーカラー固定。16bit で出す経路はコアに無い
- float→8bit の変換は四捨五入ではなく切り捨て(0.5 → 127)。ただしこの偏りは往復しても増えない=実測で 0/256 階調
- 範囲外の値はクリップされる(縮小されない)。1.2 も 5.0 も等しく 255
- コアの静止画保存ノードは
SaveImage(PNG)だけ。JPEG / 静止 WebP は無い
なぜ「色が違う」と感じるのか?
結論から言うと、ComfyUI が出す PNG は無タグだからだ。実際に SaveImage と同じ呼び出しで PNG を書き、チャンクを並べるとこうなる。
| チャンク | 役割 | ComfyUI の出力 |
|---|---|---|
IHDR |
幅・高さ・ビット深度 | あり(8bit / カラータイプ2) |
tEXt |
テキスト情報 | あり(prompt / workflow) |
iCCP |
ICC プロファイル埋め込み | 無し |
sRGB |
sRGB であるという宣言 | 無し |
gAMA / cHRM |
ガンマ・色度点 | 無し |
色の手がかりが無いので、開いた側が勝手に決める。ブラウザと多くのビューアは「タグが無ければ sRGB」と解釈するので結果的に一致することが多い。ずれるのは作業スペースが sRGB でないアプリで開いたときだ。Adobe RGB で作業していれば、同じ数値が別の色として表示される。画像が変わったのではなく解釈が変わっている。
対処は「変換」ではなく「宣言」=受け取り側で sRGB を指定する(Photoshop の「プロファイルの指定」)。「プロファイル変換」を選ぶと無タグの画素値を別空間へ作り替えてしまうので、かえってずれる。⚠ 実測したのはファイルに何が書かれているかまでで、表示差そのものは測っていない。これで直るのは原因が無タグ解釈だった場合に限る。
0.5 が 127 になるのはなぜ?
ComfyUI の画像テンソルは 0.0〜1.0 の float だ。保存時にこう変換される。
i = 255. * image.cpu().numpy()
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
astype(np.uint8) はゼロ方向へ切り捨てる。四捨五入しない。同じ式を実行するとこうなる。
| テンソル値 | 255倍 | 実際の出力 | 四捨五入なら |
|---|---|---|---|
| 0.500000 | 127.500 | 127 | 128 |
| 0.998000 | 254.490 | 254 | 254 |
| 1.000000 | 255.000 | 255 | 255 |
差は最大でも1階調(1/255)にとどまる(知覚できるかは測っていない。階調によっては見える場合がある)。そしてこの偏りは繰り返しても増えない。保存と読み直しの往復を全256階調で6世代まわすと変化した階調は0/256だった。8bit に落ちた時点で 255×(v÷255) が v に戻るので、そこから先は動かない。
つまり切り捨てが効くのは最初の1回だけだ。ただし劣化しないわけではない。往復のたびに 8bit へ量子化されるので、その間の加工が生んだ 1/255 未満の差は毎回そこで捨てられる。積もるのは誤差ではなく捨てた情報のほう。だから世代を重ねるワークフローは中間をテンソルのまま繋ぐほうが素直になる。
VRAM が足りず解像度やバッチを落としている場合、色より先にそこが効く。GPU 環境をすぐ試すなら、ComfyUI が入ったクラウドが手早い。
範囲外の値はどうなる?
np.clip(i, 0, 255) なのでクリップされる。1.2 も 5.0 も等しく 255 になり、-0.3 は 0 になる。全体を縮めて収める処理は入っていない。
白飛びに直結する。ハイライトが 1.0 を超えていれば超過分は保存の瞬間に捨てられ、後から戻せない。CFG を下げるか、書き出し前に明度・コントラストを調整するノードを挟むか——いずれにせよ保存前に抑えるしかない。
コアで選べる書き出しは何がある?
0.3.76 のコアにある保存系ノードは5つで、静止画は SaveImage の PNG だけだ。
| ノード | 形式 | 備考 |
|---|---|---|
SaveImage |
PNG(8bit) | compress_level=4 固定。UI から変更不可 |
PreviewImage |
PNG | temp ディレクトリ行き・compress_level=1 |
SaveAnimatedPNG |
APNG | メタデータは独自 comf チャンク |
SaveAnimatedWEBP |
アニメ WebP | lossless の既定は True |
SaveSVGNode |
SVG | ベクター出力用 |
PreviewImage を成果物にしない。 継承元は SaveImage だが、保存先は出力フォルダではなく temp ディレクトリで、ファイル名にランダムな接尾辞が付く。プレビューだけ繋いで「保存できている」と思い込むと、消えたときに取り返せない。
JPEG・16bit・ICC 埋め込みはすべてカスタムノードの領分だ(カスタムノードの選び方と導入手順)。
実務ではどう運用する?
- 受け取り側で sRGB を「指定」する。変換ではなく指定。無タグ解釈が原因のずれはこれで解消する
- 中間結果を PNG で往復させない。誤差は積もらないが、往復のたびに 8bit へ量子化され1/255 未満の差が毎回捨てられる
- 白飛びは保存前に潰す。クリップされた後では情報が無い
- 納品物は
SaveImageから出す。PreviewImageは temp 行き - 配布時は
tEXtを意識する。プロンプトとワークフローがそのまま埋まっているので、見られたくないなら--disable-metadataで起動する
なお 3DCG 側のカラースペース(ACES 等)は別の話だ。テクスチャの色管理はMaya × Substance Painter のカラースペース設定を参照してほしい。ComfyUI 側はそもそもタグを持たないので論点が違う。
ワークフローの整理は管理・整理術、基本操作はComfyUI の使い方完全入門を参照。
書き出しの前段でバッチ生成まで自動化したい場合は、GPU を専有できる環境のほうが結局は速い。
よくある質問
ファイルにはそう書かれていません。0.3.76 の SaveImage は iCCP も sRGB チャンクも書かないため、開く側の解釈に委ねられます。多くのビューアは無タグを sRGB とみなすため実運用では sRGB として扱われますが、これは各アプリの既定の挙動であって、ファイルが宣言しているわけではありません。
「プロファイルの指定」で sRGB を割り当ててください。「プロファイル変換」を選ぶと、無タグの画素値を別の色空間へ作り替えてしまい、かえってずれます。元画像は変わっていないので、解釈を合わせるだけで済みます。
0.3.76 のコアではできません。SaveImage は astype(np.uint8) で 8bit に固定しており、ビット深度の入力もありません。16bit や EXR が必要ならカスタムノードを導入することになります。
消せます。SaveImage は tEXt チャンクに prompt と workflow を JSON で書き込みます。ComfyUI を --disable-metadata を付けて起動すると、この書き込み自体が行われません。既存ファイルから消す場合は、画像編集ソフトで再保存するか、メタデータ削除ツールを使ってください。
SaveImage では変えられません。compress_level が 4 でコードに直書きされており、ノードの入力として公開されていないためです。ちなみに SaveAnimatedPNG は 0〜9 で指定でき、既定は同じく 4 です。
まとめ
ComfyUI の「色が違う」は、色を変換されているのではなく、色が宣言されていないことから来る。0.3.76 の SaveImage が書くのは 8bit の画素と tEXt だけで、ICC も sRGB チャンクも付かない。受け取り側で sRGB を指定すれば、この原因のずれは解消する。
float から 8bit への変換は切り捨てで、範囲外はクリップされる。ただし切り捨ての偏りは往復しても増えない(実測 0/256 階調)。失われるのは誤差ではなく 8bit 未満の情報のほうだ。中間はテンソルのまま繋ぎ、白飛びは保存前に抑える。

コメント