GPT Imageの部分編集でマスクを検証し、変更されないはずの画素を調べる

GPT Imageでマスク付き編集を実行し、対象領域外の画素を検証します。実際の出力、アルファマスクの確認方法、画素比較、再現可能な入力を紹介します。

淡いカードに黒インクで描かれたステンシルシート。セージグリーンの背景にコーラル色の点があり、「GPT Image Masks」というタイトルが記されている。

マスクを使うと、画像編集リクエストで変更したい場所を指定できます。それでも、返された画像の残りの部分を確認する必要があります。GPT Image 2.5 Flareのテストでは、マスクを使って背景を編集したところ、対象の帯以外は元画像に近く見えましたが、画素を比較するとその範囲にも差分が見つかりました。出力は、元画像に対するロスレスな部分編集ではありませんでした。

この記事では、元画像、マスクの指定方法、実際に返された画像、簡単な画素チェックを紹介します。商品画像の一部を変更しつつ、商品の変化を見落としたまま受け入れたくない開発者やクリエイター向けです。すべてのマスク付き編集が同じように動作するとも、ひとつの例でモデル全体の品質を測れるとも主張しません。

編集機能が使えない旨の表示がある、Ofoxの英語版画像プレイグラウンド

10月12日に撮影した公開埋め込みプレイグラウンドには、このインターフェースでは画像編集を利用できないという表示がありました。この記事の編集結果は、下記のAPIルートから取得したもので、このプレイグラウンドから生成したものではありません。この画面のキャプチャで、マスクや参照画像のアップロード欄を探さないでください。

リクエストの前に編集範囲を決める

9月13日にOfoxで生成した参照画像を再利用しました。ラベルのない琥珀色のボトルに、アイボリー色のキャップが付いた画像です。これは合成した商品画像であり、顧客の商品を撮影した写真ではありません。元画像は1024 × 1024のPNGです。ラベルの文字がないため、このテストではロゴ、成分表示、法規制上の表示を保持できるかどうかは分かりません。

編集入力に使用した、合成画像の琥珀色のボトル

編集対象は意図的に単純にし、最も左側の256列、つまりx = 0からx = 255までとしました。ボトルはその帯の外にあります。ボトル、キャップ、位置、その他の領域はそのままにし、帯の内側だけを落ち着いたセージグリーンの背景にするよう依頼します。

マスクには、元画像と同じ寸法のRGBA PNGを使用します。編集可能な帯のアルファ値は0で、それ以外の領域は完全に不透明です。見た目が白黒のグレースケール画像は、このアルファマスクと同じものではありません。OpenAIの画像編集リファレンスでは、透明なマスク領域と、マスクと入力画像の関係が説明されています。エンドポイントの現在のファイル要件も必ず確認してください。

マスクをローカルで検証する

アップロード前に、ファイルモード、寸法、アルファ値の分布を確認します。このコードは今回のテスト用ファイルを検証するものです。実際に編集したい範囲に合わせて領域を調整してください。

from PIL import Image

source = Image.open("source.png")
mask = Image.open("mask-left.png")
assert source.size == mask.size == (1024, 1024)
assert mask.mode == "RGBA"
alpha = mask.getchannel("A")
assert alpha.crop((0, 0, 256, 1024)).getextrema() == (0, 0)
assert alpha.crop((256, 0, 1024, 1024)).getextrema() == (255, 255)

これらのアサーションにより、課金対象のリクエストを送る前に、アルファチャンネルの反転や平坦化を検出できます。一般的な画像編集ソフトでは、プレビューの色だけに頼らず、実際の透明度を確認してください。出力画像で上書きされないように、元画像とマスクは別々に保存します。

参照画像を複数指定する場合は、エンドポイントがどの画像にマスクを適用するか確認してください。対象の元画像を先頭に置き、マスクがすべての参照画像に対する全体指示として適用されると決めつけないでください。今回のマスクテストでは、画像を1枚だけ指定しました。

実際の編集を送信する

成功したテストでは、Ofoxの/v1/images/editsエンドポイントでopenai/gpt-image-2.5-flareを使用しました。size=1024x1024とn=1を指定し、品質設定は明示しませんでした。レスポンスにはquality: lowが記録されていました。これはレスポンスに含まれていた値であり、すべてのリクエストで同じデフォルトになるという保証ではありません。

curl --silent --show-error --fail-with-body \
  --connect-timeout 15 --max-time 120 \
  'https://api.ofox.ai/v1/images/edits' \
  -H "Authorization: Bearer $OFOX_API_KEY" \
  -F 'model=openai/gpt-image-2.5-flare' \
  -F 'image=@source.png;type=image/png' \
  -F 'mask=@mask-left.png;type=image/png' \
  -F 'size=1024x1024' -F 'n=1' \
  -F 'prompt=Change only the background strip at the far left quarter to muted sage green. Keep the amber bottle, cap, its position, and every other area unchanged.' \
  -o edit-response.json

実行前にOFOX_API_KEYを安全に設定してください。このコマンドはAPI利用料が発生する可能性があります。デコードする前に保存したレスポンスを確認してください。エラーレスポンスにはデコード対象の画像がありません。今回成功したレスポンスにはdata[0].b64_jsonが含まれていました。このフィールドをbase64としてデコードし、バイト列を保存して、画像が想定した寸法で開けることを確認してください。

