43号線を西へ東へ

フリーランスの備忘録、アウトプットの実験場

背景を計算して透過するPythonスクリプト

このブログはプロモーションリンクを含むときがあります


AIで画像作成をするとき、背景の透過を忘れることが多いです。背景付きの画像を消してその物体だけを際出させたい、そんなときにPythonスクリプトでちゃちゃっと消すコードを作成しました。

Pythonでは、様々な機能を持ったライブラリを使うことで、自分の作業に最適化したアプリ的なものを作れます。

複雑な画像から人物を抜き出して背景を透過する手順ならrembgというライブラリがおすすめらしいですが、均等な背景に人やものが描かれた画像を扱っている私の使用環境では、残念ながらきれいに透過されませんでした。

今回はrembgを使わず、Numpyで画像の中にある色を集計して、一番多い色を背景と見なして、Pillowで透過する方法でバイブコーディングしました。自分用の振り返りのために書き残します。

結論としては、最初から透過背景で作成するようにAIに指示することで解決するので、「透過背景で書き出して」の一文を忘れないように気をつけようという話しです。

資料作成時にイラストの背景を消したいときがあります。白背景であっても余白が大きくなりすぎて、切り取り作業が必要なので最初から透過させることが多いです。

透過した画像としない画像のテキスト回り込みの違い

画像はAIに作らせることがほとんどなので、そもそもAIプロンプトに「透過背景のpngで」と一言加えるだけでいいのですが、ついつい忘れてしまいます。

もう一度生成するより、背景を透過するアプリはいろいろあります。Canvaは上手にやってくれますが、アプリの立ち上げや画像のアップロードが意外と手間取るので、Pythonコードをバイブコーディングすることにしました。

使用したライブラリ

Pythonはライブラリを組み合わせていろいろと機能を使いするプログラム言語です。達人になるとライブラリ名を見るとプログラムの大枠が分かるのだとか。

今回使ったのは3つのライブラリ。いずれも基本的なライブラリで動作するプログラムです。

  • Pillow:画像
  • NumPy:計算
  • os:ファイル操作

以下に使用したライブラリ3つをまとめておきます。

Pillow (from PIL import Image) :画像ファイルのエキスパート

Pillow(PIL)は、Pythonで画像を扱うための最も基本的なライブラリ。プログラム内では以下のような、画像ファイルそのものに対する操作をすべて担当しています。

  • 画像の読み込み: Image.open('input.jpg') で画像ファイルを開く。
  • 形式変換: .convert('RGBA') で透明情報(A: Alpha)を持てる形式に変えたり、.convert('P') で色数を減らしたりする。
  • データ操作: getcolors() で画像内の色を数えたり、getpalette() で色の情報を取得する。
  • 画像の保存: output_image.save('output.png') で処理後の画像をファイルとして書き出す。

簡単に言うと、画像を開いたり、保存したり、基本的な性質を変えたりするのがPillowの仕事です。

NumPy (import numpy as np) :超高速な数値計算のプロ

NumPyは、大量の数値データを高速に計算するためのライブラリです。画像は「ピクセルの色の集まり」という巨大な数値のグリッド(配列)なので、NumPyの得意分野です。

  • データ変換: np.array(img) でPillowが読み込んだ画像を、計算しやすい数値の配列に変換する。
  • 高速な計算: 背景色との差を np.abs(rgb - TARGET_COLOR).sum() のように、何十万個もある全ピクセルに対して一瞬で計算する。
  • 条件に合うピクセルの選択: diff < TOLERANCE のように、条件に合うピクセル(背景色に近いピクセル)だけを効率的に選び出す(マスキング)。
  • データの一括変更: data[mask, 3] = 0 のように、選び出したピクセルの透明情報だけをまとめて 0 (透明) に書き換える。

もしNumPyを使わずに一つ一つのピクセルをループで処理すると非常に時間がかかりますが、NumPyのおかげで複雑な計算を瞬時に実行できています。

os (import os) :ファイルとフォルダの管理人

osライブラリは、PythonプログラムがOS(Operating System)と対話するための機能を提供します。主にファイルやフォルダ(ディレクトリ)の操作に使われます。

  • 存在確認: os.path.exists(path) で、保存しようとしているファイル名のファイルがすでに存在するかをチェックする。
  • パスの操作: os.path.splitext(path) で、ファイルパスを「ファイル名 (output)」と「拡張子 (.png)」に分割する。