ダウンロード可能なキットには、元画像、マスク、返されたファイル、この操作を実行する範囲を限定したスクリプトが含まれています。GPT Image APIガイドでは、初回アクセスと画像レスポンスの保存方法を説明しています。

実際に返された2つの出力を比較する

同じ編集指示をマスクなしで1回送信し、成功した出力を取得しました。マスク付きの最初の試行はクライアント側でタイムアウトし、その後に送信したマスク付きリクエストは成功しました。クライアントのタイムアウトは、生成や課金が発生しなかった証拠にはなりません。そのため、後で課金を照合する際に、この試行を除外しないでください。

実際のマスク付き編集結果:ボトル画像の左側にある平坦なセージ色の帯

成功したマスク付きの結果では、左側に均一なセージ色の帯がはっきり見えます。ボトル全体の見た目と位置は元画像に近いようですが、見た目だけでは保護領域が変更されていない証明にはなりません。

実際のマスクなし編集結果:左側に質感のあるセージ色の帯

マスクを使わない場合も、モデルは左側にセージ色の帯を配置しました。このサンプルでは、より質感のある仕上がりです。どちらのリクエストも、指示に対して妥当な解釈を返しました。しかし、完全な保持が必要なワークフローで、2つの出力を置き換えて使えるとは限りません。

デコードしたRGB値を、x = 256から1023までの範囲で、1024行すべてについて比較しました。この範囲には786,432画素があります。3つのチャンネルのうち1つでも値が異なれば、差が1段階だけでも、その画素を変更ありと数えます。

成功した出力対象の帯の外で変更された画素チャンネルごとの平均絶対差(0~255)
マスクあり786,432画素中784,316画素3.493
マスクなし786,432画素中785,921画素5.464

この比較では、マスク付きの結果のほうが平均差分は小さくなりました。ただし、サンプルが少なすぎるため、この程度の優位性が一般に成り立つとは言えません。さらに重要なのは、どちらの結果も画素を完全に保持する要件を満たさなかったことです。変更画素数が多くても、差がごく小さい画素が含まれている場合があります。変更された画素がすべて目に見えて損なわれたという意味ではありません。

保護領域のチェックを再現する

スクリーンショット、JPEGプレビュー、縮小サムネイルではなく、ダウンロードした元ファイルを使ってください。そうした変換によって、モデル編集とは無関係な差分が生じます。

from PIL import Image, ImageChops, ImageStat

a = Image.open("source.png").convert("RGB")
b = Image.open("edited.png").convert("RGB")
if a.size != b.size:
    raise ValueError("Output dimensions changed; inspect before comparing")
region = (256, 0, 1024, 1024)
diff = ImageChops.difference(a.crop(region), b.crop(region))
changed = sum(any(pixel) for pixel in diff.getdata())
mean_difference = sum(ImageStat.Stat(diff).mean) / 3
print(changed, mean_difference)

この方法で測定できるのは画素の差分であり、ブランド表現の正確さ、知覚上の品質、商品の形状ではありません。キャップ、輪郭、素材、エッジ、文字、影はそれぞれ個別に確認してください。商品位置が変わると、画素の直接比較では見た目の類似性を適切に表せなくなります。一方で、固定位置の画素保持要件に違反していることは、引き続き正しく示せます。

完全な保持が必要なら、一般的な合成処理を使って、承認済みの編集領域だけを元画像に重ねる方法があります。境目や照明を確認してください。境界がはっきりしすぎると許容できない場合があります。これは別途行う後処理であり、モデル自体がそれらの画素を保護したという意味ではありません。公開するモデル比較で変更を隠すために、この処理を使わないでください。

再実行する前に失敗原因を調べる

意図と逆の側が変わった場合は、まずアルファ値と座標を確認してください。画像全体が変わった場合は、マスクがリクエストに実際に添付されたか、そのルートがそのパラメーターを受け付けるか、画像の順序がマスクの適用対象と一致しているかを確認します。出力と一緒にリクエストのメタデータも保存してください。

サーバーがファイルを拒否した場合は、ファイル形式、バイトサイズ、寸法、現在のエンドポイント制限を確認してください。JPEGの拡張子を.pngに変えても、形式は変換されません。リクエストがタイムアウトした場合は、再試行する前にリクエスト記録を確認してください。再試行も、課金対象となる可能性のある別の操作として扱います。

サムネイルで見たときに結果が良好でも、受け入れる前に元画像を実寸で確認してください。ラベル付き商品では、文字とロゴのチェックも追加してください。ラベルのない今回のボトル画像では、それらの特性を保証できません。シーン全体を変えながら商品を維持する方法については、参照画像1枚と2枚の比較テストを参照してください。

よくある質問

マスクを使えば、ほかの画素がすべて同一に保たれますか?
今回のマスク付きサンプルでは、対象領域外のRGB値は完全には保持されませんでした。完全な保持が納品要件なら、明示的に画素を検証してください。
マスク付きの出力は常に優れていますか?
この2例だけでは判断できません。この比較では保護領域の平均差分が小さかったものの、どちらの出力も用途に応じた目視確認が必要です。
白黒のJPEGをアルファマスクとして使えますか?
今回使用した透過PNGマスクの代わりにはなりません。アルファチャンネルを確認し、選択したエンドポイントの入力要件に従ってください。