今回のプログラムでは、出力ファイルを連番にするためのファイル名のチェックと組み立てという、地味ながら重要な役割を担っています。

ワークフロー

一番多い色を背景色と見なして透過するコードのワークフローをまとめます。

プログラムのワークフロー

プログラム全体のワークフローは以下の通りです。

使用するライブラリごとに色分けしています。

  • Pillow(画像操作):オレンジ
  • NumPy(高速計算):青
  • os(ファイル操作):みどり
graph TD
    %% === ノード定義 ===
    Start([処理開始]) --> Input[1. 画像ファイルを読み込む];
    Input --> CheckFile{入力ファイルは存在するか?};
    CheckFile -- No --> Error[エラーを表示し終了];
    
    CheckFile -- Yes --> DetectColor[2. 最も多い色を背景色として自動特定];
    DetectColor --> ApplyTransparency[3. 特定した背景色を透明にする];
    ApplyTransparency --> CheckOutput{出力ファイルは既に存在するか?};
    
    CheckOutput -- No --> SaveImage[4. PNG画像として保存];
    CheckOutput -- Yes --> AddNumber[ファイル名に連番を付与];
    
    AddNumber --> SaveImage;
    SaveImage --> End([処理終了]);
    Error --> End;

    %% === スタイル定義 ===
    classDef pillowStyle fill:#FFDAB9,stroke:#E68A00,stroke-width:2px,color:#333
    classDef numpyStyle fill:#D6EAF8,stroke:#3498DB,stroke-width:2px,color:#333
    classDef osStyle fill:#D5F5E3,stroke:#2ECC71,stroke-width:2px,color:#333
    classDef flowStyle fill:#f2f2f2,stroke:#555,stroke-width:2px,color:#333

    %% === スタイル適用 (修正済み) ===
    class Input,DetectColor,SaveImage pillowStyle
    class ApplyTransparency numpyStyle
    class CheckFile,CheckOutput,AddNumber osStyle
    class Start,End,Error flowStyle

一番多い色を計算する関数の理屈とフローチャート

画像データは、色の三要素RBGの集合です。

RGBは、光の三原色である赤 (Red)、緑 (Green)、青 (Blue)を混ぜ合わせて色を表現する加法混色モデルで、それぞれの色の強さは0から255までの256段階で表されます。

数値が255に近いほどその色が強く、すべての色が最大値 (255) になると白になります。

数値が0に近いほどその色が弱く、すべての色が最小値 (0) になると黒になります。例を下に挙げます。

  • 白: R:255, G:255, B:255
  • 赤: R:255, G:0, B:0
  • 青: R:0, G:0, B:255

透過前の画像

今回サンプルで使った画像の多い順にピクセル数を並べてみました。

順位 RGB値 ピクセル 主な使用箇所
1 rgb(245, 242, 235) 967,314 背景
2 rgb(188, 188, 188) 66,280 車体(基本色)
3 rgb(48, 48, 50) 49,603 タイヤ、窓枠、影
4 rgb(205, 205, 205) 44,792 車体(明るい部分)
5 rgb(169, 169, 169) 42,971 車体(テクスチャ)
6 rgb(88, 89, 91) 24,015 窓ガラス
7 rgb(74, 75, 77) 14,024 ホイール(暗い部分)
8 rgb(175, 177, 178) 11,260 ホイール(明るい部分)
9 rgb(61, 62, 63) 9,804 車体下部、影
10 rgb(33, 33, 35) 8,824 アウトライン(輪郭線)

サンプル画像は、私の乗っているカローラツーリングのイラストです。その中でも一番多かったrgb(245, 242, 235)は、背景部分でオフホワイトまたはエッグシェルと呼ばれるそうです。

そのオフホワイトが96万点、車体の色が6万6千点、と桁違いなので、一番多い色を背景とする法則が当てはまるので、背景を透過することが出来ました。

graph TD
    Start(["関数呼び出し: get_most_frequent_color"]) --> Input[/"Input: img (画像オブジェクト)"/];
    Input --> Quantize["1. 画像を256色に減色 (量子化)"];
    Quantize --> Count["2. 各色のピクセル数をカウント"];
    Count --> Check{"色の取得に成功したか?"};
    
    Check -- "No / 失敗" --> ReturnNone["None を返して終了"];
    Check -- "Yes / 成功" --> Process["後続の処理へ..."];
    
    ReturnNone --> End(["関数終了"]);
    Process --> End;

実際のコード

プログラムファイルのあるフォルダにinput.pngを入れた状態でプログラムを実行すると、背景が透過されたoutput.pngが出来ます。

▶コード(クリックで展開)

from PIL import Image
import numpy as np
import os

def get_most_frequent_color(img):
    """
    画像内で最も頻繁に使用されている色(RGB)を取得する関数
    """
    # 色数を256色に減色(量子化)して、似た色をまとめる
    # これにより、JPEGのノイズ等によるわずかな色の違いを無視できる
    quantized_img = img.convert('P', palette=Image.ADAPTIVE, colors=256)
    
    # getcolors()で各色が何ピクセルあるかを取得
    # 結果は [(ピクセル数1, パレットインデックス1), (ピクセル数2, パレットインデックス2), ...]
    colors = quantized_img.getcolors()
    
    if colors is None:
        return None # 色の取得に失敗した場合

    # ピクセル数で降順(多い順)にソート
    sorted_colors = sorted(colors, key=lambda x: x[0], reverse=True)
    
    # 最も多い色のパレットインデックスを取得
    most_frequent_palette_index = sorted_colors[0][1]
    
    # パレットインデックスから実際のRGB値を取得
    palette = quantized_img.getpalette()
    r = palette[most_frequent_palette_index * 3]
    g = palette[most_frequent_palette_index * 3 + 1]
    b = palette[most_frequent_palette_index * 3 + 2]
    
    return np.array([r, g, b])

# --- 設定項目 ---
# 入力画像のパス
input_path = 'input.png'

# 出力画像のベースとなるパス
output_path = 'output_auto_detect.png'

# 色の許容範囲(TOLERANCE)
# 背景色の自動検知がうまくいっても、この値の調整は重要です
TOLERANCE = 40
# ------------------------------------------------------------------

# --- 出力ファイル名の連番処理 (変更なし) ---
final_output_path = output_path
if os.path.exists(final_output_path):
    base_name, extension = os.path.splitext(output_path)
    if extension.lower() != '.png': extension = '.png'
    counter = 1
    while os.path.exists(final_output_path):
        final_output_path = f"{base_name}-{counter}{extension}"
        counter += 1
# ----------------------------------

if not os.path.exists(input_path):
    print(f"エラー: 入力ファイル '{input_path}' が見つかりません。")
else:
    try:
        img_for_analysis = Image.open(input_path).convert('RGB')
        
        # ▼▼▼【ここが新しい部分!】画像から最も多い色を自動で取得 ▼▼▼
        TARGET_COLOR = get_most_frequent_color(img_for_analysis)
        
        if TARGET_COLOR is None:
            raise ValueError("画像の主要な色の検出に失敗しました。")

        print(f"自動検出された背景色 (RGB): {TARGET_COLOR}")
        
        # 実際の処理はRGBAモードで行う
        img_for_processing = img_for_analysis.convert('RGBA')
        data = np.array(img_for_processing)
        
        rgb = data[..., :3]
        diff = np.abs(rgb - TARGET_COLOR).sum(axis=2)
        mask = diff < TOLERANCE
        data[mask, 3] = 0 # 透明化
        
        output_image = Image.fromarray(data)
        output_image.save(final_output_path)

        print(f"指定色の透過処理が完了しました! -> {final_output_path}")

    except Exception as e:
        print(f"エラーが発生しました: {e}")

まとめ

アプリを立ち上げずに簡単に画像の背景を透過するPythonスクリプトを作成しました。イラストのように単一の背景用の設定なので、風景が背景になっているようなものだと全く上手く動きません。

そもそも、AIにイラストをお願いするときに、透過背景の画像を生成するように伝えれば要らないPythonスクリプトですが、ピクセルごとにRGB値が振られていてそれを計算することができる学ぶことが出来、またPythonの勉強になりました。